@zevra/ui 0.1.0 → 0.2.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.
@@ -0,0 +1,814 @@
1
+ /* Zevra UI — navigation : le chrome de l'applicatif
2
+ * Colonne latérale, barre supérieure, tiroir, feuille de source citée, menu
3
+ * déroulant, fil d'Ariane, pagination, mécanique de défilement.
4
+ *
5
+ * DÉPEND DE :
6
+ * · tokens.css — neutres (--ink, --ink-2, --muted, --label, --line,
7
+ * --hairline, --panel, --white), --accent / --accent-soft, échelles
8
+ * --app-* et --mono-*, mesures --tap et --topbar-h, --shadow-menu ;
9
+ * · base.css — reset box-sizing, :focus-visible, style des liens sans
10
+ * classe, et la coupure globale prefers-reduced-motion (ce module ne la
11
+ * repose donc pas) ;
12
+ * · layout.css — l'inventaire des couches d'empilement, dont l'échelle
13
+ * ci-dessous est le prolongement ;
14
+ * · icons.css — .zv-icon (glyphes tracés) et .zv-icon-btn (bouton d'icône
15
+ * nu, cible --tap), réemployés ici plutôt que redéfinis.
16
+ * Ne repose ni le reset, ni le body, ni :root.
17
+ *
18
+ * NE REDÉFINIT RIEN de ce que les autres modules possèdent déjà :
19
+ * .zv-navlink et .zv-nav__* (nav de vitrine, layout.css) ne sont pas touchés —
20
+ * l'entrée applicative s'appelle .zv-sidelink ; le déclencheur du menu est
21
+ * .zv-select et l'onglet réemploie .zv-seg, tous deux dans forms.css.
22
+ *
23
+ * Ce module est un RELEVÉ de l'application Mémoire, en production, pas une
24
+ * proposition de DA : chaque valeur vient de src/components/app-shell.tsx,
25
+ * src/components/citation.tsx ou des écrans (chat, répertoire, fiche). Ce qui
26
+ * manque à la production — focus, états désactivés, clavier — est signalé par
27
+ * un ⚠️ à l'endroit exact plutôt que comblé en silence, et ce qui n'a AUCUNE
28
+ * source est étiqueté HORS RELEVÉ ou DÉRIVÉ, jamais présenté comme relevé.
29
+ *
30
+ * ⚠️ SEUIL. Mémoire bascule à `lg` (1024px) ; le paquet n'a qu'un point de
31
+ * rupture, 768px, et c'est celui-là qui s'applique. Ce qui est relevé, ce sont
32
+ * les DEUX jeux de valeurs (doigt / souris), jamais la largeur où ils
33
+ * basculent. Tout le responsive est en fin de fichier, sous la seule forme
34
+ * employée par base.css et components.css : `max-width: 767px`. Les règles de
35
+ * base décrivent donc l'écran large, et le bloc final ramène au doigt.
36
+ *
37
+ * ─── Empilement ────────────────────────────────────────────────────────
38
+ * layout.css tient l'inventaire des couches du paquet et s'arrête à 50
39
+ * (.zv-layer-top), avec la consigne « toute nouvelle couche s'arbitre par
40
+ * rapport à celle-ci ». Les recouvrements applicatifs se rangent donc
41
+ * AU-DESSUS, sans jamais se mettre à égalité avec une couche de vitrine : un
42
+ * voile de tiroir et un header fixe départagés par l'ordre du DOM, ce n'est
43
+ * pas une échelle. Le relevé donnait voile 40, menu 40, feuille 50, tiroir 60 ;
44
+ * seuls les RAPPORTS comptent, et ils sont conservés — l'origine est décalée
45
+ * en bloc pour dégager l'inventaire de vitrine :
46
+ *
47
+ * menu 60 · voile 60 · feuille de source 70 · tiroir 80
48
+ *
49
+ * L'égalité menu/voile est conservée parce qu'elle est assumée : les deux ne
50
+ * coexistent jamais (ouvrir le tiroir ferme le menu). C'est autre chose qu'une
51
+ * égalité subie entre deux pièces qui, elles, se croisent.
52
+ * N'insérer aucune valeur intermédiaire sans rejuger l'échelle entière.
53
+ *
54
+ * ─── Gabarit attendu ───────────────────────────────────────────────────
55
+ * Toutes ces pièces supposent une racine applicative en `height: 100dvh;
56
+ * overflow: hidden; display: flex` — colonne sous le seuil, rangée au-dessus.
57
+ * Sans elle, aucune zone défilante ne tient sa hauteur et c'est la page
58
+ * entière qui défile, ce qui ruine la colonne comme la feuille. Cette racine
59
+ * relève de la mise en page, pas de ce module (cf. notes).
60
+ */
61
+
62
+ /* ─── Colonne latérale ──────────────────────────────────────────────────
63
+ * Une colonne, un filet, rien d'autre : ni ombre, ni rayon. Une ombre ferait
64
+ * flotter une pièce qui ne bouge jamais, et le rayon appartient à l'action
65
+ * (--r-btn) ou à la carte (--r-card), pas au chrome. */
66
+
67
+ .zv-sidebar {
68
+ display: flex;
69
+ width: 224px;
70
+ /* Sans `flex: none`, une entrée de nav un peu longue élargit la colonne et
71
+ * décale le contenu : la largeur est une constante, pas un résultat. */
72
+ flex: none;
73
+ flex-direction: column;
74
+ /* La colonne défile pour son propre compte : une nav longue ne doit jamais
75
+ * entraîner le contenu, ni l'inverse. */
76
+ overflow-y: auto;
77
+ border-right: 1px solid var(--line);
78
+ background: var(--panel);
79
+ }
80
+
81
+ .zv-sidebar__head {
82
+ border-bottom: 1px solid var(--line);
83
+ padding: 20px 18px 14px;
84
+ }
85
+
86
+ /* `margin-top: auto` colle le pied en bas quand la nav est courte et le laisse
87
+ * suivre le contenu quand elle est longue. Un `position: sticky` aurait
88
+ * recouvert la dernière entrée dès que la colonne défile. */
89
+ .zv-sidebar__foot {
90
+ margin-top: auto;
91
+ border-top: 1px solid var(--line);
92
+ }
93
+
94
+ /* Le même balisage sert à l'aside et au panneau du tiroir. Dans le tiroir, la
95
+ * croix se pose en absolu par-dessus la tête : sans cette réserve, un logo
96
+ * client large passe dessous et devient incliquable. */
97
+ .zv-drawer .zv-sidebar__head > :first-child {
98
+ padding-right: 46px;
99
+ }
100
+
101
+ /* ─── Groupes de navigation ─────────────────────────────────────────────
102
+ * Nommés .zv-sidegroup et non .zv-navgroup : layout.css possède déjà .zv-nav,
103
+ * .zv-nav__logo, .zv-nav__links et .zv-navlink pour la nav de VITRINE. Un
104
+ * .zv-navgroup posé à côté se lirait comme une pièce de cette famille-là. La
105
+ * colonne applicative a sa propre lignée : sidebar › sidegroup › sidelink. */
106
+
107
+ .zv-sidegroup {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 2px;
111
+ padding: 12px 10px;
112
+ }
113
+
114
+ .zv-sidegroup__title {
115
+ padding: 2px 10px 8px;
116
+ font-family: var(--font-mono);
117
+ font-size: var(--mono);
118
+ font-weight: 600;
119
+ letter-spacing: 0.12em;
120
+ text-transform: uppercase;
121
+ color: var(--label);
122
+ }
123
+
124
+ /* À partir du deuxième groupe, c'est le blanc au-dessus de l'intitulé qui
125
+ * sépare — pas un filet. La colonne en porte déjà deux (tête et pied) ; un
126
+ * troisième trait la transformerait en tableau. */
127
+ .zv-sidegroup + .zv-sidegroup .zv-sidegroup__title {
128
+ padding-top: 14px;
129
+ }
130
+
131
+ /* ─── Entrée de barre latérale ──────────────────────────────────────────
132
+ * `.zv-navlink` appartient déjà à la navigation de vitrine (layout.css) :
133
+ * l'entrée applicative s'appelle `.zv-sidelink`, et les deux ne doivent
134
+ * jamais se recouvrir. */
135
+
136
+ .zv-sidelink {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 10px;
140
+ padding: 8px 10px;
141
+ font-family: var(--font-text);
142
+ font-size: var(--app);
143
+ font-weight: 500;
144
+ color: var(--ink-2);
145
+ text-decoration: none;
146
+ }
147
+
148
+ /* ⚠️ CONTRE-ORDRE, pas un style. base.css repeint les liens nus en accent, et
149
+ * l'application hôte va souvent plus loin (`a { color: accent }`,
150
+ * `a:hover { text-decoration: underline }`). Sans ces deux déclarations
151
+ * d'apparence redondante, l'entrée de nav vire au bleu souligné dès qu'elle
152
+ * tombe dans une page qui applique le style éditorial. */
153
+ .zv-sidelink:hover {
154
+ background: var(--hairline);
155
+ color: var(--ink-2);
156
+ text-decoration: none;
157
+ }
158
+
159
+ /* `aria-current="page"` est ajouté au relevé : la production ne marque
160
+ * l'entrée courante que visuellement, ce qui la rend muette au lecteur
161
+ * d'écran. Les deux sélecteurs valent la même chose pour que le balisage
162
+ * correct ne coûte rien. */
163
+ .zv-sidelink--on,
164
+ .zv-sidelink[aria-current="page"] {
165
+ background: var(--accent-soft);
166
+ font-weight: 600;
167
+ color: var(--accent);
168
+ }
169
+
170
+ /* Au survol de l'entrée courante, le fond change mais le texte reste accent :
171
+ * sinon la page où l'on se trouve « s'éteint » sous le pointeur. */
172
+ .zv-sidelink--on:hover,
173
+ .zv-sidelink[aria-current="page"]:hover {
174
+ background: var(--hairline);
175
+ color: var(--accent);
176
+ }
177
+
178
+ /* Le contour de base.css déborde de 2px ; dans une colonne en `overflow-y:
179
+ * auto` il est rogné par le bord du scroller. On le rentre plutôt que d'en
180
+ * redessiner un autre. */
181
+ .zv-sidelink:focus-visible {
182
+ outline-offset: -2px;
183
+ }
184
+
185
+ /* Largeur fixe : le rang mono et le glyphe ⚙ du panneau admin doivent tomber
186
+ * sur la même colonne, sinon les libellés se décalent d'un groupe à l'autre.
187
+ * La production écrit 10px en dur (app-shell.tsx:160) ; c'est exactement
188
+ * --mono, donc le token remplace la valeur sans rien déplacer. */
189
+ .zv-sidelink__num {
190
+ width: 16px;
191
+ flex: none;
192
+ font-family: var(--font-mono);
193
+ font-size: var(--mono);
194
+ font-weight: 500;
195
+ color: var(--label);
196
+ }
197
+
198
+ /* ─── Barre supérieure ──────────────────────────────────────────────────
199
+ * Elle remplace la colonne sous le seuil : masquée au-dessus, elle n'apparaît
200
+ * que dans le bloc mobile de fin de fichier. */
201
+
202
+ .zv-topbar {
203
+ display: none;
204
+ /* --topbar-h nomme la HAUTEUR DE LA BARRE, et elle seule : 52px. Le filet
205
+ * qui la termine est un pixel de plus, et c'est pour cela que la feuille de
206
+ * source commence à 53 (cf. .zv-cite-panel dans le bloc mobile). Les deux
207
+ * valeurs de la production sont justes et se répondent — les unifier sur 52
208
+ * ferait remonter la feuille par-dessus le filet.
209
+ * ⚠️ Nuance à connaître avant d'y toucher : base.css impose box-sizing:
210
+ * border-box, donc ce `border-bottom` est peint À L'INTÉRIEUR des 52px. Le
211
+ * modèle « 52 + 1 » n'est littéral que si le filet sort de la boîte —
212
+ * une ligne suffirait (`box-shadow: 0 1px 0 var(--line)` au lieu du
213
+ * `border-bottom`), mais c'est un écart au relevé : à arbitrer, pas à
214
+ * décider ici. */
215
+ height: var(--topbar-h);
216
+ flex: none;
217
+ align-items: center;
218
+ gap: 10px;
219
+ border-bottom: 1px solid var(--line);
220
+ background: var(--panel);
221
+ padding-inline: 16px;
222
+ }
223
+
224
+ /* Le logo cède AVANT le nom : un logo client très large ne doit jamais
225
+ * pousser la barre hors de l'écran. */
226
+ .zv-topbar__logo {
227
+ min-width: 0;
228
+ overflow: hidden;
229
+ }
230
+
231
+ .zv-topbar__title {
232
+ min-width: 0;
233
+ flex: 1 1 0%;
234
+ overflow: hidden;
235
+ text-overflow: ellipsis;
236
+ white-space: nowrap;
237
+ font-size: var(--app-sm);
238
+ font-weight: 600;
239
+ color: var(--ink);
240
+ }
241
+
242
+ /* Le hamburger n'est PAS un bouton propre à ce module : c'est le .zv-icon-btn
243
+ * d'icons.css (cible --tap, sans cadre, couleur --muted) posé dans la barre.
244
+ * Le balisage est donc `class="zv-icon-btn zv-topbar__burger"`, et seul le
245
+ * décalage optique appartient à la barre : la marge négative aligne le GLYPHE
246
+ * sur le bord de la barre alors que la boîte garde ses 40px de cible tactile —
247
+ * sans elle, il faut choisir entre l'alignement optique et le doigt.
248
+ * Le relevé ne donne AUCUN état au hamburger ; le survol d'icons.css peint
249
+ * --panel, qui est exactement le fond de cette barre — donc invisible ici.
250
+ * Seule la couleur du glyphe s'assombrit d'un cran, ce qui vient du composant
251
+ * partagé et non d'une décision de ce module. */
252
+ .zv-topbar__burger {
253
+ margin-right: -8px;
254
+ }
255
+
256
+ /* ─── Bouton de chrome (croix bordée) ───────────────────────────────────
257
+ * ⚠️ RENOMMÉ : ce bouton s'appelait .zv-icon-btn, nom que icons.css possède
258
+ * déjà pour tout autre chose (bouton nu de 40px, coins arrondis). Deux
259
+ * géométries sous un seul nom, c'est la dernière qui charge qui gagne : la
260
+ * croix bordée du chrome devient .zv-chrome-btn.
261
+ * Un seul emploi : la croix de fermeture du tiroir et de la feuille de source.
262
+ * Angles vifs — le rayon signale l'action, le chrome n'en est pas une. */
263
+
264
+ .zv-chrome-btn {
265
+ display: flex;
266
+ width: 26px;
267
+ height: 26px;
268
+ flex: none;
269
+ align-items: center;
270
+ justify-content: center;
271
+ border: 1px solid var(--line);
272
+ border-radius: 0;
273
+ background: var(--white);
274
+ /* base.css ne reset pas les boutons : sans police ni fond explicites, un
275
+ * <button> retombe sur ceux du navigateur et détonne. Même précaution que
276
+ * .zv-btn dans components.css. */
277
+ font-family: var(--font-text);
278
+ font-size: var(--app);
279
+ line-height: 1;
280
+ color: var(--muted);
281
+ cursor: pointer;
282
+ }
283
+
284
+ /* ⚠️ Divergence relevée : la croix du tiroir n'a AUCUN état en production,
285
+ * celle de la feuille de source porte ce survol. Retenu : le survol partout,
286
+ * un bouton sans réaction se lit comme un décor. */
287
+ .zv-chrome-btn:hover {
288
+ background: var(--hairline);
289
+ }
290
+
291
+ .zv-chrome-btn:focus-visible {
292
+ outline-offset: -2px;
293
+ }
294
+
295
+ /* ⚠️ HORS RELEVÉ : aucun bouton de chrome n'est jamais désactivé en
296
+ * production. Posé parce qu'un bouton grisé sans style se lit comme un bouton
297
+ * actif ; aucune couleur nouvelle n'est introduite. */
298
+ .zv-chrome-btn[disabled] {
299
+ color: var(--label);
300
+ cursor: default;
301
+ }
302
+
303
+ .zv-chrome-btn[disabled]:hover {
304
+ background: var(--white);
305
+ }
306
+
307
+ /* Glyphes relevés, à reproduire tels quels :
308
+ * · hamburger — SVG 18×12 (viewBox "0 0 18 12"),
309
+ * <path d="M0 1h18M0 6h18M0 11h18" stroke="currentColor" stroke-width="1.5"/> :
310
+ * trois traits pleine largeur, entraxe 5px, extrémités franches. Ce n'est
311
+ * PAS une icône 24×24 d'une bibliothèque, et une icône ronde tirée d'un
312
+ * jeu tiers casserait la géométrie de la barre. Il ne suit donc pas la
313
+ * grille de 24 de icons.css : il se pose en SVG nu, pas en .zv-icon.
314
+ * · fermeture — le caractère ✕ (U+2715) posé en texte, d'où le
315
+ * `font-size` ci-dessus qui le dimensionne. */
316
+
317
+ /* ─── Voile ─────────────────────────────────────────────────────────────
318
+ * Toujours l'encre de charte diluée, jamais un noir pur : la page est
319
+ * bleutée, un gris neutre y ferait un trou sale. Le voile n'existe que sous
320
+ * le seuil (cf. bloc mobile) — au-dessus, tiroir et feuille sont des colonnes,
321
+ * elles n'occultent rien. */
322
+
323
+ .zv-scrim {
324
+ display: none;
325
+ position: fixed;
326
+ inset: 0;
327
+ z-index: 60;
328
+ background: color-mix(in oklab, var(--ink) 40%, transparent);
329
+ }
330
+
331
+ /* ─── Tiroir de navigation ──────────────────────────────────────────────*/
332
+
333
+ .zv-drawer {
334
+ display: none;
335
+ position: fixed;
336
+ inset: 0;
337
+ z-index: 80;
338
+ /* `visibility` plutôt que `display` : elle sort le contenu du parcours
339
+ * clavier ET se transitionne, ce qui retient le panneau à l'écran le temps
340
+ * qu'il glisse dehors. En `display: none`, il disparaîtrait à mi-course. */
341
+ visibility: hidden;
342
+ transition: visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);
343
+ }
344
+
345
+ .zv-drawer[data-open] {
346
+ visibility: visible;
347
+ }
348
+
349
+ /* Le voile du tiroir accompagne un panneau qui glisse : il se fond. Celui de
350
+ * la feuille de source apparaît net, faute de mouvement à accompagner. */
351
+ .zv-drawer .zv-scrim {
352
+ opacity: 0;
353
+ transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
354
+ }
355
+
356
+ .zv-drawer[data-open] .zv-scrim {
357
+ opacity: 1;
358
+ }
359
+
360
+ .zv-drawer__panel {
361
+ position: absolute;
362
+ inset-block: 0;
363
+ left: 0;
364
+ display: flex;
365
+ width: 288px;
366
+ /* Le plafond en pourcentage garde toujours une bande de page visible
367
+ * derrière : un tiroir qui couvre tout l'écran ne se lit plus comme un
368
+ * tiroir, et l'utilisateur perd l'endroit où appuyer pour sortir. */
369
+ max-width: 86%;
370
+ flex-direction: column;
371
+ overflow-y: auto;
372
+ border-right: 1px solid var(--line);
373
+ background: var(--panel);
374
+ border-radius: 0;
375
+ /* Aucune ombre : c'est le voile qui fait le contraste. En ajouter une
376
+ * doublerait la séparation et salirait le bord. */
377
+ box-shadow: none;
378
+ transform: translateX(-100%);
379
+ transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
380
+ }
381
+
382
+ .zv-drawer[data-open] .zv-drawer__panel {
383
+ transform: translateX(0);
384
+ }
385
+
386
+ /* ⚠️ TENSION ASSUMÉE, à arbitrer. Les deux courbes ci-dessus sont celles du
387
+ * relevé — voile en cubic-bezier(.4,0,.2,1), panneau en ease-out, même durée.
388
+ * Leur asymétrie ne se voit pas, elle s'entend : le panneau arrive avant que
389
+ * le voile ait fini de se poser. Elles ne sont donc PAS la courbe unique
390
+ * --zv-easing que motion.css impose à la vitrine. Défendable (motion.css régit
391
+ * neuf animations de page, pas le chrome applicatif), mais c'est une divergence
392
+ * écrite ici plutôt qu'une variable empruntée en silence — d'autant que ce
393
+ * module doit rester utilisable sans motion.css.
394
+ * La coupure prefers-reduced-motion est déjà globale dans base.css : ne pas la
395
+ * reposer ici, elle neutralise ces deux transitions comme les autres. */
396
+
397
+ .zv-drawer__close {
398
+ position: absolute;
399
+ top: 7px;
400
+ right: 8px;
401
+ }
402
+
403
+ /* Comportement attendu du composant, à conserver tel quel :
404
+ * · role="dialog" aria-modal="true" aria-label="Navigation", et aria-hidden
405
+ * qui reflète l'état fermé ;
406
+ * · clic sur le voile ou touche Échap → fermeture ;
407
+ * · à l'ouverture, `body { overflow: hidden }` RESTAURÉ à sa valeur
408
+ * précédente à la fermeture, jamais remis à '' aveuglément : c'est ce qui
409
+ * évite de casser une page qui gérait déjà son propre défilement ;
410
+ * · l'état ouvert mémorise la ROUTE d'ouverture, pas un booléen — changer de
411
+ * page referme alors le tiroir sans effet ni rendu en cascade. Le
412
+ * composant a donc besoin de la route courante, pas d'une prop `open`.
413
+ * ⚠️ MANQUE en production, à combler côté composant : piège de focus dans le
414
+ * panneau et restitution du focus au bouton d'ouverture à la fermeture. */
415
+
416
+ /* ─── Feuille de source citée ───────────────────────────────────────────
417
+ * ⚠️ RENOMMÉE : cette pièce s'appelait .zv-sheet. layout.css possède déjà
418
+ * .zv-sheet-section (section de PLANCHE de documentation, avec son __title et
419
+ * son __body) : côte à côte, .zv-sheet__title et .zv-sheet-section__title
420
+ * donnent à lire un bloc et son modificateur là où il y a deux objets sans
421
+ * rapport. Elle prend le nom de ce qui l'ouvre — un clic sur une citation :
422
+ * .zv-cite-panel. Les .zv-cite / .zv-cite-row de data.css sont la CITATION dans le fil de texte ; .zv-cite-panel est la feuille qui l'accueille — deux objets distincts, sans collision, et le nom
423
+ * ne se confond pas avec .zv-src (la PUCE de source, components.css).
424
+ *
425
+ * Colonne au-dessus du seuil, feuille quasi plein écran en dessous. Trois
426
+ * zones : tête fixe, corps défilant, pied d'actions. */
427
+
428
+ .zv-cite-panel {
429
+ display: flex;
430
+ height: 100dvh;
431
+ width: 392px;
432
+ flex: none;
433
+ flex-direction: column;
434
+ border-left: 1px solid var(--line);
435
+ /* Blanc, et non --panel : c'est une surface de lecture, pas du chrome. */
436
+ background: var(--white);
437
+ }
438
+
439
+ .zv-cite-panel__head {
440
+ display: flex;
441
+ /* `flex-start` et non `center` : un titre de document sur trois lignes ne
442
+ * doit pas faire descendre la croix au milieu du bloc. */
443
+ align-items: flex-start;
444
+ justify-content: space-between;
445
+ gap: 12px;
446
+ border-bottom: 1px solid var(--line);
447
+ padding: 18px 20px 14px;
448
+ }
449
+
450
+ .zv-cite-panel__title {
451
+ font-family: var(--font-display);
452
+ font-size: var(--app-h3);
453
+ line-height: 1.35;
454
+ font-weight: 500;
455
+ /* Un nom de fichier sans espace déborderait la colonne et pousserait la
456
+ * croix hors de l'écran. */
457
+ overflow-wrap: break-word;
458
+ }
459
+
460
+ /* Le corps est la seule zone qui défile : le fond gris l'enfonce d'un cran
461
+ * pour que la page citée s'y détache comme une feuille posée. */
462
+ .zv-cite-panel__body {
463
+ flex: 1 1 0%;
464
+ overflow-y: auto;
465
+ background: var(--hairline);
466
+ padding: 18px;
467
+ }
468
+
469
+ .zv-cite-panel__foot {
470
+ display: flex;
471
+ flex-direction: column;
472
+ gap: 8px;
473
+ border-top: 1px solid var(--line);
474
+ padding: 14px 20px;
475
+ }
476
+
477
+ /* Fermeture par Échap, par le voile, par la croix, et automatiquement à chaque
478
+ * navigation. AUCUNE transition d'entrée ni de sortie, contrairement au
479
+ * tiroir : la feuille répond à un clic sur une citation, elle doit être là au
480
+ * moment où le regard y va.
481
+ * ⚠️ NON LIVRÉ : le kicker « SOURCE CITÉE » de la tête (citation.tsx:104) n'est
482
+ * dans aucun inventaire, et il n'est pas tranché s'il relève de la typographie
483
+ * (.zv-kicker, base.css) ou de ce module. Rien n'est inventé en attendant. */
484
+
485
+ /* ─── Menu déroulant ────────────────────────────────────────────────────
486
+ * Ancré sous son déclencheur (.zv-select, forms.css), qui DOIT porter
487
+ * `position: relative`, sans quoi le menu se colle au coin de la page.
488
+ * L'enveloppe .zv-select-anchor (forms.css) le porte déjà : c'est elle qu'on
489
+ * pose autour d'un déclencheur .zv-select, et non le déclencheur lui-même. */
490
+
491
+ .zv-menu {
492
+ position: absolute;
493
+ top: calc(100% + 5px);
494
+ left: 0;
495
+ z-index: 60;
496
+ display: flex;
497
+ flex-direction: column;
498
+ min-width: 230px;
499
+ border: 1px solid var(--line);
500
+ border-radius: 0;
501
+ background: var(--white);
502
+ padding: 5px;
503
+ /* Seule ombre du chrome applicatif, et elle est justifiée : un menu flotte
504
+ * vraiment au-dessus de la page, contrairement à la colonne ou au tiroir.
505
+ * --shadow-card est trop courte pour ce tirage, --shadow-panel trop indigo :
506
+ * d'où --shadow-menu, qui existe pour ce seul usage. */
507
+ box-shadow: var(--shadow-menu);
508
+ }
509
+
510
+ .zv-menu__item {
511
+ display: block;
512
+ /* Les quatre déclarations qui suivent existent pour que l'item tienne aussi
513
+ * bien en <button> qu'en <div> : sans elles, le navigateur impose sa police,
514
+ * son fond gris, son cadre et un texte centré. */
515
+ width: 100%;
516
+ border: 0;
517
+ background: none;
518
+ text-align: left;
519
+ padding: 8px 11px;
520
+ font-family: var(--font-text);
521
+ font-size: var(--app-sm);
522
+ font-weight: 400;
523
+ color: var(--ink);
524
+ cursor: pointer;
525
+ }
526
+
527
+ .zv-menu__item:hover {
528
+ background: var(--hairline);
529
+ }
530
+
531
+ .zv-menu__item:focus-visible {
532
+ outline-offset: -2px;
533
+ }
534
+
535
+ .zv-menu__item--on,
536
+ .zv-menu__item[aria-selected="true"] {
537
+ background: var(--accent-soft);
538
+ color: var(--accent);
539
+ font-weight: 600;
540
+ }
541
+
542
+ /* ⚠️ Relevé tel quel : le survol ÉCRASE le fond de l'item courant
543
+ * (accent-soft → hairline), seul le texte reste accent. Volontaire ou non,
544
+ * c'est ce qui tourne en production — signalé plutôt que corrigé en silence. */
545
+ .zv-menu__item--on:hover,
546
+ .zv-menu__item[aria-selected="true"]:hover {
547
+ background: var(--hairline);
548
+ }
549
+
550
+ /* ⚠️ HORS RELEVÉ : aucun item désactivé n'existe en production. Même
551
+ * traitement que le bouton de chrome, sans couleur nouvelle. */
552
+ .zv-menu__item[disabled],
553
+ .zv-menu__item[aria-disabled="true"] {
554
+ color: var(--label);
555
+ cursor: default;
556
+ }
557
+
558
+ .zv-menu__item[disabled]:hover,
559
+ .zv-menu__item[aria-disabled="true"]:hover {
560
+ background: none;
561
+ }
562
+
563
+ /* ⚠️ MANQUENT en production et NON comblés ici, car ce sont des comportements,
564
+ * pas du style : navigation au clavier (flèches, Origine/Fin), fermeture au
565
+ * clic extérieur, fermeture à Échap. Le CSS ci-dessus est prêt à les recevoir
566
+ * (aria-selected, :focus-visible) ; le composant doit les écrire. */
567
+
568
+ /* ─── Fil d'Ariane ──────────────────────────────────────────────────────*/
569
+
570
+ .zv-crumbs {
571
+ display: block;
572
+ margin-bottom: 14px;
573
+ font-family: var(--font-mono);
574
+ font-size: var(--mono-lg);
575
+ font-weight: 500;
576
+ /* Le fil porte des noms de dossier entiers : il a le droit de passer à la
577
+ * ligne, pas celui d'élargir la page. */
578
+ overflow-wrap: break-word;
579
+ color: var(--label);
580
+ text-decoration: none;
581
+ cursor: pointer;
582
+ }
583
+
584
+ /* Même contre-ordre que .zv-sidelink : sans lui, le fil vire au bleu souligné
585
+ * dans toute page qui applique le style de lien éditorial. */
586
+ .zv-crumbs:hover {
587
+ color: var(--label);
588
+ text-decoration: none;
589
+ }
590
+
591
+ .zv-crumbs__sep {
592
+ margin-inline: 4px;
593
+ }
594
+
595
+ /* Le dernier segment est plus sombre que le fil : c'est lui qu'on lit, le
596
+ * reste n'est qu'un chemin. Aucun état « segment courant » n'existe au relevé. */
597
+ .zv-crumbs__seg {
598
+ color: var(--ink-2);
599
+ }
600
+
601
+ /* ─── Pagination ────────────────────────────────────────────────────────
602
+ * Deux formes relevées, et deux seulement : le rang centré d'un panneau
603
+ * (‹ p. 12 / 31 ›) et le pied de tableau (compte à gauche, « ‹ Précédent ·
604
+ * Suivant › » à droite).
605
+ *
606
+ * ⚠️ DOUBLON À TRANCHER, et il est tranché ici : data.css pose .zv-table-foot
607
+ * pour le MÊME nœud du DOM (repertoire-client.tsx:165) avec les mêmes valeurs.
608
+ * La pagination est de la navigation, pas de la donnée : la forme reste dans ce
609
+ * module, sous .zv-pager--foot, et data.css doit abandonner .zv-table-foot.
610
+ * .zv-pager--foot est un MODIFICATEUR : il se pose avec .zv-pager, jamais seul. */
611
+
612
+ .zv-pager {
613
+ display: flex;
614
+ justify-content: center;
615
+ gap: 14px;
616
+ /* Décolle le rang des boutons du pied de feuille, où il est immédiatement
617
+ * suivi de deux actions pleine largeur. */
618
+ padding-bottom: 2px;
619
+ font-family: var(--font-mono);
620
+ font-size: var(--mono-lg);
621
+ font-weight: 500;
622
+ color: var(--muted);
623
+ }
624
+
625
+ /* `font: inherit` plutôt qu'une police redite : les trois éléments cliquables
626
+ * de la pagination sont des <button>, et sans lui le navigateur y impose sa
627
+ * police système. Hériter reprend exactement la mono du rang — aucune valeur
628
+ * nouvelle n'entre par cette porte. */
629
+ .zv-pager__arrow,
630
+ .zv-pager__rank,
631
+ .zv-pager__step {
632
+ border: 0;
633
+ background: none;
634
+ color: inherit;
635
+ font: inherit;
636
+ padding: 0;
637
+ cursor: pointer;
638
+ }
639
+
640
+ /* ⚠️ HORS RELEVÉ. La production n'a AUCUN état désactivé : les flèches sont
641
+ * identiques en première et en dernière page. Posé ici parce qu'une flèche
642
+ * qui ne fait rien et ne le dit pas se lit comme une panne. `pointer-events`
643
+ * n'est volontairement pas coupé : en aria-disabled, l'élément doit rester
644
+ * atteignable au clavier pour être annoncé. */
645
+ .zv-pager__arrow[disabled],
646
+ .zv-pager__arrow[aria-disabled="true"] {
647
+ color: var(--label);
648
+ cursor: default;
649
+ }
650
+
651
+ /* Pied de tableau : le compte à gauche, un SEUL élément cliquable pour les
652
+ * deux sens à droite — pas de numéros de page, pas de page courante. Le
653
+ * `padding` en raccourci annule le `padding-bottom: 2px` du rang de panneau,
654
+ * qui n'a pas de sens ici. */
655
+ .zv-pager--foot {
656
+ justify-content: space-between;
657
+ align-items: center;
658
+ gap: 0;
659
+ padding: 12px 4px;
660
+ color: var(--label);
661
+ }
662
+
663
+ /* ⚠️ TROU ASSUMÉ, et il dépasse le style : il n'existe nulle part de page
664
+ * courante, de numéros, ni de sens séparés. Une pagination digne de ce nom
665
+ * (précédent et suivant distincts, désactivables, rang courant) est une
666
+ * DÉCISION de conception, pas un relevé — cf. notes. */
667
+
668
+ /* ─── Onglets ───────────────────────────────────────────────────────────
669
+ * ⚠️ AUCUNE MATIÈRE PREMIÈRE : pas un seul jeu d'onglets dans les écrans
670
+ * relevés. Ce module ne livre donc RIEN ici.
671
+ *
672
+ * Une rangée d'onglets se construit avec .zv-seg-group et .zv-seg
673
+ * (forms.css), qui portent déjà la grammaire d'un état actif. Ce module a
674
+ * d'abord défini une .zv-tabs — display flex, wrap, gouttière 6px — qui
675
+ * était la copie exacte de .zv-seg-group : deux noms pour le MÊME nœud du
676
+ * DOM, et c'est l'ordre de chargement qui aurait tranché. Elle est retirée.
677
+ *
678
+ * Un onglet SOULIGNÉ, lui, reste à faire : il suppose d'arbitrer la couleur
679
+ * et l'épaisseur du trait actif, ce qu'aucune source ne donne.
680
+ */
681
+
682
+ /* ─── Défilement ────────────────────────────────────────────────────────
683
+ * La mécanique, elle, est relevée et livrée : `min-height: 0` ET
684
+ * `min-width: 0`. Sans ces deux-là, un enfant flex refuse de rétrécir en
685
+ * dessous de son contenu — la zone déborde, et c'est la page entière qui part
686
+ * en défilement horizontal. C'est la faute la plus fréquente de ce gabarit. */
687
+
688
+ .zv-scroll-y {
689
+ min-height: 0;
690
+ min-width: 0;
691
+ flex: 1 1 0%;
692
+ overflow-y: auto;
693
+ }
694
+
695
+ /* ⚠️ HABILLAGE DES BARRES : trou franc, vérifié deux fois. `grep -rn scrollbar`
696
+ * sur tout le source de Mémoire ne renvoie AUCUNE occurrence — les cinq zones
697
+ * défilantes utilisent la barre native du système. Il n'existe donc pas de
698
+ * valeur relevée pour la largeur, le pouce ou le rail, et ce module n'en
699
+ * applique aucune par défaut.
700
+ * L'habillage ci-dessous est OPT-IN et n'emploie que des mots-clés plus un
701
+ * token existant : aucune dimension inventée. ::-webkit-scrollbar est écarté
702
+ * pour deux raisons — il exigerait une largeur en pixels sortie de nulle part,
703
+ * et il remplace les barres flottantes de macOS/iOS par des barres permanentes
704
+ * qui mangent la colonne. */
705
+ .zv-scrollbar {
706
+ scrollbar-width: thin;
707
+ scrollbar-color: var(--line) transparent;
708
+ }
709
+
710
+ /* ─── Mobile ────────────────────────────────────────────────────────────
711
+ * Point de rupture unique du paquet. Les deux jeux de valeurs sont relevés
712
+ * (Mémoire les fait basculer à 1024px) ; seul le seuil est une convention.
713
+ * Sous ce seuil, tout ce qui se touche fait au moins --tap. */
714
+
715
+ @media (max-width: 767px) {
716
+ /* 224px de colonne à côté du contenu sur un écran de 375px ne laisse rien à
717
+ * lire : la colonne cède la place au couple barre + tiroir. */
718
+ .zv-sidebar {
719
+ display: none;
720
+ }
721
+
722
+ .zv-topbar {
723
+ display: flex;
724
+ }
725
+
726
+ .zv-sidelink {
727
+ min-height: var(--tap);
728
+ padding-block: 11px;
729
+ font-size: var(--app-lg);
730
+ }
731
+
732
+ .zv-chrome-btn {
733
+ width: var(--tap);
734
+ height: var(--tap);
735
+ }
736
+
737
+ /* DÉRIVÉ, pas relevé, et c'est une conséquence de la ligne au-dessus : la
738
+ * croix passe de 26 à 40px et son bord gauche remonte à 48px du bord droit,
739
+ * au-delà de la réserve de 46px de la tête. 40 (bouton) + 8 (son décalage)
740
+ * + 8 (garde) : le logo cesse de passer dessous. */
741
+ .zv-drawer .zv-sidebar__head > :first-child {
742
+ padding-right: calc(var(--tap) + 16px);
743
+ }
744
+
745
+ .zv-scrim {
746
+ display: block;
747
+ }
748
+
749
+ .zv-drawer {
750
+ display: block;
751
+ }
752
+
753
+ /* La feuille se cale À RAS sous la barre.
754
+ *
755
+ * La production pose top: 53px pour une barre de 52 (citation.tsx:101
756
+ * contre app-shell.tsx:268), en supposant que le filet s'ajoute à la
757
+ * hauteur. Il ne s'y ajoute pas : box-sizing: border-box le peint À
758
+ * L'INTÉRIEUR des 52px. Ce pixel n'empile donc rien, il ouvre une couture
759
+ * par laquelle le fond de page transparaît — et la feuille portant en
760
+ * plus son propre filet haut, le trait apparaissait doublé.
761
+ *
762
+ * Corrigé ici : la feuille commence à 52, et n'a pas de filet propre —
763
+ * celui de la barre les sépare déjà. C'est un écart ASSUMÉ au relevé, sur
764
+ * un artefact et non sur une intention. */
765
+ .zv-cite-panel {
766
+ position: fixed;
767
+ inset-inline: 0;
768
+ top: var(--topbar-h);
769
+ bottom: 0;
770
+ z-index: 70;
771
+ height: auto;
772
+ width: auto;
773
+ border-left: 0;
774
+ border-top: 0;
775
+ }
776
+
777
+ .zv-cite-panel__body {
778
+ padding: 12px;
779
+ }
780
+
781
+ /* Le menu s'ouvre sous un déclencheur qui peut être près du bord droit :
782
+ * le plafond en vw l'empêche de sortir de l'écran et d'emporter la page. */
783
+ .zv-menu {
784
+ max-width: calc(100vw - 40px);
785
+ }
786
+
787
+ .zv-menu__item {
788
+ display: flex;
789
+ align-items: center;
790
+ min-height: var(--tap);
791
+ }
792
+
793
+ /* Les chevrons sont des glyphes d'une dizaine de pixels : sans ce retrait,
794
+ * la cible reste celle du caractère, impossible à viser au doigt. */
795
+ .zv-pager__arrow {
796
+ padding: 8px;
797
+ }
798
+
799
+ .zv-pager__rank {
800
+ padding-block: 8px;
801
+ }
802
+
803
+ .zv-pager--foot {
804
+ flex-direction: column;
805
+ align-items: flex-start;
806
+ gap: 4px;
807
+ }
808
+
809
+ .zv-pager__step {
810
+ display: flex;
811
+ align-items: center;
812
+ min-height: var(--tap);
813
+ }
814
+ }