@zevra/ui 0.8.0 → 0.9.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 (65) hide show
  1. package/DECISIONS.md +227 -0
  2. package/dist/actions.d.ts +13 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +9 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/catalogue.d.ts.map +1 -1
  7. package/dist/catalogue.js +160 -11
  8. package/dist/catalogue.js.map +1 -1
  9. package/dist/css-inline.d.ts.map +1 -1
  10. package/dist/css-inline.js +1 -1
  11. package/dist/css-inline.js.map +1 -1
  12. package/dist/data.d.ts +34 -1
  13. package/dist/data.d.ts.map +1 -1
  14. package/dist/data.js +27 -0
  15. package/dist/data.js.map +1 -1
  16. package/dist/feedback.d.ts +49 -0
  17. package/dist/feedback.d.ts.map +1 -1
  18. package/dist/feedback.js +24 -0
  19. package/dist/feedback.js.map +1 -1
  20. package/dist/forms.d.ts +36 -0
  21. package/dist/forms.d.ts.map +1 -1
  22. package/dist/forms.js +24 -0
  23. package/dist/forms.js.map +1 -1
  24. package/dist/icon-data.d.ts +1 -1
  25. package/dist/icon-data.d.ts.map +1 -1
  26. package/dist/icon-data.js +42 -0
  27. package/dist/icon-data.js.map +1 -1
  28. package/dist/icon.d.ts +1 -1
  29. package/dist/icon.js +1 -1
  30. package/dist/layout.d.ts +21 -0
  31. package/dist/layout.d.ts.map +1 -1
  32. package/dist/layout.js +18 -0
  33. package/dist/layout.js.map +1 -1
  34. package/dist/navigation.d.ts +45 -7
  35. package/dist/navigation.d.ts.map +1 -1
  36. package/dist/navigation.js +54 -7
  37. package/dist/navigation.js.map +1 -1
  38. package/dist/surfaces.d.ts +18 -0
  39. package/dist/surfaces.d.ts.map +1 -1
  40. package/dist/surfaces.js +6 -0
  41. package/dist/surfaces.js.map +1 -1
  42. package/icons/index.json +126 -0
  43. package/icons/sprite.svg +21 -0
  44. package/package.json +4 -3
  45. package/react/actions.tsx +28 -2
  46. package/react/catalogue.tsx +343 -17
  47. package/react/css-inline.ts +1 -1
  48. package/react/data.tsx +87 -1
  49. package/react/feedback.tsx +98 -0
  50. package/react/forms.tsx +90 -0
  51. package/react/icon-data.ts +63 -0
  52. package/react/icon.tsx +2 -2
  53. package/react/layout.tsx +39 -0
  54. package/react/navigation.tsx +108 -6
  55. package/react/surfaces.tsx +45 -0
  56. package/src/base.css +16 -0
  57. package/src/components.css +135 -3
  58. package/src/data.css +143 -1
  59. package/src/feedback.css +196 -37
  60. package/src/forms.css +137 -1
  61. package/src/layout.css +72 -0
  62. package/src/navigation.css +207 -12
  63. package/src/surfaces.css +76 -7
  64. package/src/tokens.css +6 -0
  65. package/tokens/index.js +2 -0
