@zevra/ui 0.2.1 → 0.3.1

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.
package/DECISIONS.md CHANGED
@@ -173,6 +173,32 @@ 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
177
+
178
+ Trois composants n'existent dans aucune source et sont pourtant livrés, car
179
+ leur absence rend le système inutilisable :
180
+
181
+ **Le menu mobile de la vitrine.** Les cinq planches vitrine sont des rendus
182
+ figés à 1440px : aucune n'a jamais eu de menu mobile. Sans `.zv-nav__burger`
183
+ et le tiroir qu'il ouvre, une page Zevra sur téléphone n'a aucun moyen
184
+ d'atteindre sa navigation. Le tracé du burger est dérivé ; les mesures
185
+ (cible de 40px, panneau de 288px) viennent du tiroir applicatif, lui relevé.
186
+
187
+ **La modale.** Mémoire n'en emploie aucune. Celle-ci est dérivée de la carte
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 —
190
+ et qu'à défaut chaque app en réinventerait une.
191
+
192
+ **La notification flottante.** Mémoire signale tout en place, par une alerte
193
+ de bloc. Celle-ci est l'alerte détachée et posée en coin. Elle ne remplace
194
+ pas `.zv-alert` : une erreur de formulaire se dit à côté du champ, jamais
195
+ dans un coin qui disparaît.
196
+
197
+ Ces trois-là vivent dans `overlays.css`, module commun aux deux couches.
198
+ Auparavant, le voile et le tiroir étaient dans `navigation.css` : une landing
199
+ devait charger la barre latérale, la pagination et les tableaux pour obtenir
200
+ un bouton burger.
201
+
176
202
  ### Ce que le relevé ne donnait pas
177
203
 
178
204
  Aucun `<textarea>`, aucune case à cocher, aucun bouton radio n'existe dans
package/README.md CHANGED
@@ -32,6 +32,7 @@ pnpm add @zevra/ui
32
32
  @import "@zevra/ui/surfaces.css"; /* halos, sols, voiles, manifeste */
33
33
  @import "@zevra/ui/motion.css"; /* les neuf animations autorisées */
34
34
  @import "@zevra/ui/icons.css"; /* dimensionnement des icônes */
35
+ @import "@zevra/ui/overlays.css"; /* voile, tiroir, modale, notification */
35
36
  @import "@zevra/ui/forms.css"; /* champs, selects, interrupteurs, dépôt */
36
37
  @import "@zevra/ui/feedback.css"; /* alertes, badges, états, chargement */
37
38
  @import "@zevra/ui/navigation.css"; /* menus, barre, fil d'Ariane, pagination */
@@ -60,8 +61,15 @@ La **couche applicative** — `forms`, `feedback`, `navigation`, `data`, `icons`
60
61
  compose une interface qui s'opère : champs, alertes, menus, tableaux. Elle
61
62
  est plus dense, et dispose de son échelle propre (`--app`, `--mono`).
62
63
 
63
- Une app n'a aucune raison de charger les deux. Seuls `tokens.css` et
64
- `base.css` sont communs.
64
+ Une app n'a aucune raison de charger les deux. Sont communs : `tokens.css`,
65
+ `base.css`, `icons.css` et **`overlays.css`** — une page vitrine a besoin
66
+ d'un tiroir pour son menu mobile autant qu'une application pour sa
67
+ navigation.
68
+
69
+ **Le menu mobile n'est pas optionnel.** Sous 768px, `.zv-nav__links` doit
70
+ être masqué (`zv-only-desktop`) et remplacé par `.zv-nav__burger`, qui ouvre
71
+ un `.zv-drawer`. Sans lui, la navigation devient inatteignable sur
72
+ téléphone.
65
73
 
66
74
  Le tableau est **à base de grille**, pas de `<table>` : le gabarit de
67
75
  colonnes se pose en `--zv-cols` sur le conteneur, et sous 768px la rangée
package/demo/app.html CHANGED
@@ -228,6 +228,49 @@
228
228
  </div>
229
229
  </section>
230
230
 
