@zevra/ui 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/DECISIONS.md +293 -203
  2. package/README.md +21 -21
  3. package/demo/app.html +6 -6
  4. package/demo/index.html +3 -3
  5. package/dist/actions.d.ts +8 -8
  6. package/dist/actions.d.ts.map +1 -1
  7. package/dist/actions.js +1 -1
  8. package/dist/actions.js.map +1 -1
  9. package/dist/catalogue.d.ts +2 -2
  10. package/dist/catalogue.js +67 -67
  11. package/dist/catalogue.js.map +1 -1
  12. package/dist/content.d.ts +23 -23
  13. package/dist/content.d.ts.map +1 -1
  14. package/dist/content.js +16 -16
  15. package/dist/content.js.map +1 -1
  16. package/dist/css-inline.d.ts +1 -1
  17. package/dist/css-inline.d.ts.map +1 -1
  18. package/dist/css-inline.js +4 -4
  19. package/dist/css-inline.js.map +1 -1
  20. package/dist/data-table.d.ts +1 -1
  21. package/dist/data.d.ts +7 -7
  22. package/dist/data.js +5 -5
  23. package/dist/email-tokens.js +1 -1
  24. package/dist/email.d.ts +6 -6
  25. package/dist/email.js +8 -8
  26. package/dist/email.js.map +1 -1
  27. package/dist/feedback.d.ts +10 -10
  28. package/dist/feedback.d.ts.map +1 -1
  29. package/dist/feedback.js +3 -3
  30. package/dist/forms.d.ts +12 -12
  31. package/dist/forms.js +5 -5
  32. package/dist/glyph-data.d.ts +2 -2
  33. package/dist/glyph-data.js +3 -3
  34. package/dist/glyph-urls.js +1 -1
  35. package/dist/glyphes.d.ts +2 -2
  36. package/dist/glyphes.js +4 -4
  37. package/dist/icon-data.d.ts +1 -1
  38. package/dist/icon-data.js +2 -2
  39. package/dist/icon.d.ts +5 -5
  40. package/dist/icon.js +1 -1
  41. package/dist/index.js +4 -4
  42. package/dist/layout.d.ts +21 -21
  43. package/dist/layout.d.ts.map +1 -1
  44. package/dist/layout.js +15 -15
  45. package/dist/layout.js.map +1 -1
  46. package/dist/navigation.d.ts +16 -16
  47. package/dist/navigation.d.ts.map +1 -1
  48. package/dist/navigation.js +9 -9
  49. package/dist/navigation.js.map +1 -1
  50. package/dist/overlays.d.ts +6 -6
  51. package/dist/overlays.d.ts.map +1 -1
  52. package/dist/overlays.js +5 -5
  53. package/dist/select-menu.d.ts +1 -1
  54. package/dist/select-menu.js +2 -2
  55. package/dist/surfaces.d.ts +8 -8
  56. package/dist/surfaces.d.ts.map +1 -1
  57. package/dist/surfaces.js +6 -6
  58. package/dist/surfaces.js.map +1 -1
  59. package/dist/typography.d.ts +4 -4
  60. package/dist/typography.d.ts.map +1 -1
  61. package/dist/typography.js +3 -3
  62. package/dist/typography.js.map +1 -1
  63. package/icons/index.json +1 -1
  64. package/package.json +2 -2
  65. package/react/actions.tsx +9 -9
  66. package/react/catalogue.tsx +68 -68
  67. package/react/content.tsx +28 -28
  68. package/react/css-inline.ts +4 -4
  69. package/react/data-table.tsx +3 -3
  70. package/react/data.tsx +9 -9
  71. package/react/email-tokens.ts +1 -1
  72. package/react/email.ts +10 -10
  73. package/react/feedback.tsx +12 -12
  74. package/react/forms.tsx +15 -15
  75. package/react/glyph-data.ts +3 -3
  76. package/react/glyph-urls.ts +1 -1
  77. package/react/glyphes.ts +4 -4
  78. package/react/icon-data.ts +2 -2
  79. package/react/icon.tsx +6 -6
  80. package/react/index.ts +4 -4
  81. package/react/layout.tsx +22 -22
  82. package/react/navigation.tsx +18 -18
  83. package/react/overlays.tsx +9 -9
  84. package/react/select-menu.tsx +3 -3
  85. package/react/surfaces.tsx +10 -10
  86. package/react/typography.tsx +6 -6
  87. package/src/base.css +10 -10
  88. package/src/components.css +49 -49
  89. package/src/data.css +41 -41
  90. package/src/feedback.css +55 -55
  91. package/src/fonts.css +2 -2
  92. package/src/forms.css +50 -50
  93. package/src/icons.css +10 -10
  94. package/src/index.css +1 -1
  95. package/src/layout.css +27 -27
  96. package/src/motion.css +16 -16
  97. package/src/motion.js +4 -4
  98. package/src/nav.js +5 -5
  99. package/src/navigation.css +93 -62
  100. package/src/overlays.css +14 -14
  101. package/src/rail.js +6 -6
  102. package/src/surfaces.css +11 -11
  103. package/src/tokens.css +14 -14
  104. package/tokens/index.js +8 -8
package/DECISIONS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Arbitrages de relevé
2
2
 
3
3
  Les cinq maquettes de la passation ne s'accordent pas toujours. Ce fichier
4
- liste chaque désaccord, la valeur retenue et pourquoi — pour qu'aucun écart ne
4
+ liste chaque désaccord, la valeur retenue et pourquoi : pour qu'aucun écart ne
5
5
  soit tranché en silence, et que la DA puisse rejuger sur pièces.
6
6
 
7
7
  Sources : les dix planches de `reference/maquettes/` et
@@ -17,7 +17,7 @@ Les deux questions laissées ouvertes à la première livraison sont **closes**,
17
17
  et dans le sens qui avait été retenu. Les cinq planches ajoutées le
18
18
  confirment à l'unanimité.
19
19
 
20
- ### 1. Le sens du dégradé de signature — clos
20
+ ### 1. Le sens du dégradé de signature : clos
21
21
 
22
22
  | Source | Valeur |
23
23
  |---|---|
@@ -30,7 +30,7 @@ Les cinq nouvelles planches posent toutes `linear-gradient(90deg, #8a3af8,
30
30
  token est donc la bonne ; les trois planches historiques sont en retard, pas
31
31
  en désaccord.
32
32
 
33
- ### 2. Le rayon des cartes — clos
33
+ ### 2. Le rayon des cartes : clos
34
34
 
35
35
  Les cinq nouvelles planches codent `border-radius: 16px` sur `.panel`, soit
36
36
  `--r-card`. Le `14px` de DesignSystem et Logos et le `12px` du prototype
@@ -40,20 +40,20 @@ appartiennent à l'état antérieur.
40
40
 
41
41
  ## Tranché sans ambiguïté
42
42
 
43
- **Hauteur des boutons — 48px.** Main, Mobile et le prototype s'accordent ;
43
+ **Hauteur des boutons : 48px.** Main, Mobile et le prototype s'accordent ;
44
44
  DesignSystem est seule à 46px.
45
45
 
46
- **Icône du lien fléché — 15×15.** Quatre occurrences dans Main contre une à
46
+ **Icône du lien fléché : 15×15.** Quatre occurrences dans Main contre une à
47
47
  16px dans DesignSystem.
48
48
 
49
- **Pastille — 28px de haut, 10px de texte, 0.18em.** Main et le prototype
49
+ **Pastille (28px de haut, 10px de texte, 0.18em.** Main et le prototype
50
50
  s'accordent ; DesignSystem est plus grande (30/11), Mobile plus petite
51
- (26/0.16em — repris dans la déclinaison mobile).
51
+ (26/0.16em) repris dans la déclinaison mobile).
52
52
 
53
- **Puce de source — `--accent-deep`.** Main et le prototype ; DesignSystem pose
53
+ **Puce de source : `--accent-deep`.** Main et le prototype ; DesignSystem pose
54
54
  `--accent`.
55
55
 
56
- **Entité pseudonymisée — 10px, `1px 6px`.** Main et DesignSystem ; le
56
+ **Entité pseudonymisée : 10px, `1px 6px`.** Main et DesignSystem ; le
57
57
  prototype est plus grand (11px, `2px 7px`).
58
58
 
59
59
  **Les deux encarts ne sont pas le même composant.** `.zv-inset` (blanc,
@@ -77,17 +77,17 @@ Deux choix d'implémentation méritent d'être dits :
77
77
  view())`. Un navigateur sans support affiche la page complète, jamais un
78
78
  écran vide en attente d'une animation qui ne viendra pas ;
79
79
  - les deux boucles animent les propriétés `translate` et `scale`
80
- indépendantes, jamais `transform` — le halo est centré par
80
+ indépendantes, jamais `transform` : le halo est centré par
81
81
  `translateX(-50%)`, qu'une animation sur `transform` écraserait. La
82
82
  maquette contournait le problème en recopiant le `translateX(-50%)` dans
83
83
  chaque image-clé ; la propriété indépendante compose proprement.
84
84
 
85
85
  **Six blocs de plus.** Citation, carte de tarif, FAQ, aperçu d'interface,
86
- bande de preuve sociale, pied de page réduit — relevés dans `Blocks.html`,
86
+ bande de preuve sociale, pied de page réduit : relevés dans `Blocks.html`,
87
87
  qui liste les composants du site. Deux d'entre eux portent une règle
88
88
  éditoriale que le CSS ne peut pas faire respecter et qui est donc écrite en
89
89
  commentaire : **jamais de fausse citation**, et **jamais de lorem dans un
90
- aperçu d'interface** — c'est ce bloc qui porte la crédibilité de la page.
90
+ aperçu d'interface**, c'est ce bloc qui porte la crédibilité de la page.
91
91
 
92
92
  **Un palier typographique et une accroche.** `.zv-note` (12,5px) et
93
93
  `.zv-tagline`, la phrase d'accroche posée sous le nom d'un glyphe, moins
@@ -105,7 +105,7 @@ n'a été inventé pour combler l'écart.
105
105
  ## Ajouts assumés, hors relevé
106
106
 
107
107
  **Le point de rupture à 768px.** Les cinq maquettes sont à largeur fixe, sans
108
- une seule media query — Mobile.html est un rendu figé à 390px. Le seuil vient
108
+ une seule media query : Mobile.html est un rendu figé à 390px. Le seuil vient
109
109
  de la convention `md` du projet ; les valeurs mobiles, elles, sont toutes
110
110
  relevées dans Mobile.html. Un test vérifie qu'il reste unique.
111
111
 
@@ -135,20 +135,20 @@ production, qui emploie les mêmes tokens de charte. Trois arbitrages de fond
135
135
  ont été rendus :
136
136
 
137
137
  **Les neutres.** Mémoire emploie six niveaux de gris là où la charte en a
138
- trois, et deux d'entre eux — #6B7488 et #727C93 — **échouent au contraste AA**
138
+ trois, et deux d'entre eux, #6B7488 et #727C93, **échouent au contraste AA**
139
139
  sur texte normal (4,34 et 3,87), alors qu'ils portent justement le texte mono
140
140
  de 10,5px : latences, horodatages, compteurs. Les adopter aurait gravé une
141
141
  régression d'accessibilité. Ils se rabattent sur `--muted` (5,55). Un seul