package/src/feedback.css CHANGED
@@ -1,5 +1,6 @@
1
1
  /* Zevra UI — retours d'état : bandeaux, étiquettes, pastilles, absence,
2
- * travail en cours, curseur de flux.
2
+ * travail en cours, curseur de flux — et, depuis le 25/08/2026, la roue,
3
+ * le squelette et l'infobulle, longtemps refusés et enfin arbitrés.
3
4
  *
4
5
  * Dépend de tokens.css et de base.css (reset, fonte de corps, focus clavier
5
6
  * global). Indépendant des autres modules.
@@ -11,7 +12,8 @@
11
12
  * --warn, --warn-soft, --warn-line, --warn-ink,
12
13
  * --err, --err-soft, --err-line ;
13
14
  * typo — --font-display, --font-mono, --app, --app-sm,
14
- * --app-h2, --mono, --mono-sm.
15
+ * --app-h2, --mono, --mono-sm ;
16
+ * ombre — --shadow-menu (l'infobulle, qui flotte comme le menu).
15
17
  * Aucun autre. Il n'existe PAS de --ink-3…--ink-7, pas de --line-soft, pas de
16
18
  * --line-firm, pas d'échelles --text-* ni --mono-xs/--mono-2xs : une version
17
19
  * antérieure de cet en-tête les déclarait en dépendance, ce qui rendait le
@@ -37,11 +39,12 @@
37
39
  * d'un pixel ; c'est ce qui fait qu'un bandeau ample reste plus grand qu'un
38
40
  * bandeau courant, des deux côtés du seuil.
39
41
  *
40
- * Rien ici n'est interactif : pas de bouton de fermeture, pas de survol, pas
41
- * de champ. Les états :hover / :focus-visible / [disabled] / [aria-invalid]
42
- * n'ont donc aucune cible dans ce module — ils appartiennent aux boutons
43
- * (components.css) et aux champs (forms.css). Le focus clavier global de
44
- * base.css suffit au bandeau qu'on rend focusable pour l'annoncer.
42
+ * Presque rien ici n'est interactif : pas de bouton de fermeture, pas de
43
+ * champ. Les états [disabled] / [aria-invalid] n'ont aucune cible dans ce
44
+ * module — ils appartiennent aux boutons (components.css) et aux champs
45
+ * (forms.css). Le focus clavier global de base.css suffit au bandeau qu'on
46
+ * rend focusable pour l'annoncer. Seule exception, arbitrée le 25/08/2026 :
47
+ * l'infobulle, qui se montre par nature au survol et au focus de sa cible.
45
48
  */
46
49
 
47
50
  /* ─── Bandeaux d'état ───────────────────────────────────────────────────
@@ -67,15 +70,11 @@
67
70
  * nom de fichier à rallonge, identifiant, chemin. Sans coupure, il pousse
68
71
  * la page en défilement horizontal. */
69
72
  overflow-wrap: break-word;
70
- /* Angle vif, comme au relevé.
71
- * ⚠️ NON ARBITRÉ EN DA, et le module ne tranche pas à sa place : le
72
- * `border-radius: 0` posé ici et sur .zv-badge est la valeur relevée, pas
73
- * une « signature de la couche applicative ». Il entre en contradiction
74
- * directe avec .zv-empty__action, qui renvoie au .zv-btn de
75
- * components.css et donc à --r-btn (5px) : un écran vide affichera un
76
- * bouton arrondi au milieu de blocs à angle vif. La règle doit monter
77
- * dans DECISIONS.md ou tomber — elle ne peut pas rester implicite dans
78
- * quatre modules. */
73
+ /* Angle vif, comme au relevé — et, depuis le 25/08/2026, comme tout le
74
+ * paquet : l'arbitrage DA « angle vif partout » (DECISIONS.md) a ramené
75
+ * les quatre tokens de rayon à 0. La contradiction longtemps signalée ici
76
+ * (un bouton arrondi au milieu de blocs vifs dans l'écran vide) est
77
+ * éteinte par cet arbitrage, pas par ce module. */
79
78
  border-radius: 0;
80
79
  }
81
80
 
@@ -204,7 +203,7 @@
204
203
  /* Un état ne se coupe jamais en deux lignes : « EN CHARGE » sur deux
205
204
  * lignes se lit comme deux états. */
206
205
  white-space: nowrap;
207
- border-radius: 0; /* même arbitrage non tranché que .zv-alert, voir plus haut */
206
+ border-radius: 0; /* angle vif — relevé, et arbitré depuis (25/08/2026) */
208
207
  }
