layout-10-bleed-column-inset
list-07-hairline-spec-table
Tableau libellé/valeur à filets débordants
Poser les caractéristiques d'un lot — livrables, formats, cadence — en couples libellé / valeur séparés par des filets d'un pixel et demi qui DÉBORDENT la marge de texte des deux côtés. Le filet n'est pas la bordure d'un bloc, c'est une règle de la page que le module traverse : il part avant le libellé et finit après la valeur, et c'est ce seul décalage qui distingue une table de spécifications d'un encadré.
listslidewebprint1120×278px18 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
Slide ou page de livrables, de portée, de mentions techniques : deux à cinq couples dont le libellé est un mot et la valeur une énumération. Il complète title-02-condensed-overlay-stack sur la même scène, sous le titre, et n'a besoin d'aucune surface pour tenir.
Éviter quand
Les valeurs sont des NOMBRES à comparer entre eux, ou il y a plus de deux colonnes : ce module cale sa valeur à droite d'une colonne étroite, ce qui aligne les fins de ligne et non les ordres de grandeur — pour des chiffres, prendre list-04-due-rows. Écarter aussi quand le module ne peut pas déborder la marge de texte de la page (carte, encadré, cellule) : rentré dans un conteneur, le débord disparaît et il ne reste qu'un tableau à filets ordinaire.
Notes
Ce pattern est le `table-hairline-rules` que la spec de ref-10 annonçait, et son seul geste est un DÉBORD que rien ne signale quand on le perd. La mécanique tient en une ligne : le retrait est porté par le `padding` de la LIGNE, jamais par ses cellules. Un `border-top` court sur toute la boîte de bordure alors que le padding rentre le contenu — mettez le retrait sur `.vl-hst__lab` et `.vl-hst__val` et les filets se rétractent avec le texte, ce qui est exactement le geste inverse : un bloc encadré posé sur la page au lieu d'une règle que la page traverse. Deux benchmarks le tiennent des deux côtés, parce que le débord de gauche seul se relit comme une simple indentation. Le reste du module est fait de renoncements, tous mesurés : pas de filet de fermeture en bas (il refermerait un cadre), pas de bordure verticale (les colonnes sont ferrées, pas dessinées), pas de fond. La respiration est volontairement asymétrique — 14 px au-dessus, 20 en dessous — pour que chaque couple se lise accroché à SON filet ; égalisées, les lignes flottent entre deux traits et l'appartenance se perd, sans qu'aucun alignement ne casse. Libellé et valeur sont au MÊME corps : la hiérarchie vient du fer et du vide, jamais de la taille. Le rouge est celui corrigé de la référence (#C82C15, 4,57:1 sur la crème) et non celui relevé sur la source (#E33A22, 3,57:1) : à 13 px il faut le plancher du texte courant, et les deux benchmarks de contraste sont là pour que la correction ne se reperde pas au premier remappage de charte. Sur la slide d'origine le module occupe 70 % de la largeur (1120 sur 1600) et son bord gauche est celui de la SLIDE — le débord gauche va donc jusqu'au bord perdu ; posé ailleurs, il lui faut au moins la marge de texte de sa page pour exister. La source montre deux couples, le fragment en pose trois : c'est la même ligne répétée, pas un élément ajouté, et c'est ce qui rend visible que le filet appartient à la ligne qu'il ouvre.
Mots-clés
Benchmarks — la géométrie, en assertions vérifiables
Rejouables par node bin/check.mjs list-07-hairline-spec-table : le pattern se prouve, il ne se croit pas.
| assertion | mesure | attendu |
|---|---|---|
| le filet DÉBORDE le libellé par la gauche — la marge de texte de la page | (box('.vl-hst__lab').left - box('.vl-hst__row').left) / W | 0.1223 ± 0.012 |
| il déborde AUSSI par la droite : le débord n'est pas un simple retrait de bloc | (box('.vl-hst__row').right - box('.vl-hst__val').right) / W | 0.025 → 0.08 |
| le filet traverse TOUT le module, sur toute sa largeur | box('.vl-hst__row').w / W | 1 ± 0.002 |
| filet en CHEVEU : sous 0,4 % de la largeur du module, et jamais nul | num('.vl-hst__row', 'borderTopWidth') / W | 0.0008 → 0.004 |
| chaque ligne porte son propre filet, la PREMIÈRE comprise | [...document.querySelectorAll('.vl-hst__row')].filter((e) => parseFloat(getComputedStyle(e).borderTopWidth) > 0).length / document.querySelectorAll('.vl-hst__row').length | 1 ± 0 |
| AUCUN filet de fermeture en bas — sinon le module redevient un cadre | [...document.querySelectorAll('.vl-hst__row')].reduce((a, e) => a + parseFloat(getComputedStyle(e).borderBottomWidth), 0) | 0 ± 0 |
| aucune bordure VERTICALE : les colonnes ne sont pas dessinées, elles sont ferrées | [...document.querySelectorAll('.vl-hst__row')].reduce((a, e) => { const s = getComputedStyle(e); return a + parseFloat(s.borderLeftWidth) + parseFloat(s.borderRightWidth); }, 0) | 0 ± 0 |
| aucune SURFACE : ce module est fait de traits, il ne pose pas de fond | cs('.vl-hst', 'backgroundColor') === 'rgba(0, 0, 0, 0)' && cs('.vl-hst__row', 'backgroundColor') === 'rgba(0, 0, 0, 0)' ? 1 : 0 | 1 ± 0 |
| respiration ASYMÉTRIQUE : le couple est accroché au filet qui l'ouvre | num('.vl-hst__row', 'paddingBottom') / num('.vl-hst__row', 'paddingTop') | 1.2 → 2.2 |
| la valeur reste une COLONNE étroite, pas un paragraphe pleine largeur | box('.vl-hst__val').w / W | 0.14 → 0.28 |
| la valeur est ferrée à DROITE : ce sont les fins de ligne qui s'alignent | cs('.vl-hst__val', 'textAlign') === 'right' ? 1 : 0 | 1 ± 0 |
| le vide entre le libellé et sa valeur est le vrai séparateur du couple | (box('.vl-hst__val').left - box('.vl-hst__lab').right) / W | ≥ 0.25 |
| la valeur COURT sur plusieurs lignes — une énumération, pas une étiquette | box('.vl-hst__val').h / num('.vl-hst__val', 'fontSize') | ≥ 2.5 |
| libellé et valeur PENDENT du filet : leurs premières lignes s'alignent | Math.abs(box('.vl-hst__lab').top - box('.vl-hst__val').top) / num('.vl-hst__lab', 'fontSize') | ≤ 0.2 |
| libellé et valeur partagent le MÊME corps : aucune hiérarchie de taille | num('.vl-hst__lab', 'fontSize') / num('.vl-hst__val', 'fontSize') | 1 ± 0.001 |
| le libellé tient le plancher de contraste du texte courant | contrast('.vl-hst__lab') | ≥ 4.5 |
| la valeur aussi — c'est le même rouge, il doit passer aux deux endroits | contrast('.vl-hst__val') | ≥ 4.5 |
| rien ne déborde en hauteur : chaque ligne contient son texte | overflow('.vl-hst') | 0 ± 0 |
Le code
patterns/list-07-hairline-spec-table.html
Télécharger<!-- list-07-hairline-spec-table — fragment autonome. Couleurs via --vl-* uniquement, aucune valeur en dur. -->
<dl class="vl-hst">
<div class="vl-hst__row">
<dt class="vl-hst__lab">Content:</dt>
<dd class="vl-hst__val">Social media posts, video ads, creator collaborations, email
campaigns, print collateral</dd>
</div>
<div class="vl-hst__row">
<dt class="vl-hst__lab">Formats:</dt>
<dd class="vl-hst__val">Short-form videos, high-resolution photos, user-generated content,
carousel posts</dd>
</div>
<div class="vl-hst__row">
<dt class="vl-hst__lab">Cadence:</dt>
<dd class="vl-hst__val">Four drops a month, one hero film each quarter</dd>
</div>
</dl>
<style>
/* Le tableau de livrables d'une slide de campagne : des couples libellé / valeur séparés par
des filets qui DÉBORDENT la marge de texte, des deux côtés. C'est le seul geste du module.
Il tient à une seule mécanique : le retrait est porté par le PADDING de la ligne, jamais par
ses cellules — le filet est une bordure, donc il court sur toute la boîte pendant que le
texte, lui, rentre. Posez le retrait sur les cellules et les filets se rétractent avec elles :
l'ensemble redevient un bloc encadré posé sur la page, au lieu d'une règle de mise en page
que la page traverse. Aucune surface, aucune bordure verticale, aucun fond. */
.vl-hst {
position: relative;
width: 1120px;
margin: 0;
color: var(--vl-red);
font-family: Archivo, Inter, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Le filet appartient à la ligne qu'il OUVRE (border-top), et la dernière n'en reçoit aucun
en bas : un filet de fermeture referait un cadre, ce que ce module refuse.
Respiration ASYMÉTRIQUE — moins au-dessus qu'en dessous — pour que chaque couple se lise
accroché à SON filet et non flottant entre deux. Égalisez-les, l'appartenance se perd. */
.vl-hst__row {
display: grid;
grid-template-columns: 40% 60%;
/* Les deux cellules PENDENT du filet, elles ne se centrent pas dans la ligne : centré, un
libellé d'une ligne flotte au milieu d'une valeur qui en fait trois, et le couple cesse
de se lire comme appartenant au trait qui l'ouvre. Rien ne casse quand on se trompe. */
align-items: start;
border-top: var(--vl-hst-rule, 2.5px) solid var(--vl-red);
padding: 14px var(--vl-hst-tail, 4.29%) 20px var(--vl-hst-inset, 12.23%);
}
.vl-hst__lab { margin: 0; }
/* La valeur se cale à DROITE de sa colonne large et y reste étroite : une colonne de mesure,
pas un paragraphe qui remplirait la largeur disponible. */
.vl-hst__val {
margin: 0 0 0 auto;
max-width: var(--vl-hst-val-w, 270px);
text-align: right;
}
.vl-hst__lab,
.vl-hst__val {
font-size: var(--vl-hst-size, 13px);
line-height: 1.45;
font-weight: 700;
font-variation-settings: 'wght' 700, 'wdth' 100;
letter-spacing: 0.02em;
text-transform: uppercase;
}
</style>
systems/ref-10-campaign-board-red.json
Télécharger:root {
--vl-bg: #EDEAE3;
--vl-board: #EDEAE3;
--vl-slide: #EDEAE3;
--vl-red: #C82C15;
--vl-white: #FFFFFF;
--vl-inset: 137px;
--vl-gutter: 8px;
--vl-rule: 2.5px;
--vl-cow-white: #F2EFE7;
--vl-cow-shade: #D8D1C1;
--vl-cow-brown: #5A3520;
--vl-cow-brown-hi: #8A5732;
--vl-jkt-red: #C6281B;
--vl-jkt-red-hi: #EC5233;
--vl-jkt-red-lo: #8A150F;
--vl-jkt-sky: #74A9D6;
--vl-jkt-sky-lo: #A8C6DE;
--vl-jkt-btn: #26100D;
--vl-road-sky: #D2DEE6;
--vl-road-tree: #2A4622;
--vl-road-green: #5F8C46;
--vl-road-green-lo: #3B5C2C;
--vl-road-dirt: #C3A57E;
--vl-road-dirt-hi: #DDC59F;
--vl-road-rut: #A88964;
--vl-road-figure: #201E1A;
--vl-hand-bg: #D9DBD3;
--vl-hand-bg-lo: #B4B8AE;
--vl-glove: #F3F0E8;
--vl-glove-hi: #FFFFFF;
--vl-glove-sh: #CBC5B7;
--vl-vial-glass: #DEE3E1;
--vl-vial-cap: #131110;
--vl-vial-drop: #6A6660;
--vl-room-red: #CE2A1C;
--vl-room-red-lo: #8E120C;
--vl-room-fig: #6B0F0A;
--vl-cact-bg: #B5AFA0;
--vl-cact-bg-lo: #8E8A7C;
--vl-cact-green: #6C8860;
--vl-cact-green-hi: #95AC86;
--vl-cact-green-lo: #3D5436;
--vl-cact-spine: #D6402A;
--vl-cact-spine-hi: #F2A794;
}
les métadonnées — patterns/list-07-hairline-spec-table.json (télécharger)
{
"name": "Tableau libellé/valeur à filets débordants",
"family": "list",
"ref": "ref-10-campaign-board-red",
"intent": "Poser les caractéristiques d'un lot — livrables, formats, cadence — en couples libellé / valeur séparés par des filets d'un pixel et demi qui DÉBORDENT la marge de texte des deux côtés. Le filet n'est pas la bordure d'un bloc, c'est une règle de la page que le module traverse : il part avant le libellé et finit après la valeur, et c'est ce seul décalage qui distingue une table de spécifications d'un encadré.",
"when_to_use": "Slide ou page de livrables, de portée, de mentions techniques : deux à cinq couples dont le libellé est un mot et la valeur une énumération. Il complète title-02-condensed-overlay-stack sur la même scène, sous le titre, et n'a besoin d'aucune surface pour tenir.",
"avoid_when": "Les valeurs sont des NOMBRES à comparer entre eux, ou il y a plus de deux colonnes : ce module cale sa valeur à droite d'une colonne étroite, ce qui aligne les fins de ligne et non les ordres de grandeur — pour des chiffres, prendre list-04-due-rows. Écarter aussi quand le module ne peut pas déborder la marge de texte de la page (carte, encadré, cellule) : rentré dans un conteneur, le débord disparaît et il ne reste qu'un tableau à filets ordinaire.",
"media": [
"slide",
"web",
"print"
],
"tags": [
"tableau",
"filets",
"hairline",
"libellé",
"valeur",
"spécifications",
"livrables",
"débord",
"capitales",
"micro-caps",
"campagne"
],
"vars": [
{
"name": "--vl-red",
"role": "couleur unique du module — filets ET texte, jamais deux encres",
"default": "#C82C15"
},
{
"name": "--vl-hst-rule",
"role": "épaisseur du filet ; reste un cheveu, sous 0,4 % de la largeur du module",
"default": "2.5px"
},
{
"name": "--vl-hst-inset",
"role": "de combien le filet déborde le libellé À GAUCHE — c'est la marge de texte de la page, pas un réglage du module",
"default": "12.23%"
},
{
"name": "--vl-hst-tail",
"role": "de combien le filet déborde la valeur À DROITE",
"default": "4.29%"
},
{
"name": "--vl-hst-val-w",
"role": "largeur maximale de la colonne de valeur — elle reste une colonne de mesure, pas un paragraphe",
"default": "270px"
},
{
"name": "--vl-hst-size",
"role": "corps des micro-capitales, libellé et valeur confondus",
"default": "13px"
}
],
"slots": [
"rows"
],
"geometry": {
"root": ".vl-hst",
"frame": [
1120,
278
],
"ratios": {
"frame_over_slide_width": 0.7,
"rule_over_frame": 0.00223,
"inset_over_frame": 0.1223,
"tail_over_frame": 0.0429,
"value_width_over_frame": 0.2411,
"pad_bottom_over_pad_top": 1.43
},
"type_px": {
"label": 13,
"value": 13
},
"pad_ratio": {
"top": 0,
"x": 0,
"bottom": 0
}
},
"benchmarks": [
{
"name": "le filet DÉBORDE le libellé par la gauche — la marge de texte de la page",
"measure": "(box('.vl-hst__lab').left - box('.vl-hst__row').left) / W",
"expect": 0.1223,
"tol": 0.012
},
{
"name": "il déborde AUSSI par la droite : le débord n'est pas un simple retrait de bloc",
"measure": "(box('.vl-hst__row').right - box('.vl-hst__val').right) / W",
"min": 0.025,
"max": 0.08
},
{
"name": "le filet traverse TOUT le module, sur toute sa largeur",
"measure": "box('.vl-hst__row').w / W",
"expect": 1,
"tol": 0.002
},
{
"name": "filet en CHEVEU : sous 0,4 % de la largeur du module, et jamais nul",
"measure": "num('.vl-hst__row', 'borderTopWidth') / W",
"min": 0.0008,
"max": 0.004
},
{
"name": "chaque ligne porte son propre filet, la PREMIÈRE comprise",
"measure": "[...document.querySelectorAll('.vl-hst__row')].filter((e) => parseFloat(getComputedStyle(e).borderTopWidth) > 0).length / document.querySelectorAll('.vl-hst__row').length",
"expect": 1,
"tol": 0
},
{
"name": "AUCUN filet de fermeture en bas — sinon le module redevient un cadre",
"measure": "[...document.querySelectorAll('.vl-hst__row')].reduce((a, e) => a + parseFloat(getComputedStyle(e).borderBottomWidth), 0)",
"expect": 0,
"tol": 0
},
{
"name": "aucune bordure VERTICALE : les colonnes ne sont pas dessinées, elles sont ferrées",
"measure": "[...document.querySelectorAll('.vl-hst__row')].reduce((a, e) => { const s = getComputedStyle(e); return a + parseFloat(s.borderLeftWidth) + parseFloat(s.borderRightWidth); }, 0)",
"expect": 0,
"tol": 0
},
{
"name": "aucune SURFACE : ce module est fait de traits, il ne pose pas de fond",
"measure": "cs('.vl-hst', 'backgroundColor') === 'rgba(0, 0, 0, 0)' && cs('.vl-hst__row', 'backgroundColor') === 'rgba(0, 0, 0, 0)' ? 1 : 0",
"expect": 1,
"tol": 0
},
{
"name": "respiration ASYMÉTRIQUE : le couple est accroché au filet qui l'ouvre",
"measure": "num('.vl-hst__row', 'paddingBottom') / num('.vl-hst__row', 'paddingTop')",
"min": 1.2,
"max": 2.2
},
{
"name": "la valeur reste une COLONNE étroite, pas un paragraphe pleine largeur",
"measure": "box('.vl-hst__val').w / W",
"min": 0.14,
"max": 0.28
},
{
"name": "la valeur est ferrée à DROITE : ce sont les fins de ligne qui s'alignent",
"measure": "cs('.vl-hst__val', 'textAlign') === 'right' ? 1 : 0",
"expect": 1,
"tol": 0
},
{
"name": "le vide entre le libellé et sa valeur est le vrai séparateur du couple",
"measure": "(box('.vl-hst__val').left - box('.vl-hst__lab').right) / W",
"min": 0.25
},
{
"name": "la valeur COURT sur plusieurs lignes — une énumération, pas une étiquette",
"measure": "box('.vl-hst__val').h / num('.vl-hst__val', 'fontSize')",
"min": 2.5
},
{
"name": "libellé et valeur PENDENT du filet : leurs premières lignes s'alignent",
"measure": "Math.abs(box('.vl-hst__lab').top - box('.vl-hst__val').top) / num('.vl-hst__lab', 'fontSize')",
"max": 0.2
},
{
"name": "libellé et valeur partagent le MÊME corps : aucune hiérarchie de taille",
"measure": "num('.vl-hst__lab', 'fontSize') / num('.vl-hst__val', 'fontSize')",
"expect": 1,
"tol": 0.001
},
{
"name": "le libellé tient le plancher de contraste du texte courant",
"measure": "contrast('.vl-hst__lab')",
"min": 4.5
},
{
"name": "la valeur aussi — c'est le même rouge, il doit passer aux deux endroits",
"measure": "contrast('.vl-hst__val')",
"min": 4.5
},
{
"name": "rien ne déborde en hauteur : chaque ligne contient son texte",
"measure": "overflow('.vl-hst')",
"expect": 0,
"tol": 0
}
],
"notes": "Ce pattern est le `table-hairline-rules` que la spec de ref-10 annonçait, et son seul geste est un DÉBORD que rien ne signale quand on le perd. La mécanique tient en une ligne : le retrait est porté par le `padding` de la LIGNE, jamais par ses cellules. Un `border-top` court sur toute la boîte de bordure alors que le padding rentre le contenu — mettez le retrait sur `.vl-hst__lab` et `.vl-hst__val` et les filets se rétractent avec le texte, ce qui est exactement le geste inverse : un bloc encadré posé sur la page au lieu d'une règle que la page traverse. Deux benchmarks le tiennent des deux côtés, parce que le débord de gauche seul se relit comme une simple indentation. Le reste du module est fait de renoncements, tous mesurés : pas de filet de fermeture en bas (il refermerait un cadre), pas de bordure verticale (les colonnes sont ferrées, pas dessinées), pas de fond. La respiration est volontairement asymétrique — 14 px au-dessus, 20 en dessous — pour que chaque couple se lise accroché à SON filet ; égalisées, les lignes flottent entre deux traits et l'appartenance se perd, sans qu'aucun alignement ne casse. Libellé et valeur sont au MÊME corps : la hiérarchie vient du fer et du vide, jamais de la taille. Le rouge est celui corrigé de la référence (#C82C15, 4,57:1 sur la crème) et non celui relevé sur la source (#E33A22, 3,57:1) : à 13 px il faut le plancher du texte courant, et les deux benchmarks de contraste sont là pour que la correction ne se reperde pas au premier remappage de charte. Sur la slide d'origine le module occupe 70 % de la largeur (1120 sur 1600) et son bord gauche est celui de la SLIDE — le débord gauche va donc jusqu'au bord perdu ; posé ailleurs, il lui faut au moins la marge de texte de sa page pour exister. La source montre deux couples, le fragment en pose trois : c'est la même ligne répétée, pas un élément ajouté, et c'est ce qui rend visible que le filet appartient à la ligne qu'il ouvre.",
"id": "list-07-hairline-spec-table"
}
À côté
list-01-giant-numbers
Colonnes à numéro géant
list-02-ruled-index
Sommaire à filets numérotés
list-03-two-column-toc
Sommaire numéroté 2 colonnes
list-04-due-rows
Liste d'échéances à quatre zones
list-05-vitals-strip