231
+ <!-- ENROBAGES -->
232
+ <section class="d-sec">
233
+ <span class="zv-kicker"><span class="zv-spec" aria-hidden="true"></span><span class="zv-mono">05 · Enrobages</span></span>
234
+ <p class="zv-note" style="margin-top:10px">Tout ce qui se pose par-dessus la page. La modale et la notification sont dérivées, hors relevé — voir DECISIONS.md.</p>
235
+
236
+ <div class="d-row">
237
+ <button class="zv-btn zv-btn--sm" onclick="document.getElementById('m1').toggleAttribute('data-open')">Ouvrir une modale</button>
238
+ <button class="zv-btn--ghost zv-btn--sm" onclick="document.getElementById('m2').toggleAttribute('data-open')">Modale destructive</button>
239
+ </div>
240
+
241
+ <div class="d-row" style="align-items:stretch;flex-direction:column">
242
+ <span class="zv-toast">Lien copié dans le presse-papier.</span>
243
+ <span class="zv-toast zv-toast--ok">Douze documents indexés.</span>
244
+ <span class="zv-toast zv-toast--err">L'envoi a échoué. Réessayez.</span>
245
+ </div>
246
+ </section>
247
+
248
+ <div class="zv-modal" id="m1">
249
+ <span class="zv-scrim" onclick="document.getElementById('m1').removeAttribute('data-open')"></span>
250
+ <div class="zv-modal__panel">
251
+ <div class="zv-modal__head">
252
+ <h3 class="zv-modal__title">Partager ce dossier</h3>
253
+ <button class="zv-icon-btn" aria-label="Fermer" onclick="document.getElementById('m1').removeAttribute('data-open')"><svg class="zv-icon"><use href="../icons/sprite.svg#zv-fermer"/></svg></button>
254
+ </div>
255
+ <div class="zv-modal__body">Les personnes invitées accèdent aux documents de ce dossier, jamais au reste de la base.</div>
256
+ <div class="zv-modal__foot">
257
+ <button class="zv-btn--ghost zv-btn--sm" onclick="document.getElementById('m1').removeAttribute('data-open')">Annuler</button>
258
+ <button class="zv-btn zv-btn--sm">Inviter</button>
259
+ </div>
260
+ </div>
261
+ </div>
262
+
263
+ <div class="zv-modal zv-modal--danger" id="m2">
264
+ <span class="zv-scrim" onclick="document.getElementById('m2').removeAttribute('data-open')"></span>
265
+ <div class="zv-modal__panel">
266
+ <div class="zv-modal__head"><h3 class="zv-modal__title">Supprimer ce document ?</h3></div>
267
+ <div class="zv-modal__body">Cette action retire le document de la base et de l'index. Elle est définitive.</div>
268
+ <div class="zv-modal__foot">
269
+ <button class="zv-btn--ghost zv-btn--sm" onclick="document.getElementById('m2').removeAttribute('data-open')">Annuler</button>
270
+ <button class="zv-btn--ink zv-btn--sm">Supprimer</button>
271
+ </div>
272
+ </div>
273
+ </div>
231
274
  </div>
232
275
  </body>
233
276
  </html>
package/demo/index.html CHANGED
@@ -22,7 +22,9 @@
22
22
  <!-- Chrome -->
23
23
  <nav class="zv-nav zv-gutter">
24
24
  <span class="zv-display" style="font-size:21px;font-weight:600;letter-spacing:0.22em;text-transform:uppercase">Zevra</span>
25
- <a class="zv-btn zv-btn--sm zv-only-mobile" href="#">Réserver</a>
25
+ <button class="zv-nav__burger" aria-label="Ouvrir le menu" onclick="document.getElementById('menu').toggleAttribute('data-open')">
26
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
27
+ </button>
26
28
  <span class="zv-nav__links zv-only-desktop">
27
29
  <a class="zv-navlink" href="#">Produits</a>
28
30
  <a class="zv-navlink" href="#">Méthode</a>
@@ -31,6 +33,20 @@
31
33
  </span>
32
34
  </nav>
33
35
 