142
142
  niveau intermédiaire est ajouté, `--ink-2` (#4a5468, contraste 7,05).
143
143
 
144
144
  **L'échelle de texte.** Le relevé compte huit paliers répartis dans 5,5px,
145
- avec des demi-pixels et des écarts d'un pixel — de l'entropie d'utilitaires,
145
+ avec des demi-pixels et des écarts d'un pixel : de l'entropie d'utilitaires,
146
146
  pas une échelle dessinée. Ils se regroupent en trois paliers de texte
147
147
  (`--app-lg`, `--app`, `--app-sm`) et trois de labels (`--mono-lg`, `--mono`,
148
148
  `--mono-sm`).
149
149
 
150
150
  **Les filets et surfaces.** Cinq « nouveaux » tokens étaient des doublons
151
- imperceptibles de tokens existants — écart RVB de 1,4 à 6,8, sous le seuil de
151
+ imperceptibles de tokens existants : écart RVB de 1,4 à 6,8, sous le seuil de
152
152
  perception. `--line-inner` → `--hairline`, `--line-soft` → `--line`,
153
153
  `--row-hover` / `--paper-hover` / `--paper-warm` → `--panel`.
154
154
 
@@ -159,7 +159,7 @@ Au total, 32 tokens proposés se réduisent à 14.
159
159
  **La barre supérieure.** La production pose `top: 53px` pour une barre de
160
160
  52px, en supposant que son filet s'ajoute à la hauteur. Il ne s'y ajoute
161
161
  pas : `box-sizing: border-box` le peint à l'intérieur. Ce pixel n'empilait
162
- rien, il ouvrait une couture — et la feuille portant son propre filet haut,
162
+ rien, il ouvrait une couture : et la feuille portant son propre filet haut,
163
163
  le trait apparaissait doublé. Corrigé : la feuille se cale à ras, sans filet
164
164
  propre.
165
165
 
@@ -173,7 +173,7 @@ n'importe quelle police.
173
173
  `--muted`. La nuance de température disparaît ; elle n'était reconductible
174
174
  nulle part ailleurs.
175
175
 
176
- ### Enrobages — hors relevé, assumés
176
+ ### Enrobages : hors relevé, assumés
177
177
 
178
178
  Trois composants n'existent dans aucune source et sont pourtant livrés, car
179
179
  leur absence rend le système inutilisable :
@@ -186,7 +186,7 @@ d'atteindre sa navigation. Le tracé du burger est dérivé ; les mesures
186
186
 
187
187
  **La modale.** Mémoire n'en emploie aucune. Celle-ci est dérivée de la carte
188
188
  et du tiroir, sans une valeur nouvelle. Elle existe parce que la règle du
189
- projet en impose la forme — panneau plafonné à 92vh qui défile en son sein —
189
+ projet en impose la forme, panneau plafonné à 92vh qui défile en son sein,
190
190
  et qu'à défaut chaque app en réinventerait une.
191
191
 
192
192
  **La notification flottante.** Mémoire signale tout en place, par une alerte
@@ -207,7 +207,7 @@ des puces de filtre ou un interrupteur. Ces trois contrôles sont livrés,
207
207
  dérivés strictement de `.zv-input`, et signalés comme tels dans le CSS.
208
208
 
209
209
  Aucun jeu d'onglets non plus : une rangée d'onglets se construit avec
210
- `.zv-seg-group` et `.zv-seg`. Un onglet **souligné** reste à faire — il
210
+ `.zv-seg-group` et `.zv-seg`. Un onglet **souligné** reste à faire : il
211
211
  suppose d'arbitrer la couleur et l'épaisseur du trait actif.
212
212
 
213
213
  ## Les logos, ramenés à cinq
@@ -218,8 +218,8 @@ Mémoire et de la marque Zevra.
218
218
 
219
219
  Les déclinaisons ont été retirées parce qu'elles ne suivaient plus les
220
220
  marques. Le cas de Mémoire le montrait sans discussion : `memoire-glyph-navy`
221
- et ses quatre lockups portaient encore un logo abandonné — une forme de ruban
222
- plié — quand `ink`, `white` et `orig` portaient déjà le M actuel. Deux
221
+ et ses quatre lockups portaient encore un logo abandonné, une forme de ruban
222
+ plié, quand `ink`, `white` et `orig` portaient déjà le M actuel. Deux
223
223
  marques cohabitaient dans la même galerie, et rien ne disait laquelle était
224
224
  la bonne.
225
225
 
@@ -229,7 +229,7 @@ Ce que cela coûte, et qui reste à refaire :
229
229
  24 px ;
230
230
  - **les lockups** avec wordmark, pour les barres de navigation et les pieds
231
231
  de page ;
232
- - **l'inlinage vectoriel**. Un PNG s'inline en data-URI — l'admin la donne —
232
+ - **l'inlinage vectoriel**. Un PNG s'inline en data-URI, l'admin la donne,
233
233
  mais c'est 150 ko de texte là où un SVG en faisait 4.
234
234
 
235
235
  Les fichiers retirés restent dans l'historique du dépôt : rien n'est perdu,
@@ -253,7 +253,7 @@ Ces éléments restent consultables dans `reference/`.
253
253
  dur. Même chose pour `#8fd3a8` / `#f0a5ad`, les états sur fond sombre.
254
254
 
255
255
  Trois rayons de surface pour un seul token : `--r-card` (16), la puce de
256
- source (4px) et le rayon 18px de la carte verre de DesignSystem — ce dernier
256
+ source (4px) et le rayon 18px de la carte verre de DesignSystem, ce dernier
257
257
  ramené à `--r-card`, comme le fait Mobile.html.
258
258
 
259
259
 
@@ -274,7 +274,7 @@ spectre. À rejuger sur pièces si la DA en décide autrement.
274
274
 
275
275
  ### Colonnes 5 et 7 (0.5.0)
276
276
 
277
- Le commentaire de `layout.css` relevait « 3+9, 8+4, 6+6 » — mais le
277
+ Le commentaire de `layout.css` relevait « 3+9, 8+4, 6+6 » : mais le
278
278
  prototype `experience.html` compose toutes ses scènes produit en `span 5`
279
279
  (glyphe) + `span 7` (discours), et c'est cette répartition qui équilibre
280
280
  l'air entre le visuel et le texte. `zv-col-5` et `zv-col-7` complètent donc
@@ -284,46 +284,46 @@ le relevé ; en 4+8, le texte nage et le glyphe s'exile au bord.
284
284
 
285
285
  ## L'angle vif (25/08/2026)
286
286
 
287
- « Très carré, ça fait pro et inspire confiance » — arbitrage DA rendu sur
287
+ « Très carré, ça fait pro et inspire confiance » : arbitrage DA rendu sur
288
288
  pièces. Les quatre tokens de rayon (`--r-btn`, `--r-pill`, `--r-panel`,
289
289
  `--r-card`) sont ramenés à `0` et RESTENT nommés : rendre de la rondeur à
290
290
  l'une des quatre intentions doit rester une ligne à changer, pas une chasse
291
- dans douze fichiers. Seul survit le rond de FORME — pastille d'état, radio,
292
- halo, bouton d'interrupteur exclu (passé au carré), roue incluse — parce
291
+ dans douze fichiers. Seul survit le rond de FORME (pastille d'état, radio,
292
+ halo, bouton d'interrupteur exclu (passé au carré), roue incluse) parce
293
293
  qu'il porte du sens là où le rayon ne portait que du style.
294
294
 
295
- Cet arbitrage SUCCÈDE au « rayon des cartes — clos » ci-dessus : la
295
+ Cet arbitrage SUCCÈDE au « rayon des cartes, clos » ci-dessus : la
296
296
  question tranchée là était la fidélité au relevé (16px contre 14 et 12),
297
297
  celle-ci est une décision de DA qui lui passe devant.
298
298
 
299
299
  ---
300
300
 
301
- ## Compléments du 25/08/2026 (0.9.0) — les manques comblés, hors relevé
301
+ ## Compléments du 25/08/2026 (0.9.0) : les manques comblés, hors relevé
302
302
 
303
303
  La demande : compléter le système au niveau d'une bibliothèque de référence
304
304
  (états d'interaction, contrôles absents, formats), en restant dans la charte.
305
- Tout ce qui suit est **HORS RELEVÉ** — dessiné dans les tokens existants,
306
- chaque écart dit — et se rejuge sur pièces avec la DA. Un seul token entre
305
+ Tout ce qui suit est **HORS RELEVÉ** (dessiné dans les tokens existants,
306
+ chaque écart dit) et se rejuge sur pièces avec la DA. Un seul token entre
307
307
  (`--font-code`, voir « Bloc de code ») ; aucune couleur nouvelle.
308
308
 
309
309
  ### Les états de survol et d'appui des boutons
310
310
 
311
- Aucune maquette ne donne d'état `:hover` ni `:active` aux trois boutons —
311
+ Aucune maquette ne donne d'état `:hover` ni `:active` aux trois boutons :
312
312
  elles sont figées. Un bouton sans réaction se lit comme un décor (l'argument
313
313
  qui a déjà tranché pour `.zv-chrome-btn`). Arbitrages :
314
314
 
315
315
  - **le survol assombrit, l'appui assombrit davantage et rassoit** : les
316
316
  teintes sont dérivées par `color-mix` (l'idiome que le voile emploie
317
- déjà) des couleurs que le bouton porte déjà — le sommet de dégradé relevé
317
+ déjà) des couleurs que le bouton porte déjà, le sommet de dégradé relevé
318
318
  (`--zv-btn-haut`, #2b2bb4, sans token) et `--accent-deep`. Aucune teinte
319
319
  nouvelle n'entre ;
320
320
  - à l'appui, l'ombre TOMBE et le liseré s'éteint : le bouton se rassoit sur
321
- la page au lieu de flotter — aucun déplacement, c'est la lumière qui dit
321
+ la page au lieu de flotter (aucun déplacement, c'est la lumière qui dit
322
322
  l'enfoncement ;
323
323
  - le fantôme prend le filet `--accent` et l'appui pose `--accent-soft` en
324
324
  fond ; l'encre s'éclaircit d'un cran (un aplat sombre ne peut pas
325
325
  s'assombrir visiblement) ;
326
- - une seule transition, la courbe maison, 160 ms — coupée par
326
+ - une seule transition, la courbe maison, 160 ms) coupée par
327
327
  `prefers-reduced-motion` (base.css) ;
328
328
  - **aucun déplacement** : pas de translateY. Les entrées du système montent
329
329
  au défilement ; un bouton qui saute au survol raconterait autre chose.
@@ -333,7 +333,7 @@ qui a déjà tranché pour `.zv-chrome-btn`). Arbitrages :
333
333
  La modale destructive porte son filet `--err`, mais son action de
334
334
  confirmation restait un plein indigo : « Supprimer » s'y lisait comme
335
335
  n'importe quelle action. `zv-btn--danger` pose l'aplat `--err`, sans
336
- dégradé ni ombre indigo. **Il compte comme LE bouton plein de l'écran** — la
336
+ dégradé ni ombre indigo. **Il compte comme LE bouton plein de l'écran** : la
337
337
  règle « un seul plein par page » ne distingue pas les couleurs, elle compte
338
338
  les aplats.
339
339
 
@@ -341,15 +341,15 @@ les aplats.
341
341
 
342
342
  Relevé de Mémoire, cette fois (le motif de balisage documenté dans
343
343
  feedback.css) : pendant l'action, le libellé change (« Connexion… ») et le
344
- bouton passe en `cursor-wait; opacity: .7` — valeurs reprises telles
345
- quelles. Le paquet le code sur `[aria-busy="true"]` — l'attribut qui rend
346
- l'attente audible ne coûte alors rien — et la façade React l'expose en prop
344
+ bouton passe en `cursor-wait; opacity: .7` : valeurs reprises telles
345
+ quelles. Le paquet le code sur `[aria-busy="true"]`, l'attribut qui rend
346
+ l'attente audible ne coûte alors rien, et la façade React l'expose en prop
347
347
  `occupe`, qui pose `aria-disabled` et AVALE le clic plutôt que de poser
348
348
  `disabled` : un `disabled` sortirait le bouton du parcours clavier au
349
349
  moment précis où l'attente doit s'annoncer. Mémoire, elle, désactive ; cet
350
350
  écart-là est assumé.
351
351
 
352
- ### L'onglet souligné — l'arbitrage attendu
352
+ ### L'onglet souligné : l'arbitrage attendu
353
353
 
354
354
  La question ouverte depuis le relevé Mémoire est tranchée ainsi :
355
355
 
@@ -358,7 +358,7 @@ La question ouverte depuis le relevé Mémoire est tranchée ainsi :
358
358
  couleur de l'action, comme l'entrée de nav courante. PAS le spectre : un
359
359
  quatrième emploi n'existe pas.
360
360
  - l'onglet inactif est `--ink-2` graisse 500, l'actif `--accent` graisse
361
- 600 — la grammaire exacte de `.zv-seg` ;
361
+ 600, la grammaire exacte de `.zv-seg` ;
362
362
  - la rangée porte un filet bas `--line` d'un pixel que le trait actif
363
363
  recouvre : l'onglet se lit comme posé SUR une ligne, pas souligné en l'air.
364
364
 
@@ -367,39 +367,39 @@ module ne livre rien ici » réservait. La rangée de puces (`Tabs` en variante
367
367
  par défaut) reste le motif des filtres ; l'onglet souligné est celui des
368
368
  VUES d'un même objet (Aperçu / Sources).
369
369
 
370
- ### Roue, squelette, infobulle — les trois manques assumés, livrés
370
+ ### Roue, squelette, infobulle : les trois manques assumés, livrés
371
371
 
372
372
  feedback.css les refusait faute de matière première, et posait les
373
373
  contraintes à respecter « le jour où la DA les tranche ». Ce jour : les
374
374
  contraintes sont reprises telles quelles, les valeurs restantes sont
375
375
  arbitrées ici.
376
376
 
377
- - **La roue** (`zv-spinner`) : anneau de 16 px — la taille de `.zv-icon` —
377
+ - **La roue** (`zv-spinner`) : anneau de 16 px, la taille de `.zv-icon`,
378
378
  trait de 2 (l'épaisseur du jeu d'icônes), piste `--accent-line`, secteur
379
379
  `--accent`, rotation 0,8 s linéaire. La variante `--lg` fait 24 px, la
380
380
  grille du jeu d'icônes. Elle ne REMPLACE pas `.zv-loading` : la phrase
381
381
  qui dit le travail reste le motif par défaut, la roue est pour l'attente
382
382
  courte et locale (un bouton, une cellule). `prefers-reduced-motion` la
383
- fige — l'anneau interrompu reste lisible immobile. Son libellé d'annonce
383
+ fige : l'anneau interrompu reste lisible immobile. Son libellé d'annonce
384
384
  est un texte masqué (`.zv-sr-only`, base.css), pas un `aria-label` sur un
385
385
  span vide, que tous les lecteurs n'annoncent pas.
386
- - **Le squelette** (`zv-skeleton`) : `--hairline`, comme exigé — pas de
386
+ - **Le squelette** (`zv-skeleton`) : `--hairline`, comme exigé, pas de
387
387
  balayage spectral. Pulsation d'OPACITÉ (1 → 0,55, 1,8 s) : animer la
388
388
  couleur ferait un dégradé de gris que la charte n'a pas. La géométrie
389
389
  vient du consommateur (largeur/hauteur en style) ; seule la variante
390
390
  `--text` fixe une hauteur d'un cadratin de texte courant.
391
- - **L'infobulle** (`zv-tooltip`) : la combinaison imposée — panneau blanc,
392
- filet `--line`, ombre `--shadow-menu`, texte `--app-sm`/`--ink-2` —
391
+ - **L'infobulle** (`zv-tooltip`) : la combinaison imposée (panneau blanc,
392
+ filet `--line`, ombre `--shadow-menu`, texte `--app-sm`/`--ink-2`)
393
393
  posée AU-DESSUS de la cible, centrée, à 6 px (le pointeur vient d'en bas
394
394
  et la masquerait dessous ; `--bas` existe pour le rare cas inverse),
395
395
  plafonnée à 240 px et, sous le seuil, à l'écran. L'apparition attend
396
- 300 ms — sans quoi traverser une rangée d'icônes fait clignoter une bulle
397
- par icône — la disparition est immédiate. Trois exigences de WCAG 1.4.13
396
+ 300 ms, sans quoi traverser une rangée d'icônes fait clignoter une bulle
397
+ par icône, la disparition est immédiate. Trois exigences de WCAG 1.4.13
398
398
  sont tenues : elle reste tant que le pointeur ou le focus y sont, elle
399
399
  est survolable (le masquage par `visibility` évite les zones fantômes),
400
- et Échap la ferme — la seule ligne de script du composant, posée sans
400
+ et Échap la ferme, la seule ligne de script du composant, posée sans
401
401
  état. L'annonce passe par `aria-describedby`, câblée en identifiants
402
- EXPLICITES comme le couple `htmlFor`/`id` de `Field` — pas de `useId`,
402
+ EXPLICITES comme le couple `htmlFor`/`id` de `Field` : pas de `useId`,
403
403
  qui interdirait le rendu en composant serveur. Réservée à un COMPLÉMENT
404
404
  dispensable : une commande n'a pas le droit de cacher son nom dans une
405
405
  infobulle.
@@ -411,7 +411,7 @@ Aucune source. Dérivé de l'interrupteur redessiné : bouton CARRÉ de 14 px
411
411
  `--accent-deep` au survol ; piste de 3 px (la hauteur de `.zv-progress`),
412
412
  `--line`, remplie en `--accent` jusqu'au curseur. **Pas le spectre** : la
413
413
  barre de progression est un des trois emplois, le curseur n'en est pas un.
414
- Le remplissage se calcule dans la façade (pourcentage de position — une
414
+ Le remplissage se calcule dans la façade (pourcentage de position : une
415
415
  géométrie, comme la largeur de `Progress`, pas une valeur de charte).
416
416
 
417
417
  ### L'affixe de champ (`zv-affix`)
@@ -419,14 +419,14 @@ géométrie, comme la largeur de `Progress`, pas une valeur de charte).
419
419
  « 30 », seul dans un champ, ne dit pas s'il s'agit de jours ou d'euros. La
420
420
  grammaire est celle de `.zv-search`, le seul cadre-à-champ-nu du relevé :
421
421
  cadre `--line`, champ nu dedans, allumage `:focus-within` identique.
422
- L'affixe est en mono `--label` — une unité est un libellé, pas une donnée.
422
+ L'affixe est en mono `--label` : une unité est un libellé, pas une donnée.
423
423
 
424
424
  ### Le dépliant (`zv-fold`)
425
425
 
426
426
  La FAQ est le pliage de la vitrine ; l'applicatif n'en avait aucun (options
427
427
  avancées, détail d'une ligne de journal). Construit sur `<details>` natif :
428
428
  clavier et annonce viennent du navigateur, aucun JavaScript. Rangé dans
429
- data.css — c'est du contenu qui se déplie, pas de la navigation. Le chevron
429
+ data.css : c'est du contenu qui se déplie, pas de la navigation. Le chevron
430
430
  tourne, seule rotation du module, coupée par `prefers-reduced-motion`.
431
431
 
432
432
  ### La touche (`zv-kbd`) et le bloc de code (`zv-codeblock`)
@@ -435,11 +435,11 @@ Une documentation d'interface cite des raccourcis et des commandes ; le
435
435
  paquet n'avait ni l'un ni l'autre.
436
436
 
437
437
  - La touche : `--panel`, filet `--line` avec un bas de 2 px qui fait le
438
- relief de la touche — géométrie, pas ombre.
438
+ relief de la touche, géométrie, pas ombre.
439
439
  - **Le bloc de code introduit le seul token nouveau, `--font-code`**
440
440
  (`ui-monospace / SF Mono / Menlo`). La raison est déjà écrite deux fois
441
441
  dans le paquet : `--font-mono` (Space Grotesk) n'est PAS à chasse fixe, et
442
- `tabular-nums` n'aligne que les chiffres — du code exige la chasse fixe
442
+ `tabular-nums` n'aligne que les chiffres, du code exige la chasse fixe
443
443
  entière. Détourner `--font-mono` aurait menti ; un hexadécimal de pile de
444
444
  polices en dur dans data.css aurait été la première valeur de charte hors
445
445
  tokens. Le token est reflété dans le miroir JS, comme les autres.
@@ -454,7 +454,7 @@ extérieur et Échap : au composant, comme pour le menu.
454
454
 
455
455
  ### Le carrousel (`zv-carousel`)
456
456
 
457
- La bande de logos de la landing défile déjà — sans composant, chaque page
457
+ La bande de logos de la landing défile déjà : sans composant, chaque page
458
458
  réinvente le sien. Défilement natif `scroll-snap`, AUCUN JavaScript : pas de
459
459
  flèches (elles exigeraient un script et un arbitrage de dessin), pas de
460
460
  points. La barre de défilement est masquée, le clavier passe par le focus du
@@ -464,7 +464,7 @@ préfère une grille tant que le contenu tient.
464
464
  ### Le bandeau d'annonce (`zv-banner`)
465
465
 
466
466
  La bande au-dessus de la barre de navigation (annonce de version,
467
- information réglementaire). `--accent-soft` sur filet `--accent-line` — le
467
+ information réglementaire). `--accent-soft` sur filet `--accent-line` : le
468
468
  ton information de l'alerte, en pleine largeur. Un seul par page, et
469
469
  JAMAIS pour du marketing permanent : un bandeau qui ne part pas devient un
470
470
  bourrelet de chrome.
@@ -475,13 +475,13 @@ Mémoire navigue au tiroir ; une application à trois ou quatre sections
475
475
  navigue plus vite par une barre basse. N'existe que SOUS le point de
476
476
  rupture (au-dessus, la colonne latérale est là), cibles `--tap`, icône +
477
477
  micro-label, état courant `--accent` sur `aria-current="page"`. Cinq
478
- entrées au maximum — au-delà, c'est le tiroir.
478
+ entrées au maximum : au-delà, c'est le tiroir.
479
479
 
480
480
  ### Les formats sociaux (`zv-social`)
481
481
 
482
482
  Les visuels LinkedIn/OpenGraph se refaisaient à la main, hors charte. Trois
483
- gabarits FIGÉS en pixels — paysage 1200×630 (OpenGraph et lien LinkedIn),
484
- carré 1080×1080 (post), story 1080×1920 — qui ne répondent à aucun point de
483
+ gabarits FIGÉS en pixels (paysage 1200×630 (OpenGraph et lien LinkedIn),
484
+ carré 1080×1080 (post), story 1080×1920) qui ne répondent à aucun point de
485
485
  rupture : ce sont des planches d'export, pas des pages. Ils composent les
486
486
  pièces existantes (bande manifeste, kicker, halo) et portent un pied
487
487
  wordmark + adresse. La règle « jamais de faux contenu » s'y applique au
@@ -491,19 +491,19 @@ premier chef : un visuel social EST une publication.
491
491
 
492
492
  Le jeu passe de 34 à 55. Les règles du jeu ne bougent pas : trait 2, bouts
493
493
  et jonctions arrondis, grille 24, `currentColor`. Les ajouts comblent les
494
- trous les plus fréquents — `chevron-haut` manquait au chevron près, et le
495
- catalogue citait `telecharger` en exemple sans qu'elle existe — plus le
494
+ trous les plus fréquents (`chevron-haut` manquait au chevron près, et le
495
+ catalogue citait `telecharger` en exemple sans qu'elle existe) plus le
496
496
  vocabulaire du métier : balance, livre-ouvert, diplome. Aucune icône
497
497
  pleine, aucune couleur.
498
498
 
499
499
  ### La micro-entrée des blocs
500
500
 
501
501
  Demande DA du 25/08 au soir : les blocs des pages INTÉRIEURES doivent
502
- entrer en mouvement, « pas aussi poussé que sur la home — ça peut être
502
+ entrer en mouvement, « pas aussi poussé que sur la home, ça peut être
503
503
  vite chiant à la longue ». Ce n'est PAS un dixième mouvement, c'est un
504
504
  DOSAGE du premier : `zv-anim-rise--doux` reprend `zv-rise` à douze pixels
505
505
  au lieu de trente-deux, sur une plage plus courte (5 → 30 % au lieu de
506
- 10 → 45 %) — le bloc est posé avant que l'œil ait fini d'arriver, et une
506
+ 10 → 45 %), le bloc est posé avant que l'œil ait fini d'arriver, et une
507
507
  visite répétée ne le remarque plus. Mêmes règles que les neuf : la courbe
508
508
  unique, l'état au repos est l'état final, rien hors du bloc `@supports`,
509
509
  coupé par `prefers-reduced-motion`. La home garde l'entrée pleine ; c'est
@@ -511,7 +511,7 @@ elle la signature, le doux est le quotidien.
511
511
 
512
512
  - **Le code de vérification (OTP)** : aucun écran du groupe n'en pose. Le
513
513
  livrer supposerait d'inventer la gestion de focus inter-cases sans aucune
514
- source — le jour où un écran d'authentification à code existe, c'est lui
514
+ source : le jour où un écran d'authentification à code existe, c'est lui
515
515
  qui tranche.
516
516
  - **Le sélecteur de date** : le natif (`.zv-input` sur `type="date"`) reste
517
517
  le motif ; un calendrier maison est un projet, pas un complément.
@@ -525,32 +525,32 @@ elle la signature, le doux est le quotidien.
525
525
  ### Le jeu d'icônes prend la signature de la charte
526
526
 
527
527
  L'angle vif du 25/08 s'était arrêté aux surfaces : le TRAIT des icônes
528
- restait « bouts et jonctions arrondis » — le dialecte de toutes les
528
+ restait « bouts et jonctions arrondis », le dialecte de toutes les
529
529
  bibliothèques génériques. Un système qui vend le carré ne peut pas parler
530
530
  rond dans ses pictogrammes. Arbitrage :
531
531
 
532
532
  - **bouts FRANCS** (`square` : même emprise optique que le rond, la
533
533
  silhouette change sans redessiner un seul tracé) et **jonctions VIVES**
534
- (`miter`) — le réglage vit dans icons.css, les 55 tracés le suivent d'un
534
+ (`miter`), le réglage vit dans icons.css, les 55 tracés le suivent d'un
535
535
  coup ;
536
536
  - **les points d'encre passent au carré** : trois-points, ponctuation
537
537
  d'info/aide/attention, taquets du registre. Dans une icône, le point est
538
- une ponctuation, pas un état — le rond de FORME (pastille, radio) n'est
538
+ une ponctuation, pas un état (le rond de FORME (pastille, radio) n'est
539
539
  pas concerné ;
540
540
  - **les coins de rectangles tombent à zéro** (rx=0) : archiver, cadenas,
541
541
  calendrier, copier, courriel, imprimante, registre ;
542
- - ce qui ne change PAS : les courbes de DESSIN — l'épaule du dossier, la
542
+ - ce qui ne change PAS : les courbes de DESSIN) l'épaule du dossier, la
543
543
  panse de la base de données, les cercles d'objet (horloge, globe,
544
544
  cocarde d'info). Le carré est un style de trait, pas une interdiction de
545
545
  courbe.
546
546
 
547
- Portée : tout ce qui trace dans le paquet suit — le chevron du dépliant,
547
+ Portée : tout ce qui trace dans le paquet suit, le chevron du dépliant,
548
548
  l'icône de dépôt, la coche de la pastille et celle de la case, le burger de
549
549
  la vitrine. Le burger applicatif, relevé « extrémités franches », y était
550
550
  déjà : le relevé avait la bonne intuition avant l'arbitrage.
551
551
 
552
552
  **Correction embarquée** : `.zv-link-arrow svg` n'avait jamais déclaré son
553
- trait — ni `fill: none`, ni `stroke`. La flèche se rendait en TRIANGLE
553
+ trait, ni `fill: none`, ni `stroke`. La flèche se rendait en TRIANGLE
554
554
  PLEIN noir (le fût, aire nulle, disparaissait), pas dans la couleur du
555
555
  lien. Le commentaire promettait le tracé depuis le premier jour ; le CSS le
556
556
  pose enfin, au trait carré.
@@ -558,13 +558,13 @@ pose enfin, au trait carré.
558
558
  ### Les boutons à icône
559
559
 
560
560
  `icone` / `iconeApres` sur Button et ButtonLink : l'icône vient du jeu, à
561
- 16 px, héritant de la couleur du libellé — la fournir en prop évite les
561
+ 16 px, héritant de la couleur du libellé, la fournir en prop évite les
562
562
  tracés recopiés à la main (la faute qui avait produit trois flèches
563
- différentes). `carre` est le bouton d'action à icône SEULE — 48×48, 40×40
564
- en compact — pour l'envoi d'un composeur ou l'action unique d'une barre.
563
+ différentes). `carre` est le bouton d'action à icône SEULE (48×48, 40×40
564
+ en compact) pour l'envoi d'un composeur ou l'action unique d'une barre.
565
565
  Le libellé accessible y est OBLIGATOIRE et s'écrit pour l'ACTION. Ne pas
