@zevra/ui 0.17.0 → 0.21.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.
@@ -41,10 +41,34 @@
41
41
  box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
42
42
  }
43
43
 
44
- .zv-btn {
45
- /* Le sommet du dégradé, relevé dans Main.html et SANS token : écrit une
46
- * seule fois ici, les états de survol le dérivent par color-mix. */
47
- --zv-btn-haut: #2b2bb4;
44
+ /* ─── Le plein ne s'applique QUE si aucune variante n'est posée ────────
45
+ *
46
+ * `zv-btn`, `zv-btn--ghost`, `zv-btn--ink` et `zv-btn--danger` sont quatre
47
+ * classes AUTONOMES et mutuellement exclusives : le socle commun ci-dessus
48
+ * les groupe dans un même sélecteur, ce qui leur donne exactement l'air
49
+ * d'une base et de ses modificateurs. `Button` n'en rend jamais qu'une —
50
+ * mais une app qui écrit ses classes à la main lit du BEM et compose les
51
+ * deux. C'est arrivé (mcp-factory, 0.19.0), et le résultat était vicieux :
52
+ * `.zv-btn:hover` (0,2,0) l'emportait sur `.zv-btn--ghost` (0,1,0), d'où un
53
+ * fond indigo sous un texte encre — illisible, et seulement AU SURVOL, donc
54
+ * invisible en relecture comme en capture.
55
+ *
56
+ * Le `:not(:where(…))` dit ce que le système voulait déjà dire : ces quatre
57
+ * classes s'excluent. `:where()` a une spécificité NULLE — la garde ne
58
+ * change donc aucun arbitrage existant, et une app qui surcharge `.zv-btn`
59
+ * continue de gagner exactement comme avant.
60
+ *
61
+ * ⚠️ À reporter sur toute règle future qui peint le bouton plein. */
62
+
63
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)) {
64
+ /* Le sommet du dégradé. RELEVÉ dans Main.html à #2b2bb4, mais DÉRIVÉ de
65
+ * --accent depuis la 0.19.0 : la déclaration étant portée par le bouton
66
+ * lui-même, une redéfinition de marque héritée d'un conteneur ne
67
+ * l'atteignait pas — un cabinet en marque blanche obtenait un dégradé
68
+ * allant de l'indigo Zevra à SA couleur. Pour la charte par défaut,
69
+ * l'écart avec la valeur relevée est d'un point de bleu, invisible.
70
+ * Les états de survol le dérivent ensuite par color-mix. */
71
+ --zv-btn-haut: var(--accent);
48
72
  background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
49
73
  color: var(--white);
50
74
  /* Le liseré interne fait tout le galbe du bouton : il est dans Main.html
@@ -88,7 +112,7 @@
88
112
  * chose.
89
113
  * Déclarés AVANT [disabled], qui doit l'emporter à spécificité égale. */
90
114
 
91
- .zv-btn:hover {
115
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):hover {
92
116
  background: linear-gradient(
93
117
  180deg,
94
118
  color-mix(in oklab, var(--zv-btn-haut) 86%, var(--ink)) 0%,
@@ -96,7 +120,7 @@
96
120
  );
97
121
  }
98
122
 
99
- .zv-btn:active {
123
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):active {
100
124
  background: linear-gradient(
101
125
  180deg,
102
126
  color-mix(in oklab, var(--zv-btn-haut) 72%, var(--ink)) 0%,
@@ -177,8 +201,8 @@
177
201
  * désactivé OU occupé (dont le clic est avalé par la façade) : sans ces
178
202
  * règles, les états ci-dessus repeignent le fond et le bouton a l'air
179
203
  * actif. Même précaution que les contrôles désactivés de forms.css. */
180
- .zv-btn[disabled]:is(:hover, :active),
181
- .zv-btn[aria-busy='true']:is(:hover, :active) {
204
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[disabled]:is(:hover, :active),
205
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[aria-busy='true']:is(:hover, :active) {
182
206
  background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
183
207
  }
184
208
 
@@ -612,8 +636,23 @@
612
636
  /* ─── FAQ ───────────────────────────────────────────────────────────────
613
637
  * Cinq questions par page au maximum. */
614
638
 
639
+ /* Le filet, comme `.zv-fold` : sans lui, la FAQ est un aplat blanc posé sur
640
+ * le papier, sans début ni fin. Toutes les autres surfaces blanches de la
641
+ * vitrine — carte, encart, tarif — en portent un ; celle-ci faisait tache. */
615
642
  .zv-faq {
616
643
  background: var(--white);
644
+ border: 1px solid var(--line);
645
+ }
646
+
647
+ /* Chaque question est un `<details>` : le repli est natif, sans JavaScript.
648
+ * Le filet sépare les questions ENTRE elles ; la première n'en porte pas, il
649
+ * tomberait sur celui du bloc et ferait un trait double. */
650
+ .zv-faq__item {
651
+ border-top: 1px solid var(--hairline);
652
+ }
653
+
654
+ .zv-faq__item:first-child {
655
+ border-top: 0;
617
656
  }
618
657
 
619
658
  .zv-faq__q {
@@ -622,22 +661,54 @@
622
661
  align-items: center;
623
662
  gap: 16px;
624
663
  padding: 18px 24px;
625
- border-bottom: 1px solid var(--hairline);
626
664
  font-size: 14px;
627
665
  font-weight: 600;
628
666
  cursor: pointer;
667
+ /* Le marqueur natif du `<summary>` tombe : le chevron du composant fait le
668
+ * même travail sans imposer le triangle du navigateur. Même traitement que
669
+ * `.zv-fold__titre`. */
670
+ list-style: none;
671
+ }
672
+
673
+ .zv-faq__q::-webkit-details-marker {
674
+ display: none;
675
+ }
676
+
677
+ /* Une FAQ de vitrine veut ses questions dans le PLAN DU DOCUMENT : c'est ce
678
+ * que lisent les moteurs, et ce que parcourt un lecteur d'écran qui saute de
679
+ * titre en titre. On accepte donc un `<h2>`/`<h3>`/`<h4>` comme question,
680
+ * et on lui retire ce que le navigateur lui met — taille, graisse, marges —
681
+ * pour qu'il se rende comme le reste des questions. Le niveau reste au
682
+ * balisage, l'apparence au système. */
683
+ .zv-faq__q > :is(h2, h3, h4) {
684
+ margin: 0;
685
+ font: inherit;
686
+ color: inherit;
687
+ }
688
+
689
+ .zv-faq__q:hover {
690
+ background: var(--panel);
629
691
  }
630
692
 
631
- .zv-faq__q svg {
693
+ /* Le chevron porte sa classe : cibler tout `svg` du titre attraperait aussi
694
+ * une icône posée dans le libellé, qui se verrait rétrécie et retournée. */
695
+ .zv-faq__chevron {
632
696
  width: 14px;
633
697
  height: 14px;
634
698
  flex: none;
635
699
  color: var(--accent);
700
+ transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
701
+ }
702
+
703
+ .zv-faq__item[open] .zv-faq__chevron {
704
+ rotate: 180deg;
636
705
  }
637
706
 
638
707
  .zv-faq__a {
639
- padding: 14px 24px;
708
+ margin: 0;
709
+ padding: 0 24px 18px;
640
710
  font-size: 13px;
711
+ line-height: 1.6;
641
712
  color: var(--muted);
642
713
  }
643
714
 
@@ -911,6 +982,13 @@
911
982
 
912
983
 
913
984
  @media (max-width: 767px) {
985
+
986
+ /* Le `min-width: 260px` de la valeur ne tient pas sous le seuil : dans une
987
+ * carte de 215px — un écran de 320px — elle débordait de 45px. La CLÉ
988
+ * était déjà libérée (flex-basis 100%), il manquait la valeur. */
989
+ .zv-rows__val {
990
+ min-width: 0;
991
+ }
914
992
  .zv-btn--sm {
915
993
  height: 36px;
916
994
  padding: 0 14px;
@@ -1029,3 +1107,258 @@
1029
1107
  .zv-lockup--marque-seule .zv-lockup__word {
1030
1108
  display: none;
1031
1109
  }
1110
+
1111
+ /* ─── Cadre média ───────────────────────────────────────────────────────
1112
+ * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des dix planches ne montre
1113
+ * d'image de contenu — elles sont composées, pas photographiées. Le manque
1114
+ * était pourtant total : pas un `aspect-ratio`, pas un `object-fit` dans le
1115
+ * paquet, et quatre réécritures du même cadre chez un seul consommateur.
1116
+ *
1117
+ * ⚠️ Le rapport vit sur le CADRE, jamais sur l'image. Posé sur un enfant de
1118
+ * grille aligné au centre, il ne se résout pas et la colonne s'effondre à
1119
+ * zéro — c'est un correctif de production, pas une précaution théorique.
1120
+ *
1121
+ * Le fond est posé et non laissé transparent : une image qui tarde, échoue
1122
+ * ou porte de la transparence laisserait voir la surface du dessous, soit un
1123
+ * trou blanc sur une carte, soit un trou sombre sur une bande. */
1124
+
1125
+ .zv-media {
1126
+ position: relative;
1127
+ display: block;
1128
+ width: 100%;
1129
+ overflow: hidden;
1130
+ background: var(--hairline);
1131
+ /* Angle vif comme le reste ; aucune ombre, l'image est dans le flux. */
1132
+ border-radius: 0;
1133
+ }
1134
+
1135
+ /* Les quatre rapports du système. Un cinquième se demande, il ne s'invente
1136
+ * pas : c'est ainsi qu'une bibliothèque finit avec dix-sept rapports dont
1137
+ * trois servent. */
1138
+ .zv-media--16-9 {
1139
+ aspect-ratio: 16 / 9;
1140
+ }
1141
+
1142
+ .zv-media--4-3 {
1143
+ aspect-ratio: 4 / 3;
1144
+ }
1145
+
1146
+ .zv-media--1-1 {
1147
+ aspect-ratio: 1 / 1;
1148
+ }
1149
+
1150
+ .zv-media--3-2 {
1151
+ aspect-ratio: 3 / 2;
1152
+ }
1153
+
1154
+ /* L'image REMPLIT le cadre : c'est le seul comportement qui le garantisse
1155
+ * plein quelle que soit la source, et une grille dont les couvertures ne
1156
+ * s'alignent pas se voit aussitôt. En absolu, pour que le cadre garde la
1157
+ * main sur sa hauteur. */
1158
+ .zv-media__img {
1159
+ position: absolute;
1160
+ inset: 0;
1161
+ width: 100%;
1162
+ height: 100%;
1163
+ object-fit: cover;
1164
+ display: block;
1165
+ }
1166
+
1167
+ /* Le cas inverse — un logo, un schéma, une pièce qu'on ne peut pas rogner.
1168
+ * Le papier passe derrière : il restera du vide, autant qu'il soit choisi. */
1169
+ .zv-media--contient {
1170
+ background: var(--paper);
1171
+ }
1172
+
1173
+ .zv-media--contient .zv-media__img {
1174
+ object-fit: contain;
1175
+ }
1176
+
1177
+ /* Le coin d'un cadre : durée d'une vidéo, nombre de pages, mention de
1178
+ * réserve. Encre diluée et non un gris opaque — le fond d'image est
1179
+ * imprévisible, et l'encre de charte s'y pose sans jurer. */
1180
+ .zv-media__coin {
1181
+ position: absolute;
1182
+ right: 8px;
1183
+ bottom: 8px;
1184
+ z-index: 1;
1185
+ display: inline-flex;
1186
+ align-items: center;
1187
+ gap: 4px;
1188
+ padding: 3px 7px;
1189
+ background: color-mix(in srgb, var(--ink) 72%, transparent);
1190
+ color: var(--white);
1191
+ font-family: var(--font-mono);
1192
+ font-size: var(--mono);
1193
+ font-weight: 500;
1194
+ letter-spacing: 0.08em;
1195
+ }
1196
+
1197
+ .zv-media__coin--gauche {
1198
+ right: auto;
1199
+ left: 8px;
1200
+ }
1201
+
1202
+ .zv-media__coin--haut {
1203
+ bottom: auto;
1204
+ top: 8px;
1205
+ }
1206
+
1207
+ /* Voile de lecture : un titre posé SUR une image n'est lisible qu'au-dessus
1208
+ * d'un dégradé. Il part du bas, là où le texte se pose, et n'assombrit pas
1209
+ * le sujet. Il ne capte pas le pointeur — le cadre reste cliquable. */
1210
+ .zv-media__voile {
1211
+ position: absolute;
1212
+ inset: 0;
1213
+ z-index: 1;
1214
+ pointer-events: none;
1215
+ background: linear-gradient(
1216
+ 180deg,
1217
+ transparent 40%,
1218
+ color-mix(in srgb, var(--ink) 78%, transparent) 100%
1219
+ );
1220
+ }
1221
+
1222
+ /* ─── Page légale ───────────────────────────────────────────────────────
1223
+ * HORS RELEVÉ (27/08/2026, voir DECISIONS.md). La forme d'un document
1224
+ * contractuel — CGVU, mentions légales, DPA. Le TEXTE, lui, n'est pas ici :
1225
+ * il vit dans @zevra/legal, versionné et archivé. Ce module ne porte que la
1226
+ * mise en page ; c'est ce qui permet au même texte de s'afficher sur la
1227
+ * vitrine et dans une application sans exister en deux exemplaires.
1228
+ *
1229
+ * Presque tout se compose de pièces existantes : le sommaire est .zv-rail,
1230
+ * les définitions .zv-rows, les tableaux .zv-table, les encarts .zv-callout.
1231
+ * Ne restent ici que la colonne de lecture et ce que les listes de texte
1232
+ * courant n'avaient jamais eu. */
1233
+
1234
+ .zv-legal {
1235
+ display: flex;
1236
+ flex-direction: column;
1237
+ gap: 48px;
1238
+ /* La MESURE, et c'est la seule règle qui compte vraiment pour un contrat :
1239
+ * au-delà d'environ 80 signes par ligne, l'œil perd le début de la ligne
1240
+ * suivante — sur un texte qu'on lit déjà à reculons, c'est rédhibitoire. */
1241
+ max-width: 72ch;
1242
+ }
1243
+
1244
+ .zv-legal__section {
1245
+ display: flex;
1246
+ flex-direction: column;
1247
+ gap: 14px;
1248
+ /* Un lien vers un article précis ne doit pas déposer son titre sous la
1249
+ * barre fixe. Sans cette réserve, cliquer « Art. 7 » dans le sommaire
1250
+ * affiche le corps de l'article et cache son intitulé. */
1251
+ scroll-margin-top: 88px;
1252
+ }
1253
+
1254
+ /* Les listes du texte courant. Le paquet n'en avait aucune : ses listes
1255
+ * étaient toutes des objets (étapes, sommaires numérotés, rangées à filets),
1256
+ * jamais l'énumération d'une clause. */
1257
+ .zv-legal__liste {
1258
+ margin: 0;
1259
+ padding-left: 22px;
1260
+ display: flex;
1261
+ flex-direction: column;
1262
+ gap: 8px;
1263
+ font-size: var(--body);
1264
+ line-height: 1.6;
1265
+ color: var(--muted);
1266
+ }
1267
+
1268
+ .zv-legal__liste li::marker {
1269
+ color: var(--accent-line);
1270
+ }
1271
+
1272
+ /* Le renvoi vers un autre document contractuel, dans le fil d'une clause :
1273
+ * souligné, parce qu'à l'intérieur d'un paragraphe la couleur seule ne
1274
+ * suffit pas à annoncer qu'on change de contrat. */
1275
+ .zv-legal a {
1276
+ color: var(--accent);
1277
+ text-decoration: underline;
1278
+ text-underline-offset: 2px;
1279
+ }
1280
+
1281
+ /* Le tableau d'un contrat (durées de conservation, sous-traitants). Il n'
1282
+ * emprunte PAS .zv-table de data.css : celui-là est une surface de travail
1283
+ * dont les rangées se survolent et se cliquent, ce qu'un tableau de contrat
1284
+ * ne fait jamais. Filets, chiffres alignés, et rien de plus. */
1285
+ .zv-legal__table {
1286
+ width: 100%;
1287
+ border-collapse: collapse;
1288
+ font-size: var(--app);
1289
+ line-height: 1.55;
1290
+ color: var(--ink-2);
1291
+ }
1292
+
1293
+ .zv-legal__table th {
1294
+ border-bottom: 1px solid var(--line);
1295
+ padding: 8px 12px 8px 0;
1296
+ text-align: left;
1297
+ font-family: var(--font-mono);
1298
+ font-size: var(--mono);
1299
+ font-weight: 600;
1300
+ letter-spacing: 0.14em;
1301
+ text-transform: uppercase;
1302
+ color: var(--label);
1303
+ }
1304
+
1305
+ .zv-legal__table td {
1306
+ border-bottom: 1px solid var(--hairline);
1307
+ padding: 10px 12px 10px 0;
1308
+ vertical-align: top;
1309
+ font-variant-numeric: tabular-nums;
1310
+ }
1311
+
1312
+ /* L'en-tête : titre, date d'entrée en vigueur, identifiant de version.
1313
+ * ⚠️ La VERSION est affichée, et ce n'est pas décoratif : c'est ce que
1314
+ * l'utilisateur cite quand il conteste, et ce que le support lui demande. */
1315
+ .zv-legal__tete {
1316
+ display: flex;
1317
+ flex-direction: column;
1318
+ gap: 10px;
1319
+ border-bottom: 1px solid var(--line);
1320
+ padding-bottom: 24px;
1321
+ }
1322
+
1323
+ @media (max-width: 767px) {
1324
+ .zv-legal {
1325
+ gap: 36px;
1326
+ }
1327
+
1328
+ .zv-legal__section {
1329
+ scroll-margin-top: calc(var(--topbar-h) + 12px);
1330
+ }
1331
+ }
1332
+
1333
+ /* Encart d'accompagnement d'une clause : la reformulation en langage courant
1334
+ * (« En clair ») ou le point à ne pas manquer. Il COMMENTE la clause et ne
1335
+ * la remplace pas — d'où une surface d'accompagnement, et non l'aplat d'une
1336
+ * alerte, qui annoncerait un incident.
1337
+ * Il vit ici et non dans feedback.css : une page légale de vitrine n'a pas à
1338
+ * charger la couche applicative pour afficher une glose. */
1339
+ .zv-legal__encart {
1340
+ display: flex;
1341
+ flex-direction: column;
1342
+ gap: 6px;
1343
+ border-left: 2px solid var(--accent-line);
1344
+ background: var(--panel);
1345
+ padding: 14px 18px;
1346
+ }
1347
+
1348
+ .zv-legal__encart--important {
1349
+ border-left-color: var(--warn);
1350
+ background: var(--warn-soft);
1351
+ }
1352
+
1353
+ .zv-legal__encart-label {
1354
+ font-family: var(--font-mono);
1355
+ font-size: var(--mono);
1356
+ font-weight: 700;
1357
+ letter-spacing: 0.14em;
1358
+ text-transform: uppercase;
1359
+ color: var(--label);
1360
+ }
1361
+
1362
+ .zv-legal__encart--important .zv-legal__encart-label {
1363
+ color: var(--warn-ink);
1364
+ }
package/src/forms.css CHANGED
@@ -1001,9 +1001,15 @@
1001
1001
  min-height: var(--tap);
1002
1002
  }
1003
1003
 
1004
- .zv-input--bare {
1004
+ /* Le champ nu et celui d'un cadre à affixe. 16px et non --app-lg (14px) :
1005
+ * la règle des 16px ci-dessus vaut pour TOUT champ qu'on saisit au doigt,
1006
+ * et ces deux-là — le composeur, le sous-domaine d'organisation — sont
1007
+ * parmi les plus saisis d'une application. Sous 16, Safari iOS zoome à la
1008
+ * mise au point et décale la page. */
1009
+ .zv-input--bare,
1010
+ .zv-affix .zv-input--bare {
1005
1011
  min-height: var(--tap);
1006
- font-size: var(--app-lg);
1012
+ font-size: 16px;
1007
1013
  }
1008
1014
 
1009
1015
  /* Le cadre passe pleine largeur. Il n'est PAS figé (`flex: none`) : le
@@ -1015,8 +1021,11 @@
1015
1021
  flex: 0 1 auto;
1016
1022
  }
1017
1023
 
1024
+ /* Le champ de recherche n'était visé par aucune des deux règles : il
1025
+ * gardait --app (13px), donc il zoomait lui aussi. */
1018
1026
  .zv-search__input {
1019
1027
  padding-block: 12px;
1028
+ font-size: 16px;
1020
1029
  }
1021
1030
 
1022
1031
  /* Le déclencheur peut passer à la ligne : sa valeur courante est un nom de
@@ -1075,3 +1084,57 @@
1075
1084
  color: var(--accent);
1076
1085
  }
1077
1086
  }
1087
+
1088
+ /* ─── Champ mot de passe ────────────────────────────────────────────────
1089
+ * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des planches ne montre d'écran de
1090
+ * connexion, et le système n'avait donc rien pour ce champ — alors que tous
1091
+ * les produits en ont un, et que la saisie masquée est le seul endroit de
1092
+ * l'interface où l'on tape sans voir ce qu'on écrit.
1093
+ *
1094
+ * Bâti sur `.zv-affix` plutôt qu'en cadre parallèle : le champ nu, le cadre
1095
+ * qui l'entoure et son anneau de mise au point sont déjà là.
1096
+ *
1097
+ * ⚠️ Le bouton est un vrai <button type="button"> : posé sur un <span>, il
1098
+ * n'est ni atteignable au clavier ni annoncé, et il envoie le formulaire dès
1099
+ * qu'on oublie le type. Son libellé change avec l'état — « Afficher » /
1100
+ * « Masquer » — parce qu'une icône seule ne dit pas ce qui va se passer. */
1101
+
1102
+ .zv-affix__btn {
1103
+ display: inline-flex;
1104
+ align-items: center;
1105
+ justify-content: center;
1106
+ flex: none;
1107
+ /* Cible tactile obtenue par la taille, pas par du remplissage : un bouton
1108
+ * de 16 px entouré de marge reste un bouton de 16 px pour le doigt. */
1109
+ width: 32px;
1110
+ height: 32px;
1111
+ margin-right: -6px;
1112
+ padding: 0;
1113
+ border: 0;
1114
+ border-radius: 0;
1115
+ background: none;
1116
+ color: var(--muted);
1117
+ cursor: pointer;
1118
+ transition: color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);
1119
+ }
1120
+
1121
+ .zv-affix__btn:hover {
1122
+ color: var(--ink);
1123
+ }
1124
+
1125
+ .zv-affix__btn[aria-pressed='true'] {
1126
+ color: var(--accent);
1127
+ }
1128
+
1129
+ /* Le contour déborderait du cadre à affixe, qui le rogne. On le rentre. */
1130
+ .zv-affix__btn:focus-visible {
1131
+ outline-offset: -2px;
1132
+ }
1133
+
1134
+ /* Les gestionnaires de mot de passe des navigateurs posent leur propre œil
1135
+ * dans le champ : deux boutons côte à côte pour la même chose. On masque le
1136
+ * leur, le nôtre étant accessible et cohérent avec la charte. */
1137
+ .zv-affix input[type='password']::-ms-reveal,
1138
+ .zv-affix input[type='password']::-ms-clear {
1139
+ display: none;
1140
+ }
package/src/icons.css CHANGED
@@ -81,6 +81,26 @@
81
81
  color: var(--ink);
82
82
  }
83
83
 
84
+ /* Désactivé. C'était la SEULE classe interactive du module sans cet état :
85
+ * .zv-btn, .zv-chrome-btn, .zv-menu__item, .zv-pager__arrow, .zv-range et
86
+ * les champs en ont tous un. Sans lui, un bouton fermé se rendait comme un
87
+ * bouton actif — curseur main compris — et s'allumait au survol comme s'il
88
+ * allait agir.
89
+ * ⚠️ Le survol est neutralisé EXPLICITEMENT : `:hover` s'applique encore à
90
+ * un bouton désactivé, et l'application ne peut pas le rattraper depuis une
91
+ * couche d'utilitaires — ce module n'est pas layered. */
92
+ .zv-icon-btn[disabled],
93
+ .zv-icon-btn[aria-disabled='true'] {
94
+ opacity: 0.5;
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ .zv-icon-btn[disabled]:hover,
99
+ .zv-icon-btn[aria-disabled='true']:hover {
100
+ background: transparent;
101
+ color: var(--muted);
102
+ }
103
+
84
104
  /* Icône décorative posée à côté d'un libellé qui la nomme déjà : elle est
85
105
  * atténuée pour que le mot reste le point d'entrée du regard. */
86
106
  .zv-icon--quiet {
package/src/layout.css CHANGED
@@ -395,13 +395,51 @@
395
395
  padding-top: 22px;
396
396
  padding-bottom: 12px;
397
397
  z-index: 40;
398
+ /* Le voile est PLEIN derrière la barre, et il se dégrade EN DESSOUS d'elle
399
+ * (::after). Il était auparavant dégradé sur toute sa hauteur : à sa base
400
+ * il ne restait donc presque rien, et un titre de 38 ou 56px qui passait
401
+ * dessous se retrouvait COUPÉ EN DEUX — moitié voilée, moitié nette, avec
402
+ * les entrées de nav posées sur ses lettres. Deux textes superposés, aucun
403
+ * lisible. Signalé en recette sur mcp-factory.
404
+ * Le dégradé n'est pas perdu : il vit sous la barre, là où il adoucit le
405
+ * passage sans se disputer la place avec la navigation. */
406
+ background: color-mix(in srgb, var(--paper) 92%, transparent);
407
+ /* ⚠️ LE PRÉFIXÉ D'ABORD. Lightning CSS — le compilateur de Tailwind v4,
408
+ * donc celui de toutes nos applications Next — SUPPRIME `backdrop-filter`
409
+ * non préfixé quand il précède la forme `-webkit-`. Or Firefox ne connaît
410
+ * QUE la forme non préfixée : la barre y perdait son flou. Reproduit avec
411
+ * la chaîne exacte des apps, dans les deux ordres. L'ordre est donc une
412
+ * contrainte, pas un style.
413
+ *
414
+ * Le voile, lui, reste en `color-mix` : le compilateur le traite
415
+ * CORRECTEMENT — il émet un repli opaque puis la vraie valeur sous
416
+ * `@supports`, ce qu'on lit dans les bundles livrés. Une note antérieure
417
+ * affirmait qu'il le « réduisait » ; elle prenait le repli pour le
418
+ * résultat. Ne pas « corriger » vers `rgb(from …)` : cette syntaxe sort du
419
+ * compilateur NUE, sans repli, et un navigateur qui l'ignore n'a alors
420
+ * plus AUCUN fond sous une barre fixe — 3,5 % d'usage qui voyaient le
421
+ * voile correctement et ne verraient plus rien. */
422
+ -webkit-backdrop-filter: blur(6px);
423
+ backdrop-filter: blur(6px);
424
+ }
425
+
426
+ /* La frange de dégradé, posée SOUS la barre. `pointer-events: none` : elle
427
+ * couvre le haut du contenu, elle ne doit rien intercepter. */
428
+ .zv-header::after {
429
+ content: '';
430
+ position: absolute;
431
+ top: 100%;
432
+ left: 0;
433
+ right: 0;
434
+ height: 24px;
398
435
  background: linear-gradient(
399
436
  180deg,
400
437
  color-mix(in srgb, var(--paper) 92%, transparent),
401
438
  color-mix(in srgb, var(--paper) 0%, transparent)
402
439
  );
403
- backdrop-filter: blur(6px);
404
440
  -webkit-backdrop-filter: blur(6px);
441
+ backdrop-filter: blur(6px);
442
+ pointer-events: none;
405
443
  }
406
444
 
407
445
  .zv-header:has(> .zv-nav) {
@@ -832,6 +832,15 @@
832
832
  * Sous ce seuil, tout ce qui se touche fait au moins --tap. */
833
833
 
834
834
  @media (max-width: 767px) {
835
+
836
+ /* Le fil ENTIER est un seul lien, et il n'avait aucune hauteur de cible :
837
+ * à 11px de texte il passe sous le minimum tactile. On déploie la cible
838
+ * sans toucher au corps. */
839
+ .zv-crumbs {
840
+ display: inline-flex;
841
+ align-items: center;
842
+ min-height: var(--tap);
843
+ }
835
844
  /* 224px de colonne à côté du contenu sur un écran de 375px ne laisse rien à
836
845
  * lire : la colonne cède la place au couple barre + tiroir. */
837
846
  .zv-sidebar {
@@ -982,6 +991,17 @@
982
991
  padding: 8px;
983
992
  }
984
993
 
994
+ /* La cible tactile, que le padding seul ne donne pas : un chevron de dix
995
+ * pixels entouré de huit reste sous --tap. `.zv-pager__step` la portait
996
+ * déjà ; ses deux voisines l'attendaient. */
997
+ .zv-pager__arrow,
998
+ .zv-pager__rank {
999
+ display: inline-flex;
1000
+ align-items: center;
1001
+ justify-content: center;
1002
+ min-height: var(--tap);
1003
+ }
1004
+
985
1005
  .zv-pager__rank {
986
1006
  padding-block: 8px;
987
1007
  }