209
208
 
210
209
  /* Gabarit micro : niveau de log en tête de ligne, badge de la coque. À
@@ -421,8 +420,7 @@
421
420
  }
422
421
 
423
422
  /* L'action de l'écran vide (le bouton qui remplit la collection). Elle est
424
- * ici pour l'écart seul : le bouton lui-même vient de components.css — et
425
- * porte donc --r-btn, voir la contradiction signalée sur .zv-alert. */
423
+ * ici pour l'écart seul : le bouton lui-même vient de components.css. */
426
424
  .zv-empty__action {
427
425
  margin-top: 20px;
428
426
  }
@@ -459,10 +457,11 @@
459
457
 
460
458
  /* ⚠️ STATIQUE, et c'est un choix. Aucune animation dans la source : ni
461
459
  * @keyframes, ni classe d'animation, la pastille ne bat pas. Lui ajouter un
462
- * pouls est l'invention la plus tentante de tout le relevé — et les
463
- * @keyframes du paquet (motion.css) sont réservées à la vitrine, pilotées au
464
- * défilement. Ne pas confondre `.zv-dot` avec `.zv-anim-dot-active` de
465
- * motion.css : ce dernier anime un point de méthode, pas un état. */
460
+ * pouls est l'invention la plus tentante de tout le relevé. Les boucles de
461
+ * ce module (roue, squelette) disent une ATTENTE ; la pastille dit un
462
+ * ÉTAT, et un état ne bat pas. Ne pas confondre `.zv-dot` avec
463
+ * `.zv-anim-dot-active` de motion.css — ce dernier anime un point de
464
+ * méthode de la vitrine, pas un état. */
466
465
 
