shape-03-stadium-track

Rail stadium à enfant emboîté

Poser une piste à bouts ronds qui porte un objet EMBOÎTÉ — barre de progression, interrupteur, jauge — sans que le liseré entre les deux s'épaississe dans les courbes. Une seule loi le tient : l'enfant est inset d'une constante sur ses quatre côtés et son rayon vaut celui du rail moins cet inset, ce qui fait tomber les deux centres de courbure au même point. Deux remplissages en découlent, une barre partielle et une pastille, et ce ne sont pas deux objets : c'est le même rail avec deux longueurs d'enfant.

shapeslideweb483×483px13 benchmarks

Télécharger le .zip

Le fragment RÉEL, dans son propre document, sur le sol de sa charte — pas une image. Ce que vous voyez est exactement ce que vous collerez.

Employer quand

Partout où une piste doit porter une position ou un état : barre de progression, interrupteur, jauge de segment, pastille de statut dans un tableau de bord. Aussi comme primitive de composition dans un vocabulaire de formes pleines — c'est la forme la plus fréquente d'une planche géométrique, parce qu'elle est la seule à porter une orientation LONGUE sans texte.

Éviter quand

Sur une boîte proche du carré : à moins de 1,6 d'aspect les deux bouts ronds se rejoignent, le rail devient une gélule courte puis un disque, et l'enfant emboîté n'a plus de course à parcourir — c'est un anneau, pas une piste. Éviter aussi dès que l'enfant doit toucher un bord : l'inset est constant sur les quatre côtés par construction, et le rendre nul d'un côté détruit la concentricité, donc l'objet même du pattern. Et en mailing : Outlook rend `border-radius` et les dégradés de façon imprévisible — voir `node bin/emit.mjs --audit --target email`.

Notes

Ce pattern verse une IDENTITÉ que la source de `ref-01` manquait. Son relevé listait deux primitives — un `stadium-h` à barre en dégradé et un `toggle` à pastille — et ce sont le même objet : un rail stadium portant un enfant en inset constant, une fois rempli d'un dégradé partiel, une fois d'un enfant aussi large que la hauteur utile. Les lister à part, c'est deux entrées de vocabulaire pour une seule loi, et c'est exactement ce que l'élagage du 30/07 interdit. C'est aussi pourquoi le `fill-gradient-stadium` annoncé par la spec n'est pas versé seul : le dégradé n'a pas d'existence propre ici, il EST le remplissage de l'enfant. **Le rayon s'écrit en LONGUEUR.** `border-radius: 50%/50%` était au relevé, et ce n'est pas un stadium : les deux pourcentages se résolvent sur des axes différents, donc sur 483 × 232 ils donnent 241,5 de rayon horizontal contre 116 de vertical — un ovale complet. Le stadium s'écrit `calc(hauteur / 2)`, exactement comme le chanfrein de `shape-notched-corner` s'écrit en longueur pour rester à 45° et comme le rayon de `shape-02-teardrop-quadrant` pour rester circulaire. Trois formes, un seul piège. La mesure ne se contente pas de lire la valeur déclarée — ce serait faible, Chrome rendant le `%` tel quel : elle DÉRIVE le rayon attendu de la hauteur et sonde la forme peinte au point sur six angles du bout rond. Un rayon en pourcentage y échoue dès 135°. **La concentricité est la loi, et le reste en tombe.** L'enfant est inset d'une constante sur ses quatre côtés — posée par le `padding` du rail, jamais côté par côté — et son rayon vaut celui du parent MOINS cet inset. Les deux centres de courbure tombent alors au même point, ce que le benchmark vérifie directement plutôt que de le déduire : l'écart des deux bords est le même dans la courbe que sur la droite. Écrire le rayon de l'enfant à part donne un liseré qui s'épaissit dans les coins, défaut qu'aucun alignement ne signale. Conséquence, et c'est le test de la loi : **la pastille sort ronde sans qu'on l'écrive.** 200,44 de côté pour un rayon de 100,22 — personne n'a déclaré un cercle, il est ce que la soustraction produit quand l'enfant fait la hauteur utile du rail. Une primitive dont l'usage principal tombe d'une loi et non d'une exception, c'est le signe qu'on tient la bonne loi. La longueur de la barre est le seul paramètre de contenu. Elle est à 0,4477 du rail parce que, dans `ref-01`, son bord droit tombe sur une ligne de colonne de la planche — le relevé était muet, et quand un relevé est muet sur une longueur c'est le quadrillage qui tranche (DOCTRINE §3), pas le goût. Hors de cette planche, c'est la valeur de progression qui la pilote. Enfin, `background-color` doit déclarer l'extrémité claire du dégradé : un enfant dont le fond n'est qu'un `background-image` a une couleur de fond transparente, toute mesure la compose avec celle du rail, et `check-deck.mjs` déclare la couche 1:1 redondante — piège payé sur le deck de cette même référence. La planche complète est `layout-15-primitive-mosaic`, qui porte la grille et pas les formes.