36
+ <!-- Tiroir de navigation mobile -->
37
+ <div class="zv-drawer" id="menu">
38
+ <span class="zv-scrim" onclick="document.getElementById('menu').removeAttribute('data-open')"></span>
39
+ <nav class="zv-drawer__panel">
40
+ <span class="zv-nav-drawer">
41
+ <a class="zv-nav-drawer__link zv-nav-drawer__link--on" href="#">Produits</a>
42
+ <a class="zv-nav-drawer__link" href="#">Méthode</a>
43
+ <a class="zv-nav-drawer__link" href="#">Sécurité</a>
44
+ <a class="zv-nav-drawer__link" href="#">Tarifs</a>
45
+ <a class="zv-btn zv-btn--block" href="#" style="margin-top:12px">Réserver 30 min</a>
46
+ </span>
47
+ </nav>
48
+ </div>
49
+
34
50
  <!-- Hero -->
35
51
  <header class="zv-hero zv-gutter--wide">
36
52
  <span class="zv-halo zv-halo--hero zv-anim-breathe" aria-hidden="true"></span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zevra/ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Design system Zevra « Encre & Papier » — tokens, CSS des composants et logos officiels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -29,12 +29,13 @@
29
29
  "./layout.css": "./src/layout.css",
30
30
  "./surfaces.css": "./src/surfaces.css",
31
31
  "./motion.css": "./src/motion.css",
32
- "./logos/*": "./logos/*",
33
32
  "./icons.css": "./src/icons.css",
33
+ "./overlays.css": "./src/overlays.css",
34
34
  "./forms.css": "./src/forms.css",
35
35
  "./feedback.css": "./src/feedback.css",
36
36
  "./navigation.css": "./src/navigation.css",
37
37
  "./data.css": "./src/data.css",
38
+ "./logos/*": "./logos/*",
38
39
  "./icons/*": "./icons/*",
39
40
  "./demo/*": "./demo/*"
40
41
  },
package/src/index.css CHANGED
@@ -10,6 +10,7 @@
10
10
  @import "./surfaces.css";
11
11
  @import "./motion.css";
12
12
  @import "./icons.css";
13
+ @import "./overlays.css";
13
14
  @import "./forms.css";
14
15
  @import "./feedback.css";
15
16
  @import "./navigation.css";
package/src/layout.css CHANGED
@@ -195,6 +195,77 @@
195
195
  text-decoration: none;
196
196
  }
197
197
 
198
+ /* Bouton burger de la barre vitrine.
199
+ *
200
+ * ⚠️ HORS RELEVÉ des planches vitrine, qui sont toutes des rendus figés à
201
+ * 1440px et n'ont donc jamais eu de menu mobile. Mobile.html en montre un,
202
+ * mais sans en donner le tracé.
203
+ *
204
+ * Il est ici parce qu'une page sans lui n'a AUCUN moyen d'atteindre sa
205
+ * navigation sur téléphone : sous le seuil, .zv-nav__links doit être masqué
206
+ * (.zv-only-desktop) et rien ne le remplace. Le tiroir qu'il ouvre vit dans
207
+ * overlays.css.
208
+ *
209
+ * Il n'apparaît que sous le seuil : au-dessus, les liens sont là. */
210
+ .zv-nav__burger {
211
+ display: none;
212
+ align-items: center;
213
+ justify-content: center;
214
+ /* 40px de cible tactile, obtenus par la taille et non par du padding : un
215
+ * bouton de 24px entouré de marge reste un bouton de 24px pour le doigt. */
216
+ width: 40px;
217
+ height: 40px;
218
+ /* Les marges négatives ramènent le bouton à l'alignement optique de la
219
+ * barre : sans elles, sa zone tactile décale le logo et le bord droit. */
220
+ margin: -8px -8px -8px 0;
221
+ padding: 0;
222
+ border: 0;
223
+ background: transparent;
224
+ color: var(--ink);
225
+ cursor: pointer;
226
+ }
227
+
228
+ .zv-nav__burger svg {
229
+ width: 20px;
230
+ height: 20px;
231
+ fill: none;
232
+ stroke: currentColor;
233
+ stroke-width: 2;
234
+ stroke-linecap: round;
235
+ }
236
+
237
+ /* Le tiroir de la vitrine : liens empilés, respiration généreuse. Le panneau
238
+ * lui-même vient d'overlays.css ; ceci n'habille que son contenu. */
239
+ .zv-nav-drawer {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 4px;
243
+ padding: 20px;
244
+ }
245
+
246
+ .zv-nav-drawer__link {
247
+ display: flex;
248
+ align-items: center;
249
+ /* Chaque entrée fait au moins une cible tactible pleine : une liste de
250
+ * liens serrés est la première cause de faux appuis sur téléphone. */
251
+ min-height: var(--tap);
252
+ padding: 0 12px;
253
+ border-radius: var(--r-panel);
254
+ font-size: 15px;
255
+ font-weight: 500;
256
+ color: var(--ink);
257
+ text-decoration: none;
258
+ }
259
+
260
+ .zv-nav-drawer__link:hover {
261
+ background: var(--panel);
262
+ color: var(--accent);
263
+ }
264
+
265
+ .zv-nav-drawer__link--on {
266
+ color: var(--accent);
267
+ }
268
+
198
269
  .zv-navlink--quiet {
199
270
  color: var(--label);
200
271
  }