467
466
  /* ─── Curseur de flux ───────────────────────────────────────────────────
468
467
  * Bloc collé à la fin du texte en cours de diffusion, sur le dernier
@@ -496,21 +495,173 @@
496
495
  * garde existe côté script). C'est un défaut de l'application, pas une
497
496
  * intention de charte : ne pas le répliquer dans une app neuve.
498
497
  *
499
- * ─── Manques assumés : roue, squelette, infobulle ───────────────────────
500
- * AUCUNE MATIÈRE PREMIÈRE dans les cinq écrans relevés. Ni .zv-spinner, ni
501
- * .zv-skeleton, ni .zv-tooltip ne sont écrits ici : les livrer supposerait
502
- * d'inventer une durée, une amplitude et une géométrie que personne n'a
503
- * arbitrées, et un système ment moins en affichant un trou qu'une valeur
504
- * plausible. Rien n'appelle d'infobulle et rien n'en propose : le nom de
505
- * document tronqué n'a même pas d'attribut title, les barres des graphiques
506
- * n'affichent aucune valeur au pointeur.
498
+ * ─── Roue, squelette, infobulle : les manques, comblés ──────────────────
499
+ * Longtemps REFUSÉS ici même, faute de matière première dans les cinq
500
+ * écrans relevés — un système ment moins en affichant un trou qu'une valeur
501
+ * plausible. Tranchés le 25/08/2026 (DECISIONS.md, « les manques comblés »)
502
+ * en reprenant TELLES QUELLES les contraintes que ce module posait pour ce
503
+ * jour-là : pas de dégradé spectre — donc un squelette en --hairline, pas
504
+ * un balayage spectral — et une infobulle en petit panneau blanc, filet
505
+ * --line, ombre --shadow-menu, texte --app-sm / --ink-2, la seule
506
+ * combinaison déjà présente ailleurs dans le système. Les trois blocs
507
+ * suivent ; ce qui restait à inventer (durées, amplitudes) est dit sur
508
+ * place. */
509
+
510
+ /* ─── Roue ──────────────────────────────────────────────────────────────
511
+ * Pour l'attente COURTE ET LOCALE : un bouton, une cellule, un panneau qui
512
+ * n'a pas la place d'une phrase. Elle ne remplace PAS .zv-loading : la
513
+ * phrase qui dit le travail réel (« Recherche dans 4 812 documents… »)
514
+ * reste le motif par défaut du système.
507
515
  *
508
- * Contraintes à respecter le jour où la DA les tranche : pas de dégradé
509
- * spectre (réservé au filet de section, à la barre de progression et aux
510
- * points de méthode) — donc un squelette en --hairline, pas un balayage
511
- * spectral ; et une infobulle en petit panneau blanc, filet --line, ombre
512
- * --shadow-menu, texte --app-sm / --ink-2, qui est la seule combinaison déjà
513
- * présente ailleurs dans le système. */
516
+ * Géométrie empruntée, rien d'inventé sauf la durée : 16 px (la taille de
517
+ * .zv-icon), trait de 2 (l'épaisseur du jeu d'icônes), piste --accent-line,
518
+ * secteur --accent. 0,8 s linéaire — une rotation qui accélère se lit comme
519
+ * un à-coup. prefers-reduced-motion (base.css) la FIGE : l'anneau
520
+ * interrompu reste lisible immobile, c'est pour cela que le secteur est un
521
+ * quart et pas une moitié. */
522
+
523
+ @keyframes zv-spin {
524
+ to {
525
+ rotate: 1turn;
526
+ }
527
+ }
528
+
529
+ .zv-spinner {
530
+ display: inline-block;
531
+ width: 16px;
532
+ height: 16px;
533
+ flex: none;
534
+ border: 2px solid var(--accent-line);
535
+ border-top-color: var(--accent);
536
+ /* Rond de FORME, pas de style : c'est le cercle qui fait lire la
537
+ * rotation — même dérogation que la pastille d'état. */
538
+ border-radius: 9999px;
539
+ /* `rotate` et non `transform` : la propriété indépendante compose avec un
540
+ * éventuel transform de placement — la leçon des boucles de motion.css. */
541
+ animation: zv-spin 0.8s linear infinite;
542
+ }
543
+
544
+ /* En tête d'un panneau ou d'un écran d'attente : la roue seule, plus
545
+ * grande. Le trait ne grossit pas — même compensation optique que les
546
+ * icônes. */
547
+ .zv-spinner--lg {
548
+ width: 24px;
549
+ height: 24px;
550
+ }
551
+
552
+ /* ─── Squelette ─────────────────────────────────────────────────────────
553
+ * L'aplat --hairline exigé — pas de balayage spectral, pas de dégradé de
554
+ * gris que la charte n'a pas. La pulsation est une OPACITÉ (1 → 0,55), pas
555
+ * une couleur : elle dit « en chargement » sans introduire de teinte.
556
+ * 1,8 s en ease-in-out, la respiration de zv-breathe plutôt que la courbe
557
+ * d'entrée — c'est une boucle, pas un mouvement.
558
+ *
559
+ * La géométrie vient du CONSOMMATEUR (largeur et hauteur en style, comme la
560
+ * largeur de Progress) : un squelette qui invente ses dimensions ne
561
+ * ressemble jamais au contenu qu'il annonce. Seule la variante --text fixe
562
+ * une hauteur d'un cadratin, pour mimer une ligne du texte courant.
563
+ *
564
+ * ⚠️ Un squelette ANNONCE une structure : ne jamais en poser un pour un
565
+ * contenu dont la forme est inconnue — c'est .zv-loading qui dit ce cas. */
566
+
567
+ @keyframes zv-skeleton {
568
+ 0%,
569
+ 100% {
570
+ opacity: 1;
571
+ }
572
+ 50% {
573
+ opacity: 0.55;
574
+ }
575
+ }
576
+
577
+ .zv-skeleton {
578
+ display: block;
579
+ background: var(--hairline);
580
+ animation: zv-skeleton 1.8s ease-in-out infinite;
581
+ }
582
+
583
+ .zv-skeleton--text {
584
+ height: 1em;
585
+ }
586
+
587
+ /* ─── Infobulle ─────────────────────────────────────────────────────────
588
+ * La combinaison imposée par l'arbitrage : panneau blanc, filet --line,
589
+ * ombre --shadow-menu, texte --app-sm / --ink-2. Posée AU-DESSUS de la
590
+ * cible, centrée, à 6 px — au-dessus parce que le pointeur vient d'en bas
591
+ * et la masquerait en dessous.
592
+ *
593
+ * Elle se montre au survol et au focus de l'enveloppe (.zv-tooltip-hit),
594
+ * avec un retard de 300 ms à l'APPARITION seulement — sans lui, traverser
595
+ * une rangée d'icônes fait clignoter une infobulle par icône. La
596
+ * disparition, elle, est immédiate.
597
+ *
598
+ * Le masquage est `visibility` ET opacité, à dessein (WCAG 1.4.13) :
599
+ * · cachée en visibility:hidden, la bulle ne capte pas le pointeur — pas
600
+ * de zone de survol fantôme au-dessus de chaque cible ;
601
+ * · visible, elle EST survolable : descendante de l'enveloppe, le
602
+ * pointeur peut la traverser sans la fermer ;
603
+ * · Échap la ferme — la façade pose --off jusqu'au blur, seul
604
+ * comportement qui demande une ligne de script.
605
+ *
606
+ * ⚠️ Réservée à un COMPLÉMENT dispensable (un raccourci, une date exacte,
607
+ * une unité) : une commande n'a pas le droit de cacher son nom dans une
608
+ * infobulle, et le tactile ne survole pas. L'annonce passe par
609
+ * aria-describedby, que la façade React câble. */
610
+
611
+ .zv-tooltip-hit {
612
+ position: relative;
613
+ display: inline-flex;
614
+ }
615
+
616
+ .zv-tooltip {
617
+ position: absolute;
618
+ bottom: calc(100% + 6px);
619
+ left: 50%;
620
+ transform: translateX(-50%);
621
+ /* La couche du menu (navigation.css) : une infobulle flotte au même
622
+ * étage que lui et ne le croise jamais. */
623
+ z-index: 60;
624
+ width: max-content;
625
+ max-width: 240px;
626
+ border: 1px solid var(--line);
627
+ background: var(--white);
628
+ box-shadow: var(--shadow-menu);
629
+ padding: 6px 10px;
630
+ font-size: var(--app-sm);
631
+ line-height: 1.5;
632
+ font-weight: 500;
633
+ color: var(--ink-2);
634
+ text-align: left;
635
+ opacity: 0;
636
+ visibility: hidden;
637
+ /* La visibilité bascule APRÈS le fondu de sortie, jamais pendant. */
638
+ transition:
639
+ opacity 140ms cubic-bezier(0.2, 0.7, 0.2, 1),
640
+ visibility 0s linear 140ms;
641
+ }
642
+
643
+ .zv-tooltip-hit:hover > .zv-tooltip,
644
+ .zv-tooltip-hit:focus-within > .zv-tooltip {
645
+ opacity: 1;
646
+ visibility: visible;
647
+ transition-delay: 300ms, 300ms;
648
+ }
649
+
650
+ /* Fermée à Échap : posée par la façade, levée au blur ou à la sortie du
651
+ * pointeur. Déclarée APRÈS l'ouverture pour l'emporter à spécificité
652
+ * égale. */
653
+ .zv-tooltip-hit--off > .zv-tooltip {
654
+ opacity: 0;
655
+ visibility: hidden;
656
+ transition-delay: 0s, 0s;
657
+ }
658
+
659
+ /* Sous la cible, pour les rares cas où le haut est occupé (première ligne
660
+ * d'un tableau sous son en-tête). */
661
+ .zv-tooltip--bas {
662
+ bottom: auto;
663
+ top: calc(100% + 6px);
664
+ }
514
665
 
