{
  "name": "Triptyque d'images en bande",
  "family": "layout",
  "ref": "ref-07-retro-brand-hero",
  "intent": "Poser trois images de façon qu'elles se lisent comme UN bloc et non comme trois objets : une hauteur COMMUNE, une gouttière constante et petite devant une cellule, une bande remplie exactement d'un bord à l'autre. La hauteur, les trois poids de largeur et — quand la hauteur est laissée libre — le ratio qui la déduit sont les seuls paramètres : c'est par eux que la même bande donne un triptyque portrait à poids égaux dans un coin, ou une frise à cellules inégales sur toute une largeur.",
  "when_to_use": "Un bloc de trois visuels qui doit peser comme une masse unique — contrepoids d'un titre, frise de pied de page, bandeau de trois vues d'un même sujet. Se pose dans n'importe quelle largeur : la bande la remplit, et sa hauteur est soit imposée, soit déduite du ratio de la première cellule. Des poids inégaux sont un usage NORMAL du pattern, pas une entorse — c'est même eux qui font qu'une bande se lit comme une suite de photographies plutôt que comme une grille.",
  "avoid_when": "Une des trois images doit être regardée pour elle-même : la bande écrase les hiérarchies, et une image à mettre en avant demande une composition à mise en avant. Écarter aussi au-delà de trois cellules — à quatre, chaque vignette passe sous le quart de la largeur et la bande devient une planche de contact.",
  "media": [
    "web",
    "slide",
    "print"
  ],
  "tags": [
    "images",
    "triptyque",
    "bande",
    "frise",
    "photos",
    "grille",
    "gouttiere",
    "ratio",
    "flex",
    "media"
  ],
  "vars": [
    {
      "name": "--vl-gap",
      "role": "gouttière — la MÊME entre les deux paires, c'est elle qui fait la bande",
      "default": "12px"
    },
    {
      "name": "--vl-radius-photo",
      "role": "rayon des trois cellules, identique par construction",
      "default": "8px"
    },
    {
      "name": "--vl-ph-a-hi",
      "role": "image A (vague) — haut du dégradé d'eau",
      "default": "#86B0C4"
    },
    {
      "name": "--vl-ph-a-lo",
      "role": "image A — eau profonde, sert aussi à la masse de la vague",
      "default": "#3F6B84"
    },
    {
      "name": "--vl-ph-a-crest",
      "role": "image A — crête d'écume",
      "default": "#F1EDE3"
    },
    {
      "name": "--vl-ph-b-sky",
      "role": "image B (parasol) — ciel",
      "default": "#C9DEE7"
    },
    {
      "name": "--vl-ph-b-canvas",
      "role": "image B — toile du parasol, le seul rappel de l'accent hors typographie",
      "default": "#D8442E"
    },
    {
      "name": "--vl-ph-b-canvas-lo",
      "role": "image B — face à l'ombre de la toile",
      "default": "#92251C"
    },
    {
      "name": "--vl-ph-b-pole",
      "role": "image B — mât",
      "default": "#EDE4D2"
    },
    {
      "name": "--vl-ph-c-hi",
      "role": "image C (dune) — crête éclairée",
      "default": "#EFE1C6"
    },
    {
      "name": "--vl-ph-c-lo",
      "role": "image C — sable à l'ombre, sert aussi au sable de l'image B",
      "default": "#B9A181"
    }
  ],
  "slots": [
    "cell-a",
    "cell-b",
    "cell-c"
  ],
  "geometry": {
    "root": ".vl-tri",
    "frame": [
      472,
      199
    ],
    "ratios": {
      "cell_ratio": 0.75,
      "gap_over_frame": 0.0254,
      "gap_over_cell": 0.0804,
      "cell_w_over_frame": 0.3164
    },
    "type_px": {},
    "pad_ratio": {
      "top": 0,
      "x": 0,
      "bottom": 0
    }
  },
  "benchmarks": [
    {
      "name": "LA LOI DE LA BANDE — hauteur COMMUNE aux trois cellules : un pied et un sommet uniques",
      "measure": "(() => { const h = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect().height); return (Math.max(...h) - Math.min(...h)) / H; })()",
      "max": 0.005
    },
    {
      "name": "à poids égaux le même ratio en DÉCOULE — corollaire du cas ref-07, pas la loi (cf. le reparamétrage ref-08 plus bas)",
      "measure": "(() => { const r = [...document.querySelectorAll('.vl-tri__cell')].map((e) => { const b = e.getBoundingClientRect(); return b.width / b.height; }); return Math.max(...r) - Math.min(...r); })()",
      "max": 0.01
    },
    {
      "name": "GOUTTIÈRE CONSTANTE : les deux intervalles sont le même à moins de 0,2 % près",
      "measure": "(() => { const c = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect()); return Math.abs((c[1].left - c[0].right) - (c[2].left - c[1].right)) / W; })()",
      "max": 0.002
    },
    {
      "name": "gouttière PETITE devant une cellule : au-delà, elle sépare au lieu de relier",
      "measure": "(() => { const c = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect()); return (c[1].left - c[0].right) / c[0].width; })()",
      "min": 0.03,
      "max": 0.14
    },
    {
      "name": "la bande remplit sa largeur EXACTEMENT : rien ne reste à droite",
      "measure": "(W - box('.vl-tri__cell:nth-child(3)').right) / W",
      "max": 0.002
    },
    {
      "name": "ni à gauche",
      "measure": "box('.vl-tri__cell:nth-child(1)').left / W",
      "max": 0.002
    },
    {
      "name": "les poids par défaut sont ÉGAUX : trois largeurs identiques",
      "measure": "(() => { const w = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect().width); return (Math.max(...w) - Math.min(...w)) / W; })()",
      "max": 0.003
    },
    {
      "name": "les poids commandent bien la largeur : la base flex est à 0, pas au contenu",
      "measure": "cs('.vl-tri__cell','flexBasis') === '0px' ? 1 : 0",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "ratio PORTRAIT par défaut (3:4) — c'est un paramètre, l'assertion verrouille sa valeur, pas le pattern",
      "measure": "box('.vl-tri__cell:nth-child(1)').w / box('.vl-tri__cell:nth-child(1)').h",
      "expect": 0.75,
      "tol": 0.01
    },
    {
      "name": "UNE SEULE BANDE, DEUX RÉFÉRENCES — reparamétrée en ref-08 (1060 large, gouttière 16, hauteur 380, poids 1 / 1,31 / 1,14), elle rend les cellules relevées sur le deck : 298 / 390,3 / 339,7",
      "measure": "(() => { const c = document.querySelector('.vl-tri').cloneNode(true); c.style.cssText = 'width:1060px;--vl-gap:16px;--vl-tri-h:380px;--vl-tri-ratio:auto;--vl-tri-w1:1;--vl-tri-w2:1.31;--vl-tri-w3:1.14'; document.body.appendChild(c); const b = [...c.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect()); const want = [298, 390.3, 339.7]; const err = Math.max(...b.map((r, i) => Math.abs(r.width - want[i]))) / 1060; c.remove(); return err; })()",
      "max": 0.002
    },
    {
      "name": "et c'est bien la HAUTEUR qui les tient, pas le ratio : reparamétrée, la bande garde une hauteur unique alors que les trois ratios s'écartent de plus de 0,2",
      "measure": "(() => { const c = document.querySelector('.vl-tri').cloneNode(true); c.style.cssText = 'width:1060px;--vl-gap:16px;--vl-tri-h:380px;--vl-tri-ratio:auto;--vl-tri-w1:1;--vl-tri-w2:1.31;--vl-tri-w3:1.14'; document.body.appendChild(c); const b = [...c.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect()); const hs = b.map((r) => r.height), rs = b.map((r) => r.width / r.height); const flat = (Math.max(...hs) - Math.min(...hs)) / 380; const spread = Math.max(...rs) - Math.min(...rs); const full = Math.abs((b[2].right - b[0].left) - 1060) / 1060; c.remove(); return (flat <= 0.005 && spread >= 0.2 && full <= 0.002) ? 1 : 0; })()",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "chaque cellule CLIPPE son image : une bande pleine n'a pas de bord de fond visible",
      "measure": "[...document.querySelectorAll('.vl-tri__cell')].filter((e) => getComputedStyle(e).overflow === 'hidden').length",
      "expect": 3,
      "tol": 0.001
    },
    {
      "name": "l'image REMPLIT sa cellule : aucune bande de fond entre l'image et son cadre",
      "measure": "(() => { const c = document.querySelector('.vl-tri__cell').getBoundingClientRect(); const s = document.querySelector('.vl-tri__cell > svg').getBoundingClientRect(); return Math.max(Math.abs(c.width - s.width), Math.abs(c.height - s.height)) / c.width; })()",
      "max": 0.002
    },
    {
      "name": "même rayon sur les trois — un rayon qui varie recompte les objets",
      "measure": "new Set([...document.querySelectorAll('.vl-tri__cell')].map((e) => getComputedStyle(e).borderRadius)).size",
      "expect": 1,
      "tol": 0.001
    },
    {
      "name": "la hauteur de la bande est celle des cellules : rien ne déborde en dessous",
      "measure": "Math.abs(box('.vl-tri__cell:nth-child(1)').h - H) / H",
      "max": 0.005
    }
  ],
  "notes": "Trois règles font qu'on lit une bande et non trois images, et les benchmarks les tiennent une par une : hauteur COMMUNE, gouttière constante et petite, remplissage exact de la largeur.\n\nLa première a été mal nommée jusqu'au 12/08/2026, et c'est la correction qui a généralisé le pattern. On avait écrit « même RATIO pour les trois », parce que la seule référence qu'on avait sous la main — ref-07 — a des poids égaux, cas où le même ratio et la même hauteur sont la même phrase. Le relevé de ref-08 sur son deck a séparé les deux : ses cellules pèsent 1 / 1,31 / 1,14, leurs ratios vont de 0,78 à 1,03, et la bande se lit pourtant d'un bloc. Ce qui tient la bande est donc le pied et le sommet uniques ; le ratio identique n'en est que le corollaire à poids égaux. Concrètement, `aspect-ratio` a quitté les trois cellules pour ne vivre que sur la PREMIÈRE, qui donne sa taille à la ligne flex — `align-items: stretch` la recopie sur les deux autres. Posé sur les trois, il leur imposait trois hauteurs différentes dès que les poids cessaient d'être égaux : la bande se disloquait, et aucun reparamétrage n'était possible sans la casser. Deux benchmarks montent la garde là-dessus : ils CLONENT la bande, la reparamètrent en ref-08 et vérifient qu'elle rend bien les largeurs relevées sur le deck (298 / 390,3 / 339,7) à hauteur unique. C'est la preuve que les deux références sortent d'un seul pattern, et pas de deux copies qui divergeront.\n\n`flex: 1 0 0` et pas `1 0 auto`, et c'est un piège de construction, pas un détail. Avec une base `auto`, ce sont les dimensions intrinsèques des images qui fixent la largeur et les poids ne se partagent que le RESTE : les cellules dérivent dès qu'on remplace une image de démonstration par une autre, et rien ne le signale puisque la bande reste pleine. Un benchmark relit `flexBasis` pour cette raison — c'est la seule assertion du pattern qui porte sur une déclaration plutôt que sur une mesure, et c'est justifié : la faute qu'elle attrape ne se voit que sur des images qu'on n'a pas encore.\n\nLa hauteur, les trois poids et le ratio de repli sont les seuls paramètres. `ref-07` laisse la hauteur libre et prend `3 / 4` avec `1 1 1`, sur 42 % de la largeur de sa carte, calée haut-droite ; `ref-08` impose 380 px de haut avec `1 / 1,31 / 1,14`, sur 100 % de la largeur utile de sa page, calée en pied. C'est la même bande, deux tempéraments — et c'est précisément ce qu'il ne faut pas dupliquer en deux patterns.\n\nLes images de démonstration sont des compositions SVG génératives, jamais des URL distantes : rien à télécharger, rien qui périme, et chaque couleur passe par un token. Leurs arrêts de dégradé passent par des CLASSES et non par des attributs de présentation — une variable CSS dans un attribut SVG n'est pas garantie partout, une propriété CSS l'est. En usage réel, ces trois `<svg>` sont remplacés par des `<img>` : le pattern possède la bande, pas ses images.",
  "id": "layout-13-image-triptych"
}