{
  "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"
}