layout-01-nested-bento
layout-13-image-triptych
Triptyque d'images en bande
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.
layoutwebslideprint472×199px15 benchmarks
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
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.
Éviter quand
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.
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. La 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. `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. La 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. Les 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.
Mots-clés
Benchmarks — la géométrie, en assertions vérifiables
Rejouables par node bin/check.mjs layout-13-image-triptych : le pattern se prouve, il ne se croit pas.
| assertion | mesure | attendu |
|---|---|---|
| LA LOI DE LA BANDE — hauteur COMMUNE aux trois cellules : un pied et un sommet uniques | (() => { const h = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect().height); return (Math.max(...h) - Math.min(...h)) / H; })() | ≤ 0.005 |
| à 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) | (() => { 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); })() | ≤ 0.01 |
| GOUTTIÈRE CONSTANTE : les deux intervalles sont le même à moins de 0,2 % près | (() => { 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; })() | ≤ 0.002 |
| gouttière PETITE devant une cellule : au-delà, elle sépare au lieu de relier | (() => { const c = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect()); return (c[1].left - c[0].right) / c[0].width; })() | 0.03 → 0.14 |
| la bande remplit sa largeur EXACTEMENT : rien ne reste à droite | (W - box('.vl-tri__cell:nth-child(3)').right) / W | ≤ 0.002 |
| ni à gauche | box('.vl-tri__cell:nth-child(1)').left / W | ≤ 0.002 |
| les poids par défaut sont ÉGAUX : trois largeurs identiques | (() => { const w = [...document.querySelectorAll('.vl-tri__cell')].map((e) => e.getBoundingClientRect().width); return (Math.max(...w) - Math.min(...w)) / W; })() | ≤ 0.003 |
| les poids commandent bien la largeur : la base flex est à 0, pas au contenu | cs('.vl-tri__cell','flexBasis') === '0px' ? 1 : 0 | 1 ± 0 |
| ratio PORTRAIT par défaut (3:4) — c'est un paramètre, l'assertion verrouille sa valeur, pas le pattern | box('.vl-tri__cell:nth-child(1)').w / box('.vl-tri__cell:nth-child(1)').h | 0.75 ± 0.01 |
| 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 | (() => { 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; })() | ≤ 0.002 |
| 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 | (() => { 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; })() | 1 ± 0 |
| chaque cellule CLIPPE son image : une bande pleine n'a pas de bord de fond visible | [...document.querySelectorAll('.vl-tri__cell')].filter((e) => getComputedStyle(e).overflow === 'hidden').length | 3 ± 0.001 |
| l'image REMPLIT sa cellule : aucune bande de fond entre l'image et son cadre | (() => { 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; })() | ≤ 0.002 |
| même rayon sur les trois — un rayon qui varie recompte les objets | new Set([...document.querySelectorAll('.vl-tri__cell')].map((e) => getComputedStyle(e).borderRadius)).size | 1 ± 0.001 |
| la hauteur de la bande est celle des cellules : rien ne déborde en dessous | Math.abs(box('.vl-tri__cell:nth-child(1)').h - H) / H | ≤ 0.005 |
Le code
patterns/layout-13-image-triptych.html
Télécharger<!-- layout-13-image-triptych — trois images qui se lisent comme UNE bande. Ce n'est pas une
galerie : une galerie donne à voir chaque image, une bande donne à voir un bloc. Ce qui
fait la différence tient en trois règles, et elles se mesurent :
• HAUTEUR COMMUNE — c'est ELLE qui fait la bande. Un pied et un sommet uniques, et l'œil
lit un bloc même si les trois cellules n'ont pas le même ratio ;
• gouttière CONSTANTE, et PETITE devant une cellule. Élargie, elle sépare au lieu de
relier ;
• la bande occupe sa largeur EXACTEMENT, d'un bord à l'autre — pas de reste à droite.
Le « même ratio pour les trois » n'est PAS la loi : c'est ce qu'on obtient à poids égaux,
un cas particulier. ref-07 y est (1/1/1, portrait 3:4, 42 % de sa carte, haut-droite) ;
ref-08 n'y est pas (poids 1 / 1,31 / 1,14, hauteur imposée 380, pleine largeur, en pied)
et se lit pourtant comme une seule bande. Les paramètres sont donc trois : la hauteur
commune, les poids, et — quand la hauteur est laissée libre — le ratio qui la déduit.
Couleurs via --vl-* uniquement (les images de démonstration sont génératives — aucune URL
distante, rien qui périme). -->
<div class="vl-tri">
<!-- A — une vague qui se lève, crête d'écume -->
<figure class="vl-tri__cell">
<svg viewBox="0 0 150 200" preserveAspectRatio="xMidYMid slice" role="img"
aria-label="A wave lifting into a foam crest">
<defs>
<linearGradient id="vltri-a" x1="0.1" y1="0" x2="0.9" y2="1">
<stop offset="0" class="vl-tri-a-hi"/><stop offset="1" class="vl-tri-a-lo"/>
</linearGradient>
<filter id="vltri-soft"><feGaussianBlur stdDeviation="2"/></filter>
<filter id="vltri-grain"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3"/></filter>
</defs>
<rect width="150" height="200" fill="url(#vltri-a)"/>
<path d="M-10 128 C 30 96 62 82 96 92 C 128 101 142 124 160 140 L 160 210 L -10 210 Z"
class="vl-tri-a-deep" opacity="0.55" filter="url(#vltri-soft)"/>
<path d="M-10 120 C 26 90 58 74 94 84 C 122 92 138 112 160 128
C 132 122 112 118 92 122 C 60 128 26 132 -10 140 Z"
class="vl-tri-a-crest" opacity="0.9" filter="url(#vltri-soft)"/>
<rect width="150" height="200" filter="url(#vltri-grain)" opacity="0.1"
style="mix-blend-mode: overlay"/>
</svg>
</figure>
<!-- B — parasol rouge planté dans le sable -->
<figure class="vl-tri__cell">
<svg viewBox="0 0 150 200" preserveAspectRatio="xMidYMid slice" role="img"
aria-label="A red beach parasol planted in the sand">
<defs>
<linearGradient id="vltri-b" x1="0" y1="0" x2="1" y2="0.4">
<stop offset="0" class="vl-tri-b-canvas"/><stop offset="1" class="vl-tri-b-canvas-lo"/>
</linearGradient>
<filter id="vltri-soft-b"><feGaussianBlur stdDeviation="1.6"/></filter>
<filter id="vltri-grain-b"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3"/></filter>
</defs>
<rect width="150" height="200" class="vl-tri-b-sky"/>
<rect y="132" width="150" height="68" class="vl-tri-b-sand"/>
<ellipse cx="86" cy="150" rx="46" ry="9" class="vl-tri-b-sand" opacity="0.55"
filter="url(#vltri-soft-b)"/>
<path d="M18 84 C 34 40 96 26 128 58 C 138 68 140 78 138 86 Z" fill="url(#vltri-b)"/>
<rect x="84" y="82" width="4" height="66" class="vl-tri-b-pole"/>
<rect width="150" height="200" filter="url(#vltri-grain-b)" opacity="0.1"
style="mix-blend-mode: overlay"/>
</svg>
</figure>
<!-- C — une dune, cadrage serré. Pas d'oyats dessinés : cinq brins vectoriels ne font pas
une photo, ils font un pictogramme. -->
<figure class="vl-tri__cell">
<svg viewBox="0 0 150 200" preserveAspectRatio="xMidYMid slice" role="img"
aria-label="A dune ridge">
<defs>
<linearGradient id="vltri-c" x1="0.1" y1="0" x2="0.9" y2="1">
<stop offset="0" class="vl-tri-c-hi"/><stop offset="1" class="vl-tri-c-lo"/>
</linearGradient>
<filter id="vltri-soft-c"><feGaussianBlur stdDeviation="2.4"/></filter>
<filter id="vltri-grain-c"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3"/></filter>
</defs>
<rect width="150" height="200" fill="url(#vltri-c)"/>
<path d="M-10 104 C 28 78 64 92 96 78 C 120 68 138 72 160 64 L 160 210 L -10 210 Z"
class="vl-tri-c-ridge" opacity="0.55" filter="url(#vltri-soft-c)"/>
<path d="M-10 158 C 34 138 76 154 116 136 C 134 128 148 130 160 126 L 160 210 L -10 210 Z"
class="vl-tri-b-sand" opacity="0.45" filter="url(#vltri-soft-c)"/>
<path d="M-10 176 C 40 158 84 172 124 158 C 142 152 152 152 160 150 L 160 210 L -10 210 Z"
class="vl-tri-c-ridge" opacity="0.7" filter="url(#vltri-soft-c)"/>
<rect width="150" height="200" filter="url(#vltri-grain-c)" opacity="0.1"
style="mix-blend-mode: overlay"/>
</svg>
</figure>
</div>
<style>
/* Les TROIS paramètres du pattern, et il n'y en a pas d'autres :
--vl-tri-h la hauteur COMMUNE de la bande. `auto` = laissée libre, elle se déduit
alors du ratio de la première cellule ; une valeur = elle est imposée,
et le ratio ne sert plus (ref-08 : 380px).
--vl-tri-w1..3 les poids de largeur. ref-07 : 1 1 1. ref-08 : 1 / 1,31 / 1,14.
--vl-tri-ratio le ratio de la PREMIÈRE cellule, qui DÉDUIT la hauteur quand elle est
libre. À poids égaux les trois cellules l'héritent — c'est de là que
vient le « même ratio », qui est un COROLLAIRE et non la loi.
Tout le reste — gouttière, rayon, clipping, hauteur commune — est la loi de la bande. */
.vl-tri {
--vl-tri-h: auto;
--vl-tri-ratio: 3 / 4;
--vl-tri-w1: 1; --vl-tri-w2: 1; --vl-tri-w3: 1;
width: 472px;
height: var(--vl-tri-h);
display: flex; align-items: stretch; gap: var(--vl-gap);
}
/* `flex: <poids> 0 0` et non `1 0 auto` : la base à 0 est ce qui fait que les poids commandent
VRAIMENT la largeur. Avec une base auto, c'est le contenu qui décide et les poids ne font
que se partager le reste — les cellules dérivent dès qu'une image a d'autres dimensions. */
.vl-tri__cell {
margin: 0;
position: relative;
flex: 1 0 0;
border-radius: var(--vl-radius-photo);
overflow: hidden;
}
/* Le ratio ne vit que sur la PREMIÈRE cellule, et c'est ce qui généralise le pattern : il
donne à la ligne flex sa taille transversale, et `align-items: stretch` la recopie sur les
deux autres. Posé sur les trois — l'ancienne écriture — il leur imposait TROIS hauteurs
différentes dès que les poids cessaient d'être égaux : la bande se disloquait, et rien ne
pouvait la reparamétrer sans la casser. */
.vl-tri__cell:nth-child(1) { flex-grow: var(--vl-tri-w1); aspect-ratio: var(--vl-tri-ratio); }
.vl-tri__cell:nth-child(2) { flex-grow: var(--vl-tri-w2); }
.vl-tri__cell:nth-child(3) { flex-grow: var(--vl-tri-w3); }
/* `slice` : l'image REMPLIT la cellule et se fait recadrer. `meet` laisserait des bandes de
fond dans les cellules et la bande cesserait d'être pleine. Le retrait du flux (`absolute`)
n'est pas cosmétique : une image DANS le flux rend une hauteur intrinsèque à sa cellule,
qui devient candidate à la taille de la ligne — la cellule la plus haute tirerait alors
toute la bande, au lieu que ce soit le ratio de la première qui décide. */
.vl-tri__cell > svg,
.vl-tri__cell > img {
display: block; position: absolute; inset: 0;
width: 100%; height: 100%; object-fit: cover;
}
/* Les arrêts de dégradé passent par des classes : une variable CSS dans un attribut de
présentation SVG n'est pas garantie partout, une propriété CSS l'est. */
.vl-tri-a-hi { stop-color: var(--vl-ph-a-hi); }
.vl-tri-a-lo { stop-color: var(--vl-ph-a-lo); }
.vl-tri-a-deep { fill: var(--vl-ph-a-lo); }
.vl-tri-a-crest { fill: var(--vl-ph-a-crest); }
.vl-tri-b-sky { fill: var(--vl-ph-b-sky); }
.vl-tri-b-canvas { stop-color: var(--vl-ph-b-canvas); }
.vl-tri-b-canvas-lo { stop-color: var(--vl-ph-b-canvas-lo); }
.vl-tri-b-pole { fill: var(--vl-ph-b-pole); }
.vl-tri-b-sand { fill: var(--vl-ph-c-lo); }
.vl-tri-c-hi { stop-color: var(--vl-ph-c-hi); }
.vl-tri-c-lo { stop-color: var(--vl-ph-c-lo); }
.vl-tri-c-ridge { fill: var(--vl-ph-c-hi); }
</style>
systems/ref-07-retro-brand-hero.json
Télécharger:root {
--vl-card: #FBF7EC;
--vl-bg: #FBF7EC;
--vl-ink: #1E1B18;
--vl-accent: #B2201C;
--vl-on-accent: #FBF7EC;
--vl-ground-sky-hi: #D7E6EC;
--vl-ground-sky-lo: #BBD3DE;
--vl-ground-sun: #F4EFE2;
--vl-ground-sea-hi: #7FA8BC;
--vl-ground-sea-lo: #5E8AA0;
--vl-ground-foam: #EFEAE0;
--vl-ground-sand-wet: #C7B49A;
--vl-ground-sand-dry: #E4D5BC;
--vl-ph-a-hi: #86B0C4;
--vl-ph-a-lo: #3F6B84;
--vl-ph-a-crest: #F1EDE3;
--vl-ph-b-sky: #C9DEE7;
--vl-ph-b-canvas: #D8442E;
--vl-ph-b-canvas-lo: #92251C;
--vl-ph-b-pole: #EDE4D2;
--vl-ph-c-hi: #EFE1C6;
--vl-ph-c-lo: #B9A181;
--vl-radius-card: 6px;
--vl-radius-photo: 8px;
--vl-radius-pill: 4px;
--vl-gap: 12px;
--vl-margin: 44px;
--vl-shadow-card: 0 24px 60px rgba(0, 0, 0, 0.18);
}
les métadonnées — patterns/layout-13-image-triptych.json (télécharger)
{
"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"
}
À côté
layout-02-swiss-frame
Cadre suisse header / footer
layout-03-glass-board
Écran en verre dépoli — fond dégradé + modules
layout-04-graph-paper-cells
Papier millimétré et ses cases
layout-05-nav-pill-bar
Barre de menu en pilule flottante
layout-06-bento-l-span