Mots-clés

formeprimitivestadiumpilulerailtoggleprogressionjaugerayonconcentrique

Benchmarks — la géométrie, en assertions vérifiables

Rejouables par node bin/check.mjs shape-03-stadium-track : le pattern se prouve, il ne se croit pas.

assertionmesureattendu
STADIUM — le rayon vaut exactement la moitié du PETIT côté, et il est écrit en LONGUEUR : un `50%` rendrait 50 % de CHAQUE axe, soit 241,5 de rayon horizontal contre 116 de vertical sur une boîte 2:1, c'est-à-dire une ellipsenum('.vl-track--knob', 'borderTopLeftRadius') / box('.vl-track--knob').h0.5 ± 0.003
…et le bout rond est un ARC DE CERCLE de rayon h/2, sondé au point sur six angles — la mesure ne lit pas la valeur déclarée, elle interroge la forme PEINTE : un rayon en pourcentage y échoue dès 135°, là où lire la valeur calculée ne verrait qu'un « 50 % » d'apparence saine(() => { const rail = document.querySelector('.vl-track--bar'); const b = rail.getBoundingClientRect(); const R = b.height / 2; const cx = b.left + R, cy = b.top + b.height / 2; const at = (k, t) => { const e = document.elementFromPoint(cx + Math.cos(t) * R * k, cy - Math.sin(t) * R * k); return !!e && (e === rail || rail.contains(e)); }; let ok = 1; for (const a of [110, 135, 160, 200, 225, 250]) { const t = a * Math.PI / 180; if (!at(0.90, t) || at(1.10, t)) ok = 0; } return ok; })()1 ± 0
INSET CONSTANT sur les quatre côtés : c'est le padding du rail qui pose l'enfant, jamais une marge choisie côté par côté(() => { const P = ['paddingTop','paddingRight','paddingBottom','paddingLeft'].map((p) => num('.vl-track--knob', p)); return (Math.max(...P) - Math.min(...P)) / Math.max(...P); })()≤ 0.003
…et il vaut 0,068 de la hauteur du rail — assez pour que le liseré se lise, assez peu pour que l'enfant reste l'objet et le rail le contenantnum('.vl-track--knob', 'paddingTop') / box('.vl-track--knob').h0.068 ± 0.004
CONTOURS CONCENTRIQUES — le rayon de l'enfant est celui du rail MOINS l'inset, sur les deux remplissages. Écrit à part, le liseré s'épaissirait dans les coins sans qu'aucun alignement ne bouge(() => { let worst = 0; for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { worst = Math.max(worst, Math.abs((num(r, 'borderTopLeftRadius') - num(f, 'borderTopLeftRadius')) / num(r, 'paddingTop') - 1)); } return worst; })()≤ 0.01
…ce qui se vérifie à l'endroit où ça compte : les deux CENTRES DE COURBURE tombent au même point, donc l'écart des deux bords est le même dans la courbe que sur la droite(() => { let worst = 0; for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { const cr = box(r).left + num(r, 'borderTopLeftRadius'), cf = box(f).left + num(f, 'borderTopLeftRadius'); const vr = box(r).top + box(r).h / 2, vf = box(f).top + box(f).h / 2; worst = Math.max(worst, Math.abs(cr - cf) / box(r).h, Math.abs(vr - vf) / box(r).h); } return worst; })()≤ 0.002
la pastille est CARRÉE de boîte — elle fait exactement la hauteur utile du rail, inset compris des deux côtésbox('.vl-track__fill--knob').w / box('.vl-track__fill--knob').h1 ± 0.005
…et elle sort RONDE sans qu'on l'écrive : son rayon vaut la moitié de son côté, ce qui n'est pas une déclaration mais la conséquence de la soustraction ci-dessus. Une primitive dont l'usage principal tombe d'une loi, c'est le signe que la loi est la bonnenum('.vl-track__fill--knob', 'borderTopLeftRadius') / box('.vl-track__fill--knob').w0.5 ± 0.005
la BARRE ne remplit pas son rail (44,8 %) : c'est ce qui fait lire une piste portant une position, et non un aplat à bouts rondsbox('.vl-track__fill--gradient').w / box('.vl-track--bar').w0.4477 ± 0.01
…et elle part du bord GAUCHE du rail, à l'inset près : une barre centrée dans sa piste ne dit plus d'où elle progresse(box('.vl-track__fill--gradient').left - box('.vl-track--bar').left) / num('.vl-track--bar', 'paddingLeft')1 ± 0.02
le dégradé DÉCLARE sa couleur de repli, opaque : sans elle l'enfant a un fond transparent, toute mesure de couleur le compose avec le rail, et un contrôle de composition déclare la couche redondante — piège payé sur le deck de `ref-01`(() => { const p = (cs('.vl-track__fill--gradient', 'backgroundColor').match(/[\d.]+/g) || []).map(Number); return p.length >= 3 && (p.length === 4 ? p[3] : 1) >= 1 ? 1 : 0; })()1 ± 0
la pastille se DÉTACHE de son rail : les deux fonds sont mesurés l'un contre l'autre, parce qu'un interrupteur dont l'état ne se voit pas n'est pas un interrupteur(() => { const a = lum(bgOf($('.vl-track__fill--knob'))), b = lum(bgOf($('.vl-track--knob'))); const [hi, lo] = a > b ? [a, b] : [b, a]; return (hi + 0.05) / (lo + 0.05); })()≥ 3
REPARAMÉTRAGE — de 120 à 400 px de hauteur de rail, le rayon reste h/2 et l'écart des contours reste l'inset : la loi ne dépend d'aucune des valeurs d'ici(() => { const set = document.querySelector('.vl-track-set'); let worst = 0; for (const h of ['120px', '232px', '300px', '400px']) { set.style.setProperty('--vl-track-h', h); for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { const el = document.querySelector(r), fl = document.querySelector(f); const R = parseFloat(getComputedStyle(el).borderTopLeftRadius), rr = parseFloat(getComputedStyle(fl).borderTopLeftRadius), pad = parseFloat(getComputedStyle(el).paddingTop); worst = Math.max(worst, Math.abs(R / el.getBoundingClientRect().height - 0.5), Math.abs((R - rr) / pad - 1)); } } set.style.removeProperty('--vl-track-h'); return worst; })()≤ 0.01