515
666
  /* ─── Mobile ────────────────────────────────────────────────────────────
516
667
  * Un seul point de rupture, 768px, comme dans tous les modules du paquet.
@@ -555,6 +706,14 @@
555
706
  min-width: 0;
556
707
  }
557
708
 
709
+ /* Même garde-fou que le menu et le popover : plafonnée à l'écran, la
710
+ * bulle ne pousse jamais la page en défilement horizontal. Le centrage
711
+ * sur une cible collée au bord peut encore la décaler — limite connue,
712
+ * une variante d'ancrage latéral s'arbitrera si le cas se présente. */
713
+ .zv-tooltip {
714
+ max-width: calc(100vw - 40px);
715
+ }
716
+
558
717
  /* ⚠️ NON RELEVÉ : aucune variante `lg:` sur le padding de l'écran vide.
559
718
  * Ce resserrement est une décision de portage, prise parce que 32px de
560
719
  * gouttière sur 375px laissent 311px de contenu à un bloc qui porte un
package/src/forms.css CHANGED
@@ -1,5 +1,7 @@
1
1
  /* Zevra UI — formulaires : champ, recherche, zone de texte, sélection,
2
- * case/radio, interrupteur, puces de filtre, zone de dépôt.
2
+ * case/radio, interrupteur, puces de filtre, zone de dépôt — et, depuis le
3
+ * 25/08/2026 (hors relevé, voir DECISIONS.md) : le champ à affixe et le
4
+ * curseur.
3
5
  *
4
6
  * DÉPEND de tokens.css (toutes les valeurs) et de base.css (reset
5
7
  * box-sizing, anneau :focus-visible). RIEN D'AUTRE : ce module ne lit ni
@@ -258,6 +260,61 @@
258
260
  color: var(--label);
259
261
  }
260
262
 
263
+ /* ─── Champ à affixe ────────────────────────────────────────────────────
264
+ * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). « 30 », seul dans un champ,
265
+ * ne dit pas s'il s'agit de jours ou d'euros. La grammaire est celle de
266
+ * .zv-search — le seul cadre-à-champ-nu du relevé : cadre --line, champ nu
267
+ * dedans, allumage :focus-within identique. L'affixe est en mono --label,
268
+ * comme les libellés de déclencheur : une unité est un libellé, pas une
269
+ * donnée. */
270
+
271
+ .zv-affix {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 9px;
275
+ min-width: 0;
276
+ border: 1px solid var(--line);
277
+ background: var(--white);
278
+ padding-inline: 12px;
279
+ transition:
280
+ border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),
281
+ box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);
282
+ }
283
+
284
+ .zv-affix:hover {
285
+ border-color: var(--accent-line);
286
+ }
287
+
288
+ /* Même prise de focus que le cadre de recherche : filet --accent plein,
289
+ * halo --accent-soft — et l'anneau du champ intérieur est remplacé par le
290
+ * cadre, pour la souris comme pour le clavier. */
291
+ .zv-affix:focus-within {
292
+ border-color: var(--accent);
293
+ box-shadow: 0 0 0 3px var(--accent-soft);
294
+ }
295
+
296
+ .zv-affix .zv-input--bare {
297
+ /* Le champ d'un formulaire, pas le composeur : il reprend le corps et le
298
+ * rythme vertical de .zv-input. */
299
+ font-size: var(--app);
300
+ padding-block: 9px;
301
+ }
302
+
303
+ .zv-affix .zv-input--bare:focus-visible {
304
+ outline: none;
305
+ }
306
+
307
+ .zv-affix__add {
308
+ flex: none;
309
+ font-family: var(--font-mono);
310
+ font-size: var(--mono);
311
+ font-weight: 600;
312
+ letter-spacing: 0.1em;
313
+ text-transform: uppercase;
314
+ white-space: nowrap;
315
+ color: var(--label);
316
+ }
317
+
261
318
  /* ─── Zone de texte ─────────────────────────────────────────────────────
262
319
  * DÉCISION, PAS UN RELEVÉ : il n'existe AUCUN <textarea> dans l'application,
263
320
  * sur aucun des huit écrans ouverts. Le composeur du chat est un <input> où
@@ -570,6 +627,79 @@
570
627
  color: var(--accent);
571
628
  }
572
629
 
630
+ /* ─── Curseur ───────────────────────────────────────────────────────────
631
+ * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Aucune source ; dérivé de
632
+ * l'interrupteur redessiné : bouton CARRÉ de 14 px (la taille exacte du
633
+ * bouton du switch), --accent, --accent-deep au survol. La piste fait 3 px
634
+ * — la hauteur de .zv-progress — en --line, remplie en --accent jusqu'au
635
+ * curseur. PAS le spectre : la barre de progression est l'un des trois
636
+ * emplois autorisés, le curseur n'en est pas un.
637
+ *
638
+ * Le remplissage est peint par le FOND de l'input (un dégradé à arrêt
639
+ * franc), position portée par --zv-range-pos que la façade React calcule —
640
+ * une géométrie, comme la largeur de Progress, pas une valeur de charte.
641
+ * Sans elle, la piste s'affiche remplie à moitié : poser la variable, ou
642
+ * assumer le repli. Le bouton, lui, passe par les pseudo-éléments de
643
+ * chaque moteur — c'est le seul contrôle du paquet qui l'exige. */
644
+
645
+ .zv-range {
646
+ --zv-range-pos: 50%;
647
+ appearance: none;
648
+ -webkit-appearance: none;
649
+ display: block;
650
+ width: 100%;
651
+ min-width: 0;
652
+ height: 20px;
653
+ margin: 0;
654
+ padding: 0;
655
+ border: 0;
656
+ background: linear-gradient(90deg, var(--accent) var(--zv-range-pos), var(--line) 0);
657
+ background-repeat: no-repeat;
658
+ background-size: 100% 3px;
659
+ background-position: 0 center;
660
+ cursor: pointer;
661
+ }
662
+
663
+ .zv-range::-webkit-slider-thumb {
664
+ -webkit-appearance: none;
665
+ appearance: none;
666
+ width: 14px;
667
+ height: 14px;
668
+ border: 0;
669
+ border-radius: 0;
670
+ background: var(--accent);
671
+ transition: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
672
+ }
673
+
674
+ .zv-range::-moz-range-thumb {
675
+ width: 14px;
676
+ height: 14px;
677
+ border: 0;
678
+ border-radius: 0;
679
+ background: var(--accent);
680
+ }
681
+
682
+ .zv-range:hover::-webkit-slider-thumb {
683
+ background: var(--accent-deep);
684
+ }
685
+
686
+ .zv-range:hover::-moz-range-thumb {
687
+ background: var(--accent-deep);
688
+ }
689
+
690
+ .zv-range[disabled] {
691
+ opacity: 0.6;
692
+ cursor: not-allowed;
693
+ }
694
+
695
+ .zv-range[disabled]:hover::-webkit-slider-thumb {
696
+ background: var(--accent);
697
+ }
698
+
699
+ .zv-range[disabled]:hover::-moz-range-thumb {
700
+ background: var(--accent);
701
+ }
702
+
573
703
  /* ─── Puces de filtre ───────────────────────────────────────────────────
574
704
  * Le seul mécanisme de filtrage de l'application (bibliotheque-client.tsx:
575
705
  * 143-180, repertoire-client.tsx:79-94, logs-client.tsx:193-207).
@@ -912,6 +1042,12 @@
912
1042
  padding-block: 12px;
913
1043
  }
914
1044
 
1045
+ /* La piste ne bouge pas (le fond reste centré) : seule la boîte grandit
1046
+ * pour faire une cible tactile. */
1047
+ .zv-range {
1048
+ height: var(--tap);
1049
+ }
1050
+
915
1051
  .zv-drop {
916
1052
  padding: 32px 16px;
917
1053
  }