566
566
  confondre : `IconButton` est le chrome nu (40 px, sans cadre),
567
- `ChromeButton` la croix bordée (26 px) — le carré est une action, il en a
567
+ `ChromeButton` la croix bordée (26 px), le carré est une action, il en a
568
568
  le poids et l'aplat.
569
569
 
570
570
  ### La tuile d'icône (`zv-icon-tile`)
@@ -572,13 +572,13 @@ le poids et l'aplat.
572
572
  La pastille carrée qui porte une icône devant un argument de la vitrine
573
573
  (liste de garanties, grille d'atouts) : 40 px, fond `--accent-soft`, filet
574
574
  `--accent-line`, icône `--accent`. HORS RELEVÉ, dessinée dans les tokens.
575
- Décorative par défaut — c'est le libellé voisin qui nomme. Une par
575
+ Décorative par défaut : c'est le libellé voisin qui nomme. Une par
576
576
  argument, jamais en décor isolé.
577
577
 
578
578
  ### L'icône dans la colonne latérale
579
579
 
580
580
  Aucune prop nouvelle : `num` sur SideLink accepte déjà « rang mono ou
581
- glyphe » — le relevé Mémoire y posait le ⚙ du panneau admin, et la colonne
581
+ glyphe », le relevé Mémoire y posait le ⚙ du panneau admin, et la colonne
582
582
  fixe de 16 px tient l'alignement. Ce qui manquait, c'était l'EXEMPLE : le
583
583
  catalogue montre désormais `num={<Icon nom="reglages" />}`. Rang OU icône,
584
584
  jamais les deux.
@@ -593,7 +593,7 @@ refus est argumenté et écrit : il tient, la fournée ne le rouvre pas.
593
593
 
594
594
  Constat d'usage sur le site vitrine : les entrées pilotées par
595
595
  `animation-timeline: view()` rejouaient À L'ENVERS dès que le lecteur
596
- remontait — les vidéos, les cartes d'articles, les grilles entières
596
+ remontait, les vidéos, les cartes d'articles, les grilles entières
597
597
  disparaissaient sous ses yeux, y compris en pleine lecture d'un article. Et
598
598
  une grille haute, jamais « contenue » dans le viewport, n'atteignait jamais
599
599
  son état posé : la section paraissait vide.
@@ -601,7 +601,7 @@ son état posé : la section paraissait vide.
601
601
  Arbitrage : **une révélation par visite**. Les sept entrées deviennent des
602
602
  animations temporelles courtes, armées par `html.zv-motion` (posé par
603
603
  `motion.js`, importable en `@zevra/ui/motion.js`) et libérées par `.zv-in`
604
- à l'entrée au viewport — une fois, puis plus jamais. Sans JavaScript, rien
604
+ à l'entrée au viewport : une fois, puis plus jamais. Sans JavaScript, rien
605
605
  n'est armé : la page s'affiche complète, même dégradation douce qu'avant.
606
606
  Les @keyframes remontent au niveau racine pour qu'un échafaudage de scènes
607
607
  (l'expérience épinglée d'une home) puisse les re-brancher sur ses timelines
@@ -609,12 +609,12 @@ nommées : il pose `data-zv-motion-skip` sur ses scènes et motion.js n'y
609
609
  touche pas.
610
610
 
611
611
  Ce qui RESTE scroll-piloté, et doit le rester : la barre de progression
612
- (`zv-anim-progress`) et le point d'étape (`zv-anim-dot-active`) — suivre la
612
+ (`zv-anim-progress`) et le point d'étape (`zv-anim-dot-active`), suivre la
613
613
  position de lecture est leur raison d'être, avancer et reculer avec elle
614
614
  est leur contrat.
615
615
 
616
616
  Corrigé du même mouvement : dans `.zv-band`, l'emphase `strong` de
617
- zv-body/zv-lead posait l'encre (--ink) sur le navy — invisible. Elle hérite
617
+ zv-body/zv-lead posait l'encre (--ink) sur le navy, invisible. Elle hérite
618
618
  désormais de la couleur ambiante, la graisse seule la distingue.
619
619
 
620
620
  ## La page a une largeur (27/08/2026, 0.12.4)
@@ -622,22 +622,22 @@ désormais de la couleur ambiante, la graisse seule la distingue.
622
622
  Le relevé de la passation vivait sur une trame de 1440 et les enveloppes
623
623
  n'étaient que du retrait : `zv-gutter` posait 72 px de padding et laissait
624
624
  le contenu suivre l'écran. Sur un moniteur large, le hero collait au bord
625
- gauche pendant que la nav — bornée localement par le site vitrine — restait
625
+ gauche pendant que la nav, bornée localement par le site vitrine, restait
626
626
  centrée : deux largeurs de vérité sur la même page.
627
627
 
628
628
  Arbitrage : un token `--page-max` (1440px, la trame du relevé) et les trois
629
629
  enveloppes de gutter se centrent au-delà ; `zv-inset-x` garde ses 72 px de
630
630
  marge minimale ET se centre dès que l'écran offre plus (max()). En dessous
631
- de 1440, rien ne bouge — le rendu du relevé est inchangé au pixel. La
631
+ de 1440, rien ne bouge : le rendu du relevé est inchangé au pixel. La
632
632
  rustine locale du site vitrine devient inutile.
633
633
 
634
634
  ## Le mouvement baisse d'un ton (27/08/2026, 0.12.5)
635
635
 
636
- Retour d'usage : au défilement, les entrées « poussaient » trop — l'effet
636
+ Retour d'usage : au défilement, les entrées « poussaient » trop, l'effet
637
637
  de parallaxe prenait le pas sur la lecture. Les amplitudes sont divisées
638
638
  par deux, les intentions conservées : zv-rise 32 → 16 px, son dosage doux
639
639
  12 → 8 px, le glyphe 40 px/0,70 → 24 px/0,85, le pop 0,60 → 0,80. La
640
- convergence de la home et le filet ne bougent pas — l'une est la signature,
640
+ convergence de la home et le filet ne bougent pas : l'une est la signature,
641
641
  l'autre ne translate rien. Les durées et la courbe restent celles de la
642
642
  passation : c'est la distance qui fatiguait, pas le tempo.
643
643
 
@@ -645,8 +645,8 @@ passation : c'est la distance qui fatiguait, pas le tempo.
645
645
 
646
646
  Deux produits à venir reçoivent leur glyphe : le P de Plume (l'éditeur
647
647
  juridique) et le S de Supernovia (la création de contenu sourcée). Même
648
- facture que les cinq autres — le ruban de la charte, rendu couleur, PNG
649
- détouré remonté à 512px sur le grand côté — et même convention de nommage
648
+ facture que les cinq autres (le ruban de la charte, rendu couleur, PNG
649
+ détouré remonté à 512px sur le grand côté) et même convention de nommage
650
650
  `<produit>-<forme>-<nuance>.png`.
651
651
 
652
652
  Le registre des produits du script d'index les connaît désormais : ajouter
@@ -660,7 +660,7 @@ monochrome, pas de lockup. Sur navy ou sous 24px, ils ne conviennent pas.
660
660
 
661
661
  Un lockup ne se réduit pas. Celui de Zevra mesure 555×200, sa capitale
662
662
  70px, et le site l'affichait à **14,7px de capitale** dans la barre de
663
- navigation, 10,5 sur téléphone, 9,8 dans le pied de page — une réduction
663
+ navigation, 10,5 sur téléphone, 9,8 dans le pied de page : une réduction
664
664
  de près de cinq fois. Chaque fût tombait alors sur une fraction de pixel
665
665
  et recevait sa propre dose de lissage : le mot virait au gris, et le V,
666
666
  seul à n'avoir aucun fût vertical sur quoi s'aligner, semblait flotter.
@@ -683,7 +683,7 @@ le registre décrivait sans qu'aucun fichier ne les fournisse. La nuance
683
683
 
684
684
  Relevées au pixel sur le lockup d'origine : la marque vaut **2,86 fois**
685
685
  la capitale, l'écart entre les deux **0,64 fois**, et les deux pièces se
686
- **centrent l'une sur l'autre** — elles ne partagent pas de ligne de base.
686
+ **centrent l'une sur l'autre**, elles ne partagent pas de ligne de base.
687
687
  Tout se pilote par `--zv-cap`, qui EST la hauteur de capitale : le viewBox
688
688
  du SVG a 100 unités de haut pour la capitale, si bien qu'une hauteur CSS
689
689
  posée dessus se lit directement.
@@ -701,16 +701,16 @@ Le fichier vectoriel d'origine n'a pas été retrouvé : le tracé est
701
701
  reconstruit depuis le bitmap, par ajustement de droites sur le contour
702
702
  (les lettres sont géométriques, leurs côtés sont des segments). Il est
703
703
  fidèle à **0,1 % en largeur** et mesurablement plus lisible que le bitmap
704
- partout où le site le pose — mais il porte les approximations du relevé.
704
+ partout où le site le pose : mais il porte les approximations du relevé.
705
705
 
706
- **Réservé à l'écran, jusqu'à 120px de capitale.** Au-delà — grand format,
707
- impression — exiger le fichier d'origine, qui remplacera celui-ci sans
706
+ **Réservé à l'écran, jusqu'à 120px de capitale.** Au-delà (grand format,
707
+ impression) exiger le fichier d'origine, qui remplacera celui-ci sans
708
708
  changer une ligne de code.
709
709
 
710
710
  ### Au passage : le commentaire du token était faux
711
711
 
712
712
  `--font-display` annonçait « titres 200-400, wordmarks 600 ». Mesure faite,
713
- le mot du lockup a un rapport fût/capitale de 0,129, soit du **400** — et
713
+ le mot du lockup a un rapport fût/capitale de 0,129, soit du **400**, et
714
714
  surtout il n'est PAS dessiné en Outfit : à capitale égale, son E est 20 %
715
715
  plus étroit, son V 24 %, son A 26 %. Seul le Z coïncide. La mention a été
716
716
  retirée plutôt que corrigée : le mot n'est pas de la typographie, c'est un
@@ -720,7 +720,7 @@ tracé.
720
720
 
721
721
  La nav du site Zevra portait, dans son propre dépôt, cent lignes de CSS et
722
722
  un script que le paquet ne connaissait pas : menus déroulants, sélecteur de
723
- langue, tiroir groupé, chrome givré. Tout cela n'a rien de propre à Zevra —
723
+ langue, tiroir groupé, chrome givré. Tout cela n'a rien de propre à Zevra :
724
724
  c'est ce que demande n'importe quelle vitrine dès qu'elle a plus de quatre
725
725
  liens. Le paquet le reprend.
726
726
 
@@ -736,7 +736,7 @@ Son voile était écrit en `rgba(245, 246, 250, …)`, c'est-à-dire hors token
736
736
  `color-mix` sur le token.
737
737
 
738
738
  ⚠️ `backdrop-filter` fait du chrome le bloc conteneur de tout descendant en
739
- position fixe. Le tiroir doit donc vivre EN DEHORS, en frère — dedans, il
739
+ position fixe. Le tiroir doit donc vivre EN DEHORS, en frère : dedans, il
740
740
  serait rogné à la hauteur de la barre. C'est écrit dans le CSS, à l'endroit
741
741
  où l'on se tromperait.
742
742
 
@@ -744,12 +744,12 @@ où l'on se tromperait.
744
744
 
745
745
  `:hover` et `:focus-within` : accessible au clavier, rien à hydrater, la
746
746
  barre reste rendue au serveur. Un pont invisible couvre l'interstice entre
747
- l'entrée et son panneau — sans lui le survol se rompt en descendant, et le
747
+ l'entrée et son panneau : sans lui le survol se rompt en descendant, et le
748
748
  menu se referme sous le curseur.
749
749
 
750
750
  Piège consigné : le déclencheur porte `.zv-navlink` pour se rendre comme un
751
751
  lien simple. Un `font: inherit`, réflexe habituel pour dénuder un bouton,
752
- remet la fonte du conteneur (16px/400) là où `.zv-navlink` pose 14px/500 —
752
+ remet la fonte du conteneur (16px/400) là où `.zv-navlink` pose 14px/500 :
753
753
  les entrées à menu deviennent alors plus grosses que les autres. C'est
754
754
  arrivé, et ça s'est vu.
755
755
 
@@ -757,21 +757,21 @@ arrivé, et ça s'est vu.
757
757
 
758
758
  Le seuil de bascule vers le tiroir est à 767px, réglé sur la nav de
759
759
  référence : quatre liens simples. Une nav qui porte cinq familles à menu,
760
- un sélecteur de langue et un appel à l'action déborde bien avant — mesuré :
760
+ un sélecteur de langue et un appel à l'action déborde bien avant, mesuré :
761
761
  entre 768 et 1150px, le logo passe SOUS la première entrée.
762
762
 
763
763
  `.zv-nav--riche` déplace le seuil à 1150px, **pour la barre seulement**. Ce
764
764
  n'est pas un second gabarit : le reste de la page garde l'unique frontière
765
765
  de la charte. Le test d'intégrité l'accepte désormais, et un second test
766
- vérifie qu'aucun sélecteur étranger ne se glisse dans ces blocs — une
766
+ vérifie qu'aucun sélecteur étranger ne se glisse dans ces blocs : une
767
767
  frontière arbitrée pour un composant ne doit pas devenir, par commodité, le
768
768
  gabarit de tout le monde.
769
769
 
770
770
  ### Le tiroir a son module
771
771
 
772
772
  `@zevra/ui/nav.js`, sur le modèle de `motion.js` : il pose et retire
773
- `data-open`, tient `aria-expanded`, ferme sur Échap. Le reste — voile,
774
- glissade, verrouillage du défilement — était déjà dans `overlays.css` et se
773
+ `data-open`, tient `aria-expanded`, ferme sur Échap. Le reste (voile,
774
+ glissade, verrouillage du défilement) était déjà dans `overlays.css` et se
775
775
  joue sur cet attribut seul.
776
776
 
777
777
  Il travaille par délégation sur le document plutôt qu'en câblant chaque
@@ -781,14 +781,14 @@ client.
781
781
 
782
782
  Déclaré dans `sideEffects`. C'est la panne de la 0.12.0 : `motion.js` était
783
783
  exporté et correct, mais `sideEffects: ["*.css"]` autorisait le bundler à
784
- l'élaguer — tout paraissait câblé et rien ne s'animait. Un test le garde
784
+ l'élaguer, tout paraissait câblé et rien ne s'animait. Un test le garde
785
785
  maintenant pour tout module JS livré.
786
786
 
787
787
  ### Le piège de l'ordre, trouvé à la première intégration (0.15.1)
788
788
 
789
789
  Le bloc des menus déroulants était dans `layout.css`. Le panneau d'une
790
- entrée porte DEUX classes — `.zv-menu` du paquet et `.zv-navdd__menu` qui
791
- la surcharge — et elles ont la même spécificité : c'est l'ordre de
790
+ entrée porte DEUX classes, `.zv-menu` du paquet et `.zv-navdd__menu` qui
791
+ la surcharge, et elles ont la même spécificité : c'est l'ordre de
792
792
  chargement qui tranche. `index.css` importe `layout.css` AVANT
793
793
  `navigation.css`, donc `.zv-menu { display: flex }` gagnait et les trois
794
794
  menus de la barre restaient ouverts en permanence.
@@ -803,7 +803,7 @@ après elle.
803
803
  La table des matières d'une page longue, posée sur le côté : un libellé, un
804
804
  point, et le point courant qui s'allume. Elle vivait dans le site Zevra,
805
805
  sous le nom de son échafaudage de scènes. C'est le manque qui avait déjà
806
- été signalé pour les longues lectures — il est comblé.
806
+ été signalé pour les longues lectures : il est comblé.
807
807
 
808
808
  Elle NAVIGUE et elle SUIT, et il faut les deux. Un rail qui suit sans
809
809
  naviguer est un décor qui occupe la marge ; un rail qui navigue sans suivre
@@ -812,7 +812,7 @@ oblige à se demander où l'on en est.
812
812
  ### Le suivi lit la position, il n'observe pas les intersections
813
813
 
814
814
  Premier réflexe : un IntersectionObserver réduit à la bande centrale du
815
- viewport. Il tombe sur les rails réels — leurs ancres sont souvent des
815
+ viewport. Il tombe sur les rails réels : leurs ancres sont souvent des
816
816
  points MATÉRIELS de 1 px posés en tête de section, et un point de 1 px ne
817
817
  traverse une bande de 10 % que pendant quelques images. Un défilement
818
818
  rapide saute la bande entière, et l'entrée ne s'allume jamais.
@@ -820,7 +820,7 @@ rapide saute la bande entière, et l'entrée ne s'allume jamais.
820
820
  `rail.js` lit donc la position : l'entrée courante est la DERNIÈRE dont
821
821
  l'ancre a passé la ligne de lecture (45 % de la hauteur, un peu au-dessus
822
822
  du milieu, là où l'œil se pose). Déterministe, quelle que soit la vitesse.
823
- Une lecture par image au plus — le défilement émet bien plus souvent que
823
+ Une lecture par image au plus : le défilement émet bien plus souvent que
824
824
  l'écran ne se rafraîchit, et chaque lecture coûte un reflow.
825
825
 
826
826
  ### Le balisage fait foi
@@ -828,7 +828,7 @@ l'écran ne se rafraîchit, et chaque lecture coûte un reflow.
828
828
  Le module ne prend aucune liste : il lit les `<a href="#…">` du rail et
829
829
  résout chaque cible. Rien à tenir à jour en double, donc rien à faire
830
830
  diverger. Une ancre qui ne résout pas laisse son entrée cliquable et sort
831
- du suivi, plutôt que de décaler toutes les suivantes — c'est le décalage
831
+ du suivi, plutôt que de décaler toutes les suivantes : c'est le décalage
832
832
  silencieux qui coûte cher.
833
833
 
834
834
  Sans le module, le rail navigue toujours : ce sont des ancres HTML. La
@@ -836,7 +836,7 @@ dégradation est douce, la page n'est jamais cassée.
836
836
 
837
837
  ### Il demande de la place
838
838
 
839
- Sous le point de rupture il disparaît — un rail fixe sur un téléphone se
839
+ Sous le point de rupture il disparaît : un rail fixe sur un téléphone se
840
840
  pose sur la lecture. Au-dessus, c'est la mesure de la page qui décide, et
841
841
  le paquet ne peut pas la connaître : une page dont le contenu court d'un
842
842
  bord à l'autre jusqu'à 1100px gagnera à relever le seuil chez elle. C'est
@@ -852,13 +852,13 @@ constat vient du ruban de la home, qui passait derrière et le délavait.
852
852
  ## Les favicons (26/08/2026)
853
853
 
854
854
  Chaque produit reçoit son jeu de favicons, composé depuis le glyphe couleur
855
- du registre — `logos/favicon/<produit>/`, publié avec le paquet comme le
855
+ du registre : `logos/favicon/<produit>/`, publié avec le paquet comme le
856
856
  reste de `logos/`.
857
857
 
858
858
  **Dérogation assumée à la règle « mono sous 24 px ».** Cette règle existe
859
859
  pour l'encre posée SUR une page de charte, dont on maîtrise le fond. Un
860
860
  favicon vit ailleurs : sur un onglet au fond inconnu (clair ou sombre selon
861
- le navigateur), au milieu des favicons couleur de tous les autres sites —
861
+ le navigateur), au milieu des favicons couleur de tous les autres sites,
862
862
  c'est la couleur qui y est l'identifiant, et une version encre y disparaît