Le code

fragment — patterns/shape-03-stadium-track.html Télécharger
<!-- shape-03-stadium-track — un rail en stadium portant un enfant EMBOÎTÉ, et les deux
     remplissages qui en font deux objets là où il n'y en a qu'un : une barre de progression
     et un interrupteur. La source de `ref-01` les listait comme deux primitives distinctes ;
     ce sont le même rail, la même loi d'emboîtement, et deux enfants de longueur différente.

     Trois règles portent tout, et elles se mesurent :

       • le rayon du rail s'écrit en LONGUEUR — la moitié du PETIT côté. Jamais `50%`, et
         encore moins `50%/50%` : les deux pourcentages se résolvent sur des axes différents,
         donc sur une boîte de 483 × 232 ils donnent 241,5 de rayon horizontal contre 116 de
         vertical, c'est-à-dire une ELLIPSE et pas un rectangle à bouts ronds ;
       • l'enfant est INSET d'une constante sur ses QUATRE côtés — c'est le `padding` du rail
         qui le pose, jamais une marge choisie côté par côté ;
       • et son rayon vaut celui du rail MOINS cet inset. C'est cette soustraction, et elle
         seule, qui rend les deux contours CONCENTRIQUES : l'écart entre le bord du rail et
         le bord de l'enfant est le même partout, y compris dans les courbes. Écrire le rayon
         de l'enfant à part, c'est un liseré qui s'épaissit dans les coins.

     Ce qui en découle sans qu'on l'écrive : un enfant aussi large que la hauteur utile du
     rail sort ROND. La pastille de l'interrupteur n'est pas déclarée circulaire — elle l'est
     parce que 200,44 de côté pour un rayon de 100,22, c'est un cercle. Une primitive dont
     l'usage principal tombe d'une loi et non d'une exception, c'est le signe qu'on tient la
     bonne loi.

     La longueur de la barre est le SEUL paramètre de contenu : elle ne remplit pas son rail,
     donc le rail se lit comme une piste qui porte une position. Couleurs via --vl-* uniquement. -->