package/src/layout.css CHANGED
@@ -167,6 +167,36 @@
167
167
  font-size: 17px;
168
168
  }
169
169
 
170
+ /* ─── Bandeau d'annonce ─────────────────────────────────────────────────
171
+ * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La bande AU-DESSUS de la
172
+ * barre de navigation : annonce de version, information réglementaire. Le
173
+ * ton information de l'alerte (--accent-soft sur --accent-line), en pleine
174
+ * largeur. UN SEUL par page, et jamais pour du marketing permanent : un
175
+ * bandeau qui ne part pas devient un bourrelet de chrome. */
176
+
177
+ .zv-banner {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ gap: 10px;
182
+ flex-wrap: wrap;
183
+ border-bottom: 1px solid var(--accent-line);
184
+ background: var(--accent-soft);
185
+ padding: 9px 20px;
186
+ font-size: 13px;
187
+ font-weight: 500;
188
+ text-align: center;
189
+ color: var(--accent);
190
+ }
191
+
192
+ /* Le lien du bandeau se souligne : sur un fond déjà indigo, la couleur
193
+ * seule ne suffit plus à le distinguer du texte. */
194
+ .zv-banner a {
195
+ color: var(--accent-deep);
196
+ font-weight: 600;
197
+ text-decoration: underline;
198
+ }
199
+
170
200
  /* ─── Chrome : navigation ───────────────────────────────────────────────*/