863
863
  un onglet sur deux. Le glyphe couleur reste donc couleur, à toutes les
864
864
  tailles.
@@ -867,14 +867,14 @@ tailles.
867
867
  foi), puis centré sur un canevas carré :
868
868
 
869
869
  - `favicon.ico` (16, 32, 48 en PNG embarqués), `icon-192.png` et
870
- `icon-512.png` : fond TRANSPARENT, glyphe à 86 % du canevas — la marge
870
+ `icon-512.png` : fond TRANSPARENT, glyphe à 86 % du canevas, la marge
871
871
  évite que les pointes touchent le bord de l'onglet ;
872
- - `apple-touch-icon.png` (180) : fond OPAQUE `--paper` et glyphe à 64 % —
872
+ - `apple-touch-icon.png` (180) : fond OPAQUE `--paper` et glyphe à 64 %,
873
873
  Apple aplatit la transparence en noir et arrondit les coins lui-même,
874
874
  l'icône doit donc apporter son fond et laisser l'air que le masque
875
875
  mangera.
876
876
 
877
- Le script est `scripts/build-favicons.py` — Python et non Node comme les
877
+ Le script est `scripts/build-favicons.py`, Python et non Node comme les
878
878
  autres : la composition raster exige un canal alpha que `sips` ne sait pas