<div class="vl-track-set">

  <!-- Remplissage 1 — la barre en dégradé. `background-color` déclare l'extrémité CLAIRE du
       dégradé : sans elle l'enfant a un fond transparent, toute mesure de couleur compose
       avec celle du rail, et un contrôle de composition déclare la couche redondante. -->
  <div class="vl-track vl-track--bar">
    <div class="vl-track__fill vl-track__fill--gradient"></div>
  </div>

  <!-- Remplissage 2 — la pastille. Même rail, même inset, même rayon : seule la longueur
       de l'enfant change. -->
  <div class="vl-track vl-track--knob">
    <div class="vl-track__fill vl-track__fill--knob"></div>
  </div>

</div>

<style>
/* Les paramètres du rail, et il n'y en a pas d'autres :
     --vl-track-h      la hauteur — c'est elle qui fixe le rayon (h/2) et l'inset ;
     --vl-track-w      la longueur du rail ;
     --vl-track-inset  l'écart de l'enfant, CONSTANT sur les quatre côtés ;
     --vl-track-fill   la longueur de la barre, en fraction du rail.
   Le rayon de l'enfant n'est pas un paramètre : c'est une soustraction. */
.vl-track-set {
  --vl-track-h: 232px;
  --vl-track-w: 483px;
  --vl-track-inset: calc(var(--vl-track-h) * 0.068);
  --vl-track-fill: 0.4477;
  --vl-track-gap: 19px;

  display: grid;
  gap: var(--vl-track-gap);
  width: max-content;
}

/* Le rail. Le rayon en LONGUEUR : la moitié du petit côté, jamais un pourcentage. Et c'est le
   `padding` qui pose l'inset — un enfant positionné à la main dériverait au premier
   changement de hauteur. */
.vl-track {
  width: var(--vl-track-w);
  height: var(--vl-track-h);
  background: var(--vl-ink);
  border-radius: calc(var(--vl-track-h) / 2);
  padding: var(--vl-track-inset);
  display: flex;
  align-items: center;
}

/* CONTOURS CONCENTRIQUES — rayon du parent moins l'inset. Les deux centres de courbure
   tombent alors au même point, et l'écart des deux bords est constant tout autour. */
.vl-track__fill {
  height: calc(var(--vl-track-h) - 2 * var(--vl-track-inset));
  border-radius: calc(var(--vl-track-h) / 2 - var(--vl-track-inset));
}