171
201
 
172
202
  .zv-nav {
@@ -433,6 +463,39 @@
433
463
  opacity: 0.72;
434
464
  }
435
465
 
466
+ /* ─── Carrousel ─────────────────────────────────────────────────────────
467
+ * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Défilement natif
468
+ * scroll-snap, AUCUN JavaScript : pas de flèches (elles exigeraient un
469
+ * script et un arbitrage de dessin), pas de points. Le clavier passe par le
470
+ * focus du conteneur, que la façade rend focalisable.
471
+ *
472
+ * La barre de défilement tombe — le rail visible transformerait la vitrine
473
+ * en fenêtre d'application. C'est un MASQUAGE, pas l'habillage que
474
+ * navigation.css refuse : aucune dimension inventée.
475
+ *
476
+ * ⚠️ Un carrousel reste un aveu d'espace : la règle de variété lui préfère
477
+ * une grille tant que le contenu tient. La largeur des éléments vient du
478
+ * consommateur — un carrousel qui la fixerait déciderait de la mise en page
479
+ * à la place de la grille. */
480
+
481
+ .zv-carousel {
482
+ display: flex;
483
+ gap: 24px;
484
+ overflow-x: auto;
485
+ scroll-snap-type: x mandatory;
486
+ scrollbar-width: none;
487
+ }
488
+
489
+ .zv-carousel::-webkit-scrollbar {
490
+ display: none;
491
+ }
492
+
493
+ .zv-carousel__item {
494
+ flex: none;
495
+ min-width: 0;
496
+ scroll-snap-align: start;
497
+ }
498
+
436
499
  /* ─── Pied de page réduit ───────────────────────────────────────────────
437
500
  * Pages secondaires. Le pied complet à quatre colonnes reste réservé à la
438
501
  * page d'accueil. */
@@ -507,6 +570,15 @@
507
570
  margin-right: 20px;
508
571
  }
509
572
 
573
+ .zv-banner {
574
+ padding: 8px 16px;
575
+ font-size: 12px;
576
+ }
577
+
578
+ .zv-carousel {
579
+ gap: 16px;
580
+ }
581
+
510
582
  .zv-grid-12,
511
583
  .zv-grid-3,
512
584
  .zv-section-head,