879
879
  préserver au recadrage, et Pillow est le seul outil d'image du poste. Les
880
880
  fichiers produits sont COMMITTÉS, comme le sprite : un consommateur n'a
@@ -886,7 +886,7 @@ aucune étape de build à exécuter. Relancer après tout changement de glyphe.
886
886
 
887
887
  Jusqu'ici le système ne savait afficher aucune image. Pas une déclaration
888
888
  `aspect-ratio`, pas un `object-fit`, pas un sélecteur `img` dans les quatorze
889
- modules — les seules images qu'il connaissait étaient ses propres logos et la
889
+ modules : les seules images qu'il connaissait étaient ses propres logos et la
890
890
  planche d'export social, figée en pixels. Chaque produit a donc réécrit le
891
891
  même cadre : campus quatre fois (couverture de parcours, carte scindée, attente
892
892
  de vidéo, grille du fil), la vitrine autant.
@@ -906,8 +906,8 @@ ainsi qu'une bibliothèque finit avec dix-sept rapports dont trois servent.
906
906
  **L'image RECOUVRE par défaut** (`object-fit: cover`) : c'est le seul
907
907
  comportement qui garantisse un cadre plein quelle que soit la source, et une
908
908
  grille de cartes dont les couvertures ne s'alignent pas se voit
909
- immédiatement. `.zv-media--contient` existe pour le cas inverse — un logo, un
910
- schéma, une pièce qu'on ne peut pas rogner — et pose alors le papier derrière,
909
+ immédiatement. `.zv-media--contient` existe pour le cas inverse (un logo, un
910
+ schéma, une pièce qu'on ne peut pas rogner) et pose alors le papier derrière,
911
911
  puisqu'il restera du vide.
912
912
 
913
913
  **Le fond est posé, pas laissé transparent.** Une image qui tarde, échoue ou
@@ -932,7 +932,7 @@ Quatorze icônes entrent : `lecture`, `pause`, `rembobiner`, `avancer`,
932
932
  Le jeu en comptait cinquante-cinq et n'en portait aucune de lecture ni
933
933
  d'édition. Conséquence directe : tout produit qui pose un lecteur ou un
934
934
  éditeur recopie des tracés d'une autre bibliothèque, avec une graisse et des
935
- bouts de trait qui ne sont pas les nôtres — campus en avait une quarantaine.
935
+ bouts de trait qui ne sont pas les nôtres, campus en avait une quarantaine.
936
936
  Les nouvelles suivent la signature du jeu : grille de 24, trait de 2, bouts
937
937
  francs. `lecture` et `pause` sont pleines plutôt que filaires, parce qu'à
938
938
  16 px un triangle en fil de fer se lit mal sur un fond d'image.
@@ -952,7 +952,7 @@ l'entoure et l'anneau de mise au point du groupe existaient déjà. Un second
952
952
  cadre aurait dérivé au premier réglage.
953
953
 
954
954
  **Le bouton est un vrai `<button type="button">`.** Posé sur un `<span>`, il
955
- n'est ni atteignable au clavier ni annoncé — et sans le `type`, il envoie le
955
+ n'est ni atteignable au clavier ni annoncé : et sans le `type`, il envoie le
956
956
  formulaire. Son libellé change avec l'état (« Afficher » / « Masquer »)
957
957
  plutôt que de rester fixe : une icône seule ne dit pas ce qui va se passer.
958
958
  `aria-pressed` porte l'état pour les lecteurs d'écran.
@@ -964,13 +964,13 @@ côté navigateur pour un champ de saisie.
964
964
  **L'œil natif du navigateur est masqué** (`::-ms-reveal`) : sinon deux boutons
965
965
  voisins font la même chose, dont un seul suit la charte.
966
966
 
967
- Vient avec l'icône `oeil-barre`, le pendant qui manquait à `oeil` — sans elle,
967
+ Vient avec l'icône `oeil-barre`, le pendant qui manquait à `oeil` : sans elle,
968
968
  la bascule n'a pas de second état à montrer.
969
969
 
970
970
  ## Ce que l'intégration de mcp-factory a révélé (27/08/2026, 0.19.0)
971
971
 
972
- Six défauts trouvés en migrant une application entière — trente pages, six
973
- zones — vers le paquet. Aucun n'était visible en planche : tous se voient à
972
+ Six défauts trouvés en migrant une application entière (trente pages, six
973
+ zones) vers le paquet. Aucun n'était visible en planche : tous se voient à
974
974
  l'usage, et plusieurs ne se voient qu'à un ÉTAT. Ils avaient tous été
975
975
  rattrapés dans le `globals.css` de l'app, et memoire en portait déjà trois
976
976
  des siens : un correctif recopié dans deux apps est un défaut du paquet qui
@@ -979,14 +979,14 @@ s'ignore.
979
979
  ### `.zv-em` penchait
980
980
 
981
981
  `Em` rend un vrai `<em>`, et `.zv-em` ne déclarait que la couleur et la
982
- graisse — c'est bien ce que demande la charte : « la deuxième moitié de
982
+ graisse, c'est bien ce que demande la charte : « la deuxième moitié de
983
983
  phrase d'un titre bascule en indigo et prend un cran de graisse ». Mais le
984
984
  navigateur, lui, penche les `<em>` par défaut, et Outfit n'a **aucune
985
985
  italique** chargée : il en fabriquait donc une oblique de synthèse. Sur la
986
986
  page pilier de mcp-factory, la seconde moitié du `<h1>` partait en faux
987
987
  italique à 76 px, sur le premier écran.
988
988
 
989
- `.zv-inset em` remettait déjà `font-style: normal` — la règle existait donc
989
+ `.zv-inset em` remettait déjà `font-style: normal` : la règle existait donc
990
990
  au paquet, mais seulement là où l'on avait rencontré le problème. Elle passe
991
991
  sur `.zv-em`, qui est l'endroit qui en a besoin par construction.
992
992
 
@@ -995,11 +995,11 @@ sur `.zv-em`, qui est l'endroit qui en a besoin par construction.
995
995
  C'était la **seule** classe interactive du paquet sans état désactivé.
996
996
  `.zv-btn`, `.zv-chrome-btn`, `.zv-menu__item`, `.zv-pager__arrow`,
997
997
  `.zv-range` et tous les champs en ont un, et plusieurs neutralisent
998
- explicitement le survol — leurs auteurs savaient donc que `:hover`
998
+ explicitement le survol : leurs auteurs savaient donc que `:hover`
999
999
  s'applique encore à un bouton désactivé.
1000
1000
 
1001
- Conséquence à l'usage : la corbeille de révocation d'une clé API unique — le
1002
- cas de tout compte à sa première visite — se rendait exactement comme une
1001
+ Conséquence à l'usage : la corbeille de révocation d'une clé API unique, le
1002
+ cas de tout compte à sa première visite, se rendait exactement comme une
1003
1003
  corbeille active, curseur main compris, et s'allumait au survol comme si
1004
1004
  elle allait agir. Rien dans le rendu ne disait que l'action était fermée.
1005
1005
 
@@ -1016,13 +1016,13 @@ couleur. memoire le rattrapait déjà.
1016
1016
 
1017
1017
  **On dérive donc le sommet de `--accent`.** C'est un écart assumé au relevé :
1018
1018
  pour la charte par défaut, l'écart entre `#2b2bb4` et `#2a2aa8` est d'un
1019
- point de bleu — invisible. La valeur relevée reste écrite en commentaire,
1019
+ point de bleu, invisible. La valeur relevée reste écrite en commentaire,
1020
1020
  pour que l'arbitrage se relise.
1021
1021
 
1022
- ### `.zv-header` perdait son flou — et le diagnostic d'origine était faux
1022
+ ### `.zv-header` perdait son flou : et le diagnostic d'origine était faux
1023
1023
 
1024
- **Le défaut réel : l'ordre des deux `backdrop-filter`.** Lightning CSS — le
1025
- compilateur de Tailwind v4, donc celui de toutes nos applications Next —
1024
+ **Le défaut réel : l'ordre des deux `backdrop-filter`.** Lightning CSS (le
1025
+ compilateur de Tailwind v4, donc celui de toutes nos applications Next)
1026
1026
  SUPPRIME la déclaration non préfixée quand elle précède la forme `-webkit-`.
1027
1027
  Or Firefox ne connaît QUE la forme non préfixée. La barre y perdait son flou.
1028
1028
  Reproduit avec la chaîne exacte des apps, dans les deux ordres : l'inversion
@@ -1037,40 +1037,40 @@ repli, écrasé juste après par une règle de spécificité égale et postérie
1037
1037
 
1038
1038
  Ce paquet a d'abord repris cette prémisse et remplacé le voile par
1039
1039
  `rgb(from var(--paper) r g b / 0.92)`. **C'était une régression**, trouvée
1040
- avant publication : cette syntaxe sort du compilateur NUE — ni `@supports`,
1040
+ avant publication : cette syntaxe sort du compilateur NUE, ni `@supports`,
1041
1041
  ni repli. Sur un navigateur sans couleurs relatives, la valeur devient
1042
1042
  invalide au calcul, le raccourci `background` retombe à sa valeur initiale,
1043
1043
  et une barre FIXE se retrouve sans aucun fond au-dessus du texte qui défile.
1044
1044
  Soit 3,5 % d'usage qui voyaient le voile correctement et ne verraient plus
1045
- rien — Chrome ≤ 118, Firefox ≤ 127, Safari ≤ 16.3.
1045
+ rien : Chrome ≤ 118, Firefox ≤ 127, Safari ≤ 16.3.
1046
1046
 
1047
1047
  Le voile reste donc en `color-mix`, et le commentaire sur place dit
1048
1048
  maintenant pourquoi il ne faut PAS le « corriger ».
1049
1049
 
1050
1050
  **La leçon, plus large que ce correctif** : un correctif recopié dans deux
1051
- apps est un signal fort — c'est ce qui a motivé cette fournée. Mais ce peut
1051
+ apps est un signal fort, c'est ce qui a motivé cette fournée. Mais ce peut
1052
1052
  aussi être une ERREUR recopiée dans deux apps. Le nombre d'occurrences dit