@@ -395,15 +466,23 @@
395
466
  * mêmes éléments de navigation — les liens de nav desktop deviennent un
396
467
  * menu burger — et que sans eux chaque app réinvente la même règle. */
397
468
 
398
- .zv-only-desktop {
399
- display: none;
400
- }
401
-
402
- @media (min-width: 768px) {
469
+ /* Chaque utilitaire MASQUE dans la plage où l'élément n'a pas sa place, et ne
470
+ * dit RIEN dans l'autre.
471
+ *
472
+ * La version précédente posait `display: none` par défaut puis
473
+ * `display: revert` au-dessus du seuil. C'était faux : `revert` revient au
474
+ * défaut du NAVIGATEUR — `inline` pour un span — et écrasait donc le
475
+ * `display: flex` que l'élément tenait de sa propre classe. Une rangée de
476
+ * liens de navigation perdait sa gouttière et ses libellés se collaient.
477
+ *
478
+ * En ne déclarant rien dans la plage visible, l'élément garde son display. */
479
+ @media (max-width: 767px) {
403
480
  .zv-only-desktop {
404
- display: revert;
481
+ display: none;
405
482
  }
483
+ }
406
484
 
485
+ @media (min-width: 768px) {
407
486
  .zv-only-mobile {
408
487
  display: none;
409
488
  }
@@ -473,6 +552,11 @@
473
552
  padding-top: 20px;
474
553
  }
475
554
 
555
+ /* Le burger n'existe que là où les liens disparaissent. */
556
+ .zv-nav__burger {
557
+ display: inline-flex;
558
+ }
559
+
476
560
  .zv-nav__logo {
477
561
  height: 30px;
478
562
  }
@@ -314,105 +314,6 @@
314
314
  * · fermeture — le caractère ✕ (U+2715) posé en texte, d'où le
315
315
  * `font-size` ci-dessus qui le dimensionne. */
316
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
317
  /* ─── Feuille de source citée ───────────────────────────────────────────
417
318
  * ⚠️ RENOMMÉE : cette pièce s'appelait .zv-sheet. layout.css possède déjà
418
319
  * .zv-sheet-section (section de PLANCHE de documentation, avec son __title et
@@ -742,14 +643,6 @@
742
643
  padding-right: calc(var(--tap) + 16px);
743
644
  }
744
645
 
745
- .zv-scrim {
746
- display: block;
747
- }
748
-
749
- .zv-drawer {
750
- display: block;
751
- }
752
-
753
646
  /* La feuille se cale À RAS sous la barre.
754
647
  *
755
648
  * La production pose top: 53px pour une barre de 52 (citation.tsx:101
@@ -0,0 +1,305 @@
1
+ /* Zevra UI — enrobages : voile, tiroir, modale, notification
2
+ * Dépend de tokens.css et base.css.
3
+ *
4
+ * Ce module est COMMUN aux deux couches. Une page vitrine a besoin d'un
5
+ * tiroir pour son menu mobile ; une application en a besoin pour sa
6
+ * navigation. Les laisser dans navigation.css obligeait une landing à
7
+ * charger la barre latérale, la pagination et les tableaux pour obtenir un
8
+ * bouton burger — c'est ce qui a motivé cette séparation.
9
+ *
10
+ * Tout ce qui vit ici se pose PAR-DESSUS la page et lui vole le regard. D'où
11
+ * une règle commune : rien ne s'ouvre sans un moyen évident d'en sortir, et
12
+ * l'échappement au clavier est du ressort du gabarit, que le CSS ne peut pas
13
+ * assurer seul.
14
+ *
15
+ * Empilement, dans l'ordre : voile 60, tiroir 80, modale 90, notification 100.
16
+ * Les couches inférieures (2, 5, 40, 50) sont dans layout.css.
17
+ */
18
+
19
+ /* ─── Voile ─────────────────────────────────────────────────────────────
20
+ * Toujours l'encre de charte diluée, jamais un noir pur : la page est
21
+ * bleutée, un gris neutre y ferait un trou sale. Le voile n'existe que sous
22
+ * le seuil (cf. bloc mobile) — au-dessus, tiroir et feuille sont des colonnes,
23
+ * elles n'occultent rien. */
24
+
25
+ .zv-scrim {
26
+ display: none;
27
+ position: fixed;
28
+ inset: 0;
29
+ z-index: 60;
30
+ background: color-mix(in oklab, var(--ink) 40%, transparent);
31
+ }
32
+
33
+ /* ─── Tiroir de navigation ──────────────────────────────────────────────*/
34
+
35
+ .zv-drawer {
36
+ display: none;
37
+ position: fixed;
38
+ inset: 0;
39
+ z-index: 80;
40
+ /* `visibility` plutôt que `display` : elle sort le contenu du parcours
41
+ * clavier ET se transitionne, ce qui retient le panneau à l'écran le temps
42
+ * qu'il glisse dehors. En `display: none`, il disparaîtrait à mi-course. */
43
+ visibility: hidden;
44
+ transition: visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);
45
+ }
46
+
47
+ .zv-drawer[data-open] {
48
+ visibility: visible;
49
+ }
50
+
51
+ /* Le voile du tiroir accompagne un panneau qui glisse : il se fond. Celui de
52
+ * la feuille de source apparaît net, faute de mouvement à accompagner.
53
+ *
54
+ * Et il redescend au sol : son z-index de 60 est la couche qu'il occupe
55
+ * quand il est SEUL sur la page. À l'intérieur d'un enrobage, qui ouvre son
56
+ * propre contexte d'empilement, ce 60 l'emportait sur le panneau et voilait
57
+ * le tiroir par-dessus — il paraissait délavé, derrière son propre écran. */
58
+ .zv-drawer .zv-scrim {
59
+ z-index: 0;
60
+ opacity: 0;
61
+ transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
62
+ }
63
+
64
+ .zv-drawer[data-open] .zv-scrim {
65
+ opacity: 1;
66
+ }
67
+
68
+ .zv-drawer__panel {
69
+ position: absolute;
70
+ /* Le panneau passe DEVANT son voile. .zv-drawer ouvre un contexte
71
+ * d'empilement, dans lequel le z-index 60 du voile l'emporte sur un
72
+ * panneau resté en `auto` : le tiroir s'affichait alors délavé, comme s'il
73
+ * était derrière son propre écran. */
74
+ z-index: 1;
75
+ inset-block: 0;
76
+ left: 0;
77
+ display: flex;
78
+ width: 288px;
79
+ /* Le plafond en pourcentage garde toujours une bande de page visible
80
+ * derrière : un tiroir qui couvre tout l'écran ne se lit plus comme un
81
+ * tiroir, et l'utilisateur perd l'endroit où appuyer pour sortir. */
82
+ max-width: 86%;
83
+ flex-direction: column;
84
+ overflow-y: auto;
85
+ border-right: 1px solid var(--line);
86
+ background: var(--panel);
87
+ border-radius: 0;
88
+ /* Aucune ombre : c'est le voile qui fait le contraste. En ajouter une
89
+ * doublerait la séparation et salirait le bord. */
90
+ box-shadow: none;
91
+ transform: translateX(-100%);
92
+ transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
93
+ }
94
+
95
+ .zv-drawer[data-open] .zv-drawer__panel {
96
+ transform: translateX(0);
97
+ }
98
+
99
+ /* ⚠️ TENSION ASSUMÉE, à arbitrer. Les deux courbes ci-dessus sont celles du
100
+ * relevé — voile en cubic-bezier(.4,0,.2,1), panneau en ease-out, même durée.
101
+ * Leur asymétrie ne se voit pas, elle s'entend : le panneau arrive avant que
102
+ * le voile ait fini de se poser. Elles ne sont donc PAS la courbe unique
103
+ * --zv-easing que motion.css impose à la vitrine. Défendable (motion.css régit
104
+ * neuf animations de page, pas le chrome applicatif), mais c'est une divergence
105
+ * écrite ici plutôt qu'une variable empruntée en silence — d'autant que ce
106
+ * module doit rester utilisable sans motion.css.
107
+ * La coupure prefers-reduced-motion est déjà globale dans base.css : ne pas la
108
+ * reposer ici, elle neutralise ces deux transitions comme les autres. */
109
+
110
+ .zv-drawer__close {
111
+ position: absolute;
112
+ top: 7px;
113
+ right: 8px;
114
+ }
115
+
116
+ /* Comportement attendu du composant, à conserver tel quel :
117
+ * · role="dialog" aria-modal="true" aria-label="Navigation", et aria-hidden
118
+ * qui reflète l'état fermé ;
119
+ * · clic sur le voile ou touche Échap → fermeture ;
120
+ * · à l'ouverture, `body { overflow: hidden }` RESTAURÉ à sa valeur
121
+ * précédente à la fermeture, jamais remis à '' aveuglément : c'est ce qui
122
+ * évite de casser une page qui gérait déjà son propre défilement ;
123
+ * · l'état ouvert mémorise la ROUTE d'ouverture, pas un booléen — changer de
124
+ * page referme alors le tiroir sans effet ni rendu en cascade. Le
125
+ * composant a donc besoin de la route courante, pas d'une prop `open`.
126
+ * ⚠️ MANQUE en production, à combler côté composant : piège de focus dans le
127
+ * panneau et restitution du focus au bouton d'ouverture à la fermeture. */
128
+
129
+
130
+ /* ─── Modale ────────────────────────────────────────────────────────────
131
+ * ⚠️ HORS RELEVÉ. Aucune modale dans les sources de Mémoire — l'application
132
+ * n'en emploie pas. Celle-ci est DÉRIVÉE de la carte et du tiroir, sans
133
+ * introduire une seule valeur nouvelle : elle réemploie --r-card,
134
+ * --shadow-panel et le voile ci-dessus.
135
+ *
136
+ * Elle existe parce que la règle du projet en impose la forme (panneau
137
+ * plafonné à 92vh, qui défile en son sein) et qu'à défaut chaque app en
138
+ * réinventerait une.
139
+ *
140
+ * Employer <dialog> plutôt que ce balisage quand c'est possible : le
141
+ * navigateur y apporte le piège de focus et la fermeture par Échap, que le
142
+ * CSS ne sait pas faire. */
143
+
144
+ .zv-modal {
145
+ display: none;
146
+ position: fixed;
147
+ inset: 0;
148
+ z-index: 90;
149
+ align-items: center;
150
+ justify-content: center;
151
+ padding: 16px;
152
+ }
153
+
154
+ .zv-modal[data-open] {
155
+ display: flex;
156
+ }
157
+
158
+ /* Le voile d'une modale se montre à TOUTES les largeurs, et passe sous son
159
+ * panneau.
160
+ *
161
+ * .zv-scrim est masqué par défaut et ne s'affiche que sous le seuil : c'est
162
+ * juste pour le tiroir, qui n'existe qu'en mobile. La modale, elle, existe
163
+ * partout — sans cette règle, elle s'ouvrait en desktop sans rien occulter.
164
+ * Le z-index revient à 0 pour la même raison que sur le tiroir. */
165
+ .zv-modal .zv-scrim {
166
+ z-index: 0;
167
+ display: block;
168
+ }
169
+
170
+ .zv-modal__panel {
171
+ position: relative;
172
+ z-index: 1;
173
+ display: flex;
174
+ width: 100%;
175
+ max-width: 520px;
176
+ /* Le plafond ET le défilement interne : sans les deux, une modale au
177
+ * contenu long pousse ses actions hors de l'écran, et l'utilisateur ne
178
+ * peut plus ni valider ni annuler. */
179
+ max-height: 92vh;
180
+ flex-direction: column;
181
+ overflow: auto;
182
+ border: 1px solid var(--line);
183
+ border-radius: var(--r-card);
184
+ background: var(--white);
185
+ box-shadow: var(--shadow-panel);
186
+ }
187
+
188
+ .zv-modal__head {
189
+ display: flex;
190
+ align-items: flex-start;
191
+ justify-content: space-between;
192
+ gap: 16px;
193
+ padding: 20px 22px 0;
194
+ }
195
+
196
+ .zv-modal__title {
197
+ margin: 0;
198
+ font-family: var(--font-display);
199
+ letter-spacing: -0.02em;
200
+ font-size: var(--app-h2);
201
+ font-weight: 400;
202
+ color: var(--ink);
203
+ }
204
+
205
+ .zv-modal__body {
206
+ padding: 14px 22px;
207
+ font-size: var(--app);
208
+ line-height: 1.6;
209
+ color: var(--muted);
210
+ }
211
+
212
+ /* Les actions restent visibles quand le corps défile : c'est tout l'intérêt
213
+ * du plafond ci-dessus. */
214
+ .zv-modal__foot {
215
+ position: sticky;
216
+ bottom: 0;
217
+ display: flex;
218
+ justify-content: flex-end;
219
+ gap: 10px;
220
+ border-top: 1px solid var(--hairline);
221
+ background: var(--white);
222
+ padding: 14px 22px;
223
+ }
224
+
225
+ /* Modale destructive : le filet haut porte l'avertissement, pas un fond
226
+ * rouge — une surface d'alerte pleine rendrait le texte pénible à lire. */
227
+ .zv-modal--danger .zv-modal__panel {
228
+ border-top: 3px solid var(--err);
229
+ }
230
+
231
+ /* ─── Notification flottante ────────────────────────────────────────────
232
+ * ⚠️ HORS RELEVÉ, même raison : Mémoire signale tout en place, par une
233
+ * alerte de bloc. Cette notification est l'alerte du module de retours,
234
+ * détachée et posée en coin.
235
+ *
236
+ * Elle ne remplace PAS .zv-alert : une erreur de formulaire se dit à côté du
237
+ * champ, jamais dans un coin qui disparaît. Réserver ce composant à ce qui
238
+ * est vrai après coup — « copié », « enregistré », « envoyé ». */
239
+
240
+ .zv-toasts {
241
+ position: fixed;
242
+ z-index: 100;
243
+ right: 16px;
244
+ bottom: 16px;
245
+ display: flex;
246
+ flex-direction: column-reverse;
247
+ gap: 8px;
248
+ /* Le conteneur ne capte rien : seules les notifications elles-mêmes le
249
+ * font, sans quoi une colonne invisible bloquerait les clics dans tout le
250
+ * coin de l'écran. */
251
+ pointer-events: none;
252
+ }
253
+
254
+ .zv-toast {
255
+ pointer-events: auto;
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 10px;
259
+ max-width: 380px;
260
+ border: 1px solid var(--line);
261
+ border-radius: var(--r-panel);
262
+ background: var(--white);
263
+ box-shadow: var(--shadow-menu);
264
+ padding: 12px 14px;
265
+ font-size: var(--app-sm);
266
+ color: var(--ink);
267
+ }
268
+
269
+ .zv-toast--ok {
270
+ border-color: var(--ok-line);
271
+ background: var(--ok-soft);
272
+ color: var(--ok);
273
+ }
274
+
275
+ .zv-toast--err {
276
+ border-color: var(--err-line);
277
+ background: var(--err-soft);
278
+ color: var(--err);
279
+ }
280
+
281
+ /* ─── Mobile ────────────────────────────────────────────────────────────*/
282
+
283
+ @media (max-width: 767px) {
284
+ /* Voile et tiroir n'existent QUE sous le seuil : au-dessus, la navigation
285
+ * est une colonne permanente, elle n'occulte rien. */
286
+ .zv-scrim {
287
+ display: block;
288
+ }
289
+
290
+ .zv-drawer {
291
+ display: block;
292
+ }
293
+
294
+ /* La notification prend la largeur : un coin de 380px sur un écran de 390
295
+ * n'est plus un coin. */
296
+ .zv-toasts {
297
+ right: 12px;
298
+ left: 12px;
299
+ bottom: 12px;
300
+ }
301
+
302
+ .zv-toast {
303
+ max-width: none;
304
+ }
305
+ }