/* La barre ne remplit pas son rail : c'est ce qui fait lire une PISTE et une position, et non
   un aplat à bouts ronds. 0,4477 est le réglage de `ref-01`, où le bord droit de la barre
   tombe sur une ligne de colonne de la planche — quand le relevé est muet sur une longueur,
   c'est le quadrillage qui tranche. */
.vl-track__fill--gradient {
  width: calc(var(--vl-track-w) * var(--vl-track-fill));
  background-color: var(--vl-white);
  background-image: linear-gradient(90deg, var(--vl-white), var(--vl-accent));
}

/* Aussi large que la hauteur utile du rail : la pastille sort RONDE sans qu'on l'écrive. */
.vl-track__fill--knob {
  width: calc(var(--vl-track-h) - 2 * var(--vl-track-inset));
  background: var(--vl-white);
}
</style>
tokens de la charte — systems/ref-01-bento-pills-2030.json Télécharger
:root {
  --vl-bg: #F5F4F2;
  --vl-ink: #0B0B0B;
  --vl-accent: #E8342A;
  --vl-accent-deep: #8E1114;
  --vl-white: #FFFFFF;
  --vl-on-accent: #FFFFFF;
  --vl-index: #6E6C68;
  --vl-cell: 232px;
  --vl-gutter: 19px;
  --vl-margin: 58.5px;
  --vl-inset: 15.78px;
  --vl-radius-stadium: 116px;
  --vl-radius-squircle: 69.6px;
  --vl-radius-teardrop: 51.04px;
}
les métadonnées — patterns/shape-03-stadium-track.json (télécharger)
{
  "name": "Rail stadium à enfant emboîté",
  "family": "shape",
  "ref": "ref-01-bento-pills-2030",
  "intent": "Poser une piste à bouts ronds qui porte un objet EMBOÎTÉ — barre de progression, interrupteur, jauge — sans que le liseré entre les deux s'épaississe dans les courbes. Une seule loi le tient : l'enfant est inset d'une constante sur ses quatre côtés et son rayon vaut celui du rail moins cet inset, ce qui fait tomber les deux centres de courbure au même point. Deux remplissages en découlent, une barre partielle et une pastille, et ce ne sont pas deux objets : c'est le même rail avec deux longueurs d'enfant.",
  "when_to_use": "Partout où une piste doit porter une position ou un état : barre de progression, interrupteur, jauge de segment, pastille de statut dans un tableau de bord. Aussi comme primitive de composition dans un vocabulaire de formes pleines — c'est la forme la plus fréquente d'une planche géométrique, parce qu'elle est la seule à porter une orientation LONGUE sans texte.",
  "avoid_when": "Sur une boîte proche du carré : à moins de 1,6 d'aspect les deux bouts ronds se rejoignent, le rail devient une gélule courte puis un disque, et l'enfant emboîté n'a plus de course à parcourir — c'est un anneau, pas une piste. Éviter aussi dès que l'enfant doit toucher un bord : l'inset est constant sur les quatre côtés par construction, et le rendre nul d'un côté détruit la concentricité, donc l'objet même du pattern. Et en mailing : Outlook rend `border-radius` et les dégradés de façon imprévisible — voir `node bin/emit.mjs --audit --target email`.",
  "media": [
    "slide",
    "web"
  ],
  "tags": [
    "forme",
    "primitive",
    "stadium",
    "pilule",
    "rail",
    "toggle",
    "progression",
    "jauge",
    "rayon",
    "concentrique"
  ],
  "vars": [
    {
      "name": "--vl-track-h",
      "role": "la hauteur du rail — le seul paramètre d'échelle : elle fixe le rayon (h/2) et l'inset (0,068 h)",
      "default": "232px"
    },
    {
      "name": "--vl-track-w",
      "role": "la longueur du rail — au moins 1,6 fois la hauteur, sans quoi la piste n'a plus de course",
      "default": "483px"
    },
    {
      "name": "--vl-track-inset",
      "role": "l'écart de l'enfant, CONSTANT sur les quatre côtés — posé par le padding du rail, jamais côté par côté",
      "default": "15.776px"
    },
    {
      "name": "--vl-track-fill",
      "role": "la longueur de la barre, en fraction du rail — le seul paramètre de contenu du pattern",
      "default": "0.4477"
    },
    {
      "name": "--vl-track-gap",
      "role": "l'écart entre les deux remplissages de la démonstration",
      "default": "19px"
    },
    {
      "name": "--vl-ink",
      "role": "l'aplat du rail",
      "default": "#0B0B0B"
    },
    {
      "name": "--vl-white",
      "role": "l'enfant emboîté — pastille, et extrémité claire du dégradé",
      "default": "#FFFFFF"
    },
    {
      "name": "--vl-accent",
      "role": "l'extrémité chaude du dégradé de la barre",
      "default": "#E8342A"
    }
  ],
  "slots": [],
  "geometry": {
    "root": ".vl-track-set",
    "frame": [
      483,
      483
    ],
    "ratios": {
      "track_aspect": 2.082,
      "radius_over_height": 0.5,
      "inset_over_height": 0.068,
      "inner_radius_over_height": 0.432,
      "fill_over_track": 0.4477,
      "knob_over_height": 0.864
    }
  },
  "benchmarks": [
    {
      "name": "STADIUM — le rayon vaut exactement la moitié du PETIT côté, et il est écrit en LONGUEUR : un `50%` rendrait 50 % de CHAQUE axe, soit 241,5 de rayon horizontal contre 116 de vertical sur une boîte 2:1, c'est-à-dire une ellipse",
      "measure": "num('.vl-track--knob', 'borderTopLeftRadius') / box('.vl-track--knob').h",
      "expect": 0.5,
      "tol": 0.003
    },
    {
      "name": "…et le bout rond est un ARC DE CERCLE de rayon h/2, sondé au point sur six angles — la mesure ne lit pas la valeur déclarée, elle interroge la forme PEINTE : un rayon en pourcentage y échoue dès 135°, là où lire la valeur calculée ne verrait qu'un « 50 % » d'apparence saine",
      "measure": "(() => { const rail = document.querySelector('.vl-track--bar'); const b = rail.getBoundingClientRect(); const R = b.height / 2; const cx = b.left + R, cy = b.top + b.height / 2; const at = (k, t) => { const e = document.elementFromPoint(cx + Math.cos(t) * R * k, cy - Math.sin(t) * R * k); return !!e && (e === rail || rail.contains(e)); }; let ok = 1; for (const a of [110, 135, 160, 200, 225, 250]) { const t = a * Math.PI / 180; if (!at(0.90, t) || at(1.10, t)) ok = 0; } return ok; })()",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "INSET CONSTANT sur les quatre côtés : c'est le padding du rail qui pose l'enfant, jamais une marge choisie côté par côté",
      "measure": "(() => { const P = ['paddingTop','paddingRight','paddingBottom','paddingLeft'].map((p) => num('.vl-track--knob', p)); return (Math.max(...P) - Math.min(...P)) / Math.max(...P); })()",
      "max": 0.003
    },
    {
      "name": "…et il vaut 0,068 de la hauteur du rail — assez pour que le liseré se lise, assez peu pour que l'enfant reste l'objet et le rail le contenant",
      "measure": "num('.vl-track--knob', 'paddingTop') / box('.vl-track--knob').h",
      "expect": 0.068,
      "tol": 0.004
    },
    {
      "name": "CONTOURS CONCENTRIQUES — le rayon de l'enfant est celui du rail MOINS l'inset, sur les deux remplissages. Écrit à part, le liseré s'épaissirait dans les coins sans qu'aucun alignement ne bouge",
      "measure": "(() => { let worst = 0; for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { worst = Math.max(worst, Math.abs((num(r, 'borderTopLeftRadius') - num(f, 'borderTopLeftRadius')) / num(r, 'paddingTop') - 1)); } return worst; })()",
      "max": 0.01
    },
    {
      "name": "…ce qui se vérifie à l'endroit où ça compte : les deux CENTRES DE COURBURE tombent au même point, donc l'écart des deux bords est le même dans la courbe que sur la droite",
      "measure": "(() => { let worst = 0; for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { const cr = box(r).left + num(r, 'borderTopLeftRadius'), cf = box(f).left + num(f, 'borderTopLeftRadius'); const vr = box(r).top + box(r).h / 2, vf = box(f).top + box(f).h / 2; worst = Math.max(worst, Math.abs(cr - cf) / box(r).h, Math.abs(vr - vf) / box(r).h); } return worst; })()",
      "max": 0.002
    },
    {
      "name": "la pastille est CARRÉE de boîte — elle fait exactement la hauteur utile du rail, inset compris des deux côtés",
      "measure": "box('.vl-track__fill--knob').w / box('.vl-track__fill--knob').h",
      "expect": 1,
      "tol": 0.005
    },
    {
      "name": "…et elle sort RONDE sans qu'on l'écrive : son rayon vaut la moitié de son côté, ce qui n'est pas une déclaration mais la conséquence de la soustraction ci-dessus. Une primitive dont l'usage principal tombe d'une loi, c'est le signe que la loi est la bonne",
      "measure": "num('.vl-track__fill--knob', 'borderTopLeftRadius') / box('.vl-track__fill--knob').w",
      "expect": 0.5,
      "tol": 0.005
    },
    {
      "name": "la BARRE ne remplit pas son rail (44,8 %) : c'est ce qui fait lire une piste portant une position, et non un aplat à bouts ronds",
      "measure": "box('.vl-track__fill--gradient').w / box('.vl-track--bar').w",
      "expect": 0.4477,
      "tol": 0.01
    },
    {
      "name": "…et elle part du bord GAUCHE du rail, à l'inset près : une barre centrée dans sa piste ne dit plus d'où elle progresse",
      "measure": "(box('.vl-track__fill--gradient').left - box('.vl-track--bar').left) / num('.vl-track--bar', 'paddingLeft')",
      "expect": 1,
      "tol": 0.02
    },
    {
      "name": "le dégradé DÉCLARE sa couleur de repli, opaque : sans elle l'enfant a un fond transparent, toute mesure de couleur le compose avec le rail, et un contrôle de composition déclare la couche redondante — piège payé sur le deck de `ref-01`",
      "measure": "(() => { const p = (cs('.vl-track__fill--gradient', 'backgroundColor').match(/[\\d.]+/g) || []).map(Number); return p.length >= 3 && (p.length === 4 ? p[3] : 1) >= 1 ? 1 : 0; })()",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "la pastille se DÉTACHE de son rail : les deux fonds sont mesurés l'un contre l'autre, parce qu'un interrupteur dont l'état ne se voit pas n'est pas un interrupteur",
      "measure": "(() => { const a = lum(bgOf($('.vl-track__fill--knob'))), b = lum(bgOf($('.vl-track--knob'))); const [hi, lo] = a > b ? [a, b] : [b, a]; return (hi + 0.05) / (lo + 0.05); })()",
      "min": 3
    },
    {
      "name": "REPARAMÉTRAGE — de 120 à 400 px de hauteur de rail, le rayon reste h/2 et l'écart des contours reste l'inset : la loi ne dépend d'aucune des valeurs d'ici",
      "measure": "(() => { const set = document.querySelector('.vl-track-set'); let worst = 0; for (const h of ['120px', '232px', '300px', '400px']) { set.style.setProperty('--vl-track-h', h); for (const [r, f] of [['.vl-track--bar','.vl-track__fill--gradient'], ['.vl-track--knob','.vl-track__fill--knob']]) { const el = document.querySelector(r), fl = document.querySelector(f); const R = parseFloat(getComputedStyle(el).borderTopLeftRadius), rr = parseFloat(getComputedStyle(fl).borderTopLeftRadius), pad = parseFloat(getComputedStyle(el).paddingTop); worst = Math.max(worst, Math.abs(R / el.getBoundingClientRect().height - 0.5), Math.abs((R - rr) / pad - 1)); } } set.style.removeProperty('--vl-track-h'); return worst; })()",
      "max": 0.01
    }
  ],
  "notes": "Ce pattern verse une IDENTITÉ que la source de `ref-01` manquait. Son relevé listait deux primitives — un `stadium-h` à barre en dégradé et un `toggle` à pastille — et ce sont le même objet : un rail stadium portant un enfant en inset constant, une fois rempli d'un dégradé partiel, une fois d'un enfant aussi large que la hauteur utile. Les lister à part, c'est deux entrées de vocabulaire pour une seule loi, et c'est exactement ce que l'élagage du 30/07 interdit. C'est aussi pourquoi le `fill-gradient-stadium` annoncé par la spec n'est pas versé seul : le dégradé n'a pas d'existence propre ici, il EST le remplissage de l'enfant.\n\n**Le rayon s'écrit en LONGUEUR.** `border-radius: 50%/50%` était au relevé, et ce n'est pas un stadium : les deux pourcentages se résolvent sur des axes différents, donc sur 483 × 232 ils donnent 241,5 de rayon horizontal contre 116 de vertical — un ovale complet. Le stadium s'écrit `calc(hauteur / 2)`, exactement comme le chanfrein de `shape-notched-corner` s'écrit en longueur pour rester à 45° et comme le rayon de `shape-02-teardrop-quadrant` pour rester circulaire. Trois formes, un seul piège. La mesure ne se contente pas de lire la valeur déclarée — ce serait faible, Chrome rendant le `%` tel quel : elle DÉRIVE le rayon attendu de la hauteur et sonde la forme peinte au point sur six angles du bout rond. Un rayon en pourcentage y échoue dès 135°.\n\n**La concentricité est la loi, et le reste en tombe.** L'enfant est inset d'une constante sur ses quatre côtés — posée par le `padding` du rail, jamais côté par côté — et son rayon vaut celui du parent MOINS cet inset. Les deux centres de courbure tombent alors au même point, ce que le benchmark vérifie directement plutôt que de le déduire : l'écart des deux bords est le même dans la courbe que sur la droite. Écrire le rayon de l'enfant à part donne un liseré qui s'épaissit dans les coins, défaut qu'aucun alignement ne signale.\n\nConséquence, et c'est le test de la loi : **la pastille sort ronde sans qu'on l'écrive.** 200,44 de côté pour un rayon de 100,22 — personne n'a déclaré un cercle, il est ce que la soustraction produit quand l'enfant fait la hauteur utile du rail. Une primitive dont l'usage principal tombe d'une loi et non d'une exception, c'est le signe qu'on tient la bonne loi.\n\nLa longueur de la barre est le seul paramètre de contenu. Elle est à 0,4477 du rail parce que, dans `ref-01`, son bord droit tombe sur une ligne de colonne de la planche — le relevé était muet, et quand un relevé est muet sur une longueur c'est le quadrillage qui tranche (DOCTRINE §3), pas le goût. Hors de cette planche, c'est la valeur de progression qui la pilote. Enfin, `background-color` doit déclarer l'extrémité claire du dégradé : un enfant dont le fond n'est qu'un `background-image` a une couleur de fond transparente, toute mesure la compose avec celle du rail, et `check-deck.mjs` déclare la couche 1:1 redondante — piège payé sur le deck de cette même référence.\n\nLa planche complète est `layout-15-primitive-mosaic`, qui porte la grille et pas les formes.",
  "id": "shape-03-stadium-track"
}

À côté