1053
1053
  qu'il faut regarder ; il ne dit pas que le diagnostic est juste. Ici, la
1054
1054
  seconde app avait copié le commentaire de la première sans revérifier.
1055
1055
 
1056
1056
  ### Le mobile zoomait encore, et deux cibles restaient sous le doigt
1057
1057
 
1058
- Le module `forms` pose la règle des 16 px lui-même — « en dessous, Safari
1059
- iOS zoome à la mise au point du champ et décale toute la page » — mais
1058
+ Le module `forms` pose la règle des 16 px lui-même (« en dessous, Safari
1059
+ iOS zoome à la mise au point du champ et décale toute la page ») mais
1060
1060
  `.zv-input--bare` y échappait : son bloc mobile le redescend à `--app-lg`,
1061
1061
  soit 14 px. `.zv-search__input` n'était visé par aucune des deux règles.
1062
1062
  Ce sont précisément les deux champs les plus saisis d'une application.
1063
1063
 
1064
1064
  Trois cibles restaient par ailleurs sous `--tap` : les deux commandes de
1065
- pagination et le fil d'Ariane — qui est un seul lien pour tout le fil.
1065
+ pagination et le fil d'Ariane, qui est un seul lien pour tout le fil.
1066
1066
  Et `.zv-rows__val` gardait son `min-width: 260px` sous le seuil, alors que
1067
1067
  le module libérait déjà la CLÉ : dans une carte de 215 px, la valeur
1068
1068
  débordait de 45 px.
1069
1069
 
1070
1070
  ### Cinq composants d'ancre refusaient `target` et `rel`
1071
1071
 
1072
- `ButtonLink`, `LinkArrow` et tout le module `navigation` — `SideLink`
1073
- compris — étaient déjà typés sur `AnchorHTMLAttributes`. Cinq autres ne
1072
+ `ButtonLink`, `LinkArrow` et tout le module `navigation`, `SideLink`
1073
+ compris, étaient déjà typés sur `AnchorHTMLAttributes`. Cinq autres ne
1074
1074
  l'étaient pas : `Link` (typography), et `NavLink`, `NavDrawerLink`, `Langue`
1075
1075
  et `RailItem` (layout). Ils portaient `HTMLAttributes<HTMLAnchorElement>`,
1076
1076
  qui ne connaît ni `target`, ni `rel`, ni `download`, ni `hreflang`.
@@ -1078,7 +1078,7 @@ qui ne connaît ni `target`, ni `rel`, ni `download`, ni `hreflang`.
1078
1078
  Ce n'est pas une nuance de typage : un lien sortant ne peut pas porter
1079
1079
  `rel="noopener"`, et un lien qui doit s'ouvrir à côté ne le peut pas non
1080
1080
  plus. mcp-factory a dû garder deux `className="zv-link"` écrits à la main
1081
- pour cette seule raison — dont un dans un formulaire à moitié rempli, où
1081
+ pour cette seule raison : dont un dans un formulaire à moitié rempli, où
1082
1082
  suivre le lien sans `target="_blank"` aurait vidé la saisie.
1083
1083
 
1084
1084
  L'incohérence n'était pas un choix : `navigation` avait la bonne base depuis
@@ -1091,20 +1091,20 @@ appel existant ne casse.
1091
1091
  ### Les quatre variantes de bouton s'excluent, et le CSS le dit enfin
1092
1092
 
1093
1093
  Le défaut ci-dessus a une cause structurelle, et c'est elle qu'il fallait
1094
- traiter — sans quoi la prochaine app le refait.
1094
+ traiter : sans quoi la prochaine app le refait.
1095
1095
 
1096
1096
  `zv-btn`, `zv-btn--ghost`, `zv-btn--ink` et `zv-btn--danger` sont quatre
1097
1097
  classes **autonomes et mutuellement exclusives**. Mais le socle commun les
1098
- groupe dans un même sélecteur — ce qui leur donne exactement l'air d'une base
1098
+ groupe dans un même sélecteur : ce qui leur donne exactement l'air d'une base
1099
1099
  et de ses modificateurs. `Button` n'en rend jamais qu'une ; une app qui écrit
1100
1100
  ses classes à la main, elle, lit du BEM et compose les deux. C'est arrivé.
1101
1101
 
1102
1102
  Le résultat était vicieux : `.zv-btn:hover` (0,2,0) l'emportait sur
1103
- `.zv-btn--ghost` (0,1,0). Un fond indigo apparaissait sous un texte encre —
1103
+ `.zv-btn--ghost` (0,1,0). Un fond indigo apparaissait sous un texte encre,
1104
1104
  illisible, et **seulement au survol** : invisible en relecture de code comme
1105
1105
  en capture d'écran. Aucun outil ne pouvait l'attraper. Ni le compilateur, qui
1106
1106
  ne lit pas les chaînes de classes ; ni les tests du paquet, qui vérifient ce
1107
- que les composants émettent — or ils émettaient juste.
1107
+ que les composants émettent : or ils émettaient juste.
1108
1108
 
1109
1109
  Les règles qui peignent le plein portent donc désormais
1110
1110
  `:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))`. Le CSS dit
@@ -1121,8 +1121,8 @@ d'autres.
1121
1121
  commentaire est sur place, au-dessus de la première.
1122
1122
 
1123
1123
  **Ce qui a été écarté** : rendre la combinaison visiblement fautive (un
1124
- liseré d'alerte) — on ne met pas de piège de développement dans du CSS de
1125
- production ; et renommer `zv-btn` en `zv-btn--plein` — c'est une rupture pour
1124
+ liseré d'alerte), on ne met pas de piège de développement dans du CSS de
1125
+ production ; et renommer `zv-btn` en `zv-btn--plein`, c'est une rupture pour
1126
1126
  les quatre consommateurs, dont la landing qui écrit toutes ses classes à la
1127
1127
  main, pour un gain de lisibilité seul.
1128
1128
 
@@ -1130,7 +1130,7 @@ main, pour un gain de lisibilité seul.
1130
1130
 
1131
1131
  Deux retours de recette sur mcp-factory, sur la même page.
1132
1132
 
1133
- ### La FAQ se replie enfin — et le CSS l'attendait
1133
+ ### La FAQ se replie enfin : et le CSS l'attendait
1134
1134
 
1135
1135
  `FaqItem` rendait deux `<p>`, tout ouvert, avec ce commentaire : « la cacher
1136
1136
  derrière un clic n'ajoute rien à cinq questions ». L'argument tient à cinq
@@ -1141,7 +1141,7 @@ qu'on n'a pas demandées.
1141
1141
  Surtout, **le CSS prévoyait déjà l'accordéon** : `.zv-faq__q` portait
1142
1142
  `cursor: pointer` et un `svg` de chevron stylé en `--accent`. La façade React
1143
1143
  ne les a jamais servis. Le curseur promettait donc un clic qui ne faisait
1144
- rien — l'affordance était un mensonge.
1144
+ rien : l'affordance était un mensonge.
1145
1145
 
1146
1146
  `FaqItem` rend maintenant un `<details>`, comme `Fold`. Donc **aucun
1147
1147
  JavaScript** : ouverture au clic, à Entrée et à Espace, annonce native de
@@ -1155,26 +1155,26 @@ peut ouvrir. Une app avait dû rendre ses réponses ouvertes pour cette raison
1155
1155
  précise, son accordéon maison démontant son contenu.
1156
1156
 
1157
1157
  L'API ne bouge pas : `<FaqItem question={…}>réponse</FaqItem>`. Une prop
1158
- `ouvert` s'ajoute, pour la question qu'on veut dépliée d'entrée — **une par
1158
+ `ouvert` s'ajoute, pour la question qu'on veut dépliée d'entrée : **une par
1159
1159
  page au plus**, sinon le repli ne sert plus à rien.
1160
1160
 
1161
1161
  **Et le bloc reçoit son filet.** `.zv-faq` était un aplat blanc posé sur le
1162
1162
  papier, sans début ni fin, là où toutes les autres surfaces blanches de la
1163
- vitrine — carte, encart, tarif, dépliant — en portent un. Il faisait tache.
1163
+ vitrine (carte, encart, tarif, dépliant) en portent un. Il faisait tache.
1164
1164
  C'est le même filet que `.zv-fold`.
1165
1165
 
1166
1166
  ### La barre coupait les titres en deux
1167
1167
 
1168
1168
  Le voile de `.zv-header` se dégradait sur TOUTE sa hauteur : à sa base il ne
1169
1169
  restait presque rien. Un titre de 38 ou 56 px qui passait dessous se
1170
- retrouvait donc coupé en deux — moitié voilée, moitié nette — avec les
1170
+ retrouvait donc coupé en deux (moitié voilée, moitié nette) avec les
1171
1171
  entrées de navigation posées sur ses lettres. Deux textes superposés, aucun
1172
1172
  lisible.
1173
1173
 
1174
1174
  Le voile est désormais PLEIN derrière la barre, et le dégradé descend SOUS
1175
1175
  elle, dans un `::after` de 24 px. Le dégradé n'est pas perdu : il adoucit le
1176
1176
  passage là où il ne se dispute plus la place avec la navigation. La frange
1177
- porte `pointer-events: none` — elle couvre le haut du contenu, elle ne doit
1177
+ porte `pointer-events: none` : elle couvre le haut du contenu, elle ne doit
1178
1178
  rien intercepter.
1179
1179
 
1180
1180
  Aucun changement de gabarit : le `::after` est en absolu sous une barre déjà
@@ -1185,13 +1185,13 @@ fixée, il ne prend pas de place dans le flux.
1185
1185
  ## Les e-mails (27/08/2026)
1186
1186
 
1187
1187
  Le paquet gagne des gabarits d'e-mail (`@zevra/ui/email`). Ils étaient
1188
- jusqu'ici refaits par chaque produit, hors charte — fond crème, angles
1188
+ jusqu'ici refaits par chaque produit, hors charte, fond crème, angles
1189
1189
  ronds, surligné jaune, titres en serif : rien de tout cela n'est dans le
1190
1190
  système.
1191
1191
 
1192
1192
  ### La dérogation à la règle première, et pourquoi elle est imposée
1193
1193
 
1194
- « Le CSS est la source de vérité, les composants n'en sont qu'une façade » —
1194
+ « Le CSS est la source de vérité, les composants n'en sont qu'une façade » :
1195
1195
  sauf ici. Ce n'est pas un choix, c'est le support :
1196
1196
 
1197
1197
  - il n'y a **aucune feuille de style** à charger dans un e-mail ;
@@ -1201,7 +1201,7 @@ sauf ici. Ce n'est pas un choix, c'est le support :
1201
1201
  - ni flexbox ni grid ne sont fiables : la mise en page est en **tables** ;
1202
1202
  - les **dégradés** ne se rendent pas dans Outlook.
1203
1203
 
1204
- Les valeurs sont donc écrites en clair — mais tirées du **miroir JS des
1204
+ Les valeurs sont donc écrites en clair : mais tirées du **miroir JS des
1205
1205
  tokens**, que `tokens/index.js` destinait déjà explicitement à cet usage
1206
1206
  (« couleurs de thème d'un e-mail ») et que `tests/tokens.test.js` tient
1207
1207
  aligné sur le CSS. Aucune seconde source n'apparaît. Le détour par
@@ -1212,12 +1212,12 @@ aligné sur le CSS. Aucune seconde source n'apparaît. Le détour par
1212
1212
  | Dans le système | Dans l'e-mail | Pourquoi |
1213
1213
  |---|---|---|
1214
1214
  | Bouton plein en dégradé indigo | Aplat `--accent-deep` | Les dégradés ne se rendent pas dans Outlook |
1215
- | Filet de spectre en `linear-gradient` | Trois aplats de 19 px | Idem — une bande vide sinon |
1215
+ | Filet de spectre en `linear-gradient` | Trois aplats de 19 px | Idem : une bande vide sinon |
1216
1216
  | `padding` sur le `<a>` du bouton | Table + `bgcolor` | Outlook ignore le padding d'un `<a>` |
1217
1217
  | Largeur `--page-max` | 600 px | Le volet de lecture d'Outlook |
1218
1218
 
1219
1219
  Ce qui NE change pas : l'angle vif, le papier, l'encre, les trois familles,
1220
- un seul bouton plein par message, et le filet de spectre comme signature —
1220
+ un seul bouton plein par message, et le filet de spectre comme signature,
1221
1221
  premier de ses trois emplois autorisés.
1222
1222
 
1223
1223
  ### Le mode sombre est refusé
@@ -1225,7 +1225,7 @@ premier de ses trois emplois autorisés.
1225
1225
  `color-scheme: light`, explicitement. La charte est claire par nature :
1226
1226
  l'inversion automatique d'Apple Mail retourne les neutres **sans toucher aux
1227
1227
  aplats**, et l'indigo du bouton se retrouverait sur un fond devenu noir. Même
1228
- règle que pour les surfaces sombres du système — elles se font avec la bande
1228
+ règle que pour les surfaces sombres du système : elles se font avec la bande
1229
1229
  manifeste, jamais en inversant les neutres.
1230
1230
 
1231
1231
  ### Deux règles qui ne sont pas décoratives
@@ -1235,7 +1235,7 @@ deux est bloquée par défaut ; un e-mail qui ne dit pas de qui il vient part à
1235
1235
  la corbeille. D'où `marque` obligatoire, et le logo en option.
1236
1236
 
1237
1237
  **Le preheader est un contenu, pas un détail.** Sans lui, le client affiche
1238
- les premiers mots du HTML dans la liste des messages — c'est la seule ligne
1238
+ les premiers mots du HTML dans la liste des messages : c'est la seule ligne
1239
1239
  dont on dispose pour convaincre d'ouvrir.
1240
1240
 
1241
1241
  ### Un piège rencontré, et gardé
@@ -1245,7 +1245,7 @@ Les piles de polices du paquet contiennent des **guillemets doubles**
1245
1245
  mêmes guillemets : l'attribut se referme au premier guillemet interne et le
1246
1246
  nœud perd **tout** son style. L'e-mail entier repassait en Times, sans
1247
1247
  couleurs, avec un libellé de bouton blanc devenu illisible sur l'aplat
1248
- indigo. Cela ne se voit pas à la relecture — cela s'est vu au rendu. Les
1248
+ indigo. Cela ne se voit pas à la relecture : cela s'est vu au rendu. Les
1249
1249
  piles sont converties en guillemets simples, et un test refuse désormais
1250
1250
  tout attribut `style` contenant un guillemet double.
1251
1251
 
@@ -1255,14 +1255,14 @@ tout attribut `style` contenant un guillemet double.
1255
1255
 
1256
1256
  La barre gagne, à DROITE, deux pièces que toute suite de produits finit par
1257
1257
  avoir : le **lanceur** pour passer d'un produit à l'autre, et la **zone de
1258
- compte** pour savoir qui l'on est. HORS RELEVÉ — aucune planche n'en montre.
1258
+ compte** pour savoir qui l'on est. HORS RELEVÉ : aucune planche n'en montre.
1259
1259
 
1260
1260
  ### Quatre carrés, pas neuf points
1261
1261
 
1262
1262
  L'icône du lanceur est une grille de **quatre carrés pleins** (icône
1263
1263
  `applications`), là où la convention du web est neuf points ronds. Deux
1264
- raisons : la charte est carrée depuis l'arbitrage du 25/08 — les points
1265
- d'encre du jeu d'icônes sont d'ailleurs déjà des carrés —, et quatre carrés
1264
+ raisons : la charte est carrée depuis l'arbitrage du 25/08, les points
1265
+ d'encre du jeu d'icônes sont d'ailleurs déjà des carrés, et quatre carrés
1266
1266
  se lisent mieux que neuf points à 20 px. L'affordance reste immédiate : un
1267
1267
  pavé de formes régulières au bord droit d'une barre ne se confond avec rien.
1268
1268
 
@@ -1278,19 +1278,19 @@ composant.
1278
1278
 
1279
1279
  `.zv-nav__links` disparaît sous le point de rupture (et sous 1150px en
1280
1280
  `nav--riche`), remplacée par le burger. Un compte placé dedans s'évanouit
1281
- sur téléphone — et se connecter est justement la première chose qu'on y
1281
+ sur téléphone : et se connecter est justement la première chose qu'on y
1282
1282
  fait. D'où `.zv-nav__actions`, une zone sœur qui survit au seuil.
1283
1283
 
1284
1284
  ### L'état : CSS d'abord, prop ensuite
1285
1285
 
1286
- Ouverture au survol ET au focus, en CSS seul — la barre reste rendue au
1286
+ Ouverture au survol ET au focus, en CSS seul : la barre reste rendue au
1287
1287
  serveur, sans rien à hydrater, comme `.zv-navdd`. Une app qui préfère
1288
1288
  l'ouverture au CLIC (la convention des lanceurs) tient l'état et passe
1289
1289
  `ouvert` : un état se dit en PROP, il ne se peint pas depuis l'app.
1290
1290
 
1291
1291
  Le panneau n'est pas un `role="menu"` : ce rôle promet une navigation aux
1292
1292
  flèches que le CSS seul ne peut pas tenir. Ce sont des liens dans un
1293
- panneau, atteignables à la tabulation — une promesse tenue vaut mieux qu'un
1293
+ panneau, atteignables à la tabulation : une promesse tenue vaut mieux qu'un
1294
1294
  rôle exact et menteur.
1295
1295
 
1296
1296
  ### Deux défauts trouvés au rendu, pas à la relecture
@@ -1298,7 +1298,7 @@ rôle exact et menteur.
1298
1298
  **Le double ancrage.** Le `.zv-menu` du paquet est lui-même en position
1299
1299
  absolue : imbriqué dans un panneau qui porte déjà l'ancrage, il se
1300
1300
  positionnait une seconde fois, à partir du bord droit, et la grille sortait
1301
- de l'écran. Sur grand écran la place restante le masquait — c'est le rendu à
1301
+ de l'écran. Sur grand écran la place restante le masquait : c'est le rendu à
1302
1302
  390 px qui l'a montré. Le panneau ancre, le menu ne fournit plus que sa
1303
1303
  surface.
1304
1304
 
@@ -1311,40 +1311,40 @@ déconnecter » non. C'est la BALISE qui change, pas l'apparence.
1311
1311
 
1312
1312
  Le lanceur montrait l'initiale des produits faute de pouvoir charger leur
1313
1313
  glyphe : le catalogue est compilé par `tsc`, qui ne sait pas importer un
1314
- PNG, et il est rendu hors du dépôt — planche, kit de canvas, page d'admin —
1314
+ PNG, et il est rendu hors du dépôt (planche, kit de canvas, page d'admin)
1315
1315
  où un chemin relatif ne résout pas. Deux « M » (Mémoire, MCP) se
1316
1316
  retrouvaient côte à côte.
1317
1317
 
1318
1318
  Même arbitrage que pour les icônes : **inliner**. Ce qui l'empêchait, c'est
1319
- le poids — DECISIONS le notait déjà : « un PNG s'inline en data-URI, mais
1319
+ le poids, DECISIONS le notait déjà : « un PNG s'inline en data-URI, mais
1320
1320
  c'est 150 ko de texte là où un SVG en faisait 4 ». Les originaux sont du
1321
1321
  matériel de charte (512 px, 900 Ko à eux sept) ; une tuile les affiche à
1322
- 28 px. Réduits à 96 px — le double de la plus grande taille d'affichage,
1323
- pour les écrans à densité — ils tombent à **60 Ko**, soit ~80 Ko en base64.
1322
+ 28 px. Réduits à 96 px (le double de la plus grande taille d'affichage,
1323
+ pour les écrans à densité) ils tombent à **60 Ko**, soit ~80 Ko en base64.
1324
1324
  L'inlinage redevient tenable.
1325
1325
 
1326
1326
  Deux étapes, chacune sur un précédent du dépôt :
1327
1327
 
1328
1328
  - `scripts/build-glyphes.py` réduit et **committe** `logos/glyphes/*.png`,
1329
- comme `logos/favicon/` — la réduction demande Pillow, le build n'a pas à
1329
+ comme `logos/favicon/` (la réduction demande Pillow, le build n'a pas à
1330
1330
  en dépendre ;
1331
1331
  - `scripts/build-glyph-data.mjs` les inline dans `react/glyph-data.ts`,
1332
- comme `react/icon-data.ts` — généré, ignoré par git, dans la chaîne Node.
1332
+ comme `react/icon-data.ts`) généré, ignoré par git, dans la chaîne Node.
1333
1333
 
1334
1334
  ⚠️ **Entrée SÉPARÉE** (`@zevra/ui/glyphes`) : 80 Ko n'ont rien à faire dans
1335
1335
  le barrel React, que toute app importe. Seule celle qui affiche des logos
1336
1336
  la charge. `LanceurApp` garde donc `glyphe` en prop plutôt que de résoudre
1337
- un identifiant lui-même — sinon `layout.tsx`, qui est dans le barrel,
1337
+ un identifiant lui-même : sinon `layout.tsx`, qui est dans le barrel,
1338
1338
  tirerait les 80 Ko derrière lui.
1339
1339
 
1340
- ⚠️ Réduction SEULE : aucune recoloration, aucune resaturation — la charte
1340
+ ⚠️ Réduction SEULE : aucune recoloration, aucune resaturation, la charte
1341
1341
  l'interdit sur les glyphes couleur. Et ils ne descendent pas sous 24 px :
1342
1342
  la charte y demande une version monochrome, qui n'existe pas encore.
1343
1343
 
1344
1344
  ### Les logos dans les e-mails (27/08/2026)
1345
1345
 
1346
1346
  Un e-mail ne peut afficher un glyphe ni par chemin relatif, ni par data-URI
1347
- — Gmail bloque les secondes. Il lui faut une **URL absolue et publique**.
1347
+ - Gmail bloque les secondes. Il lui faut une **URL absolue et publique**.
1348
1348
 
1349
1349
  Retenu : la planche les publie. `design.zevra.tech` est déployé depuis ce
1350
1350
  dépôt, à chaque version : c'est le seul endroit où l'image ne peut pas
@@ -1354,18 +1354,18 @@ les adresses. `email({ produit: 'memoire' })` suffit.
1354
1354
 
1355
1355
  ⚠️ Ce que cela suppose, et qu'il faut savoir : les e-mails déjà partis
1356
1356
  pointent vers cette adresse pour toujours. **Le chemin `/logos/glyphes/` ne
1357
- se renomme pas** — un e-mail d'il y a deux ans doit continuer d'afficher son
1357
+ se renomme pas** : un e-mail d'il y a deux ans doit continuer d'afficher son
1358
1358
  logo. Si la planche devait changer d'hôte, l'ancien devrait redirigier.
1359
1359
 
1360
1360
  Et si l'adresse tombe, le repli est déjà en place : `alt` porte le nom de la
1361
1361
  marque, et `marque` reste obligatoire. Une image sur deux est bloquée de
1362
- toute façon — c'est le texte qui identifie l'expéditeur, jamais l'image.
1362
+ toute façon : c'est le texte qui identifie l'expéditeur, jamais l'image.
1363
1363
 
1364
1364
  ## Le lanceur d'applications devient lisible (27/08/2026, 0.29.0)
1365
1365
 
1366
1366
  Le panneau du lanceur montrait sept glyphes et sept noms de marque. Pour
1367
1367
  qui travaille chez Zevra, c'est suffisant. Pour un client qui découvre la
1368
- suite — et c'est lui qui ouvre ce panneau —, « Lexform » et « Plume » ne
1368
+ suite, et c'est lui qui ouvre ce panneau, « Lexform » et « Plume » ne
1369
1369
  disent **rien**. Une grille de marques n'est pas une liste d'applications :
1370
1370
  c'est un test de mémoire.
1371
1371
 
@@ -1385,19 +1385,19 @@ aplat : le filet passe à `--accent-line`, l'application courante le porte
1385
1385
  déjà. Sans lui, sept PNG flottaient sur du blanc.
1386
1386
 
1387
1387
  **Un en-tête, et des groupes.** Le titre « Applications » précédé du filet
1388
- de spectre — deuxième emploi de la signature, celui qui fait qu'un panneau
1388
+ de spectre : deuxième emploi de la signature, celui qui fait qu'un panneau
1389
1389
  d'apps ressemble à Zevra et pas à n'importe quel lanceur. Le groupe
1390
1390
  (« Suite Zevra ») est facultatif : avec une seule famille il n'apporte rien.
1391
1391
 
1392
1392
  **Quatre colonnes, plus trois.** Sept produits en trois colonnes laissaient
1393
1393
  une dernière rangée d'une seule tuile ; en quatre, ils tiennent en 4 + 3.
1394
- Sur téléphone, trois colonnes souples — quatre tuiles de 104px ne tiennent
1394
+ Sur téléphone, trois colonnes souples : quatre tuiles de 104px ne tiennent
1395
1395
  pas dans les 358px qui restent.
1396
1396
 
1397
1397
  ⚠️ **PAS DE CHAMP DE RECHERCHE**, et c'est délibéré. La maquette d'origine
1398
1398
  en portait un. Sept produits se lisent d'un coup d'œil : une recherche y
1399
1399
  ajouterait un contrôle à servir, un état à tenir, un focus à gérer et un
1400
- clavier qui s'ouvre sur téléphone — pour trouver ce qui est déjà à l'écran.
1400
+ clavier qui s'ouvre sur téléphone, pour trouver ce qui est déjà à l'écran.
1401
1401
  Le jour où la suite dépasse la douzaine, c'est une autre conversation.
1402
1402
 
1403
1403
  Ce qui n'a **pas** été repris de la maquette : les angles arrondis des
@@ -1406,13 +1406,13 @@ d'applications n'est pas une exception.
1406
1406
 
1407
1407
  ## Le drapeau qui se prenait pour un composant (28/08/2026, 0.29.1)
1408
1408
 
1409
- `rail.js` marquait « déjà armé » en posant `data-zv-rail` sur `<html>` —
1409
+ `rail.js` marquait « déjà armé » en posant `data-zv-rail` sur `<html>` :
1410
1410
  le nom même de l'attribut qui DÉSIGNE un rail. Son propre sélecteur,
1411
1411
  `[data-zv-rail]`, ramassait donc `<html>` : **la page entière devenait un
1412
1412
  rail**.
1413
1413
 
1414
1414
  Sur une page qui a un vrai rail, cela ne se voyait pas. Sur une page qui
1415
- n'en a pas — la seule où ça compte — `a[href^="#"]` ramassait tous les
1415
+ n'en a pas, la seule où ça compte, `a[href^="#"]` ramassait tous les
1416
1416
  liens d'ancre, boutons compris, et le premier recevait
1417
1417
  `.zv-rail__item--on`. Or cette classe pose `color: var(--accent-deep)` :
1418
1418
  un appel à l'action indigo sur fond indigo, illisible.
@@ -1421,7 +1421,7 @@ Constaté sur `/bootcamp` : « Candidater pour octobre » était le premier
1421
1421
  lien d'ancre de la page.
1422
1422
 
1423
1423
  Le drapeau s'appelle désormais `data-zv-rail-arme`, et `rails()` écarte
1424
- explicitement `<html>` et `<body>` — ceinture et bretelles, parce qu'un
1424
+ explicitement `<html>` et `<body>` : ceinture et bretelles, parce qu'un
1425
1425
  consommateur peut écrire ce qu'il veut sur la racine.
1426
1426
 
1427
1427
  Un test garde la classe entière de fautes : dans tout module livré, aucun
@@ -1447,13 +1447,13 @@ ne fait que doser ce qu'on en voit.
1447
1447
 
1448
1448
  Le prolongement sous la barre (`::after`) part désormais de 72 % lui
1449
1449
  aussi. Son sommet doit valoir exactement l'opacité de la barre, sinon la
1450
- jointure fait une marche — d'autant plus visible que la barre est
1450
+ jointure fait une marche : d'autant plus visible que la barre est
1451
1451
  translucide.
1452
1452
 
1453
1453
  ### Ce qui n'est PAS revenu
1454
1454
 
1455
1455
  Le dégradé sur toute la hauteur de la barre. C'est lui qui coupait un
1456
- grand titre en deux — moitié voilée, moitié nette, avec les entrées de
1456
+ grand titre en deux : moitié voilée, moitié nette, avec les entrées de
1457
1457
  nav posées sur ses lettres. Le voile reste PLAT derrière la barre : c'est
1458
1458
  l'uniformité qui règle ce défaut, pas l'opacité. On récupère donc le
1459
1459
  verre sans rouvrir la panne.
@@ -1471,7 +1471,7 @@ l'exemple de référence.
1471
1471
 
1472
1472
  **Elle vit dans `components.css`, et c'est un choix.** Le découpage du paquet
1473
1473
  oppose une couche vitrine (`layout`, `surfaces`, `motion`) à une couche
1474
- applicative (`forms`, `feedback`, `navigation`, `data`) — or une page
1474
+ applicative (`forms`, `feedback`, `navigation`, `data`), or une page
1475
1475
  d'erreur est réclamée par les DEUX : la landing Astro comme l'app Next.
1476
1476
  `feedback.css` en aurait privé la landing, `layout.css` en aurait privé les
1477
1477
  apps, et la dupliquer est exclu. `components.css` ne dépend que de `tokens`
@@ -1480,13 +1480,13 @@ C'est le seul module que les deux couches chargent réellement.
1480
1480
 
1481
1481
  **Le composant ne porte AUCUN texte.** C'est la décision qui compte. Une
1482
1482
  plaisanterie câblée dans un design system part dans les sept apps, dans une
1483
- seule langue, et n'en revient jamais — et elle vieillit vite. Le composant
1483
+ seule langue, et n'en revient jamais : et elle vieillit vite. Le composant
1484
1484
  n'expose que des emplacements ; la copie de référence vit dans l'exemple du
1485
1485
  catalogue, là où on la lit avant de la copier, et où la changer n'oblige
1486
1486
  personne.
1487
1487
 
1488
1488
  **Drôle par le VOCABULAIRE, pas par le ton.** La copie de référence
1489
- plaisante en droit — « elle a peut-être exercé son droit à l'effacement »
1489
+ plaisante en droit : « elle a peut-être exercé son droit à l'effacement »
1490
1490
  (art. 17 RGPD), « vous n'avez pas qualité pour agir » pour un 403. Ça
1491
1491
  fonctionne parce que le lecteur est un juriste : c'est un mot juste employé
1492
1492
  de travers, pas une blague sur un ton qu'Encre & Papier n'a pas. Et
@@ -1494,7 +1494,7 @@ l'ordre est fixe : **le titre dit la panne en clair**, l'esprit vient
1494
1494
  ensuite et se dédit lui-même (« Plus sérieusement : … »). Une page d'erreur
1495
1495
  qui fait rire avant d'informer est une page d'erreur ratée.
1496
1496
 
1497
- **La ligne de référence en bas** — `RÉF. 404 · GET /dossier/2019` — n'est pas
1497
+ **La ligne de référence en bas** : `RÉF. 404 · GET /dossier/2019` : n'est pas
1498
1498
  un ornement. Elle donne au visiteur de quoi signaler l'incident et au support
1499
1499
  de quoi le retrouver, et elle a la forme d'une cote de dossier, ce qui est la
1500
1500
  seule illustration dont cette maison ait besoin.
@@ -1512,8 +1512,8 @@ Mémoire, et **aucune prop ne permettait de le corriger**. Le même composant
1512
1512
  « Erreur 404 ». Son propre arbitrage, écrit dans le même commit, disait
1513
1513
  pourtant qu'elle « ne porte AUCUN texte ».
1514
1514
 
1515
- C'est la faute déjà relevée pour `.zv-btn` et `.zv-meta` — une valeur qu'une
1516
- app aura légitimement besoin d'ajuster ne se pose pas en dur — mais vue d'un
1515
+ C'est la faute déjà relevée pour `.zv-btn` et `.zv-meta`, une valeur qu'une
1516
+ app aura légitimement besoin d'ajuster ne se pose pas en dur, mais vue d'un
1517
1517
  angle qui la rend **pire**. Une couleur posée en dur, l'app peut au moins
1518
1518
  tenter un `className` et constater que rien ne bouge. Un `alt`, le CSS ne le
1519
1519
  peint pas : l'app n'a **aucun recours**. Et le défaut est invisible à
@@ -1524,19 +1524,19 @@ Deux règles en sortent.
1524
1524
 
1525
1525
  **Le glyphe d'une page d'erreur est DÉCORATIF par défaut** (`glypheAlt=""`).
1526
1526
  Sur une page qui annonce déjà « Erreur 404 » et « Page introuvable », la
1527
- marque n'ajoute rien — l'annoncer place un nom de produit devant
1527
+ marque n'ajoute rien : l'annoncer place un nom de produit devant
1528
1528
  l'information que le lecteur est venu chercher. La prop existe pour l'app
1529
1529
  qui a une raison de la nommer.
1530
1530
 
1531
1531
  **Et un garde, parce qu'un correctif ponctuel ne protège que ce cas-là.**
1532
1532
  `tests/react.test.js` refuse désormais toute littérale dans un `alt` ou un
1533
- `aria-label` de la couche React — le catalogue excepté, puisque sa copie EST
1534
- l'exemple à recopier — et exige qu'un `<img>` porte un `alt`, fût-il vide :
1533
+ `aria-label` de la couche React (le catalogue excepté, puisque sa copie EST
1534
+ l'exemple à recopier) et exige qu'un `<img>` porte un `alt`, fût-il vide :
1535
1535
  sans attribut du tout, un lecteur d'écran annonce le nom du fichier, souvent
1536
1536
  un data-URI de 60 Ko. Le garde a été vérifié en réintroduisant la faute : il
1537
1537
  la signale à la ligne près.
1538
1538
 
1539
- Il a aussi attrapé son propre défaut au premier jet — un quantificateur
1539
+ Il a aussi attrapé son propre défaut au premier jet : un quantificateur
1540
1540
  paresseux traversait `alt=""` et attrapait l'attribut suivant.
1541
1541
 
1542
1542
  ## Le lanceur ne liste que ce qui s'ouvre (28/08/2026, 0.34.0)
@@ -1548,20 +1548,110 @@ correction descend dans les sept apps, aucune n'a de liste à tenir.
1548
1548
  **Un produit sans adresse n'entre pas dans le lanceur.** Plume et Supernovia
1549
1549
  ont leur glyphe depuis le 26/08 et rien à ouvrir. Une tuile qui mène à rien
1550
1550
  est pire qu'une absence : elle promet un produit, le visiteur clique, et il
1551
- tombe. Le lanceur se construit donc sur `APPS` — les produits qui ont une
1552
- adresse — et non sur `PRODUITS`. La liste se DÉDUIT du registre : le jour où
1551
+ tombe. Le lanceur se construit donc sur `APPS`, les produits qui ont une
1552
+ adresse, et non sur `PRODUITS`. La liste se DÉDUIT du registre : le jour où
1553
1553
  Plume ouvre, une ligne l'y fait apparaître partout.
1554
1554
 
1555
1555
  **https, toujours.** `memoire.zevra.tech` avait été communiqué en `http` :
1556
1556
  cette adresse-là répond **404** (vérifié au curl), la version chiffrée répond
1557
- 200. Un test refuse désormais toute adresse non chiffrée — l'erreur ne se
1557
+ 200. Un test refuse désormais toute adresse non chiffrée : l'erreur ne se
1558
1558
  serait pas vue en relecture, et elle aurait été le même lien mort dans les
1559
1559
  sept apps.
1560
1560
 
1561
1561
  **Et une conséquence de mise en page, qui n'était pas dans la demande.** À
1562
- cinq tuiles, la grille à quatre colonnes redonnait 4 + 1 — la dernière rangée
1562
+ cinq tuiles, la grille à quatre colonnes redonnait 4 + 1 : la dernière rangée
1563
1563
  d'une seule tuile que le passage à quatre colonnes avait justement servi à
1564
1564
  supprimer pour sept. Une requête de quantité (`:has(> :nth-child(5):last-child)`)
1565
1565
  repasse à trois colonnes pour ce compte-là seulement, et **se désarme
1566
1566
  d'elle-même** : vérifié au navigateur, 3 colonnes à 5 tuiles, 4 dès qu'il y
1567
1567
  en a 6 ou 7. Personne n'aura à y penser quand la suite s'agrandira.
1568
+
1569
+ ## Les panneaux de la barre débordaient de l'écran par la gauche (28/08/2026, 0.35.0)
1570
+
1571
+ Trouvé en mesurant mcp-factory dans une iframe de 375 px : la seule façon de
1572
+ juger un téléphone quand la fenêtre du navigateur ne redimensionne pas le
1573
+ viewport.
1574
+
1575
+ `.zv-lanceur__panneau` et `.zv-compte__panneau` sont en `position: absolute;
1576
+ right: 0` : ils se calent sur le bord droit de LEUR DÉCLENCHEUR. Or le
1577
+ lanceur n'est pas au bord de l'écran : le compte et le burger le suivent,
1578
+ soit une soixantaine de pixels. Le plafond `max-width: calc(100vw - 32px)`,
1579
+ lui, a été écrit en pensant un panneau ancré au bord de l'écran : son
1580
+ commentaire dit « au bord droit d'un téléphone ».
1581
+
1582
+ Les deux ensemble donnent un panneau à la bonne LARGEUR mais à la mauvaise
1583
+ PLACE. Mesuré à 375 px : largeur 343 (le plafond fait son travail), bord
1584
+ gauche à **−65**. Deux tuiles sur cinq étaient coupées, et la partie hors
1585
+ écran était inatteignable.
1586
+
1587
+ ⚠️ ET C'EST SILENCIEUX. Un débordement par la GAUCHE n'engendre aucun
1588
+ défilement horizontal : le navigateur le rogne, sans rien signaler. Le
1589
+ contrôle « la page ne déborde pas » que l'on fait spontanément répondait donc
1590
+ OUI pendant que deux produits étaient hors de portée.
1591
+
1592
+ Sous le point de rupture, les panneaux s'ancrent désormais à l'ÉCRAN et non à
1593
+ leur déclencheur : `left: 16px; right: 16px`, largeur pleine. C'est le motif
1594
+ que tout le monde connaît sur téléphone, et il ne dépend plus de ce qui est
1595
+ posé à droite du déclencheur : un jour où la barre gagnera une entrée, rien
1596
+ ne sera à recalculer. Le plafond en `100vw` devient alors redondant : il
1597
+ reste, il ne coûte rien et il protège un panneau posé ailleurs.
1598
+
1599
+ **Ce qui a été écarté** : réduire le plafond de la largeur du burger et du
1600
+ compte. Ça marche tant que personne ne touche à la barre, et ça casse au
1601
+ premier ajout : une constante qui encode la mise en page d'à côté.
1602
+
1603
+ ## Le tiret cadratin est banni (29/08/2026, 0.35.0)
1604
+
1605
+ Règle impérative, inscrite dans AGENTS.md avec celles qui ne se négocient
1606
+ pas : le signe U+2014 ne paraît plus dans le dépôt. Ni le code, ni les
1607
+ commentaires, ni la documentation, ni la copie rendue.
1608
+
1609
+ Pourquoi c'est une règle et pas un goût : le tiret cadratin est le signe
1610
+ qu'on pose quand on n'a pas tranché. Le français en a de plus précis, et il
1611
+ faut choisir lequel. **Deux-points** quand ce qui suit explique ce qui
1612
+ précède, **virgule** quand la phrase porte déjà un « : », **parenthèses**
1613
+ pour une incise qui contient elle-même des virgules, **trait d'union** pour
1614
+ une puce de liste. C'est la répartition qu'a suivie le passage : 600
1615
+ deux-points, 222 virgules, 132 parenthèses, 87 incises, 12 puces.
1616
+
1617
+ Le **demi-cadratin** U+2013 reste autorisé dans son seul emploi légitime,
1618
+ l'intervalle (« 1 – 25 sur 128 », « 0,14–0,2em »). Autre signe, autre
1619
+ travail.
1620
+
1621
+ ### Ce que ce nettoyage a failli casser
1622
+
1623
+ Un premier transformateur appliquait des expressions régulières de nettoyage
1624
+ sur le fichier ENTIER. Il a fusionné
1625
+
1626
+ .zv-sidelink--on,
1627
+ .zv-sidelink[aria-current="page"] {
1628
+
1629
+ en un seul sélecteur. Deux règles CSS devenues une, en silence, et aucun
1630
+ test du dépôt n'aurait pu le voir : les classes existaient toujours. La
1631
+ règle qui en sort vaut au-delà de ce cas : **un tiret cadratin ne vit que
1632
+ dans de la prose**, donc on n'ouvre jamais le reste du fichier. Le second
1633
+ transformateur ne touche qu'aux commentaires, aux chaînes et au markdown, et
1634
+ un script de preuve compare le squelette de chaque fichier (code sans
1635
+ commentaires ni contenu de chaînes) avant et après. Il est resté identique.
1636
+
1637
+ Deux autres défauts, attrapés à la relecture du diff et non par un test :
1638
+ les espaces se règlent AU POINT D'INSERTION, jamais sur le fichier (« site ,
1639
+ c'est » et « page d'admin ) » venaient de là), et la ponctuation ne se
1640
+ double pas, parce que le français ferme souvent une incise par un tiret
1641
+ suivi d'une virgule (un « halo » suivi du tiret puis d'une virgule donnait « halo,, parce que »).
1642
+
1643
+ ### Le garde, et sa particularité
1644
+
1645
+ `tests/paquet.test.js` refuse le signe dans tout fichier suivi par git, hors
1646
+ binaires. Sans lui la règle durerait un commit : elle comptait **1 079
1647
+ infractions** le jour où elle a été écrite, ce qui est sa propre
1648
+ démonstration.
1649
+
1650
+ ⚠️ Une règle qui bannit un caractère ne peut pas le porter. Le motif du test
1651
+ s'écrit donc en échappement `\u2014`, et AGENTS.md nomme le signe par son
1652
+ point de code : sinon le garde se dénonce lui-même à la première exécution.
1653
+ C'est exactement ce qu'il a fait au premier jet, et il avait raison.
1654
+
1655
+ Il a aussi rendu un service immédiat : un `git checkout` d'essai avait
1656
+ annulé le nettoyage de `tokens.css`, encore non commité. Le garde a signalé
1657
+ la régression dans la minute.