@zevra/ui 0.26.0 → 0.27.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.
@@ -4,7 +4,7 @@
4
4
  * document isolé (iframe, shadow root) qui ne peut pas résoudre de <link>.
5
5
  * Une app normale importe les modules CSS un par un — pas cette constante,
6
6
  * qui ne se minifie ni ne se met en cache comme une feuille. */
7
- export const CSS_COMPLET = "/* ── tokens.css ── */\n/* Zevra — Design tokens « Encre & Papier »\n * Source : passation DA du 23/08/2026 (canvas Claude Design « Zevra DA Encre\n * et Papier »). Ce fichier est la SOURCE DE VÉRITÉ des valeurs de marque :\n * toute app doit lire ces variables plutôt que recopier un hexadécimal.\n *\n * ⚠️ Système CLAIR par nature (papier/encre). Les surfaces sombres se font\n * avec --manifeste et --accent-on-dark, pas en inversant les neutres : les\n * contrastes ont été réglés dans ce sens-là et pas dans l'autre.\n */\n\n:root {\n /* Largeur de page. Le relevé de la passation a été fait sur une trame de\n * 1440 : au-delà, les enveloppes (zv-gutter et variantes) se centrent au\n * lieu de s'étirer — un hero qui colle au bord gauche d'un écran de\n * 2 560 px ne raconte plus une page, il raconte un débordement. */\n --page-max: 1440px;\n\n /* Neutres (charte Mémoire) */\n --ink: #0b1020;\n /* Niveau intermédiaire, entre l'encre et le gris : titre de colonne,\n * valeur de cellule, segment de fil d'Ariane. L'interface applicative en a\n * besoin, la vitrine non — d'où son absence des premières planches.\n * Contraste 7,05 sur papier : conforme AA sur texte normal. */\n --ink-2: #4a5468;\n --muted: #5a637a;\n --label: #8a93a8;\n --line: #e3e6ef;\n --hairline: #eef1f8;\n --paper: #f5f6fa;\n --panel: #fbfcfe;\n --white: #ffffff;\n\n /* Indigo (action, labels, liens) */\n --accent-deep: #1e1b8c;\n --accent: #2a2aa8;\n --accent-line: #c9d6f5;\n --accent-soft: #eaf1ff;\n\n /* Spectre produit (prélevé dans les logos officiels).\n * ⚠️ Usage strictement limité : filet de section (56×2), barre de\n * progression, points de méthode. Jamais en fond, jamais en texte. */\n --violet: #8a3af8;\n --iris: #5848f8;\n --bleu: #2f6ef8;\n --cyan: #14c2f8;\n --lavande: #b8a8f8;\n --spectre: linear-gradient(90deg, #8a3af8, #2f6ef8 55%, #14c2f8);\n\n /* Fond sombre (bande manifeste) — dégradé type logo */\n --manifeste: linear-gradient(135deg, #06063a 0%, #0b0b5e 28%, #1e1b8c 55%, #3d36d6 80%, #2080f0 100%);\n --accent-on-dark: #a5b4fc;\n\n /* États — trois familles symétriques : le ton plein (texte, icône), le\n * fond atténué (bandeau d'alerte) et le filet (bordure de carte).\n * Relevés dans l'application Mémoire, où ils servent déjà en production. */\n --ok: #1c6b4a;\n --ok-soft: #e6f3ec;\n --ok-line: #bfdccb;\n --warn: #b98a1f;\n --warn-soft: #faf3e2;\n --warn-line: #ebdcb4;\n /* L'ambre plein est trop clair pour du texte sur fond atténué : il lui\n * faut une déclinaison sombre. --ok et --err, eux, sont déjà assez\n * contrastés pour servir de couleur de texte — d'où l'asymétrie. */\n --warn-ink: #8a6d1e;\n --err: #b5202c;\n --err-soft: #fdece9;\n --err-line: #efc9cd;\n\n /* Typo — Google Fonts : Outfit (display), Public Sans (texte), Space Grotesk (labels) */\n --font-display: \"Outfit\", \"Helvetica Neue\", Arial, sans-serif; /* titres 200-400 */\n --font-text: \"Public Sans\", \"Helvetica Neue\", Arial, sans-serif; /* 400/600 */\n --font-mono: \"Space Grotesk\", \"Menlo\", monospace; /* labels caps, 0.18-0.2em */\n /* Chasse fixe RÉELLE, pour le code seulement. --font-mono (Space Grotesk)\n * est proportionnelle : elle porte le REGISTRE des labels, pas\n * l'alignement — du code exige la chasse fixe entière, que tabular-nums\n * ne donne qu'aux chiffres. Pile système, aucune fonte chargée.\n * HORS RELEVÉ (25/08/2026), voir DECISIONS.md « Bloc de code ». */\n --font-code: ui-monospace, \"SF Mono\", \"Menlo\", \"Consolas\", monospace;\n\n /* Échelle display (letter-spacing -0.02em, graisse 300 + chute en 400 --accent-deep) */\n --h1: 76px;\n --h2: 56px;\n --h3: 38px;\n --card-title: 24px;\n --lead: 18px;\n --body: 15px;\n\n /* Rayons — ARBITRÉS À ZÉRO le 25/08/2026.\n *\n * La DA a tranché sur pièces : « très carré, ça fait pro et inspire\n * confiance » ; le rond disait « flexible, jovial », qui n'est pas ce que\n * vend Zevra. Le relevé donnait 5/3/10/16px, hérités d'un état antérieur\n * de la charte — et déjà contredits sur place, puisque les champs de\n * saisie, les étiquettes et les pastilles d'initiales étaient à angle vif\n * depuis le premier jour. Les quatre tokens s'alignent sur eux.\n *\n * ⚠️ Les tokens RESTENT, à zéro : ils nomment quatre intentions\n * distinctes (action, pastille, panneau flottant, carte). Le jour où la DA\n * veut rendre de la rondeur à l'une d'elles, c'est une ligne ici — pas une\n * chasse aux `border-radius` dans douze fichiers.\n *\n * ⚠️ Ce qui n'est PAS concerné : le rond de forme, qui porte du sens et\n * non du style — pastille d'état, bouton d'interrupteur, radio, halo. Ils\n * écrivent leur `border-radius` en clair, sans passer par un token. */\n --r-btn: 0;\n --r-pill: 0;\n --r-panel: 0;\n --r-card: 0;\n\n /* Ombres */\n --shadow-card: 0 2px 10px rgba(11, 16, 32, 0.05);\n --shadow-panel: 0 24px 60px rgba(30, 27, 140, 0.12);\n --shadow-btn: 0 10px 28px rgba(30, 27, 140, 0.30);\n --shadow-glyph: drop-shadow(0 24px 32px rgba(88, 72, 248, 0.28));\n\n /* ─── Couche applicative ──────────────────────────────────────────────\n * Une interface est plus dense qu'une page vitrine : elle a besoin d'une\n * échelle plus serrée que --body/--lead, et de mesures que la vitrine\n * n'a jamais eu à nommer.\n *\n * Relevé dans l'application Mémoire, puis RATIONALISÉ : la production\n * emploie huit paliers de texte répartis dans 5,5 px, avec des demi-pixels\n * et des écarts d'un pixel. C'est de l'entropie d'utilitaires, pas une\n * échelle. Les paliers ci-dessous les regroupent ; le détail des\n * rabattements est dans DECISIONS.md. */\n\n /* Texte applicatif */\n --app-lg: 14px; /* corps de message, lien de navigation */\n --app: 13px; /* texte courant : cellule, carte, champ de saisie */\n --app-sm: 12px; /* mention secondaire : statut, aide, note de contexte */\n\n /* Titres applicatifs — au-dessus, --card-title (24px) fait le titre de page */\n --app-h2: 20px; /* titre de fiche, d'état vide, d'interstice */\n --app-h3: 16px; /* titre de panneau latéral, de feuille de source */\n\n /* Labels capitales, échelle applicative */\n --mono-lg: 11px; /* compteur, fil d'Ariane, horodatage */\n --mono: 10px; /* métadonnée, label de colonne */\n --mono-sm: 9px; /* badge dense : niveau de log, statut de coque */\n\n /* Mesures */\n --tap: 40px; /* cible tactile minimale, sous le point de rupture */\n --topbar-h: 52px; /* hauteur de la barre supérieure applicative */\n\n /* Ombre de menu déroulant — la seule surface qui flotte au-dessus du\n * contenu sans être une carte ; ni --shadow-card ni --shadow-panel ne\n * conviennent, l'une est trop plate, l'autre trop théâtrale. */\n --shadow-menu: 0 8px 24px rgba(11, 16, 32, 0.1);\n\n /* Couleurs de format de fichier. Elles ne viennent PAS de la charte : ce\n * sont les couleurs que le monde associe à ces formats, et les détourner\n * rendrait les vignettes illisibles au premier coup d'œil. Seul usage\n * autorisé : la pastille de format. */\n --fmt-pdf: #c4362b;\n --fmt-docx: #2a5ba8;\n\n /* Signature */\n --spec-filet: 56px; /* filet de spectre 56×2px devant chaque kicker */\n}\n\n\n/* ── base.css ── */\n/* Zevra UI — socle : reset, document, typographie, signature\n * Dépend de tokens.css. À charger en premier.\n *\n * Chaque valeur ci-dessous est relevée dans les maquettes de la passation du\n * 23/08/2026. Quand deux planches divergent, l'arbitrage est écrit dans\n * DECISIONS.md — jamais tranché en silence.\n *\n * POINT DE RUPTURE : 768px. Il ne vient PAS du relevé (les cinq maquettes sont\n * à largeur fixe, sans une seule media query). C'est la convention `md` du\n * projet, et les valeurs mobiles viennent de Mobile.html, qui est un rendu\n * figé à 390px. Les quatre modules utilisent ce seuil et lui seul.\n */\n\n/* ─── Reset ─────────────────────────────────────────────────────────────\n * Posé ici et nulle part ailleurs : les autres modules ne touchent ni au\n * box-sizing ni au body. */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n background: var(--paper);\n color: var(--ink);\n font-family: var(--font-text);\n -webkit-font-smoothing: antialiased;\n}\n\n/* ─── Liens ─────────────────────────────────────────────────────────────\n * Restreint aux liens SANS classe. Les maquettes stylent `a` nu, ce qui\n * repeint le libellé des boutons au survol (a:hover, 0-1-1, l'emporte sur\n * .zv-btn, 0-1-0) — bug visible dans Main.html et signalé au relevé. Le\n * périmètre est réduit plutôt que neutralisé par un :hover inventé. */\n\na:not([class]) {\n color: var(--accent);\n text-decoration: none;\n}\n\na:not([class]):hover {\n color: var(--accent-deep);\n}\n\n.zv-link {\n color: var(--accent);\n text-decoration: none;\n}\n\n.zv-link:hover {\n color: var(--accent-deep);\n}\n\n/* ─── Accessibilité ─────────────────────────────────────────────────────\n * HORS RELEVÉ, et assumé : aucune des cinq maquettes ne déclare d'état de\n * focus. Un système livré sans focus clavier est inutilisable au clavier —\n * on ne réplique pas cette lacune. Seules des valeurs existantes sont\n * employées. */\n\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n/* Texte pour lecteurs d'écran seulement — la géométrie standard du motif,\n * aucune valeur de charte. HORS RELEVÉ (25/08/2026) : ajouté avec la roue,\n * dont le libellé d'annonce doit être un TEXTE dans le flux (un aria-label\n * sur un span vide n'est pas annoncé par tous les lecteurs). */\n.zv-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* ─── Titres ────────────────────────────────────────────────────────────\n * Outfit, graisse 300, interlettrage -0.02em. L'échelle est celle de\n * Main.html, la planche de référence. */\n\n.zv-display {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n}\n\n.zv-h1 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h1);\n line-height: 1.05;\n font-weight: 300;\n color: var(--ink);\n max-width: 1100px;\n text-wrap: balance;\n}\n\n.zv-h2 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h2);\n line-height: 1.06;\n font-weight: 300;\n text-wrap: balance;\n}\n\n/* Le titre de CTA final est d'un cran au-dessus des titres de section :\n * 64px contre 56px (Main.html). Aucun token ne couvre cette taille. */\n.zv-h2--cta {\n font-size: 64px;\n max-width: 900px;\n}\n\n/* Titre de section des landings produit : un cran EN DESSOUS du 56px de la\n * home. Les trois landings de la passation le posent en classe `.h2`, aux\n * côtés des 56 et 64px qui restent pour la page d'accueil. */\n.zv-h2--sm {\n font-size: 48px;\n}\n\n.zv-h3 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h3);\n line-height: 1.1;\n font-weight: 300;\n max-width: 480px;\n}\n\n.zv-card-title {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--card-title);\n line-height: 1.25;\n font-weight: 400;\n}\n\n/* La deuxième moitié de phrase d'un titre bascule en indigo et prend un cran\n * de graisse. Motif systématique : présent dans les cinq maquettes. */\n.zv-em {\n color: var(--accent-deep);\n font-weight: 400;\n /* Le composant `Em` rend un vrai <em>, que le navigateur penche par\n * défaut — et AUCUNE italique n'est chargée : Outfit n'en a pas, il en\n * fabrique donc une oblique de synthèse. Sur un titre de 76px, ça se voit.\n * La charte ne demande à cette classe qu'un changement de couleur et de\n * graisse ; on remet donc la droite. (`.zv-inset em` portait déjà cette\n * règle : elle vit désormais là où le besoin est structurel.) */\n font-style: normal;\n}\n\n/* Sur fond sombre l'accent ne change pas de couleur mais de graisse. */\n.zv-em--on-dark {\n color: var(--white);\n font-weight: 500;\n}\n\n/* ─── Texte courant ─────────────────────────────────────────────────────\n * Quatre paliers relevés : 18 (chapô), 16 (colonne d'accompagnement),\n * 15 (corps), 13 (carte dense). */\n\n.zv-lead {\n margin: 0;\n font-size: var(--lead);\n line-height: 1.6;\n color: var(--muted);\n max-width: 660px;\n text-wrap: pretty;\n}\n\n.zv-body {\n margin: 0;\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* 16px : colonne de droite des en-têtes de section (Main.html). */\n.zv-body--lg {\n font-size: 16px;\n}\n\n/* 13px : légende de carte, texte serré. */\n.zv-body--dense {\n font-size: 13px;\n}\n\n.zv-body strong,\n.zv-lead strong {\n font-weight: 600;\n color: var(--ink);\n}\n\n/* 14px : texte de règle et de légende des planches. */\n.zv-rule {\n margin: 0;\n font-size: 14px;\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* 12,5px : la note d'accompagnement d'un bloc ou d'une carte de\n * documentation. Le plus petit palier de texte courant du système — en\n * dessous, on passe en label capitales. */\n.zv-note {\n margin: 0;\n font-size: 12.5px;\n line-height: 1.55;\n color: var(--muted);\n}\n\n/* ─── Labels capitales ──────────────────────────────────────────────────\n * Space Grotesk, majuscules, très interlettré. Trois tailles seulement :\n * 11px (kicker de section), 10px (label de carte), 9px (micro-label). */\n\n.zv-mono {\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.2em;\n font-weight: 500;\n font-size: 11px;\n color: var(--accent);\n}\n\n.zv-mono--label {\n font-size: 10px;\n font-weight: 700;\n}\n\n.zv-mono--micro {\n font-size: 9px;\n font-weight: 500;\n color: var(--label);\n}\n\n.zv-mono--muted {\n color: var(--label);\n}\n\n.zv-mono--ink {\n color: var(--ink);\n}\n\n/* Sur fond sombre les labels passent en indigo clair, jamais en blanc pur. */\n.zv-mono--on-dark {\n color: var(--accent-on-dark);\n}\n\n/* Accroche produit, posée sous le nom d'un glyphe : « Votre savoir,\n * interrogeable en une phrase ». Moins interlettrée que le label qu'elle\n * suit (0.14em contre 0.2em) — sinon la phrase se disloque. */\n.zv-tagline {\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.14em;\n font-weight: 500;\n font-size: 9px;\n line-height: 1.6;\n color: var(--muted);\n text-align: center;\n /* Remonte sous le label : l'ensemble se lit comme un seul bloc. */\n margin-top: -4px;\n}\n\n/* ─── Signature : le filet de spectre ───────────────────────────────────\n * 56×2px devant chaque kicker. C'est, avec la barre de progression et les\n * points de méthode, l'un des trois seuls emplois autorisés du spectre.\n *\n * ⚠️ ARBITRAGE (voir DECISIONS.md) : --spectre vaut violet → bleu → cyan,\n * comme la charte des logos qui le nomme « signature ». Trois autres\n * planches posent iris → violet → cyan. Le token fait foi ici ; la question\n * est ouverte pour la DA. */\n\n.zv-spec {\n display: block;\n width: var(--spec-filet);\n height: 2px;\n background: var(--spectre);\n /* Angle vif comme tout le reste depuis le 25/08 — sur 2px de haut, le\n * rayon d'1px du relevé ne se voyait de toute façon pas. */\n border-radius: 0;\n}\n\n/* Bas de page : filet raccourci. */\n.zv-spec--sm {\n width: 40px;\n}\n\n/* Sur fond sombre le filet s'éclaircit pour rester lisible. */\n.zv-spec--on-dark {\n background: linear-gradient(90deg, #7fd8ff, var(--violet));\n}\n\n/* Filet + label, la composition de tête de section. */\n.zv-kicker {\n display: flex;\n align-items: center;\n gap: 14px;\n}\n\n.zv-kicker--stack {\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n\n.zv-kicker--center {\n flex-direction: column;\n align-items: center;\n gap: 14px;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Valeurs relevées dans Mobile.html. Le seuil, lui, est la convention du\n * projet (cf. en-tête). */\n\n@media (max-width: 767px) {\n .zv-h1 {\n font-size: 40px;\n line-height: 1.08;\n }\n\n .zv-h2 {\n font-size: 30px;\n line-height: 1.1;\n }\n\n .zv-h2--cta {\n font-size: 32px;\n line-height: 1.08;\n }\n\n .zv-card-title {\n font-size: 18px;\n font-weight: 500;\n }\n\n .zv-lead {\n font-size: 15px;\n line-height: 1.55;\n }\n\n /* .zv-body garde 15px : c'est déjà la taille du chapô mobile. Seule\n * l'interligne se resserre — et --dense reste distinct, à 13px/1.45. */\n .zv-body {\n line-height: 1.55;\n }\n\n .zv-body--dense {\n font-size: 13px;\n line-height: 1.45;\n }\n\n .zv-mono {\n letter-spacing: 0.18em;\n font-size: 9px;\n }\n\n .zv-mono--label {\n font-size: 8px;\n }\n\n .zv-spec {\n width: 44px;\n }\n\n /* Re-déclaré : une media query n'ajoute pas de spécificité, le modificateur\n * serait sinon écrasé par la règle ci-dessus. */\n .zv-spec--sm {\n width: 40px;\n }\n}\n\n\n/* ── components.css ── */\n/* Zevra UI — composants : boutons, badges, cartes, encarts\n * Dépend de tokens.css et base.css.\n *\n * Les cinq maquettes ne divergent jamais sur l'intention, souvent sur le\n * chiffre. La règle appliquée ici : Main.html fait foi (c'est la page de\n * référence, la plus complète), les écarts des autres planches sont notés\n * en commentaire et détaillés dans DECISIONS.md.\n */\n\n/* ─── Boutons ───────────────────────────────────────────────────────────\n * UN SEUL bouton plein par écran : l'indigo porte l'action, il perd tout\n * s'il est répété. Les actions secondaires prennent la variante fantôme.\n *\n * Hauteur 48px : Main.html, Mobile.html et le prototype s'accordent.\n * DesignSystem.html pose 46px — planche isolée, écartée. */\n\n.zv-btn,\n.zv-btn--ghost,\n.zv-btn--ink,\n.zv-btn--danger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n height: 48px;\n padding: 0 24px;\n border-radius: var(--r-btn);\n font-family: var(--font-text);\n font-weight: 600;\n font-size: 14px;\n text-decoration: none;\n border: 0;\n cursor: pointer;\n /* La courbe maison, 160 ms — coupée globalement par prefers-reduced-motion\n * (base.css). Le fond en dégradé du bouton plein ne s'interpole pas : lui\n * seul bascule sans fondu, l'écart est trop faible pour se voir. */\n transition:\n background-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n/* ─── Le plein ne s'applique QUE si aucune variante n'est posée ────────\n *\n * `zv-btn`, `zv-btn--ghost`, `zv-btn--ink` et `zv-btn--danger` sont quatre\n * classes AUTONOMES et mutuellement exclusives : le socle commun ci-dessus\n * les groupe dans un même sélecteur, ce qui leur donne exactement l'air\n * d'une base et de ses modificateurs. `Button` n'en rend jamais qu'une —\n * mais une app qui écrit ses classes à la main lit du BEM et compose les\n * deux. C'est arrivé (mcp-factory, 0.19.0), et le résultat était vicieux :\n * `.zv-btn:hover` (0,2,0) l'emportait sur `.zv-btn--ghost` (0,1,0), d'où un\n * fond indigo sous un texte encre — illisible, et seulement AU SURVOL, donc\n * invisible en relecture comme en capture.\n *\n * Le `:not(:where(…))` dit ce que le système voulait déjà dire : ces quatre\n * classes s'excluent. `:where()` a une spécificité NULLE — la garde ne\n * change donc aucun arbitrage existant, et une app qui surcharge `.zv-btn`\n * continue de gagner exactement comme avant.\n *\n * ⚠️ À reporter sur toute règle future qui peint le bouton plein. */\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)) {\n /* Le sommet du dégradé. RELEVÉ dans Main.html à #2b2bb4, mais DÉRIVÉ de\n * --accent depuis la 0.19.0 : la déclaration étant portée par le bouton\n * lui-même, une redéfinition de marque héritée d'un conteneur ne\n * l'atteignait pas — un cabinet en marque blanche obtenait un dégradé\n * allant de l'indigo Zevra à SA couleur. Pour la charte par défaut,\n * l'écart avec la valeur relevée est d'un point de bleu, invisible.\n * Les états de survol le dérivent ensuite par color-mix. */\n --zv-btn-haut: var(--accent);\n background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);\n color: var(--white);\n /* Le liseré interne fait tout le galbe du bouton : il est dans Main.html\n * mais absent de --shadow-btn, d'où la composition ici. */\n box-shadow:\n 0 1px 0 rgba(255, 255, 255, 0.18) inset,\n var(--shadow-btn);\n}\n\n.zv-btn--ghost {\n background: var(--white);\n color: var(--ink);\n border: 1px solid var(--accent-line);\n}\n\n.zv-btn--ink {\n background: var(--ink);\n color: var(--white);\n}\n\n/* Action DESTRUCTIVE : la confirmation d'une modale danger, la suppression\n * définitive. HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : sans elle,\n * « Supprimer » se rendait en plein indigo et se lisait comme n'importe\n * quelle action. Aplat --err, sans dégradé ni ombre indigo.\n * ⚠️ Elle COMPTE comme le bouton plein de l'écran : la règle « un seul plein\n * par page » compte les aplats, pas les couleurs. */\n.zv-btn--danger {\n background: var(--err);\n color: var(--white);\n}\n\n/* ─── États de survol et d'appui ────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : aucune maquette n'a d'état\n * hover — elles sont figées. Le survol ASSOMBRIT, l'appui assombrit\n * davantage et rassoit le bouton (l'ombre tombe, le liseré s'éteint).\n * Les teintes sont DÉRIVÉES par color-mix — l'idiome du voile\n * (overlays.css) — des couleurs que le bouton porte déjà : le sommet de\n * dégradé relevé (--zv-btn-haut, sans token) et --accent-deep. Aucune\n * teinte nouvelle n'entre. Aucun déplacement : les entrées du système\n * montent au défilement, un bouton qui saute au survol raconterait autre\n * chose.\n * Déclarés AVANT [disabled], qui doit l'emporter à spécificité égale. */\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):hover {\n background: linear-gradient(\n 180deg,\n color-mix(in oklab, var(--zv-btn-haut) 86%, var(--ink)) 0%,\n color-mix(in oklab, var(--accent-deep) 86%, var(--ink)) 100%\n );\n}\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):active {\n background: linear-gradient(\n 180deg,\n color-mix(in oklab, var(--zv-btn-haut) 72%, var(--ink)) 0%,\n color-mix(in oklab, var(--accent-deep) 72%, var(--ink)) 100%\n );\n box-shadow: none;\n}\n\n.zv-btn--ghost:hover {\n border-color: var(--accent);\n}\n\n.zv-btn--ghost:active {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n\n/* Un aplat d'encre ne peut pas s'assombrir visiblement : il s'ÉCLAIRCIT au\n * survol, et l'appui revient à l'encre pleine — plus sombre que le survol,\n * c'est ce qui fait lire l'enfoncement. */\n.zv-btn--ink:hover {\n background: color-mix(in oklab, var(--ink) 82%, var(--white));\n}\n\n.zv-btn--ink:active {\n background: var(--ink);\n}\n\n.zv-btn--danger:hover {\n background: color-mix(in oklab, var(--err) 86%, var(--ink));\n}\n\n.zv-btn--danger:active {\n background: color-mix(in oklab, var(--err) 72%, var(--ink));\n}\n\n/* Variante compacte : barres de navigation. L'ombre tombe — un bouton de\n * chrome ne décolle pas de la page. */\n.zv-btn--sm {\n height: 40px;\n padding: 0 18px;\n box-shadow: none;\n}\n\n/* CTA mobile : le bouton prend toute la largeur. */\n.zv-btn--block {\n width: 100%;\n}\n\n/* Bouton d'action à icône SEULE : l'envoi d'un composeur, l'action unique\n * d'une barre. HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Le libellé\n * accessible est OBLIGATOIRE (aria-label), et s'écrit pour l'ACTION.\n * Ne pas confondre : .zv-icon-btn est le chrome nu (40 px, sans cadre),\n * .zv-chrome-btn la croix bordée (26 px) — le carré est une action, il en\n * a le poids et l'aplat. */\n.zv-btn--carre {\n width: 48px;\n padding: 0;\n}\n\n.zv-btn--sm.zv-btn--carre {\n width: 40px;\n padding: 0;\n}\n\n.zv-btn[disabled],\n.zv-btn--ghost[disabled],\n.zv-btn--ink[disabled],\n.zv-btn--danger[disabled] {\n /* HORS RELEVÉ — aucune maquette ne montre de bouton désactivé. Traitement\n * minimal et neutre plutôt qu'une couleur inventée. */\n opacity: 0.5;\n cursor: not-allowed;\n box-shadow: none;\n}\n\n/* Ni le survol ni l'appui ne doivent rattraper un bouton inerte —\n * désactivé OU occupé (dont le clic est avalé par la façade) : sans ces\n * règles, les états ci-dessus repeignent le fond et le bouton a l'air\n * actif. Même précaution que les contrôles désactivés de forms.css. */\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[disabled]:is(:hover, :active),\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[aria-busy='true']:is(:hover, :active) {\n background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);\n}\n\n/* Occupé mais pas désactivé, le bouton plein garde son ombre à l'appui —\n * un enfoncement qui ne déclenche rien serait un mensonge de rendu. Le\n * compact, lui, n'a pas d'ombre à garder. */\n.zv-btn:not(.zv-btn--sm)[aria-busy='true']:active {\n box-shadow:\n 0 1px 0 rgba(255, 255, 255, 0.18) inset,\n var(--shadow-btn);\n}\n\n.zv-btn--ghost[disabled]:is(:hover, :active),\n.zv-btn--ghost[aria-busy='true']:is(:hover, :active) {\n border-color: var(--accent-line);\n background: var(--white);\n}\n\n.zv-btn--ink[disabled]:is(:hover, :active),\n.zv-btn--ink[aria-busy='true']:is(:hover, :active) {\n background: var(--ink);\n}\n\n.zv-btn--danger[disabled]:is(:hover, :active),\n.zv-btn--danger[aria-busy='true']:is(:hover, :active) {\n background: var(--err);\n}\n\n/* Attente : le motif de Mémoire (feedback.css le documente) — le LIBELLÉ\n * change (« Connexion… ») pendant que le bouton s'estompe, en\n * `cursor-wait; opacity: .7`, valeurs relevées reprises telles quelles.\n * Codé sur [aria-busy], l'attribut qui rend l'attente audible : le\n * balisage correct ne coûte rien. La façade React l'expose en prop\n * `occupe` — qui pose aria-disabled et AVALE le clic plutôt que de poser\n * `disabled`, pour que le bouton reste dans le parcours clavier pendant\n * que l'attente s'annonce (voir DECISIONS.md). Déclaré APRÈS [disabled]\n * pour l'emporter à spécificité égale quand les deux se combinent. */\n.zv-btn[aria-busy='true'],\n.zv-btn--ghost[aria-busy='true'],\n.zv-btn--ink[aria-busy='true'],\n.zv-btn--danger[aria-busy='true'] {\n cursor: wait;\n opacity: 0.7;\n}\n\n.zv-btn-row {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.zv-btn-row--stack {\n flex-direction: column;\n gap: 10px;\n width: 100%;\n}\n\n/* ─── Lien fléché ───────────────────────────────────────────────────────\n * Le SVG fait 15×15 : quatre occurrences dans Main.html contre une seule\n * à 16px dans DesignSystem.html. Tracés : M5 12h14 et M13 6l6 6-6 6, sur\n * un viewBox 0 0 24 24 — au trait carré depuis le 25/08/2026, comme tout\n * le jeu d'icônes (DECISIONS.md, « Le trait carré »). */\n\n/* ⚠️ REPRIS le 25/08/2026 — la DA le jugeait « pas ouf ». Le diagnostic :\n * c'était un lien bleu avec un chevron collé, sans rien qui le distingue\n * d'un lien de texte, et sans aucune récompense au survol. Le mouvement du\n * paquet est réservé à la vitrine et piloté au défilement ; il n'y avait\n * donc PAS de vocabulaire pour un lien d'action.\n *\n * Trois ajouts, tous dessinés dans les tokens existants :\n * · un FILET qui se déploie sous le libellé au survol, de gauche à droite\n * (scaleX sur une origine gauche) — pas un `text-decoration`, qui ne\n * s'anime pas et coupe les jambages ;\n * · la flèche avance de 4px, ce qui donne au lien sa direction ;\n * · le libellé passe en --accent-deep.\n * Le tout est repeint en une seule transition, donc coupé net par\n * prefers-reduced-motion (base.css) — l'état final reste lisible sans. */\n\n.zv-link-arrow {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n font-weight: 600;\n color: var(--accent);\n text-decoration: none;\n transition: color 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n/* Le filet est un pseudo-élément et non une bordure : une bordure\n * occuperait de la place et ferait sauter la ligne de base au survol. */\n.zv-link-arrow::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n bottom: -3px;\n height: 1px;\n background: currentColor;\n transform: scaleX(0);\n transform-origin: left center;\n transition: transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-link-arrow:hover,\n.zv-link-arrow:focus-visible {\n color: var(--accent-deep);\n}\n\n.zv-link-arrow:hover::after,\n.zv-link-arrow:focus-visible::after {\n transform: scaleX(1);\n}\n\n.zv-link-arrow svg {\n width: 15px;\n height: 15px;\n flex: none;\n /* ⚠️ CORRIGÉ le 25/08/2026 : le trait n'avait JAMAIS été déclaré — ni\n * fill:none ni stroke —, la flèche se rendait en triangle PLEIN noir (le\n * fût, d'aire nulle, disparaissait) au lieu du tracé que le commentaire\n * d'en-tête promettait depuis le premier jour. Posé au trait carré, avec\n * le reste du jeu. */\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: square;\n stroke-linejoin: miter;\n transition: transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-link-arrow:hover svg,\n.zv-link-arrow:focus-visible svg {\n transform: translateX(4px);\n}\n\n/* Sur la bande manifeste : le filet suit `currentColor`, il n'y a donc rien\n * à redéclarer — c'est tout l'intérêt de l'avoir posé en currentColor. */\n.zv-link-arrow--on-dark {\n color: var(--white);\n}\n\n.zv-link-arrow--on-dark:hover,\n.zv-link-arrow--on-dark:focus-visible {\n color: var(--accent-on-dark);\n}\n\n/* ─── Badges et puces ───────────────────────────────────────────────────*/\n\n/* Pastille pleine : numéro de pièce, catégorie de section. */\n.zv-pill {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n height: 28px;\n padding: 0 13px;\n background: var(--accent-deep);\n color: var(--white);\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.18em;\n font-size: 10px;\n font-weight: 700;\n border-radius: var(--r-pill);\n}\n\n/* Puce de source (BOSS, Judilibre, KALI…). Le rayon 4px n'a pas de token :\n * il est entre --r-pill (3) et --r-panel (10), sans équivalent. */\n.zv-src {\n display: inline-block;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--accent-deep);\n background: var(--white);\n border: 1px solid var(--accent-line);\n /* La puce de source est une pastille : elle suit le token, désormais à 0. */\n border-radius: var(--r-pill);\n padding: 6px 10px;\n}\n\n/* Variante atténuée : le « + 11 » qui clôt une liste de sources. */\n.zv-src--quiet {\n color: var(--label);\n border-color: var(--line);\n}\n\n.zv-src-row {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* Entité pseudonymisée Lexform, posée dans le fil du texte. */\n.zv-ent {\n display: inline-block;\n background: linear-gradient(135deg, var(--accent-deep), #3d36d6);\n color: var(--white);\n font-family: var(--font-mono);\n font-size: 10px;\n padding: 1px 6px;\n border-radius: var(--r-pill);\n vertical-align: middle;\n}\n\n/* Le paragraphe qui accueille des entités respire davantage : sans cette\n * interligne, les tags se télescopent d'une ligne à l'autre. */\n.zv-ent-text {\n font-size: 14px;\n line-height: 1.9;\n color: var(--ink);\n}\n\n/* Micro-label de statut. */\n.zv-tag {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n white-space: nowrap;\n}\n\n.zv-tag--ok {\n color: var(--ok);\n}\n\n.zv-tag--err {\n color: var(--err);\n}\n\n/* Sur fond sombre les états s'éclaircissent, sinon ils virent au noir. */\n.zv-tag--ok-on-dark {\n color: #8fd3a8;\n}\n\n.zv-tag--err-on-dark {\n color: #f0a5ad;\n}\n\n/* ─── Cartes ────────────────────────────────────────────────────────────\n * ⚠️ ARBITRAGE : le rayon vient de --r-card (16px), comme dans Main.html et\n * Mobile.html. DesignSystem.html et Logos.html codent 14px en dur, le\n * prototype 12px. Voir DECISIONS.md. */\n\n.zv-card {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n box-shadow: var(--shadow-card);\n}\n\n/* Paddings relevés : 22 / 24 / 28 / 32px selon la densité. */\n.zv-card--pad {\n padding: 24px;\n}\n\n.zv-card--pad-lg {\n padding: 32px;\n}\n\n/* Carte de statut. Les trois familles d'état ont désormais leur filet. */\n.zv-card--ok {\n border-color: var(--ok-line);\n}\n\n.zv-card--err {\n border-color: var(--err-line);\n}\n\n/* Carte flottante : celle qui se détache au-dessus d'une scène. */\n.zv-card--float {\n box-shadow: var(--shadow-panel);\n}\n\n/* ─── Carte verre ───────────────────────────────────────────────────────\n * Surface semi-opaque posée sur un halo ou une scène. Ne fonctionne QUE\n * devant quelque chose : sur du papier nu, elle disparaît. */\n\n.zv-glass {\n background: rgba(255, 255, 255, 0.86);\n border: 1px solid rgba(255, 255, 255, 1);\n border-radius: var(--r-card);\n box-shadow:\n 0 30px 70px rgba(30, 27, 140, 0.18),\n 0 2px 8px rgba(11, 16, 32, 0.05);\n}\n\n/* ─── Encarts ───────────────────────────────────────────────────────────\n * Deux encarts distincts au relevé, longtemps confondus. Ils ne se\n * substituent pas : le premier est une citation d'interface (fond blanc,\n * texte gris), le second un extrait de document (fond panel, texte encre,\n * très aéré). */\n\n.zv-inset {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 14px 16px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--muted);\n max-width: 380px;\n}\n\n.zv-inset__key,\n.zv-inset em {\n font-style: normal;\n color: var(--accent);\n font-weight: 600;\n}\n\n.zv-inset--quiet {\n background: var(--panel);\n border-color: var(--hairline);\n font-size: 12px;\n line-height: 1.9;\n color: var(--ink);\n}\n\n/* Poussé en bas de la carte qui l'accueille. */\n.zv-inset--bottom {\n margin-top: auto;\n}\n\n/* ─── Statistiques ──────────────────────────────────────────────────────*/\n\n/* Paire de mini-statistiques au pied d'une carte produit. */\n.zv-ministat {\n display: flex;\n gap: 10px;\n}\n\n.zv-ministat__item {\n flex: 1;\n background: var(--panel);\n border: 1px solid var(--hairline);\n border-radius: var(--r-panel);\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.zv-ministat__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 18px;\n font-weight: 500;\n}\n\n/* Carte statistique autonome (prototype). */\n.zv-stat-card {\n flex: 1;\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n box-shadow: 0 12px 32px rgba(30, 27, 140, 0.08);\n}\n\n.zv-stat-card__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 22px;\n font-weight: 500;\n}\n\n/* ─── Citation ──────────────────────────────────────────────────────────\n * Fond navy, filet clair, citation en Outfit 300.\n * ⚠️ Règle de la passation : jamais de fausse citation. Tant qu'un verbatim\n * n'a pas été recueilli, le bloc reste en réserve, pas en placeholder crédible. */\n\n.zv-quote {\n background: linear-gradient(135deg, #06063a, var(--accent-deep));\n color: var(--white);\n border-radius: var(--r-card);\n padding: 28px 32px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-quote__text {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 17px;\n font-weight: 300;\n}\n\n/* ─── Carte de tarif ────────────────────────────────────────────────────\n * Trois par page au maximum. La formule recommandée se signale par le\n * bandeau indigo et sa pastille — pas par une couleur de fond. */\n\n.zv-price {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n box-shadow: var(--shadow-card);\n padding: 26px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-price--featured {\n border-top: 3px solid var(--accent-deep);\n}\n\n.zv-price__amount {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 28px;\n font-weight: 300;\n}\n\n/* Le « HT · 1re année » accolé au montant, et la mention sous la carte. */\n.zv-price__unit,\n.zv-price__note {\n font-size: 12px;\n color: var(--muted);\n font-weight: 400;\n letter-spacing: normal;\n}\n\n/* ─── FAQ ───────────────────────────────────────────────────────────────\n * Cinq questions par page au maximum. */\n\n/* Le filet, comme `.zv-fold` : sans lui, la FAQ est un aplat blanc posé sur\n * le papier, sans début ni fin. Toutes les autres surfaces blanches de la\n * vitrine — carte, encart, tarif — en portent un ; celle-ci faisait tache. */\n.zv-faq {\n background: var(--white);\n border: 1px solid var(--line);\n}\n\n/* Chaque question est un `<details>` : le repli est natif, sans JavaScript.\n * Le filet sépare les questions ENTRE elles ; la première n'en porte pas, il\n * tomberait sur celui du bloc et ferait un trait double. */\n.zv-faq__item {\n border-top: 1px solid var(--hairline);\n}\n\n.zv-faq__item:first-child {\n border-top: 0;\n}\n\n.zv-faq__q {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n padding: 18px 24px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n /* Le marqueur natif du `<summary>` tombe : le chevron du composant fait le\n * même travail sans imposer le triangle du navigateur. Même traitement que\n * `.zv-fold__titre`. */\n list-style: none;\n}\n\n.zv-faq__q::-webkit-details-marker {\n display: none;\n}\n\n/* Une FAQ de vitrine veut ses questions dans le PLAN DU DOCUMENT : c'est ce\n * que lisent les moteurs, et ce que parcourt un lecteur d'écran qui saute de\n * titre en titre. On accepte donc un `<h2>`/`<h3>`/`<h4>` comme question,\n * et on lui retire ce que le navigateur lui met — taille, graisse, marges —\n * pour qu'il se rende comme le reste des questions. Le niveau reste au\n * balisage, l'apparence au système. */\n.zv-faq__q > :is(h2, h3, h4) {\n margin: 0;\n font: inherit;\n color: inherit;\n}\n\n.zv-faq__q:hover {\n background: var(--panel);\n}\n\n/* Le chevron porte sa classe : cibler tout `svg` du titre attraperait aussi\n * une icône posée dans le libellé, qui se verrait rétrécie et retournée. */\n.zv-faq__chevron {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--accent);\n transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-faq__item[open] .zv-faq__chevron {\n rotate: 180deg;\n}\n\n.zv-faq__a {\n margin: 0;\n padding: 0 24px 18px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* ─── Aperçu d'interface ────────────────────────────────────────────────\n * La preuve produit : une question, une réponse, ses sources.\n * ⚠️ Règle de la passation : toujours de vraies données réalistes, jamais\n * de lorem — c'est ce bloc qui porte la crédibilité de la page. */\n\n.zv-ui-preview {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 18px 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-ui-preview__q {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n color: var(--ink);\n}\n\n.zv-ui-preview__q svg {\n width: 12px;\n height: 12px;\n flex: none;\n color: var(--label);\n}\n\n.zv-ui-preview__a {\n font-size: 11px;\n color: var(--muted);\n}\n\n/* Le compteur de sources, seul élément mis en avant de la réponse. */\n.zv-ui-preview__count {\n color: var(--accent-deep);\n font-weight: 600;\n}\n\n/* Les puces de source rétrécissent dans un aperçu. */\n.zv-ui-preview .zv-src {\n font-size: 8px;\n}\n\n/* ─── Pastille de validation ────────────────────────────────────────────*/\n\n.zv-check {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--ok-soft);\n color: var(--ok);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n\n/* ─── Filets ────────────────────────────────────────────────────────────*/\n\n.zv-divider {\n height: 1px;\n background: var(--hairline);\n border: 0;\n}\n\n.zv-divider--strong {\n background: var(--line);\n}\n\n/* Sur fond sombre. La valeur est relevée en border-top de la ligne de\n * statistiques du manifeste ; elle est reprise ici en fond de filet. */\n.zv-divider--on-dark {\n background: rgba(255, 255, 255, 0.18);\n}\n\n/* ─── Barre de progression ──────────────────────────────────────────────\n * Deuxième des trois emplois autorisés du spectre. */\n\n.zv-progress {\n height: 3px;\n background: var(--hairline);\n border-radius: 0;\n overflow: hidden;\n}\n\n.zv-progress__fill {\n height: 3px;\n background: var(--spectre);\n}\n\n\n/* ─── Blocs éditoriaux ──────────────────────────────────────────────────\n * HORS RELEVÉ — ajoutés le 24/08/2026 à la demande de la DA (règle de\n * variété : jamais deux blocs de même forme qui se suivent, 80 % de formes\n * différentes par page). Quatre façons de poser du contenu SANS carte,\n * pour que les grilles de cartes redeviennent un choix et non un réflexe.\n * Dessinés dans les tokens existants, rien de nouveau dans la palette. */\n\n/* Rangées à filets : un libellé fort, un contenu, un trait fin entre\n * chaque rangée. Pour les listes où l'alignement fait la lecture\n * (profils rencontrés, prestations, caractéristiques). */\n.zv-rows {\n display: flex;\n flex-direction: column;\n}\n\n.zv-rows__row {\n display: flex;\n gap: 24px;\n align-items: baseline;\n flex-wrap: wrap;\n padding: 18px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-rows__row:first-child {\n border-top: none;\n}\n\n.zv-rows__key {\n font-weight: 600;\n flex: 0 0 280px;\n}\n\n.zv-rows__val {\n flex: 1;\n min-width: 260px;\n /* C'est le corps de texte du paquet — le token, pas sa valeur recopiée. */\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* Sommaire numéroté : le numéro en display léger, le contenu à droite,\n * un filet entre les entrées. Pour les suites longues qui étoufferaient\n * en cartes (offres détaillées, programmes). */\n.zv-index {\n display: flex;\n flex-direction: column;\n}\n\n.zv-index__item {\n display: flex;\n gap: 28px;\n align-items: flex-start;\n padding: 28px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-index__item:first-child {\n border-top: none;\n padding-top: 0;\n}\n\n.zv-index__num {\n font-family: var(--font-display);\n font-weight: 300;\n font-size: 40px;\n line-height: 1;\n letter-spacing: -0.02em;\n color: var(--accent-deep);\n flex: 0 0 64px;\n}\n\n.zv-index__body {\n flex: 1;\n min-width: 0;\n}\n\n/* Variante en colonnes : les mêmes entrées numérotées, posées en grille\n * de trois, sans filets — l'espace fait la séparation. */\n.zv-index--cols {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 40px 32px;\n}\n\n.zv-index--cols .zv-index__item {\n flex-direction: column;\n gap: 10px;\n padding: 0;\n border-top: none;\n}\n\n.zv-index--cols .zv-index__num {\n flex: none;\n font-size: 32px;\n color: var(--accent-line);\n}\n\n/* Déroulé vertical : le rail et ses points, à la verticale. La déclinaison\n * verticale de zv-steps, pour les méthodes qui se lisent comme un récit.\n * Les points reprennent les couleurs des points de méthode (l'un des\n * trois emplois autorisés du spectre). */\n.zv-flow {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 36px;\n padding-left: 32px;\n}\n\n.zv-flow::before {\n content: '';\n position: absolute;\n left: 5px;\n top: 8px;\n bottom: 8px;\n width: 2px;\n background: var(--line);\n}\n\n.zv-flow__step {\n position: relative;\n --zv-flow-dot: var(--iris);\n}\n\n.zv-flow__step::before {\n content: '';\n position: absolute;\n left: -32px;\n top: 5px;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--zv-flow-dot);\n box-shadow:\n 0 0 0 4px var(--paper),\n 0 0 0 5px var(--accent-line);\n}\n\n.zv-flow__step--violet {\n --zv-flow-dot: var(--violet);\n}\n\n.zv-flow__step--cyan {\n --zv-flow-dot: var(--cyan);\n}\n\n/* Encart d'appel : une bande claire sur fond indigo dilué, un titre, une\n * action. Pour l'offre transverse qui doit se voir sans crier\n * (remise Campus, renvoi vers une autre page). */\n.zv-callout {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n flex-wrap: wrap;\n background: var(--accent-soft);\n border: 1px solid var(--accent-line);\n border-radius: var(--r-card);\n padding: 32px 36px;\n}\n\n.zv-callout__title {\n margin: 0;\n font-family: var(--font-display);\n font-weight: 300;\n letter-spacing: -0.01em;\n font-size: 26px;\n line-height: 1.2;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n\n@media (max-width: 767px) {\n\n /* Le `min-width: 260px` de la valeur ne tient pas sous le seuil : dans une\n * carte de 215px — un écran de 320px — elle débordait de 45px. La CLÉ\n * était déjà libérée (flex-basis 100%), il manquait la valeur. */\n .zv-rows__val {\n min-width: 0;\n }\n .zv-btn--sm {\n height: 36px;\n padding: 0 14px;\n font-size: 13px;\n }\n\n /* Le carré compact suit la hauteur compacte mobile. */\n .zv-btn--sm.zv-btn--carre {\n width: 36px;\n padding: 0;\n }\n\n .zv-pill {\n height: 26px;\n padding: 0 12px;\n letter-spacing: 0.16em;\n }\n\n .zv-glass {\n background: rgba(255, 255, 255, 0.88);\n box-shadow:\n 0 20px 48px rgba(30, 27, 140, 0.18),\n 0 2px 8px rgba(11, 16, 32, 0.05);\n }\n\n .zv-inset {\n max-width: none;\n }\n\n .zv-rows__key {\n flex-basis: 100%;\n }\n\n .zv-index__item {\n gap: 18px;\n padding: 22px 0;\n }\n\n .zv-index__num {\n flex-basis: 44px;\n font-size: 30px;\n }\n\n .zv-index--cols {\n grid-template-columns: minmax(0, 1fr);\n gap: 28px;\n }\n\n .zv-callout {\n padding: 24px 22px;\n }\n\n .zv-callout__title {\n font-size: 21px;\n }\n}\n\n/* ─── Le lockup : la marque et le mot ───────────────────────────────────\n *\n * Jusqu'ici le nom d'un produit voyageait cuit dans un PNG, marque et mot\n * ensemble. Un lockup de 555×200 dont la capitale mesure 70px se retrouvait\n * affiché à 15px de capitale dans une barre de navigation et 10px dans un\n * pied de page : réduit de près de cinq fois, chaque fût tombait sur une\n * fraction de pixel et recevait sa propre dose de lissage. Le mot devenait\n * gris et le V, seul à n'avoir aucun fût vertical sur quoi s'aligner,\n * semblait flotter.\n *\n * Le mot est donc un SVG (`logos/svg/<produit>-wordmark.svg`), à\n * `currentColor` : net à toute taille, et il prend enfin les nuances encre\n * / navy / blanc que le registre des logos décrit. La marque reste le PNG\n * couleur du paquet — c'est un dégradé, il ne se recolore pas.\n *\n * ⚠️ Ces tracés sont RELEVÉS sur le lockup bitmap d'origine, faute de son\n * fichier vectoriel. Ils sont fidèles à 0,1 % en largeur et nettement plus\n * lisibles que le bitmap partout où le site les pose, mais ils portent les\n * approximations du relevé : réservés à l'ÉCRAN, jusqu'à 120px de capitale.\n * Au-delà — grand format, impression — exiger le fichier d'origine.\n *\n * Les proportions viennent du lockup, mesurées au pixel : la marque vaut\n * 2,86 fois la capitale, l'écart entre les deux 0,64 fois, et les deux\n * pièces se centrent l'une sur l'autre (elles ne partagent pas de ligne de\n * base). Tout se pilote par --zv-cap, qui EST la hauteur de capitale : le\n * SVG du mot a un viewBox dont la hauteur vaut la capitale, si bien qu'une\n * hauteur CSS posée dessus se lit directement. */\n\n.zv-lockup {\n --zv-cap: 15px;\n display: inline-flex;\n align-items: center;\n gap: calc(var(--zv-cap) * 0.64);\n color: var(--ink);\n text-decoration: none;\n}\n\n.zv-lockup__mark {\n height: calc(var(--zv-cap) * 2.86);\n width: auto;\n display: block;\n}\n\n.zv-lockup__word {\n height: var(--zv-cap);\n width: auto;\n display: block;\n}\n\n/* Le seuil de lisibilité. Sous 11px de capitale, le mot ne se lit plus :\n * ce n'est plus une question de netteté mais de taille, et aucun fichier\n * n'y changera rien. On montre la marque, seule — c'est elle qui identifie.\n * Poser cette classe, ou masquer .zv-lockup__word sous un point de rupture\n * quand la barre se resserre. */\n.zv-lockup--marque-seule {\n gap: 0;\n}\n\n.zv-lockup--marque-seule .zv-lockup__word {\n display: none;\n}\n\n/* ─── Cadre média ───────────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des dix planches ne montre\n * d'image de contenu — elles sont composées, pas photographiées. Le manque\n * était pourtant total : pas un `aspect-ratio`, pas un `object-fit` dans le\n * paquet, et quatre réécritures du même cadre chez un seul consommateur.\n *\n * ⚠️ Le rapport vit sur le CADRE, jamais sur l'image. Posé sur un enfant de\n * grille aligné au centre, il ne se résout pas et la colonne s'effondre à\n * zéro — c'est un correctif de production, pas une précaution théorique.\n *\n * Le fond est posé et non laissé transparent : une image qui tarde, échoue\n * ou porte de la transparence laisserait voir la surface du dessous, soit un\n * trou blanc sur une carte, soit un trou sombre sur une bande. */\n\n.zv-media {\n position: relative;\n display: block;\n width: 100%;\n overflow: hidden;\n background: var(--hairline);\n /* Angle vif comme le reste ; aucune ombre, l'image est dans le flux. */\n border-radius: 0;\n}\n\n/* Les quatre rapports du système. Un cinquième se demande, il ne s'invente\n * pas : c'est ainsi qu'une bibliothèque finit avec dix-sept rapports dont\n * trois servent. */\n.zv-media--16-9 {\n aspect-ratio: 16 / 9;\n}\n\n.zv-media--4-3 {\n aspect-ratio: 4 / 3;\n}\n\n.zv-media--1-1 {\n aspect-ratio: 1 / 1;\n}\n\n.zv-media--3-2 {\n aspect-ratio: 3 / 2;\n}\n\n/* L'image REMPLIT le cadre : c'est le seul comportement qui le garantisse\n * plein quelle que soit la source, et une grille dont les couvertures ne\n * s'alignent pas se voit aussitôt. En absolu, pour que le cadre garde la\n * main sur sa hauteur. */\n.zv-media__img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n/* Le cas inverse — un logo, un schéma, une pièce qu'on ne peut pas rogner.\n * Le papier passe derrière : il restera du vide, autant qu'il soit choisi. */\n.zv-media--contient {\n background: var(--paper);\n}\n\n.zv-media--contient .zv-media__img {\n object-fit: contain;\n}\n\n/* Le coin d'un cadre : durée d'une vidéo, nombre de pages, mention de\n * réserve. Encre diluée et non un gris opaque — le fond d'image est\n * imprévisible, et l'encre de charte s'y pose sans jurer. */\n.zv-media__coin {\n position: absolute;\n right: 8px;\n bottom: 8px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 7px;\n background: color-mix(in srgb, var(--ink) 72%, transparent);\n color: var(--white);\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n letter-spacing: 0.08em;\n}\n\n.zv-media__coin--gauche {\n right: auto;\n left: 8px;\n}\n\n.zv-media__coin--haut {\n bottom: auto;\n top: 8px;\n}\n\n/* Voile de lecture : un titre posé SUR une image n'est lisible qu'au-dessus\n * d'un dégradé. Il part du bas, là où le texte se pose, et n'assombrit pas\n * le sujet. Il ne capte pas le pointeur — le cadre reste cliquable. */\n.zv-media__voile {\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: none;\n background: linear-gradient(\n 180deg,\n transparent 40%,\n color-mix(in srgb, var(--ink) 78%, transparent) 100%\n );\n}\n\n/* ─── Page légale ───────────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, voir DECISIONS.md). La forme d'un document\n * contractuel — CGVU, mentions légales, DPA. Le TEXTE, lui, n'est pas ici :\n * il vit dans @zevra/legal, versionné et archivé. Ce module ne porte que la\n * mise en page ; c'est ce qui permet au même texte de s'afficher sur la\n * vitrine et dans une application sans exister en deux exemplaires.\n *\n * Presque tout se compose de pièces existantes : le sommaire est .zv-rail,\n * les définitions .zv-rows, les tableaux .zv-table, les encarts .zv-callout.\n * Ne restent ici que la colonne de lecture et ce que les listes de texte\n * courant n'avaient jamais eu. */\n\n.zv-legal {\n display: flex;\n flex-direction: column;\n gap: 48px;\n /* La MESURE, et c'est la seule règle qui compte vraiment pour un contrat :\n * au-delà d'environ 80 signes par ligne, l'œil perd le début de la ligne\n * suivante — sur un texte qu'on lit déjà à reculons, c'est rédhibitoire. */\n max-width: 72ch;\n}\n\n.zv-legal__section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* Un lien vers un article précis ne doit pas déposer son titre sous la\n * barre fixe. Sans cette réserve, cliquer « Art. 7 » dans le sommaire\n * affiche le corps de l'article et cache son intitulé. */\n scroll-margin-top: 88px;\n}\n\n/* Les listes du texte courant. Le paquet n'en avait aucune : ses listes\n * étaient toutes des objets (étapes, sommaires numérotés, rangées à filets),\n * jamais l'énumération d'une clause. */\n.zv-legal__liste {\n margin: 0;\n padding-left: 22px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n.zv-legal__liste li::marker {\n color: var(--accent-line);\n}\n\n/* Le renvoi vers un autre document contractuel, dans le fil d'une clause :\n * souligné, parce qu'à l'intérieur d'un paragraphe la couleur seule ne\n * suffit pas à annoncer qu'on change de contrat. */\n.zv-legal a {\n color: var(--accent);\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Le tableau d'un contrat (durées de conservation, sous-traitants). Il n'\n * emprunte PAS .zv-table de data.css : celui-là est une surface de travail\n * dont les rangées se survolent et se cliquent, ce qu'un tableau de contrat\n * ne fait jamais. Filets, chiffres alignés, et rien de plus. */\n.zv-legal__table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--app);\n line-height: 1.55;\n color: var(--ink-2);\n}\n\n.zv-legal__table th {\n border-bottom: 1px solid var(--line);\n padding: 8px 12px 8px 0;\n text-align: left;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-legal__table td {\n border-bottom: 1px solid var(--hairline);\n padding: 10px 12px 10px 0;\n vertical-align: top;\n font-variant-numeric: tabular-nums;\n}\n\n/* L'en-tête : titre, date d'entrée en vigueur, identifiant de version.\n * ⚠️ La VERSION est affichée, et ce n'est pas décoratif : c'est ce que\n * l'utilisateur cite quand il conteste, et ce que le support lui demande. */\n.zv-legal__tete {\n display: flex;\n flex-direction: column;\n gap: 10px;\n border-bottom: 1px solid var(--line);\n padding-bottom: 24px;\n}\n\n@media (max-width: 767px) {\n .zv-legal {\n gap: 36px;\n }\n\n .zv-legal__section {\n scroll-margin-top: calc(var(--topbar-h) + 12px);\n }\n}\n\n/* Encart d'accompagnement d'une clause : la reformulation en langage courant\n * (« En clair ») ou le point à ne pas manquer. Il COMMENTE la clause et ne\n * la remplace pas — d'où une surface d'accompagnement, et non l'aplat d'une\n * alerte, qui annoncerait un incident.\n * Il vit ici et non dans feedback.css : une page légale de vitrine n'a pas à\n * charger la couche applicative pour afficher une glose. */\n.zv-legal__encart {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-left: 2px solid var(--accent-line);\n background: var(--panel);\n padding: 14px 18px;\n}\n\n.zv-legal__encart--important {\n border-left-color: var(--warn);\n background: var(--warn-soft);\n}\n\n.zv-legal__encart-label {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-legal__encart--important .zv-legal__encart-label {\n color: var(--warn-ink);\n}\n\n\n/* ── layout.css ── */\n/* Zevra UI — mise en page : conteneurs, grilles, sections, chrome\n * Dépend de tokens.css et base.css. Ne pose ni reset ni fond de page :\n * c'est le rôle de base.css.\n *\n * ⚠️ Les maquettes sont des rendus FIGÉS (1440px pour les planches, 390px\n * pour Mobile.html). Les reproduire en `width: 1440px` donnerait un\n * débordement horizontal garanti sur téléphone — ce que les règles du projet\n * interdisent explicitement. Les conteneurs sont donc fluides et plafonnés,\n * ce qui rend le rendu identique au-delà de 1440px et utilisable en dessous.\n */\n\n/* ─── Page ──────────────────────────────────────────────────────────────*/\n\n.zv-page {\n width: 100%;\n max-width: 1440px;\n margin: 0 auto;\n position: relative;\n overflow: hidden;\n background: var(--paper);\n}\n\n/* ─── Gouttières ────────────────────────────────────────────────────────\n * Trois retraits relevés, et un seul mécanisme pour les poser : ces\n * utilitaires. Les blocs ci-dessous ne portent QUE leur rythme vertical,\n * jamais leur retrait horizontal — sans quoi poser un utilitaire par-dessus\n * un bloc écraserait son padding vertical (shorthand contre longhand). */\n\n.zv-gutter {\n padding-left: 72px;\n padding-right: 72px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Blocs centrés : hero, CTA, manifeste du prototype. */\n.zv-gutter--wide {\n padding-left: 120px;\n padding-right: 120px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Chrome haut du prototype. */\n.zv-gutter--chrome {\n padding-left: 56px;\n padding-right: 56px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Variante en marge, pour les blocs détachés du bord (grille produits,\n * vitrine, pied de page). Le max() tient les deux promesses à la fois :\n * jamais moins de 72px de marge visible, et un centrage sur --page-max dès\n * que l'écran en offre davantage. */\n.zv-inset-x {\n max-width: calc(var(--page-max) - 144px);\n margin-left: max(72px, calc((100% - (var(--page-max) - 144px)) / 2));\n margin-right: max(72px, calc((100% - (var(--page-max) - 144px)) / 2));\n}\n\n/* ─── Grille 12 colonnes ────────────────────────────────────────────────\n * Répartitions relevées : 3+9, 8+4, 6+6. */\n\n.zv-grid-12 {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n}\n\n.zv-col-3 { grid-column: span 3; }\n.zv-col-4 { grid-column: span 4; }\n.zv-col-5 { grid-column: span 5; }\n.zv-col-6 { grid-column: span 6; }\n.zv-col-7 { grid-column: span 7; }\n.zv-col-8 { grid-column: span 8; }\n.zv-col-9 { grid-column: span 9; }\n\n/* Grille de cartes produit. */\n.zv-grid-3 {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 24px;\n}\n\n.zv-grid-4 {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 16px;\n}\n\n/* ─── Sections ──────────────────────────────────────────────────────────*/\n\n/* En-tête de section : titre à gauche sur 8 colonnes, texte\n * d'accompagnement à droite sur 4, alignés par le bas. */\n.zv-section-head {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n align-items: end;\n padding-top: 104px;\n}\n\n.zv-section-head__main {\n grid-column: span 8;\n display: flex;\n flex-direction: column;\n gap: 18px;\n}\n\n.zv-section-head__aside {\n grid-column: span 4;\n}\n\n/* Section de planche : filet haut, titre sur 3 colonnes, corps sur 9.\n * C'est la structure des planches de documentation (DesignSystem, Logos),\n * pas celle des pages produit. */\n.zv-sheet-section {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n padding: 56px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-sheet-section__title {\n grid-column: span 3;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-sheet-section__body {\n grid-column: span 9;\n}\n\n/* Séparateur de section, avec son rythme vertical. */\n.zv-section-rule {\n border-top: 1px solid var(--line);\n padding-top: 40px;\n padding-bottom: 40px;\n}\n\n.zv-section-rule--quiet {\n border-top-color: var(--hairline);\n padding-top: 18px;\n padding-bottom: 28px;\n}\n\n/* ─── Blocs centrés : hero et CTA ───────────────────────────────────────*/\n\n.zv-hero {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding-top: 96px;\n gap: 28px;\n z-index: 2;\n}\n\n.zv-cta {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding-top: 128px;\n padding-bottom: 96px;\n gap: 24px;\n}\n\n.zv-cta .zv-lead {\n max-width: 580px;\n font-size: 17px;\n}\n\n/* ─── Bandeau d'annonce ─────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La bande AU-DESSUS de la\n * barre de navigation : annonce de version, information réglementaire. Le\n * ton information de l'alerte (--accent-soft sur --accent-line), en pleine\n * largeur. UN SEUL par page, et jamais pour du marketing permanent : un\n * bandeau qui ne part pas devient un bourrelet de chrome. */\n\n.zv-banner {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--accent-line);\n background: var(--accent-soft);\n padding: 9px 20px;\n font-size: 13px;\n font-weight: 500;\n text-align: center;\n color: var(--accent);\n}\n\n/* Le lien du bandeau se souligne : sur un fond déjà indigo, la couleur\n * seule ne suffit plus à le distinguer du texte. */\n.zv-banner a {\n color: var(--accent-deep);\n font-weight: 600;\n text-decoration: underline;\n}\n\n/* ─── Chrome : navigation ───────────────────────────────────────────────*/\n\n.zv-nav {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 26px;\n z-index: 2;\n}\n\n.zv-nav__logo {\n height: 42px;\n width: auto;\n display: block;\n}\n\n.zv-nav__links {\n display: flex;\n align-items: center;\n gap: 34px;\n}\n\n.zv-navlink {\n font-size: 14px;\n font-weight: 500;\n color: var(--accent);\n text-decoration: none;\n}\n\n/* ─── Zone d'actions de la barre ────────────────────────────────────────\n * Le lanceur et le compte, à droite. Elle ne fait PAS partie de\n * `.zv-nav__links` et c'est tout l'intérêt : cette rangée-là disparaît sous\n * le point de rupture (et sous 1150px pour `.zv-nav--riche`), remplacée par\n * le burger. Le compte, lui, doit rester — sinon on ne peut plus se\n * connecter depuis un téléphone, et c'est la première chose qu'on y fait.\n *\n * Elle se pose APRÈS `.zv-nav__links` dans le balisage, et avant le burger. */\n.zv-nav__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex: none;\n /* Décolle du dernier lien de la rangée quand les deux coexistent. */\n margin-left: 10px;\n}\n\n@media (max-width: 767px) {\n .zv-nav__actions {\n margin-left: 0;\n }\n}\n\n/* Bouton burger de la barre vitrine.\n *\n * ⚠️ HORS RELEVÉ des planches vitrine, qui sont toutes des rendus figés à\n * 1440px et n'ont donc jamais eu de menu mobile. Mobile.html en montre un,\n * mais sans en donner le tracé.\n *\n * Il est ici parce qu'une page sans lui n'a AUCUN moyen d'atteindre sa\n * navigation sur téléphone : sous le seuil, .zv-nav__links doit être masqué\n * (.zv-only-desktop) et rien ne le remplace. Le tiroir qu'il ouvre vit dans\n * overlays.css.\n *\n * Il n'apparaît que sous le seuil : au-dessus, les liens sont là. */\n.zv-nav__burger {\n display: none;\n align-items: center;\n justify-content: center;\n /* 40px de cible tactile, obtenus par la taille et non par du padding : un\n * bouton de 24px entouré de marge reste un bouton de 24px pour le doigt. */\n width: 40px;\n height: 40px;\n /* Les marges négatives ramènent le bouton à l'alignement optique de la\n * barre : sans elles, sa zone tactile décale le logo et le bord droit. */\n margin: -8px -8px -8px 0;\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--ink);\n cursor: pointer;\n}\n\n.zv-nav__burger svg {\n width: 20px;\n height: 20px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* Bouts francs — le trait carré du jeu d'icônes (25/08/2026). Le burger\n * applicatif, relevé « extrémités franches », y était déjà. */\n stroke-linecap: square;\n}\n\n/* Le tiroir de la vitrine : liens empilés, respiration généreuse. Le panneau\n * lui-même vient d'overlays.css ; ceci n'habille que son contenu. */\n.zv-nav-drawer {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 20px;\n}\n\n.zv-nav-drawer__link {\n display: flex;\n align-items: center;\n /* Chaque entrée fait au moins une cible tactible pleine : une liste de\n * liens serrés est la première cause de faux appuis sur téléphone. */\n min-height: var(--tap);\n padding: 0 12px;\n border-radius: var(--r-panel);\n font-size: 15px;\n font-weight: 500;\n color: var(--ink);\n text-decoration: none;\n}\n\n.zv-nav-drawer__link:hover {\n background: var(--panel);\n color: var(--accent);\n}\n\n.zv-nav-drawer__link--on {\n color: var(--accent);\n}\n\n.zv-navlink--quiet {\n color: var(--label);\n}\n\n/* ─── Sélecteur de langue ───────────────────────────────────────────────\n * Des initiales au registre mono, pas un menu : trois langues se lisent\n * d'un coup d'œil, et un menu pour trois entrées coûte un clic pour rien.\n * Une page qui n'existe que dans une langue ne le pose pas — il vaut mieux\n * pas de sélecteur qu'un sélecteur qui mène à des 404. */\n\n.zv-langues {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n}\n\n.zv-langue {\n color: var(--label);\n text-decoration: none;\n letter-spacing: 0.12em;\n transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-langue:hover {\n color: var(--accent);\n}\n\n.zv-langue--on {\n color: var(--ink);\n font-weight: 700;\n}\n\n/* Dans le tiroir, il se détache en pied de liste. */\n.zv-langues--tiroir {\n display: flex;\n gap: 16px;\n margin-top: 18px;\n padding-top: 14px;\n border-top: 1px solid var(--hairline);\n}\n\n/* ─── Le tiroir, groupé ─────────────────────────────────────────────────\n * Quand la barre porte des familles, les aplatir en une liste unique rend\n * le tiroir illisible. Un label capitale par famille, ses entrées en\n * dessous : la même architecture, repliée. */\n\n.zv-nav-drawer__groupe {\n display: block;\n margin: 16px 0 4px;\n}\n\n.zv-nav-drawer__groupe:first-child {\n margin-top: 0;\n}\n\n.zv-nav-drawer__link--sous {\n font-size: 15px;\n padding-block: 7px;\n}\n\n/* Chrome fixe : voile dégradé plutôt qu'un fond plein, pour que le contenu\n * s'y fonde au défilement.\n *\n * Deux usages, une seule classe. Il est LA BARRE quand il porte directement\n * ses pièces ; il l'ENVELOPPE quand il contient une .zv-nav — c'est la forme\n * qu'impose une nav complète, dont les menus déroulants et le tiroir ont\n * besoin d'un conteneur à eux. Le `:has` bascule tout seul, plutôt que de\n * demander un modificateur qu'on oublierait.\n *\n * ⚠️ backdrop-filter fait de ce bloc le containing block de TOUT descendant\n * en position fixe. Le tiroir (fixe lui aussi) doit donc vivre EN DEHORS,\n * en frère — dedans, il serait rogné par la hauteur de la barre.\n *\n * Le voile venait en rgba littéral, c'est-à-dire hors token : --paper\n * repeint, la barre serait restée sur l'ancienne couleur. */\n.zv-header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 22px;\n padding-bottom: 12px;\n z-index: 40;\n /* Le voile est PLEIN derrière la barre, et il se dégrade EN DESSOUS d'elle\n * (::after). Il était auparavant dégradé sur toute sa hauteur : à sa base\n * il ne restait donc presque rien, et un titre de 38 ou 56px qui passait\n * dessous se retrouvait COUPÉ EN DEUX — moitié voilée, moitié nette, avec\n * les entrées de nav posées sur ses lettres. Deux textes superposés, aucun\n * lisible. Signalé en recette sur mcp-factory.\n * Le dégradé n'est pas perdu : il vit sous la barre, là où il adoucit le\n * passage sans se disputer la place avec la navigation. */\n background: color-mix(in srgb, var(--paper) 92%, transparent);\n /* ⚠️ LE PRÉFIXÉ D'ABORD. Lightning CSS — le compilateur de Tailwind v4,\n * donc celui de toutes nos applications Next — SUPPRIME `backdrop-filter`\n * non préfixé quand il précède la forme `-webkit-`. Or Firefox ne connaît\n * QUE la forme non préfixée : la barre y perdait son flou. Reproduit avec\n * la chaîne exacte des apps, dans les deux ordres. L'ordre est donc une\n * contrainte, pas un style.\n *\n * Le voile, lui, reste en `color-mix` : le compilateur le traite\n * CORRECTEMENT — il émet un repli opaque puis la vraie valeur sous\n * `@supports`, ce qu'on lit dans les bundles livrés. Une note antérieure\n * affirmait qu'il le « réduisait » ; elle prenait le repli pour le\n * résultat. Ne pas « corriger » vers `rgb(from …)` : cette syntaxe sort du\n * compilateur NUE, sans repli, et un navigateur qui l'ignore n'a alors\n * plus AUCUN fond sous une barre fixe — 3,5 % d'usage qui voyaient le\n * voile correctement et ne verraient plus rien. */\n -webkit-backdrop-filter: blur(6px);\n backdrop-filter: blur(6px);\n}\n\n/* La frange de dégradé, posée SOUS la barre. `pointer-events: none` : elle\n * couvre le haut du contenu, elle ne doit rien intercepter. */\n.zv-header::after {\n content: '';\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n height: 24px;\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--paper) 92%, transparent),\n color-mix(in srgb, var(--paper) 0%, transparent)\n );\n -webkit-backdrop-filter: blur(6px);\n backdrop-filter: blur(6px);\n pointer-events: none;\n}\n\n.zv-header:has(> .zv-nav) {\n display: block;\n padding-top: 0;\n padding-bottom: 0;\n}\n\n.zv-header > .zv-nav {\n padding-bottom: 14px;\n}\n\n/* ─── Frise d'étapes ────────────────────────────────────────────────────\n * Troisième et dernier emploi autorisé du spectre : les points de méthode,\n * dans l'ordre iris → violet → cyan. */\n\n.zv-steps {\n position: relative;\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 32px;\n}\n\n.zv-steps__rail {\n position: absolute;\n top: 5px;\n left: 0;\n width: 100%;\n height: 2px;\n background: var(--line);\n}\n\n.zv-steps__progress {\n position: absolute;\n top: 5px;\n left: 0;\n height: 2px;\n background: linear-gradient(90deg, var(--iris), var(--violet) 70%, var(--cyan));\n}\n\n.zv-step {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 36px 32px 0 0;\n}\n\n.zv-step:last-child {\n padding-right: 0;\n}\n\n.zv-step__dot {\n position: absolute;\n top: 0;\n left: 0;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n /* Le double anneau détache la puce du rail qui passe derrière. */\n background: var(--iris);\n box-shadow:\n 0 0 0 4px var(--paper),\n 0 0 0 5px var(--accent-line);\n}\n\n/* Troisième et dernier emploi du spectre : les puces, dans l'ordre\n * iris → violet → cyan. La couleur se pose sur la puce elle-même, et non\n * par :nth-child sur l'étape : le rail et sa progression sont des enfants\n * du même conteneur, ils décalent la numérotation et laissent les puces\n * suivantes sans fond. */\n.zv-step__dot--violet {\n background: var(--violet);\n}\n\n.zv-step__dot--cyan {\n background: var(--cyan);\n}\n\n/* ─── Ligne de statistiques ─────────────────────────────────────────────\n * Posée sur le bandeau manifeste, d'où le filet clair. */\n\n.zv-stats {\n display: flex;\n gap: 64px;\n padding-top: 32px;\n border-top: 1px solid rgba(255, 255, 255, 0.18);\n}\n\n.zv-stat {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.zv-stat__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 40px;\n font-weight: 300;\n}\n\n/* ─── Pied de page ──────────────────────────────────────────────────────*/\n\n.zv-footer {\n position: relative;\n padding-top: 40px;\n padding-bottom: 40px;\n border-top: 1px solid var(--line);\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 48px;\n}\n\n.zv-footer__brand {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 320px;\n}\n\n.zv-footer__cols {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 48px;\n font-size: 13px;\n color: var(--muted);\n line-height: 2;\n}\n\n/* ─── Bande de preuve sociale ───────────────────────────────────────────\n * Label à gauche, noms de clients à droite, en Outfit 500 atténué.\n * ⚠️ Règle de la passation : jamais plus d'une ligne. Au-delà, la preuve\n * se dilue au lieu de s'accumuler. */\n\n.zv-proof-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 24px 40px;\n}\n\n.zv-proof-bar__names {\n display: flex;\n gap: 36px;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 17px;\n font-weight: 500;\n color: var(--ink);\n opacity: 0.72;\n}\n\n/* ─── Carrousel ─────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Défilement natif\n * scroll-snap, AUCUN JavaScript : pas de flèches (elles exigeraient un\n * script et un arbitrage de dessin), pas de points. Le clavier passe par le\n * focus du conteneur, que la façade rend focalisable.\n *\n * La barre de défilement tombe — le rail visible transformerait la vitrine\n * en fenêtre d'application. C'est un MASQUAGE, pas l'habillage que\n * navigation.css refuse : aucune dimension inventée.\n *\n * ⚠️ Un carrousel reste un aveu d'espace : la règle de variété lui préfère\n * une grille tant que le contenu tient. La largeur des éléments vient du\n * consommateur — un carrousel qui la fixerait déciderait de la mise en page\n * à la place de la grille. */\n\n.zv-carousel {\n display: flex;\n gap: 24px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n}\n\n.zv-carousel::-webkit-scrollbar {\n display: none;\n}\n\n.zv-carousel__item {\n flex: none;\n min-width: 0;\n scroll-snap-align: start;\n}\n\n/* ─── Pied de page réduit ───────────────────────────────────────────────\n * Pages secondaires. Le pied complet à quatre colonnes reste réservé à la\n * page d'accueil. */\n\n.zv-footer-mini {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 20px 32px;\n background: var(--paper);\n}\n\n.zv-footer-mini__logo {\n height: 22px;\n width: auto;\n display: block;\n}\n\n/* ─── Empilement ────────────────────────────────────────────────────────\n * Inventaire complet des couches relevées. Toute nouvelle couche s'arbitre\n * par rapport à celle-ci plutôt qu'en surenchérissant. */\n\n.zv-layer-scene { z-index: 1; } /* scène constellation, sous la nav */\n.zv-layer-content { z-index: 2; } /* nav, hero */\n.zv-layer-progress { z-index: 5; } /* barre de progression en flux */\n.zv-layer-chrome { z-index: 40; } /* header fixe, rail d'étapes */\n.zv-layer-top { z-index: 50; } /* barre de progression fixe */\n\n/* ─── Bascule d'affichage ───────────────────────────────────────────────\n * HORS RELEVÉ : utilitaires mécaniques, pas des décisions de charte. Ils\n * existent parce que les maquettes desktop et mobile ne montrent PAS les\n * mêmes éléments de navigation — les liens de nav desktop deviennent un\n * menu burger — et que sans eux chaque app réinvente la même règle. */\n\n/* Chaque utilitaire MASQUE dans la plage où l'élément n'a pas sa place, et ne\n * dit RIEN dans l'autre.\n *\n * La version précédente posait `display: none` par défaut puis\n * `display: revert` au-dessus du seuil. C'était faux : `revert` revient au\n * défaut du NAVIGATEUR — `inline` pour un span — et écrasait donc le\n * `display: flex` que l'élément tenait de sa propre classe. Une rangée de\n * liens de navigation perdait sa gouttière et ses libellés se collaient.\n *\n * En ne déclarant rien dans la plage visible, l'élément garde son display. */\n@media (max-width: 767px) {\n .zv-only-desktop {\n display: none;\n }\n}\n\n@media (min-width: 768px) {\n .zv-only-mobile {\n display: none;\n }\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Les grilles s'empilent, les gouttières se resserrent aux valeurs de\n * Mobile.html (20 / 22 / 24px). */\n\n@media (max-width: 767px) {\n .zv-gutter,\n .zv-gutter--wide,\n .zv-gutter--chrome {\n padding-left: 20px;\n padding-right: 20px;\n }\n\n .zv-inset-x {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .zv-banner {\n padding: 8px 16px;\n font-size: 12px;\n }\n\n .zv-carousel {\n gap: 16px;\n }\n\n .zv-grid-12,\n .zv-grid-3,\n .zv-section-head,\n .zv-sheet-section,\n .zv-steps {\n grid-template-columns: minmax(0, 1fr);\n gap: 20px;\n }\n\n .zv-grid-4 {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .zv-col-3,\n .zv-col-4,\n .zv-col-5,\n .zv-col-6,\n .zv-col-7,\n .zv-col-8,\n .zv-col-9,\n .zv-section-head__main,\n .zv-section-head__aside,\n .zv-sheet-section__title,\n .zv-sheet-section__body {\n grid-column: auto;\n }\n\n .zv-section-head {\n padding-top: 44px;\n align-items: start;\n text-align: center;\n justify-items: center;\n }\n\n .zv-hero {\n padding-top: 44px;\n gap: 18px;\n }\n\n .zv-cta {\n padding-top: 56px;\n padding-bottom: 48px;\n gap: 16px;\n }\n\n .zv-nav {\n padding-top: 20px;\n }\n\n /* Le burger n'existe que là où les liens disparaissent. */\n .zv-nav__burger {\n display: inline-flex;\n }\n\n .zv-nav__logo {\n height: 30px;\n }\n\n /* Les liens de navigation desktop n'ont pas d'équivalent mobile au\n * relevé : Mobile.html les remplace par un menu burger. Ils ne sont pas\n * masqués d'office — cela casserait les pages sans burger — mais\n * .zv-only-desktop est là pour le faire en une classe. */\n\n .zv-steps__rail,\n .zv-steps__progress {\n display: none;\n }\n\n .zv-step {\n padding: 0 0 0 24px;\n }\n\n .zv-step__dot {\n top: 4px;\n }\n\n .zv-stats {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 16px;\n padding-top: 16px;\n }\n\n .zv-stat {\n gap: 4px;\n }\n\n .zv-stat__value {\n font-size: 28px;\n }\n\n .zv-proof-bar,\n .zv-footer-mini {\n flex-direction: column;\n align-items: center;\n text-align: center;\n gap: 12px;\n padding-left: 20px;\n padding-right: 20px;\n }\n\n /* Les noms de clients passent en lignes plutôt qu'en une seule rangée,\n * qui déborderait. La règle « jamais plus d'une ligne » vaut pour le\n * desktop : à 390px, aucune rangée de six noms ne tient. */\n .zv-proof-bar__names {\n flex-wrap: wrap;\n justify-content: center;\n gap: 8px 20px;\n font-size: 15px;\n }\n\n .zv-footer {\n flex-direction: column;\n gap: 32px;\n }\n\n .zv-footer__cols {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 24px;\n }\n}\n\n/* ─── Rail de lecture ───────────────────────────────────────────────────\n *\n * La table des matières d'une page longue, posée sur le côté : un libellé,\n * un point, et le point courant qui s'allume. Elle NAVIGUE (chaque entrée\n * est une ancre) et elle SUIT (le module rail.js marque la section en\n * cours) — les deux, sinon ce n'est qu'un décor qui occupe la marge.\n *\n * ⚠️ Elle demande de la place à côté du texte. Sous le point de rupture\n * elle disparaît : un rail fixe sur un téléphone se pose sur la lecture.\n * Au-dessus, c'est la mesure de la page qui décide — une page dont le\n * contenu court d'un bord à l'autre jusqu'à 1100px gagnera à relever le\n * seuil chez elle.\n *\n * L'empilement vient de .zv-layer-chrome, à poser avec : le rail est du\n * chrome, il passe au-dessus des décors de scène. */\n\n.zv-rail {\n position: fixed;\n right: 40px;\n top: 50%;\n translate: 0 -50%;\n display: none;\n flex-direction: column;\n align-items: flex-end;\n gap: 16px;\n}\n\n@media (min-width: 768px) {\n .zv-rail {\n display: flex;\n }\n}\n\n/* Sur la gauche, l'ordre s'inverse : le point précède le libellé, sinon il\n * se retrouve au milieu de la colonne au lieu d'en tenir le bord. */\n.zv-rail--gauche {\n right: auto;\n left: 40px;\n align-items: flex-start;\n}\n\n.zv-rail--gauche .zv-rail__item {\n flex-direction: row-reverse;\n}\n\n.zv-rail__item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 3px 0;\n color: var(--muted);\n text-decoration: none;\n /* Un rail flotte au-dessus de ce que la page met dans sa marge : un\n * libellé gris pâle sur un décor pâle devient illisible par manque de\n * contraste, sans qu'aucun empilement soit en cause. Le halo couleur\n * papier le détache de tout ce qui passe dessous, sans lui donner de\n * cadre. */\n text-shadow:\n 0 0 6px var(--paper),\n 0 0 12px var(--paper);\n transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n/* Le libellé est posé au registre mono par l'appelant : il doit suivre la\n * couleur de son entrée, pas garder la sienne. */\n.zv-rail__item .zv-mono {\n color: inherit;\n}\n\n.zv-rail__item:hover {\n color: var(--accent-deep);\n}\n\n.zv-rail__dot {\n width: 8px;\n height: 8px;\n flex: none;\n border-radius: 50%;\n background: var(--accent-line);\n transition:\n background 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1)),\n scale 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-rail__item:hover .zv-rail__dot {\n background: var(--accent);\n scale: 1.35;\n}\n\n/* L'entrée courante, posée par rail.js. Le halo du point est le seul\n * signal dont une navigation discrète a besoin — pas de fond, pas de\n * filet. */\n.zv-rail__item--on {\n color: var(--accent-deep);\n}\n\n.zv-rail__item--on .zv-rail__dot {\n background: var(--iris);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--iris) 18%, transparent);\n}\n\n/* ─── Le seuil d'une nav riche ──────────────────────────────────────────\n *\n * La bascule vers le tiroir est à 767px, réglée sur la nav de référence :\n * quatre liens simples. Une nav qui porte l'architecture d'un site — cinq\n * familles à menu, un sélecteur de langue, un appel à l'action — déborde\n * bien avant. Mesuré : entre 768 et 1150px, le logo passe SOUS la première\n * entrée.\n *\n * `.zv-nav--riche` déplace le seuil à 1150px, POUR LA BARRE SEULEMENT. Ce\n * n'est pas un second point de rupture de mise en page : le reste de la\n * page garde l'unique seuil de la charte. C'est le seuil de débordement\n * d'un composant, et il se déclare sur ce composant. */\n\n@media (max-width: 1150px) {\n .zv-nav--riche .zv-nav__links {\n display: none;\n }\n\n .zv-nav--riche .zv-nav__burger {\n display: inline-flex;\n }\n\n .zv-nav--riche .zv-nav__logo {\n height: 30px;\n }\n}\n\n\n/* ── surfaces.css ── */\n/* Zevra UI — surfaces : halos, sols quadrillés, voiles, bandeau manifeste —\n * et, depuis le 25/08/2026 (hors relevé, voir DECISIONS.md), les formats\n * sociaux. Dépend de tokens.css. Indépendant des autres modules.\n *\n * C'est ce qui distingue une page Zevra d'une page blanche bien composée.\n * Les surfaces décoratives (halo, sol, voile, filigrane, lueur) se posent\n * en absolu derrière le contenu, ne captent jamais le pointeur, et n'ont\n * pas à être lues par un lecteur d'écran (les poser en aria-hidden) ; elles\n * supposent un parent `position: relative; overflow: hidden`.\n * ⚠️ Les formats sociaux, eux, sont du CONTENU — une planche d'export qui\n * porte un texte : jamais d'aria-hidden sur eux.\n */\n\n/* ─── Halo ambiant ──────────────────────────────────────────────────────\n * Le violet et le cyan du spectre, très dilués, derrière un bloc centré.\n * Ce n'est pas un emploi du spectre en aplat : c'est une lumière. */\n\n.zv-halo {\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n border-radius: 50%;\n pointer-events: none;\n}\n\n/* Derrière un hero. */\n.zv-halo--hero {\n top: -140px;\n width: 1400px;\n height: 900px;\n background: radial-gradient(\n closest-side,\n rgba(138, 58, 248, 0.2),\n rgba(20, 194, 248, 0.1) 48%,\n rgba(245, 246, 250, 0) 72%\n );\n}\n\n/* Derrière un CTA final : plus petit, plus discret. */\n.zv-halo--cta {\n top: 40px;\n width: 900px;\n height: 500px;\n background: radial-gradient(\n closest-side,\n rgba(138, 58, 248, 0.18),\n rgba(20, 194, 248, 0.08) 55%,\n rgba(245, 246, 250, 0) 75%\n );\n}\n\n/* Noyau lumineux d'une scène : blanc au centre, pour détacher un objet. */\n.zv-halo--core {\n top: 42%;\n width: 980px;\n height: 560px;\n transform: translate(-50%, -50%);\n background: radial-gradient(\n closest-side,\n rgba(255, 255, 255, 1) 0%,\n rgba(228, 222, 255, 0.85) 35%,\n rgba(138, 58, 248, 0.14) 62%,\n rgba(20, 194, 248, 0.06) 80%,\n rgba(245, 246, 250, 0) 100%\n );\n}\n\n/* ─── Sol quadrillé ─────────────────────────────────────────────────────\n * Une grille indigo très pâle, basculée en perspective : le « sol » sur\n * lequel les glyphes produit sont posés. */\n\n.zv-grid-floor {\n position: absolute;\n left: 50%;\n bottom: 0;\n width: 1800px;\n height: 560px;\n transform: translateX(-50%) perspective(900px) rotateX(58deg);\n transform-origin: 50% 100%;\n pointer-events: none;\n background-image:\n linear-gradient(rgba(88, 72, 248, 0.13) 1px, transparent 1px),\n linear-gradient(90deg, rgba(88, 72, 248, 0.13) 1px, transparent 1px);\n background-size: 64px 64px;\n}\n\n/* Variante à l'échelle d'une carte. */\n.zv-grid-floor--card {\n width: 900px;\n height: 260px;\n transform: translateX(-50%) perspective(600px) rotateX(62deg);\n}\n\n/* ─── Voiles de fondu ───────────────────────────────────────────────────\n * Posés PAR-DESSUS un sol ou une scène pour en noyer les bords dans le\n * papier. Sans eux, la grille se coupe net et le truc se voit. */\n\n.zv-fade {\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: linear-gradient(\n 180deg,\n var(--paper) 0%,\n rgba(245, 246, 250, 0) 40%,\n rgba(245, 246, 250, 0) 66%,\n var(--paper) 100%\n );\n}\n\n/* Variante sur fond blanc (intérieur de carte). */\n.zv-fade--white {\n background: linear-gradient(\n 180deg,\n #fff 0%,\n rgba(255, 255, 255, 0) 35%,\n rgba(255, 255, 255, 0) 70%,\n #fff 100%\n );\n}\n\n/* ─── Bandeau manifeste ─────────────────────────────────────────────────\n * La seule surface sombre du système. Le dégradé est celui du logo, en\n * diagonale. On n'y inverse PAS les neutres : les labels passent en indigo\n * clair (--accent-on-dark), les filets en blanc à 18 %. */\n\n.zv-band {\n position: relative;\n overflow: hidden;\n background: var(--manifeste);\n color: var(--white);\n padding-top: 112px;\n padding-bottom: 112px;\n}\n\n.zv-band__content {\n position: relative;\n z-index: 1;\n}\n\n/* L'emphase de base.css pose --ink sur les strong de zv-body/zv-lead — sur\n * le navy du manifeste, cette encre disparaît dans le fond. Dans la bande,\n * l'emphase hérite de la couleur ambiante (blanc), et seule la graisse la\n * distingue. */\n.zv-band .zv-body strong,\n.zv-band .zv-lead strong {\n color: inherit;\n}\n\n/* Filigrane : le glyphe, blanchi et presque effacé, débordant du cadre. */\n.zv-band__watermark {\n position: absolute;\n right: -40px;\n top: -80px;\n width: 640px;\n height: auto;\n opacity: 0.07;\n pointer-events: none;\n /* Ramène n'importe quel logo couleur en blanc pur. */\n filter: brightness(0) invert(1);\n}\n\n/* Lueur bleue au pied du bandeau, qui l'empêche de finir en aplat mort. */\n.zv-band__glow {\n position: absolute;\n left: 50%;\n bottom: -300px;\n width: 900px;\n height: 600px;\n transform: translateX(-50%);\n border-radius: 50%;\n pointer-events: none;\n background: radial-gradient(closest-side, rgba(32, 160, 240, 0.5), rgba(32, 160, 240, 0) 70%);\n}\n\n/* Texte courant sur fond sombre : ni blanc pur, ni le gris du mode clair. */\n.zv-band .zv-body,\n.zv-band .zv-rule,\n.zv-text-on-dark {\n color: #c7cde0;\n}\n\n/* Panneau navy compact (planches de logos). */\n.zv-panel-navy {\n background: linear-gradient(135deg, #06063a 0%, #0b0b5e 40%, var(--accent-deep) 100%);\n border-radius: var(--r-card);\n padding: 28px;\n color: var(--white);\n}\n\n/* ─── Glyphes ───────────────────────────────────────────────────────────\n * Les glyphes produit se posent toujours avec leur ombre portée — c'est\n * elle qui les décolle du papier. Jamais de recoloration ni d'inclinaison. */\n\n.zv-glyph {\n filter: var(--shadow-glyph);\n}\n\n/* Version appuyée : glyphe présenté seul, en grand. */\n.zv-glyph--deep {\n filter:\n drop-shadow(0 24px 32px rgba(88, 72, 248, 0.28))\n drop-shadow(0 2px 4px rgba(11, 16, 32, 0.08));\n}\n\n/* Glyphe en filigrane d'une carte, poussé dans l'angle. */\n.zv-glyph--watermark {\n position: absolute;\n top: 26px;\n right: 24px;\n width: 100px;\n height: auto;\n pointer-events: none;\n}\n\n/* ─── Formats sociaux ───────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Trois planches d'EXPORT aux\n * dimensions des réseaux — paysage 1200×630 (OpenGraph, lien LinkedIn),\n * carré 1080×1080 (post), story 1080×1920. FIGÉES en pixels, hors du point\n * de rupture : ce sont des planches qu'on CAPTURE, pas des pages qu'on\n * publie en HTML — la seule dérogation du paquet à l'interdit des largeurs\n * fixes, et elle est là pour ça.\n *\n * Elles composent les pièces existantes — bande par --manifeste, halo, sol,\n * kicker, display — et portent un pied wordmark + adresse. La règle\n * « jamais de faux contenu » s'y applique au premier chef : un visuel\n * social EST une publication. */\n\n.zv-social {\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 40px;\n flex: none;\n width: 1200px;\n height: 630px;\n /* La gouttière standard de la page (layout.css), reprise telle quelle. */\n padding: 72px;\n background: var(--paper);\n color: var(--ink);\n}\n\n.zv-social--carre {\n width: 1080px;\n height: 1080px;\n}\n\n.zv-social--story {\n width: 1080px;\n height: 1920px;\n padding: 96px 72px;\n}\n\n/* La planche sombre : le manifeste en fond, les règles du fond sombre\n * s'appliquent (--accent-on-dark pour les labels, jamais les neutres\n * inversés). */\n.zv-social--manifeste {\n background: var(--manifeste);\n color: var(--white);\n}\n\n/* Contenu et pied passent AU-DESSUS des surfaces décoratives (halo, sol),\n * comme dans une page. */\n.zv-social__contenu {\n position: relative;\n z-index: 1;\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n\n.zv-social__pied {\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n}\n\n/* ─── Mouvement ────────────────────────────────────────────────────────\n * Les animations vivent dans motion.css, qui porte les neuf mouvements\n * autorisés par la passation. Les surfaces d'ici en sont les cibles\n * habituelles : .zv-anim-breathe sur un halo, .zv-anim-drift sur un glyphe. */\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n@media (max-width: 767px) {\n .zv-halo--hero {\n top: -160px;\n width: 600px;\n height: 520px;\n background: radial-gradient(\n closest-side,\n rgba(91, 75, 240, 0.3),\n rgba(32, 128, 240, 0.16) 45%,\n rgba(245, 246, 250, 0) 72%\n );\n }\n\n .zv-band {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n\n .zv-band__watermark {\n right: -40px;\n top: -40px;\n width: 280px;\n }\n}\n\n\n/* ── motion.css ── */\n/* Zevra UI — mouvement : les neuf animations autorisées\n * Dépend de tokens.css. Relevé dans `maquettes/Motion.html`, qui les nomme\n * et fixe leurs plages de défilement.\n *\n * NEUF, PAS UNE DE PLUS — plus un DOSAGE. La micro-entrée des blocs\n * (.zv-anim-rise--doux, arbitrée le 25/08/2026, DECISIONS.md) n'est pas un\n * dixième mouvement : c'est zv-rise à douze pixels, pour les pages\n * intérieures où l'entrée pleine de la home lasserait à la longue.\n * Les règles de la passation :\n * · un seul type de mouvement par scène, plus le flottement continu ;\n * · les entrées se font toujours vers le HAUT, jamais latéralement ;\n * · aucun mouvement sur la typographie une fois posée ;\n * · une seule courbe : cubic-bezier(.2, .7, .2, 1).\n *\n * ─── Comment c'est construit ────────────────────────────────────────────\n * L'état AU REPOS de chaque élément est son état FINAL : sans animation, la\n * page est complète et lisible.\n *\n * Les ENTRÉES se jouent UNE FOIS (arbitrage du 26/08/2026, DECISIONS.md) :\n * l'ancien pilotage par `animation-timeline: view()` rejouait chaque entrée\n * à l'envers dès qu'on remontait — le contenu disparaissait sous le lecteur,\n * et une grille haute n'était jamais « contenue », donc jamais posée. Le\n * paquet arme désormais les entrées par `html.zv-motion` (posé par\n * motion.js, importable en `@zevra/ui/motion.js`), et chaque élément se\n * révèle quand il entre au viewport, puis N'EN BOUGE PLUS. Sans JavaScript,\n * rien n'est armé : la page s'affiche complète — même dégradation douce\n * qu'avant.\n *\n * Un échafaudage de scènes (l'expérience épinglée d'une home) peut vouloir\n * garder le pilotage au défilement : poser `data-zv-motion-skip` sur la\n * scène — motion.js n'y touche pas — et re-brancher les @keyframes du\n * paquet sur ses timelines nommées. Seuls la barre de progression\n * (zv-anim-progress) et le point d'étape (zv-anim-dot-active) restent\n * scroll-pilotés ici : suivre la lecture est leur raison d'être.\n *\n * Les deux boucles (drift, breathe) animent les propriétés `translate` et\n * `scale` INDÉPENDANTES, jamais `transform` : les éléments concernés — halo\n * centré par translateX(-50%), glyphe en scène — portent déjà un transform\n * de placement, qu'une animation sur `transform` écraserait.\n *\n * Décalage : poser `--zv-delay` en style inline. Le stagger d'un groupe est\n * de 120 ms (entrées), les vagues de badges de 6 % de défilement.\n */\n\n:root {\n --zv-easing: cubic-bezier(0.2, 0.7, 0.2, 1);\n --zv-delay: 0s;\n}\n\n/* ─── Boucles continues ─────────────────────────────────────────────────\n * Les deux seules animations qui ne dépendent pas du défilement. */\n\n/* 1 · Flottement des glyphes en scène. Jamais sur du texte. */\n@keyframes zv-drift {\n 0%,\n 100% {\n translate: 0 0;\n }\n 50% {\n translate: 0 -10px;\n }\n}\n\n.zv-anim-drift {\n animation: zv-drift 7s ease-in-out infinite;\n animation-delay: var(--zv-delay);\n}\n\n/* 2 · Respiration du halo de hero. Le SEUL mouvement autorisé avant le\n * premier défilement. */\n@keyframes zv-breathe {\n 0%,\n 100% {\n scale: 1;\n opacity: 1;\n }\n 50% {\n scale: 1.06;\n opacity: 0.92;\n }\n}\n\n.zv-anim-breathe {\n animation: zv-breathe 9s ease-in-out infinite;\n}\n\n/* ─── Entrées : une révélation par visite ───────────────────────────────\n * Les @keyframes vivent au niveau racine : un échafaudage de scènes peut\n * les re-brancher sur ses propres timelines nommées (voir l'en-tête). */\n\n@keyframes zv-rise {\n from {\n opacity: 0;\n transform: translateY(16px);\n }\n}\n\n/* Le même mouvement, DOSÉ pour les blocs des pages intérieures : huit\n * pixels au lieu de seize — le bloc est posé avant que l'œil ait fini\n * d'arriver, et une visite répétée ne le remarque plus. */\n@keyframes zv-rise-doux {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n}\n\n@keyframes zv-glyph-in {\n from {\n opacity: 0;\n transform: translateY(24px) scale(0.85);\n }\n}\n\n/* La signature de la page d'accueil : les glyphes convergent vers le Z\n * depuis les quatre coins — la seule entrée latérale du système, latérale\n * parce qu'elle converge, pas parce qu'elle glisse. */\n@keyframes zv-converge-tl {\n from {\n opacity: 0;\n transform: translate(-420px, -420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-tr {\n from {\n opacity: 0;\n transform: translate(420px, -420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-bl {\n from {\n opacity: 0;\n transform: translate(-420px, 420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-br {\n from {\n opacity: 0;\n transform: translate(420px, 420px) scale(1.5);\n }\n}\n\n@keyframes zv-pop {\n from {\n opacity: 0;\n transform: scale(0.8);\n }\n}\n\n/* Le filet de spectre se dessine : on anime bien la largeur, et non une\n * mise à l'échelle — le dégradé doit se révéler, pas s'étirer. */\n@keyframes zv-filet {\n from {\n width: 0;\n }\n}\n\n/* ─── Le contrat au repos : des définitions inertes ─────────────────────\n * Chaque classe d'entrée possède ici sa définition PROPRE, volontairement\n * inerte tant que rien n'est armé : sans motion.js, aucune animation n'est\n * attachée et ces propriétés n'ont pas d'effet — la page s'affiche\n * complète. C'est aussi le point d'ancrage du dosage : le décalage d'un\n * groupe se règle en posant --zv-delay par élément (120 ms). */\n\n.zv-anim-rise {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-rise--doux {\n animation-duration: 0.45s;\n}\n\n.zv-anim-glyph-in {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-converge {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-converge--tr {\n animation-name: zv-converge-tr;\n}\n\n.zv-anim-converge--bl {\n animation-name: zv-converge-bl;\n}\n\n.zv-anim-converge--br {\n animation-name: zv-converge-br;\n}\n\n.zv-anim-pop {\n animation-delay: var(--zv-delay);\n}\n\n/* Les six vagues : 70 ms d'écart — un délai en secondes retrouve son sens\n * maintenant que l'entrée est temporelle. */\n.zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }\n.zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }\n.zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }\n.zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }\n.zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }\n\n.zv-anim-filet {\n animation-delay: var(--zv-delay);\n}\n\n/* ─── L'armement, puis la libération ────────────────────────────────────\n * `:where(html.zv-motion)` garde la spécificité d'une classe simple : les\n * définitions inertes ci-dessus (délais, noms, durées) restent lisibles à\n * travers l'armement, et `.zv-in` — plus bas dans le fichier — gagne à la\n * cascade sans surenchère. Armé, l'élément attend en PAUSE sur la première\n * image de son animation (fill both) ; libéré, elle se joue une fois et\n * s'arrête sur l'état final. Le défilement n'y reviendra jamais. */\n\n@media (prefers-reduced-motion: no-preference) {\n /* 3 · Entrée standard de tout contenu texte et panneau. */\n :where(html.zv-motion) .zv-anim-rise {\n animation: zv-rise 0.6s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n /* 3 bis · Le dosage des pages intérieures : se pose AVEC zv-anim-rise,\n * dont il ne change que l'amplitude et la durée. La home garde\n * l'entrée pleine : c'est elle la signature, le doux est le\n * quotidien. */\n :where(html.zv-motion) .zv-anim-rise--doux {\n animation-name: zv-rise-doux;\n animation-duration: 0.45s;\n }\n\n /* 4 · Entrée des glyphes produit : montée et grossissement. */\n :where(html.zv-motion) .zv-anim-glyph-in {\n animation: zv-glyph-in 0.6s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n /* 5 · La convergence. Direction par modificateur. */\n :where(html.zv-motion) .zv-anim-converge {\n animation: zv-converge-tl 0.7s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n :where(html.zv-motion) .zv-anim-converge--tr {\n animation-name: zv-converge-tr;\n }\n\n :where(html.zv-motion) .zv-anim-converge--bl {\n animation-name: zv-converge-bl;\n }\n\n :where(html.zv-motion) .zv-anim-converge--br {\n animation-name: zv-converge-br;\n }\n\n /* 6 · Badges, jetons d'entités, éléments de preuve — en cascade. */\n :where(html.zv-motion) .zv-anim-pop {\n animation: zv-pop 0.4s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n :where(html.zv-motion) .zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }\n :where(html.zv-motion) .zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }\n :where(html.zv-motion) .zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }\n :where(html.zv-motion) .zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }\n :where(html.zv-motion) .zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }\n\n /* 7 · Le filet de spectre, à l'entrée de chaque en-tête de section. */\n :where(html.zv-motion) .zv-anim-filet {\n animation: zv-filet 0.5s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n}\n\n/* La libération, commune : posée par motion.js à l'entrée au viewport, une\n * seule fois. Sa place APRÈS l'armement fait tout : à spécificité égale,\n * c'est elle qui a le dernier mot. */\n.zv-in {\n animation-play-state: running;\n}\n\n/* ─── Les deux lectures du défilement, conservées ───────────────────────\n * Suivre la position de lecture est leur raison d'être : elles doivent\n * avancer ET reculer avec le lecteur. */\n\n@supports (animation-timeline: view()) {\n /* 8 · Le point d'étape s'allume à l'entrée de sa scène et s'éteint à la\n * sortie. Un seul actif à la fois. */\n @keyframes zv-dot-active {\n 0%,\n 100% {\n background: var(--accent-line);\n box-shadow: none;\n }\n 35%,\n 70% {\n background: var(--iris);\n box-shadow: 0 0 0 4px rgba(88, 72, 248, 0.18);\n }\n }\n\n .zv-anim-dot-active {\n animation: zv-dot-active linear both;\n animation-timeline: view();\n animation-range: cover 0% cover 100%;\n }\n\n /* 9 · La barre de progression, pilotée par le défilement global de la\n * page — la seule à lire `scroll(root)` plutôt que sa propre entrée. */\n @keyframes zv-progress {\n from {\n transform: scaleX(0);\n }\n to {\n transform: scaleX(1);\n }\n }\n\n .zv-anim-progress {\n transform-origin: 0 50%;\n animation: zv-progress linear both;\n animation-timeline: scroll(root);\n }\n}\n\n/* ─── Accessibilité ─────────────────────────────────────────────────────\n * Les neuf classes se neutralisent d'un seul bloc. Une durée courte ne\n * suffit PAS pour une animation pilotée par le défilement — elle ignore la\n * durée : il faut couper la timeline et l'animation elles-mêmes. La page\n * reste complète et lisible, dans son état final. */\n\n@media (prefers-reduced-motion: reduce) {\n .zv-anim-drift,\n .zv-anim-breathe,\n .zv-anim-rise,\n .zv-anim-glyph-in,\n .zv-anim-converge,\n .zv-anim-pop,\n .zv-anim-filet,\n .zv-anim-dot-active,\n .zv-anim-progress {\n animation: none !important;\n animation-timeline: none !important;\n }\n}\n\n\n/* ── icons.css ── */\n/* Zevra UI — icônes\n * Dépend de tokens.css. Le jeu lui-même vit dans icons/ (index + sprite).\n *\n * Les icônes sont TRACÉES, jamais pleines : trait à l'épaisseur 2, sur une\n * grille de 24, à BOUTS FRANCS et JONCTIONS VIVES — le trait carré de la\n * charte, arbitré le 25/08/2026 (DECISIONS.md, « Le trait carré ») : les\n * bouts arrondis étaient le dialecte des bibliothèques génériques, pas le\n * nôtre. Le réglage vit ici, les 55 tracés le suivent d'un coup. Elles\n * héritent de la couleur du texte — c'est ce qui leur permet de fonctionner\n * sur papier comme sur la bande manifeste sans qu'on ait à les décliner.\n *\n * Deux façons de les poser :\n *\n * <svg class=\"zv-icon\"><use href=\"…/icons/sprite.svg#zv-recherche\"/></svg>\n *\n * ou, là où les ressources distantes sont interdites — un canvas Claude\n * Design —, en collant le corps de l'icône depuis icons/index.json.\n */\n\n.zv-icon {\n display: inline-block;\n width: 16px;\n height: 16px;\n /* `flex: none` évite l'écrasement quand l'icône est posée à côté d'un\n * libellé long dans un conteneur flex — le cas le plus fréquent, et la\n * déformation la plus visible. */\n flex: none;\n vertical-align: middle;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* `square` garde l'emprise optique du rond (le bout dépasse d'une\n * demi-épaisseur, comme lui) : la silhouette change sans qu'un seul\n * tracé soit redessiné. Les angles très aigus retombent d'eux-mêmes en\n * biseau (miter-limit par défaut) — rien à régler. */\n stroke-linecap: square;\n stroke-linejoin: miter;\n}\n\n/* Accolée à du texte dense : métadonnée, puce de source, cellule. */\n.zv-icon--sm {\n width: 13px;\n height: 13px;\n /* Le trait s'épaissit optiquement quand l'icône rétrécit : on le compense,\n * sinon une rangée d'icônes de tailles mêlées paraît déséquilibrée. */\n stroke-width: 1.75;\n}\n\n/* Entrée de navigation, bouton de chrome. */\n.zv-icon--lg {\n width: 20px;\n height: 20px;\n}\n\n/* Illustration d'un état vide, en tête de panneau. */\n.zv-icon--xl {\n width: 28px;\n height: 28px;\n stroke-width: 1.5;\n}\n\n/* Une icône seule dans un bouton n'a pas de libellé pour la nommer : elle\n * doit porter un titre accessible. Cette classe ne fait que réserver la\n * cible tactile ; le nom, lui, se pose en aria-label côté balisage. */\n.zv-icon-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tap);\n height: var(--tap);\n padding: 0;\n border: 0;\n border-radius: var(--r-panel);\n background: transparent;\n color: var(--muted);\n cursor: pointer;\n}\n\n.zv-icon-btn:hover {\n background: var(--panel);\n color: var(--ink);\n}\n\n/* Désactivé. C'était la SEULE classe interactive du module sans cet état :\n * .zv-btn, .zv-chrome-btn, .zv-menu__item, .zv-pager__arrow, .zv-range et\n * les champs en ont tous un. Sans lui, un bouton fermé se rendait comme un\n * bouton actif — curseur main compris — et s'allumait au survol comme s'il\n * allait agir.\n * ⚠️ Le survol est neutralisé EXPLICITEMENT : `:hover` s'applique encore à\n * un bouton désactivé, et l'application ne peut pas le rattraper depuis une\n * couche d'utilitaires — ce module n'est pas layered. */\n.zv-icon-btn[disabled],\n.zv-icon-btn[aria-disabled='true'] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.zv-icon-btn[disabled]:hover,\n.zv-icon-btn[aria-disabled='true']:hover {\n background: transparent;\n color: var(--muted);\n}\n\n/* Icône décorative posée à côté d'un libellé qui la nomme déjà : elle est\n * atténuée pour que le mot reste le point d'entrée du regard. */\n.zv-icon--quiet {\n color: var(--label);\n}\n\n/* ─── Tuile d'icône ─────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La pastille carrée qui\n * porte une icône devant un argument de la vitrine — liste de garanties,\n * grille d'atouts. Carrée par NATURE, pas par style : c'est une tuile.\n * Une par argument, jamais en décor isolé ; c'est le libellé voisin qui\n * nomme, la tuile est décorative par défaut. */\n.zv-icon-tile {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n flex: none;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n\n/* ── overlays.css ── */\n/* Zevra UI — enrobages : voile, tiroir, modale, notification\n * Dépend de tokens.css et base.css.\n *\n * Ce module est COMMUN aux deux couches. Une page vitrine a besoin d'un\n * tiroir pour son menu mobile ; une application en a besoin pour sa\n * navigation. Les laisser dans navigation.css obligeait une landing à\n * charger la barre latérale, la pagination et les tableaux pour obtenir un\n * bouton burger — c'est ce qui a motivé cette séparation.\n *\n * Tout ce qui vit ici se pose PAR-DESSUS la page et lui vole le regard. D'où\n * une règle commune : rien ne s'ouvre sans un moyen évident d'en sortir, et\n * l'échappement au clavier est du ressort du gabarit, que le CSS ne peut pas\n * assurer seul.\n *\n * Empilement, dans l'ordre : voile 60, tiroir 80, modale 90, notification 100.\n * Les couches inférieures (2, 5, 40, 50) sont dans layout.css.\n *\n * OUVERTURE — deux écritures équivalentes, au choix du gabarit :\n *\n * <div class=\"zv-drawer\" data-open> en HTML, en Astro\n * <div class=\"zv-drawer zv-drawer--open\"> en React, en Vue\n *\n * La seconde existe parce que la première est pénible à écrire en JSX : un\n * attribut booléen y demande `{...(ouvert ? {'data-open': ''} : {})}`, là où\n * une classe conditionnelle est le geste habituel. Ne PAS employer\n * `data-open={ouvert}` en React : il rend `data-open=\"false\"`, que le\n * sélecteur d'attribut considère comme présent — le panneau resterait\n * ouvert.\n */\n\n/* ─── Voile ─────────────────────────────────────────────────────────────\n * Toujours l'encre de charte diluée, jamais un noir pur : la page est\n * bleutée, un gris neutre y ferait un trou sale. Le voile n'existe que sous\n * le seuil (cf. bloc mobile) — au-dessus, tiroir et feuille sont des colonnes,\n * elles n'occultent rien. */\n\n.zv-scrim {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 60;\n background: color-mix(in oklab, var(--ink) 40%, transparent);\n}\n\n/* ─── Tiroir de navigation ──────────────────────────────────────────────*/\n\n/* Tiroir ou modale ouverts : la page derrière ne défile plus. `:has()` est\n * le seul chemin CSS pour verrouiller le body depuis un état descendant —\n * sans lui, la molette fait défiler le fond sous le voile et le panneau\n * semble dériver. */\nhtml:has(.zv-drawer[data-open]),\nhtml:has(.zv-drawer--open),\nhtml:has(.zv-modal[data-open]),\nhtml:has(.zv-modal--open) {\n overflow: hidden;\n}\n\n.zv-drawer {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 80;\n /* `visibility` plutôt que `display` : elle sort le contenu du parcours\n * clavier ET se transitionne, ce qui retient le panneau à l'écran le temps\n * qu'il glisse dehors. En `display: none`, il disparaîtrait à mi-course. */\n visibility: hidden;\n transition: visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open],\n.zv-drawer--open {\n visibility: visible;\n}\n\n/* Le voile du tiroir accompagne un panneau qui glisse : il se fond. Celui de\n * la feuille de source apparaît net, faute de mouvement à accompagner.\n *\n * Et il redescend au sol : son z-index de 60 est la couche qu'il occupe\n * quand il est SEUL sur la page. À l'intérieur d'un enrobage, qui ouvre son\n * propre contexte d'empilement, ce 60 l'emportait sur le panneau et voilait\n * le tiroir par-dessus — il paraissait délavé, derrière son propre écran. */\n.zv-drawer .zv-scrim {\n z-index: 0;\n opacity: 0;\n transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open] .zv-scrim,\n.zv-drawer--open .zv-scrim {\n opacity: 1;\n}\n\n.zv-drawer__panel {\n position: absolute;\n /* Le panneau passe DEVANT son voile. .zv-drawer ouvre un contexte\n * d'empilement, dans lequel le z-index 60 du voile l'emporte sur un\n * panneau resté en `auto` : le tiroir s'affichait alors délavé, comme s'il\n * était derrière son propre écran. */\n z-index: 1;\n inset-block: 0;\n left: 0;\n display: flex;\n width: 288px;\n /* Le plafond en pourcentage garde toujours une bande de page visible\n * derrière : un tiroir qui couvre tout l'écran ne se lit plus comme un\n * tiroir, et l'utilisateur perd l'endroit où appuyer pour sortir. */\n max-width: 86%;\n flex-direction: column;\n overflow-y: auto;\n border-right: 1px solid var(--line);\n background: var(--panel);\n border-radius: 0;\n /* Aucune ombre : c'est le voile qui fait le contraste. En ajouter une\n * doublerait la séparation et salirait le bord. */\n box-shadow: none;\n transform: translateX(-100%);\n transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open] .zv-drawer__panel,\n.zv-drawer--open .zv-drawer__panel {\n transform: translateX(0);\n}\n\n/* ⚠️ TENSION ASSUMÉE, à arbitrer. Les deux courbes ci-dessus sont celles du\n * relevé — voile en cubic-bezier(.4,0,.2,1), panneau en ease-out, même durée.\n * Leur asymétrie ne se voit pas, elle s'entend : le panneau arrive avant que\n * le voile ait fini de se poser. Elles ne sont donc PAS la courbe unique\n * --zv-easing que motion.css impose à la vitrine. Défendable (motion.css régit\n * neuf animations de page, pas le chrome applicatif), mais c'est une divergence\n * écrite ici plutôt qu'une variable empruntée en silence — d'autant que ce\n * module doit rester utilisable sans motion.css.\n * La coupure prefers-reduced-motion est déjà globale dans base.css : ne pas la\n * reposer ici, elle neutralise ces deux transitions comme les autres. */\n\n.zv-drawer__close {\n position: absolute;\n top: 7px;\n right: 8px;\n}\n\n/* Comportement attendu du composant, à conserver tel quel :\n * · role=\"dialog\" aria-modal=\"true\" aria-label=\"Navigation\", et aria-hidden\n * qui reflète l'état fermé ;\n * · clic sur le voile ou touche Échap → fermeture ;\n * · à l'ouverture, `body { overflow: hidden }` RESTAURÉ à sa valeur\n * précédente à la fermeture, jamais remis à '' aveuglément : c'est ce qui\n * évite de casser une page qui gérait déjà son propre défilement ;\n * · l'état ouvert mémorise la ROUTE d'ouverture, pas un booléen — changer de\n * page referme alors le tiroir sans effet ni rendu en cascade. Le\n * composant a donc besoin de la route courante, pas d'une prop `open`.\n * ⚠️ MANQUE en production, à combler côté composant : piège de focus dans le\n * panneau et restitution du focus au bouton d'ouverture à la fermeture. */\n\n\n/* ─── Modale ────────────────────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ. Aucune modale dans les sources de Mémoire — l'application\n * n'en emploie pas. Celle-ci est DÉRIVÉE de la carte et du tiroir, sans\n * introduire une seule valeur nouvelle : elle réemploie --r-card,\n * --shadow-panel et le voile ci-dessus.\n *\n * Elle existe parce que la règle du projet en impose la forme (panneau\n * plafonné à 92vh, qui défile en son sein) et qu'à défaut chaque app en\n * réinventerait une.\n *\n * Employer <dialog> plutôt que ce balisage quand c'est possible : le\n * navigateur y apporte le piège de focus et la fermeture par Échap, que le\n * CSS ne sait pas faire. */\n\n.zv-modal {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 90;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n\n.zv-modal[data-open],\n.zv-modal--open {\n display: flex;\n}\n\n/* Le voile d'une modale se montre à TOUTES les largeurs, et passe sous son\n * panneau.\n *\n * .zv-scrim est masqué par défaut et ne s'affiche que sous le seuil : c'est\n * juste pour le tiroir, qui n'existe qu'en mobile. La modale, elle, existe\n * partout — sans cette règle, elle s'ouvrait en desktop sans rien occulter.\n * Le z-index revient à 0 pour la même raison que sur le tiroir. */\n.zv-modal .zv-scrim {\n z-index: 0;\n display: block;\n}\n\n.zv-modal__panel {\n position: relative;\n z-index: 1;\n display: flex;\n width: 100%;\n max-width: 520px;\n /* Le plafond ET le défilement interne : sans les deux, une modale au\n * contenu long pousse ses actions hors de l'écran, et l'utilisateur ne\n * peut plus ni valider ni annuler. */\n max-height: 92vh;\n flex-direction: column;\n overflow: auto;\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n background: var(--white);\n box-shadow: var(--shadow-panel);\n}\n\n.zv-modal__head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n padding: 20px 22px 0;\n}\n\n.zv-modal__title {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--app-h2);\n font-weight: 400;\n color: var(--ink);\n}\n\n.zv-modal__body {\n padding: 14px 22px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* Les actions restent visibles quand le corps défile : c'est tout l'intérêt\n * du plafond ci-dessus. */\n.zv-modal__foot {\n position: sticky;\n bottom: 0;\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n border-top: 1px solid var(--hairline);\n background: var(--white);\n padding: 14px 22px;\n}\n\n/* Modale destructive : le filet haut porte l'avertissement, pas un fond\n * rouge — une surface d'alerte pleine rendrait le texte pénible à lire. */\n.zv-modal--danger .zv-modal__panel {\n border-top: 3px solid var(--err);\n}\n\n/* ─── Notification flottante ────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ, même raison : Mémoire signale tout en place, par une\n * alerte de bloc. Cette notification est l'alerte du module de retours,\n * détachée et posée en coin.\n *\n * Elle ne remplace PAS .zv-alert : une erreur de formulaire se dit à côté du\n * champ, jamais dans un coin qui disparaît. Réserver ce composant à ce qui\n * est vrai après coup — « copié », « enregistré », « envoyé ». */\n\n.zv-toasts {\n position: fixed;\n z-index: 100;\n right: 16px;\n bottom: 16px;\n display: flex;\n flex-direction: column-reverse;\n gap: 8px;\n /* Le conteneur ne capte rien : seules les notifications elles-mêmes le\n * font, sans quoi une colonne invisible bloquerait les clics dans tout le\n * coin de l'écran. */\n pointer-events: none;\n}\n\n.zv-toast {\n pointer-events: auto;\n display: flex;\n align-items: center;\n gap: 10px;\n max-width: 380px;\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 12px 14px;\n font-size: var(--app-sm);\n color: var(--ink);\n}\n\n.zv-toast--ok {\n border-color: var(--ok-line);\n background: var(--ok-soft);\n color: var(--ok);\n}\n\n.zv-toast--err {\n border-color: var(--err-line);\n background: var(--err-soft);\n color: var(--err);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n@media (max-width: 767px) {\n /* Voile et tiroir n'existent QUE sous le seuil : au-dessus, la navigation\n * est une colonne permanente, elle n'occulte rien. */\n .zv-scrim {\n display: block;\n }\n\n .zv-drawer {\n display: block;\n }\n\n /* La notification prend la largeur : un coin de 380px sur un écran de 390\n * n'est plus un coin. */\n .zv-toasts {\n right: 12px;\n left: 12px;\n bottom: 12px;\n }\n\n .zv-toast {\n max-width: none;\n }\n}\n\n\n/* ── forms.css ── */\n/* Zevra UI — formulaires : champ, recherche, zone de texte, sélection,\n * case/radio, interrupteur, puces de filtre, zone de dépôt — et, depuis le\n * 25/08/2026 (hors relevé, voir DECISIONS.md) : le champ à affixe et le\n * curseur.\n *\n * DÉPEND de tokens.css (toutes les valeurs) et de base.css (reset\n * box-sizing, anneau :focus-visible). RIEN D'AUTRE : ce module ne lit ni\n * layout.css, ni components.css, ni motion.css. C'est volontaire — une app\n * purement applicative n'a aucune raison de charger la mise en page de la\n * vitrine.\n *\n * Ce qu'il NE porte PAS :\n * · le menu déroulé par .zv-select — il appartient à navigation.css\n * (.zv-menu) ; ce module pose l'ancre (.zv-select-anchor) et le\n * déclencheur, rien de plus ;\n * · le bouton d'envoi d'un formulaire — c'est .zv-btn (components.css) ;\n * · l'alerte de bloc (role=\"alert\") — module de retours.\n *\n * PROVENANCE. Relevé dans l'application Mémoire (production, mêmes tokens de\n * charte) sur six écrans : /connexion, /chat, /repertoire, /bibliotheque,\n * /logs, /import. Deux écrans de plus ont été rouverts APRÈS le premier\n * relevé, parce qu'ils contiennent des motifs que les six premiers n'ont\n * pas : /admin (add-member-form.tsx, billing-card.tsx) pour les deux seuls\n * <select> natifs et le seul message d'erreur au niveau du champ. Restent\n * NON relevés : /tableau-de-bord et /documents/[slug]. Chaque section dit ce\n * qui est relevé et ce qui est dérivé ; rien n'est comblé en silence.\n *\n * VOCABULAIRE. Le relevé emploie huit paliers de texte dans 5,5 px et une\n * demi-douzaine de gris intermédiaires ; tokens.css les regroupe. Les\n * rabattements appliqués ici, une fois pour toutes :\n * #4A5468 et #727C93 → var(--ink-2) / var(--muted)\n * #9AA3B5 (placeholder) → var(--label)\n * #C9CEDD (filet ferme) → var(--accent-line)\n * #FDFDFF (survol papier) → var(--panel)\n * 13/14/12,5/11,5 px → var(--app) / var(--app-lg) / var(--app-sm)\n * 11/10,5/9,5/8,5 px mono → var(--mono-lg) / var(--mono) / var(--mono-sm)\n * Deux d'entre eux font disparaître une nuance du relevé : c'est dit sur\n * place, à l'endroit exact où la nuance tombe.\n *\n * POINT DE RUPTURE : 768px, comme les autres modules. Les sources basculent\n * à 1024px (`lg` de Tailwind) : les valeurs sont reprises telles quelles,\n * seul le seuil change.\n */\n\n/* ─── Groupe de champ ───────────────────────────────────────────────────\n * Intitulé au-dessus, contrôle, puis aide ou erreur. Relevé sur les\n * formulaires de connexion (login-card.tsx:16-18, 163-200 ;\n * reset-password-card.tsx:102-140) et confirmé sur /admin\n * (add-member-form.tsx). L'écart intitulé → contrôle vaut 6px et non 8 :\n * c'est le `mt-[6px]` du champ, pas une estimation. */\n\n.zv-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n /* Sans min-width:0, un champ placé dans une ligne flex refuse de rétrécir\n * sous la largeur de son placeholder et fait déborder la page. */\n min-width: 0;\n}\n\n/* Ligne d'intitulé quand un lien l'accompagne (« Mot de passe oublié ? ») :\n * les deux se lisent sur la même ligne de base plutôt que de s'empiler.\n * Gouttière NULLE ici : le relevé ferme l'écart au-dessus du seuil\n * (login-card.tsx:180, `gap-3 … lg:gap-0`) et laisse space-between faire\n * tout le travail. Les 12px sont une valeur MOBILE, ils sont en fin de\n * fichier. */\n.zv-field__head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 0;\n}\n\n/* Les capitales sont LITTÉRALES dans le JSX de Mémoire. Elles passent en CSS\n * ici : recopiées dans le contenu, elles seraient épelées lettre par lettre\n * par un lecteur d'écran. */\n.zv-field__label {\n display: block;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-field__help {\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n/* Le message d'erreur AU NIVEAU DU CHAMP existe — une seule fois dans toute\n * l'application, hors des six écrans du relevé initial\n * (add-member-form.tsx:94). Ses valeurs propres (taille, graisse, écart)\n * n'ont PAS été relevées : celles ci-dessous sont dérivées du reste du\n * groupe de champ et de la teinte de l'alerte de bloc. Aucune couleur\n * nouvelle n'est introduite. */\n.zv-field__err {\n font-size: var(--app-sm);\n font-weight: 500;\n color: var(--err);\n}\n\n/* ─── Champ de saisie ───────────────────────────────────────────────────\n * Référence : le champ des formulaires de connexion, seul <input> bordé de\n * l'application (login-card.tsx:18), au motif strictement identique dans\n * /admin. Les 10px/13px de l'inventaire venaient du CADRE de recherche, pas\n * d'un champ : ils restent sur .zv-search. */\n\n.zv-input {\n width: 100%;\n min-width: 0;\n border: 1px solid var(--line);\n /* Angle vif : aucun rounded-* sur un seul champ des huit écrans. Posé\n * explicitement, sinon Safari arrondit les champs de lui-même. */\n border-radius: 0;\n background: var(--white);\n padding: 9px 12px;\n /* Un <input> n'hérite pas de la police du document : sans cette ligne, le\n * champ repasse en police système et jure avec le reste de la page. */\n font-family: var(--font-text);\n font-size: var(--app);\n color: var(--ink);\n}\n\n.zv-input::placeholder {\n color: var(--label);\n}\n\n/* Relevé (`focus:border-accent-line`) : c'est :focus et non :focus-visible,\n * la bordure réagit donc aussi à la souris. L'anneau clavier de base.css\n * s'y AJOUTE — on ne reprend PAS le `input { outline: none }` de la prod,\n * qui laissait l'application inutilisable au clavier. */\n.zv-input:focus {\n border-color: var(--accent-line);\n}\n\n/* Identifiant d'organisation : même champ, chiffres et tirets alignés\n * (login-card.tsx:145). */\n.zv-input--mono {\n font-family: var(--font-mono);\n}\n\n/* <select> NATIF, habillé comme le champ. C'est le motif de sélection\n * accessible de l'application, relevé deux fois (add-member-form.tsx:82,\n * billing-card.tsx:135) avec exactement les valeurs de .zv-input —\n * `border-line bg-white px-3 py-[9px] text-[13px] text-ink`. D'où un simple\n * modificateur : tout est déjà là.\n *\n * L'`appearance` n'est PAS neutralisée : la flèche, l'ouverture au clavier,\n * la liste native et son annonce viennent du navigateur. La remplacer\n * imposerait de redessiner tout cela, et le relevé ne le fait pas non plus.\n * À ne pas confondre avec .zv-select, qui est un déclencheur de menu maison\n * — voir plus bas. */\n.zv-input--select {\n cursor: pointer;\n}\n\n/* Champ nu : ni fond ni filet propres, c'est le CADRE qui l'entoure qui fait\n * le champ (composeur du chat). Lui redonner une bordure dessinerait deux\n * cadres imbriqués. */\n.zv-input--bare {\n flex: 1 1 0%;\n border: none;\n background: transparent;\n padding: 2px 0;\n font-size: var(--app-lg);\n}\n\n/* ─── Recherche ─────────────────────────────────────────────────────────\n * Cadre en flex, loupe au filet, saisie sans bordure propre. Deux relevés\n * identiques (repertoire-client.tsx:59-78, bibliotheque-client.tsx:185-204).\n *\n * La loupe est un SVG dans le balisage, pas un fond CSS : viewBox 0 0 13 13,\n * fill=\"none\", stroke-width=\"1.5\",\n * <circle cx=\"5.5\" cy=\"5.5\" r=\"4\"/><line x1=\"8.6\" y1=\"8.6\" x2=\"12\" y2=\"12\"/> */\n\n.zv-search {\n display: flex;\n width: auto;\n min-width: 240px;\n flex: 1 1 0%;\n align-items: center;\n gap: 9px;\n border: 1px solid var(--line);\n background: var(--white);\n padding-inline: 13px;\n /* La prise de focus s'ANIME (25/08) : sans transition, le cadre sautait\n * d'un état à l'autre et le retour se lisait comme un défaut de rendu. */\n transition:\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-search:hover {\n border-color: var(--accent-line);\n}\n\n/* DÉRIVÉ : la bordure de focus relevée sur .zv-input, portée par le cadre.\n *\n * DEUX MARQUAGES, ASSUMÉS. L'anneau :focus-visible de base.css est un\n * sélecteur universel : au clavier il se dessine AUSSI autour du champ nu,\n * à l'intérieur du cadre, en plus de ce filet. On le garde tel quel plutôt\n * que de trancher l'un des deux :\n * · supprimer l'anneau (outline:none) rendrait le champ nu invisible au\n * clavier — c'est exactement la faute de la production ;\n * · supprimer ce filet priverait la souris de tout retour, alors que le\n * relevé lui en donne un sur les champs bordés.\n * La redondance coûte moins cher que l'une ou l'autre perte. */\n/* ⚠️ REPRIS le 25/08/2026 — la DA jugeait la prise de focus « boff ». Elle\n * l'était : le cadre passait à --accent-line, un filet si pâle qu'à côté de\n * l'anneau du champ intérieur il ne se voyait pas. Trois corrections :\n * · le filet passe à --accent PLEIN, pas à sa version diluée ;\n * · un halo --accent-soft de 3px double le trait — c'est lui qui rend le\n * retour visible du coin de l'œil, sans déplacer un pixel de la mise en\n * page (box-shadow ne prend pas de place) ;\n * · la loupe s'allume avec le cadre : elle cesse d'être une invite grise\n * pour devenir la marque du champ actif.\n *\n * L'anneau :focus-visible du champ INTÉRIEUR est neutralisé ici, et\n * seulement ici : le cadre le remplace, et il le remplace pour la souris\n * comme pour le clavier. C'est ce qui lève la double marque assumée jusque-là\n * — un seul objet à l'écran, un seul retour. Le champ nu reste parfaitement\n * repérable au clavier, puisque c'est le cadre entier qui s'allume. */\n.zv-search:focus-within {\n border-color: var(--accent);\n box-shadow: 0 0 0 3px var(--accent-soft);\n}\n\n.zv-search:focus-within .zv-search__icon {\n stroke: var(--accent);\n}\n\n.zv-search__input:focus-visible {\n outline: none;\n}\n\n.zv-search__icon {\n flex: none;\n width: 13px;\n height: 13px;\n /* Même gris que le placeholder : la loupe est une invite, pas une donnée.\n * Elle s'allume avec le cadre — voir :focus-within plus bas. */\n stroke: var(--label);\n transition: stroke 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-search__input {\n min-width: 0;\n flex: 1 1 0%;\n border: none;\n background: transparent;\n padding-block: 10px;\n font-family: var(--font-text);\n font-size: var(--app);\n color: var(--ink);\n}\n\n.zv-search__input::placeholder {\n color: var(--label);\n}\n\n/* ─── Champ à affixe ────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). « 30 », seul dans un champ,\n * ne dit pas s'il s'agit de jours ou d'euros. La grammaire est celle de\n * .zv-search — le seul cadre-à-champ-nu du relevé : cadre --line, champ nu\n * dedans, allumage :focus-within identique. L'affixe est en mono --label,\n * comme les libellés de déclencheur : une unité est un libellé, pas une\n * donnée. */\n\n.zv-affix {\n display: flex;\n align-items: center;\n gap: 9px;\n min-width: 0;\n border: 1px solid var(--line);\n background: var(--white);\n padding-inline: 12px;\n transition:\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-affix:hover {\n border-color: var(--accent-line);\n}\n\n/* Même prise de focus que le cadre de recherche : filet --accent plein,\n * halo --accent-soft — et l'anneau du champ intérieur est remplacé par le\n * cadre, pour la souris comme pour le clavier. */\n.zv-affix:focus-within {\n border-color: var(--accent);\n box-shadow: 0 0 0 3px var(--accent-soft);\n}\n\n.zv-affix .zv-input--bare {\n /* Le champ d'un formulaire, pas le composeur : il reprend le corps et le\n * rythme vertical de .zv-input. */\n font-size: var(--app);\n padding-block: 9px;\n}\n\n.zv-affix .zv-input--bare:focus-visible {\n outline: none;\n}\n\n.zv-affix__add {\n flex: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n white-space: nowrap;\n color: var(--label);\n}\n\n/* ─── Zone de texte ─────────────────────────────────────────────────────\n * DÉCISION, PAS UN RELEVÉ : il n'existe AUCUN <textarea> dans l'application,\n * sur aucun des huit écrans ouverts. Le composeur du chat est un <input> où\n * Entrée envoie (chat-client.tsx:365). Le bloc ci-dessous est dérivé mot à\n * mot de .zv-input ; seules min-height et resize s'y ajoutent — sans elles\n * le champ se rend sur une ligne et le composant n'existe pas. min-height\n * est la SEULE valeur de ce module sans aucune source, dans aucun écran.\n *\n * Ne PAS remplacer l'input du composeur par ce bloc : ce serait un\n * changement de comportement (Entrée n'enverrait plus). */\n\n.zv-textarea {\n width: 100%;\n min-width: 0;\n min-height: 96px; /* sans source — voir ci-dessus */\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 9px 12px;\n font-family: var(--font-text);\n font-size: var(--app);\n /* 1.6 : l'interligne du corps de texte du paquet (base.css, .zv-body). */\n line-height: 1.6;\n color: var(--ink);\n /* Vertical seul : en libre, l'utilisateur élargit le champ hors de sa\n * colonne et casse la mise en page. */\n resize: vertical;\n}\n\n.zv-textarea::placeholder {\n color: var(--label);\n}\n\n.zv-textarea:focus {\n border-color: var(--accent-line);\n}\n\n/* ─── Déclencheur de sélection (menu maison) ────────────────────────────\n * « PÉRIMÈTRE : Toute la base ▾ » (chat-client.tsx:168-175). Ce n'est pas un\n * <select> : c'est un bouton qui ouvre .zv-menu (navigation.css). Pour un\n * vrai choix dans une liste, préférer .zv-input--select, qui est natif.\n *\n * ANCRAGE — arbitrage à consigner dans DECISIONS.md, les deux modules ne\n * disent pas la même chose. Le relevé donne raison à celui-ci : c'est\n * l'ENVELOPPE qui porte position:relative (`relative inline-block\n * max-w-full`), pas le déclencheur. Ancrer le menu sur .zv-select\n * fonctionnerait par accident, mais casserait son `max-width` calculé sur\n * la fenêtre. D'où .zv-select-anchor, posée ici, à côté de son\n * déclencheur. */\n\n.zv-select-anchor {\n position: relative;\n display: inline-block;\n max-width: 100%;\n}\n\n/* Ancre pleine largeur : le cas d'un CHAMP de formulaire, où la liste doit\n * tomber exactement sous le contrôle et faire la même largeur que lui. La\n * variante en ligne ci-dessus reste celle des filtres, où le déclencheur se\n * dimensionne sur son libellé. */\n.zv-select-anchor--block {\n display: block;\n width: 100%;\n}\n\n.zv-select-anchor--block .zv-menu {\n width: 100%;\n min-width: 0;\n}\n\n/* ─── Déclencheur de sélection DESSINÉE ─────────────────────────────────\n * ⚠️ AJOUTÉ le 25/08/2026 : « le select, faut le designer jusque dans\n * l'affichage de la liste » (DA, sur pièces).\n *\n * Le <select> natif (.zv-input--select) reste dans le paquet et garde son\n * usage — mais il ne peut PAS être habillé : la liste déroulée est peinte\n * par le système, pas par la page. Sur un macOS en mode sombre, elle sort\n * en menu noir au milieu d'une interface claire. Aucun CSS ne corrige cela,\n * c'est une limite du contrôle.\n *\n * D'où ce déclencheur, qui a l'apparence exacte d'un champ et ouvre le\n * .zv-menu du paquet. Ce qu'il COÛTE, et qu'il faut assumer en le\n * choisissant : le clavier, l'annonce et la liste native sont à réimplémenter\n * — c'est fait dans le composant React (SelectMenu), pas dans ce fichier.\n * Un gabarit qui pose ces classes à la main sans ce travail livre un select\n * inutilisable au clavier. Dans ce cas, prendre le natif. */\n.zv-input--trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n text-align: left;\n line-height: 1.4;\n cursor: pointer;\n transition: border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-input--trigger:hover {\n border-color: var(--accent-line);\n}\n\n/* Ouvert, le déclencheur reste marqué : sans cela, la liste flotte au-dessus\n * d'un champ qui a l'air au repos, et on ne sait plus lequel elle sert. */\n.zv-input--trigger[aria-expanded='true'] {\n border-color: var(--accent);\n}\n\n/* Le placeholder d'un déclencheur est du texte, pas un ::placeholder : il\n * lui faut donc sa propre teinte, celle du champ vide. */\n.zv-input--trigger[data-vide='oui'] {\n color: var(--label);\n}\n\n.zv-select {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n border: 1px solid var(--line);\n background: var(--white);\n padding: 6px 11px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n /* La valeur courante peut être longue : elle sort du cadre plutôt que de\n * casser la pastille en deux lignes. */\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n}\n\n.zv-select:hover {\n border-color: var(--accent-line);\n}\n\n/* La valeur choisie, seule partie mise en avant du déclencheur. */\n.zv-select strong {\n font-weight: 700;\n color: var(--accent);\n}\n\n/* ▾ (U+25BE) posé dans le balisage. */\n.zv-select__chevron {\n font-size: var(--mono-sm);\n color: var(--label);\n}\n\n/* ─── Case à cocher et bouton radio ─────────────────────────────────────\n * DÉRIVÉS INTÉGRALEMENT, ET ILS DOIVENT ÊTRE LUS COMME TELS : il n'existe\n * zéro <input type=\"checkbox\"> et zéro <input type=\"radio\"> dans toute\n * l'application — la sélection s'y fait toujours par .zv-seg ou .zv-switch.\n * Le bloc ci-dessous réemploie la grammaire déjà en production (carré au\n * filet --line sur --white, coché en aplat --accent, rond des pastilles)\n * sans introduire de valeur de charte nouvelle ; ses DIMENSIONS, elles\n * (16px, taille du glyphe, épaisseur de l'anneau intérieur), n'ont aucune\n * source. Le jour où un vrai écran en pose, c'est lui qui tranche.\n *\n * ⚠️ .zv-check est DÉJÀ PRIS par la pastille de validation\n * (components.css:482) — d'où .zv-check-box.\n *\n * Contrôles natifs volontairement conservés (appearance:none seulement) : le\n * clavier, :focus-visible, [disabled] et l'annonce au lecteur d'écran\n * viennent alors du navigateur, pas d'un <div> à réoutiller. */\n\n.zv-check-box,\n.zv-radio {\n appearance: none;\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n cursor: pointer;\n}\n\n.zv-radio {\n border-radius: 9999px;\n}\n\n.zv-check-box:hover,\n.zv-radio:hover {\n border-color: var(--accent-line);\n}\n\n/* Le blanc du glyphe est écrit en clair (%23fff) : une data-URI ne lit pas\n * les variables CSS. Valeur strictement égale à --white. La coche est au\n * trait carré depuis le 25/08/2026, comme tout ce qui trace. */\n.zv-check-box:checked {\n border-color: var(--accent);\n background-color: var(--accent);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='2.5,6.5 5,9 9.5,3.5'/%3E%3C/svg%3E\");\n background-position: center;\n background-repeat: no-repeat;\n background-size: 12px 12px;\n}\n\n/* Le point du radio est creusé dans l'aplat par un anneau blanc interne :\n * un pseudo-élément sur un <input> n'est pas fiable d'un moteur à l'autre.\n * Ce box-shadow est une géométrie, pas une ombre de charte — d'où l'absence\n * de token. */\n.zv-radio:checked {\n border-color: var(--accent);\n background-color: var(--accent);\n box-shadow: inset 0 0 0 4px var(--white);\n}\n\n/* ─── Interrupteur ──────────────────────────────────────────────────────\n * Ligne de réglage des dossiers (logs-client.tsx:267-293). Piste\n * RECTANGULAIRE, bouton rond : relevé tel quel, ne pas arrondir la piste.\n *\n * ⚠️ RABATTEMENT VISIBLE : la piste éteinte était un gris ferme (#C9CEDD),\n * rabattu sur --accent-line, qui est bleuté. L'état éteint prend donc une\n * teinte indigo très pâle. La distinction éteint/allumé tient quand même —\n * elle est portée par l'aplat plein et par le libellé accolé, pas par cette\n * nuance-là. */\n\n/* ⚠️ REDESSINÉ le 25/08/2026 — la DA a tranché « switch horrible » sur\n * pièces, et elle avait raison sur trois points :\n * · 28×16 avec un bouton rond de 12, c'est un jouet posé au milieu de\n * contrôles carrés de 16px de haut ; la ligne de réglage n'avait aucune\n * unité ;\n * · la piste éteinte était --accent-line, un indigo très pâle : éteint et\n * allumé se distinguaient par la SATURATION d'une même teinte, ce qui\n * tombe au premier écran mal calibré et disparaît en daltonisme ;\n * · le bouton rond contredisait l'arbitrage « carré » du même jour.\n * La piste passe donc à 36×20, à angle vif, avec un filet qui la pose comme\n * les champs voisins ; éteint elle est BLANCHE au filet gris, allumée elle\n * est un aplat indigo plein. L'écart n'est plus une nuance, c'est un\n * contraste. */\n.zv-switch {\n position: relative;\n display: inline-block;\n width: 36px;\n height: 20px;\n flex: none;\n border: 1px solid var(--line);\n background: var(--white);\n /* La courbe est celle du paquet (motion.css, `--zv-easing`), recopiée en\n * clair : forms.css ne dépend pas de motion.css et n'a pas à en dépendre\n * pour une transition de couleur. Ce n'est PAS `ease` — le défaut d'un\n * framework tiers n'a rien à faire dans le système. */\n transition:\n background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch-hit:hover .zv-switch {\n border-color: var(--accent-line);\n}\n\n.zv-switch--on {\n border-color: var(--accent);\n background: var(--accent);\n}\n\n.zv-switch-hit:hover .zv-switch--on {\n border-color: var(--accent-deep);\n background: var(--accent-deep);\n}\n\n/* Bouton CARRÉ : le rond ici n'est pas une forme porteuse de sens (ce que\n * sont la pastille d'état ou le radio), c'était un style — il tombe avec les\n * autres rayons. Éteint il est gris comme un contrôle inactif, allumé il\n * passe en blanc sur l'aplat indigo. */\n.zv-switch__knob {\n position: absolute;\n inset-block-start: 2px;\n inset-inline-start: 2px;\n width: 14px;\n height: 14px;\n border-radius: 0;\n background: var(--label);\n transition:\n inset-inline-start 150ms cubic-bezier(0.2, 0.7, 0.2, 1),\n background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch--on .zv-switch__knob {\n inset-inline-start: 18px;\n background: var(--white);\n}\n\n/* Cible tactile portée par une enveloppe : agrandir la piste elle-même\n * décalerait toute la ligne de réglage (cf. mobile en fin de fichier). */\n.zv-switch-hit {\n display: flex;\n /* La source place la gouttière de 7px sur une enveloppe PARENTE, et\n * n'entoure que la piste de sa zone tactile (logs-client.tsx:267-273).\n * Elle est ramenée ici : cette classe enveloppe le contrôle ET son\n * libellé, ce qui rend le texte cliquable — un <label> qui n'entoure que\n * l'interrupteur laisse une cible de 28px là où il en faut 40. Sans la\n * gouttière, le libellé vient coller à la piste. */\n align-items: center;\n gap: 7px;\n cursor: pointer;\n}\n\n/* Le libellé accolé change avec l'état : c'est lui qui porte le sens, la\n * piste seule ne dit pas ce qui est activé. */\n.zv-switch__text {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n color: var(--label);\n}\n\n.zv-switch__text--on {\n color: var(--accent);\n}\n\n/* ─── Curseur ───────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Aucune source ; dérivé de\n * l'interrupteur redessiné : bouton CARRÉ de 14 px (la taille exacte du\n * bouton du switch), --accent, --accent-deep au survol. La piste fait 3 px\n * — la hauteur de .zv-progress — en --line, remplie en --accent jusqu'au\n * curseur. PAS le spectre : la barre de progression est l'un des trois\n * emplois autorisés, le curseur n'en est pas un.\n *\n * Le remplissage est peint par le FOND de l'input (un dégradé à arrêt\n * franc), position portée par --zv-range-pos que la façade React calcule —\n * une géométrie, comme la largeur de Progress, pas une valeur de charte.\n * Sans elle, la piste s'affiche remplie à moitié : poser la variable, ou\n * assumer le repli. Le bouton, lui, passe par les pseudo-éléments de\n * chaque moteur — c'est le seul contrôle du paquet qui l'exige. */\n\n.zv-range {\n --zv-range-pos: 50%;\n appearance: none;\n -webkit-appearance: none;\n display: block;\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n padding: 0;\n border: 0;\n background: linear-gradient(90deg, var(--accent) var(--zv-range-pos), var(--line) 0);\n background-repeat: no-repeat;\n background-size: 100% 3px;\n background-position: 0 center;\n cursor: pointer;\n}\n\n.zv-range::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 14px;\n height: 14px;\n border: 0;\n border-radius: 0;\n background: var(--accent);\n transition: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-range::-moz-range-thumb {\n width: 14px;\n height: 14px;\n border: 0;\n border-radius: 0;\n background: var(--accent);\n}\n\n.zv-range:hover::-webkit-slider-thumb {\n background: var(--accent-deep);\n}\n\n.zv-range:hover::-moz-range-thumb {\n background: var(--accent-deep);\n}\n\n.zv-range[disabled] {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.zv-range[disabled]:hover::-webkit-slider-thumb {\n background: var(--accent);\n}\n\n.zv-range[disabled]:hover::-moz-range-thumb {\n background: var(--accent);\n}\n\n/* ─── Puces de filtre ───────────────────────────────────────────────────\n * Le seul mécanisme de filtrage de l'application (bibliotheque-client.tsx:\n * 143-180, repertoire-client.tsx:79-94, logs-client.tsx:193-207).\n *\n * ⚠️ LA SOURCE DIVERGE, ET C'EST TRANCHÉ ICI :\n * · gouttière du groupe — bibliothèque 5px, répertoire et logs 6px. On\n * retient 6px : deux écrans sur trois, et c'est la valeur de .zv-tabs\n * (navigation.css), qui héberge les mêmes puces. Deux conteneurs des\n * mêmes puces avec deux écarts différents, c'est un défaut, pas une\n * nuance.\n * · taille et densité — bibliothèque descend à 11px/4-9px au-dessus du\n * seuil, le répertoire reste à 11px/8-12px partout, en graisse 600. On\n * retient la puce DENSE de la bibliothèque au-dessus du seuil, et la\n * graisse 500 (la 600 est réservée à l'état sélectionné, sinon\n * sélectionné et non sélectionné ne se distinguent plus que par la\n * couleur).\n * · corps — 11 et 12px se rabattent tous les deux sur --app-sm (12px) :\n * l'écart d'un pixel entre les deux écrans disparaît, et la variante\n * mobile de la bibliothèque avec lui. C'est le prix de l'échelle\n * rationalisée, assumé.\n *\n * ⚠️ L'état sélectionné n'est porté par AUCUN aria-pressed en production :\n * il est invisible pour un lecteur d'écran. Le paquet impose <button\n * aria-pressed=\"true|false\"> — et le style suit l'attribut autant que la\n * classe, pour que le balisage correct ne coûte rien. */\n\n.zv-seg-group {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* ⚠️ HORS RELEVÉ. Les quatre rangées de puces du dépôt passent TOUTES à la\n * ligne ; le motif « nowrap + défilement » n'apparaît que sur des CADRES\n * (tableau, bande de trente jours), jamais sur une rangée de puces.\n * Cette variante n'existe que parce que la règle du projet interdit tout\n * défilement horizontal de PAGE : une rangée qu'on refuse d'enrouler doit\n * alors déborder dans son propre cadre. À poser explicitement, jamais par\n * défaut. */\n.zv-seg-group--rail {\n flex-wrap: nowrap;\n overflow-x: auto;\n}\n\n.zv-seg {\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 4px 9px;\n font-family: var(--font-text);\n font-size: var(--app-sm);\n font-weight: 500;\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n}\n\n/* La rangée de puces vit souvent dans un rail qui défile horizontalement\n * (.zv-tabs--rail, navigation.css). L'anneau de base.css y serait rogné aux\n * deux bords. On ne le REDÉFINIT pas — on déplace seulement son tracé à\n * l'intérieur de la puce, où rien ne le coupe. */\n.zv-seg:focus-visible {\n outline-offset: -2px;\n}\n\n/* Sélection DOUCE (bibliothèque), la seule livrée par défaut : elle\n * n'ajoute pas un second aplat indigo à l'écran, là où la charte n'autorise\n * qu'un seul plein par page. */\n.zv-seg--on,\n.zv-seg[aria-pressed=\"true\"] {\n border-color: var(--accent-line);\n background: var(--accent-soft);\n font-weight: 600;\n color: var(--accent);\n}\n\n/* Sélection PLEINE (répertoire, logs) : OPT-IN, à poser explicitement, et\n * BORNÉE — un seul groupe plein par page, jamais sur une page qui porte\n * déjà un bouton plein. Une page de filtres à plusieurs groupes (la\n * bibliothèque en a deux) afficherait sinon autant d'aplats indigo que de\n * groupes, simultanément : c'est le seul mécanisme du paquet capable de\n * multiplier le plein sans le dire. */\n.zv-seg--on-solid,\n.zv-seg--on-solid[aria-pressed=\"true\"] {\n border-color: var(--accent);\n background: var(--accent);\n color: var(--white);\n}\n\n.zv-seg__count {\n margin-inline-start: 6px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n}\n\n/* Niveaux de log : libellés de longueur inégale (INFO, WARN, ERROR) centrés\n * pour que la rangée ne danse pas d'une puce à l'autre. */\n.zv-seg--mono {\n justify-content: center;\n padding: 4px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.06em;\n}\n\n/* ─── Zone de dépôt ─────────────────────────────────────────────────────\n * Elle tient lieu d'action principale de l'écran d'import : il n'y a aucun\n * bouton plein sur cette page (import-client.tsx:144-202).\n *\n * ⚠️ En production c'est un <div onClick> sans role ni tabIndex : la page\n * d'import est inatteignable au clavier. Le paquet impose role=\"button\" +\n * tabindex=\"0\" (l'anneau de base.css suit alors tout seul).\n *\n * L'icône est un SVG du balisage (import-client.tsx:159-166) : viewBox\n * 0 0 30 30, fill=\"none\", stroke=\"var(--accent)\", stroke-width=\"1.5\" —\n * relevée en stroke-linecap=\"round\", REPRISE au trait carré le 25/08/2026\n * comme tout ce qui trace (DECISIONS.md, « Le trait carré ») :\n * <line x1=\"15\" y1=\"4\" x2=\"15\" y2=\"18\"/>\n * <polyline points=\"9.5,12.5 15,18 20.5,12.5\"/>\n * <path d=\"M5 21 v3 a2 2 0 0 0 2 2 h16 a2 2 0 0 0 2-2 v-3\"/> */\n\n.zv-drop {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n /* 1,5px : l'épaisseur du pointillé est celle du tracé de l'icône, les deux\n * se répondent. */\n border: 1.5px dashed var(--accent-line);\n background: var(--white);\n padding: 38px 24px;\n cursor: pointer;\n}\n\n/* Survol et survol de dépôt rendus à l'identique : la zone répond pareil que\n * le fichier soit traîné ou non, sinon le retour visuel se contredit.\n *\n * ⚠️ RABATTEMENT VISIBLE : le fond de survol du relevé (#FDFDFF) tombe sur\n * --panel, à peine distinct du --white de repos. Le fond ne SIGNALE donc\n * plus rien — le retour visuel repose entièrement sur le filet qui passe à\n * --accent. Ne pas le supprimer en croyant que le fond compense. */\n.zv-drop:hover,\n.zv-drop.is-dragging {\n border-color: var(--accent);\n background: var(--panel);\n}\n\n/* Stockage indisponible : la zone reste visible mais inerte — la masquer\n * ferait croire à une page cassée. */\n.zv-drop--disabled {\n cursor: not-allowed;\n border-color: var(--line);\n background: var(--panel);\n opacity: 0.6;\n}\n\n.zv-drop--disabled:hover {\n border-color: var(--line);\n background: var(--panel);\n}\n\n/* 15px : c'est une phrase, pas un libellé d'interface — d'où le palier de\n * corps de texte (--body) plutôt que l'échelle applicative, qui s'arrête à\n * 14px. */\n.zv-drop__title {\n font-size: var(--body);\n font-weight: 500;\n color: var(--ink);\n text-align: center;\n}\n\n.zv-drop__hint {\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n.zv-drop__hint a,\n.zv-drop__hint strong {\n font-weight: 600;\n color: var(--accent);\n text-decoration: underline;\n}\n\n/* Sous le seuil il n'y a pas de glisser-déposer : le lien souligné cède la\n * place à une vraie cible tactile. LES DEUX NŒUDS SONT POSÉS EN PERMANENCE\n * DANS LE BALISAGE et c'est CE fichier qui les échange, au seul seuil du\n * paquet. Ne pas y ajouter .zv-only-desktop / .zv-only-mobile (layout.css) :\n * les deux mécanismes se disputeraient la même déclaration `display`, et le\n * module tirerait la mise en page de la vitrine derrière lui. */\n.zv-drop__browse {\n display: none;\n}\n\n.zv-drop__spec {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n color: var(--label);\n}\n\n/* Le vrai <input type=\"file\"> est retiré du flux, la zone entière lui sert\n * de déclencheur. Nommé en élément de .zv-drop, et non `.zv-file` : le\n * paquet a par ailleurs une vignette de format (.zv-filetype, data.css) et\n * les deux se liraient comme un seul bloc.\n *\n * Conséquence du retrait du flux : l'input n'est PAS focusable — d'où le\n * role/tabindex exigés sur .zv-drop ci-dessus. Remettre value='' après\n * chaque sélection, sinon redéposer le même fichier ne déclenche rien. */\n.zv-drop__input {\n display: none;\n}\n\n/* ─── États transverses ─────────────────────────────────────────────────\n * HORS RELEVÉ pour l'essentiel, et assumé : la production ne désactive aucun\n * champ (seul le bouton d'envoi passe en `cursor-wait; opacity:.7` pendant\n * la requête) et ne signale l'erreur au niveau du champ qu'à un seul\n * endroit. Un système livré sans ces deux états oblige chaque app à les\n * réinventer, chacune à sa façon. Les valeurs sont empruntées, sans\n * exception, à ce qui existe déjà : le traitement désactivé de\n * .zv-drop--disabled et le filet de l'alerte d'erreur. */\n\n.zv-input[disabled],\n.zv-textarea[disabled],\n.zv-select[disabled],\n.zv-seg[disabled],\n.zv-check-box[disabled],\n.zv-radio[disabled] {\n cursor: not-allowed;\n border-color: var(--line);\n background: var(--panel);\n opacity: 0.6;\n}\n\n/* Le survol ne doit pas rattraper un contrôle désactivé : sans ces règles,\n * .zv-seg:hover et .zv-select:hover repeignent le filet et le contrôle a\n * l'air actif. */\n.zv-select[disabled]:hover,\n.zv-check-box[disabled]:hover,\n.zv-radio[disabled]:hover {\n border-color: var(--line);\n}\n\n/* Champ en erreur : seul le filet change. Un fond rouge sur un champ à\n * remplir rendrait la saisie illisible — l'aplat --err-soft reste réservé au\n * bandeau d'alerte. Le sélecteur suit l'attribut autant que la classe :\n * aria-invalid est ce qui rend l'erreur audible, il ne doit rien coûter. */\n.zv-field--err .zv-input,\n.zv-field--err .zv-textarea,\n.zv-field--err .zv-select,\n.zv-input[aria-invalid=\"true\"],\n.zv-textarea[aria-invalid=\"true\"] {\n border-color: var(--err-line);\n}\n\n/* Le focus l'emporte sur l'erreur : pendant la correction, l'utilisateur doit\n * voir où il est, pas ce qui était faux. */\n.zv-field--err .zv-input:focus,\n.zv-field--err .zv-textarea:focus,\n.zv-input[aria-invalid=\"true\"]:focus,\n.zv-textarea[aria-invalid=\"true\"]:focus {\n border-color: var(--accent-line);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Valeurs relevées aux mêmes endroits, sous le seuil des sources (1024px),\n * ramenées ici au seuil unique du paquet.\n *\n * NOTE : le micro-libellé du groupe de champ ne bouge PAS ici. Le relevé le\n * grossit d'un pixel sous le seuil (9,5 → 10,5) ; les deux valeurs tombent\n * sur --mono. La nuance n'existe plus, et une règle qui ne change rien vaut\n * moins qu'une ligne qui dit pourquoi. */\n\n@media (max-width: 767px) {\n /* Le lien qui accompagne l'intitulé devient une cible tactile : la ligne\n * de base ne tient plus quand les deux éléments font 40px de haut, et il\n * leur faut alors une gouttière (gap-3), là où space-between suffisait. */\n .zv-field__head {\n align-items: center;\n gap: 12px;\n }\n\n /* 44px, pas --tap (40px) : c'est la valeur des formulaires de connexion et\n * du pavé d'import, les deux seuls endroits où l'on saisit au doigt.\n * 16px : en dessous, Safari iOS zoome à la mise au point du champ et\n * décale toute la page — ce n'est pas un choix typographique, aucun token\n * n'a à porter cette valeur. */\n .zv-input,\n .zv-textarea {\n min-height: 44px;\n font-size: 16px;\n }\n\n /* Les <select> de /admin, eux, sont relevés à 40px (`min-h-10 lg:min-h-0`)\n * et non à 44 : un autre écran, une autre main. */\n .zv-input--select {\n min-height: var(--tap);\n }\n\n /* Le champ nu et celui d'un cadre à affixe. 16px et non --app-lg (14px) :\n * la règle des 16px ci-dessus vaut pour TOUT champ qu'on saisit au doigt,\n * et ces deux-là — le composeur, le sous-domaine d'organisation — sont\n * parmi les plus saisis d'une application. Sous 16, Safari iOS zoome à la\n * mise au point et décale la page. */\n .zv-input--bare,\n .zv-affix .zv-input--bare {\n min-height: var(--tap);\n font-size: 16px;\n }\n\n /* Le cadre passe pleine largeur. Il n'est PAS figé (`flex: none`) : le\n * relevé lui laisse le `0 1 auto` par défaut, il peut donc encore se\n * comprimer si la ligne qui l'accueille l'exige. */\n .zv-search {\n width: 100%;\n min-width: 0;\n flex: 0 1 auto;\n }\n\n /* Le champ de recherche n'était visé par aucune des deux règles : il\n * gardait --app (13px), donc il zoomait lui aussi. */\n .zv-search__input {\n padding-block: 12px;\n font-size: 16px;\n }\n\n /* Le déclencheur peut passer à la ligne : sa valeur courante est un nom de\n * dossier libre, qui déborderait sinon de l'écran. */\n .zv-select {\n min-height: var(--tap);\n flex-wrap: wrap;\n }\n\n /* La puce se desserre pour devenir une cible tactile. Le corps, lui, ne\n * change plus : 11 et 12px se rabattent sur le même palier (voir la\n * section « Puces de filtre »). */\n .zv-seg {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n padding: 8px 12px;\n }\n\n /* Cible de 40px sans gonfler la ligne de réglage : la marge négative\n * annule exactement le padding ajouté. */\n .zv-switch-hit {\n margin-block: -12px;\n padding-block: 12px;\n }\n\n /* La piste ne bouge pas (le fond reste centré) : seule la boîte grandit\n * pour faire une cible tactile. */\n .zv-range {\n height: var(--tap);\n }\n\n .zv-drop {\n padding: 32px 16px;\n }\n\n /* Pas de glisser-déposer au doigt : le lien souligné est remplacé par une\n * cible pleine largeur. Les deux nœuds appartiennent à ce module, l'échange\n * se fait ici et nulle part ailleurs. */\n .zv-drop__hint {\n display: none;\n }\n\n .zv-drop__browse {\n display: flex;\n width: 100%;\n max-width: 280px;\n min-height: 44px;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n padding-inline: 16px;\n font-size: var(--app);\n font-weight: 600;\n color: var(--accent);\n }\n}\n\n/* ─── Champ mot de passe ────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des planches ne montre d'écran de\n * connexion, et le système n'avait donc rien pour ce champ — alors que tous\n * les produits en ont un, et que la saisie masquée est le seul endroit de\n * l'interface où l'on tape sans voir ce qu'on écrit.\n *\n * Bâti sur `.zv-affix` plutôt qu'en cadre parallèle : le champ nu, le cadre\n * qui l'entoure et son anneau de mise au point sont déjà là.\n *\n * ⚠️ Le bouton est un vrai <button type=\"button\"> : posé sur un <span>, il\n * n'est ni atteignable au clavier ni annoncé, et il envoie le formulaire dès\n * qu'on oublie le type. Son libellé change avec l'état — « Afficher » /\n * « Masquer » — parce qu'une icône seule ne dit pas ce qui va se passer. */\n\n.zv-affix__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n /* Cible tactile obtenue par la taille, pas par du remplissage : un bouton\n * de 16 px entouré de marge reste un bouton de 16 px pour le doigt. */\n width: 32px;\n height: 32px;\n margin-right: -6px;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: none;\n color: var(--muted);\n cursor: pointer;\n transition: color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-affix__btn:hover {\n color: var(--ink);\n}\n\n.zv-affix__btn[aria-pressed='true'] {\n color: var(--accent);\n}\n\n/* Le contour déborderait du cadre à affixe, qui le rogne. On le rentre. */\n.zv-affix__btn:focus-visible {\n outline-offset: -2px;\n}\n\n/* Les gestionnaires de mot de passe des navigateurs posent leur propre œil\n * dans le champ : deux boutons côte à côte pour la même chose. On masque le\n * leur, le nôtre étant accessible et cohérent avec la charte. */\n.zv-affix input[type='password']::-ms-reveal,\n.zv-affix input[type='password']::-ms-clear {\n display: none;\n}\n\n\n/* ── feedback.css ── */\n/* Zevra UI — retours d'état : bandeaux, étiquettes, pastilles, absence,\n * travail en cours, curseur de flux — et, depuis le 25/08/2026, la roue,\n * le squelette et l'infobulle, longtemps refusés et enfin arbitrés.\n *\n * Dépend de tokens.css et de base.css (reset, fonte de corps, focus clavier\n * global). Indépendant des autres modules.\n *\n * Tokens réellement employés ici, tous vérifiés dans tokens.css :\n * couleurs — --ink, --ink-2, --muted, --label, --line, --hairline, --white,\n * --accent, --accent-line, --accent-soft,\n * --ok, --ok-soft, --ok-line,\n * --warn, --warn-soft, --warn-line, --warn-ink,\n * --err, --err-soft, --err-line ;\n * typo — --font-display, --font-mono, --app, --app-sm,\n * --app-h2, --mono, --mono-sm ;\n * ombre — --shadow-menu (l'infobulle, qui flotte comme le menu).\n * Aucun autre. Il n'existe PAS de --ink-3…--ink-7, pas de --line-soft, pas de\n * --line-firm, pas d'échelles --text-* ni --mono-xs/--mono-2xs : une version\n * antérieure de cet en-tête les déclarait en dépendance, ce qui rendait le\n * module inauditable (rien ne disait quelle valeur portait --ink-6) et l'aurait\n * rendu sans couleur ni taille chez le consommateur. Les valeurs relevées qui\n * tombaient sur ces noms sont rabattues sur les paliers du paquet, et chaque\n * rabattement qui déplace le rendu est écrit en commentaire à l'endroit exact\n * où il est fait.\n *\n * Relevé sur l'application Mémoire, en production. Une différence de méthode\n * avec les modules de vitrine : Mémoire est écrite mobile-first et bascule à\n * `lg` (1024px), alors que la charte ne connaît qu'UN point de rupture, à\n * 768px. Les valeurs desktop sont donc posées en base et les valeurs mobiles\n * regroupées en fin de fichier — le rendu desktop est identique au relevé, le\n * rendu mobile arrive 256px plus tôt. C'est délibéré : deux ruptures dans un\n * système qui en annonce une seule est une dette qui se paie sur chaque app.\n *\n * ÉCHELLE DE TEXTE — la production emploie des demi-pixels (11,5 / 12,5 /\n * 13,5) que le paquet a rationalisés en trois paliers (12 / 13 / 14). Le\n * rabattement retenu ici conserve les ÉCARTS du relevé plutôt que la valeur\n * absolue : 11,5 et 12,5 → --app-sm (12), 13,5 → --app (13), 14 → --app-lg.\n * Deux tailles distinctes d'un pixel dans la source restent donc distinctes\n * d'un pixel ; c'est ce qui fait qu'un bandeau ample reste plus grand qu'un\n * bandeau courant, des deux côtés du seuil.\n *\n * Presque rien ici n'est interactif : pas de bouton de fermeture, pas de\n * champ. Les états [disabled] / [aria-invalid] n'ont aucune cible dans ce\n * module — ils appartiennent aux boutons (components.css) et aux champs\n * (forms.css). Le focus clavier global de base.css suffit au bandeau qu'on\n * rend focusable pour l'annoncer. Seule exception, arbitrée le 25/08/2026 :\n * l'infobulle, qui se montre par nature au survol et au focus de sa cible.\n */\n\n/* ─── Bandeaux d'état ───────────────────────────────────────────────────\n * Filet + fond atténué + texte du même ton : la grammaire la plus\n * systématique du relevé, appliquée à l'identique six fois. Elle tient\n * parce que les trois valeurs viennent d'une même famille de tokens ; la\n * recomposer à la main avec des opacités produit six bandeaux différents.\n *\n * Le ton est OBLIGATOIRE : `.zv-alert` seule hérite d'un filet en\n * currentColor, c'est-à-dire un trait d'encre autour d'un fond blanc.\n * Toujours l'accompagner de --info / --ok / --warn / --err / --dash. */\n\n.zv-alert {\n border: 1px solid;\n padding: 12px 16px;\n /* 12,5px relevé → palier 12. La valeur mobile (13,5 → 13) est en fin de\n * fichier : les cinq bandeaux d'authentification GROSSISSENT sous le\n * seuil, ils ne rétrécissent pas. */\n font-size: var(--app-sm);\n line-height: 1.55;\n font-weight: 500;\n /* Un bandeau porte souvent un message d'erreur brut, non maîtrisé :\n * nom de fichier à rallonge, identifiant, chemin. Sans coupure, il pousse\n * la page en défilement horizontal. */\n overflow-wrap: break-word;\n /* Angle vif, comme au relevé — et, depuis le 25/08/2026, comme tout le\n * paquet : l'arbitrage DA « angle vif partout » (DECISIONS.md) a ramené\n * les quatre tokens de rayon à 0. La contradiction longtemps signalée ici\n * (un bouton arrondi au milieu de blocs vifs dans l'écran vide) est\n * éteinte par cet arbitrage, pas par ce module. */\n border-radius: 0;\n}\n\n/* Information — le seul ton relevé en bandeau de bas de page (garanties,\n * explication d'un écran). */\n.zv-alert--info {\n border-color: var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n/* Avertissement — le texte est --warn-ink, JAMAIS --warn : l'ambre plein\n * sur fond ambre atténué passe sous le seuil de contraste. C'est la seule\n * famille asymétrique du système, et l'erreur la plus facile à commettre. */\n.zv-alert--warn {\n border-color: var(--warn-line);\n background: var(--warn-soft);\n color: var(--warn-ink);\n}\n\n/* Erreur — à porter avec role=\"alert\" côté balisage, sinon le message\n * apparaît sans être annoncé aux lecteurs d'écran : c'est précisément le\n * seul contenu de l'écran qu'on ne peut pas se permettre de rater. */\n.zv-alert--err {\n border-color: var(--err-line);\n background: var(--err-soft);\n color: var(--err);\n}\n\n/* Succès — RELEVÉ, pleine largeur, deux fois : reset-password-card.tsx:90 et\n * forgot-password-card.tsx:60, en `border-ok-line bg-ok-soft px-3 py-[10px]\n * text-[13.5px]/[1.6] font-medium text-ok`. (Une version antérieure de ce\n * commentaire le disait « extrapolé » : c'était faux, les deux écrans de\n * réinitialisation n'avaient pas été relus. Le gabarit du relevé est celui\n * de --sm, à composer avec.) */\n.zv-alert--ok {\n border-color: var(--ok-line);\n background: var(--ok-soft);\n color: var(--ok);\n}\n\n/* Variante « aveu d'ignorance » : ce que le système n'a PAS trouvé. Le\n * pointillé sans fond dit qu'il n'y a rien derrière — un fond plein ferait\n * lire une réponse là où il n'y en a pas. Avec la zone de dépôt, c'est le\n * seul pointillé de toute la couche applicative : le garder rare, c'est ce\n * qui lui laisse son sens.\n *\n * ⚠️ DEUX PERTES DE TEINTE, assumées et signalées plutôt que glissées :\n * 1. le texte est `#6E6A61` en production (chat-client.tsx:265), un gris\n * CHAUD — le seul du fil de réponse. Le paquet n'a pas de neutre chaud :\n * --muted (#5a637a) est bleuté. Le rabattement est arrêté, mais il\n * REFROIDIT ce texte, et il le range sous le même token que les gris\n * froids du relevé (#6B7488 du pager, du bouton d'icône). Deux familles\n * de teinte finissent donc sur --muted : c'est une fusion, pas une\n * nuance. Le jour où la DA veut retrouver ce chaud, il faut un token\n * dédié dans tokens.css — pas une valeur en dur ici.\n * 2. le filet est `#C9CEDD` en production, un gris froid neutre ; --accent-line\n * (#c9d6f5) est le seul filet du paquet à cette clarté, mais il est\n * légèrement indigo. Écart perceptible sur un pointillé large. */\n.zv-alert--dash {\n border: 1px dashed var(--accent-line);\n background: none;\n padding: 10px 14px;\n font-size: var(--app);\n line-height: 1.55;\n color: var(--muted);\n}\n\n/* Gabarit compact (`px-3 py-[9px]`) — relevé quatre fois : login-card:207,\n * bibliotheque-client:210, branding-card:219, billing-card:164. Deux d'entre\n * eux sont des alertes de PAGE, pas de panneau : ce gabarit n'est donc pas\n * « le bandeau dans un cadre », c'est simplement le bandeau resserré. La\n * carte de suppression (delete-org-card:39) n'a aucun bandeau — l'histoire\n * qui justifiait ces valeurs dans une version antérieure était inventée, les\n * valeurs, elles, sont bonnes. */\n.zv-alert--sm {\n padding: 9px 12px;\n}\n\n/* Gabarit ample : le message d'incident qui REMPLACE une réponse attendue\n * (GPU en charge). Il occupe la place du contenu manquant, d'où le corps plus\n * grand. Source chat-client.tsx:308 : `px-4 py-[14px] text-[13.5px]/[1.6]\n * lg:px-5 lg:py-4` — le padding change au seuil, la TAILLE non (13,5px des\n * deux côtés). D'où l'absence de font-size dans le bloc mobile. */\n.zv-alert--lg {\n padding: 16px 20px;\n font-size: var(--app);\n line-height: 1.6;\n}\n\n/* ⚠️ DEUX ARBITRAGES de couleur portés ici, pas glissés sous le tapis :\n * 1. le filet vert est écrit `border-ok/30` en production (un color-mix à\n * 30 %) alors que --ok-line existe dans le paquet et n'y sert jamais.\n * Retenu : --ok-line. Le rendu diffère — le color-mix laissait\n * transparaître --ok-soft, le token est un vert franc.\n * 2. l'ambre de production est #FFF6E3 / #EBD9AE (teintes écrites en dur\n * dans deux tables), le token du paquet #faf3e2 / #ebdcb4. Retenu : les\n * tokens. Écart visible à l'œil sur les bandeaux et badges WARN. */\n\n/* ─── Étiquettes d'état ─────────────────────────────────────────────────\n * Rôle d'une entité, niveau d'un log, état d'un service, issue d'un envoi :\n * un seul objet, trois gabarits, cinq tons. Le mono en petit corps est ce\n * qui les distingue d'un texte — une étiquette se lit d'un coup d'œil, elle\n * ne se lit pas en phrase.\n *\n * Pas de text-transform : les libellés sont écrits en capitales à la source.\n * Le laisser au contenu évite qu'un libellé accentué (« INDEXÉ ») soit\n * transformé différemment selon la locale du navigateur.\n *\n * Ne pas confondre avec .zv-pill / .zv-tag / .zv-src de components.css :\n * ceux-là appartiennent à la vitrine (pastille pleine indigo, micro-label\n * espacé, puce de source). Aucun nom n'est repris ici. */\n\n.zv-badge {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n /* En cellule de grille, une étiquette étirée sur toute la colonne cesse\n * de se lire comme une étiquette : elle se cale à gauche sur sa largeur. */\n justify-self: start;\n border: 1px solid;\n padding: 3px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono); /* 10px relevé, palier exact du paquet */\n font-weight: 600;\n /* Un état ne se coupe jamais en deux lignes : « EN CHARGE » sur deux\n * lignes se lit comme deux états. */\n white-space: nowrap;\n border-radius: 0; /* angle vif — relevé, et arbitré depuis (25/08/2026) */\n}\n\n/* Gabarit micro : niveau de log en tête de ligne, badge de la coque. À\n * cette taille la graisse monte et les lettres s'espacent, sinon les\n * capitales se referment et INFO/WARN ne se distinguent plus en diagonale.\n * 8,5px relevé → --mono-sm (9px), le plus petit palier du paquet. */\n.zv-badge--micro {\n padding: 2px 6px;\n font-size: var(--mono-sm);\n font-weight: 700;\n letter-spacing: 0.06em;\n}\n\n/* Gabarit état de service (OK / CHARGE). ⚠️ 9,5px au relevé, rabattu sur\n * --mono (10px) : il retombe donc sur la MÊME taille que .zv-badge, et ne\n * s'en distingue plus que par l'interlettrage. C'est la conséquence assumée\n * de l'échelle à trois paliers ; le gabarit est conservé pour garder un nom\n * à cet usage, pas parce qu'il change encore de corps. */\n.zv-badge--state {\n padding: 3px 8px;\n font-size: var(--mono);\n letter-spacing: 0.06em;\n}\n\n/* Tons — triplets relevés tels quels dans les tables de teintes. */\n\n/* Neutre : `border-line bg-hairline text-[#4A5468]` (repertoire-client:25).\n * Le filet est bien --line, pas un filet adouci : une version antérieure y\n * avait substitué un token intermédiaire sans le dire. #4A5468 est le neutre\n * intermédiaire du paquet, --ink-2. */\n.zv-badge--neutre {\n background: var(--hairline);\n border-color: var(--line);\n color: var(--ink-2);\n}\n.zv-badge--accent {\n background: var(--accent-soft);\n border-color: var(--accent-line);\n color: var(--accent);\n}\n.zv-badge--ok {\n background: var(--ok-soft);\n border-color: var(--ok-line);\n color: var(--ok);\n}\n.zv-badge--warn {\n background: var(--warn-soft);\n border-color: var(--warn-line);\n color: var(--warn-ink);\n}\n.zv-badge--err {\n background: var(--err-soft);\n border-color: var(--err-line);\n color: var(--err);\n}\n\n/* Étiquette à pastille (« RÉSEAU PRIVÉ · IA DÉDIÉE », « TOUT\n * OPÉRATIONNEL »). La pastille reprend la couleur du texte plutôt qu'un ton\n * réglé à part : c'est ce que fait le relevé, et cela garantit que les deux\n * ne divergent jamais quand on change de ton.\n * ⚠️ Un jour où l'on posera une pastille sur .zv-badge--warn, currentColor\n * donnera --warn-ink (l'ambre sombre) et non --warn : cas absent du relevé,\n * à trancher avec la DA plutôt qu'à corriger ici en douce. */\n.zv-badge--dot .zv-dot {\n background: currentColor;\n}\n\n/* ─── Pastilles d'état ──────────────────────────────────────────────────\n * Seule ou dans une étiquette. Ronde par défaut ; CARRÉE dans les bandeaux\n * de provenance qui coiffent une réponse. La distinction est tenue sur cinq\n * occurrences : ce n'est pas un oubli de `rounded-full`, c'est la marque\n * que le point désigne une SOURCE et non un ÉTAT. Ne pas l'uniformiser. */\n\n.zv-dot {\n width: 6px;\n height: 6px;\n /* Sans flex: none la pastille s'écrase quand le libellé voisin est long —\n * elle devient un ovale, et l'état n'est plus lisible. */\n flex: none;\n border-radius: 9999px;\n /* Repli gris : un statut inconnu (nouvel état ajouté côté serveur, jamais\n * déployé côté interface) doit rester neutre. Le teindre en vert par\n * défaut ferait passer un échec pour un succès. */\n background: var(--label);\n}\n\n/* Pastille de provenance : carrée. Seule dérogation au rond de tout le\n * module. */\n.zv-dot--square {\n border-radius: 0;\n}\n\n/* Dans une étiquette de la coque ou une garantie : plus discrète. */\n.zv-dot--sm {\n width: 5px;\n height: 5px;\n}\n\n/* Devant un libellé de service ou une phrase de travail en cours : la\n * pastille porte seule l'information, elle grossit d'un pixel. */\n.zv-dot--lg {\n width: 7px;\n height: 7px;\n}\n\n/* Correspondance statut → couleur, relevée telle quelle. Elle vit en CSS et\n * pas dans chaque app : c'est la seule façon que « reçu » et « OCR en\n * cours » restent du même ambre partout. */\n.zv-dot--recu,\n.zv-dot--ocr {\n background: var(--warn);\n}\n.zv-dot--indexation {\n background: var(--accent);\n}\n.zv-dot--indexe,\n.zv-dot--ok {\n background: var(--ok);\n}\n.zv-dot--erreur {\n background: var(--err);\n}\n\n/* ─── Statut en clair ───────────────────────────────────────────────────\n * Pastille + libellé, sans cadre — contrairement à l'étiquette. Le cadre\n * est réservé à ce qui classe (rôle, niveau) ; le statut, lui, se lit dans\n * le fil de la ligne. Le message d'erreur REMPLACE le libellé quand il y en\n * a un : l'utilisateur veut lire la cause, pas le mot « Erreur ».\n *\n * Source : bibliotheque-client.tsx:259, `flex min-w-0 items-center\n * gap-[6px] text-[11.5px] font-medium text-[#3A4256] lg:min-w-[auto]`, avec\n * un `<span className=\"truncate\">` à l'intérieur et AUCUNE variante `lg:`\n * sur ce span. Autrement dit : le libellé tronque, et il tronque SOUS le\n * seuil, là où min-width vaut 0 ; au-dessus, la colonne reprend sa largeur\n * naturelle. Le fichier est desktop-first, donc `min-width: auto` est ici et\n * `min-width: 0` dans le bloc mobile — l'inverse exact de ce qu'une version\n * antérieure de ce module posait, qui faisait passer le libellé à la ligne\n * sur téléphone et décalait la pastille de 5px vers le bas. Rien de tout\n * cela n'existe en production. */\n\n.zv-status {\n display: flex;\n align-items: center;\n gap: 6px;\n /* Valeur initiale, écrite pour être lue : au-dessus du seuil la colonne\n * n'est pas contrainte, le libellé la pousse — c'est le `lg:min-w-[auto]`\n * du relevé. La troncature n'opère réellement que sous le seuil. */\n min-width: auto;\n font-size: var(--app-sm); /* 11,5px relevé → palier 12 */\n font-weight: 500;\n color: var(--ink-2); /* #3A4256 relevé → neutre intermédiaire du paquet */\n}\n\n/* Le libellé tronque des DEUX côtés du seuil : la classe `truncate` de la\n * source n'a pas de variante responsive. */\n.zv-status__label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ─── Absence ───────────────────────────────────────────────────────────\n * DEUX objets distincts, à ne surtout pas fusionner :\n * — la ligne d'absence, quand un filtre ne ramène rien : la collection\n * existe, seul le filtre est trop étroit. Elle vit DANS le cadre, à la\n * place des lignes, pour que l'en-tête de tableau reste visible ;\n * — l'écran d'absence, quand la collection est vide : il remplace le\n * tableau entier, porte un titre et l'action qui la remplira.\n * Les confondre donne soit un écran de bienvenue affiché à chaque filtre\n * trop strict, soit une collection vide qui n'offre aucune porte de sortie. */\n\n/* ⚠️ Les quatre lignes d'absence relevées ne s'accordent pas : `p-[26px]`\n * (repertoire-client:159), `px-[14px] py-6` (bibliotheque-client:230),\n * `px-[18px] py-8` (import-client:263), `py-5` seul (logs-client:234).\n * Retenue : la seule qui soit écrite telle quelle, 26px sur les quatre\n * côtés. Une version antérieure posait « 26px 14px », c'est-à-dire le\n * vertical de la première recomposé avec l'horizontal de la deuxième — une\n * paire plausible qui n'existe dans aucune source. */\n.zv-empty {\n padding: 26px;\n text-align: center;\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n.zv-empty--page {\n border: 1px solid var(--line);\n background: var(--white);\n padding: 48px 32px;\n text-align: center;\n}\n\n/* --app-h2 (20px) est le palier que tokens.css destine explicitement au\n * « titre de fiche, d'état vide, d'interstice ». */\n.zv-empty__title {\n /* Posé explicitement : sur un <span>, le titre viendrait se coller au\n * texte qui le suit au lieu de le surmonter, et l'état vide se lirait\n * comme une seule phrase. */\n display: block;\n font-family: var(--font-display);\n font-size: var(--app-h2);\n font-weight: 300;\n color: var(--ink);\n}\n\n.zv-empty__text {\n /* La mesure bornée n'est pas décorative : centré et pleine largeur, ce\n * paragraphe devient illisible dès 900px de cadre. */\n margin: 8px auto 0;\n max-width: 420px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* L'action de l'écran vide (le bouton qui remplit la collection). Elle est\n * ici pour l'écart seul : le bouton lui-même vient de components.css. */\n.zv-empty__action {\n margin-top: 20px;\n}\n\n/* Ni icône, ni illustration, ni « réinitialiser les filtres » : rien de tel\n * n'existe dans les sources, et ce sont exactement les trois ajouts qu'un\n * état vide attire. */\n\n/* ─── Travail en cours ──────────────────────────────────────────────────\n * En production ce n'est PAS une roue : c'est une pastille suivie d'une\n * phrase qui dit le travail réel (« Recherche dans 4 812 documents\n * anonymisés… »). Une roue dit « attendez » ; cette phrase dit ce qu'on\n * attend, sur combien, et qu'il ne se passe rien d'anormal. */\n\n.zv-loading {\n display: flex;\n align-items: center;\n gap: 9px;\n font-family: var(--font-mono);\n font-size: var(--app-sm);\n font-weight: 500;\n /* #727C93 au relevé — un gris froid, rabattu sur --muted comme les autres\n * neutres secondaires. Le token porte donc plusieurs gris du relevé : la\n * hiérarchie interne des gris de Mémoire (#6B7488 / #727C93) n'existe plus\n * dans le paquet, elle échouait de toute façon au seuil AA. */\n color: var(--muted);\n}\n\n.zv-loading .zv-dot {\n width: 7px;\n height: 7px;\n background: var(--accent);\n}\n\n/* ⚠️ STATIQUE, et c'est un choix. Aucune animation dans la source : ni\n * @keyframes, ni classe d'animation, la pastille ne bat pas. Lui ajouter un\n * pouls est l'invention la plus tentante de tout le relevé. Les boucles de\n * ce module (roue, squelette) disent une ATTENTE ; la pastille dit un\n * ÉTAT, et un état ne bat pas. Ne pas confondre `.zv-dot` avec\n * `.zv-anim-dot-active` de motion.css — ce dernier anime un point de\n * méthode de la vitrine, pas un état. */\n\n/* ─── Curseur de flux ───────────────────────────────────────────────────\n * Bloc collé à la fin du texte en cours de diffusion, sur le dernier\n * message seulement. Il dit « la réponse n'est pas finie » — sans lui, une\n * réponse coupée à mi-phrase se lit comme une réponse complète et fausse. */\n\n.zv-caret {\n margin-left: 2px;\n color: var(--accent);\n}\n\n/* Le glyphe ▍ (U+258D) est un CARACTÈRE posé dans le texte, pas un\n * pseudo-élément : il suit la dernière lettre où qu'elle tombe, y compris\n * en fin de ligne justifiée. STATIQUE là encore, aucun clignotement.\n *\n * Le flux interrompu, lui, n'a AUCUN style : c'est la phrase\n * « [Flux interrompu — la réponse est incomplète.] » ajoutée au corps du\n * message, rendue par white-space: pre-wrap. La réponse partielle est\n * conservée, jamais effacée — on n'efface pas ce que l'utilisateur a déjà lu. */\n\n/* ─── Retour d'attente : ce que fait Mémoire, et ce qu'elle ne fait pas ──\n * Le retour d'attente de cette application est TEXTUEL, et c'est cohérent\n * avec .zv-loading ci-dessus : le libellé du bouton change pendant l'action\n * (`{busy === d.id ? 'Suppression…' : 'Supprimer'}`, bibliotheque-client:283 ;\n * `{pending ? 'Connexion…' : 'Se connecter'}` au login), en plus d'un\n * `disabled:cursor-wait`. Ce n'est pas du CSS : c'est un motif de balisage à\n * reprendre, et il ne demande aucune classe.\n *\n * Restent DEUX actions sans aucun retour : la déconnexion et l'envoi de\n * message (le bouton reste plein et cliquable en apparence alors que la\n * garde existe côté script). C'est un défaut de l'application, pas une\n * intention de charte : ne pas le répliquer dans une app neuve.\n *\n * ─── Roue, squelette, infobulle : les manques, comblés ──────────────────\n * Longtemps REFUSÉS ici même, faute de matière première dans les cinq\n * écrans relevés — un système ment moins en affichant un trou qu'une valeur\n * plausible. Tranchés le 25/08/2026 (DECISIONS.md, « les manques comblés »)\n * en reprenant TELLES QUELLES les contraintes que ce module posait pour ce\n * jour-là : pas de dégradé spectre — donc un squelette en --hairline, pas\n * un balayage spectral — et une infobulle en petit panneau blanc, filet\n * --line, ombre --shadow-menu, texte --app-sm / --ink-2, la seule\n * combinaison déjà présente ailleurs dans le système. Les trois blocs\n * suivent ; ce qui restait à inventer (durées, amplitudes) est dit sur\n * place. */\n\n/* ─── Roue ──────────────────────────────────────────────────────────────\n * Pour l'attente COURTE ET LOCALE : un bouton, une cellule, un panneau qui\n * n'a pas la place d'une phrase. Elle ne remplace PAS .zv-loading : la\n * phrase qui dit le travail réel (« Recherche dans 4 812 documents… »)\n * reste le motif par défaut du système.\n *\n * Géométrie empruntée, rien d'inventé sauf la durée : 16 px (la taille de\n * .zv-icon), trait de 2 (l'épaisseur du jeu d'icônes), piste --accent-line,\n * secteur --accent. 0,8 s linéaire — une rotation qui accélère se lit comme\n * un à-coup. prefers-reduced-motion (base.css) la FIGE : l'anneau\n * interrompu reste lisible immobile, c'est pour cela que le secteur est un\n * quart et pas une moitié. */\n\n@keyframes zv-spin {\n to {\n rotate: 1turn;\n }\n}\n\n.zv-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n flex: none;\n border: 2px solid var(--accent-line);\n border-top-color: var(--accent);\n /* Rond de FORME, pas de style : c'est le cercle qui fait lire la\n * rotation — même dérogation que la pastille d'état. */\n border-radius: 9999px;\n /* `rotate` et non `transform` : la propriété indépendante compose avec un\n * éventuel transform de placement — la leçon des boucles de motion.css. */\n animation: zv-spin 0.8s linear infinite;\n}\n\n/* En tête d'un panneau ou d'un écran d'attente : la roue seule, plus\n * grande. Le trait ne grossit pas — même compensation optique que les\n * icônes. */\n.zv-spinner--lg {\n width: 24px;\n height: 24px;\n}\n\n/* ─── Squelette ─────────────────────────────────────────────────────────\n * L'aplat --hairline exigé — pas de balayage spectral, pas de dégradé de\n * gris que la charte n'a pas. La pulsation est une OPACITÉ (1 → 0,55), pas\n * une couleur : elle dit « en chargement » sans introduire de teinte.\n * 1,8 s en ease-in-out, la respiration de zv-breathe plutôt que la courbe\n * d'entrée — c'est une boucle, pas un mouvement.\n *\n * La géométrie vient du CONSOMMATEUR (largeur et hauteur en style, comme la\n * largeur de Progress) : un squelette qui invente ses dimensions ne\n * ressemble jamais au contenu qu'il annonce. Seule la variante --text fixe\n * une hauteur d'un cadratin, pour mimer une ligne du texte courant.\n *\n * ⚠️ Un squelette ANNONCE une structure : ne jamais en poser un pour un\n * contenu dont la forme est inconnue — c'est .zv-loading qui dit ce cas. */\n\n@keyframes zv-skeleton {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.55;\n }\n}\n\n.zv-skeleton {\n display: block;\n background: var(--hairline);\n animation: zv-skeleton 1.8s ease-in-out infinite;\n}\n\n.zv-skeleton--text {\n height: 1em;\n}\n\n/* ─── Infobulle ─────────────────────────────────────────────────────────\n * La combinaison imposée par l'arbitrage : panneau blanc, filet --line,\n * ombre --shadow-menu, texte --app-sm / --ink-2. Posée AU-DESSUS de la\n * cible, centrée, à 6 px — au-dessus parce que le pointeur vient d'en bas\n * et la masquerait en dessous.\n *\n * Elle se montre au survol et au focus de l'enveloppe (.zv-tooltip-hit),\n * avec un retard de 300 ms à l'APPARITION seulement — sans lui, traverser\n * une rangée d'icônes fait clignoter une infobulle par icône. La\n * disparition, elle, est immédiate.\n *\n * Le masquage est `visibility` ET opacité, à dessein (WCAG 1.4.13) :\n * · cachée en visibility:hidden, la bulle ne capte pas le pointeur — pas\n * de zone de survol fantôme au-dessus de chaque cible ;\n * · visible, elle EST survolable : descendante de l'enveloppe, le\n * pointeur peut la traverser sans la fermer ;\n * · Échap la ferme — la façade pose --off jusqu'au blur, seul\n * comportement qui demande une ligne de script.\n *\n * ⚠️ Réservée à un COMPLÉMENT dispensable (un raccourci, une date exacte,\n * une unité) : une commande n'a pas le droit de cacher son nom dans une\n * infobulle, et le tactile ne survole pas. L'annonce passe par\n * aria-describedby, que la façade React câble. */\n\n.zv-tooltip-hit {\n position: relative;\n display: inline-flex;\n}\n\n.zv-tooltip {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n /* La couche du menu (navigation.css) : une infobulle flotte au même\n * étage que lui et ne le croise jamais. */\n z-index: 60;\n width: max-content;\n max-width: 240px;\n border: 1px solid var(--line);\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 6px 10px;\n font-size: var(--app-sm);\n line-height: 1.5;\n font-weight: 500;\n color: var(--ink-2);\n text-align: left;\n opacity: 0;\n visibility: hidden;\n /* La visibilité bascule APRÈS le fondu de sortie, jamais pendant. */\n transition:\n opacity 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n visibility 0s linear 140ms;\n}\n\n.zv-tooltip-hit:hover > .zv-tooltip,\n.zv-tooltip-hit:focus-within > .zv-tooltip {\n opacity: 1;\n visibility: visible;\n transition-delay: 300ms, 300ms;\n}\n\n/* Fermée à Échap : posée par la façade, levée au blur ou à la sortie du\n * pointeur. Déclarée APRÈS l'ouverture pour l'emporter à spécificité\n * égale. */\n.zv-tooltip-hit--off > .zv-tooltip {\n opacity: 0;\n visibility: hidden;\n transition-delay: 0s, 0s;\n}\n\n/* Sous la cible, pour les rares cas où le haut est occupé (première ligne\n * d'un tableau sous son en-tête). */\n.zv-tooltip--bas {\n bottom: auto;\n top: calc(100% + 6px);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Un seul point de rupture, 768px, comme dans tous les modules du paquet.\n * Ce bloc ne contient QUE des bascules attestées dans la source (variantes\n * `lg:` de Mémoire, lues en sens inverse) — pas une seule règle de confort\n * ajoutée en chemin. */\n\n@media (max-width: 767px) {\n /* Les cinq bandeaux d'authentification portent `text-[13.5px]\n * lg:text-[12.5px]` (login-card:207, reset-password-card:78 et :137,\n * forgot-password-card:60 et :96) : le texte GROSSIT d'un palier sous le\n * seuil. C'est le motif le mieux attesté du module ; l'oublier laissait le\n * bandeau à sa plus petite taille précisément sur l'écran le plus étroit. */\n .zv-alert {\n font-size: var(--app);\n }\n\n /* Le bandeau pointillé fait l'inverse — `text-[13px]` en desktop,\n * `text-[12.5px]` sous le seuil — et resserre sa gouttière : 16px de\n * chaque côté sur 375px déjà bordés par le padding de page, c'est la\n * moitié de la largeur perdue en marges. Déclaré APRÈS .zv-alert pour\n * l'emporter à spécificité égale. */\n .zv-alert--dash {\n padding: 10px 12px;\n font-size: var(--app-sm);\n }\n\n /* Le message d'incident resserre son padding (`px-4 py-[14px]`) et GARDE\n * sa taille : la source n'a pas de variante `lg:` sur le corps. Redéclaré\n * ici uniquement pour n'être pas rétréci par la règle .zv-alert ci-dessus,\n * à spécificité égale. */\n .zv-alert--lg {\n padding: 14px 16px;\n font-size: var(--app);\n }\n\n /* C'est ici, et seulement ici, que la troncature du statut opère : le\n * `min-w-0` de la source vaut sous le seuil, et sans lui l'ellipse ne se\n * déclenche pas dans une cellule flex ou grid — le libellé pousse la\n * colonne. C'est la cause n° 1 de débordement horizontal des tableaux. */\n .zv-status {\n min-width: 0;\n }\n\n /* Même garde-fou que le menu et le popover : plafonnée à l'écran, la\n * bulle ne pousse jamais la page en défilement horizontal. Le centrage\n * sur une cible collée au bord peut encore la décaler — limite connue,\n * une variante d'ancrage latéral s'arbitrera si le cas se présente. */\n .zv-tooltip {\n max-width: calc(100vw - 40px);\n }\n\n /* ⚠️ NON RELEVÉ : aucune variante `lg:` sur le padding de l'écran vide.\n * Ce resserrement est une décision de portage, prise parce que 32px de\n * gouttière sur 375px laissent 311px de contenu à un bloc qui porte un\n * titre et un bouton. Si la DA la refuse, c'est cette règle qui saute —\n * pas le reste du bloc. */\n .zv-empty--page {\n padding: 40px 20px;\n }\n}\n\n\n/* ── navigation.css ── */\n/* Zevra UI — navigation : le chrome de l'applicatif\n * Colonne latérale, barre supérieure, tiroir, feuille de source citée, menu\n * déroulant, fil d'Ariane, pagination, mécanique de défilement — et, depuis\n * le 25/08/2026 (hors relevé, voir DECISIONS.md) : l'onglet souligné, le\n * popover et la barre d'onglets mobile.\n *\n * DÉPEND DE :\n * · tokens.css — neutres (--ink, --ink-2, --muted, --label, --line,\n * --hairline, --panel, --white), --accent / --accent-soft, échelles\n * --app-* et --mono-*, mesures --tap et --topbar-h, --shadow-menu ;\n * · base.css — reset box-sizing, :focus-visible, style des liens sans\n * classe, et la coupure globale prefers-reduced-motion (ce module ne la\n * repose donc pas) ;\n * · layout.css — l'inventaire des couches d'empilement, dont l'échelle\n * ci-dessous est le prolongement ;\n * · icons.css — .zv-icon (glyphes tracés) et .zv-icon-btn (bouton d'icône\n * nu, cible --tap), réemployés ici plutôt que redéfinis.\n * Ne repose ni le reset, ni le body, ni :root.\n *\n * NE REDÉFINIT RIEN de ce que les autres modules possèdent déjà :\n * .zv-navlink et .zv-nav__* (nav de vitrine, layout.css) ne sont pas touchés —\n * l'entrée applicative s'appelle .zv-sidelink ; le déclencheur du menu est\n * .zv-select et l'onglet en PUCES réemploie .zv-seg, tous deux dans\n * forms.css. L'onglet souligné, lui, a sa classe ici depuis l'arbitrage du\n * 25/08/2026.\n *\n * Ce module est un RELEVÉ de l'application Mémoire, en production, pas une\n * proposition de DA : chaque valeur vient de src/components/app-shell.tsx,\n * src/components/citation.tsx ou des écrans (chat, répertoire, fiche). Ce qui\n * manque à la production — focus, états désactivés, clavier — est signalé par\n * un ⚠️ à l'endroit exact plutôt que comblé en silence, et ce qui n'a AUCUNE\n * source est étiqueté HORS RELEVÉ ou DÉRIVÉ, jamais présenté comme relevé.\n *\n * ⚠️ SEUIL. Mémoire bascule à `lg` (1024px) ; le paquet n'a qu'un point de\n * rupture, 768px, et c'est celui-là qui s'applique. Ce qui est relevé, ce sont\n * les DEUX jeux de valeurs (doigt / souris), jamais la largeur où ils\n * basculent. Tout le responsive est en fin de fichier, sous la seule forme\n * employée par base.css et components.css : `max-width: 767px`. Les règles de\n * base décrivent donc l'écran large, et le bloc final ramène au doigt.\n *\n * ─── Empilement ────────────────────────────────────────────────────────\n * layout.css tient l'inventaire des couches du paquet et s'arrête à 50\n * (.zv-layer-top), avec la consigne « toute nouvelle couche s'arbitre par\n * rapport à celle-ci ». Les recouvrements applicatifs se rangent donc\n * AU-DESSUS, sans jamais se mettre à égalité avec une couche de vitrine : un\n * voile de tiroir et un header fixe départagés par l'ordre du DOM, ce n'est\n * pas une échelle. Le relevé donnait voile 40, menu 40, feuille 50, tiroir 60 ;\n * seuls les RAPPORTS comptent, et ils sont conservés — l'origine est décalée\n * en bloc pour dégager l'inventaire de vitrine :\n *\n * menu 60 · voile 60 · feuille de source 70 · tiroir 80\n *\n * L'égalité menu/voile est conservée parce qu'elle est assumée : les deux ne\n * coexistent jamais (ouvrir le tiroir ferme le menu). C'est autre chose qu'une\n * égalité subie entre deux pièces qui, elles, se croisent.\n * N'insérer aucune valeur intermédiaire sans rejuger l'échelle entière.\n *\n * ─── Gabarit attendu ───────────────────────────────────────────────────\n * Toutes ces pièces supposent une racine applicative en `height: 100dvh;\n * overflow: hidden; display: flex` — colonne sous le seuil, rangée au-dessus.\n * Sans elle, aucune zone défilante ne tient sa hauteur et c'est la page\n * entière qui défile, ce qui ruine la colonne comme la feuille. Cette racine\n * relève de la mise en page, pas de ce module (cf. notes).\n */\n\n/* ─── Colonne latérale ──────────────────────────────────────────────────\n * Une colonne, un filet, rien d'autre : ni ombre, ni rayon. Une ombre ferait\n * flotter une pièce qui ne bouge jamais, et le rayon appartient à l'action\n * (--r-btn) ou à la carte (--r-card), pas au chrome. */\n\n.zv-sidebar {\n display: flex;\n width: 224px;\n /* Sans `flex: none`, une entrée de nav un peu longue élargit la colonne et\n * décale le contenu : la largeur est une constante, pas un résultat. */\n flex: none;\n flex-direction: column;\n /* La colonne défile pour son propre compte : une nav longue ne doit jamais\n * entraîner le contenu, ni l'inverse. */\n overflow-y: auto;\n border-right: 1px solid var(--line);\n background: var(--panel);\n}\n\n.zv-sidebar__head {\n border-bottom: 1px solid var(--line);\n padding: 20px 18px 14px;\n}\n\n/* `margin-top: auto` colle le pied en bas quand la nav est courte et le laisse\n * suivre le contenu quand elle est longue. Un `position: sticky` aurait\n * recouvert la dernière entrée dès que la colonne défile. */\n.zv-sidebar__foot {\n margin-top: auto;\n border-top: 1px solid var(--line);\n}\n\n/* Le même balisage sert à l'aside et au panneau du tiroir. Dans le tiroir, la\n * croix se pose en absolu par-dessus la tête : sans cette réserve, un logo\n * client large passe dessous et devient incliquable. */\n.zv-drawer .zv-sidebar__head > :first-child {\n padding-right: 46px;\n}\n\n/* ─── Groupes de navigation ─────────────────────────────────────────────\n * Nommés .zv-sidegroup et non .zv-navgroup : layout.css possède déjà .zv-nav,\n * .zv-nav__logo, .zv-nav__links et .zv-navlink pour la nav de VITRINE. Un\n * .zv-navgroup posé à côté se lirait comme une pièce de cette famille-là. La\n * colonne applicative a sa propre lignée : sidebar › sidegroup › sidelink. */\n\n.zv-sidegroup {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 12px 10px;\n}\n\n.zv-sidegroup__title {\n padding: 2px 10px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* À partir du deuxième groupe, c'est le blanc au-dessus de l'intitulé qui\n * sépare — pas un filet. La colonne en porte déjà deux (tête et pied) ; un\n * troisième trait la transformerait en tableau. */\n.zv-sidegroup + .zv-sidegroup .zv-sidegroup__title {\n padding-top: 14px;\n}\n\n/* ─── Entrée de barre latérale ──────────────────────────────────────────\n * `.zv-navlink` appartient déjà à la navigation de vitrine (layout.css) :\n * l'entrée applicative s'appelle `.zv-sidelink`, et les deux ne doivent\n * jamais se recouvrir. */\n\n.zv-sidelink {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n font-family: var(--font-text);\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink-2);\n text-decoration: none;\n}\n\n/* ⚠️ CONTRE-ORDRE, pas un style. base.css repeint les liens nus en accent, et\n * l'application hôte va souvent plus loin (`a { color: accent }`,\n * `a:hover { text-decoration: underline }`). Sans ces deux déclarations\n * d'apparence redondante, l'entrée de nav vire au bleu souligné dès qu'elle\n * tombe dans une page qui applique le style éditorial. */\n.zv-sidelink:hover {\n background: var(--hairline);\n color: var(--ink-2);\n text-decoration: none;\n}\n\n/* `aria-current=\"page\"` est ajouté au relevé : la production ne marque\n * l'entrée courante que visuellement, ce qui la rend muette au lecteur\n * d'écran. Les deux sélecteurs valent la même chose pour que le balisage\n * correct ne coûte rien. */\n.zv-sidelink--on,\n.zv-sidelink[aria-current=\"page\"] {\n background: var(--accent-soft);\n font-weight: 600;\n color: var(--accent);\n}\n\n/* Au survol de l'entrée courante, le fond change mais le texte reste accent :\n * sinon la page où l'on se trouve « s'éteint » sous le pointeur. */\n.zv-sidelink--on:hover,\n.zv-sidelink[aria-current=\"page\"]:hover {\n background: var(--hairline);\n color: var(--accent);\n}\n\n/* Le contour de base.css déborde de 2px ; dans une colonne en `overflow-y:\n * auto` il est rogné par le bord du scroller. On le rentre plutôt que d'en\n * redessiner un autre. */\n.zv-sidelink:focus-visible {\n outline-offset: -2px;\n}\n\n/* Largeur fixe : le rang mono et le glyphe ⚙ du panneau admin doivent tomber\n * sur la même colonne, sinon les libellés se décalent d'un groupe à l'autre.\n * La production écrit 10px en dur (app-shell.tsx:160) ; c'est exactement\n * --mono, donc le token remplace la valeur sans rien déplacer. */\n.zv-sidelink__num {\n width: 16px;\n flex: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n color: var(--label);\n}\n\n/* ─── Barre supérieure ──────────────────────────────────────────────────\n * Elle remplace la colonne sous le seuil : masquée au-dessus, elle n'apparaît\n * que dans le bloc mobile de fin de fichier. */\n\n.zv-topbar {\n display: none;\n /* --topbar-h nomme la HAUTEUR DE LA BARRE, et elle seule : 52px. Le filet\n * qui la termine est un pixel de plus, et c'est pour cela que la feuille de\n * source commence à 53 (cf. .zv-cite-panel dans le bloc mobile). Les deux\n * valeurs de la production sont justes et se répondent — les unifier sur 52\n * ferait remonter la feuille par-dessus le filet.\n * ⚠️ Nuance à connaître avant d'y toucher : base.css impose box-sizing:\n * border-box, donc ce `border-bottom` est peint À L'INTÉRIEUR des 52px. Le\n * modèle « 52 + 1 » n'est littéral que si le filet sort de la boîte —\n * une ligne suffirait (`box-shadow: 0 1px 0 var(--line)` au lieu du\n * `border-bottom`), mais c'est un écart au relevé : à arbitrer, pas à\n * décider ici. */\n height: var(--topbar-h);\n flex: none;\n align-items: center;\n gap: 10px;\n border-bottom: 1px solid var(--line);\n background: var(--panel);\n padding-inline: 16px;\n}\n\n/* Le logo cède AVANT le nom : un logo client très large ne doit jamais\n * pousser la barre hors de l'écran. */\n.zv-topbar__logo {\n min-width: 0;\n overflow: hidden;\n}\n\n.zv-topbar__title {\n min-width: 0;\n flex: 1 1 0%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app-sm);\n font-weight: 600;\n color: var(--ink);\n}\n\n/* Le hamburger n'est PAS un bouton propre à ce module : c'est le .zv-icon-btn\n * d'icons.css (cible --tap, sans cadre, couleur --muted) posé dans la barre.\n * Le balisage est donc `class=\"zv-icon-btn zv-topbar__burger\"`, et seul le\n * décalage optique appartient à la barre : la marge négative aligne le GLYPHE\n * sur le bord de la barre alors que la boîte garde ses 40px de cible tactile —\n * sans elle, il faut choisir entre l'alignement optique et le doigt.\n * Le relevé ne donne AUCUN état au hamburger ; le survol d'icons.css peint\n * --panel, qui est exactement le fond de cette barre — donc invisible ici.\n * Seule la couleur du glyphe s'assombrit d'un cran, ce qui vient du composant\n * partagé et non d'une décision de ce module. */\n.zv-topbar__burger {\n margin-right: -8px;\n}\n\n/* ─── Barre d'onglets mobile ────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Mémoire navigue au tiroir ;\n * une application à trois ou quatre sections navigue plus vite par une\n * barre basse. CINQ entrées au maximum — au-delà, c'est le tiroir.\n *\n * N'existe que SOUS le point de rupture : au-dessus, la colonne latérale\n * est là. Tout le dessin vit donc dans le bloc mobile de fin de fichier ;\n * ici, seulement l'effacement desktop.\n *\n * ⚠️ Elle réserve de la place : le gabarit qui la pose doit donner au\n * contenu un padding-bottom à sa hauteur, sinon la dernière ligne de la\n * page se cache dessous. */\n\n.zv-tabbar {\n display: none;\n}\n\n/* ─── Bouton de chrome (croix bordée) ───────────────────────────────────\n * ⚠️ RENOMMÉ : ce bouton s'appelait .zv-icon-btn, nom que icons.css possède\n * déjà pour tout autre chose (bouton nu de 40px, coins arrondis). Deux\n * géométries sous un seul nom, c'est la dernière qui charge qui gagne : la\n * croix bordée du chrome devient .zv-chrome-btn.\n * Un seul emploi : la croix de fermeture du tiroir et de la feuille de source.\n * Angles vifs — le rayon signale l'action, le chrome n'en est pas une. */\n\n.zv-chrome-btn {\n display: flex;\n width: 26px;\n height: 26px;\n flex: none;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n /* base.css ne reset pas les boutons : sans police ni fond explicites, un\n * <button> retombe sur ceux du navigateur et détonne. Même précaution que\n * .zv-btn dans components.css. */\n font-family: var(--font-text);\n font-size: var(--app);\n line-height: 1;\n color: var(--muted);\n cursor: pointer;\n}\n\n/* ⚠️ Divergence relevée : la croix du tiroir n'a AUCUN état en production,\n * celle de la feuille de source porte ce survol. Retenu : le survol partout,\n * un bouton sans réaction se lit comme un décor. */\n.zv-chrome-btn:hover {\n background: var(--hairline);\n}\n\n.zv-chrome-btn:focus-visible {\n outline-offset: -2px;\n}\n\n/* ⚠️ HORS RELEVÉ : aucun bouton de chrome n'est jamais désactivé en\n * production. Posé parce qu'un bouton grisé sans style se lit comme un bouton\n * actif ; aucune couleur nouvelle n'est introduite. */\n.zv-chrome-btn[disabled] {\n color: var(--label);\n cursor: default;\n}\n\n.zv-chrome-btn[disabled]:hover {\n background: var(--white);\n}\n\n/* Glyphes relevés, à reproduire tels quels :\n * · hamburger — SVG 18×12 (viewBox \"0 0 18 12\"),\n * <path d=\"M0 1h18M0 6h18M0 11h18\" stroke=\"currentColor\" stroke-width=\"1.5\"/> :\n * trois traits pleine largeur, entraxe 5px, extrémités franches. Ce n'est\n * PAS une icône 24×24 d'une bibliothèque, et une icône ronde tirée d'un\n * jeu tiers casserait la géométrie de la barre. Il ne suit donc pas la\n * grille de 24 de icons.css : il se pose en SVG nu, pas en .zv-icon.\n * · fermeture — le caractère ✕ (U+2715) posé en texte, d'où le\n * `font-size` ci-dessus qui le dimensionne. */\n\n/* ─── Feuille de source citée ───────────────────────────────────────────\n * ⚠️ RENOMMÉE : cette pièce s'appelait .zv-sheet. layout.css possède déjà\n * .zv-sheet-section (section de PLANCHE de documentation, avec son __title et\n * son __body) : côte à côte, .zv-sheet__title et .zv-sheet-section__title\n * donnent à lire un bloc et son modificateur là où il y a deux objets sans\n * rapport. Elle prend le nom de ce qui l'ouvre — un clic sur une citation :\n * .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\n * ne se confond pas avec .zv-src (la PUCE de source, components.css).\n *\n * Colonne au-dessus du seuil, feuille quasi plein écran en dessous. Trois\n * zones : tête fixe, corps défilant, pied d'actions. */\n\n.zv-cite-panel {\n display: flex;\n height: 100dvh;\n width: 392px;\n flex: none;\n flex-direction: column;\n border-left: 1px solid var(--line);\n /* Blanc, et non --panel : c'est une surface de lecture, pas du chrome. */\n background: var(--white);\n}\n\n.zv-cite-panel__head {\n display: flex;\n /* `flex-start` et non `center` : un titre de document sur trois lignes ne\n * doit pas faire descendre la croix au milieu du bloc. */\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n border-bottom: 1px solid var(--line);\n padding: 18px 20px 14px;\n}\n\n.zv-cite-panel__title {\n font-family: var(--font-display);\n font-size: var(--app-h3);\n line-height: 1.35;\n font-weight: 500;\n /* Un nom de fichier sans espace déborderait la colonne et pousserait la\n * croix hors de l'écran. */\n overflow-wrap: break-word;\n}\n\n/* Le corps est la seule zone qui défile : le fond gris l'enfonce d'un cran\n * pour que la page citée s'y détache comme une feuille posée. */\n.zv-cite-panel__body {\n flex: 1 1 0%;\n overflow-y: auto;\n background: var(--hairline);\n padding: 18px;\n}\n\n.zv-cite-panel__foot {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border-top: 1px solid var(--line);\n padding: 14px 20px;\n}\n\n/* Fermeture par Échap, par le voile, par la croix, et automatiquement à chaque\n * navigation. AUCUNE transition d'entrée ni de sortie, contrairement au\n * tiroir : la feuille répond à un clic sur une citation, elle doit être là au\n * moment où le regard y va.\n * ⚠️ NON LIVRÉ : le kicker « SOURCE CITÉE » de la tête (citation.tsx:104) n'est\n * dans aucun inventaire, et il n'est pas tranché s'il relève de la typographie\n * (.zv-kicker, base.css) ou de ce module. Rien n'est inventé en attendant. */\n\n/* ─── Menu déroulant ────────────────────────────────────────────────────\n * Ancré sous son déclencheur (.zv-select, forms.css), qui DOIT porter\n * `position: relative`, sans quoi le menu se colle au coin de la page.\n * L'enveloppe .zv-select-anchor (forms.css) le porte déjà : c'est elle qu'on\n * pose autour d'un déclencheur .zv-select, et non le déclencheur lui-même. */\n\n.zv-menu {\n position: absolute;\n top: calc(100% + 5px);\n left: 0;\n z-index: 60;\n display: flex;\n flex-direction: column;\n min-width: 230px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 5px;\n /* Seule ombre du chrome applicatif, et elle est justifiée : un menu flotte\n * vraiment au-dessus de la page, contrairement à la colonne ou au tiroir.\n * --shadow-card est trop courte pour ce tirage, --shadow-panel trop indigo :\n * d'où --shadow-menu, qui existe pour ce seul usage. */\n box-shadow: var(--shadow-menu);\n}\n\n.zv-menu__item {\n display: block;\n /* Les quatre déclarations qui suivent existent pour que l'item tienne aussi\n * bien en <button> qu'en <div> : sans elles, le navigateur impose sa police,\n * son fond gris, son cadre et un texte centré. */\n width: 100%;\n border: 0;\n background: none;\n text-align: left;\n padding: 8px 11px;\n font-family: var(--font-text);\n font-size: var(--app-sm);\n font-weight: 400;\n color: var(--ink);\n cursor: pointer;\n}\n\n.zv-menu__item:hover {\n background: var(--hairline);\n}\n\n/* ⚠️ CONTRE-ORDRE. Une entrée de menu rendue en <a> (une navigation, par\n * opposition à une action) hérite du soulignement par défaut du navigateur,\n * là où la même entrée en <button> n'a rien : dans un même panneau,\n * « Mon compte » se soulignait et « Se déconnecter » non. Les deux doivent\n * se rendre à l'identique — c'est la BALISE qui change, pas l'apparence. */\na.zv-menu__item,\na.zv-menu__item:hover {\n text-decoration: none;\n}\n\n/* ⚠️ Option SURVOLÉE AU CLAVIER — ajoutée le 25/08/2026 avec la sélection\n * dessinée. Ce n'est ni :hover (la souris n'est pas dessus) ni :focus (le\n * focus reste sur le déclencheur, c'est aria-activedescendant qui porte\n * l'annonce). Sans cette classe, parcourir la liste aux flèches ne se voit\n * pas : le lecteur d'écran suit, l'œil non. */\n.zv-menu__item--survol {\n background: var(--hairline);\n}\n\n.zv-menu__item:focus-visible {\n outline-offset: -2px;\n}\n\n.zv-menu__item--on,\n.zv-menu__item[aria-selected=\"true\"] {\n background: var(--accent-soft);\n color: var(--accent);\n font-weight: 600;\n}\n\n/* ⚠️ Relevé tel quel : le survol ÉCRASE le fond de l'item courant\n * (accent-soft → hairline), seul le texte reste accent. Volontaire ou non,\n * c'est ce qui tourne en production — signalé plutôt que corrigé en silence. */\n.zv-menu__item--on:hover,\n.zv-menu__item[aria-selected=\"true\"]:hover {\n background: var(--hairline);\n}\n\n/* ⚠️ HORS RELEVÉ : aucun item désactivé n'existe en production. Même\n * traitement que le bouton de chrome, sans couleur nouvelle. */\n.zv-menu__item[disabled],\n.zv-menu__item[aria-disabled=\"true\"] {\n color: var(--label);\n cursor: default;\n}\n\n.zv-menu__item[disabled]:hover,\n.zv-menu__item[aria-disabled=\"true\"]:hover {\n background: none;\n}\n\n/* ⚠️ MANQUENT en production et NON comblés ici, car ce sont des comportements,\n * pas du style : navigation au clavier (flèches, Origine/Fin), fermeture au\n * clic extérieur, fermeture à Échap. Le CSS ci-dessus est prêt à les recevoir\n * (aria-selected, :focus-visible) ; le composant doit les écrire. */\n\n/* ─── Entrées à menu déroulant de la barre vitrine ──────────────────────\n *\n * ⚠️ CE BLOC DOIT RESTER APRÈS `.zv-menu`, et donc dans CE fichier. Le\n * panneau d'une entrée porte les deux classes ; `.zv-navdd__menu` reprend à\n * `.zv-menu` son display, sa position et son ancrage, à spécificité égale —\n * c'est l'ORDRE qui tranche. Posé dans layout.css, qu'index.css importe\n * AVANT navigation.css, il perdait : les trois menus d'une barre\n * s'affichaient en même temps, ouverts en permanence.\n *\n * Une nav de vitrine qui porte l'architecture d'un site — des familles,\n * pas quatre liens — a besoin de déplier. Ouverture au survol ET au focus\n * (`:focus-within`) : en CSS seul, donc accessible au clavier sans une\n * ligne de JavaScript et sans état à tenir.\n *\n * Le panneau est le .zv-menu du paquet, simplement ancré sous l'entrée. */\n\n.zv-navdd {\n position: relative;\n display: inline-flex;\n}\n\n/* Le bouton porte .zv-navlink : il doit se rendre EXACTEMENT comme un lien\n * simple de la barre. Piège à éviter — un `font: inherit`, réflexe habituel\n * pour dénuder un bouton, remet la fonte du CONTENEUR (16px/400) là où\n * .zv-navlink pose 14px/500 : les entrées à menu deviennent plus grosses\n * que les autres. On ne reprend donc du bouton que ce qui doit disparaître. */\n.zv-navdd__btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 0;\n border: 0;\n background: none;\n cursor: pointer;\n font-family: inherit;\n line-height: inherit;\n}\n\n.zv-navdd__chevron {\n width: 12px;\n height: 12px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* Bouts francs : le trait carré du jeu d'icônes (25/08/2026). */\n stroke-linecap: square;\n stroke-linejoin: miter;\n transition: transform 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-navdd:hover .zv-navdd__chevron,\n.zv-navdd:focus-within .zv-navdd__chevron {\n transform: rotate(180deg);\n}\n\n/* Le pont invisible (::before) couvre l'interstice entre l'entrée et son\n * panneau : sans lui, le survol se rompt en descendant vers le menu et le\n * menu se referme sous le curseur. */\n.zv-navdd__menu {\n position: absolute;\n top: 100%;\n left: 50%;\n translate: -50% 0;\n padding-top: 14px;\n display: none;\n min-width: 250px;\n z-index: 50;\n}\n\n.zv-navdd__menu::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 14px;\n}\n\n.zv-navdd:hover .zv-navdd__menu,\n.zv-navdd:focus-within .zv-navdd__menu {\n display: flex;\n flex-direction: column;\n}\n\n.zv-navdd__sep {\n display: block;\n height: 1px;\n margin: 6px 0;\n background: var(--hairline);\n}\n\n/* ─── Popover ───────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Le menu impose ses items ;\n * il n'existait aucune surface ancrée LIBRE — aide contextuelle,\n * mini-formulaire de filtre. Même ancre que le menu (.zv-select-anchor,\n * forms.css), même surface (filet --line, ombre --shadow-menu), padding de\n * panneau au lieu d'items. Fermeture par clic extérieur et Échap : au\n * composant, comme pour le menu.\n * ⚠️ Ce n'est PAS une infobulle : le popover s'ouvre au CLIC et peut porter\n * de l'interactif ; l'infobulle (feedback.css) se montre au survol et n'en\n * porte jamais. */\n\n.zv-popover {\n position: absolute;\n top: calc(100% + 5px);\n left: 0;\n z-index: 60;\n min-width: 230px;\n max-width: 320px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 14px 16px;\n}\n\n/* ─── Fil d'Ariane ──────────────────────────────────────────────────────*/\n\n.zv-crumbs {\n display: block;\n margin-bottom: 14px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n /* Le fil porte des noms de dossier entiers : il a le droit de passer à la\n * ligne, pas celui d'élargir la page. */\n overflow-wrap: break-word;\n color: var(--label);\n text-decoration: none;\n cursor: pointer;\n}\n\n/* Même contre-ordre que .zv-sidelink : sans lui, le fil vire au bleu souligné\n * dans toute page qui applique le style de lien éditorial. */\n.zv-crumbs:hover {\n color: var(--label);\n text-decoration: none;\n}\n\n.zv-crumbs__sep {\n margin-inline: 4px;\n}\n\n/* Le dernier segment est plus sombre que le fil : c'est lui qu'on lit, le\n * reste n'est qu'un chemin. Aucun état « segment courant » n'existe au relevé. */\n.zv-crumbs__seg {\n color: var(--ink-2);\n}\n\n/* ─── Pagination ────────────────────────────────────────────────────────\n * Deux formes relevées, et deux seulement : le rang centré d'un panneau\n * (‹ p. 12 / 31 ›) et le pied de tableau (compte à gauche, « ‹ Précédent ·\n * Suivant › » à droite).\n *\n * ⚠️ DOUBLON À TRANCHER, et il est tranché ici : data.css pose .zv-table-foot\n * pour le MÊME nœud du DOM (repertoire-client.tsx:165) avec les mêmes valeurs.\n * La pagination est de la navigation, pas de la donnée : la forme reste dans ce\n * module, sous .zv-pager--foot, et data.css doit abandonner .zv-table-foot.\n * .zv-pager--foot est un MODIFICATEUR : il se pose avec .zv-pager, jamais seul. */\n\n.zv-pager {\n display: flex;\n justify-content: center;\n gap: 14px;\n /* Décolle le rang des boutons du pied de feuille, où il est immédiatement\n * suivi de deux actions pleine largeur. */\n padding-bottom: 2px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n color: var(--muted);\n}\n\n/* `font: inherit` plutôt qu'une police redite : les trois éléments cliquables\n * de la pagination sont des <button>, et sans lui le navigateur y impose sa\n * police système. Hériter reprend exactement la mono du rang — aucune valeur\n * nouvelle n'entre par cette porte. */\n.zv-pager__arrow,\n.zv-pager__rank,\n.zv-pager__step {\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n padding: 0;\n cursor: pointer;\n}\n\n/* ⚠️ HORS RELEVÉ. La production n'a AUCUN état désactivé : les flèches sont\n * identiques en première et en dernière page. Posé ici parce qu'une flèche\n * qui ne fait rien et ne le dit pas se lit comme une panne. `pointer-events`\n * n'est volontairement pas coupé : en aria-disabled, l'élément doit rester\n * atteignable au clavier pour être annoncé. */\n.zv-pager__arrow[disabled],\n.zv-pager__arrow[aria-disabled=\"true\"] {\n color: var(--label);\n cursor: default;\n}\n\n/* Pied de tableau : le compte à gauche, un SEUL élément cliquable pour les\n * deux sens à droite — pas de numéros de page, pas de page courante. Le\n * `padding` en raccourci annule le `padding-bottom: 2px` du rang de panneau,\n * qui n'a pas de sens ici. */\n.zv-pager--foot {\n justify-content: space-between;\n align-items: center;\n gap: 0;\n padding: 12px 4px;\n color: var(--label);\n}\n\n/* ⚠️ TROU ASSUMÉ, et il dépasse le style : il n'existe nulle part de page\n * courante, de numéros, ni de sens séparés. Une pagination digne de ce nom\n * (précédent et suivant distincts, désactivables, rang courant) est une\n * DÉCISION de conception, pas un relevé — cf. notes. */\n\n/* ─── Onglets ───────────────────────────────────────────────────────────\n * ⚠️ AUCUNE MATIÈRE PREMIÈRE : pas un seul jeu d'onglets dans les écrans\n * relevés. La rangée de PUCES se construit toujours avec .zv-seg-group et\n * .zv-seg (forms.css), qui portent déjà la grammaire d'un état actif —\n * c'est le motif des FILTRES, qui restreignent une collection.\n *\n * L'onglet SOULIGNÉ, longtemps « à faire », est ARBITRÉ le 25/08/2026\n * (DECISIONS.md) : trait actif de 2 px — l'épaisseur des filets dessinés du\n * système (filet de spectre, rail d'étapes) — en --accent, la couleur de\n * l'action, comme l'entrée de nav courante. PAS le spectre : un quatrième\n * emploi n'existe pas. C'est le motif des VUES d'un même objet (Aperçu /\n * Sources) ; pour filtrer, prendre les puces. */\n\n.zv-tabs-line {\n display: flex;\n gap: 22px;\n /* La rangée ne s'enroule pas — des onglets sur deux lignes ne se lisent\n * plus comme des onglets : elle déborde dans son propre cadre, jamais\n * dans la page. */\n overflow-x: auto;\n /* La ligne de la rangée est une ombre INTERNE, pas une bordure : les\n * onglets (étirés à la hauteur de la rangée) peignent leur trait\n * par-dessus. Le montage classique — bordure de rangée + marge basse de\n * -1px sur l'onglet — fait déborder l'onglet d'un pixel dans un conteneur\n * en overflow, et cet unique pixel suffit à faire naître un ascenseur\n * vertical. */\n box-shadow: inset 0 -1px 0 var(--line);\n}\n\n/* L'onglet se pose SUR la ligne : étiré à la hauteur de la rangée (le\n * défaut flex), son trait de 2 px partage son bord bas avec elle. Inactif,\n * le trait est transparent et laisse voir la ligne ; actif, il la couvre. */\n.zv-tabs-line__tab {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n flex: none;\n border: 0;\n border-bottom: 2px solid transparent;\n background: none;\n padding: 0 2px 9px;\n font-family: var(--font-text);\n font-size: var(--app);\n font-weight: 500;\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n transition:\n color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-tabs-line__tab:hover {\n color: var(--ink);\n}\n\n/* Le contour de base.css serait rogné par l'overflow de la rangée : il\n * rentre, comme dans la colonne latérale. */\n.zv-tabs-line__tab:focus-visible {\n outline-offset: -2px;\n}\n\n/* La grammaire d'état de .zv-seg : 500 → 600, la graisse distingue l'actif\n * avant la couleur. Le sélecteur suit l'attribut autant que la classe pour\n * que le balisage tablist correct ne coûte rien. */\n.zv-tabs-line__tab--on,\n.zv-tabs-line__tab[aria-selected='true'] {\n border-bottom-color: var(--accent);\n color: var(--accent);\n font-weight: 600;\n}\n\n/* Au survol de l'onglet actif, le trait reste : la vue où l'on se trouve ne\n * s'éteint pas sous le pointeur — même règle que l'entrée de nav courante. */\n.zv-tabs-line__tab--on:hover,\n.zv-tabs-line__tab[aria-selected='true']:hover {\n color: var(--accent);\n}\n\n/* Le compte de l'onglet — mêmes mesures que celui de la puce (.zv-seg__count,\n * forms.css) mais SANS sa marge : ici c'est la gouttière de l'onglet qui\n * espace, et réemployer la classe de la puce doublait l'écart (6 + 7 px). */\n.zv-tabs-line__count {\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n}\n\n/* ─── Défilement ────────────────────────────────────────────────────────\n * La mécanique, elle, est relevée et livrée : `min-height: 0` ET\n * `min-width: 0`. Sans ces deux-là, un enfant flex refuse de rétrécir en\n * dessous de son contenu — la zone déborde, et c'est la page entière qui part\n * en défilement horizontal. C'est la faute la plus fréquente de ce gabarit. */\n\n.zv-scroll-y {\n min-height: 0;\n min-width: 0;\n flex: 1 1 0%;\n overflow-y: auto;\n}\n\n/* ⚠️ HABILLAGE DES BARRES : trou franc, vérifié deux fois. `grep -rn scrollbar`\n * sur tout le source de Mémoire ne renvoie AUCUNE occurrence — les cinq zones\n * défilantes utilisent la barre native du système. Il n'existe donc pas de\n * valeur relevée pour la largeur, le pouce ou le rail, et ce module n'en\n * applique aucune par défaut.\n * L'habillage ci-dessous est OPT-IN et n'emploie que des mots-clés plus un\n * token existant : aucune dimension inventée. ::-webkit-scrollbar est écarté\n * pour deux raisons — il exigerait une largeur en pixels sortie de nulle part,\n * et il remplace les barres flottantes de macOS/iOS par des barres permanentes\n * qui mangent la colonne. */\n.zv-scrollbar {\n scrollbar-width: thin;\n scrollbar-color: var(--line) transparent;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Point de rupture unique du paquet. Les deux jeux de valeurs sont relevés\n * (Mémoire les fait basculer à 1024px) ; seul le seuil est une convention.\n * Sous ce seuil, tout ce qui se touche fait au moins --tap. */\n\n@media (max-width: 767px) {\n\n /* Le fil ENTIER est un seul lien, et il n'avait aucune hauteur de cible :\n * à 11px de texte il passe sous le minimum tactile. On déploie la cible\n * sans toucher au corps. */\n .zv-crumbs {\n display: inline-flex;\n align-items: center;\n min-height: var(--tap);\n }\n /* 224px de colonne à côté du contenu sur un écran de 375px ne laisse rien à\n * lire : la colonne cède la place au couple barre + tiroir. */\n .zv-sidebar {\n display: none;\n }\n\n .zv-topbar {\n display: flex;\n }\n\n .zv-sidelink {\n min-height: var(--tap);\n padding-block: 11px;\n font-size: var(--app-lg);\n }\n\n .zv-chrome-btn {\n width: var(--tap);\n height: var(--tap);\n }\n\n /* DÉRIVÉ, pas relevé, et c'est une conséquence de la ligne au-dessus : la\n * croix passe de 26 à 40px et son bord gauche remonte à 48px du bord droit,\n * au-delà de la réserve de 46px de la tête. 40 (bouton) + 8 (son décalage)\n * + 8 (garde) : le logo cesse de passer dessous. */\n .zv-drawer .zv-sidebar__head > :first-child {\n padding-right: calc(var(--tap) + 16px);\n }\n\n /* La feuille se cale À RAS sous la barre.\n *\n * La production pose top: 53px pour une barre de 52 (citation.tsx:101\n * contre app-shell.tsx:268), en supposant que le filet s'ajoute à la\n * hauteur. Il ne s'y ajoute pas : box-sizing: border-box le peint À\n * L'INTÉRIEUR des 52px. Ce pixel n'empile donc rien, il ouvre une couture\n * par laquelle le fond de page transparaît — et la feuille portant en\n * plus son propre filet haut, le trait apparaissait doublé.\n *\n * Corrigé ici : la feuille commence à 52, et n'a pas de filet propre —\n * celui de la barre les sépare déjà. C'est un écart ASSUMÉ au relevé, sur\n * un artefact et non sur une intention. */\n .zv-cite-panel {\n position: fixed;\n inset-inline: 0;\n top: var(--topbar-h);\n bottom: 0;\n z-index: 70;\n height: auto;\n width: auto;\n border-left: 0;\n border-top: 0;\n }\n\n .zv-cite-panel__body {\n padding: 12px;\n }\n\n /* Le menu s'ouvre sous un déclencheur qui peut être près du bord droit :\n * le plafond en vw l'empêche de sortir de l'écran et d'emporter la page. */\n .zv-menu {\n max-width: calc(100vw - 40px);\n }\n\n /* Même garde-fou pour le popover, qui partage l'ancre du menu. */\n .zv-popover {\n max-width: calc(100vw - 40px);\n }\n\n /* L'onglet souligné devient une cible tactile ; le trait descend avec le\n * padding, la géométrie ne change pas. */\n .zv-tabs-line__tab {\n min-height: var(--tap);\n padding-bottom: 12px;\n }\n\n /* ─── Barre d'onglets mobile — le dessin ──────────────────────────────\n * z-index 50 : le SOMMET des couches de page (l'étage de .zv-layer-top,\n * layout.css), et SOUS tous les recouvrements applicatifs (60 et plus) —\n * le voile doit voiler la barre, le tiroir passer devant. C'est du chrome\n * permanent, pas un enrobage : il ne monte pas dans leur échelle. */\n .zv-tabbar {\n position: fixed;\n inset-inline: 0;\n bottom: 0;\n z-index: 50;\n display: flex;\n border-top: 1px solid var(--line);\n background: var(--panel);\n /* L'encoche basse d'iOS : sans cette réserve, la rangée tombe sous la\n * barre système du téléphone. Mot-clé d'environnement, pas une mesure\n * inventée. */\n padding-bottom: env(safe-area-inset-bottom);\n }\n\n .zv-tabbar__item {\n flex: 1 1 0%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 3px;\n min-height: calc(var(--tap) + 8px);\n padding: 6px 4px;\n border: 0;\n background: none;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--muted);\n text-decoration: none;\n cursor: pointer;\n }\n\n /* Même contre-ordre que .zv-sidelink : sans lui, l'entrée vire au bleu\n * souligné dans une page qui applique le style de lien éditorial. */\n .zv-tabbar__item:hover {\n color: var(--muted);\n text-decoration: none;\n }\n\n .zv-tabbar__item:focus-visible {\n outline-offset: -2px;\n }\n\n /* aria-current=\"page\" est le marquage attendu ; la classe existe pour que\n * l'état se pose aussi sans routeur. */\n .zv-tabbar__item--on,\n .zv-tabbar__item[aria-current='page'] {\n color: var(--accent);\n }\n\n .zv-tabbar__item--on:hover,\n .zv-tabbar__item[aria-current='page']:hover {\n color: var(--accent);\n }\n\n .zv-menu__item {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n }\n\n /* Les chevrons sont des glyphes d'une dizaine de pixels : sans ce retrait,\n * la cible reste celle du caractère, impossible à viser au doigt. */\n .zv-pager__arrow {\n padding: 8px;\n }\n\n /* La cible tactile, que le padding seul ne donne pas : un chevron de dix\n * pixels entouré de huit reste sous --tap. `.zv-pager__step` la portait\n * déjà ; ses deux voisines l'attendaient. */\n .zv-pager__arrow,\n .zv-pager__rank {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: var(--tap);\n }\n\n .zv-pager__rank {\n padding-block: 8px;\n }\n\n .zv-pager--foot {\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n }\n\n .zv-pager__step {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n }\n}\n\n/* ─── Lanceur d'applications, et zone de compte ─────────────────────────\n *\n * ⚠️ CE BLOC AUSSI DOIT RESTER APRÈS `.zv-menu`, pour la même raison que\n * `.zv-navdd` : les deux panneaux portent `.zv-menu` et lui reprennent son\n * display et son ancrage à spécificité égale — c'est l'ORDRE qui tranche.\n *\n * Les deux vivent À DROITE de la barre, côte à côte : le lanceur pour passer\n * d'un produit à l'autre, le compte pour savoir qui l'on est. C'est la\n * convention que tout le monde connaît, et ce n'est pas le moment d'en\n * inventer une autre.\n *\n * ─── L'ÉTAT ────────────────────────────────────────────────────────────\n * Ouverts au survol ET au focus (`:focus-within`), en CSS seul : la barre\n * reste rendue au serveur, sans une ligne à hydrater — même mécanique que\n * `.zv-navdd`. Une app qui préfère l'ouverture au CLIC (la convention des\n * lanceurs) pose `.zv-lanceur--ouvert` / `.zv-compte--ouvert` : la prop\n * `ouvert` du composant les rend. C'est la règle du dépôt — un état se dit\n * en prop, il ne se peint pas depuis l'app.\n */\n\n.zv-lanceur,\n.zv-compte {\n position: relative;\n display: inline-flex;\n flex: none;\n}\n\n/* Le déclencheur du lanceur : la cible tactile du paquet, sans cadre — le\n * chrome n'est pas une action. */\n.zv-lanceur__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tap);\n height: var(--tap);\n padding: 0;\n border: 0;\n background: none;\n color: var(--ink-2);\n cursor: pointer;\n transition: color 140ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-lanceur:hover .zv-lanceur__btn,\n.zv-lanceur:focus-within .zv-lanceur__btn {\n color: var(--accent);\n}\n\n/* Le déclencheur du compte : la pastille d'initiales, cliquable. Elle garde\n * son carré — le rond ici serait un usage de réseau social, et la pastille\n * d'initiales du paquet est carrée depuis le premier jour (data.css). */\n.zv-compte__btn {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 0;\n border: 0;\n background: none;\n font-family: inherit;\n cursor: pointer;\n}\n\n/* Le nom, à côté de la pastille. Il tombe sous le point de rupture : une\n * barre de téléphone n'a pas la place d'un nom, et la pastille suffit. */\n.zv-compte__nom {\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink-2);\n white-space: nowrap;\n}\n\n/* ─── Les deux panneaux ─────────────────────────────────────────────────\n * Ancrés à DROITE et non centrés : ils vivent au bord droit de la fenêtre,\n * et un panneau centré sur son bouton en sortirait. Le pont invisible\n * (::before) couvre l'interstice, sans quoi le survol se rompt en descendant\n * vers le panneau et celui-ci se referme sous le curseur. */\n\n.zv-lanceur__panneau,\n.zv-compte__panneau {\n position: absolute;\n top: 100%;\n right: 0;\n left: auto;\n translate: none;\n padding-top: 12px;\n display: none;\n z-index: 60;\n}\n\n/* ⚠️ Le `.zv-menu` intérieur est LUI-MÊME en position absolue (il est fait\n * pour s'ancrer seul sous un déclencheur). Imbriqué dans un panneau qui\n * porte déjà l'ancrage, il se positionnait une SECONDE fois — à partir du\n * bord droit du panneau, donc vers la droite, et la grille sortait de\n * l'écran sur téléphone. Sur grand écran la place restante le masquait :\n * c'est le rendu à 390px qui l'a montré. Ici le panneau ancre, le menu ne\n * fournit plus que sa surface (filet, fond, ombre). */\n.zv-lanceur__panneau .zv-menu,\n.zv-compte__panneau .zv-menu {\n position: static;\n}\n\n.zv-lanceur__panneau::before,\n.zv-compte__panneau::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 12px;\n}\n\n.zv-lanceur:hover .zv-lanceur__panneau,\n.zv-lanceur:focus-within .zv-lanceur__panneau,\n.zv-lanceur--ouvert .zv-lanceur__panneau,\n.zv-compte:hover .zv-compte__panneau,\n.zv-compte:focus-within .zv-compte__panneau,\n.zv-compte--ouvert .zv-compte__panneau {\n display: flex;\n flex-direction: column;\n}\n\n/* ─── La grille d'applications ──────────────────────────────────────────\n * Trois par rangée : au-delà, les noms se coupent ; en deçà, le panneau\n * s'allonge en colonne et cesse de se lire d'un coup d'œil. */\n\n.zv-lanceur__apps {\n display: grid;\n grid-template-columns: repeat(3, 92px);\n gap: 4px;\n padding: 6px;\n}\n\n.zv-lanceur__app {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n padding: 14px 6px;\n text-align: center;\n text-decoration: none;\n color: var(--ink-2);\n}\n\n/* Même contre-ordre que .zv-sidelink : sans lui, la tuile vire au bleu\n * souligné dans une page qui applique le style de lien éditorial. */\n.zv-lanceur__app:hover {\n background: var(--hairline);\n color: var(--ink-2);\n text-decoration: none;\n}\n\n.zv-lanceur__app:focus-visible {\n outline-offset: -2px;\n}\n\n/* L'application COURANTE reste marquée : sans elle, on ne sait pas d'où\n * l'on part. aria-current=\"page\" est le marquage attendu ; la classe existe\n * pour que l'état se pose aussi sans routeur. */\n.zv-lanceur__app--on,\n.zv-lanceur__app[aria-current='page'] {\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n/* Le glyphe du produit. 28px : la taille de .zv-icon--xl, pour qu'une tuile\n * à image et une tuile à icône tracée se posent sur la même ligne. */\n.zv-lanceur__glyphe {\n display: block;\n width: 28px;\n height: 28px;\n object-fit: contain;\n}\n\n.zv-lanceur__nom {\n font-size: var(--app-sm);\n font-weight: 500;\n line-height: 1.3;\n /* Un nom de produit trop long ne doit pas élargir la colonne : il se coupe\n * plutôt que de déformer la grille. */\n overflow-wrap: anywhere;\n}\n\n/* ─── L'en-tête du panneau de compte ────────────────────────────────────\n * Qui l'on est, avant ce qu'on peut faire. Non connecté, il porte l'invite\n * de connexion : la zone de compte existe TOUJOURS, connecté ou non — une\n * barre où l'on ne peut pas se connecter est une barre où l'on ne revient\n * pas. */\n\n.zv-compte__tete {\n display: flex;\n flex-direction: column;\n gap: 2px;\n border-bottom: 1px solid var(--hairline);\n padding: 12px 14px;\n margin-bottom: 4px;\n}\n\n.zv-compte__tete-nom {\n font-size: var(--app);\n font-weight: 600;\n color: var(--ink);\n}\n\n.zv-compte__tete-mail {\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n overflow-wrap: anywhere;\n}\n\n.zv-compte__panneau .zv-menu {\n min-width: 240px;\n}\n\n/* Le pied du panneau : la déconnexion, séparée de ce qui précède. Elle ne\n * prend PAS l'encre d'erreur — se déconnecter n'est pas un incident. */\n.zv-compte__sortie {\n border-top: 1px solid var(--hairline);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n@media (max-width: 767px) {\n /* Le nom tombe : la pastille suffit à dire qui l'on est, et la barre n'a\n * plus la place. */\n .zv-compte__nom {\n display: none;\n }\n\n /* Les deux panneaux sont plafonnés à l'écran, comme le menu et le\n * popover : au bord droit d'un téléphone, un panneau de 300px pousserait\n * la page en défilement horizontal. */\n .zv-lanceur__panneau .zv-menu,\n .zv-compte__panneau .zv-menu {\n max-width: calc(100vw - 32px);\n }\n\n .zv-lanceur__apps {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n\n/* ── data.css ── */\n/* Zevra UI — données : tableaux, listes, cellules, métadonnées, citations\n *\n * CE QUE PORTE LE MODULE\n * le cadre de tableau et sa grille (.zv-table, .zv-table__head, .zv-row),\n * les trois registres de cellule, le dégroupage mobile (.zv-unwrap), la\n * liste sans colonnes, la métadonnée mono, le couple clé-valeur, la\n * pastille d'initiales, les deux utilitaires de troncature, l'étiquette de\n * catégorie, la puce de citation et la vignette de format — et, depuis le\n * 25/08/2026 (hors relevé, voir DECISIONS.md) : la rangée dépliante, la\n * touche de clavier et le code, en ligne comme en bloc.\n *\n * DÉPENDANCES\n * tokens.css — pour les seules variables qui y sont déclarées ;\n * base.css — reset (box-sizing), body, et le :focus-visible global, dont\n * ce module ne redéfinit ni la couleur ni l'épaisseur.\n * Ni le reset, ni body, ni :root ne sont reposés ici.\n *\n * CE QUE LE MODULE NE PORTE PAS\n * la pagination (.zv-pager--foot, navigation.css) : le pied de tableau et\n * le pied de pagination sont LE MÊME nœud dans la source (un seul\n * conteneur flex, compteur à gauche, flèches à droite). La version de ce\n * module a été supprimée, elle faisait doublon — les emboîter donnait deux\n * space-between imbriqués et 24px de padding vertical.\n * Aucun contrôle de formulaire non plus : [disabled] et [aria-invalid]\n * n'ont pas d'objet ici, ils vivent dans forms.css.\n *\n * ÉCHELLES — comment lire les tailles ci-dessous\n * La source emploie des demi-pixels (9,5 / 10,5 / 11,5) que le paquet ne\n * nomme pas. Deux rabattements, appliqués systématiquement :\n * — registre TEXTE : plancher --app-sm (12px). Les paliers 10,5 et 11,5\n * relevés y remontent ; c'est une décision, pas un relevé.\n * — micro-labels et glyphes (label de colonne, initiales, vignette) :\n * l'échelle mono (--mono-lg 11 / --mono 10 / --mono-sm 9), employée là\n * comme MESURE, la famille restant celle que la règle déclare.\n * ⚠️ Toutes les tailles et tous les interlettrages mono de la source ont\n * été mesurés sur Menlo (chasse fixe). Le paquet rend la mono en Space\n * Grotesk, de largeur et de hauteur d'x différentes : ces valeurs ne sont\n * donc plus des relevés, ce sont des transpositions à revérifier à l'œil.\n *\n * ÉTATS\n * Le seul état interactif de la source est le survol (.zv-row, .zv-cite).\n * Ni sélection, ni ligne active, ni tri : c'est dit à chaque endroit\n * concerné plutôt que comblé par une valeur inventée.\n */\n\n/* ─── Cadre de tableau ──────────────────────────────────────────────────\n * Le tableau scrolle DANS son cadre : sans ce confinement, une colonne trop\n * large fait scroller la page entière et le rail de navigation part avec.\n * Angle vif et sans ombre : un tableau est une surface de travail, pas une\n * carte éditoriale. Aucun rayon n'est posé — la valeur par défaut est déjà 0,\n * il n'y a rien à annuler tant qu'on ne compose pas .zv-table avec .zv-card\n * (ce que ce module déconseille : ce sont deux intentions différentes). */\n\n.zv-table {\n overflow-x: auto;\n border: 1px solid var(--line);\n background: var(--white);\n\n /* --zv-cols est un PARAMÈTRE de grille, pas un token de marque : il n'a\n * rien à faire dans tokens.css et n'y est pas. Il est déclaré ici, sur la\n * classe qui l'expose, avec une valeur de repli ; l'appelant l'écrase en\n * style inline sur ce même élément (le style inline l'emporte), et les\n * enfants en héritent. Une seule déclaration pour l'en-tête ET les lignes :\n * recopié aux deux endroits, on l'oublie à l'un des deux et les intitulés\n * se décalent des valeurs sans que rien ne casse visiblement.\n *\n * Repli à une colonne souple bornée à 0 : sans gabarit, la rangée se\n * dégrade en pile dont les cellules tronquent, au lieu de pousser le cadre.\n *\n * Gabarits relevés, à reprendre tels quels :\n * bibliothèque : 26px minmax(150px, 1fr) 78px 92px 170px 128px\n * répertoire : minmax(190px, 1.4fr) 108px 118px minmax(150px, 1fr) 74px 96px\n * Les colonnes souples sont en minmax(0|150px, …) : sans borne basse à 0 ou\n * sans min-width sur la cellule, la troncature ne se déclenche jamais. */\n --zv-cols: minmax(0, 1fr);\n}\n\n.zv-table__head {\n display: grid;\n /* Le repli est répété dans le var() : .zv-table__head et .zv-row servent\n * aussi hors de .zv-table (rangée seule dans une carte), où rien ne\n * déclarerait le paramètre. */\n grid-template-columns: var(--zv-cols, minmax(0, 1fr));\n align-items: center;\n gap: 10px;\n /* Filet plein (--line) contre le cheveu (--hairline) des lignes : c'est le\n * seul repère qui sépare les intitulés des données, il doit être plus\n * marqué qu'une séparation de lignes. */\n border-bottom: 1px solid var(--line);\n background: var(--panel);\n padding: 9px 14px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n /* 0,14em et non les 0,2em de .zv-mono : c'est l'interlettrage que le paquet\n * emploie déjà pour les capitales denses (.zv-tag, .zv-src). À 0,2em, six\n * intitulés de colonne ne tiennent plus dans leurs pistes. */\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* En-tête d'un tableau posé DANS une carte : la carte porte déjà son filet,\n * un second trait au-dessus ferait doublon. */\n.zv-table__head--inset {\n border-bottom: 0;\n padding: 7px 10px;\n}\n\n/* ⚠️ Aucun tri : les intitulés sont inertes, sans indicateur ni gestionnaire.\n * Rien dans la source ne dit ce que trierait un clic ni comment se marque la\n * colonne active — un en-tête triable serait une décision, pas un style. */\n\n/* ─── Ligne de données ─────────────────────────────────────────────────\n * Une seule écriture de balisage pour deux mises en page : grille au-dessus\n * du seuil, carte empilée en dessous (voir .zv-unwrap et le bloc responsive).\n * Aucun zébrage nulle part dans la source — le survol est le seul repère de\n * ligne, ce qui laisse le fond blanc porter la lecture. */\n\n.zv-row {\n display: grid;\n grid-template-columns: var(--zv-cols, minmax(0, 1fr));\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--hairline);\n text-decoration: none;\n cursor: pointer;\n}\n\n/* La dernière ligne colle au cadre : son filet doublerait le trait de bordure\n * et épaissirait le bas du tableau. C'est déjà ce que fait la source côté\n * bibliothèque et côté import (`last:border-b-0`) ; seul le répertoire garde\n * le filet. Le module tranche pour le retrait — ce n'est donc pas une\n * correction contre la production, c'est l'alignement sur sa majorité. */\n.zv-row:last-child {\n border-bottom: 0;\n}\n\n.zv-row:hover {\n /* --panel sur --white : l'écart est faible (2 %), c'est le seul fond de\n * survol dont dispose un système clair sans introduire un troisième gris.\n * Ne pas y accrocher un signal qui doit être vu de loin. */\n background: var(--panel);\n /* La ligne est un lien : sans cette annulation, le survol souligne d'un\n * coup tout le contenu de la rangée. */\n text-decoration: none;\n}\n\n/* Le cadre du tableau est en overflow auto : un contour de focus posé à\n * l'extérieur de la ligne y serait rogné sur les bords. On le rentre.\n * (La couleur et l'épaisseur restent celles de base.css.) */\n.zv-row:focus-visible {\n outline-offset: -2px;\n}\n\n/* ⚠️ Ni état sélectionné, ni :focus-within, ni ligne active : la source n'en\n * contient aucun, et les inventer ferait apparaître un fond de plus dans un\n * système qui n'en a que deux (blanc, survol). */\n\n/* ─── En-tête triable ──────────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ — ajouté le 25/08/2026 avec l'adaptateur TanStack Table.\n * Aucun tableau de la production n'est triable ; l'intitulé y est du texte\n * inerte. Un en-tête qui trie DOIT être un <button> — c'est la seule façon\n * qu'il soit atteignable au clavier et annoncé comme actionnable.\n *\n * `font: inherit` plutôt qu'une police redite : l'en-tête a déjà la sienne\n * (mono, capitales, interlettrée), et un <button> retomberait sur celle du\n * navigateur. Hériter ne fait entrer aucune valeur nouvelle. */\n\n.zv-sort {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n border: 0;\n background: none;\n padding: 0;\n font: inherit;\n letter-spacing: inherit;\n text-transform: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n.zv-sort:hover {\n color: var(--accent);\n}\n\n.zv-sort:focus-visible {\n /* L'en-tête colle au bord du cadre : l'anneau de base.css y serait rogné. */\n outline-offset: -2px;\n}\n\n/* La marque de tri reste TOUJOURS posée, même sur une colonne non triée\n * (elle vaut alors ↕) : si elle n'apparaissait qu'au tri, la largeur de la\n * colonne changerait au clic et toute la ligne d'en-tête danserait. */\n.zv-sort__mark {\n font-size: var(--mono-sm);\n color: var(--label);\n}\n\n.zv-sort:hover .zv-sort__mark {\n color: var(--accent);\n}\n\n/* ─── Cellules ─────────────────────────────────────────────────────────\n * Trois registres seulement : le texte courant, le nombre, et l'intitulé qui\n * remplace l'en-tête quand celui-ci disparaît. */\n\n.zv-cell {\n /* Sans min-width: 0, une cellule de grille ne descend pas sous la largeur\n * de son contenu : la troncature ne se déclenche pas et la ligne pousse le\n * tableau au lieu de couper. C'est la panne la plus fréquente ici. */\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app-sm);\n color: var(--ink-2);\n}\n\n/* Cellule d'identité (nom de document, nom d'entité) : c'est elle qu'on lit\n * en premier, elle monte d'un cran en taille, en graisse et en contraste.\n * Sans cette variante, .zv-cell rendrait le titre au même poids que ses\n * métadonnées et la ligne perdrait son point d'entrée. */\n.zv-cell--name {\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink);\n}\n\n/* Nombres et dates, alignés à droite.\n *\n * ⚠️ CORRECTION DE FOND. La version précédente comptait sur --font-mono pour\n * aligner les colonnes de chiffres. Dans la source, --font-mono valait\n * `ui-monospace, Menlo` — une vraie chasse fixe. Dans CE paquet, --font-mono\n * vaut « Space Grotesk », une grotesque PROPORTIONNELLE : le 1 y est plus\n * étroit que le 8, les unités ne tombent pas les unes sous les autres et\n * l'alignement annoncé n'existe pas.\n *\n * Ce qui aligne réellement, c'est font-variant-numeric: tabular-nums : la\n * fonte sert alors ses chiffres à chasse égale, quelle que soit sa famille.\n * La famille mono est conservée ici pour le REGISTRE (valeur technique), pas\n * pour l'alignement — les deux ne sont plus confondus. */\n.zv-cell--num {\n /* Sans effet dans la grille ; utile sous 768px, où la cellule se retrouve\n * enfant d'un .zv-unwrap devenu flex et ne doit pas se laisser comprimer. */\n flex: none;\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n color: var(--muted);\n}\n\n/* Intitulé porté par la valeur : quand la ligne devient une carte, l'en-tête\n * de colonnes n'existe plus et « 12 » ne veut plus rien dire. Il disparaît\n * dès que la colonne reprend son titre (bloc responsive). */\n.zv-cell__label {\n display: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* Même logique pour l'unité en clair — « 42 docs » en carte, « 42 » en\n * tableau. Reste INLINE : elle appartient à la valeur, pas à sa ligne. */\n.zv-cell__unit {\n display: none;\n}\n\n/* ─── Dégroupage ───────────────────────────────────────────────────────\n * La primitive structurelle du module : un groupe qui rassemble deux ou trois\n * cellules pour l'empilement mobile, puis s'efface (display: contents) et\n * rend ses enfants DIRECTEMENT à la grille du tableau. C'est ce qui permet\n * d'écrire le balisage une seule fois pour deux mises en page — sans lui, il\n * faudrait dupliquer la rangée et les deux copies divergeraient.\n *\n * Quatre enveloppes de ce type dans la source, sur trois écrans indépendants :\n * c'est une primitive, pas une astuce locale.\n *\n * ⚠️ Ce que .zv-unwrap NE fait PAS : la fiche client emploie le mouvement\n * inverse (`contents` sur mobile, flex au-dessus du seuil) pour défaire ses\n * colonnes en pile ordonnée. Cette classe-ci pose `contents` en base : elle\n * ne peut pas exprimer ce cas, et le prétendre serait faux. */\n\n.zv-unwrap {\n display: contents;\n}\n\n/* ─── Liste sans colonnes ──────────────────────────────────────────────\n * Même cadre que le tableau, mais rien à aligner : pas d'en-tête, pas de\n * gabarit. Sert dès que les rangées n'ont pas de colonnes comparables. */\n\n.zv-list {\n border: 1px solid var(--line);\n background: var(--white);\n}\n\n.zv-list__row {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 13px 18px;\n border-bottom: 1px solid var(--hairline);\n}\n\n.zv-list__row:last-child {\n border-bottom: 0;\n}\n\n/* Rangée « envoi en cours ».\n * ⚠️ La source porte ici un papier CHAUD (#FDFCF8) : une température, pas un\n * accent. Le paquet n'a pas de fond chaud, et le plus proche (--panel) est à\n * 2 % du blanc de la liste : ce fond est, en pratique, INVISIBLE. Il est\n * conservé comme rappel discret, mais le signal « quelque chose est en train\n * de se passer » doit être porté par la jauge (.zv-progress) ou par le\n * libellé de la rangée — surtout pas par ce fond. */\n.zv-list__row--upload {\n background: var(--panel);\n}\n\n.zv-list__main {\n /* Colonne, et non bloc : le nom surmonte sa métadonnée. Sans direction\n * explicite, les deux se suivaient sur une même ligne et la ligne de\n * liste se lisait comme une phrase. */\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 0%;\n}\n\n.zv-list__name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app);\n font-weight: 500;\n}\n\n/* ⚠️ Aucun état interactif dans la source : ni survol de rangée, ni sélection,\n * ni focus. Une liste de fichiers n'y est pas cliquable — si elle le devient,\n * c'est .zv-row qu'il faut employer, pas cette rangée-ci. */\n\n/* ─── Métadonnée mono ──────────────────────────────────────────────────\n * Second registre mono du système, en BAS DE CASSE : taille de fichier, date,\n * horodatage, compteur, latence. Ni capitales ni interlettrage — c'est\n * exactement ce qui le distingue de .zv-tag et .zv-mono (capitales,\n * 0,14–0,2em) : une date ou un nom de fichier passés en capitales espacées\n * deviennent illisibles et s'alignent mal.\n *\n * tabular-nums pour la même raison qu'en .zv-cell--num : Space Grotesk est\n * proportionnelle, et une colonne d'horodatages y danse d'une ligne à\n * l'autre sans cette propriété. */\n\n.zv-meta {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n color: var(--muted);\n}\n\n/* La source échelonne 10,5 / 10 / 11px. Les deux premiers se rabattent sur le\n * même palier du paquet (--mono, 10px) : la variante basse est re-fondée sur\n * --mono-sm (9px, plancher du paquet) pour garder un écart perceptible.\n * C'est une décision assumée, pas une mesure. */\n.zv-meta--sm {\n font-size: var(--mono-sm);\n}\n\n.zv-meta--md {\n font-size: var(--mono-lg);\n}\n\n/* Métadonnée de second plan (taille d'un fichier en cours d'envoi, compteur\n * d'en-tête) : elle recule d'un cran pour ne pas concurrencer le nom. */\n.zv-meta--quiet {\n color: var(--label);\n}\n\n/* Plusieurs métadonnées sur une même ligne : le retour à la ligne est autorisé\n * dès l'origine, une date et une taille ne tiennent pas côte à côte à 320px. */\n.zv-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: 10px;\n row-gap: 4px;\n}\n\n/* ─── Clé-valeur ───────────────────────────────────────────────────────\n * ⚠️ RÔLES REMIS DANS LE SENS DE LA SOURCE. La version précédente mettait la\n * mono à droite et la césure à gauche ; la fiche client fait l'inverse : la\n * CLÉ est en mono, en accent, et se coupe ; la VALEUR est en texte courant,\n * en gris de label, et ne se coupe jamais. Inverser les deux change ce que\n * l'œil balaie en premier dans une colonne de métadonnées. */\n\n.zv-kv {\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-size: var(--app-sm);\n font-weight: 500;\n color: var(--ink-2);\n}\n\n.zv-kv__row {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n}\n\n.zv-kv__key {\n /* Une clé longue doit se couper plutôt que pousser la valeur hors de la\n * carte : ici on préfère la césure à la troncature, un libellé amputé de sa\n * fin ne se devine pas. */\n min-width: 0;\n overflow-wrap: break-word;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n /* L'accent, parce que la clé est le point d'entrée de la ligne. Les alias\n * sémantiques de citation (--cite-ink & co.) n'existent pas dans ce paquet :\n * c'est --accent, directement. */\n color: var(--accent);\n}\n\n.zv-kv__val {\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n color: var(--label);\n}\n\n/* Variante en pied de carte : le filet supérieur détache le récapitulatif du\n * contenu qu'il résume. Valeurs de la fiche client (mt 12 / pt 10 / corps\n * 11px / graisse 400). Le corps 11px n'a pas de palier texte dans le paquet,\n * dont l'échelle s'arrête à --app-sm (12px) : rabattu, et dit. */\n.zv-kv--foot {\n margin-top: 12px;\n border-top: 1px solid var(--hairline);\n padding-top: 10px;\n font-size: var(--app-sm);\n font-weight: 400;\n color: var(--muted);\n}\n\n/* Variante en grille (métadonnées d'une source citée) : deux colonnes des DEUX\n * côtés du seuil — ce sont quatre valeurs courtes, les empiler ferait une\n * colonne inutilement longue. Le libellé passe au-dessus de la valeur. */\n.zv-kv--grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n column-gap: 14px;\n row-gap: 8px;\n font-size: var(--app-sm);\n}\n\n.zv-kv--grid .zv-kv__row {\n display: block;\n min-width: 0;\n overflow-wrap: break-word;\n}\n\n/* En grille, la clé redevient un label de colonne : capitales, plus petit,\n * gris de label — et non plus l'accent de la variante en ligne. */\n.zv-kv--grid .zv-kv__key {\n display: block;\n margin-bottom: 2px;\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* La valeur peut se couper ici : dans une case de 50 % de largeur, un\n * nowrap déborderait de sa colonne. */\n.zv-kv--grid .zv-kv__val {\n white-space: normal;\n}\n\n/* ─── Pastille d'initiales ─────────────────────────────────────────────\n * CARRÉE, jamais ronde : le rond est un usage de réseau social, l'encre et le\n * papier posent des blocs. La teinte dit la NATURE de l'entité, elle n'est pas\n * décorative — la changer, c'est changer le sens.\n *\n * 9,5px dans la source. Le paquet ne descend sous 12px que sur son échelle\n * mono : --mono-sm (9px) est employé ici comme MESURE, la pastille restant\n * rendue dans la famille de texte héritée. */\n\n.zv-avatar {\n display: flex;\n width: 26px;\n height: 26px;\n flex: none;\n align-items: center;\n justify-content: center;\n font-size: var(--mono-sm);\n font-weight: 600;\n}\n\n/* Puce d'organisation, dans le chrome : une taille en dessous pour ne pas\n * peser plus que le nom qu'elle accompagne. */\n.zv-avatar--sm {\n width: 22px;\n height: 22px;\n}\n\n.zv-avatar--accent {\n background: var(--accent);\n color: var(--white);\n}\n\n.zv-avatar--ink {\n background: var(--ink-2);\n color: var(--white);\n}\n\n/* Défaut (société, confrère) : sans aplat, pour que les pastilles pleines\n * restent l'exception et gardent leur valeur de signal. */\n.zv-avatar--soft {\n border: 1px solid var(--line);\n background: var(--hairline);\n color: var(--ink-2);\n}\n\n/* Règles d'initiales, à respecter côté appelant. Il y en a DEUX, et les\n * confondre change le rendu :\n * — .zv-avatar (26px, entité du répertoire) : préfixes « M. | Me | SARL |\n * SCI | SA » retirés, découpe sur [espace ou &], AUCUN repli ;\n * — .zv-avatar--sm (22px, organisation dans le chrome) : deux mots au plus,\n * mots-outils écartés (« cabinet », « étude », « office », « selarl »,\n * « scp », « et », « de », « du », « des », « la », « le », « les »),\n * repli sur « · » quand rien ne sort — sans ce repli, une pastille vide\n * passe pour un défaut d'affichage.\n * Prescrire la seconde pour la première change les initiales du répertoire. */\n\n/* ─── Troncature ───────────────────────────────────────────────────────\n * Deux comportements, jamais interchangeables : couper, ou revenir à la ligne. */\n\n/* Couper : réservé à ce qui reste identifiable amputé (titres, noms d'entité)\n * ET à ce qui est récupérable ailleurs. ⚠️ Ne marche QUE si le parent porte\n * min-width: 0 (ou si la colonne est en minmax(0, 1fr)) : sans cela, la boîte\n * ne descend pas sous son contenu, rien n'est coupé et la grille est poussée.\n * Livré seul, c'est un utilitaire qui ne marche pas. */\n.zv-trunc {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Revenir à la ligne : tout ce qui ne se devine pas coupé — adresses e-mail,\n * noms de fichiers, jetons, messages d'erreur. Perdre la fin d'un message\n * d'erreur, c'est perdre l'incident. */\n.zv-break {\n min-width: 0;\n overflow-wrap: break-word;\n}\n\n/* ─── Étiquette de catégorie ───────────────────────────────────────────\n * Étiquette NON porteuse d'alerte : nature d'entité, matière, mot-clé. À ne\n * pas confondre avec le badge d'état (feedback.css), qui porte une couleur\n * sémantique, ni avec .zv-tag (components.css : mono capitales, sans fond) qui est\n * un kicker éditorial. Le fond hairline suffit : une catégorie n'a pas à\n * crier.\n *\n * 10,5 puis 11px dans la source : deux micro-labels, rabattus sur --mono et\n * --mono-lg pris comme mesures (la famille reste celle du texte). */\n\n.zv-chip {\n display: inline-block;\n background: var(--hairline);\n padding: 3px 9px;\n font-size: var(--mono);\n font-weight: 600;\n color: var(--ink-2);\n}\n\n/* Valeur technique dans une étiquette (date normalisée, référence) : la mono\n * sans capitales, comme .zv-meta. */\n.zv-chip--mono {\n font-family: var(--font-mono);\n font-variant-numeric: tabular-nums;\n}\n\n/* Variante à contour : employée quand les étiquettes sont posées sur une carte\n * blanche, où le fond hairline ne se détacherait plus assez. */\n.zv-chip--outline {\n border: 1px solid var(--line);\n background: none;\n padding: 4px 10px;\n font-size: var(--mono-lg);\n font-weight: 500;\n}\n\n.zv-chip-row {\n display: flex;\n flex-wrap: nowrap;\n gap: 6px;\n}\n\n/* ─── Citation ─────────────────────────────────────────────────────────\n * Référence cliquable, insérée dans le fil du texte ou groupée en pied de\n * réponse ; elle ouvre la feuille de source. Fond, filet et encre sont ceux\n * de l'accent (--accent-soft / --accent-line / --accent) : le paquet ne\n * déclare pas d'alias sémantique de citation, et en inventer un ferait un\n * token de plus qu'aucun test n'accepterait. */\n\n.zv-cite {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 100%;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n padding: 2px 8px;\n /* La puce est insérée au fil d'un paragraphe : sans ce recalage elle pend\n * sous la ligne de base et hache l'interligne. */\n vertical-align: 1px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n /* nowrap l'emporte tant qu'on est au-dessus du seuil ; overflow-wrap est\n * posé d'avance pour le passage en white-space: normal du bloc mobile. */\n overflow-wrap: break-word;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.zv-cite:hover {\n border-color: var(--accent);\n}\n\n.zv-cite-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n}\n\n/* ⚠️ Aucun état « source déjà ouverte » : la puce ne se marque pas quand sa\n * feuille est affichée, on ne sait pas laquelle on lit dès qu'il y en a\n * plusieurs. À trancher, pas à combler au hasard.\n * ⚠️ En production c'est un <span onClick> : inatteignable au clavier. Le\n * rendre focusable côté appelant (<button>) suffit à lui donner le contour de\n * base.css — c'est la correction à faire, pas un style à ajouter ici.\n * ⚠️ Recoupe .zv-src (components.css : mono capitales, fond blanc, filet\n * --accent-line, rayon 4px) : même rôle éditorial, aucune valeur commune. La\n * version applicative est remplie et à angle vif. Fusion ou dédoublement\n * assumé : à arbitrer, ce module ne tranche pas seul. */\n\n/* ─── Vignette de format ───────────────────────────────────────────────\n * ⚠️ HORS CHARTE, ET ASSUMÉ : --fmt-pdf et --fmt-docx sont le rouge Acrobat\n * et le bleu Word, pas --err ni --accent. Deux relevés indépendants les codent\n * à l'identique — c'est une intention (reconnaître le format d'un coup d'œil),\n * pas une dérive ; tokens.css les déclare et n'en autorise pas d'autre usage.\n * ⚠️ La source pose 7,5px, sous le plancher du paquet (--mono-sm, 9px).\n * Rabattu sur ce plancher : la vignette grossit d'un pixel et demi, ce qui\n * n'est PAS le relevé — à revoir à l'œil sur une rangée dense. Réservé à deux\n * ou quatre caractères en capitales, jamais à du texte.\n * L'interlettrage reste à 0,06em et non aux 0,14em du reste des capitales :\n * sur quatre caractères dans 30px de large, 0,14em déborde de la vignette.\n * Règle de choix relevée : DOCX si le type MIME contient « word », PDF sinon. */\n\n.zv-filetype {\n flex: none;\n padding: 2px 5px;\n text-align: center;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 700;\n letter-spacing: 0.06em;\n color: var(--white);\n}\n\n.zv-filetype--pdf {\n background: var(--fmt-pdf);\n}\n\n.zv-filetype--docx {\n background: var(--fmt-docx);\n}\n\n/* ─── Rangée dépliante ─────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La FAQ est le pliage de la\n * vitrine ; l'applicatif n'en avait aucun — options avancées, détail d'une\n * ligne de journal. Construite sur <details> NATIF : le clavier, l'annonce\n * et l'état ouvert viennent du navigateur, aucun JavaScript.\n * Plusieurs rangées se suivent bord à bord : le filet mitoyen tombe. */\n\n.zv-fold {\n border: 1px solid var(--line);\n background: var(--white);\n}\n\n.zv-fold + .zv-fold {\n border-top: 0;\n}\n\n.zv-fold__titre {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 16px;\n font-size: var(--app);\n font-weight: 600;\n color: var(--ink);\n cursor: pointer;\n /* Le marqueur natif du <summary> tombe : le chevron du composant fait le\n * même travail sans imposer le triangle du navigateur. */\n list-style: none;\n}\n\n.zv-fold__titre::-webkit-details-marker {\n display: none;\n}\n\n.zv-fold__titre:hover {\n background: var(--panel);\n}\n\n/* Le chevron porte sa classe : cibler tout svg du titre attraperait aussi\n * une icône posée dans le libellé, qui se verrait rétrécie et retournée à\n * l'ouverture. */\n.zv-fold__chevron {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--label);\n /* `rotate` et non `transform` : la propriété indépendante, comme partout\n * où le paquet anime. Coupée par prefers-reduced-motion (base.css). */\n transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-fold[open] > .zv-fold__titre {\n border-bottom: 1px solid var(--hairline);\n}\n\n.zv-fold[open] > .zv-fold__titre .zv-fold__chevron {\n rotate: 180deg;\n}\n\n.zv-fold__corps {\n padding: 12px 16px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* ─── Touche de clavier ────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026). Une documentation d'interface cite des\n * raccourcis ; le paquet n'avait rien pour les poser. Le bas de 2 px fait\n * le relief de la touche — géométrie, pas ombre de charte, comme l'anneau\n * du radio. */\n\n.zv-kbd {\n display: inline-block;\n padding: 2px 6px;\n border: 1px solid var(--line);\n border-bottom-width: 2px;\n background: var(--panel);\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 600;\n white-space: nowrap;\n color: var(--ink-2);\n}\n\n/* ─── Code ─────────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md — c'est l'entrée qui a créé\n * --font-code). --font-mono (Space Grotesk) est PROPORTIONNELLE : elle\n * porte le registre des labels, pas l'alignement, et tabular-nums n'aligne\n * que les chiffres. Du code exige la chasse fixe entière : --font-code, la\n * pile système, sans fonte chargée.\n * Aucune coloration syntaxique : elle imposerait une palette entière que la\n * charte n'a pas. Le code se lit à l'encre sur panneau, comme un extrait. */\n\n/* En ligne, dans une phrase : `pnpm build`. Taille RELATIVE au texte qui\n * l'accueille — le même extrait sert dans un corps 15 et une note 12,5. */\n.zv-code {\n padding: 1px 5px;\n border: 1px solid var(--hairline);\n background: var(--panel);\n font-family: var(--font-code);\n font-size: 0.85em;\n overflow-wrap: break-word;\n color: var(--ink-2);\n}\n\n/* En bloc. Le cadre défile en son sein, comme le tableau : une ligne longue\n * ne pousse jamais la page en défilement horizontal. */\n.zv-codeblock {\n overflow-x: auto;\n margin: 0;\n border: 1px solid var(--line);\n background: var(--panel);\n padding: 14px 16px;\n font-family: var(--font-code);\n font-size: var(--app-sm);\n line-height: 1.7;\n tab-size: 2;\n color: var(--ink);\n}\n\n/* Le <code> intérieur n'apporte que la sémantique : sans ce rappel, il\n * retomberait sur la mono du navigateur, à une autre taille. */\n.zv-codeblock code {\n font: inherit;\n}\n\n/* ─── Sous 768px ───────────────────────────────────────────────────────\n * Point de rupture unique de la charte. Les lignes cessent d'être des lignes :\n * une grille de six colonnes à 375px produit six colonnes illisibles, ou un\n * débordement horizontal de la page. Le balisage, lui, ne change pas — c'est\n * .zv-unwrap qui rend l'inversion possible.\n *\n * ⚠️ La source coupe à 1024px (lg de Tailwind), pas à 768 : entre les deux,\n * l'application affichera des cartes là où la source affiche encore un\n * tableau. C'est la charte qui tranche, mais les gabarits de colonnes ont été\n * réglés pour 1024 et méritent une vérification à 800px. */\n\n@media (max-width: 767px) {\n /* Des intitulés de colonnes sans colonnes ne veulent rien dire : ce sont les\n * valeurs qui reprennent leur étiquette (.zv-cell__label). */\n .zv-table__head {\n display: none;\n }\n\n /* stretch, pas flex-start : c'est ce que fait la source (aucun items-* au\n * répertoire, items-stretch à l'import). Avec flex-start, tout enfant\n * direct qui n'est pas une enveloppe se rétracte à sa largeur de contenu. */\n .zv-row {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 7px;\n padding-block: 12px;\n }\n\n /* Le groupe redevient une boîte : ses deux ou trois cellules tiennent sur\n * une même ligne au lieu de s'empiler chacune sur la sienne.\n * flex-wrap: wrap n'est pas optionnel — deux des quatre enveloppes de la\n * source passent à la ligne, et sans lui la rangée taille + date + statut\n * déborde de la carte à 320px, c'est-à-dire exactement le défaut que le\n * passage en carte devait supprimer.\n * ⚠️ La gouttière est arbitrée : la source en emploie trois (6, 10, 12px).\n * 10px est la valeur médiane et la plus fréquente, pas une mesure unique. */\n .zv-unwrap {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: 10px;\n row-gap: 4px;\n min-width: 0;\n width: 100%;\n }\n\n .zv-cell__label {\n display: inline-block;\n }\n\n /* La source ne pose PAS de marge sur l'étiquette : elle rend la CELLULE\n * flex, intitulé à gauche et valeur à droite\n * (tableau-de-bord/page.tsx:120). Sans cela, « TAILLE » vient se coller à\n * « 2,4 Mo » et les deux se lisent comme un seul mot.\n * `:has()` cible les seules cellules qui portent une étiquette — les\n * autres gardent leur flux. */\n .zv-cell:has(.zv-cell__label) {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n }\n\n /* Inline, et pas inline-block : « 42 docs » doit rester un seul mot visuel\n * collé à sa valeur. */\n .zv-cell__unit {\n display: inline;\n }\n\n .zv-list__row {\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 12px 14px;\n }\n\n /* Clé et valeur empilées : côte à côte à 320px, elles se chevauchent dès que\n * le libellé dépasse trois mots. */\n .zv-kv__row {\n flex-direction: column;\n gap: 2px;\n }\n\n /* Un libellé long ne tient pas sur une ligne à cette largeur : les\n * étiquettes passent en plusieurs rangées plutôt que de déborder. */\n .zv-chip-row {\n flex-wrap: wrap;\n }\n\n /* Une référence complète ne tient pas en une ligne sur un écran de 320px :\n * sans césure autorisée, la puce déborde de sa colonne. */\n .zv-cite {\n white-space: normal;\n }\n\n /* L'en-tête du dépliant devient une cible tactile. */\n .zv-fold__titre {\n min-height: var(--tap);\n padding-block: 10px;\n }\n\n /* 16px de gouttière interne sur 375px déjà bordés par la page : le bloc\n * se resserre, comme les bandeaux. */\n .zv-codeblock {\n padding: 12px;\n }\n}\n";
7
+ export const CSS_COMPLET = "/* ── tokens.css ── */\n/* Zevra — Design tokens « Encre & Papier »\n * Source : passation DA du 23/08/2026 (canvas Claude Design « Zevra DA Encre\n * et Papier »). Ce fichier est la SOURCE DE VÉRITÉ des valeurs de marque :\n * toute app doit lire ces variables plutôt que recopier un hexadécimal.\n *\n * ⚠️ Système CLAIR par nature (papier/encre). Les surfaces sombres se font\n * avec --manifeste et --accent-on-dark, pas en inversant les neutres : les\n * contrastes ont été réglés dans ce sens-là et pas dans l'autre.\n */\n\n:root {\n /* Largeur de page. Le relevé de la passation a été fait sur une trame de\n * 1440 : au-delà, les enveloppes (zv-gutter et variantes) se centrent au\n * lieu de s'étirer — un hero qui colle au bord gauche d'un écran de\n * 2 560 px ne raconte plus une page, il raconte un débordement. */\n --page-max: 1440px;\n\n /* Neutres (charte Mémoire) */\n --ink: #0b1020;\n /* Niveau intermédiaire, entre l'encre et le gris : titre de colonne,\n * valeur de cellule, segment de fil d'Ariane. L'interface applicative en a\n * besoin, la vitrine non — d'où son absence des premières planches.\n * Contraste 7,05 sur papier : conforme AA sur texte normal. */\n --ink-2: #4a5468;\n --muted: #5a637a;\n --label: #8a93a8;\n --line: #e3e6ef;\n --hairline: #eef1f8;\n --paper: #f5f6fa;\n --panel: #fbfcfe;\n --white: #ffffff;\n\n /* Indigo (action, labels, liens) */\n --accent-deep: #1e1b8c;\n --accent: #2a2aa8;\n --accent-line: #c9d6f5;\n --accent-soft: #eaf1ff;\n\n /* Spectre produit (prélevé dans les logos officiels).\n * ⚠️ Usage strictement limité : filet de section (56×2), barre de\n * progression, points de méthode. Jamais en fond, jamais en texte. */\n --violet: #8a3af8;\n --iris: #5848f8;\n --bleu: #2f6ef8;\n --cyan: #14c2f8;\n --lavande: #b8a8f8;\n --spectre: linear-gradient(90deg, #8a3af8, #2f6ef8 55%, #14c2f8);\n\n /* Fond sombre (bande manifeste) — dégradé type logo */\n --manifeste: linear-gradient(135deg, #06063a 0%, #0b0b5e 28%, #1e1b8c 55%, #3d36d6 80%, #2080f0 100%);\n --accent-on-dark: #a5b4fc;\n\n /* États — trois familles symétriques : le ton plein (texte, icône), le\n * fond atténué (bandeau d'alerte) et le filet (bordure de carte).\n * Relevés dans l'application Mémoire, où ils servent déjà en production. */\n --ok: #1c6b4a;\n --ok-soft: #e6f3ec;\n --ok-line: #bfdccb;\n --warn: #b98a1f;\n --warn-soft: #faf3e2;\n --warn-line: #ebdcb4;\n /* L'ambre plein est trop clair pour du texte sur fond atténué : il lui\n * faut une déclinaison sombre. --ok et --err, eux, sont déjà assez\n * contrastés pour servir de couleur de texte — d'où l'asymétrie. */\n --warn-ink: #8a6d1e;\n --err: #b5202c;\n --err-soft: #fdece9;\n --err-line: #efc9cd;\n\n /* Typo — Google Fonts : Outfit (display), Public Sans (texte), Space Grotesk (labels) */\n --font-display: \"Outfit\", \"Helvetica Neue\", Arial, sans-serif; /* titres 200-400 */\n --font-text: \"Public Sans\", \"Helvetica Neue\", Arial, sans-serif; /* 400/600 */\n --font-mono: \"Space Grotesk\", \"Menlo\", monospace; /* labels caps, 0.18-0.2em */\n /* Chasse fixe RÉELLE, pour le code seulement. --font-mono (Space Grotesk)\n * est proportionnelle : elle porte le REGISTRE des labels, pas\n * l'alignement — du code exige la chasse fixe entière, que tabular-nums\n * ne donne qu'aux chiffres. Pile système, aucune fonte chargée.\n * HORS RELEVÉ (25/08/2026), voir DECISIONS.md « Bloc de code ». */\n --font-code: ui-monospace, \"SF Mono\", \"Menlo\", \"Consolas\", monospace;\n\n /* Échelle display (letter-spacing -0.02em, graisse 300 + chute en 400 --accent-deep) */\n --h1: 76px;\n --h2: 56px;\n --h3: 38px;\n --card-title: 24px;\n --lead: 18px;\n --body: 15px;\n\n /* Rayons — ARBITRÉS À ZÉRO le 25/08/2026.\n *\n * La DA a tranché sur pièces : « très carré, ça fait pro et inspire\n * confiance » ; le rond disait « flexible, jovial », qui n'est pas ce que\n * vend Zevra. Le relevé donnait 5/3/10/16px, hérités d'un état antérieur\n * de la charte — et déjà contredits sur place, puisque les champs de\n * saisie, les étiquettes et les pastilles d'initiales étaient à angle vif\n * depuis le premier jour. Les quatre tokens s'alignent sur eux.\n *\n * ⚠️ Les tokens RESTENT, à zéro : ils nomment quatre intentions\n * distinctes (action, pastille, panneau flottant, carte). Le jour où la DA\n * veut rendre de la rondeur à l'une d'elles, c'est une ligne ici — pas une\n * chasse aux `border-radius` dans douze fichiers.\n *\n * ⚠️ Ce qui n'est PAS concerné : le rond de forme, qui porte du sens et\n * non du style — pastille d'état, bouton d'interrupteur, radio, halo. Ils\n * écrivent leur `border-radius` en clair, sans passer par un token. */\n --r-btn: 0;\n --r-pill: 0;\n --r-panel: 0;\n --r-card: 0;\n\n /* Ombres */\n --shadow-card: 0 2px 10px rgba(11, 16, 32, 0.05);\n --shadow-panel: 0 24px 60px rgba(30, 27, 140, 0.12);\n --shadow-btn: 0 10px 28px rgba(30, 27, 140, 0.30);\n --shadow-glyph: drop-shadow(0 24px 32px rgba(88, 72, 248, 0.28));\n\n /* ─── Couche applicative ──────────────────────────────────────────────\n * Une interface est plus dense qu'une page vitrine : elle a besoin d'une\n * échelle plus serrée que --body/--lead, et de mesures que la vitrine\n * n'a jamais eu à nommer.\n *\n * Relevé dans l'application Mémoire, puis RATIONALISÉ : la production\n * emploie huit paliers de texte répartis dans 5,5 px, avec des demi-pixels\n * et des écarts d'un pixel. C'est de l'entropie d'utilitaires, pas une\n * échelle. Les paliers ci-dessous les regroupent ; le détail des\n * rabattements est dans DECISIONS.md. */\n\n /* Texte applicatif */\n --app-lg: 14px; /* corps de message, lien de navigation */\n --app: 13px; /* texte courant : cellule, carte, champ de saisie */\n --app-sm: 12px; /* mention secondaire : statut, aide, note de contexte */\n\n /* Titres applicatifs — au-dessus, --card-title (24px) fait le titre de page */\n --app-h2: 20px; /* titre de fiche, d'état vide, d'interstice */\n --app-h3: 16px; /* titre de panneau latéral, de feuille de source */\n\n /* Labels capitales, échelle applicative */\n --mono-lg: 11px; /* compteur, fil d'Ariane, horodatage */\n --mono: 10px; /* métadonnée, label de colonne */\n --mono-sm: 9px; /* badge dense : niveau de log, statut de coque */\n\n /* Mesures */\n --tap: 40px; /* cible tactile minimale, sous le point de rupture */\n --topbar-h: 52px; /* hauteur de la barre supérieure applicative */\n\n /* Ombre de menu déroulant — la seule surface qui flotte au-dessus du\n * contenu sans être une carte ; ni --shadow-card ni --shadow-panel ne\n * conviennent, l'une est trop plate, l'autre trop théâtrale. */\n --shadow-menu: 0 8px 24px rgba(11, 16, 32, 0.1);\n\n /* Couleurs de format de fichier. Elles ne viennent PAS de la charte : ce\n * sont les couleurs que le monde associe à ces formats, et les détourner\n * rendrait les vignettes illisibles au premier coup d'œil. Seul usage\n * autorisé : la pastille de format. */\n --fmt-pdf: #c4362b;\n --fmt-docx: #2a5ba8;\n\n /* Signature */\n --spec-filet: 56px; /* filet de spectre 56×2px devant chaque kicker */\n}\n\n\n/* ── base.css ── */\n/* Zevra UI — socle : reset, document, typographie, signature\n * Dépend de tokens.css. À charger en premier.\n *\n * Chaque valeur ci-dessous est relevée dans les maquettes de la passation du\n * 23/08/2026. Quand deux planches divergent, l'arbitrage est écrit dans\n * DECISIONS.md — jamais tranché en silence.\n *\n * POINT DE RUPTURE : 768px. Il ne vient PAS du relevé (les cinq maquettes sont\n * à largeur fixe, sans une seule media query). C'est la convention `md` du\n * projet, et les valeurs mobiles viennent de Mobile.html, qui est un rendu\n * figé à 390px. Les quatre modules utilisent ce seuil et lui seul.\n */\n\n/* ─── Reset ─────────────────────────────────────────────────────────────\n * Posé ici et nulle part ailleurs : les autres modules ne touchent ni au\n * box-sizing ni au body. */\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n background: var(--paper);\n color: var(--ink);\n font-family: var(--font-text);\n -webkit-font-smoothing: antialiased;\n}\n\n/* ─── Liens ─────────────────────────────────────────────────────────────\n * Restreint aux liens SANS classe. Les maquettes stylent `a` nu, ce qui\n * repeint le libellé des boutons au survol (a:hover, 0-1-1, l'emporte sur\n * .zv-btn, 0-1-0) — bug visible dans Main.html et signalé au relevé. Le\n * périmètre est réduit plutôt que neutralisé par un :hover inventé. */\n\na:not([class]) {\n color: var(--accent);\n text-decoration: none;\n}\n\na:not([class]):hover {\n color: var(--accent-deep);\n}\n\n.zv-link {\n color: var(--accent);\n text-decoration: none;\n}\n\n.zv-link:hover {\n color: var(--accent-deep);\n}\n\n/* ─── Accessibilité ─────────────────────────────────────────────────────\n * HORS RELEVÉ, et assumé : aucune des cinq maquettes ne déclare d'état de\n * focus. Un système livré sans focus clavier est inutilisable au clavier —\n * on ne réplique pas cette lacune. Seules des valeurs existantes sont\n * employées. */\n\n:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n\n/* Texte pour lecteurs d'écran seulement — la géométrie standard du motif,\n * aucune valeur de charte. HORS RELEVÉ (25/08/2026) : ajouté avec la roue,\n * dont le libellé d'annonce doit être un TEXTE dans le flux (un aria-label\n * sur un span vide n'est pas annoncé par tous les lecteurs). */\n.zv-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* ─── Titres ────────────────────────────────────────────────────────────\n * Outfit, graisse 300, interlettrage -0.02em. L'échelle est celle de\n * Main.html, la planche de référence. */\n\n.zv-display {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n}\n\n.zv-h1 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h1);\n line-height: 1.05;\n font-weight: 300;\n color: var(--ink);\n max-width: 1100px;\n text-wrap: balance;\n}\n\n.zv-h2 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h2);\n line-height: 1.06;\n font-weight: 300;\n text-wrap: balance;\n}\n\n/* Le titre de CTA final est d'un cran au-dessus des titres de section :\n * 64px contre 56px (Main.html). Aucun token ne couvre cette taille. */\n.zv-h2--cta {\n font-size: 64px;\n max-width: 900px;\n}\n\n/* Titre de section des landings produit : un cran EN DESSOUS du 56px de la\n * home. Les trois landings de la passation le posent en classe `.h2`, aux\n * côtés des 56 et 64px qui restent pour la page d'accueil. */\n.zv-h2--sm {\n font-size: 48px;\n}\n\n.zv-h3 {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--h3);\n line-height: 1.1;\n font-weight: 300;\n max-width: 480px;\n}\n\n.zv-card-title {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--card-title);\n line-height: 1.25;\n font-weight: 400;\n}\n\n/* La deuxième moitié de phrase d'un titre bascule en indigo et prend un cran\n * de graisse. Motif systématique : présent dans les cinq maquettes. */\n.zv-em {\n color: var(--accent-deep);\n font-weight: 400;\n /* Le composant `Em` rend un vrai <em>, que le navigateur penche par\n * défaut — et AUCUNE italique n'est chargée : Outfit n'en a pas, il en\n * fabrique donc une oblique de synthèse. Sur un titre de 76px, ça se voit.\n * La charte ne demande à cette classe qu'un changement de couleur et de\n * graisse ; on remet donc la droite. (`.zv-inset em` portait déjà cette\n * règle : elle vit désormais là où le besoin est structurel.) */\n font-style: normal;\n}\n\n/* Sur fond sombre l'accent ne change pas de couleur mais de graisse. */\n.zv-em--on-dark {\n color: var(--white);\n font-weight: 500;\n}\n\n/* ─── Texte courant ─────────────────────────────────────────────────────\n * Quatre paliers relevés : 18 (chapô), 16 (colonne d'accompagnement),\n * 15 (corps), 13 (carte dense). */\n\n.zv-lead {\n margin: 0;\n font-size: var(--lead);\n line-height: 1.6;\n color: var(--muted);\n max-width: 660px;\n text-wrap: pretty;\n}\n\n.zv-body {\n margin: 0;\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* 16px : colonne de droite des en-têtes de section (Main.html). */\n.zv-body--lg {\n font-size: 16px;\n}\n\n/* 13px : légende de carte, texte serré. */\n.zv-body--dense {\n font-size: 13px;\n}\n\n.zv-body strong,\n.zv-lead strong {\n font-weight: 600;\n color: var(--ink);\n}\n\n/* 14px : texte de règle et de légende des planches. */\n.zv-rule {\n margin: 0;\n font-size: 14px;\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* 12,5px : la note d'accompagnement d'un bloc ou d'une carte de\n * documentation. Le plus petit palier de texte courant du système — en\n * dessous, on passe en label capitales. */\n.zv-note {\n margin: 0;\n font-size: 12.5px;\n line-height: 1.55;\n color: var(--muted);\n}\n\n/* ─── Labels capitales ──────────────────────────────────────────────────\n * Space Grotesk, majuscules, très interlettré. Trois tailles seulement :\n * 11px (kicker de section), 10px (label de carte), 9px (micro-label). */\n\n.zv-mono {\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.2em;\n font-weight: 500;\n font-size: 11px;\n color: var(--accent);\n}\n\n.zv-mono--label {\n font-size: 10px;\n font-weight: 700;\n}\n\n.zv-mono--micro {\n font-size: 9px;\n font-weight: 500;\n color: var(--label);\n}\n\n.zv-mono--muted {\n color: var(--label);\n}\n\n.zv-mono--ink {\n color: var(--ink);\n}\n\n/* Sur fond sombre les labels passent en indigo clair, jamais en blanc pur. */\n.zv-mono--on-dark {\n color: var(--accent-on-dark);\n}\n\n/* Accroche produit, posée sous le nom d'un glyphe : « Votre savoir,\n * interrogeable en une phrase ». Moins interlettrée que le label qu'elle\n * suit (0.14em contre 0.2em) — sinon la phrase se disloque. */\n.zv-tagline {\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.14em;\n font-weight: 500;\n font-size: 9px;\n line-height: 1.6;\n color: var(--muted);\n text-align: center;\n /* Remonte sous le label : l'ensemble se lit comme un seul bloc. */\n margin-top: -4px;\n}\n\n/* ─── Signature : le filet de spectre ───────────────────────────────────\n * 56×2px devant chaque kicker. C'est, avec la barre de progression et les\n * points de méthode, l'un des trois seuls emplois autorisés du spectre.\n *\n * ⚠️ ARBITRAGE (voir DECISIONS.md) : --spectre vaut violet → bleu → cyan,\n * comme la charte des logos qui le nomme « signature ». Trois autres\n * planches posent iris → violet → cyan. Le token fait foi ici ; la question\n * est ouverte pour la DA. */\n\n.zv-spec {\n display: block;\n width: var(--spec-filet);\n height: 2px;\n background: var(--spectre);\n /* Angle vif comme tout le reste depuis le 25/08 — sur 2px de haut, le\n * rayon d'1px du relevé ne se voyait de toute façon pas. */\n border-radius: 0;\n}\n\n/* Bas de page : filet raccourci. */\n.zv-spec--sm {\n width: 40px;\n}\n\n/* Sur fond sombre le filet s'éclaircit pour rester lisible. */\n.zv-spec--on-dark {\n background: linear-gradient(90deg, #7fd8ff, var(--violet));\n}\n\n/* Filet + label, la composition de tête de section. */\n.zv-kicker {\n display: flex;\n align-items: center;\n gap: 14px;\n}\n\n.zv-kicker--stack {\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n\n.zv-kicker--center {\n flex-direction: column;\n align-items: center;\n gap: 14px;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Valeurs relevées dans Mobile.html. Le seuil, lui, est la convention du\n * projet (cf. en-tête). */\n\n@media (max-width: 767px) {\n .zv-h1 {\n font-size: 40px;\n line-height: 1.08;\n }\n\n .zv-h2 {\n font-size: 30px;\n line-height: 1.1;\n }\n\n .zv-h2--cta {\n font-size: 32px;\n line-height: 1.08;\n }\n\n .zv-card-title {\n font-size: 18px;\n font-weight: 500;\n }\n\n .zv-lead {\n font-size: 15px;\n line-height: 1.55;\n }\n\n /* .zv-body garde 15px : c'est déjà la taille du chapô mobile. Seule\n * l'interligne se resserre — et --dense reste distinct, à 13px/1.45. */\n .zv-body {\n line-height: 1.55;\n }\n\n .zv-body--dense {\n font-size: 13px;\n line-height: 1.45;\n }\n\n .zv-mono {\n letter-spacing: 0.18em;\n font-size: 9px;\n }\n\n .zv-mono--label {\n font-size: 8px;\n }\n\n .zv-spec {\n width: 44px;\n }\n\n /* Re-déclaré : une media query n'ajoute pas de spécificité, le modificateur\n * serait sinon écrasé par la règle ci-dessus. */\n .zv-spec--sm {\n width: 40px;\n }\n}\n\n\n/* ── components.css ── */\n/* Zevra UI — composants : boutons, badges, cartes, encarts\n * Dépend de tokens.css et base.css.\n *\n * Les cinq maquettes ne divergent jamais sur l'intention, souvent sur le\n * chiffre. La règle appliquée ici : Main.html fait foi (c'est la page de\n * référence, la plus complète), les écarts des autres planches sont notés\n * en commentaire et détaillés dans DECISIONS.md.\n */\n\n/* ─── Boutons ───────────────────────────────────────────────────────────\n * UN SEUL bouton plein par écran : l'indigo porte l'action, il perd tout\n * s'il est répété. Les actions secondaires prennent la variante fantôme.\n *\n * Hauteur 48px : Main.html, Mobile.html et le prototype s'accordent.\n * DesignSystem.html pose 46px — planche isolée, écartée. */\n\n.zv-btn,\n.zv-btn--ghost,\n.zv-btn--ink,\n.zv-btn--danger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n height: 48px;\n padding: 0 24px;\n border-radius: var(--r-btn);\n font-family: var(--font-text);\n font-weight: 600;\n font-size: 14px;\n text-decoration: none;\n border: 0;\n cursor: pointer;\n /* La courbe maison, 160 ms — coupée globalement par prefers-reduced-motion\n * (base.css). Le fond en dégradé du bouton plein ne s'interpole pas : lui\n * seul bascule sans fondu, l'écart est trop faible pour se voir. */\n transition:\n background-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n/* ─── Le plein ne s'applique QUE si aucune variante n'est posée ────────\n *\n * `zv-btn`, `zv-btn--ghost`, `zv-btn--ink` et `zv-btn--danger` sont quatre\n * classes AUTONOMES et mutuellement exclusives : le socle commun ci-dessus\n * les groupe dans un même sélecteur, ce qui leur donne exactement l'air\n * d'une base et de ses modificateurs. `Button` n'en rend jamais qu'une —\n * mais une app qui écrit ses classes à la main lit du BEM et compose les\n * deux. C'est arrivé (mcp-factory, 0.19.0), et le résultat était vicieux :\n * `.zv-btn:hover` (0,2,0) l'emportait sur `.zv-btn--ghost` (0,1,0), d'où un\n * fond indigo sous un texte encre — illisible, et seulement AU SURVOL, donc\n * invisible en relecture comme en capture.\n *\n * Le `:not(:where(…))` dit ce que le système voulait déjà dire : ces quatre\n * classes s'excluent. `:where()` a une spécificité NULLE — la garde ne\n * change donc aucun arbitrage existant, et une app qui surcharge `.zv-btn`\n * continue de gagner exactement comme avant.\n *\n * ⚠️ À reporter sur toute règle future qui peint le bouton plein. */\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)) {\n /* Le sommet du dégradé. RELEVÉ dans Main.html à #2b2bb4, mais DÉRIVÉ de\n * --accent depuis la 0.19.0 : la déclaration étant portée par le bouton\n * lui-même, une redéfinition de marque héritée d'un conteneur ne\n * l'atteignait pas — un cabinet en marque blanche obtenait un dégradé\n * allant de l'indigo Zevra à SA couleur. Pour la charte par défaut,\n * l'écart avec la valeur relevée est d'un point de bleu, invisible.\n * Les états de survol le dérivent ensuite par color-mix. */\n --zv-btn-haut: var(--accent);\n background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);\n color: var(--white);\n /* Le liseré interne fait tout le galbe du bouton : il est dans Main.html\n * mais absent de --shadow-btn, d'où la composition ici. */\n box-shadow:\n 0 1px 0 rgba(255, 255, 255, 0.18) inset,\n var(--shadow-btn);\n}\n\n.zv-btn--ghost {\n background: var(--white);\n color: var(--ink);\n border: 1px solid var(--accent-line);\n}\n\n.zv-btn--ink {\n background: var(--ink);\n color: var(--white);\n}\n\n/* Action DESTRUCTIVE : la confirmation d'une modale danger, la suppression\n * définitive. HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : sans elle,\n * « Supprimer » se rendait en plein indigo et se lisait comme n'importe\n * quelle action. Aplat --err, sans dégradé ni ombre indigo.\n * ⚠️ Elle COMPTE comme le bouton plein de l'écran : la règle « un seul plein\n * par page » compte les aplats, pas les couleurs. */\n.zv-btn--danger {\n background: var(--err);\n color: var(--white);\n}\n\n/* ─── États de survol et d'appui ────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : aucune maquette n'a d'état\n * hover — elles sont figées. Le survol ASSOMBRIT, l'appui assombrit\n * davantage et rassoit le bouton (l'ombre tombe, le liseré s'éteint).\n * Les teintes sont DÉRIVÉES par color-mix — l'idiome du voile\n * (overlays.css) — des couleurs que le bouton porte déjà : le sommet de\n * dégradé relevé (--zv-btn-haut, sans token) et --accent-deep. Aucune\n * teinte nouvelle n'entre. Aucun déplacement : les entrées du système\n * montent au défilement, un bouton qui saute au survol raconterait autre\n * chose.\n * Déclarés AVANT [disabled], qui doit l'emporter à spécificité égale. */\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):hover {\n background: linear-gradient(\n 180deg,\n color-mix(in oklab, var(--zv-btn-haut) 86%, var(--ink)) 0%,\n color-mix(in oklab, var(--accent-deep) 86%, var(--ink)) 100%\n );\n}\n\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):active {\n background: linear-gradient(\n 180deg,\n color-mix(in oklab, var(--zv-btn-haut) 72%, var(--ink)) 0%,\n color-mix(in oklab, var(--accent-deep) 72%, var(--ink)) 100%\n );\n box-shadow: none;\n}\n\n.zv-btn--ghost:hover {\n border-color: var(--accent);\n}\n\n.zv-btn--ghost:active {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n\n/* Un aplat d'encre ne peut pas s'assombrir visiblement : il s'ÉCLAIRCIT au\n * survol, et l'appui revient à l'encre pleine — plus sombre que le survol,\n * c'est ce qui fait lire l'enfoncement. */\n.zv-btn--ink:hover {\n background: color-mix(in oklab, var(--ink) 82%, var(--white));\n}\n\n.zv-btn--ink:active {\n background: var(--ink);\n}\n\n.zv-btn--danger:hover {\n background: color-mix(in oklab, var(--err) 86%, var(--ink));\n}\n\n.zv-btn--danger:active {\n background: color-mix(in oklab, var(--err) 72%, var(--ink));\n}\n\n/* Variante compacte : barres de navigation. L'ombre tombe — un bouton de\n * chrome ne décolle pas de la page. */\n.zv-btn--sm {\n height: 40px;\n padding: 0 18px;\n box-shadow: none;\n}\n\n/* CTA mobile : le bouton prend toute la largeur. */\n.zv-btn--block {\n width: 100%;\n}\n\n/* Bouton d'action à icône SEULE : l'envoi d'un composeur, l'action unique\n * d'une barre. HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Le libellé\n * accessible est OBLIGATOIRE (aria-label), et s'écrit pour l'ACTION.\n * Ne pas confondre : .zv-icon-btn est le chrome nu (40 px, sans cadre),\n * .zv-chrome-btn la croix bordée (26 px) — le carré est une action, il en\n * a le poids et l'aplat. */\n.zv-btn--carre {\n width: 48px;\n padding: 0;\n}\n\n.zv-btn--sm.zv-btn--carre {\n width: 40px;\n padding: 0;\n}\n\n.zv-btn[disabled],\n.zv-btn--ghost[disabled],\n.zv-btn--ink[disabled],\n.zv-btn--danger[disabled] {\n /* HORS RELEVÉ — aucune maquette ne montre de bouton désactivé. Traitement\n * minimal et neutre plutôt qu'une couleur inventée. */\n opacity: 0.5;\n cursor: not-allowed;\n box-shadow: none;\n}\n\n/* Ni le survol ni l'appui ne doivent rattraper un bouton inerte —\n * désactivé OU occupé (dont le clic est avalé par la façade) : sans ces\n * règles, les états ci-dessus repeignent le fond et le bouton a l'air\n * actif. Même précaution que les contrôles désactivés de forms.css. */\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[disabled]:is(:hover, :active),\n.zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[aria-busy='true']:is(:hover, :active) {\n background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);\n}\n\n/* Occupé mais pas désactivé, le bouton plein garde son ombre à l'appui —\n * un enfoncement qui ne déclenche rien serait un mensonge de rendu. Le\n * compact, lui, n'a pas d'ombre à garder. */\n.zv-btn:not(.zv-btn--sm)[aria-busy='true']:active {\n box-shadow:\n 0 1px 0 rgba(255, 255, 255, 0.18) inset,\n var(--shadow-btn);\n}\n\n.zv-btn--ghost[disabled]:is(:hover, :active),\n.zv-btn--ghost[aria-busy='true']:is(:hover, :active) {\n border-color: var(--accent-line);\n background: var(--white);\n}\n\n.zv-btn--ink[disabled]:is(:hover, :active),\n.zv-btn--ink[aria-busy='true']:is(:hover, :active) {\n background: var(--ink);\n}\n\n.zv-btn--danger[disabled]:is(:hover, :active),\n.zv-btn--danger[aria-busy='true']:is(:hover, :active) {\n background: var(--err);\n}\n\n/* Attente : le motif de Mémoire (feedback.css le documente) — le LIBELLÉ\n * change (« Connexion… ») pendant que le bouton s'estompe, en\n * `cursor-wait; opacity: .7`, valeurs relevées reprises telles quelles.\n * Codé sur [aria-busy], l'attribut qui rend l'attente audible : le\n * balisage correct ne coûte rien. La façade React l'expose en prop\n * `occupe` — qui pose aria-disabled et AVALE le clic plutôt que de poser\n * `disabled`, pour que le bouton reste dans le parcours clavier pendant\n * que l'attente s'annonce (voir DECISIONS.md). Déclaré APRÈS [disabled]\n * pour l'emporter à spécificité égale quand les deux se combinent. */\n.zv-btn[aria-busy='true'],\n.zv-btn--ghost[aria-busy='true'],\n.zv-btn--ink[aria-busy='true'],\n.zv-btn--danger[aria-busy='true'] {\n cursor: wait;\n opacity: 0.7;\n}\n\n.zv-btn-row {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.zv-btn-row--stack {\n flex-direction: column;\n gap: 10px;\n width: 100%;\n}\n\n/* ─── Lien fléché ───────────────────────────────────────────────────────\n * Le SVG fait 15×15 : quatre occurrences dans Main.html contre une seule\n * à 16px dans DesignSystem.html. Tracés : M5 12h14 et M13 6l6 6-6 6, sur\n * un viewBox 0 0 24 24 — au trait carré depuis le 25/08/2026, comme tout\n * le jeu d'icônes (DECISIONS.md, « Le trait carré »). */\n\n/* ⚠️ REPRIS le 25/08/2026 — la DA le jugeait « pas ouf ». Le diagnostic :\n * c'était un lien bleu avec un chevron collé, sans rien qui le distingue\n * d'un lien de texte, et sans aucune récompense au survol. Le mouvement du\n * paquet est réservé à la vitrine et piloté au défilement ; il n'y avait\n * donc PAS de vocabulaire pour un lien d'action.\n *\n * Trois ajouts, tous dessinés dans les tokens existants :\n * · un FILET qui se déploie sous le libellé au survol, de gauche à droite\n * (scaleX sur une origine gauche) — pas un `text-decoration`, qui ne\n * s'anime pas et coupe les jambages ;\n * · la flèche avance de 4px, ce qui donne au lien sa direction ;\n * · le libellé passe en --accent-deep.\n * Le tout est repeint en une seule transition, donc coupé net par\n * prefers-reduced-motion (base.css) — l'état final reste lisible sans. */\n\n.zv-link-arrow {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n font-weight: 600;\n color: var(--accent);\n text-decoration: none;\n transition: color 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n/* Le filet est un pseudo-élément et non une bordure : une bordure\n * occuperait de la place et ferait sauter la ligne de base au survol. */\n.zv-link-arrow::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n bottom: -3px;\n height: 1px;\n background: currentColor;\n transform: scaleX(0);\n transform-origin: left center;\n transition: transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-link-arrow:hover,\n.zv-link-arrow:focus-visible {\n color: var(--accent-deep);\n}\n\n.zv-link-arrow:hover::after,\n.zv-link-arrow:focus-visible::after {\n transform: scaleX(1);\n}\n\n.zv-link-arrow svg {\n width: 15px;\n height: 15px;\n flex: none;\n /* ⚠️ CORRIGÉ le 25/08/2026 : le trait n'avait JAMAIS été déclaré — ni\n * fill:none ni stroke —, la flèche se rendait en triangle PLEIN noir (le\n * fût, d'aire nulle, disparaissait) au lieu du tracé que le commentaire\n * d'en-tête promettait depuis le premier jour. Posé au trait carré, avec\n * le reste du jeu. */\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: square;\n stroke-linejoin: miter;\n transition: transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-link-arrow:hover svg,\n.zv-link-arrow:focus-visible svg {\n transform: translateX(4px);\n}\n\n/* Sur la bande manifeste : le filet suit `currentColor`, il n'y a donc rien\n * à redéclarer — c'est tout l'intérêt de l'avoir posé en currentColor. */\n.zv-link-arrow--on-dark {\n color: var(--white);\n}\n\n.zv-link-arrow--on-dark:hover,\n.zv-link-arrow--on-dark:focus-visible {\n color: var(--accent-on-dark);\n}\n\n/* ─── Badges et puces ───────────────────────────────────────────────────*/\n\n/* Pastille pleine : numéro de pièce, catégorie de section. */\n.zv-pill {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n height: 28px;\n padding: 0 13px;\n background: var(--accent-deep);\n color: var(--white);\n font-family: var(--font-mono);\n text-transform: uppercase;\n letter-spacing: 0.18em;\n font-size: 10px;\n font-weight: 700;\n border-radius: var(--r-pill);\n}\n\n/* Puce de source (BOSS, Judilibre, KALI…). Le rayon 4px n'a pas de token :\n * il est entre --r-pill (3) et --r-panel (10), sans équivalent. */\n.zv-src {\n display: inline-block;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--accent-deep);\n background: var(--white);\n border: 1px solid var(--accent-line);\n /* La puce de source est une pastille : elle suit le token, désormais à 0. */\n border-radius: var(--r-pill);\n padding: 6px 10px;\n}\n\n/* Variante atténuée : le « + 11 » qui clôt une liste de sources. */\n.zv-src--quiet {\n color: var(--label);\n border-color: var(--line);\n}\n\n.zv-src-row {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* Entité pseudonymisée Lexform, posée dans le fil du texte. */\n.zv-ent {\n display: inline-block;\n background: linear-gradient(135deg, var(--accent-deep), #3d36d6);\n color: var(--white);\n font-family: var(--font-mono);\n font-size: 10px;\n padding: 1px 6px;\n border-radius: var(--r-pill);\n vertical-align: middle;\n}\n\n/* Le paragraphe qui accueille des entités respire davantage : sans cette\n * interligne, les tags se télescopent d'une ligne à l'autre. */\n.zv-ent-text {\n font-size: 14px;\n line-height: 1.9;\n color: var(--ink);\n}\n\n/* Micro-label de statut. */\n.zv-tag {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n white-space: nowrap;\n}\n\n.zv-tag--ok {\n color: var(--ok);\n}\n\n.zv-tag--err {\n color: var(--err);\n}\n\n/* Sur fond sombre les états s'éclaircissent, sinon ils virent au noir. */\n.zv-tag--ok-on-dark {\n color: #8fd3a8;\n}\n\n.zv-tag--err-on-dark {\n color: #f0a5ad;\n}\n\n/* ─── Cartes ────────────────────────────────────────────────────────────\n * ⚠️ ARBITRAGE : le rayon vient de --r-card (16px), comme dans Main.html et\n * Mobile.html. DesignSystem.html et Logos.html codent 14px en dur, le\n * prototype 12px. Voir DECISIONS.md. */\n\n.zv-card {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n box-shadow: var(--shadow-card);\n}\n\n/* Paddings relevés : 22 / 24 / 28 / 32px selon la densité. */\n.zv-card--pad {\n padding: 24px;\n}\n\n.zv-card--pad-lg {\n padding: 32px;\n}\n\n/* Carte de statut. Les trois familles d'état ont désormais leur filet. */\n.zv-card--ok {\n border-color: var(--ok-line);\n}\n\n.zv-card--err {\n border-color: var(--err-line);\n}\n\n/* Carte flottante : celle qui se détache au-dessus d'une scène. */\n.zv-card--float {\n box-shadow: var(--shadow-panel);\n}\n\n/* ─── Carte verre ───────────────────────────────────────────────────────\n * Surface semi-opaque posée sur un halo ou une scène. Ne fonctionne QUE\n * devant quelque chose : sur du papier nu, elle disparaît. */\n\n.zv-glass {\n background: rgba(255, 255, 255, 0.86);\n border: 1px solid rgba(255, 255, 255, 1);\n border-radius: var(--r-card);\n box-shadow:\n 0 30px 70px rgba(30, 27, 140, 0.18),\n 0 2px 8px rgba(11, 16, 32, 0.05);\n}\n\n/* ─── Encarts ───────────────────────────────────────────────────────────\n * Deux encarts distincts au relevé, longtemps confondus. Ils ne se\n * substituent pas : le premier est une citation d'interface (fond blanc,\n * texte gris), le second un extrait de document (fond panel, texte encre,\n * très aéré). */\n\n.zv-inset {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 14px 16px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--muted);\n max-width: 380px;\n}\n\n.zv-inset__key,\n.zv-inset em {\n font-style: normal;\n color: var(--accent);\n font-weight: 600;\n}\n\n.zv-inset--quiet {\n background: var(--panel);\n border-color: var(--hairline);\n font-size: 12px;\n line-height: 1.9;\n color: var(--ink);\n}\n\n/* Poussé en bas de la carte qui l'accueille. */\n.zv-inset--bottom {\n margin-top: auto;\n}\n\n/* ─── Statistiques ──────────────────────────────────────────────────────*/\n\n/* Paire de mini-statistiques au pied d'une carte produit. */\n.zv-ministat {\n display: flex;\n gap: 10px;\n}\n\n.zv-ministat__item {\n flex: 1;\n background: var(--panel);\n border: 1px solid var(--hairline);\n border-radius: var(--r-panel);\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.zv-ministat__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 18px;\n font-weight: 500;\n}\n\n/* Carte statistique autonome (prototype). */\n.zv-stat-card {\n flex: 1;\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n box-shadow: 0 12px 32px rgba(30, 27, 140, 0.08);\n}\n\n.zv-stat-card__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 22px;\n font-weight: 500;\n}\n\n/* ─── Citation ──────────────────────────────────────────────────────────\n * Fond navy, filet clair, citation en Outfit 300.\n * ⚠️ Règle de la passation : jamais de fausse citation. Tant qu'un verbatim\n * n'a pas été recueilli, le bloc reste en réserve, pas en placeholder crédible. */\n\n.zv-quote {\n background: linear-gradient(135deg, #06063a, var(--accent-deep));\n color: var(--white);\n border-radius: var(--r-card);\n padding: 28px 32px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-quote__text {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 17px;\n font-weight: 300;\n}\n\n/* ─── Carte de tarif ────────────────────────────────────────────────────\n * Trois par page au maximum. La formule recommandée se signale par le\n * bandeau indigo et sa pastille — pas par une couleur de fond. */\n\n.zv-price {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n box-shadow: var(--shadow-card);\n padding: 26px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-price--featured {\n border-top: 3px solid var(--accent-deep);\n}\n\n.zv-price__amount {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 28px;\n font-weight: 300;\n}\n\n/* Le « HT · 1re année » accolé au montant, et la mention sous la carte. */\n.zv-price__unit,\n.zv-price__note {\n font-size: 12px;\n color: var(--muted);\n font-weight: 400;\n letter-spacing: normal;\n}\n\n/* ─── FAQ ───────────────────────────────────────────────────────────────\n * Cinq questions par page au maximum. */\n\n/* Le filet, comme `.zv-fold` : sans lui, la FAQ est un aplat blanc posé sur\n * le papier, sans début ni fin. Toutes les autres surfaces blanches de la\n * vitrine — carte, encart, tarif — en portent un ; celle-ci faisait tache. */\n.zv-faq {\n background: var(--white);\n border: 1px solid var(--line);\n}\n\n/* Chaque question est un `<details>` : le repli est natif, sans JavaScript.\n * Le filet sépare les questions ENTRE elles ; la première n'en porte pas, il\n * tomberait sur celui du bloc et ferait un trait double. */\n.zv-faq__item {\n border-top: 1px solid var(--hairline);\n}\n\n.zv-faq__item:first-child {\n border-top: 0;\n}\n\n.zv-faq__q {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n padding: 18px 24px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n /* Le marqueur natif du `<summary>` tombe : le chevron du composant fait le\n * même travail sans imposer le triangle du navigateur. Même traitement que\n * `.zv-fold__titre`. */\n list-style: none;\n}\n\n.zv-faq__q::-webkit-details-marker {\n display: none;\n}\n\n/* Une FAQ de vitrine veut ses questions dans le PLAN DU DOCUMENT : c'est ce\n * que lisent les moteurs, et ce que parcourt un lecteur d'écran qui saute de\n * titre en titre. On accepte donc un `<h2>`/`<h3>`/`<h4>` comme question,\n * et on lui retire ce que le navigateur lui met — taille, graisse, marges —\n * pour qu'il se rende comme le reste des questions. Le niveau reste au\n * balisage, l'apparence au système. */\n.zv-faq__q > :is(h2, h3, h4) {\n margin: 0;\n font: inherit;\n color: inherit;\n}\n\n.zv-faq__q:hover {\n background: var(--panel);\n}\n\n/* Le chevron porte sa classe : cibler tout `svg` du titre attraperait aussi\n * une icône posée dans le libellé, qui se verrait rétrécie et retournée. */\n.zv-faq__chevron {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--accent);\n transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-faq__item[open] .zv-faq__chevron {\n rotate: 180deg;\n}\n\n.zv-faq__a {\n margin: 0;\n padding: 0 24px 18px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* ─── Aperçu d'interface ────────────────────────────────────────────────\n * La preuve produit : une question, une réponse, ses sources.\n * ⚠️ Règle de la passation : toujours de vraies données réalistes, jamais\n * de lorem — c'est ce bloc qui porte la crédibilité de la page. */\n\n.zv-ui-preview {\n background: var(--white);\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n padding: 18px 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-ui-preview__q {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n color: var(--ink);\n}\n\n.zv-ui-preview__q svg {\n width: 12px;\n height: 12px;\n flex: none;\n color: var(--label);\n}\n\n.zv-ui-preview__a {\n font-size: 11px;\n color: var(--muted);\n}\n\n/* Le compteur de sources, seul élément mis en avant de la réponse. */\n.zv-ui-preview__count {\n color: var(--accent-deep);\n font-weight: 600;\n}\n\n/* Les puces de source rétrécissent dans un aperçu. */\n.zv-ui-preview .zv-src {\n font-size: 8px;\n}\n\n/* ─── Pastille de validation ────────────────────────────────────────────*/\n\n.zv-check {\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--ok-soft);\n color: var(--ok);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n\n/* ─── Filets ────────────────────────────────────────────────────────────*/\n\n.zv-divider {\n height: 1px;\n background: var(--hairline);\n border: 0;\n}\n\n.zv-divider--strong {\n background: var(--line);\n}\n\n/* Sur fond sombre. La valeur est relevée en border-top de la ligne de\n * statistiques du manifeste ; elle est reprise ici en fond de filet. */\n.zv-divider--on-dark {\n background: rgba(255, 255, 255, 0.18);\n}\n\n/* ─── Barre de progression ──────────────────────────────────────────────\n * Deuxième des trois emplois autorisés du spectre. */\n\n.zv-progress {\n height: 3px;\n background: var(--hairline);\n border-radius: 0;\n overflow: hidden;\n}\n\n.zv-progress__fill {\n height: 3px;\n background: var(--spectre);\n}\n\n\n/* ─── Blocs éditoriaux ──────────────────────────────────────────────────\n * HORS RELEVÉ — ajoutés le 24/08/2026 à la demande de la DA (règle de\n * variété : jamais deux blocs de même forme qui se suivent, 80 % de formes\n * différentes par page). Quatre façons de poser du contenu SANS carte,\n * pour que les grilles de cartes redeviennent un choix et non un réflexe.\n * Dessinés dans les tokens existants, rien de nouveau dans la palette. */\n\n/* Rangées à filets : un libellé fort, un contenu, un trait fin entre\n * chaque rangée. Pour les listes où l'alignement fait la lecture\n * (profils rencontrés, prestations, caractéristiques). */\n.zv-rows {\n display: flex;\n flex-direction: column;\n}\n\n.zv-rows__row {\n display: flex;\n gap: 24px;\n align-items: baseline;\n flex-wrap: wrap;\n padding: 18px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-rows__row:first-child {\n border-top: none;\n}\n\n.zv-rows__key {\n font-weight: 600;\n flex: 0 0 280px;\n}\n\n.zv-rows__val {\n flex: 1;\n min-width: 260px;\n /* C'est le corps de texte du paquet — le token, pas sa valeur recopiée. */\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* Sommaire numéroté : le numéro en display léger, le contenu à droite,\n * un filet entre les entrées. Pour les suites longues qui étoufferaient\n * en cartes (offres détaillées, programmes). */\n.zv-index {\n display: flex;\n flex-direction: column;\n}\n\n.zv-index__item {\n display: flex;\n gap: 28px;\n align-items: flex-start;\n padding: 28px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-index__item:first-child {\n border-top: none;\n padding-top: 0;\n}\n\n.zv-index__num {\n font-family: var(--font-display);\n font-weight: 300;\n font-size: 40px;\n line-height: 1;\n letter-spacing: -0.02em;\n color: var(--accent-deep);\n flex: 0 0 64px;\n}\n\n.zv-index__body {\n flex: 1;\n min-width: 0;\n}\n\n/* Variante en colonnes : les mêmes entrées numérotées, posées en grille\n * de trois, sans filets — l'espace fait la séparation. */\n.zv-index--cols {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 40px 32px;\n}\n\n.zv-index--cols .zv-index__item {\n flex-direction: column;\n gap: 10px;\n padding: 0;\n border-top: none;\n}\n\n.zv-index--cols .zv-index__num {\n flex: none;\n font-size: 32px;\n color: var(--accent-line);\n}\n\n/* Déroulé vertical : le rail et ses points, à la verticale. La déclinaison\n * verticale de zv-steps, pour les méthodes qui se lisent comme un récit.\n * Les points reprennent les couleurs des points de méthode (l'un des\n * trois emplois autorisés du spectre). */\n.zv-flow {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 36px;\n padding-left: 32px;\n}\n\n.zv-flow::before {\n content: '';\n position: absolute;\n left: 5px;\n top: 8px;\n bottom: 8px;\n width: 2px;\n background: var(--line);\n}\n\n.zv-flow__step {\n position: relative;\n --zv-flow-dot: var(--iris);\n}\n\n.zv-flow__step::before {\n content: '';\n position: absolute;\n left: -32px;\n top: 5px;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--zv-flow-dot);\n box-shadow:\n 0 0 0 4px var(--paper),\n 0 0 0 5px var(--accent-line);\n}\n\n.zv-flow__step--violet {\n --zv-flow-dot: var(--violet);\n}\n\n.zv-flow__step--cyan {\n --zv-flow-dot: var(--cyan);\n}\n\n/* Encart d'appel : une bande claire sur fond indigo dilué, un titre, une\n * action. Pour l'offre transverse qui doit se voir sans crier\n * (remise Campus, renvoi vers une autre page). */\n.zv-callout {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n flex-wrap: wrap;\n background: var(--accent-soft);\n border: 1px solid var(--accent-line);\n border-radius: var(--r-card);\n padding: 32px 36px;\n}\n\n.zv-callout__title {\n margin: 0;\n font-family: var(--font-display);\n font-weight: 300;\n letter-spacing: -0.01em;\n font-size: 26px;\n line-height: 1.2;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n\n@media (max-width: 767px) {\n\n /* Le `min-width: 260px` de la valeur ne tient pas sous le seuil : dans une\n * carte de 215px — un écran de 320px — elle débordait de 45px. La CLÉ\n * était déjà libérée (flex-basis 100%), il manquait la valeur. */\n .zv-rows__val {\n min-width: 0;\n }\n .zv-btn--sm {\n height: 36px;\n padding: 0 14px;\n font-size: 13px;\n }\n\n /* Le carré compact suit la hauteur compacte mobile. */\n .zv-btn--sm.zv-btn--carre {\n width: 36px;\n padding: 0;\n }\n\n .zv-pill {\n height: 26px;\n padding: 0 12px;\n letter-spacing: 0.16em;\n }\n\n .zv-glass {\n background: rgba(255, 255, 255, 0.88);\n box-shadow:\n 0 20px 48px rgba(30, 27, 140, 0.18),\n 0 2px 8px rgba(11, 16, 32, 0.05);\n }\n\n .zv-inset {\n max-width: none;\n }\n\n .zv-rows__key {\n flex-basis: 100%;\n }\n\n .zv-index__item {\n gap: 18px;\n padding: 22px 0;\n }\n\n .zv-index__num {\n flex-basis: 44px;\n font-size: 30px;\n }\n\n .zv-index--cols {\n grid-template-columns: minmax(0, 1fr);\n gap: 28px;\n }\n\n .zv-callout {\n padding: 24px 22px;\n }\n\n .zv-callout__title {\n font-size: 21px;\n }\n}\n\n/* ─── Le lockup : la marque et le mot ───────────────────────────────────\n *\n * Jusqu'ici le nom d'un produit voyageait cuit dans un PNG, marque et mot\n * ensemble. Un lockup de 555×200 dont la capitale mesure 70px se retrouvait\n * affiché à 15px de capitale dans une barre de navigation et 10px dans un\n * pied de page : réduit de près de cinq fois, chaque fût tombait sur une\n * fraction de pixel et recevait sa propre dose de lissage. Le mot devenait\n * gris et le V, seul à n'avoir aucun fût vertical sur quoi s'aligner,\n * semblait flotter.\n *\n * Le mot est donc un SVG (`logos/svg/<produit>-wordmark.svg`), à\n * `currentColor` : net à toute taille, et il prend enfin les nuances encre\n * / navy / blanc que le registre des logos décrit. La marque reste le PNG\n * couleur du paquet — c'est un dégradé, il ne se recolore pas.\n *\n * ⚠️ Ces tracés sont RELEVÉS sur le lockup bitmap d'origine, faute de son\n * fichier vectoriel. Ils sont fidèles à 0,1 % en largeur et nettement plus\n * lisibles que le bitmap partout où le site les pose, mais ils portent les\n * approximations du relevé : réservés à l'ÉCRAN, jusqu'à 120px de capitale.\n * Au-delà — grand format, impression — exiger le fichier d'origine.\n *\n * Les proportions viennent du lockup, mesurées au pixel : la marque vaut\n * 2,86 fois la capitale, l'écart entre les deux 0,64 fois, et les deux\n * pièces se centrent l'une sur l'autre (elles ne partagent pas de ligne de\n * base). Tout se pilote par --zv-cap, qui EST la hauteur de capitale : le\n * SVG du mot a un viewBox dont la hauteur vaut la capitale, si bien qu'une\n * hauteur CSS posée dessus se lit directement. */\n\n.zv-lockup {\n --zv-cap: 15px;\n display: inline-flex;\n align-items: center;\n gap: calc(var(--zv-cap) * 0.64);\n color: var(--ink);\n text-decoration: none;\n}\n\n.zv-lockup__mark {\n height: calc(var(--zv-cap) * 2.86);\n width: auto;\n display: block;\n}\n\n.zv-lockup__word {\n height: var(--zv-cap);\n width: auto;\n display: block;\n}\n\n/* Le seuil de lisibilité. Sous 11px de capitale, le mot ne se lit plus :\n * ce n'est plus une question de netteté mais de taille, et aucun fichier\n * n'y changera rien. On montre la marque, seule — c'est elle qui identifie.\n * Poser cette classe, ou masquer .zv-lockup__word sous un point de rupture\n * quand la barre se resserre. */\n.zv-lockup--marque-seule {\n gap: 0;\n}\n\n.zv-lockup--marque-seule .zv-lockup__word {\n display: none;\n}\n\n/* ─── Cadre média ───────────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des dix planches ne montre\n * d'image de contenu — elles sont composées, pas photographiées. Le manque\n * était pourtant total : pas un `aspect-ratio`, pas un `object-fit` dans le\n * paquet, et quatre réécritures du même cadre chez un seul consommateur.\n *\n * ⚠️ Le rapport vit sur le CADRE, jamais sur l'image. Posé sur un enfant de\n * grille aligné au centre, il ne se résout pas et la colonne s'effondre à\n * zéro — c'est un correctif de production, pas une précaution théorique.\n *\n * Le fond est posé et non laissé transparent : une image qui tarde, échoue\n * ou porte de la transparence laisserait voir la surface du dessous, soit un\n * trou blanc sur une carte, soit un trou sombre sur une bande. */\n\n.zv-media {\n position: relative;\n display: block;\n width: 100%;\n overflow: hidden;\n background: var(--hairline);\n /* Angle vif comme le reste ; aucune ombre, l'image est dans le flux. */\n border-radius: 0;\n}\n\n/* Les quatre rapports du système. Un cinquième se demande, il ne s'invente\n * pas : c'est ainsi qu'une bibliothèque finit avec dix-sept rapports dont\n * trois servent. */\n.zv-media--16-9 {\n aspect-ratio: 16 / 9;\n}\n\n.zv-media--4-3 {\n aspect-ratio: 4 / 3;\n}\n\n.zv-media--1-1 {\n aspect-ratio: 1 / 1;\n}\n\n.zv-media--3-2 {\n aspect-ratio: 3 / 2;\n}\n\n/* L'image REMPLIT le cadre : c'est le seul comportement qui le garantisse\n * plein quelle que soit la source, et une grille dont les couvertures ne\n * s'alignent pas se voit aussitôt. En absolu, pour que le cadre garde la\n * main sur sa hauteur. */\n.zv-media__img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n/* Le cas inverse — un logo, un schéma, une pièce qu'on ne peut pas rogner.\n * Le papier passe derrière : il restera du vide, autant qu'il soit choisi. */\n.zv-media--contient {\n background: var(--paper);\n}\n\n.zv-media--contient .zv-media__img {\n object-fit: contain;\n}\n\n/* Le coin d'un cadre : durée d'une vidéo, nombre de pages, mention de\n * réserve. Encre diluée et non un gris opaque — le fond d'image est\n * imprévisible, et l'encre de charte s'y pose sans jurer. */\n.zv-media__coin {\n position: absolute;\n right: 8px;\n bottom: 8px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 7px;\n background: color-mix(in srgb, var(--ink) 72%, transparent);\n color: var(--white);\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n letter-spacing: 0.08em;\n}\n\n.zv-media__coin--gauche {\n right: auto;\n left: 8px;\n}\n\n.zv-media__coin--haut {\n bottom: auto;\n top: 8px;\n}\n\n/* Voile de lecture : un titre posé SUR une image n'est lisible qu'au-dessus\n * d'un dégradé. Il part du bas, là où le texte se pose, et n'assombrit pas\n * le sujet. Il ne capte pas le pointeur — le cadre reste cliquable. */\n.zv-media__voile {\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: none;\n background: linear-gradient(\n 180deg,\n transparent 40%,\n color-mix(in srgb, var(--ink) 78%, transparent) 100%\n );\n}\n\n/* ─── Page légale ───────────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, voir DECISIONS.md). La forme d'un document\n * contractuel — CGVU, mentions légales, DPA. Le TEXTE, lui, n'est pas ici :\n * il vit dans @zevra/legal, versionné et archivé. Ce module ne porte que la\n * mise en page ; c'est ce qui permet au même texte de s'afficher sur la\n * vitrine et dans une application sans exister en deux exemplaires.\n *\n * Presque tout se compose de pièces existantes : le sommaire est .zv-rail,\n * les définitions .zv-rows, les tableaux .zv-table, les encarts .zv-callout.\n * Ne restent ici que la colonne de lecture et ce que les listes de texte\n * courant n'avaient jamais eu. */\n\n.zv-legal {\n display: flex;\n flex-direction: column;\n gap: 48px;\n /* La MESURE, et c'est la seule règle qui compte vraiment pour un contrat :\n * au-delà d'environ 80 signes par ligne, l'œil perd le début de la ligne\n * suivante — sur un texte qu'on lit déjà à reculons, c'est rédhibitoire. */\n max-width: 72ch;\n}\n\n.zv-legal__section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* Un lien vers un article précis ne doit pas déposer son titre sous la\n * barre fixe. Sans cette réserve, cliquer « Art. 7 » dans le sommaire\n * affiche le corps de l'article et cache son intitulé. */\n scroll-margin-top: 88px;\n}\n\n/* Les listes du texte courant. Le paquet n'en avait aucune : ses listes\n * étaient toutes des objets (étapes, sommaires numérotés, rangées à filets),\n * jamais l'énumération d'une clause. */\n.zv-legal__liste {\n margin: 0;\n padding-left: 22px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-size: var(--body);\n line-height: 1.6;\n color: var(--muted);\n}\n\n.zv-legal__liste li::marker {\n color: var(--accent-line);\n}\n\n/* Le renvoi vers un autre document contractuel, dans le fil d'une clause :\n * souligné, parce qu'à l'intérieur d'un paragraphe la couleur seule ne\n * suffit pas à annoncer qu'on change de contrat. */\n.zv-legal a {\n color: var(--accent);\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Le tableau d'un contrat (durées de conservation, sous-traitants). Il n'\n * emprunte PAS .zv-table de data.css : celui-là est une surface de travail\n * dont les rangées se survolent et se cliquent, ce qu'un tableau de contrat\n * ne fait jamais. Filets, chiffres alignés, et rien de plus. */\n.zv-legal__table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--app);\n line-height: 1.55;\n color: var(--ink-2);\n}\n\n.zv-legal__table th {\n border-bottom: 1px solid var(--line);\n padding: 8px 12px 8px 0;\n text-align: left;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-legal__table td {\n border-bottom: 1px solid var(--hairline);\n padding: 10px 12px 10px 0;\n vertical-align: top;\n font-variant-numeric: tabular-nums;\n}\n\n/* L'en-tête : titre, date d'entrée en vigueur, identifiant de version.\n * ⚠️ La VERSION est affichée, et ce n'est pas décoratif : c'est ce que\n * l'utilisateur cite quand il conteste, et ce que le support lui demande. */\n.zv-legal__tete {\n display: flex;\n flex-direction: column;\n gap: 10px;\n border-bottom: 1px solid var(--line);\n padding-bottom: 24px;\n}\n\n@media (max-width: 767px) {\n .zv-legal {\n gap: 36px;\n }\n\n .zv-legal__section {\n scroll-margin-top: calc(var(--topbar-h) + 12px);\n }\n}\n\n/* Encart d'accompagnement d'une clause : la reformulation en langage courant\n * (« En clair ») ou le point à ne pas manquer. Il COMMENTE la clause et ne\n * la remplace pas — d'où une surface d'accompagnement, et non l'aplat d'une\n * alerte, qui annoncerait un incident.\n * Il vit ici et non dans feedback.css : une page légale de vitrine n'a pas à\n * charger la couche applicative pour afficher une glose. */\n.zv-legal__encart {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-left: 2px solid var(--accent-line);\n background: var(--panel);\n padding: 14px 18px;\n}\n\n.zv-legal__encart--important {\n border-left-color: var(--warn);\n background: var(--warn-soft);\n}\n\n.zv-legal__encart-label {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-legal__encart--important .zv-legal__encart-label {\n color: var(--warn-ink);\n}\n\n\n/* ── layout.css ── */\n/* Zevra UI — mise en page : conteneurs, grilles, sections, chrome\n * Dépend de tokens.css et base.css. Ne pose ni reset ni fond de page :\n * c'est le rôle de base.css.\n *\n * ⚠️ Les maquettes sont des rendus FIGÉS (1440px pour les planches, 390px\n * pour Mobile.html). Les reproduire en `width: 1440px` donnerait un\n * débordement horizontal garanti sur téléphone — ce que les règles du projet\n * interdisent explicitement. Les conteneurs sont donc fluides et plafonnés,\n * ce qui rend le rendu identique au-delà de 1440px et utilisable en dessous.\n */\n\n/* ─── Page ──────────────────────────────────────────────────────────────*/\n\n.zv-page {\n width: 100%;\n max-width: 1440px;\n margin: 0 auto;\n position: relative;\n overflow: hidden;\n background: var(--paper);\n}\n\n/* ─── Gouttières ────────────────────────────────────────────────────────\n * Trois retraits relevés, et un seul mécanisme pour les poser : ces\n * utilitaires. Les blocs ci-dessous ne portent QUE leur rythme vertical,\n * jamais leur retrait horizontal — sans quoi poser un utilitaire par-dessus\n * un bloc écraserait son padding vertical (shorthand contre longhand). */\n\n.zv-gutter {\n padding-left: 72px;\n padding-right: 72px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Blocs centrés : hero, CTA, manifeste du prototype. */\n.zv-gutter--wide {\n padding-left: 120px;\n padding-right: 120px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Chrome haut du prototype. */\n.zv-gutter--chrome {\n padding-left: 56px;\n padding-right: 56px;\n max-width: var(--page-max);\n margin-inline: auto;\n}\n\n/* Variante en marge, pour les blocs détachés du bord (grille produits,\n * vitrine, pied de page). Le max() tient les deux promesses à la fois :\n * jamais moins de 72px de marge visible, et un centrage sur --page-max dès\n * que l'écran en offre davantage. */\n.zv-inset-x {\n max-width: calc(var(--page-max) - 144px);\n margin-left: max(72px, calc((100% - (var(--page-max) - 144px)) / 2));\n margin-right: max(72px, calc((100% - (var(--page-max) - 144px)) / 2));\n}\n\n/* ─── Grille 12 colonnes ────────────────────────────────────────────────\n * Répartitions relevées : 3+9, 8+4, 6+6. */\n\n.zv-grid-12 {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n}\n\n.zv-col-3 { grid-column: span 3; }\n.zv-col-4 { grid-column: span 4; }\n.zv-col-5 { grid-column: span 5; }\n.zv-col-6 { grid-column: span 6; }\n.zv-col-7 { grid-column: span 7; }\n.zv-col-8 { grid-column: span 8; }\n.zv-col-9 { grid-column: span 9; }\n\n/* Grille de cartes produit. */\n.zv-grid-3 {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 24px;\n}\n\n.zv-grid-4 {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 16px;\n}\n\n/* ─── Sections ──────────────────────────────────────────────────────────*/\n\n/* En-tête de section : titre à gauche sur 8 colonnes, texte\n * d'accompagnement à droite sur 4, alignés par le bas. */\n.zv-section-head {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n align-items: end;\n padding-top: 104px;\n}\n\n.zv-section-head__main {\n grid-column: span 8;\n display: flex;\n flex-direction: column;\n gap: 18px;\n}\n\n.zv-section-head__aside {\n grid-column: span 4;\n}\n\n/* Section de planche : filet haut, titre sur 3 colonnes, corps sur 9.\n * C'est la structure des planches de documentation (DesignSystem, Logos),\n * pas celle des pages produit. */\n.zv-sheet-section {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 32px;\n padding: 56px 0;\n border-top: 1px solid var(--line);\n}\n\n.zv-sheet-section__title {\n grid-column: span 3;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.zv-sheet-section__body {\n grid-column: span 9;\n}\n\n/* Séparateur de section, avec son rythme vertical. */\n.zv-section-rule {\n border-top: 1px solid var(--line);\n padding-top: 40px;\n padding-bottom: 40px;\n}\n\n.zv-section-rule--quiet {\n border-top-color: var(--hairline);\n padding-top: 18px;\n padding-bottom: 28px;\n}\n\n/* ─── Blocs centrés : hero et CTA ───────────────────────────────────────*/\n\n.zv-hero {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding-top: 96px;\n gap: 28px;\n z-index: 2;\n}\n\n.zv-cta {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding-top: 128px;\n padding-bottom: 96px;\n gap: 24px;\n}\n\n.zv-cta .zv-lead {\n max-width: 580px;\n font-size: 17px;\n}\n\n/* ─── Bandeau d'annonce ─────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La bande AU-DESSUS de la\n * barre de navigation : annonce de version, information réglementaire. Le\n * ton information de l'alerte (--accent-soft sur --accent-line), en pleine\n * largeur. UN SEUL par page, et jamais pour du marketing permanent : un\n * bandeau qui ne part pas devient un bourrelet de chrome. */\n\n.zv-banner {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--accent-line);\n background: var(--accent-soft);\n padding: 9px 20px;\n font-size: 13px;\n font-weight: 500;\n text-align: center;\n color: var(--accent);\n}\n\n/* Le lien du bandeau se souligne : sur un fond déjà indigo, la couleur\n * seule ne suffit plus à le distinguer du texte. */\n.zv-banner a {\n color: var(--accent-deep);\n font-weight: 600;\n text-decoration: underline;\n}\n\n/* ─── Chrome : navigation ───────────────────────────────────────────────*/\n\n.zv-nav {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 26px;\n z-index: 2;\n}\n\n.zv-nav__logo {\n height: 42px;\n width: auto;\n display: block;\n}\n\n.zv-nav__links {\n display: flex;\n align-items: center;\n gap: 34px;\n}\n\n.zv-navlink {\n font-size: 14px;\n font-weight: 500;\n color: var(--accent);\n text-decoration: none;\n}\n\n/* ─── Zone d'actions de la barre ────────────────────────────────────────\n * Le lanceur et le compte, à droite. Elle ne fait PAS partie de\n * `.zv-nav__links` et c'est tout l'intérêt : cette rangée-là disparaît sous\n * le point de rupture (et sous 1150px pour `.zv-nav--riche`), remplacée par\n * le burger. Le compte, lui, doit rester — sinon on ne peut plus se\n * connecter depuis un téléphone, et c'est la première chose qu'on y fait.\n *\n * Elle se pose APRÈS `.zv-nav__links` dans le balisage, et avant le burger. */\n.zv-nav__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex: none;\n /* Décolle du dernier lien de la rangée quand les deux coexistent. */\n margin-left: 10px;\n}\n\n@media (max-width: 767px) {\n .zv-nav__actions {\n margin-left: 0;\n }\n}\n\n/* Bouton burger de la barre vitrine.\n *\n * ⚠️ HORS RELEVÉ des planches vitrine, qui sont toutes des rendus figés à\n * 1440px et n'ont donc jamais eu de menu mobile. Mobile.html en montre un,\n * mais sans en donner le tracé.\n *\n * Il est ici parce qu'une page sans lui n'a AUCUN moyen d'atteindre sa\n * navigation sur téléphone : sous le seuil, .zv-nav__links doit être masqué\n * (.zv-only-desktop) et rien ne le remplace. Le tiroir qu'il ouvre vit dans\n * overlays.css.\n *\n * Il n'apparaît que sous le seuil : au-dessus, les liens sont là. */\n.zv-nav__burger {\n display: none;\n align-items: center;\n justify-content: center;\n /* 40px de cible tactile, obtenus par la taille et non par du padding : un\n * bouton de 24px entouré de marge reste un bouton de 24px pour le doigt. */\n width: 40px;\n height: 40px;\n /* Les marges négatives ramènent le bouton à l'alignement optique de la\n * barre : sans elles, sa zone tactile décale le logo et le bord droit. */\n margin: -8px -8px -8px 0;\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--ink);\n cursor: pointer;\n}\n\n.zv-nav__burger svg {\n width: 20px;\n height: 20px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* Bouts francs — le trait carré du jeu d'icônes (25/08/2026). Le burger\n * applicatif, relevé « extrémités franches », y était déjà. */\n stroke-linecap: square;\n}\n\n/* Le tiroir de la vitrine : liens empilés, respiration généreuse. Le panneau\n * lui-même vient d'overlays.css ; ceci n'habille que son contenu. */\n.zv-nav-drawer {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 20px;\n}\n\n.zv-nav-drawer__link {\n display: flex;\n align-items: center;\n /* Chaque entrée fait au moins une cible tactible pleine : une liste de\n * liens serrés est la première cause de faux appuis sur téléphone. */\n min-height: var(--tap);\n padding: 0 12px;\n border-radius: var(--r-panel);\n font-size: 15px;\n font-weight: 500;\n color: var(--ink);\n text-decoration: none;\n}\n\n.zv-nav-drawer__link:hover {\n background: var(--panel);\n color: var(--accent);\n}\n\n.zv-nav-drawer__link--on {\n color: var(--accent);\n}\n\n.zv-navlink--quiet {\n color: var(--label);\n}\n\n/* ─── Sélecteur de langue ───────────────────────────────────────────────\n * Des initiales au registre mono, pas un menu : trois langues se lisent\n * d'un coup d'œil, et un menu pour trois entrées coûte un clic pour rien.\n * Une page qui n'existe que dans une langue ne le pose pas — il vaut mieux\n * pas de sélecteur qu'un sélecteur qui mène à des 404. */\n\n.zv-langues {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n}\n\n.zv-langue {\n color: var(--label);\n text-decoration: none;\n letter-spacing: 0.12em;\n transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-langue:hover {\n color: var(--accent);\n}\n\n.zv-langue--on {\n color: var(--ink);\n font-weight: 700;\n}\n\n/* Dans le tiroir, il se détache en pied de liste. */\n.zv-langues--tiroir {\n display: flex;\n gap: 16px;\n margin-top: 18px;\n padding-top: 14px;\n border-top: 1px solid var(--hairline);\n}\n\n/* ─── Le tiroir, groupé ─────────────────────────────────────────────────\n * Quand la barre porte des familles, les aplatir en une liste unique rend\n * le tiroir illisible. Un label capitale par famille, ses entrées en\n * dessous : la même architecture, repliée. */\n\n.zv-nav-drawer__groupe {\n display: block;\n margin: 16px 0 4px;\n}\n\n.zv-nav-drawer__groupe:first-child {\n margin-top: 0;\n}\n\n.zv-nav-drawer__link--sous {\n font-size: 15px;\n padding-block: 7px;\n}\n\n/* Chrome fixe : voile dégradé plutôt qu'un fond plein, pour que le contenu\n * s'y fonde au défilement.\n *\n * Deux usages, une seule classe. Il est LA BARRE quand il porte directement\n * ses pièces ; il l'ENVELOPPE quand il contient une .zv-nav — c'est la forme\n * qu'impose une nav complète, dont les menus déroulants et le tiroir ont\n * besoin d'un conteneur à eux. Le `:has` bascule tout seul, plutôt que de\n * demander un modificateur qu'on oublierait.\n *\n * ⚠️ backdrop-filter fait de ce bloc le containing block de TOUT descendant\n * en position fixe. Le tiroir (fixe lui aussi) doit donc vivre EN DEHORS,\n * en frère — dedans, il serait rogné par la hauteur de la barre.\n *\n * Le voile venait en rgba littéral, c'est-à-dire hors token : --paper\n * repeint, la barre serait restée sur l'ancienne couleur. */\n.zv-header {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 22px;\n padding-bottom: 12px;\n z-index: 40;\n /* Le voile est PLEIN derrière la barre, et il se dégrade EN DESSOUS d'elle\n * (::after). Il était auparavant dégradé sur toute sa hauteur : à sa base\n * il ne restait donc presque rien, et un titre de 38 ou 56px qui passait\n * dessous se retrouvait COUPÉ EN DEUX — moitié voilée, moitié nette, avec\n * les entrées de nav posées sur ses lettres. Deux textes superposés, aucun\n * lisible. Signalé en recette sur mcp-factory.\n * Le dégradé n'est pas perdu : il vit sous la barre, là où il adoucit le\n * passage sans se disputer la place avec la navigation. */\n background: color-mix(in srgb, var(--paper) 92%, transparent);\n /* ⚠️ LE PRÉFIXÉ D'ABORD. Lightning CSS — le compilateur de Tailwind v4,\n * donc celui de toutes nos applications Next — SUPPRIME `backdrop-filter`\n * non préfixé quand il précède la forme `-webkit-`. Or Firefox ne connaît\n * QUE la forme non préfixée : la barre y perdait son flou. Reproduit avec\n * la chaîne exacte des apps, dans les deux ordres. L'ordre est donc une\n * contrainte, pas un style.\n *\n * Le voile, lui, reste en `color-mix` : le compilateur le traite\n * CORRECTEMENT — il émet un repli opaque puis la vraie valeur sous\n * `@supports`, ce qu'on lit dans les bundles livrés. Une note antérieure\n * affirmait qu'il le « réduisait » ; elle prenait le repli pour le\n * résultat. Ne pas « corriger » vers `rgb(from …)` : cette syntaxe sort du\n * compilateur NUE, sans repli, et un navigateur qui l'ignore n'a alors\n * plus AUCUN fond sous une barre fixe — 3,5 % d'usage qui voyaient le\n * voile correctement et ne verraient plus rien. */\n -webkit-backdrop-filter: blur(6px);\n backdrop-filter: blur(6px);\n}\n\n/* La frange de dégradé, posée SOUS la barre. `pointer-events: none` : elle\n * couvre le haut du contenu, elle ne doit rien intercepter. */\n.zv-header::after {\n content: '';\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n height: 24px;\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--paper) 92%, transparent),\n color-mix(in srgb, var(--paper) 0%, transparent)\n );\n -webkit-backdrop-filter: blur(6px);\n backdrop-filter: blur(6px);\n pointer-events: none;\n}\n\n.zv-header:has(> .zv-nav) {\n display: block;\n padding-top: 0;\n padding-bottom: 0;\n}\n\n.zv-header > .zv-nav {\n padding-bottom: 14px;\n}\n\n/* ─── Frise d'étapes ────────────────────────────────────────────────────\n * Troisième et dernier emploi autorisé du spectre : les points de méthode,\n * dans l'ordre iris → violet → cyan. */\n\n.zv-steps {\n position: relative;\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 32px;\n}\n\n.zv-steps__rail {\n position: absolute;\n top: 5px;\n left: 0;\n width: 100%;\n height: 2px;\n background: var(--line);\n}\n\n.zv-steps__progress {\n position: absolute;\n top: 5px;\n left: 0;\n height: 2px;\n background: linear-gradient(90deg, var(--iris), var(--violet) 70%, var(--cyan));\n}\n\n.zv-step {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 36px 32px 0 0;\n}\n\n.zv-step:last-child {\n padding-right: 0;\n}\n\n.zv-step__dot {\n position: absolute;\n top: 0;\n left: 0;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n /* Le double anneau détache la puce du rail qui passe derrière. */\n background: var(--iris);\n box-shadow:\n 0 0 0 4px var(--paper),\n 0 0 0 5px var(--accent-line);\n}\n\n/* Troisième et dernier emploi du spectre : les puces, dans l'ordre\n * iris → violet → cyan. La couleur se pose sur la puce elle-même, et non\n * par :nth-child sur l'étape : le rail et sa progression sont des enfants\n * du même conteneur, ils décalent la numérotation et laissent les puces\n * suivantes sans fond. */\n.zv-step__dot--violet {\n background: var(--violet);\n}\n\n.zv-step__dot--cyan {\n background: var(--cyan);\n}\n\n/* ─── Ligne de statistiques ─────────────────────────────────────────────\n * Posée sur le bandeau manifeste, d'où le filet clair. */\n\n.zv-stats {\n display: flex;\n gap: 64px;\n padding-top: 32px;\n border-top: 1px solid rgba(255, 255, 255, 0.18);\n}\n\n.zv-stat {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.zv-stat__value {\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 40px;\n font-weight: 300;\n}\n\n/* ─── Pied de page ──────────────────────────────────────────────────────*/\n\n.zv-footer {\n position: relative;\n padding-top: 40px;\n padding-bottom: 40px;\n border-top: 1px solid var(--line);\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 48px;\n}\n\n.zv-footer__brand {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 320px;\n}\n\n.zv-footer__cols {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 48px;\n font-size: 13px;\n color: var(--muted);\n line-height: 2;\n}\n\n/* ─── Bande de preuve sociale ───────────────────────────────────────────\n * Label à gauche, noms de clients à droite, en Outfit 500 atténué.\n * ⚠️ Règle de la passation : jamais plus d'une ligne. Au-delà, la preuve\n * se dilue au lieu de s'accumuler. */\n\n.zv-proof-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 24px 40px;\n}\n\n.zv-proof-bar__names {\n display: flex;\n gap: 36px;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: 17px;\n font-weight: 500;\n color: var(--ink);\n opacity: 0.72;\n}\n\n/* ─── Carrousel ─────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Défilement natif\n * scroll-snap, AUCUN JavaScript : pas de flèches (elles exigeraient un\n * script et un arbitrage de dessin), pas de points. Le clavier passe par le\n * focus du conteneur, que la façade rend focalisable.\n *\n * La barre de défilement tombe — le rail visible transformerait la vitrine\n * en fenêtre d'application. C'est un MASQUAGE, pas l'habillage que\n * navigation.css refuse : aucune dimension inventée.\n *\n * ⚠️ Un carrousel reste un aveu d'espace : la règle de variété lui préfère\n * une grille tant que le contenu tient. La largeur des éléments vient du\n * consommateur — un carrousel qui la fixerait déciderait de la mise en page\n * à la place de la grille. */\n\n.zv-carousel {\n display: flex;\n gap: 24px;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n}\n\n.zv-carousel::-webkit-scrollbar {\n display: none;\n}\n\n.zv-carousel__item {\n flex: none;\n min-width: 0;\n scroll-snap-align: start;\n}\n\n/* ─── Pied de page réduit ───────────────────────────────────────────────\n * Pages secondaires. Le pied complet à quatre colonnes reste réservé à la\n * page d'accueil. */\n\n.zv-footer-mini {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n padding: 20px 32px;\n background: var(--paper);\n}\n\n.zv-footer-mini__logo {\n height: 22px;\n width: auto;\n display: block;\n}\n\n/* ─── Empilement ────────────────────────────────────────────────────────\n * Inventaire complet des couches relevées. Toute nouvelle couche s'arbitre\n * par rapport à celle-ci plutôt qu'en surenchérissant. */\n\n.zv-layer-scene { z-index: 1; } /* scène constellation, sous la nav */\n.zv-layer-content { z-index: 2; } /* nav, hero */\n.zv-layer-progress { z-index: 5; } /* barre de progression en flux */\n.zv-layer-chrome { z-index: 40; } /* header fixe, rail d'étapes */\n.zv-layer-top { z-index: 50; } /* barre de progression fixe */\n\n/* ─── Bascule d'affichage ───────────────────────────────────────────────\n * HORS RELEVÉ : utilitaires mécaniques, pas des décisions de charte. Ils\n * existent parce que les maquettes desktop et mobile ne montrent PAS les\n * mêmes éléments de navigation — les liens de nav desktop deviennent un\n * menu burger — et que sans eux chaque app réinvente la même règle. */\n\n/* Chaque utilitaire MASQUE dans la plage où l'élément n'a pas sa place, et ne\n * dit RIEN dans l'autre.\n *\n * La version précédente posait `display: none` par défaut puis\n * `display: revert` au-dessus du seuil. C'était faux : `revert` revient au\n * défaut du NAVIGATEUR — `inline` pour un span — et écrasait donc le\n * `display: flex` que l'élément tenait de sa propre classe. Une rangée de\n * liens de navigation perdait sa gouttière et ses libellés se collaient.\n *\n * En ne déclarant rien dans la plage visible, l'élément garde son display. */\n@media (max-width: 767px) {\n .zv-only-desktop {\n display: none;\n }\n}\n\n@media (min-width: 768px) {\n .zv-only-mobile {\n display: none;\n }\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Les grilles s'empilent, les gouttières se resserrent aux valeurs de\n * Mobile.html (20 / 22 / 24px). */\n\n@media (max-width: 767px) {\n .zv-gutter,\n .zv-gutter--wide,\n .zv-gutter--chrome {\n padding-left: 20px;\n padding-right: 20px;\n }\n\n .zv-inset-x {\n margin-left: 20px;\n margin-right: 20px;\n }\n\n .zv-banner {\n padding: 8px 16px;\n font-size: 12px;\n }\n\n .zv-carousel {\n gap: 16px;\n }\n\n .zv-grid-12,\n .zv-grid-3,\n .zv-section-head,\n .zv-sheet-section,\n .zv-steps {\n grid-template-columns: minmax(0, 1fr);\n gap: 20px;\n }\n\n .zv-grid-4 {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .zv-col-3,\n .zv-col-4,\n .zv-col-5,\n .zv-col-6,\n .zv-col-7,\n .zv-col-8,\n .zv-col-9,\n .zv-section-head__main,\n .zv-section-head__aside,\n .zv-sheet-section__title,\n .zv-sheet-section__body {\n grid-column: auto;\n }\n\n .zv-section-head {\n padding-top: 44px;\n align-items: start;\n text-align: center;\n justify-items: center;\n }\n\n .zv-hero {\n padding-top: 44px;\n gap: 18px;\n }\n\n .zv-cta {\n padding-top: 56px;\n padding-bottom: 48px;\n gap: 16px;\n }\n\n .zv-nav {\n padding-top: 20px;\n }\n\n /* Le burger n'existe que là où les liens disparaissent. */\n .zv-nav__burger {\n display: inline-flex;\n }\n\n .zv-nav__logo {\n height: 30px;\n }\n\n /* Les liens de navigation desktop n'ont pas d'équivalent mobile au\n * relevé : Mobile.html les remplace par un menu burger. Ils ne sont pas\n * masqués d'office — cela casserait les pages sans burger — mais\n * .zv-only-desktop est là pour le faire en une classe. */\n\n .zv-steps__rail,\n .zv-steps__progress {\n display: none;\n }\n\n .zv-step {\n padding: 0 0 0 24px;\n }\n\n .zv-step__dot {\n top: 4px;\n }\n\n .zv-stats {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 16px;\n padding-top: 16px;\n }\n\n .zv-stat {\n gap: 4px;\n }\n\n .zv-stat__value {\n font-size: 28px;\n }\n\n .zv-proof-bar,\n .zv-footer-mini {\n flex-direction: column;\n align-items: center;\n text-align: center;\n gap: 12px;\n padding-left: 20px;\n padding-right: 20px;\n }\n\n /* Les noms de clients passent en lignes plutôt qu'en une seule rangée,\n * qui déborderait. La règle « jamais plus d'une ligne » vaut pour le\n * desktop : à 390px, aucune rangée de six noms ne tient. */\n .zv-proof-bar__names {\n flex-wrap: wrap;\n justify-content: center;\n gap: 8px 20px;\n font-size: 15px;\n }\n\n .zv-footer {\n flex-direction: column;\n gap: 32px;\n }\n\n .zv-footer__cols {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 24px;\n }\n}\n\n/* ─── Rail de lecture ───────────────────────────────────────────────────\n *\n * La table des matières d'une page longue, posée sur le côté : un libellé,\n * un point, et le point courant qui s'allume. Elle NAVIGUE (chaque entrée\n * est une ancre) et elle SUIT (le module rail.js marque la section en\n * cours) — les deux, sinon ce n'est qu'un décor qui occupe la marge.\n *\n * ⚠️ Elle demande de la place à côté du texte. Sous le point de rupture\n * elle disparaît : un rail fixe sur un téléphone se pose sur la lecture.\n * Au-dessus, c'est la mesure de la page qui décide — une page dont le\n * contenu court d'un bord à l'autre jusqu'à 1100px gagnera à relever le\n * seuil chez elle.\n *\n * L'empilement vient de .zv-layer-chrome, à poser avec : le rail est du\n * chrome, il passe au-dessus des décors de scène. */\n\n.zv-rail {\n position: fixed;\n right: 40px;\n top: 50%;\n translate: 0 -50%;\n display: none;\n flex-direction: column;\n align-items: flex-end;\n gap: 16px;\n}\n\n@media (min-width: 768px) {\n .zv-rail {\n display: flex;\n }\n}\n\n/* Sur la gauche, l'ordre s'inverse : le point précède le libellé, sinon il\n * se retrouve au milieu de la colonne au lieu d'en tenir le bord. */\n.zv-rail--gauche {\n right: auto;\n left: 40px;\n align-items: flex-start;\n}\n\n.zv-rail--gauche .zv-rail__item {\n flex-direction: row-reverse;\n}\n\n.zv-rail__item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 3px 0;\n color: var(--muted);\n text-decoration: none;\n /* Un rail flotte au-dessus de ce que la page met dans sa marge : un\n * libellé gris pâle sur un décor pâle devient illisible par manque de\n * contraste, sans qu'aucun empilement soit en cause. Le halo couleur\n * papier le détache de tout ce qui passe dessous, sans lui donner de\n * cadre. */\n text-shadow:\n 0 0 6px var(--paper),\n 0 0 12px var(--paper);\n transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n/* Le libellé est posé au registre mono par l'appelant : il doit suivre la\n * couleur de son entrée, pas garder la sienne. */\n.zv-rail__item .zv-mono {\n color: inherit;\n}\n\n.zv-rail__item:hover {\n color: var(--accent-deep);\n}\n\n.zv-rail__dot {\n width: 8px;\n height: 8px;\n flex: none;\n border-radius: 50%;\n background: var(--accent-line);\n transition:\n background 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1)),\n scale 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-rail__item:hover .zv-rail__dot {\n background: var(--accent);\n scale: 1.35;\n}\n\n/* L'entrée courante, posée par rail.js. Le halo du point est le seul\n * signal dont une navigation discrète a besoin — pas de fond, pas de\n * filet. */\n.zv-rail__item--on {\n color: var(--accent-deep);\n}\n\n.zv-rail__item--on .zv-rail__dot {\n background: var(--iris);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--iris) 18%, transparent);\n}\n\n/* ─── Le seuil d'une nav riche ──────────────────────────────────────────\n *\n * La bascule vers le tiroir est à 767px, réglée sur la nav de référence :\n * quatre liens simples. Une nav qui porte l'architecture d'un site — cinq\n * familles à menu, un sélecteur de langue, un appel à l'action — déborde\n * bien avant. Mesuré : entre 768 et 1150px, le logo passe SOUS la première\n * entrée.\n *\n * `.zv-nav--riche` déplace le seuil à 1150px, POUR LA BARRE SEULEMENT. Ce\n * n'est pas un second point de rupture de mise en page : le reste de la\n * page garde l'unique seuil de la charte. C'est le seuil de débordement\n * d'un composant, et il se déclare sur ce composant. */\n\n@media (max-width: 1150px) {\n .zv-nav--riche .zv-nav__links {\n display: none;\n }\n\n .zv-nav--riche .zv-nav__burger {\n display: inline-flex;\n }\n\n .zv-nav--riche .zv-nav__logo {\n height: 30px;\n }\n}\n\n\n/* ── surfaces.css ── */\n/* Zevra UI — surfaces : halos, sols quadrillés, voiles, bandeau manifeste —\n * et, depuis le 25/08/2026 (hors relevé, voir DECISIONS.md), les formats\n * sociaux. Dépend de tokens.css. Indépendant des autres modules.\n *\n * C'est ce qui distingue une page Zevra d'une page blanche bien composée.\n * Les surfaces décoratives (halo, sol, voile, filigrane, lueur) se posent\n * en absolu derrière le contenu, ne captent jamais le pointeur, et n'ont\n * pas à être lues par un lecteur d'écran (les poser en aria-hidden) ; elles\n * supposent un parent `position: relative; overflow: hidden`.\n * ⚠️ Les formats sociaux, eux, sont du CONTENU — une planche d'export qui\n * porte un texte : jamais d'aria-hidden sur eux.\n */\n\n/* ─── Halo ambiant ──────────────────────────────────────────────────────\n * Le violet et le cyan du spectre, très dilués, derrière un bloc centré.\n * Ce n'est pas un emploi du spectre en aplat : c'est une lumière. */\n\n.zv-halo {\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n border-radius: 50%;\n pointer-events: none;\n}\n\n/* Derrière un hero. */\n.zv-halo--hero {\n top: -140px;\n width: 1400px;\n height: 900px;\n background: radial-gradient(\n closest-side,\n rgba(138, 58, 248, 0.2),\n rgba(20, 194, 248, 0.1) 48%,\n rgba(245, 246, 250, 0) 72%\n );\n}\n\n/* Derrière un CTA final : plus petit, plus discret. */\n.zv-halo--cta {\n top: 40px;\n width: 900px;\n height: 500px;\n background: radial-gradient(\n closest-side,\n rgba(138, 58, 248, 0.18),\n rgba(20, 194, 248, 0.08) 55%,\n rgba(245, 246, 250, 0) 75%\n );\n}\n\n/* Noyau lumineux d'une scène : blanc au centre, pour détacher un objet. */\n.zv-halo--core {\n top: 42%;\n width: 980px;\n height: 560px;\n transform: translate(-50%, -50%);\n background: radial-gradient(\n closest-side,\n rgba(255, 255, 255, 1) 0%,\n rgba(228, 222, 255, 0.85) 35%,\n rgba(138, 58, 248, 0.14) 62%,\n rgba(20, 194, 248, 0.06) 80%,\n rgba(245, 246, 250, 0) 100%\n );\n}\n\n/* ─── Sol quadrillé ─────────────────────────────────────────────────────\n * Une grille indigo très pâle, basculée en perspective : le « sol » sur\n * lequel les glyphes produit sont posés. */\n\n.zv-grid-floor {\n position: absolute;\n left: 50%;\n bottom: 0;\n width: 1800px;\n height: 560px;\n transform: translateX(-50%) perspective(900px) rotateX(58deg);\n transform-origin: 50% 100%;\n pointer-events: none;\n background-image:\n linear-gradient(rgba(88, 72, 248, 0.13) 1px, transparent 1px),\n linear-gradient(90deg, rgba(88, 72, 248, 0.13) 1px, transparent 1px);\n background-size: 64px 64px;\n}\n\n/* Variante à l'échelle d'une carte. */\n.zv-grid-floor--card {\n width: 900px;\n height: 260px;\n transform: translateX(-50%) perspective(600px) rotateX(62deg);\n}\n\n/* ─── Voiles de fondu ───────────────────────────────────────────────────\n * Posés PAR-DESSUS un sol ou une scène pour en noyer les bords dans le\n * papier. Sans eux, la grille se coupe net et le truc se voit. */\n\n.zv-fade {\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: linear-gradient(\n 180deg,\n var(--paper) 0%,\n rgba(245, 246, 250, 0) 40%,\n rgba(245, 246, 250, 0) 66%,\n var(--paper) 100%\n );\n}\n\n/* Variante sur fond blanc (intérieur de carte). */\n.zv-fade--white {\n background: linear-gradient(\n 180deg,\n #fff 0%,\n rgba(255, 255, 255, 0) 35%,\n rgba(255, 255, 255, 0) 70%,\n #fff 100%\n );\n}\n\n/* ─── Bandeau manifeste ─────────────────────────────────────────────────\n * La seule surface sombre du système. Le dégradé est celui du logo, en\n * diagonale. On n'y inverse PAS les neutres : les labels passent en indigo\n * clair (--accent-on-dark), les filets en blanc à 18 %. */\n\n.zv-band {\n position: relative;\n overflow: hidden;\n background: var(--manifeste);\n color: var(--white);\n padding-top: 112px;\n padding-bottom: 112px;\n}\n\n.zv-band__content {\n position: relative;\n z-index: 1;\n}\n\n/* L'emphase de base.css pose --ink sur les strong de zv-body/zv-lead — sur\n * le navy du manifeste, cette encre disparaît dans le fond. Dans la bande,\n * l'emphase hérite de la couleur ambiante (blanc), et seule la graisse la\n * distingue. */\n.zv-band .zv-body strong,\n.zv-band .zv-lead strong {\n color: inherit;\n}\n\n/* Filigrane : le glyphe, blanchi et presque effacé, débordant du cadre. */\n.zv-band__watermark {\n position: absolute;\n right: -40px;\n top: -80px;\n width: 640px;\n height: auto;\n opacity: 0.07;\n pointer-events: none;\n /* Ramène n'importe quel logo couleur en blanc pur. */\n filter: brightness(0) invert(1);\n}\n\n/* Lueur bleue au pied du bandeau, qui l'empêche de finir en aplat mort. */\n.zv-band__glow {\n position: absolute;\n left: 50%;\n bottom: -300px;\n width: 900px;\n height: 600px;\n transform: translateX(-50%);\n border-radius: 50%;\n pointer-events: none;\n background: radial-gradient(closest-side, rgba(32, 160, 240, 0.5), rgba(32, 160, 240, 0) 70%);\n}\n\n/* ─── Texte sur fond sombre ─────────────────────────────────────────────\n * Ni blanc pur, ni le gris du mode clair.\n *\n * ⚠️ POURQUOI CETTE RÈGLE EXISTE, ET POURQUOI ELLE COUVRE TOUT. Les classes\n * de texte du paquet posent leur PROPRE couleur (.zv-body, .zv-lead,\n * .zv-note et .zv-rule sont en --muted ; .zv-h1 en --ink). Cette couleur\n * l'emporte sur celle héritée de la surface : poser un paragraphe dans un\n * panneau navy donnait du gris de mode clair sur du bleu nuit, et un titre\n * h1 y devenait carrément invisible.\n *\n * La règle ne visait que `.zv-band` et deux classes. Les autres surfaces\n * sombres du système — le panneau navy, l'enveloppe .zv-text-on-dark, la\n * planche sociale — n'étaient pas couvertes, et les trois autres paliers de\n * texte non plus. Corrigé le 27/08/2026 : une seule liste, toutes les\n * surfaces, tous les paliers.\n *\n * Ce qui reste EXPLICITE et ne s'adapte pas tout seul : les labels mono\n * (`Mono ton=\"on-dark\"`) et la mise en valeur (`Em surSombre`). Ceux-là\n * changent de RÔLE sur fond sombre, pas seulement de clarté — c'est une\n * décision de rédaction, elle se pose à la main. */\n:is(.zv-band, .zv-panel-navy, .zv-text-on-dark, .zv-social--manifeste)\n :is(.zv-body, .zv-lead, .zv-rule, .zv-note),\n.zv-text-on-dark {\n color: #c7cde0;\n}\n\n/* Les titres, eux, reprennent le blanc : sur une surface sombre ils portent\n * la même hiérarchie que l'encre sur le papier. */\n:is(.zv-band, .zv-panel-navy, .zv-text-on-dark, .zv-social--manifeste)\n :is(.zv-h1, .zv-h2, .zv-h3, .zv-card-title, .zv-display) {\n color: var(--white);\n}\n\n/* Panneau navy compact (planches de logos). */\n.zv-panel-navy {\n background: linear-gradient(135deg, #06063a 0%, #0b0b5e 40%, var(--accent-deep) 100%);\n border-radius: var(--r-card);\n padding: 28px;\n color: var(--white);\n}\n\n/* ─── Glyphes ───────────────────────────────────────────────────────────\n * Les glyphes produit se posent toujours avec leur ombre portée — c'est\n * elle qui les décolle du papier. Jamais de recoloration ni d'inclinaison. */\n\n.zv-glyph {\n filter: var(--shadow-glyph);\n}\n\n/* Version appuyée : glyphe présenté seul, en grand. */\n.zv-glyph--deep {\n filter:\n drop-shadow(0 24px 32px rgba(88, 72, 248, 0.28))\n drop-shadow(0 2px 4px rgba(11, 16, 32, 0.08));\n}\n\n/* Glyphe en filigrane d'une carte, poussé dans l'angle. */\n.zv-glyph--watermark {\n position: absolute;\n top: 26px;\n right: 24px;\n width: 100px;\n height: auto;\n pointer-events: none;\n}\n\n/* ─── Formats sociaux ───────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Trois planches d'EXPORT aux\n * dimensions des réseaux — paysage 1200×630 (OpenGraph, lien LinkedIn),\n * carré 1080×1080 (post), story 1080×1920. FIGÉES en pixels, hors du point\n * de rupture : ce sont des planches qu'on CAPTURE, pas des pages qu'on\n * publie en HTML — la seule dérogation du paquet à l'interdit des largeurs\n * fixes, et elle est là pour ça.\n *\n * Elles composent les pièces existantes — bande par --manifeste, halo, sol,\n * kicker, display — et portent un pied wordmark + adresse. La règle\n * « jamais de faux contenu » s'y applique au premier chef : un visuel\n * social EST une publication. */\n\n.zv-social {\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 40px;\n flex: none;\n width: 1200px;\n height: 630px;\n /* La gouttière standard de la page (layout.css), reprise telle quelle. */\n padding: 72px;\n background: var(--paper);\n color: var(--ink);\n}\n\n.zv-social--carre {\n width: 1080px;\n height: 1080px;\n}\n\n.zv-social--story {\n width: 1080px;\n height: 1920px;\n padding: 96px 72px;\n}\n\n/* La planche sombre : le manifeste en fond, les règles du fond sombre\n * s'appliquent (--accent-on-dark pour les labels, jamais les neutres\n * inversés). */\n.zv-social--manifeste {\n background: var(--manifeste);\n color: var(--white);\n}\n\n/* Contenu et pied passent AU-DESSUS des surfaces décoratives (halo, sol),\n * comme dans une page. */\n.zv-social__contenu {\n position: relative;\n z-index: 1;\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n\n.zv-social__pied {\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 24px;\n}\n\n/* ─── Mouvement ────────────────────────────────────────────────────────\n * Les animations vivent dans motion.css, qui porte les neuf mouvements\n * autorisés par la passation. Les surfaces d'ici en sont les cibles\n * habituelles : .zv-anim-breathe sur un halo, .zv-anim-drift sur un glyphe. */\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n@media (max-width: 767px) {\n .zv-halo--hero {\n top: -160px;\n width: 600px;\n height: 520px;\n background: radial-gradient(\n closest-side,\n rgba(91, 75, 240, 0.3),\n rgba(32, 128, 240, 0.16) 45%,\n rgba(245, 246, 250, 0) 72%\n );\n }\n\n .zv-band {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n\n .zv-band__watermark {\n right: -40px;\n top: -40px;\n width: 280px;\n }\n}\n\n\n/* ── motion.css ── */\n/* Zevra UI — mouvement : les neuf animations autorisées\n * Dépend de tokens.css. Relevé dans `maquettes/Motion.html`, qui les nomme\n * et fixe leurs plages de défilement.\n *\n * NEUF, PAS UNE DE PLUS — plus un DOSAGE. La micro-entrée des blocs\n * (.zv-anim-rise--doux, arbitrée le 25/08/2026, DECISIONS.md) n'est pas un\n * dixième mouvement : c'est zv-rise à douze pixels, pour les pages\n * intérieures où l'entrée pleine de la home lasserait à la longue.\n * Les règles de la passation :\n * · un seul type de mouvement par scène, plus le flottement continu ;\n * · les entrées se font toujours vers le HAUT, jamais latéralement ;\n * · aucun mouvement sur la typographie une fois posée ;\n * · une seule courbe : cubic-bezier(.2, .7, .2, 1).\n *\n * ─── Comment c'est construit ────────────────────────────────────────────\n * L'état AU REPOS de chaque élément est son état FINAL : sans animation, la\n * page est complète et lisible.\n *\n * Les ENTRÉES se jouent UNE FOIS (arbitrage du 26/08/2026, DECISIONS.md) :\n * l'ancien pilotage par `animation-timeline: view()` rejouait chaque entrée\n * à l'envers dès qu'on remontait — le contenu disparaissait sous le lecteur,\n * et une grille haute n'était jamais « contenue », donc jamais posée. Le\n * paquet arme désormais les entrées par `html.zv-motion` (posé par\n * motion.js, importable en `@zevra/ui/motion.js`), et chaque élément se\n * révèle quand il entre au viewport, puis N'EN BOUGE PLUS. Sans JavaScript,\n * rien n'est armé : la page s'affiche complète — même dégradation douce\n * qu'avant.\n *\n * Un échafaudage de scènes (l'expérience épinglée d'une home) peut vouloir\n * garder le pilotage au défilement : poser `data-zv-motion-skip` sur la\n * scène — motion.js n'y touche pas — et re-brancher les @keyframes du\n * paquet sur ses timelines nommées. Seuls la barre de progression\n * (zv-anim-progress) et le point d'étape (zv-anim-dot-active) restent\n * scroll-pilotés ici : suivre la lecture est leur raison d'être.\n *\n * Les deux boucles (drift, breathe) animent les propriétés `translate` et\n * `scale` INDÉPENDANTES, jamais `transform` : les éléments concernés — halo\n * centré par translateX(-50%), glyphe en scène — portent déjà un transform\n * de placement, qu'une animation sur `transform` écraserait.\n *\n * Décalage : poser `--zv-delay` en style inline. Le stagger d'un groupe est\n * de 120 ms (entrées), les vagues de badges de 6 % de défilement.\n */\n\n:root {\n --zv-easing: cubic-bezier(0.2, 0.7, 0.2, 1);\n --zv-delay: 0s;\n}\n\n/* ─── Boucles continues ─────────────────────────────────────────────────\n * Les deux seules animations qui ne dépendent pas du défilement. */\n\n/* 1 · Flottement des glyphes en scène. Jamais sur du texte. */\n@keyframes zv-drift {\n 0%,\n 100% {\n translate: 0 0;\n }\n 50% {\n translate: 0 -10px;\n }\n}\n\n.zv-anim-drift {\n animation: zv-drift 7s ease-in-out infinite;\n animation-delay: var(--zv-delay);\n}\n\n/* 2 · Respiration du halo de hero. Le SEUL mouvement autorisé avant le\n * premier défilement. */\n@keyframes zv-breathe {\n 0%,\n 100% {\n scale: 1;\n opacity: 1;\n }\n 50% {\n scale: 1.06;\n opacity: 0.92;\n }\n}\n\n.zv-anim-breathe {\n animation: zv-breathe 9s ease-in-out infinite;\n}\n\n/* ─── Entrées : une révélation par visite ───────────────────────────────\n * Les @keyframes vivent au niveau racine : un échafaudage de scènes peut\n * les re-brancher sur ses propres timelines nommées (voir l'en-tête). */\n\n@keyframes zv-rise {\n from {\n opacity: 0;\n transform: translateY(16px);\n }\n}\n\n/* Le même mouvement, DOSÉ pour les blocs des pages intérieures : huit\n * pixels au lieu de seize — le bloc est posé avant que l'œil ait fini\n * d'arriver, et une visite répétée ne le remarque plus. */\n@keyframes zv-rise-doux {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n}\n\n@keyframes zv-glyph-in {\n from {\n opacity: 0;\n transform: translateY(24px) scale(0.85);\n }\n}\n\n/* La signature de la page d'accueil : les glyphes convergent vers le Z\n * depuis les quatre coins — la seule entrée latérale du système, latérale\n * parce qu'elle converge, pas parce qu'elle glisse. */\n@keyframes zv-converge-tl {\n from {\n opacity: 0;\n transform: translate(-420px, -420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-tr {\n from {\n opacity: 0;\n transform: translate(420px, -420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-bl {\n from {\n opacity: 0;\n transform: translate(-420px, 420px) scale(1.5);\n }\n}\n\n@keyframes zv-converge-br {\n from {\n opacity: 0;\n transform: translate(420px, 420px) scale(1.5);\n }\n}\n\n@keyframes zv-pop {\n from {\n opacity: 0;\n transform: scale(0.8);\n }\n}\n\n/* Le filet de spectre se dessine : on anime bien la largeur, et non une\n * mise à l'échelle — le dégradé doit se révéler, pas s'étirer. */\n@keyframes zv-filet {\n from {\n width: 0;\n }\n}\n\n/* ─── Le contrat au repos : des définitions inertes ─────────────────────\n * Chaque classe d'entrée possède ici sa définition PROPRE, volontairement\n * inerte tant que rien n'est armé : sans motion.js, aucune animation n'est\n * attachée et ces propriétés n'ont pas d'effet — la page s'affiche\n * complète. C'est aussi le point d'ancrage du dosage : le décalage d'un\n * groupe se règle en posant --zv-delay par élément (120 ms). */\n\n.zv-anim-rise {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-rise--doux {\n animation-duration: 0.45s;\n}\n\n.zv-anim-glyph-in {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-converge {\n animation-delay: var(--zv-delay);\n}\n\n.zv-anim-converge--tr {\n animation-name: zv-converge-tr;\n}\n\n.zv-anim-converge--bl {\n animation-name: zv-converge-bl;\n}\n\n.zv-anim-converge--br {\n animation-name: zv-converge-br;\n}\n\n.zv-anim-pop {\n animation-delay: var(--zv-delay);\n}\n\n/* Les six vagues : 70 ms d'écart — un délai en secondes retrouve son sens\n * maintenant que l'entrée est temporelle. */\n.zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }\n.zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }\n.zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }\n.zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }\n.zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }\n\n.zv-anim-filet {\n animation-delay: var(--zv-delay);\n}\n\n/* ─── L'armement, puis la libération ────────────────────────────────────\n * `:where(html.zv-motion)` garde la spécificité d'une classe simple : les\n * définitions inertes ci-dessus (délais, noms, durées) restent lisibles à\n * travers l'armement, et `.zv-in` — plus bas dans le fichier — gagne à la\n * cascade sans surenchère. Armé, l'élément attend en PAUSE sur la première\n * image de son animation (fill both) ; libéré, elle se joue une fois et\n * s'arrête sur l'état final. Le défilement n'y reviendra jamais. */\n\n@media (prefers-reduced-motion: no-preference) {\n /* 3 · Entrée standard de tout contenu texte et panneau. */\n :where(html.zv-motion) .zv-anim-rise {\n animation: zv-rise 0.6s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n /* 3 bis · Le dosage des pages intérieures : se pose AVEC zv-anim-rise,\n * dont il ne change que l'amplitude et la durée. La home garde\n * l'entrée pleine : c'est elle la signature, le doux est le\n * quotidien. */\n :where(html.zv-motion) .zv-anim-rise--doux {\n animation-name: zv-rise-doux;\n animation-duration: 0.45s;\n }\n\n /* 4 · Entrée des glyphes produit : montée et grossissement. */\n :where(html.zv-motion) .zv-anim-glyph-in {\n animation: zv-glyph-in 0.6s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n /* 5 · La convergence. Direction par modificateur. */\n :where(html.zv-motion) .zv-anim-converge {\n animation: zv-converge-tl 0.7s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n :where(html.zv-motion) .zv-anim-converge--tr {\n animation-name: zv-converge-tr;\n }\n\n :where(html.zv-motion) .zv-anim-converge--bl {\n animation-name: zv-converge-bl;\n }\n\n :where(html.zv-motion) .zv-anim-converge--br {\n animation-name: zv-converge-br;\n }\n\n /* 6 · Badges, jetons d'entités, éléments de preuve — en cascade. */\n :where(html.zv-motion) .zv-anim-pop {\n animation: zv-pop 0.4s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n\n :where(html.zv-motion) .zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }\n :where(html.zv-motion) .zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }\n :where(html.zv-motion) .zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }\n :where(html.zv-motion) .zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }\n :where(html.zv-motion) .zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }\n\n /* 7 · Le filet de spectre, à l'entrée de chaque en-tête de section. */\n :where(html.zv-motion) .zv-anim-filet {\n animation: zv-filet 0.5s var(--zv-easing) both paused;\n animation-delay: var(--zv-delay);\n }\n}\n\n/* La libération, commune : posée par motion.js à l'entrée au viewport, une\n * seule fois. Sa place APRÈS l'armement fait tout : à spécificité égale,\n * c'est elle qui a le dernier mot. */\n.zv-in {\n animation-play-state: running;\n}\n\n/* ─── Les deux lectures du défilement, conservées ───────────────────────\n * Suivre la position de lecture est leur raison d'être : elles doivent\n * avancer ET reculer avec le lecteur. */\n\n@supports (animation-timeline: view()) {\n /* 8 · Le point d'étape s'allume à l'entrée de sa scène et s'éteint à la\n * sortie. Un seul actif à la fois. */\n @keyframes zv-dot-active {\n 0%,\n 100% {\n background: var(--accent-line);\n box-shadow: none;\n }\n 35%,\n 70% {\n background: var(--iris);\n box-shadow: 0 0 0 4px rgba(88, 72, 248, 0.18);\n }\n }\n\n .zv-anim-dot-active {\n animation: zv-dot-active linear both;\n animation-timeline: view();\n animation-range: cover 0% cover 100%;\n }\n\n /* 9 · La barre de progression, pilotée par le défilement global de la\n * page — la seule à lire `scroll(root)` plutôt que sa propre entrée. */\n @keyframes zv-progress {\n from {\n transform: scaleX(0);\n }\n to {\n transform: scaleX(1);\n }\n }\n\n .zv-anim-progress {\n transform-origin: 0 50%;\n animation: zv-progress linear both;\n animation-timeline: scroll(root);\n }\n}\n\n/* ─── Accessibilité ─────────────────────────────────────────────────────\n * Les neuf classes se neutralisent d'un seul bloc. Une durée courte ne\n * suffit PAS pour une animation pilotée par le défilement — elle ignore la\n * durée : il faut couper la timeline et l'animation elles-mêmes. La page\n * reste complète et lisible, dans son état final. */\n\n@media (prefers-reduced-motion: reduce) {\n .zv-anim-drift,\n .zv-anim-breathe,\n .zv-anim-rise,\n .zv-anim-glyph-in,\n .zv-anim-converge,\n .zv-anim-pop,\n .zv-anim-filet,\n .zv-anim-dot-active,\n .zv-anim-progress {\n animation: none !important;\n animation-timeline: none !important;\n }\n}\n\n\n/* ── icons.css ── */\n/* Zevra UI — icônes\n * Dépend de tokens.css. Le jeu lui-même vit dans icons/ (index + sprite).\n *\n * Les icônes sont TRACÉES, jamais pleines : trait à l'épaisseur 2, sur une\n * grille de 24, à BOUTS FRANCS et JONCTIONS VIVES — le trait carré de la\n * charte, arbitré le 25/08/2026 (DECISIONS.md, « Le trait carré ») : les\n * bouts arrondis étaient le dialecte des bibliothèques génériques, pas le\n * nôtre. Le réglage vit ici, les 55 tracés le suivent d'un coup. Elles\n * héritent de la couleur du texte — c'est ce qui leur permet de fonctionner\n * sur papier comme sur la bande manifeste sans qu'on ait à les décliner.\n *\n * Deux façons de les poser :\n *\n * <svg class=\"zv-icon\"><use href=\"…/icons/sprite.svg#zv-recherche\"/></svg>\n *\n * ou, là où les ressources distantes sont interdites — un canvas Claude\n * Design —, en collant le corps de l'icône depuis icons/index.json.\n */\n\n.zv-icon {\n display: inline-block;\n width: 16px;\n height: 16px;\n /* `flex: none` évite l'écrasement quand l'icône est posée à côté d'un\n * libellé long dans un conteneur flex — le cas le plus fréquent, et la\n * déformation la plus visible. */\n flex: none;\n vertical-align: middle;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* `square` garde l'emprise optique du rond (le bout dépasse d'une\n * demi-épaisseur, comme lui) : la silhouette change sans qu'un seul\n * tracé soit redessiné. Les angles très aigus retombent d'eux-mêmes en\n * biseau (miter-limit par défaut) — rien à régler. */\n stroke-linecap: square;\n stroke-linejoin: miter;\n}\n\n/* Accolée à du texte dense : métadonnée, puce de source, cellule. */\n.zv-icon--sm {\n width: 13px;\n height: 13px;\n /* Le trait s'épaissit optiquement quand l'icône rétrécit : on le compense,\n * sinon une rangée d'icônes de tailles mêlées paraît déséquilibrée. */\n stroke-width: 1.75;\n}\n\n/* Entrée de navigation, bouton de chrome. */\n.zv-icon--lg {\n width: 20px;\n height: 20px;\n}\n\n/* Illustration d'un état vide, en tête de panneau. */\n.zv-icon--xl {\n width: 28px;\n height: 28px;\n stroke-width: 1.5;\n}\n\n/* Une icône seule dans un bouton n'a pas de libellé pour la nommer : elle\n * doit porter un titre accessible. Cette classe ne fait que réserver la\n * cible tactile ; le nom, lui, se pose en aria-label côté balisage. */\n.zv-icon-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tap);\n height: var(--tap);\n padding: 0;\n border: 0;\n border-radius: var(--r-panel);\n background: transparent;\n color: var(--muted);\n cursor: pointer;\n}\n\n.zv-icon-btn:hover {\n background: var(--panel);\n color: var(--ink);\n}\n\n/* Désactivé. C'était la SEULE classe interactive du module sans cet état :\n * .zv-btn, .zv-chrome-btn, .zv-menu__item, .zv-pager__arrow, .zv-range et\n * les champs en ont tous un. Sans lui, un bouton fermé se rendait comme un\n * bouton actif — curseur main compris — et s'allumait au survol comme s'il\n * allait agir.\n * ⚠️ Le survol est neutralisé EXPLICITEMENT : `:hover` s'applique encore à\n * un bouton désactivé, et l'application ne peut pas le rattraper depuis une\n * couche d'utilitaires — ce module n'est pas layered. */\n.zv-icon-btn[disabled],\n.zv-icon-btn[aria-disabled='true'] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.zv-icon-btn[disabled]:hover,\n.zv-icon-btn[aria-disabled='true']:hover {\n background: transparent;\n color: var(--muted);\n}\n\n/* Icône décorative posée à côté d'un libellé qui la nomme déjà : elle est\n * atténuée pour que le mot reste le point d'entrée du regard. */\n.zv-icon--quiet {\n color: var(--label);\n}\n\n/* ─── Tuile d'icône ─────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La pastille carrée qui\n * porte une icône devant un argument de la vitrine — liste de garanties,\n * grille d'atouts. Carrée par NATURE, pas par style : c'est une tuile.\n * Une par argument, jamais en décor isolé ; c'est le libellé voisin qui\n * nomme, la tuile est décorative par défaut. */\n.zv-icon-tile {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n flex: none;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n\n/* ── overlays.css ── */\n/* Zevra UI — enrobages : voile, tiroir, modale, notification\n * Dépend de tokens.css et base.css.\n *\n * Ce module est COMMUN aux deux couches. Une page vitrine a besoin d'un\n * tiroir pour son menu mobile ; une application en a besoin pour sa\n * navigation. Les laisser dans navigation.css obligeait une landing à\n * charger la barre latérale, la pagination et les tableaux pour obtenir un\n * bouton burger — c'est ce qui a motivé cette séparation.\n *\n * Tout ce qui vit ici se pose PAR-DESSUS la page et lui vole le regard. D'où\n * une règle commune : rien ne s'ouvre sans un moyen évident d'en sortir, et\n * l'échappement au clavier est du ressort du gabarit, que le CSS ne peut pas\n * assurer seul.\n *\n * Empilement, dans l'ordre : voile 60, tiroir 80, modale 90, notification 100.\n * Les couches inférieures (2, 5, 40, 50) sont dans layout.css.\n *\n * OUVERTURE — deux écritures équivalentes, au choix du gabarit :\n *\n * <div class=\"zv-drawer\" data-open> en HTML, en Astro\n * <div class=\"zv-drawer zv-drawer--open\"> en React, en Vue\n *\n * La seconde existe parce que la première est pénible à écrire en JSX : un\n * attribut booléen y demande `{...(ouvert ? {'data-open': ''} : {})}`, là où\n * une classe conditionnelle est le geste habituel. Ne PAS employer\n * `data-open={ouvert}` en React : il rend `data-open=\"false\"`, que le\n * sélecteur d'attribut considère comme présent — le panneau resterait\n * ouvert.\n */\n\n/* ─── Voile ─────────────────────────────────────────────────────────────\n * Toujours l'encre de charte diluée, jamais un noir pur : la page est\n * bleutée, un gris neutre y ferait un trou sale. Le voile n'existe que sous\n * le seuil (cf. bloc mobile) — au-dessus, tiroir et feuille sont des colonnes,\n * elles n'occultent rien. */\n\n.zv-scrim {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 60;\n background: color-mix(in oklab, var(--ink) 40%, transparent);\n}\n\n/* ─── Tiroir de navigation ──────────────────────────────────────────────*/\n\n/* Tiroir ou modale ouverts : la page derrière ne défile plus. `:has()` est\n * le seul chemin CSS pour verrouiller le body depuis un état descendant —\n * sans lui, la molette fait défiler le fond sous le voile et le panneau\n * semble dériver. */\nhtml:has(.zv-drawer[data-open]),\nhtml:has(.zv-drawer--open),\nhtml:has(.zv-modal[data-open]),\nhtml:has(.zv-modal--open) {\n overflow: hidden;\n}\n\n.zv-drawer {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 80;\n /* `visibility` plutôt que `display` : elle sort le contenu du parcours\n * clavier ET se transitionne, ce qui retient le panneau à l'écran le temps\n * qu'il glisse dehors. En `display: none`, il disparaîtrait à mi-course. */\n visibility: hidden;\n transition: visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open],\n.zv-drawer--open {\n visibility: visible;\n}\n\n/* Le voile du tiroir accompagne un panneau qui glisse : il se fond. Celui de\n * la feuille de source apparaît net, faute de mouvement à accompagner.\n *\n * Et il redescend au sol : son z-index de 60 est la couche qu'il occupe\n * quand il est SEUL sur la page. À l'intérieur d'un enrobage, qui ouvre son\n * propre contexte d'empilement, ce 60 l'emportait sur le panneau et voilait\n * le tiroir par-dessus — il paraissait délavé, derrière son propre écran. */\n.zv-drawer .zv-scrim {\n z-index: 0;\n opacity: 0;\n transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open] .zv-scrim,\n.zv-drawer--open .zv-scrim {\n opacity: 1;\n}\n\n.zv-drawer__panel {\n position: absolute;\n /* Le panneau passe DEVANT son voile. .zv-drawer ouvre un contexte\n * d'empilement, dans lequel le z-index 60 du voile l'emporte sur un\n * panneau resté en `auto` : le tiroir s'affichait alors délavé, comme s'il\n * était derrière son propre écran. */\n z-index: 1;\n inset-block: 0;\n left: 0;\n display: flex;\n width: 288px;\n /* Le plafond en pourcentage garde toujours une bande de page visible\n * derrière : un tiroir qui couvre tout l'écran ne se lit plus comme un\n * tiroir, et l'utilisateur perd l'endroit où appuyer pour sortir. */\n max-width: 86%;\n flex-direction: column;\n overflow-y: auto;\n border-right: 1px solid var(--line);\n background: var(--panel);\n border-radius: 0;\n /* Aucune ombre : c'est le voile qui fait le contraste. En ajouter une\n * doublerait la séparation et salirait le bord. */\n box-shadow: none;\n transform: translateX(-100%);\n transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n.zv-drawer[data-open] .zv-drawer__panel,\n.zv-drawer--open .zv-drawer__panel {\n transform: translateX(0);\n}\n\n/* ⚠️ TENSION ASSUMÉE, à arbitrer. Les deux courbes ci-dessus sont celles du\n * relevé — voile en cubic-bezier(.4,0,.2,1), panneau en ease-out, même durée.\n * Leur asymétrie ne se voit pas, elle s'entend : le panneau arrive avant que\n * le voile ait fini de se poser. Elles ne sont donc PAS la courbe unique\n * --zv-easing que motion.css impose à la vitrine. Défendable (motion.css régit\n * neuf animations de page, pas le chrome applicatif), mais c'est une divergence\n * écrite ici plutôt qu'une variable empruntée en silence — d'autant que ce\n * module doit rester utilisable sans motion.css.\n * La coupure prefers-reduced-motion est déjà globale dans base.css : ne pas la\n * reposer ici, elle neutralise ces deux transitions comme les autres. */\n\n.zv-drawer__close {\n position: absolute;\n top: 7px;\n right: 8px;\n}\n\n/* Comportement attendu du composant, à conserver tel quel :\n * · role=\"dialog\" aria-modal=\"true\" aria-label=\"Navigation\", et aria-hidden\n * qui reflète l'état fermé ;\n * · clic sur le voile ou touche Échap → fermeture ;\n * · à l'ouverture, `body { overflow: hidden }` RESTAURÉ à sa valeur\n * précédente à la fermeture, jamais remis à '' aveuglément : c'est ce qui\n * évite de casser une page qui gérait déjà son propre défilement ;\n * · l'état ouvert mémorise la ROUTE d'ouverture, pas un booléen — changer de\n * page referme alors le tiroir sans effet ni rendu en cascade. Le\n * composant a donc besoin de la route courante, pas d'une prop `open`.\n * ⚠️ MANQUE en production, à combler côté composant : piège de focus dans le\n * panneau et restitution du focus au bouton d'ouverture à la fermeture. */\n\n\n/* ─── Modale ────────────────────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ. Aucune modale dans les sources de Mémoire — l'application\n * n'en emploie pas. Celle-ci est DÉRIVÉE de la carte et du tiroir, sans\n * introduire une seule valeur nouvelle : elle réemploie --r-card,\n * --shadow-panel et le voile ci-dessus.\n *\n * Elle existe parce que la règle du projet en impose la forme (panneau\n * plafonné à 92vh, qui défile en son sein) et qu'à défaut chaque app en\n * réinventerait une.\n *\n * Employer <dialog> plutôt que ce balisage quand c'est possible : le\n * navigateur y apporte le piège de focus et la fermeture par Échap, que le\n * CSS ne sait pas faire. */\n\n.zv-modal {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 90;\n align-items: center;\n justify-content: center;\n padding: 16px;\n}\n\n.zv-modal[data-open],\n.zv-modal--open {\n display: flex;\n}\n\n/* Le voile d'une modale se montre à TOUTES les largeurs, et passe sous son\n * panneau.\n *\n * .zv-scrim est masqué par défaut et ne s'affiche que sous le seuil : c'est\n * juste pour le tiroir, qui n'existe qu'en mobile. La modale, elle, existe\n * partout — sans cette règle, elle s'ouvrait en desktop sans rien occulter.\n * Le z-index revient à 0 pour la même raison que sur le tiroir. */\n.zv-modal .zv-scrim {\n z-index: 0;\n display: block;\n}\n\n.zv-modal__panel {\n position: relative;\n z-index: 1;\n display: flex;\n width: 100%;\n max-width: 520px;\n /* Le plafond ET le défilement interne : sans les deux, une modale au\n * contenu long pousse ses actions hors de l'écran, et l'utilisateur ne\n * peut plus ni valider ni annuler. */\n max-height: 92vh;\n flex-direction: column;\n overflow: auto;\n border: 1px solid var(--line);\n border-radius: var(--r-card);\n background: var(--white);\n box-shadow: var(--shadow-panel);\n}\n\n.zv-modal__head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n padding: 20px 22px 0;\n}\n\n.zv-modal__title {\n margin: 0;\n font-family: var(--font-display);\n letter-spacing: -0.02em;\n font-size: var(--app-h2);\n font-weight: 400;\n color: var(--ink);\n}\n\n.zv-modal__body {\n padding: 14px 22px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* Les actions restent visibles quand le corps défile : c'est tout l'intérêt\n * du plafond ci-dessus. */\n.zv-modal__foot {\n position: sticky;\n bottom: 0;\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n border-top: 1px solid var(--hairline);\n background: var(--white);\n padding: 14px 22px;\n}\n\n/* Modale destructive : le filet haut porte l'avertissement, pas un fond\n * rouge — une surface d'alerte pleine rendrait le texte pénible à lire. */\n.zv-modal--danger .zv-modal__panel {\n border-top: 3px solid var(--err);\n}\n\n/* ─── Notification flottante ────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ, même raison : Mémoire signale tout en place, par une\n * alerte de bloc. Cette notification est l'alerte du module de retours,\n * détachée et posée en coin.\n *\n * Elle ne remplace PAS .zv-alert : une erreur de formulaire se dit à côté du\n * champ, jamais dans un coin qui disparaît. Réserver ce composant à ce qui\n * est vrai après coup — « copié », « enregistré », « envoyé ». */\n\n.zv-toasts {\n position: fixed;\n z-index: 100;\n right: 16px;\n bottom: 16px;\n display: flex;\n flex-direction: column-reverse;\n gap: 8px;\n /* Le conteneur ne capte rien : seules les notifications elles-mêmes le\n * font, sans quoi une colonne invisible bloquerait les clics dans tout le\n * coin de l'écran. */\n pointer-events: none;\n}\n\n.zv-toast {\n pointer-events: auto;\n display: flex;\n align-items: center;\n gap: 10px;\n max-width: 380px;\n border: 1px solid var(--line);\n border-radius: var(--r-panel);\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 12px 14px;\n font-size: var(--app-sm);\n color: var(--ink);\n}\n\n.zv-toast--ok {\n border-color: var(--ok-line);\n background: var(--ok-soft);\n color: var(--ok);\n}\n\n.zv-toast--err {\n border-color: var(--err-line);\n background: var(--err-soft);\n color: var(--err);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────*/\n\n@media (max-width: 767px) {\n /* Voile et tiroir n'existent QUE sous le seuil : au-dessus, la navigation\n * est une colonne permanente, elle n'occulte rien. */\n .zv-scrim {\n display: block;\n }\n\n .zv-drawer {\n display: block;\n }\n\n /* La notification prend la largeur : un coin de 380px sur un écran de 390\n * n'est plus un coin. */\n .zv-toasts {\n right: 12px;\n left: 12px;\n bottom: 12px;\n }\n\n .zv-toast {\n max-width: none;\n }\n}\n\n\n/* ── forms.css ── */\n/* Zevra UI — formulaires : champ, recherche, zone de texte, sélection,\n * case/radio, interrupteur, puces de filtre, zone de dépôt — et, depuis le\n * 25/08/2026 (hors relevé, voir DECISIONS.md) : le champ à affixe et le\n * curseur.\n *\n * DÉPEND de tokens.css (toutes les valeurs) et de base.css (reset\n * box-sizing, anneau :focus-visible). RIEN D'AUTRE : ce module ne lit ni\n * layout.css, ni components.css, ni motion.css. C'est volontaire — une app\n * purement applicative n'a aucune raison de charger la mise en page de la\n * vitrine.\n *\n * Ce qu'il NE porte PAS :\n * · le menu déroulé par .zv-select — il appartient à navigation.css\n * (.zv-menu) ; ce module pose l'ancre (.zv-select-anchor) et le\n * déclencheur, rien de plus ;\n * · le bouton d'envoi d'un formulaire — c'est .zv-btn (components.css) ;\n * · l'alerte de bloc (role=\"alert\") — module de retours.\n *\n * PROVENANCE. Relevé dans l'application Mémoire (production, mêmes tokens de\n * charte) sur six écrans : /connexion, /chat, /repertoire, /bibliotheque,\n * /logs, /import. Deux écrans de plus ont été rouverts APRÈS le premier\n * relevé, parce qu'ils contiennent des motifs que les six premiers n'ont\n * pas : /admin (add-member-form.tsx, billing-card.tsx) pour les deux seuls\n * <select> natifs et le seul message d'erreur au niveau du champ. Restent\n * NON relevés : /tableau-de-bord et /documents/[slug]. Chaque section dit ce\n * qui est relevé et ce qui est dérivé ; rien n'est comblé en silence.\n *\n * VOCABULAIRE. Le relevé emploie huit paliers de texte dans 5,5 px et une\n * demi-douzaine de gris intermédiaires ; tokens.css les regroupe. Les\n * rabattements appliqués ici, une fois pour toutes :\n * #4A5468 et #727C93 → var(--ink-2) / var(--muted)\n * #9AA3B5 (placeholder) → var(--label)\n * #C9CEDD (filet ferme) → var(--accent-line)\n * #FDFDFF (survol papier) → var(--panel)\n * 13/14/12,5/11,5 px → var(--app) / var(--app-lg) / var(--app-sm)\n * 11/10,5/9,5/8,5 px mono → var(--mono-lg) / var(--mono) / var(--mono-sm)\n * Deux d'entre eux font disparaître une nuance du relevé : c'est dit sur\n * place, à l'endroit exact où la nuance tombe.\n *\n * POINT DE RUPTURE : 768px, comme les autres modules. Les sources basculent\n * à 1024px (`lg` de Tailwind) : les valeurs sont reprises telles quelles,\n * seul le seuil change.\n */\n\n/* ─── Groupe de champ ───────────────────────────────────────────────────\n * Intitulé au-dessus, contrôle, puis aide ou erreur. Relevé sur les\n * formulaires de connexion (login-card.tsx:16-18, 163-200 ;\n * reset-password-card.tsx:102-140) et confirmé sur /admin\n * (add-member-form.tsx). L'écart intitulé → contrôle vaut 6px et non 8 :\n * c'est le `mt-[6px]` du champ, pas une estimation. */\n\n.zv-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n /* Sans min-width:0, un champ placé dans une ligne flex refuse de rétrécir\n * sous la largeur de son placeholder et fait déborder la page. */\n min-width: 0;\n}\n\n/* Ligne d'intitulé quand un lien l'accompagne (« Mot de passe oublié ? ») :\n * les deux se lisent sur la même ligne de base plutôt que de s'empiler.\n * Gouttière NULLE ici : le relevé ferme l'écart au-dessus du seuil\n * (login-card.tsx:180, `gap-3 … lg:gap-0`) et laisse space-between faire\n * tout le travail. Les 12px sont une valeur MOBILE, ils sont en fin de\n * fichier. */\n.zv-field__head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 0;\n}\n\n/* Les capitales sont LITTÉRALES dans le JSX de Mémoire. Elles passent en CSS\n * ici : recopiées dans le contenu, elles seraient épelées lettre par lettre\n * par un lecteur d'écran. */\n.zv-field__label {\n display: block;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n.zv-field__help {\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n/* Le message d'erreur AU NIVEAU DU CHAMP existe — une seule fois dans toute\n * l'application, hors des six écrans du relevé initial\n * (add-member-form.tsx:94). Ses valeurs propres (taille, graisse, écart)\n * n'ont PAS été relevées : celles ci-dessous sont dérivées du reste du\n * groupe de champ et de la teinte de l'alerte de bloc. Aucune couleur\n * nouvelle n'est introduite. */\n.zv-field__err {\n font-size: var(--app-sm);\n font-weight: 500;\n color: var(--err);\n}\n\n/* ─── Champ de saisie ───────────────────────────────────────────────────\n * Référence : le champ des formulaires de connexion, seul <input> bordé de\n * l'application (login-card.tsx:18), au motif strictement identique dans\n * /admin. Les 10px/13px de l'inventaire venaient du CADRE de recherche, pas\n * d'un champ : ils restent sur .zv-search. */\n\n.zv-input {\n width: 100%;\n min-width: 0;\n border: 1px solid var(--line);\n /* Angle vif : aucun rounded-* sur un seul champ des huit écrans. Posé\n * explicitement, sinon Safari arrondit les champs de lui-même. */\n border-radius: 0;\n background: var(--white);\n padding: 9px 12px;\n /* Un <input> n'hérite pas de la police du document : sans cette ligne, le\n * champ repasse en police système et jure avec le reste de la page. */\n font-family: var(--font-text);\n font-size: var(--app);\n color: var(--ink);\n}\n\n.zv-input::placeholder {\n color: var(--label);\n}\n\n/* Relevé (`focus:border-accent-line`) : c'est :focus et non :focus-visible,\n * la bordure réagit donc aussi à la souris. L'anneau clavier de base.css\n * s'y AJOUTE — on ne reprend PAS le `input { outline: none }` de la prod,\n * qui laissait l'application inutilisable au clavier. */\n.zv-input:focus {\n border-color: var(--accent-line);\n}\n\n/* Identifiant d'organisation : même champ, chiffres et tirets alignés\n * (login-card.tsx:145). */\n.zv-input--mono {\n font-family: var(--font-mono);\n}\n\n/* <select> NATIF, habillé comme le champ. C'est le motif de sélection\n * accessible de l'application, relevé deux fois (add-member-form.tsx:82,\n * billing-card.tsx:135) avec exactement les valeurs de .zv-input —\n * `border-line bg-white px-3 py-[9px] text-[13px] text-ink`. D'où un simple\n * modificateur : tout est déjà là.\n *\n * L'`appearance` n'est PAS neutralisée : la flèche, l'ouverture au clavier,\n * la liste native et son annonce viennent du navigateur. La remplacer\n * imposerait de redessiner tout cela, et le relevé ne le fait pas non plus.\n * À ne pas confondre avec .zv-select, qui est un déclencheur de menu maison\n * — voir plus bas. */\n.zv-input--select {\n cursor: pointer;\n}\n\n/* Champ nu : ni fond ni filet propres, c'est le CADRE qui l'entoure qui fait\n * le champ (composeur du chat). Lui redonner une bordure dessinerait deux\n * cadres imbriqués. */\n.zv-input--bare {\n flex: 1 1 0%;\n border: none;\n background: transparent;\n padding: 2px 0;\n font-size: var(--app-lg);\n}\n\n/* ─── Recherche ─────────────────────────────────────────────────────────\n * Cadre en flex, loupe au filet, saisie sans bordure propre. Deux relevés\n * identiques (repertoire-client.tsx:59-78, bibliotheque-client.tsx:185-204).\n *\n * La loupe est un SVG dans le balisage, pas un fond CSS : viewBox 0 0 13 13,\n * fill=\"none\", stroke-width=\"1.5\",\n * <circle cx=\"5.5\" cy=\"5.5\" r=\"4\"/><line x1=\"8.6\" y1=\"8.6\" x2=\"12\" y2=\"12\"/> */\n\n.zv-search {\n display: flex;\n width: auto;\n min-width: 240px;\n flex: 1 1 0%;\n align-items: center;\n gap: 9px;\n border: 1px solid var(--line);\n background: var(--white);\n padding-inline: 13px;\n /* La prise de focus s'ANIME (25/08) : sans transition, le cadre sautait\n * d'un état à l'autre et le retour se lisait comme un défaut de rendu. */\n transition:\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-search:hover {\n border-color: var(--accent-line);\n}\n\n/* DÉRIVÉ : la bordure de focus relevée sur .zv-input, portée par le cadre.\n *\n * DEUX MARQUAGES, ASSUMÉS. L'anneau :focus-visible de base.css est un\n * sélecteur universel : au clavier il se dessine AUSSI autour du champ nu,\n * à l'intérieur du cadre, en plus de ce filet. On le garde tel quel plutôt\n * que de trancher l'un des deux :\n * · supprimer l'anneau (outline:none) rendrait le champ nu invisible au\n * clavier — c'est exactement la faute de la production ;\n * · supprimer ce filet priverait la souris de tout retour, alors que le\n * relevé lui en donne un sur les champs bordés.\n * La redondance coûte moins cher que l'une ou l'autre perte. */\n/* ⚠️ REPRIS le 25/08/2026 — la DA jugeait la prise de focus « boff ». Elle\n * l'était : le cadre passait à --accent-line, un filet si pâle qu'à côté de\n * l'anneau du champ intérieur il ne se voyait pas. Trois corrections :\n * · le filet passe à --accent PLEIN, pas à sa version diluée ;\n * · un halo --accent-soft de 3px double le trait — c'est lui qui rend le\n * retour visible du coin de l'œil, sans déplacer un pixel de la mise en\n * page (box-shadow ne prend pas de place) ;\n * · la loupe s'allume avec le cadre : elle cesse d'être une invite grise\n * pour devenir la marque du champ actif.\n *\n * L'anneau :focus-visible du champ INTÉRIEUR est neutralisé ici, et\n * seulement ici : le cadre le remplace, et il le remplace pour la souris\n * comme pour le clavier. C'est ce qui lève la double marque assumée jusque-là\n * — un seul objet à l'écran, un seul retour. Le champ nu reste parfaitement\n * repérable au clavier, puisque c'est le cadre entier qui s'allume. */\n.zv-search:focus-within {\n border-color: var(--accent);\n box-shadow: 0 0 0 3px var(--accent-soft);\n}\n\n.zv-search:focus-within .zv-search__icon {\n stroke: var(--accent);\n}\n\n.zv-search__input:focus-visible {\n outline: none;\n}\n\n.zv-search__icon {\n flex: none;\n width: 13px;\n height: 13px;\n /* Même gris que le placeholder : la loupe est une invite, pas une donnée.\n * Elle s'allume avec le cadre — voir :focus-within plus bas. */\n stroke: var(--label);\n transition: stroke 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-search__input {\n min-width: 0;\n flex: 1 1 0%;\n border: none;\n background: transparent;\n padding-block: 10px;\n font-family: var(--font-text);\n font-size: var(--app);\n color: var(--ink);\n}\n\n.zv-search__input::placeholder {\n color: var(--label);\n}\n\n/* ─── Champ à affixe ────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). « 30 », seul dans un champ,\n * ne dit pas s'il s'agit de jours ou d'euros. La grammaire est celle de\n * .zv-search — le seul cadre-à-champ-nu du relevé : cadre --line, champ nu\n * dedans, allumage :focus-within identique. L'affixe est en mono --label,\n * comme les libellés de déclencheur : une unité est un libellé, pas une\n * donnée. */\n\n.zv-affix {\n display: flex;\n align-items: center;\n gap: 9px;\n min-width: 0;\n border: 1px solid var(--line);\n background: var(--white);\n padding-inline: 12px;\n transition:\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n box-shadow 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-affix:hover {\n border-color: var(--accent-line);\n}\n\n/* Même prise de focus que le cadre de recherche : filet --accent plein,\n * halo --accent-soft — et l'anneau du champ intérieur est remplacé par le\n * cadre, pour la souris comme pour le clavier. */\n.zv-affix:focus-within {\n border-color: var(--accent);\n box-shadow: 0 0 0 3px var(--accent-soft);\n}\n\n.zv-affix .zv-input--bare {\n /* Le champ d'un formulaire, pas le composeur : il reprend le corps et le\n * rythme vertical de .zv-input. */\n font-size: var(--app);\n padding-block: 9px;\n}\n\n.zv-affix .zv-input--bare:focus-visible {\n outline: none;\n}\n\n.zv-affix__add {\n flex: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n white-space: nowrap;\n color: var(--label);\n}\n\n/* ─── Zone de texte ─────────────────────────────────────────────────────\n * DÉCISION, PAS UN RELEVÉ : il n'existe AUCUN <textarea> dans l'application,\n * sur aucun des huit écrans ouverts. Le composeur du chat est un <input> où\n * Entrée envoie (chat-client.tsx:365). Le bloc ci-dessous est dérivé mot à\n * mot de .zv-input ; seules min-height et resize s'y ajoutent — sans elles\n * le champ se rend sur une ligne et le composant n'existe pas. min-height\n * est la SEULE valeur de ce module sans aucune source, dans aucun écran.\n *\n * Ne PAS remplacer l'input du composeur par ce bloc : ce serait un\n * changement de comportement (Entrée n'enverrait plus). */\n\n.zv-textarea {\n width: 100%;\n min-width: 0;\n min-height: 96px; /* sans source — voir ci-dessus */\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 9px 12px;\n font-family: var(--font-text);\n font-size: var(--app);\n /* 1.6 : l'interligne du corps de texte du paquet (base.css, .zv-body). */\n line-height: 1.6;\n color: var(--ink);\n /* Vertical seul : en libre, l'utilisateur élargit le champ hors de sa\n * colonne et casse la mise en page. */\n resize: vertical;\n}\n\n.zv-textarea::placeholder {\n color: var(--label);\n}\n\n.zv-textarea:focus {\n border-color: var(--accent-line);\n}\n\n/* ─── Déclencheur de sélection (menu maison) ────────────────────────────\n * « PÉRIMÈTRE : Toute la base ▾ » (chat-client.tsx:168-175). Ce n'est pas un\n * <select> : c'est un bouton qui ouvre .zv-menu (navigation.css). Pour un\n * vrai choix dans une liste, préférer .zv-input--select, qui est natif.\n *\n * ANCRAGE — arbitrage à consigner dans DECISIONS.md, les deux modules ne\n * disent pas la même chose. Le relevé donne raison à celui-ci : c'est\n * l'ENVELOPPE qui porte position:relative (`relative inline-block\n * max-w-full`), pas le déclencheur. Ancrer le menu sur .zv-select\n * fonctionnerait par accident, mais casserait son `max-width` calculé sur\n * la fenêtre. D'où .zv-select-anchor, posée ici, à côté de son\n * déclencheur. */\n\n.zv-select-anchor {\n position: relative;\n display: inline-block;\n max-width: 100%;\n}\n\n/* Ancre pleine largeur : le cas d'un CHAMP de formulaire, où la liste doit\n * tomber exactement sous le contrôle et faire la même largeur que lui. La\n * variante en ligne ci-dessus reste celle des filtres, où le déclencheur se\n * dimensionne sur son libellé. */\n.zv-select-anchor--block {\n display: block;\n width: 100%;\n}\n\n.zv-select-anchor--block .zv-menu {\n width: 100%;\n min-width: 0;\n}\n\n/* ─── Déclencheur de sélection DESSINÉE ─────────────────────────────────\n * ⚠️ AJOUTÉ le 25/08/2026 : « le select, faut le designer jusque dans\n * l'affichage de la liste » (DA, sur pièces).\n *\n * Le <select> natif (.zv-input--select) reste dans le paquet et garde son\n * usage — mais il ne peut PAS être habillé : la liste déroulée est peinte\n * par le système, pas par la page. Sur un macOS en mode sombre, elle sort\n * en menu noir au milieu d'une interface claire. Aucun CSS ne corrige cela,\n * c'est une limite du contrôle.\n *\n * D'où ce déclencheur, qui a l'apparence exacte d'un champ et ouvre le\n * .zv-menu du paquet. Ce qu'il COÛTE, et qu'il faut assumer en le\n * choisissant : le clavier, l'annonce et la liste native sont à réimplémenter\n * — c'est fait dans le composant React (SelectMenu), pas dans ce fichier.\n * Un gabarit qui pose ces classes à la main sans ce travail livre un select\n * inutilisable au clavier. Dans ce cas, prendre le natif. */\n.zv-input--trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n text-align: left;\n line-height: 1.4;\n cursor: pointer;\n transition: border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-input--trigger:hover {\n border-color: var(--accent-line);\n}\n\n/* Ouvert, le déclencheur reste marqué : sans cela, la liste flotte au-dessus\n * d'un champ qui a l'air au repos, et on ne sait plus lequel elle sert. */\n.zv-input--trigger[aria-expanded='true'] {\n border-color: var(--accent);\n}\n\n/* Le placeholder d'un déclencheur est du texte, pas un ::placeholder : il\n * lui faut donc sa propre teinte, celle du champ vide. */\n.zv-input--trigger[data-vide='oui'] {\n color: var(--label);\n}\n\n.zv-select {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n border: 1px solid var(--line);\n background: var(--white);\n padding: 6px 11px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n /* La valeur courante peut être longue : elle sort du cadre plutôt que de\n * casser la pastille en deux lignes. */\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n}\n\n.zv-select:hover {\n border-color: var(--accent-line);\n}\n\n/* La valeur choisie, seule partie mise en avant du déclencheur. */\n.zv-select strong {\n font-weight: 700;\n color: var(--accent);\n}\n\n/* ▾ (U+25BE) posé dans le balisage. */\n.zv-select__chevron {\n font-size: var(--mono-sm);\n color: var(--label);\n}\n\n/* ─── Case à cocher et bouton radio ─────────────────────────────────────\n * DÉRIVÉS INTÉGRALEMENT, ET ILS DOIVENT ÊTRE LUS COMME TELS : il n'existe\n * zéro <input type=\"checkbox\"> et zéro <input type=\"radio\"> dans toute\n * l'application — la sélection s'y fait toujours par .zv-seg ou .zv-switch.\n * Le bloc ci-dessous réemploie la grammaire déjà en production (carré au\n * filet --line sur --white, coché en aplat --accent, rond des pastilles)\n * sans introduire de valeur de charte nouvelle ; ses DIMENSIONS, elles\n * (16px, taille du glyphe, épaisseur de l'anneau intérieur), n'ont aucune\n * source. Le jour où un vrai écran en pose, c'est lui qui tranche.\n *\n * ⚠️ .zv-check est DÉJÀ PRIS par la pastille de validation\n * (components.css:482) — d'où .zv-check-box.\n *\n * Contrôles natifs volontairement conservés (appearance:none seulement) : le\n * clavier, :focus-visible, [disabled] et l'annonce au lecteur d'écran\n * viennent alors du navigateur, pas d'un <div> à réoutiller. */\n\n.zv-check-box,\n.zv-radio {\n appearance: none;\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n cursor: pointer;\n}\n\n.zv-radio {\n border-radius: 9999px;\n}\n\n.zv-check-box:hover,\n.zv-radio:hover {\n border-color: var(--accent-line);\n}\n\n/* Le blanc du glyphe est écrit en clair (%23fff) : une data-URI ne lit pas\n * les variables CSS. Valeur strictement égale à --white. La coche est au\n * trait carré depuis le 25/08/2026, comme tout ce qui trace. */\n.zv-check-box:checked {\n border-color: var(--accent);\n background-color: var(--accent);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='2.5,6.5 5,9 9.5,3.5'/%3E%3C/svg%3E\");\n background-position: center;\n background-repeat: no-repeat;\n background-size: 12px 12px;\n}\n\n/* Le point du radio est creusé dans l'aplat par un anneau blanc interne :\n * un pseudo-élément sur un <input> n'est pas fiable d'un moteur à l'autre.\n * Ce box-shadow est une géométrie, pas une ombre de charte — d'où l'absence\n * de token. */\n.zv-radio:checked {\n border-color: var(--accent);\n background-color: var(--accent);\n box-shadow: inset 0 0 0 4px var(--white);\n}\n\n/* ─── Interrupteur ──────────────────────────────────────────────────────\n * Ligne de réglage des dossiers (logs-client.tsx:267-293). Piste\n * RECTANGULAIRE, bouton rond : relevé tel quel, ne pas arrondir la piste.\n *\n * ⚠️ RABATTEMENT VISIBLE : la piste éteinte était un gris ferme (#C9CEDD),\n * rabattu sur --accent-line, qui est bleuté. L'état éteint prend donc une\n * teinte indigo très pâle. La distinction éteint/allumé tient quand même —\n * elle est portée par l'aplat plein et par le libellé accolé, pas par cette\n * nuance-là. */\n\n/* ⚠️ REDESSINÉ le 25/08/2026 — la DA a tranché « switch horrible » sur\n * pièces, et elle avait raison sur trois points :\n * · 28×16 avec un bouton rond de 12, c'est un jouet posé au milieu de\n * contrôles carrés de 16px de haut ; la ligne de réglage n'avait aucune\n * unité ;\n * · la piste éteinte était --accent-line, un indigo très pâle : éteint et\n * allumé se distinguaient par la SATURATION d'une même teinte, ce qui\n * tombe au premier écran mal calibré et disparaît en daltonisme ;\n * · le bouton rond contredisait l'arbitrage « carré » du même jour.\n * La piste passe donc à 36×20, à angle vif, avec un filet qui la pose comme\n * les champs voisins ; éteint elle est BLANCHE au filet gris, allumée elle\n * est un aplat indigo plein. L'écart n'est plus une nuance, c'est un\n * contraste. */\n.zv-switch {\n position: relative;\n display: inline-block;\n width: 36px;\n height: 20px;\n flex: none;\n border: 1px solid var(--line);\n background: var(--white);\n /* La courbe est celle du paquet (motion.css, `--zv-easing`), recopiée en\n * clair : forms.css ne dépend pas de motion.css et n'a pas à en dépendre\n * pour une transition de couleur. Ce n'est PAS `ease` — le défaut d'un\n * framework tiers n'a rien à faire dans le système. */\n transition:\n background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch-hit:hover .zv-switch {\n border-color: var(--accent-line);\n}\n\n.zv-switch--on {\n border-color: var(--accent);\n background: var(--accent);\n}\n\n.zv-switch-hit:hover .zv-switch--on {\n border-color: var(--accent-deep);\n background: var(--accent-deep);\n}\n\n/* Bouton CARRÉ : le rond ici n'est pas une forme porteuse de sens (ce que\n * sont la pastille d'état ou le radio), c'était un style — il tombe avec les\n * autres rayons. Éteint il est gris comme un contrôle inactif, allumé il\n * passe en blanc sur l'aplat indigo. */\n.zv-switch__knob {\n position: absolute;\n inset-block-start: 2px;\n inset-inline-start: 2px;\n width: 14px;\n height: 14px;\n border-radius: 0;\n background: var(--label);\n transition:\n inset-inline-start 150ms cubic-bezier(0.2, 0.7, 0.2, 1),\n background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch--on .zv-switch__knob {\n inset-inline-start: 18px;\n background: var(--white);\n}\n\n/* Cible tactile portée par une enveloppe : agrandir la piste elle-même\n * décalerait toute la ligne de réglage (cf. mobile en fin de fichier). */\n.zv-switch-hit {\n display: flex;\n /* La source place la gouttière de 7px sur une enveloppe PARENTE, et\n * n'entoure que la piste de sa zone tactile (logs-client.tsx:267-273).\n * Elle est ramenée ici : cette classe enveloppe le contrôle ET son\n * libellé, ce qui rend le texte cliquable — un <label> qui n'entoure que\n * l'interrupteur laisse une cible de 28px là où il en faut 40. Sans la\n * gouttière, le libellé vient coller à la piste. */\n align-items: center;\n gap: 7px;\n cursor: pointer;\n}\n\n/* Le libellé accolé change avec l'état : c'est lui qui porte le sens, la\n * piste seule ne dit pas ce qui est activé. */\n.zv-switch__text {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n color: var(--label);\n}\n\n.zv-switch__text--on {\n color: var(--accent);\n}\n\n/* ─── Curseur ───────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Aucune source ; dérivé de\n * l'interrupteur redessiné : bouton CARRÉ de 14 px (la taille exacte du\n * bouton du switch), --accent, --accent-deep au survol. La piste fait 3 px\n * — la hauteur de .zv-progress — en --line, remplie en --accent jusqu'au\n * curseur. PAS le spectre : la barre de progression est l'un des trois\n * emplois autorisés, le curseur n'en est pas un.\n *\n * Le remplissage est peint par le FOND de l'input (un dégradé à arrêt\n * franc), position portée par --zv-range-pos que la façade React calcule —\n * une géométrie, comme la largeur de Progress, pas une valeur de charte.\n * Sans elle, la piste s'affiche remplie à moitié : poser la variable, ou\n * assumer le repli. Le bouton, lui, passe par les pseudo-éléments de\n * chaque moteur — c'est le seul contrôle du paquet qui l'exige. */\n\n.zv-range {\n --zv-range-pos: 50%;\n appearance: none;\n -webkit-appearance: none;\n display: block;\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n padding: 0;\n border: 0;\n background: linear-gradient(90deg, var(--accent) var(--zv-range-pos), var(--line) 0);\n background-repeat: no-repeat;\n background-size: 100% 3px;\n background-position: 0 center;\n cursor: pointer;\n}\n\n.zv-range::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 14px;\n height: 14px;\n border: 0;\n border-radius: 0;\n background: var(--accent);\n transition: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-range::-moz-range-thumb {\n width: 14px;\n height: 14px;\n border: 0;\n border-radius: 0;\n background: var(--accent);\n}\n\n.zv-range:hover::-webkit-slider-thumb {\n background: var(--accent-deep);\n}\n\n.zv-range:hover::-moz-range-thumb {\n background: var(--accent-deep);\n}\n\n.zv-range[disabled] {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.zv-range[disabled]:hover::-webkit-slider-thumb {\n background: var(--accent);\n}\n\n.zv-range[disabled]:hover::-moz-range-thumb {\n background: var(--accent);\n}\n\n/* ─── Puces de filtre ───────────────────────────────────────────────────\n * Le seul mécanisme de filtrage de l'application (bibliotheque-client.tsx:\n * 143-180, repertoire-client.tsx:79-94, logs-client.tsx:193-207).\n *\n * ⚠️ LA SOURCE DIVERGE, ET C'EST TRANCHÉ ICI :\n * · gouttière du groupe — bibliothèque 5px, répertoire et logs 6px. On\n * retient 6px : deux écrans sur trois, et c'est la valeur de .zv-tabs\n * (navigation.css), qui héberge les mêmes puces. Deux conteneurs des\n * mêmes puces avec deux écarts différents, c'est un défaut, pas une\n * nuance.\n * · taille et densité — bibliothèque descend à 11px/4-9px au-dessus du\n * seuil, le répertoire reste à 11px/8-12px partout, en graisse 600. On\n * retient la puce DENSE de la bibliothèque au-dessus du seuil, et la\n * graisse 500 (la 600 est réservée à l'état sélectionné, sinon\n * sélectionné et non sélectionné ne se distinguent plus que par la\n * couleur).\n * · corps — 11 et 12px se rabattent tous les deux sur --app-sm (12px) :\n * l'écart d'un pixel entre les deux écrans disparaît, et la variante\n * mobile de la bibliothèque avec lui. C'est le prix de l'échelle\n * rationalisée, assumé.\n *\n * ⚠️ L'état sélectionné n'est porté par AUCUN aria-pressed en production :\n * il est invisible pour un lecteur d'écran. Le paquet impose <button\n * aria-pressed=\"true|false\"> — et le style suit l'attribut autant que la\n * classe, pour que le balisage correct ne coûte rien. */\n\n.zv-seg-group {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* ⚠️ HORS RELEVÉ. Les quatre rangées de puces du dépôt passent TOUTES à la\n * ligne ; le motif « nowrap + défilement » n'apparaît que sur des CADRES\n * (tableau, bande de trente jours), jamais sur une rangée de puces.\n * Cette variante n'existe que parce que la règle du projet interdit tout\n * défilement horizontal de PAGE : une rangée qu'on refuse d'enrouler doit\n * alors déborder dans son propre cadre. À poser explicitement, jamais par\n * défaut. */\n.zv-seg-group--rail {\n flex-wrap: nowrap;\n overflow-x: auto;\n}\n\n.zv-seg {\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 4px 9px;\n font-family: var(--font-text);\n font-size: var(--app-sm);\n font-weight: 500;\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n}\n\n/* La rangée de puces vit souvent dans un rail qui défile horizontalement\n * (.zv-tabs--rail, navigation.css). L'anneau de base.css y serait rogné aux\n * deux bords. On ne le REDÉFINIT pas — on déplace seulement son tracé à\n * l'intérieur de la puce, où rien ne le coupe. */\n.zv-seg:focus-visible {\n outline-offset: -2px;\n}\n\n/* Sélection DOUCE (bibliothèque), la seule livrée par défaut : elle\n * n'ajoute pas un second aplat indigo à l'écran, là où la charte n'autorise\n * qu'un seul plein par page. */\n.zv-seg--on,\n.zv-seg[aria-pressed=\"true\"] {\n border-color: var(--accent-line);\n background: var(--accent-soft);\n font-weight: 600;\n color: var(--accent);\n}\n\n/* Sélection PLEINE (répertoire, logs) : OPT-IN, à poser explicitement, et\n * BORNÉE — un seul groupe plein par page, jamais sur une page qui porte\n * déjà un bouton plein. Une page de filtres à plusieurs groupes (la\n * bibliothèque en a deux) afficherait sinon autant d'aplats indigo que de\n * groupes, simultanément : c'est le seul mécanisme du paquet capable de\n * multiplier le plein sans le dire. */\n.zv-seg--on-solid,\n.zv-seg--on-solid[aria-pressed=\"true\"] {\n border-color: var(--accent);\n background: var(--accent);\n color: var(--white);\n}\n\n.zv-seg__count {\n margin-inline-start: 6px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n}\n\n/* Niveaux de log : libellés de longueur inégale (INFO, WARN, ERROR) centrés\n * pour que la rangée ne danse pas d'une puce à l'autre. */\n.zv-seg--mono {\n justify-content: center;\n padding: 4px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.06em;\n}\n\n/* ─── Zone de dépôt ─────────────────────────────────────────────────────\n * Elle tient lieu d'action principale de l'écran d'import : il n'y a aucun\n * bouton plein sur cette page (import-client.tsx:144-202).\n *\n * ⚠️ En production c'est un <div onClick> sans role ni tabIndex : la page\n * d'import est inatteignable au clavier. Le paquet impose role=\"button\" +\n * tabindex=\"0\" (l'anneau de base.css suit alors tout seul).\n *\n * L'icône est un SVG du balisage (import-client.tsx:159-166) : viewBox\n * 0 0 30 30, fill=\"none\", stroke=\"var(--accent)\", stroke-width=\"1.5\" —\n * relevée en stroke-linecap=\"round\", REPRISE au trait carré le 25/08/2026\n * comme tout ce qui trace (DECISIONS.md, « Le trait carré ») :\n * <line x1=\"15\" y1=\"4\" x2=\"15\" y2=\"18\"/>\n * <polyline points=\"9.5,12.5 15,18 20.5,12.5\"/>\n * <path d=\"M5 21 v3 a2 2 0 0 0 2 2 h16 a2 2 0 0 0 2-2 v-3\"/> */\n\n.zv-drop {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n /* 1,5px : l'épaisseur du pointillé est celle du tracé de l'icône, les deux\n * se répondent. */\n border: 1.5px dashed var(--accent-line);\n background: var(--white);\n padding: 38px 24px;\n cursor: pointer;\n}\n\n/* Survol et survol de dépôt rendus à l'identique : la zone répond pareil que\n * le fichier soit traîné ou non, sinon le retour visuel se contredit.\n *\n * ⚠️ RABATTEMENT VISIBLE : le fond de survol du relevé (#FDFDFF) tombe sur\n * --panel, à peine distinct du --white de repos. Le fond ne SIGNALE donc\n * plus rien — le retour visuel repose entièrement sur le filet qui passe à\n * --accent. Ne pas le supprimer en croyant que le fond compense. */\n.zv-drop:hover,\n.zv-drop.is-dragging {\n border-color: var(--accent);\n background: var(--panel);\n}\n\n/* Stockage indisponible : la zone reste visible mais inerte — la masquer\n * ferait croire à une page cassée. */\n.zv-drop--disabled {\n cursor: not-allowed;\n border-color: var(--line);\n background: var(--panel);\n opacity: 0.6;\n}\n\n.zv-drop--disabled:hover {\n border-color: var(--line);\n background: var(--panel);\n}\n\n/* 15px : c'est une phrase, pas un libellé d'interface — d'où le palier de\n * corps de texte (--body) plutôt que l'échelle applicative, qui s'arrête à\n * 14px. */\n.zv-drop__title {\n font-size: var(--body);\n font-weight: 500;\n color: var(--ink);\n text-align: center;\n}\n\n.zv-drop__hint {\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n.zv-drop__hint a,\n.zv-drop__hint strong {\n font-weight: 600;\n color: var(--accent);\n text-decoration: underline;\n}\n\n/* Sous le seuil il n'y a pas de glisser-déposer : le lien souligné cède la\n * place à une vraie cible tactile. LES DEUX NŒUDS SONT POSÉS EN PERMANENCE\n * DANS LE BALISAGE et c'est CE fichier qui les échange, au seul seuil du\n * paquet. Ne pas y ajouter .zv-only-desktop / .zv-only-mobile (layout.css) :\n * les deux mécanismes se disputeraient la même déclaration `display`, et le\n * module tirerait la mise en page de la vitrine derrière lui. */\n.zv-drop__browse {\n display: none;\n}\n\n.zv-drop__spec {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n color: var(--label);\n}\n\n/* Le vrai <input type=\"file\"> est retiré du flux, la zone entière lui sert\n * de déclencheur. Nommé en élément de .zv-drop, et non `.zv-file` : le\n * paquet a par ailleurs une vignette de format (.zv-filetype, data.css) et\n * les deux se liraient comme un seul bloc.\n *\n * Conséquence du retrait du flux : l'input n'est PAS focusable — d'où le\n * role/tabindex exigés sur .zv-drop ci-dessus. Remettre value='' après\n * chaque sélection, sinon redéposer le même fichier ne déclenche rien. */\n.zv-drop__input {\n display: none;\n}\n\n/* ─── États transverses ─────────────────────────────────────────────────\n * HORS RELEVÉ pour l'essentiel, et assumé : la production ne désactive aucun\n * champ (seul le bouton d'envoi passe en `cursor-wait; opacity:.7` pendant\n * la requête) et ne signale l'erreur au niveau du champ qu'à un seul\n * endroit. Un système livré sans ces deux états oblige chaque app à les\n * réinventer, chacune à sa façon. Les valeurs sont empruntées, sans\n * exception, à ce qui existe déjà : le traitement désactivé de\n * .zv-drop--disabled et le filet de l'alerte d'erreur. */\n\n.zv-input[disabled],\n.zv-textarea[disabled],\n.zv-select[disabled],\n.zv-seg[disabled],\n.zv-check-box[disabled],\n.zv-radio[disabled] {\n cursor: not-allowed;\n border-color: var(--line);\n background: var(--panel);\n opacity: 0.6;\n}\n\n/* Le survol ne doit pas rattraper un contrôle désactivé : sans ces règles,\n * .zv-seg:hover et .zv-select:hover repeignent le filet et le contrôle a\n * l'air actif. */\n.zv-select[disabled]:hover,\n.zv-check-box[disabled]:hover,\n.zv-radio[disabled]:hover {\n border-color: var(--line);\n}\n\n/* Champ en erreur : seul le filet change. Un fond rouge sur un champ à\n * remplir rendrait la saisie illisible — l'aplat --err-soft reste réservé au\n * bandeau d'alerte. Le sélecteur suit l'attribut autant que la classe :\n * aria-invalid est ce qui rend l'erreur audible, il ne doit rien coûter. */\n.zv-field--err .zv-input,\n.zv-field--err .zv-textarea,\n.zv-field--err .zv-select,\n.zv-input[aria-invalid=\"true\"],\n.zv-textarea[aria-invalid=\"true\"] {\n border-color: var(--err-line);\n}\n\n/* Le focus l'emporte sur l'erreur : pendant la correction, l'utilisateur doit\n * voir où il est, pas ce qui était faux. */\n.zv-field--err .zv-input:focus,\n.zv-field--err .zv-textarea:focus,\n.zv-input[aria-invalid=\"true\"]:focus,\n.zv-textarea[aria-invalid=\"true\"]:focus {\n border-color: var(--accent-line);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Valeurs relevées aux mêmes endroits, sous le seuil des sources (1024px),\n * ramenées ici au seuil unique du paquet.\n *\n * NOTE : le micro-libellé du groupe de champ ne bouge PAS ici. Le relevé le\n * grossit d'un pixel sous le seuil (9,5 → 10,5) ; les deux valeurs tombent\n * sur --mono. La nuance n'existe plus, et une règle qui ne change rien vaut\n * moins qu'une ligne qui dit pourquoi. */\n\n@media (max-width: 767px) {\n /* Le lien qui accompagne l'intitulé devient une cible tactile : la ligne\n * de base ne tient plus quand les deux éléments font 40px de haut, et il\n * leur faut alors une gouttière (gap-3), là où space-between suffisait. */\n .zv-field__head {\n align-items: center;\n gap: 12px;\n }\n\n /* 44px, pas --tap (40px) : c'est la valeur des formulaires de connexion et\n * du pavé d'import, les deux seuls endroits où l'on saisit au doigt.\n * 16px : en dessous, Safari iOS zoome à la mise au point du champ et\n * décale toute la page — ce n'est pas un choix typographique, aucun token\n * n'a à porter cette valeur. */\n .zv-input,\n .zv-textarea {\n min-height: 44px;\n font-size: 16px;\n }\n\n /* Les <select> de /admin, eux, sont relevés à 40px (`min-h-10 lg:min-h-0`)\n * et non à 44 : un autre écran, une autre main. */\n .zv-input--select {\n min-height: var(--tap);\n }\n\n /* Le champ nu et celui d'un cadre à affixe. 16px et non --app-lg (14px) :\n * la règle des 16px ci-dessus vaut pour TOUT champ qu'on saisit au doigt,\n * et ces deux-là — le composeur, le sous-domaine d'organisation — sont\n * parmi les plus saisis d'une application. Sous 16, Safari iOS zoome à la\n * mise au point et décale la page. */\n .zv-input--bare,\n .zv-affix .zv-input--bare {\n min-height: var(--tap);\n font-size: 16px;\n }\n\n /* Le cadre passe pleine largeur. Il n'est PAS figé (`flex: none`) : le\n * relevé lui laisse le `0 1 auto` par défaut, il peut donc encore se\n * comprimer si la ligne qui l'accueille l'exige. */\n .zv-search {\n width: 100%;\n min-width: 0;\n flex: 0 1 auto;\n }\n\n /* Le champ de recherche n'était visé par aucune des deux règles : il\n * gardait --app (13px), donc il zoomait lui aussi. */\n .zv-search__input {\n padding-block: 12px;\n font-size: 16px;\n }\n\n /* Le déclencheur peut passer à la ligne : sa valeur courante est un nom de\n * dossier libre, qui déborderait sinon de l'écran. */\n .zv-select {\n min-height: var(--tap);\n flex-wrap: wrap;\n }\n\n /* La puce se desserre pour devenir une cible tactile. Le corps, lui, ne\n * change plus : 11 et 12px se rabattent sur le même palier (voir la\n * section « Puces de filtre »). */\n .zv-seg {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n padding: 8px 12px;\n }\n\n /* Cible de 40px sans gonfler la ligne de réglage : la marge négative\n * annule exactement le padding ajouté. */\n .zv-switch-hit {\n margin-block: -12px;\n padding-block: 12px;\n }\n\n /* La piste ne bouge pas (le fond reste centré) : seule la boîte grandit\n * pour faire une cible tactile. */\n .zv-range {\n height: var(--tap);\n }\n\n .zv-drop {\n padding: 32px 16px;\n }\n\n /* Pas de glisser-déposer au doigt : le lien souligné est remplacé par une\n * cible pleine largeur. Les deux nœuds appartiennent à ce module, l'échange\n * se fait ici et nulle part ailleurs. */\n .zv-drop__hint {\n display: none;\n }\n\n .zv-drop__browse {\n display: flex;\n width: 100%;\n max-width: 280px;\n min-height: 44px;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n padding-inline: 16px;\n font-size: var(--app);\n font-weight: 600;\n color: var(--accent);\n }\n}\n\n/* ─── Champ mot de passe ────────────────────────────────────────────────\n * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des planches ne montre d'écran de\n * connexion, et le système n'avait donc rien pour ce champ — alors que tous\n * les produits en ont un, et que la saisie masquée est le seul endroit de\n * l'interface où l'on tape sans voir ce qu'on écrit.\n *\n * Bâti sur `.zv-affix` plutôt qu'en cadre parallèle : le champ nu, le cadre\n * qui l'entoure et son anneau de mise au point sont déjà là.\n *\n * ⚠️ Le bouton est un vrai <button type=\"button\"> : posé sur un <span>, il\n * n'est ni atteignable au clavier ni annoncé, et il envoie le formulaire dès\n * qu'on oublie le type. Son libellé change avec l'état — « Afficher » /\n * « Masquer » — parce qu'une icône seule ne dit pas ce qui va se passer. */\n\n.zv-affix__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n /* Cible tactile obtenue par la taille, pas par du remplissage : un bouton\n * de 16 px entouré de marge reste un bouton de 16 px pour le doigt. */\n width: 32px;\n height: 32px;\n margin-right: -6px;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: none;\n color: var(--muted);\n cursor: pointer;\n transition: color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-affix__btn:hover {\n color: var(--ink);\n}\n\n.zv-affix__btn[aria-pressed='true'] {\n color: var(--accent);\n}\n\n/* Le contour déborderait du cadre à affixe, qui le rogne. On le rentre. */\n.zv-affix__btn:focus-visible {\n outline-offset: -2px;\n}\n\n/* Les gestionnaires de mot de passe des navigateurs posent leur propre œil\n * dans le champ : deux boutons côte à côte pour la même chose. On masque le\n * leur, le nôtre étant accessible et cohérent avec la charte. */\n.zv-affix input[type='password']::-ms-reveal,\n.zv-affix input[type='password']::-ms-clear {\n display: none;\n}\n\n\n/* ── feedback.css ── */\n/* Zevra UI — retours d'état : bandeaux, étiquettes, pastilles, absence,\n * travail en cours, curseur de flux — et, depuis le 25/08/2026, la roue,\n * le squelette et l'infobulle, longtemps refusés et enfin arbitrés.\n *\n * Dépend de tokens.css et de base.css (reset, fonte de corps, focus clavier\n * global). Indépendant des autres modules.\n *\n * Tokens réellement employés ici, tous vérifiés dans tokens.css :\n * couleurs — --ink, --ink-2, --muted, --label, --line, --hairline, --white,\n * --accent, --accent-line, --accent-soft,\n * --ok, --ok-soft, --ok-line,\n * --warn, --warn-soft, --warn-line, --warn-ink,\n * --err, --err-soft, --err-line ;\n * typo — --font-display, --font-mono, --app, --app-sm,\n * --app-h2, --mono, --mono-sm ;\n * ombre — --shadow-menu (l'infobulle, qui flotte comme le menu).\n * Aucun autre. Il n'existe PAS de --ink-3…--ink-7, pas de --line-soft, pas de\n * --line-firm, pas d'échelles --text-* ni --mono-xs/--mono-2xs : une version\n * antérieure de cet en-tête les déclarait en dépendance, ce qui rendait le\n * module inauditable (rien ne disait quelle valeur portait --ink-6) et l'aurait\n * rendu sans couleur ni taille chez le consommateur. Les valeurs relevées qui\n * tombaient sur ces noms sont rabattues sur les paliers du paquet, et chaque\n * rabattement qui déplace le rendu est écrit en commentaire à l'endroit exact\n * où il est fait.\n *\n * Relevé sur l'application Mémoire, en production. Une différence de méthode\n * avec les modules de vitrine : Mémoire est écrite mobile-first et bascule à\n * `lg` (1024px), alors que la charte ne connaît qu'UN point de rupture, à\n * 768px. Les valeurs desktop sont donc posées en base et les valeurs mobiles\n * regroupées en fin de fichier — le rendu desktop est identique au relevé, le\n * rendu mobile arrive 256px plus tôt. C'est délibéré : deux ruptures dans un\n * système qui en annonce une seule est une dette qui se paie sur chaque app.\n *\n * ÉCHELLE DE TEXTE — la production emploie des demi-pixels (11,5 / 12,5 /\n * 13,5) que le paquet a rationalisés en trois paliers (12 / 13 / 14). Le\n * rabattement retenu ici conserve les ÉCARTS du relevé plutôt que la valeur\n * absolue : 11,5 et 12,5 → --app-sm (12), 13,5 → --app (13), 14 → --app-lg.\n * Deux tailles distinctes d'un pixel dans la source restent donc distinctes\n * d'un pixel ; c'est ce qui fait qu'un bandeau ample reste plus grand qu'un\n * bandeau courant, des deux côtés du seuil.\n *\n * Presque rien ici n'est interactif : pas de bouton de fermeture, pas de\n * champ. Les états [disabled] / [aria-invalid] n'ont aucune cible dans ce\n * module — ils appartiennent aux boutons (components.css) et aux champs\n * (forms.css). Le focus clavier global de base.css suffit au bandeau qu'on\n * rend focusable pour l'annoncer. Seule exception, arbitrée le 25/08/2026 :\n * l'infobulle, qui se montre par nature au survol et au focus de sa cible.\n */\n\n/* ─── Bandeaux d'état ───────────────────────────────────────────────────\n * Filet + fond atténué + texte du même ton : la grammaire la plus\n * systématique du relevé, appliquée à l'identique six fois. Elle tient\n * parce que les trois valeurs viennent d'une même famille de tokens ; la\n * recomposer à la main avec des opacités produit six bandeaux différents.\n *\n * Le ton est OBLIGATOIRE : `.zv-alert` seule hérite d'un filet en\n * currentColor, c'est-à-dire un trait d'encre autour d'un fond blanc.\n * Toujours l'accompagner de --info / --ok / --warn / --err / --dash. */\n\n.zv-alert {\n border: 1px solid;\n padding: 12px 16px;\n /* 12,5px relevé → palier 12. La valeur mobile (13,5 → 13) est en fin de\n * fichier : les cinq bandeaux d'authentification GROSSISSENT sous le\n * seuil, ils ne rétrécissent pas. */\n font-size: var(--app-sm);\n line-height: 1.55;\n font-weight: 500;\n /* Un bandeau porte souvent un message d'erreur brut, non maîtrisé :\n * nom de fichier à rallonge, identifiant, chemin. Sans coupure, il pousse\n * la page en défilement horizontal. */\n overflow-wrap: break-word;\n /* Angle vif, comme au relevé — et, depuis le 25/08/2026, comme tout le\n * paquet : l'arbitrage DA « angle vif partout » (DECISIONS.md) a ramené\n * les quatre tokens de rayon à 0. La contradiction longtemps signalée ici\n * (un bouton arrondi au milieu de blocs vifs dans l'écran vide) est\n * éteinte par cet arbitrage, pas par ce module. */\n border-radius: 0;\n}\n\n/* Information — le seul ton relevé en bandeau de bas de page (garanties,\n * explication d'un écran). */\n.zv-alert--info {\n border-color: var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n/* Avertissement — le texte est --warn-ink, JAMAIS --warn : l'ambre plein\n * sur fond ambre atténué passe sous le seuil de contraste. C'est la seule\n * famille asymétrique du système, et l'erreur la plus facile à commettre. */\n.zv-alert--warn {\n border-color: var(--warn-line);\n background: var(--warn-soft);\n color: var(--warn-ink);\n}\n\n/* Erreur — à porter avec role=\"alert\" côté balisage, sinon le message\n * apparaît sans être annoncé aux lecteurs d'écran : c'est précisément le\n * seul contenu de l'écran qu'on ne peut pas se permettre de rater. */\n.zv-alert--err {\n border-color: var(--err-line);\n background: var(--err-soft);\n color: var(--err);\n}\n\n/* Succès — RELEVÉ, pleine largeur, deux fois : reset-password-card.tsx:90 et\n * forgot-password-card.tsx:60, en `border-ok-line bg-ok-soft px-3 py-[10px]\n * text-[13.5px]/[1.6] font-medium text-ok`. (Une version antérieure de ce\n * commentaire le disait « extrapolé » : c'était faux, les deux écrans de\n * réinitialisation n'avaient pas été relus. Le gabarit du relevé est celui\n * de --sm, à composer avec.) */\n.zv-alert--ok {\n border-color: var(--ok-line);\n background: var(--ok-soft);\n color: var(--ok);\n}\n\n/* Variante « aveu d'ignorance » : ce que le système n'a PAS trouvé. Le\n * pointillé sans fond dit qu'il n'y a rien derrière — un fond plein ferait\n * lire une réponse là où il n'y en a pas. Avec la zone de dépôt, c'est le\n * seul pointillé de toute la couche applicative : le garder rare, c'est ce\n * qui lui laisse son sens.\n *\n * ⚠️ DEUX PERTES DE TEINTE, assumées et signalées plutôt que glissées :\n * 1. le texte est `#6E6A61` en production (chat-client.tsx:265), un gris\n * CHAUD — le seul du fil de réponse. Le paquet n'a pas de neutre chaud :\n * --muted (#5a637a) est bleuté. Le rabattement est arrêté, mais il\n * REFROIDIT ce texte, et il le range sous le même token que les gris\n * froids du relevé (#6B7488 du pager, du bouton d'icône). Deux familles\n * de teinte finissent donc sur --muted : c'est une fusion, pas une\n * nuance. Le jour où la DA veut retrouver ce chaud, il faut un token\n * dédié dans tokens.css — pas une valeur en dur ici.\n * 2. le filet est `#C9CEDD` en production, un gris froid neutre ; --accent-line\n * (#c9d6f5) est le seul filet du paquet à cette clarté, mais il est\n * légèrement indigo. Écart perceptible sur un pointillé large. */\n.zv-alert--dash {\n border: 1px dashed var(--accent-line);\n background: none;\n padding: 10px 14px;\n font-size: var(--app);\n line-height: 1.55;\n color: var(--muted);\n}\n\n/* Gabarit compact (`px-3 py-[9px]`) — relevé quatre fois : login-card:207,\n * bibliotheque-client:210, branding-card:219, billing-card:164. Deux d'entre\n * eux sont des alertes de PAGE, pas de panneau : ce gabarit n'est donc pas\n * « le bandeau dans un cadre », c'est simplement le bandeau resserré. La\n * carte de suppression (delete-org-card:39) n'a aucun bandeau — l'histoire\n * qui justifiait ces valeurs dans une version antérieure était inventée, les\n * valeurs, elles, sont bonnes. */\n.zv-alert--sm {\n padding: 9px 12px;\n}\n\n/* Gabarit ample : le message d'incident qui REMPLACE une réponse attendue\n * (GPU en charge). Il occupe la place du contenu manquant, d'où le corps plus\n * grand. Source chat-client.tsx:308 : `px-4 py-[14px] text-[13.5px]/[1.6]\n * lg:px-5 lg:py-4` — le padding change au seuil, la TAILLE non (13,5px des\n * deux côtés). D'où l'absence de font-size dans le bloc mobile. */\n.zv-alert--lg {\n padding: 16px 20px;\n font-size: var(--app);\n line-height: 1.6;\n}\n\n/* ⚠️ DEUX ARBITRAGES de couleur portés ici, pas glissés sous le tapis :\n * 1. le filet vert est écrit `border-ok/30` en production (un color-mix à\n * 30 %) alors que --ok-line existe dans le paquet et n'y sert jamais.\n * Retenu : --ok-line. Le rendu diffère — le color-mix laissait\n * transparaître --ok-soft, le token est un vert franc.\n * 2. l'ambre de production est #FFF6E3 / #EBD9AE (teintes écrites en dur\n * dans deux tables), le token du paquet #faf3e2 / #ebdcb4. Retenu : les\n * tokens. Écart visible à l'œil sur les bandeaux et badges WARN. */\n\n/* ─── Étiquettes d'état ─────────────────────────────────────────────────\n * Rôle d'une entité, niveau d'un log, état d'un service, issue d'un envoi :\n * un seul objet, trois gabarits, cinq tons. Le mono en petit corps est ce\n * qui les distingue d'un texte — une étiquette se lit d'un coup d'œil, elle\n * ne se lit pas en phrase.\n *\n * Pas de text-transform : les libellés sont écrits en capitales à la source.\n * Le laisser au contenu évite qu'un libellé accentué (« INDEXÉ ») soit\n * transformé différemment selon la locale du navigateur.\n *\n * Ne pas confondre avec .zv-pill / .zv-tag / .zv-src de components.css :\n * ceux-là appartiennent à la vitrine (pastille pleine indigo, micro-label\n * espacé, puce de source). Aucun nom n'est repris ici. */\n\n.zv-badge {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n /* En cellule de grille, une étiquette étirée sur toute la colonne cesse\n * de se lire comme une étiquette : elle se cale à gauche sur sa largeur. */\n justify-self: start;\n /* Et son équivalent en flex : une colonne flex étire ses enfants, et\n * l'étiquette y deviendrait une bande. */\n align-self: start;\n border: 1px solid;\n padding: 3px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono); /* 10px relevé, palier exact du paquet */\n font-weight: 600;\n /* Un état ne se coupe jamais en deux lignes : « EN CHARGE » sur deux\n * lignes se lit comme deux états. */\n white-space: nowrap;\n border-radius: 0; /* angle vif — relevé, et arbitré depuis (25/08/2026) */\n}\n\n/* Gabarit micro : niveau de log en tête de ligne, badge de la coque. À\n * cette taille la graisse monte et les lettres s'espacent, sinon les\n * capitales se referment et INFO/WARN ne se distinguent plus en diagonale.\n * 8,5px relevé → --mono-sm (9px), le plus petit palier du paquet. */\n.zv-badge--micro {\n padding: 2px 6px;\n font-size: var(--mono-sm);\n font-weight: 700;\n letter-spacing: 0.06em;\n}\n\n/* Gabarit état de service (OK / CHARGE). ⚠️ 9,5px au relevé, rabattu sur\n * --mono (10px) : il retombe donc sur la MÊME taille que .zv-badge, et ne\n * s'en distingue plus que par l'interlettrage. C'est la conséquence assumée\n * de l'échelle à trois paliers ; le gabarit est conservé pour garder un nom\n * à cet usage, pas parce qu'il change encore de corps. */\n.zv-badge--state {\n padding: 3px 8px;\n font-size: var(--mono);\n letter-spacing: 0.06em;\n}\n\n/* Tons — triplets relevés tels quels dans les tables de teintes. */\n\n/* Neutre : `border-line bg-hairline text-[#4A5468]` (repertoire-client:25).\n * Le filet est bien --line, pas un filet adouci : une version antérieure y\n * avait substitué un token intermédiaire sans le dire. #4A5468 est le neutre\n * intermédiaire du paquet, --ink-2. */\n.zv-badge--neutre {\n background: var(--hairline);\n border-color: var(--line);\n color: var(--ink-2);\n}\n.zv-badge--accent {\n background: var(--accent-soft);\n border-color: var(--accent-line);\n color: var(--accent);\n}\n.zv-badge--ok {\n background: var(--ok-soft);\n border-color: var(--ok-line);\n color: var(--ok);\n}\n.zv-badge--warn {\n background: var(--warn-soft);\n border-color: var(--warn-line);\n color: var(--warn-ink);\n}\n.zv-badge--err {\n background: var(--err-soft);\n border-color: var(--err-line);\n color: var(--err);\n}\n\n/* Étiquette à pastille (« RÉSEAU PRIVÉ · IA DÉDIÉE », « TOUT\n * OPÉRATIONNEL »). La pastille reprend la couleur du texte plutôt qu'un ton\n * réglé à part : c'est ce que fait le relevé, et cela garantit que les deux\n * ne divergent jamais quand on change de ton.\n * ⚠️ Un jour où l'on posera une pastille sur .zv-badge--warn, currentColor\n * donnera --warn-ink (l'ambre sombre) et non --warn : cas absent du relevé,\n * à trancher avec la DA plutôt qu'à corriger ici en douce. */\n.zv-badge--dot .zv-dot {\n background: currentColor;\n}\n\n/* ─── Pastilles d'état ──────────────────────────────────────────────────\n * Seule ou dans une étiquette. Ronde par défaut ; CARRÉE dans les bandeaux\n * de provenance qui coiffent une réponse. La distinction est tenue sur cinq\n * occurrences : ce n'est pas un oubli de `rounded-full`, c'est la marque\n * que le point désigne une SOURCE et non un ÉTAT. Ne pas l'uniformiser. */\n\n.zv-dot {\n width: 6px;\n height: 6px;\n /* Sans flex: none la pastille s'écrase quand le libellé voisin est long —\n * elle devient un ovale, et l'état n'est plus lisible. */\n flex: none;\n border-radius: 9999px;\n /* Repli gris : un statut inconnu (nouvel état ajouté côté serveur, jamais\n * déployé côté interface) doit rester neutre. Le teindre en vert par\n * défaut ferait passer un échec pour un succès. */\n background: var(--label);\n}\n\n/* Pastille de provenance : carrée. Seule dérogation au rond de tout le\n * module. */\n.zv-dot--square {\n border-radius: 0;\n}\n\n/* Dans une étiquette de la coque ou une garantie : plus discrète. */\n.zv-dot--sm {\n width: 5px;\n height: 5px;\n}\n\n/* Devant un libellé de service ou une phrase de travail en cours : la\n * pastille porte seule l'information, elle grossit d'un pixel. */\n.zv-dot--lg {\n width: 7px;\n height: 7px;\n}\n\n/* Correspondance statut → couleur, relevée telle quelle. Elle vit en CSS et\n * pas dans chaque app : c'est la seule façon que « reçu » et « OCR en\n * cours » restent du même ambre partout. */\n.zv-dot--recu,\n.zv-dot--ocr {\n background: var(--warn);\n}\n.zv-dot--indexation {\n background: var(--accent);\n}\n.zv-dot--indexe,\n.zv-dot--ok {\n background: var(--ok);\n}\n.zv-dot--erreur {\n background: var(--err);\n}\n\n/* ─── Statut en clair ───────────────────────────────────────────────────\n * Pastille + libellé, sans cadre — contrairement à l'étiquette. Le cadre\n * est réservé à ce qui classe (rôle, niveau) ; le statut, lui, se lit dans\n * le fil de la ligne. Le message d'erreur REMPLACE le libellé quand il y en\n * a un : l'utilisateur veut lire la cause, pas le mot « Erreur ».\n *\n * Source : bibliotheque-client.tsx:259, `flex min-w-0 items-center\n * gap-[6px] text-[11.5px] font-medium text-[#3A4256] lg:min-w-[auto]`, avec\n * un `<span className=\"truncate\">` à l'intérieur et AUCUNE variante `lg:`\n * sur ce span. Autrement dit : le libellé tronque, et il tronque SOUS le\n * seuil, là où min-width vaut 0 ; au-dessus, la colonne reprend sa largeur\n * naturelle. Le fichier est desktop-first, donc `min-width: auto` est ici et\n * `min-width: 0` dans le bloc mobile — l'inverse exact de ce qu'une version\n * antérieure de ce module posait, qui faisait passer le libellé à la ligne\n * sur téléphone et décalait la pastille de 5px vers le bas. Rien de tout\n * cela n'existe en production. */\n\n.zv-status {\n display: flex;\n align-items: center;\n gap: 6px;\n /* Valeur initiale, écrite pour être lue : au-dessus du seuil la colonne\n * n'est pas contrainte, le libellé la pousse — c'est le `lg:min-w-[auto]`\n * du relevé. La troncature n'opère réellement que sous le seuil. */\n min-width: auto;\n font-size: var(--app-sm); /* 11,5px relevé → palier 12 */\n font-weight: 500;\n color: var(--ink-2); /* #3A4256 relevé → neutre intermédiaire du paquet */\n}\n\n/* Le libellé tronque des DEUX côtés du seuil : la classe `truncate` de la\n * source n'a pas de variante responsive. */\n.zv-status__label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ─── Absence ───────────────────────────────────────────────────────────\n * DEUX objets distincts, à ne surtout pas fusionner :\n * — la ligne d'absence, quand un filtre ne ramène rien : la collection\n * existe, seul le filtre est trop étroit. Elle vit DANS le cadre, à la\n * place des lignes, pour que l'en-tête de tableau reste visible ;\n * — l'écran d'absence, quand la collection est vide : il remplace le\n * tableau entier, porte un titre et l'action qui la remplira.\n * Les confondre donne soit un écran de bienvenue affiché à chaque filtre\n * trop strict, soit une collection vide qui n'offre aucune porte de sortie. */\n\n/* ⚠️ Les quatre lignes d'absence relevées ne s'accordent pas : `p-[26px]`\n * (repertoire-client:159), `px-[14px] py-6` (bibliotheque-client:230),\n * `px-[18px] py-8` (import-client:263), `py-5` seul (logs-client:234).\n * Retenue : la seule qui soit écrite telle quelle, 26px sur les quatre\n * côtés. Une version antérieure posait « 26px 14px », c'est-à-dire le\n * vertical de la première recomposé avec l'horizontal de la deuxième — une\n * paire plausible qui n'existe dans aucune source. */\n.zv-empty {\n padding: 26px;\n text-align: center;\n font-size: var(--app-sm);\n color: var(--muted);\n}\n\n.zv-empty--page {\n border: 1px solid var(--line);\n background: var(--white);\n padding: 48px 32px;\n text-align: center;\n}\n\n/* --app-h2 (20px) est le palier que tokens.css destine explicitement au\n * « titre de fiche, d'état vide, d'interstice ». */\n.zv-empty__title {\n /* Posé explicitement : sur un <span>, le titre viendrait se coller au\n * texte qui le suit au lieu de le surmonter, et l'état vide se lirait\n * comme une seule phrase. */\n display: block;\n font-family: var(--font-display);\n font-size: var(--app-h2);\n font-weight: 300;\n color: var(--ink);\n}\n\n.zv-empty__text {\n /* La mesure bornée n'est pas décorative : centré et pleine largeur, ce\n * paragraphe devient illisible dès 900px de cadre. */\n margin: 8px auto 0;\n max-width: 420px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* L'action de l'écran vide (le bouton qui remplit la collection). Elle est\n * ici pour l'écart seul : le bouton lui-même vient de components.css. */\n.zv-empty__action {\n margin-top: 20px;\n}\n\n/* Ni icône, ni illustration, ni « réinitialiser les filtres » : rien de tel\n * n'existe dans les sources, et ce sont exactement les trois ajouts qu'un\n * état vide attire. */\n\n/* ─── Travail en cours ──────────────────────────────────────────────────\n * En production ce n'est PAS une roue : c'est une pastille suivie d'une\n * phrase qui dit le travail réel (« Recherche dans 4 812 documents\n * anonymisés… »). Une roue dit « attendez » ; cette phrase dit ce qu'on\n * attend, sur combien, et qu'il ne se passe rien d'anormal. */\n\n.zv-loading {\n display: flex;\n align-items: center;\n gap: 9px;\n font-family: var(--font-mono);\n font-size: var(--app-sm);\n font-weight: 500;\n /* #727C93 au relevé — un gris froid, rabattu sur --muted comme les autres\n * neutres secondaires. Le token porte donc plusieurs gris du relevé : la\n * hiérarchie interne des gris de Mémoire (#6B7488 / #727C93) n'existe plus\n * dans le paquet, elle échouait de toute façon au seuil AA. */\n color: var(--muted);\n}\n\n.zv-loading .zv-dot {\n width: 7px;\n height: 7px;\n background: var(--accent);\n}\n\n/* ⚠️ STATIQUE, et c'est un choix. Aucune animation dans la source : ni\n * @keyframes, ni classe d'animation, la pastille ne bat pas. Lui ajouter un\n * pouls est l'invention la plus tentante de tout le relevé. Les boucles de\n * ce module (roue, squelette) disent une ATTENTE ; la pastille dit un\n * ÉTAT, et un état ne bat pas. Ne pas confondre `.zv-dot` avec\n * `.zv-anim-dot-active` de motion.css — ce dernier anime un point de\n * méthode de la vitrine, pas un état. */\n\n/* ─── Curseur de flux ───────────────────────────────────────────────────\n * Bloc collé à la fin du texte en cours de diffusion, sur le dernier\n * message seulement. Il dit « la réponse n'est pas finie » — sans lui, une\n * réponse coupée à mi-phrase se lit comme une réponse complète et fausse. */\n\n.zv-caret {\n margin-left: 2px;\n color: var(--accent);\n}\n\n/* Le glyphe ▍ (U+258D) est un CARACTÈRE posé dans le texte, pas un\n * pseudo-élément : il suit la dernière lettre où qu'elle tombe, y compris\n * en fin de ligne justifiée. STATIQUE là encore, aucun clignotement.\n *\n * Le flux interrompu, lui, n'a AUCUN style : c'est la phrase\n * « [Flux interrompu — la réponse est incomplète.] » ajoutée au corps du\n * message, rendue par white-space: pre-wrap. La réponse partielle est\n * conservée, jamais effacée — on n'efface pas ce que l'utilisateur a déjà lu. */\n\n/* ─── Retour d'attente : ce que fait Mémoire, et ce qu'elle ne fait pas ──\n * Le retour d'attente de cette application est TEXTUEL, et c'est cohérent\n * avec .zv-loading ci-dessus : le libellé du bouton change pendant l'action\n * (`{busy === d.id ? 'Suppression…' : 'Supprimer'}`, bibliotheque-client:283 ;\n * `{pending ? 'Connexion…' : 'Se connecter'}` au login), en plus d'un\n * `disabled:cursor-wait`. Ce n'est pas du CSS : c'est un motif de balisage à\n * reprendre, et il ne demande aucune classe.\n *\n * Restent DEUX actions sans aucun retour : la déconnexion et l'envoi de\n * message (le bouton reste plein et cliquable en apparence alors que la\n * garde existe côté script). C'est un défaut de l'application, pas une\n * intention de charte : ne pas le répliquer dans une app neuve.\n *\n * ─── Roue, squelette, infobulle : les manques, comblés ──────────────────\n * Longtemps REFUSÉS ici même, faute de matière première dans les cinq\n * écrans relevés — un système ment moins en affichant un trou qu'une valeur\n * plausible. Tranchés le 25/08/2026 (DECISIONS.md, « les manques comblés »)\n * en reprenant TELLES QUELLES les contraintes que ce module posait pour ce\n * jour-là : pas de dégradé spectre — donc un squelette en --hairline, pas\n * un balayage spectral — et une infobulle en petit panneau blanc, filet\n * --line, ombre --shadow-menu, texte --app-sm / --ink-2, la seule\n * combinaison déjà présente ailleurs dans le système. Les trois blocs\n * suivent ; ce qui restait à inventer (durées, amplitudes) est dit sur\n * place. */\n\n/* ─── Roue ──────────────────────────────────────────────────────────────\n * Pour l'attente COURTE ET LOCALE : un bouton, une cellule, un panneau qui\n * n'a pas la place d'une phrase. Elle ne remplace PAS .zv-loading : la\n * phrase qui dit le travail réel (« Recherche dans 4 812 documents… »)\n * reste le motif par défaut du système.\n *\n * Géométrie empruntée, rien d'inventé sauf la durée : 16 px (la taille de\n * .zv-icon), trait de 2 (l'épaisseur du jeu d'icônes), piste --accent-line,\n * secteur --accent. 0,8 s linéaire — une rotation qui accélère se lit comme\n * un à-coup. prefers-reduced-motion (base.css) la FIGE : l'anneau\n * interrompu reste lisible immobile, c'est pour cela que le secteur est un\n * quart et pas une moitié. */\n\n@keyframes zv-spin {\n to {\n rotate: 1turn;\n }\n}\n\n.zv-spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n flex: none;\n border: 2px solid var(--accent-line);\n border-top-color: var(--accent);\n /* Rond de FORME, pas de style : c'est le cercle qui fait lire la\n * rotation — même dérogation que la pastille d'état. */\n border-radius: 9999px;\n /* `rotate` et non `transform` : la propriété indépendante compose avec un\n * éventuel transform de placement — la leçon des boucles de motion.css. */\n animation: zv-spin 0.8s linear infinite;\n}\n\n/* En tête d'un panneau ou d'un écran d'attente : la roue seule, plus\n * grande. Le trait ne grossit pas — même compensation optique que les\n * icônes. */\n.zv-spinner--lg {\n width: 24px;\n height: 24px;\n}\n\n/* ─── Squelette ─────────────────────────────────────────────────────────\n * L'aplat --hairline exigé — pas de balayage spectral, pas de dégradé de\n * gris que la charte n'a pas. La pulsation est une OPACITÉ (1 → 0,55), pas\n * une couleur : elle dit « en chargement » sans introduire de teinte.\n * 1,8 s en ease-in-out, la respiration de zv-breathe plutôt que la courbe\n * d'entrée — c'est une boucle, pas un mouvement.\n *\n * La géométrie vient du CONSOMMATEUR (largeur et hauteur en style, comme la\n * largeur de Progress) : un squelette qui invente ses dimensions ne\n * ressemble jamais au contenu qu'il annonce. Seule la variante --text fixe\n * une hauteur d'un cadratin, pour mimer une ligne du texte courant.\n *\n * ⚠️ Un squelette ANNONCE une structure : ne jamais en poser un pour un\n * contenu dont la forme est inconnue — c'est .zv-loading qui dit ce cas. */\n\n@keyframes zv-skeleton {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.55;\n }\n}\n\n.zv-skeleton {\n display: block;\n background: var(--hairline);\n animation: zv-skeleton 1.8s ease-in-out infinite;\n}\n\n.zv-skeleton--text {\n height: 1em;\n}\n\n/* ─── Infobulle ─────────────────────────────────────────────────────────\n * La combinaison imposée par l'arbitrage : panneau blanc, filet --line,\n * ombre --shadow-menu, texte --app-sm / --ink-2. Posée AU-DESSUS de la\n * cible, centrée, à 6 px — au-dessus parce que le pointeur vient d'en bas\n * et la masquerait en dessous.\n *\n * Elle se montre au survol et au focus de l'enveloppe (.zv-tooltip-hit),\n * avec un retard de 300 ms à l'APPARITION seulement — sans lui, traverser\n * une rangée d'icônes fait clignoter une infobulle par icône. La\n * disparition, elle, est immédiate.\n *\n * Le masquage est `visibility` ET opacité, à dessein (WCAG 1.4.13) :\n * · cachée en visibility:hidden, la bulle ne capte pas le pointeur — pas\n * de zone de survol fantôme au-dessus de chaque cible ;\n * · visible, elle EST survolable : descendante de l'enveloppe, le\n * pointeur peut la traverser sans la fermer ;\n * · Échap la ferme — la façade pose --off jusqu'au blur, seul\n * comportement qui demande une ligne de script.\n *\n * ⚠️ Réservée à un COMPLÉMENT dispensable (un raccourci, une date exacte,\n * une unité) : une commande n'a pas le droit de cacher son nom dans une\n * infobulle, et le tactile ne survole pas. L'annonce passe par\n * aria-describedby, que la façade React câble. */\n\n.zv-tooltip-hit {\n position: relative;\n display: inline-flex;\n}\n\n.zv-tooltip {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n /* La couche du menu (navigation.css) : une infobulle flotte au même\n * étage que lui et ne le croise jamais. */\n z-index: 60;\n width: max-content;\n max-width: 240px;\n border: 1px solid var(--line);\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 6px 10px;\n font-size: var(--app-sm);\n line-height: 1.5;\n font-weight: 500;\n color: var(--ink-2);\n text-align: left;\n opacity: 0;\n visibility: hidden;\n /* La visibilité bascule APRÈS le fondu de sortie, jamais pendant. */\n transition:\n opacity 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n visibility 0s linear 140ms;\n}\n\n.zv-tooltip-hit:hover > .zv-tooltip,\n.zv-tooltip-hit:focus-within > .zv-tooltip {\n opacity: 1;\n visibility: visible;\n transition-delay: 300ms, 300ms;\n}\n\n/* Fermée à Échap : posée par la façade, levée au blur ou à la sortie du\n * pointeur. Déclarée APRÈS l'ouverture pour l'emporter à spécificité\n * égale. */\n.zv-tooltip-hit--off > .zv-tooltip {\n opacity: 0;\n visibility: hidden;\n transition-delay: 0s, 0s;\n}\n\n/* Sous la cible, pour les rares cas où le haut est occupé (première ligne\n * d'un tableau sous son en-tête). */\n.zv-tooltip--bas {\n bottom: auto;\n top: calc(100% + 6px);\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Un seul point de rupture, 768px, comme dans tous les modules du paquet.\n * Ce bloc ne contient QUE des bascules attestées dans la source (variantes\n * `lg:` de Mémoire, lues en sens inverse) — pas une seule règle de confort\n * ajoutée en chemin. */\n\n@media (max-width: 767px) {\n /* Les cinq bandeaux d'authentification portent `text-[13.5px]\n * lg:text-[12.5px]` (login-card:207, reset-password-card:78 et :137,\n * forgot-password-card:60 et :96) : le texte GROSSIT d'un palier sous le\n * seuil. C'est le motif le mieux attesté du module ; l'oublier laissait le\n * bandeau à sa plus petite taille précisément sur l'écran le plus étroit. */\n .zv-alert {\n font-size: var(--app);\n }\n\n /* Le bandeau pointillé fait l'inverse — `text-[13px]` en desktop,\n * `text-[12.5px]` sous le seuil — et resserre sa gouttière : 16px de\n * chaque côté sur 375px déjà bordés par le padding de page, c'est la\n * moitié de la largeur perdue en marges. Déclaré APRÈS .zv-alert pour\n * l'emporter à spécificité égale. */\n .zv-alert--dash {\n padding: 10px 12px;\n font-size: var(--app-sm);\n }\n\n /* Le message d'incident resserre son padding (`px-4 py-[14px]`) et GARDE\n * sa taille : la source n'a pas de variante `lg:` sur le corps. Redéclaré\n * ici uniquement pour n'être pas rétréci par la règle .zv-alert ci-dessus,\n * à spécificité égale. */\n .zv-alert--lg {\n padding: 14px 16px;\n font-size: var(--app);\n }\n\n /* C'est ici, et seulement ici, que la troncature du statut opère : le\n * `min-w-0` de la source vaut sous le seuil, et sans lui l'ellipse ne se\n * déclenche pas dans une cellule flex ou grid — le libellé pousse la\n * colonne. C'est la cause n° 1 de débordement horizontal des tableaux. */\n .zv-status {\n min-width: 0;\n }\n\n /* Même garde-fou que le menu et le popover : plafonnée à l'écran, la\n * bulle ne pousse jamais la page en défilement horizontal. Le centrage\n * sur une cible collée au bord peut encore la décaler — limite connue,\n * une variante d'ancrage latéral s'arbitrera si le cas se présente. */\n .zv-tooltip {\n max-width: calc(100vw - 40px);\n }\n\n /* ⚠️ NON RELEVÉ : aucune variante `lg:` sur le padding de l'écran vide.\n * Ce resserrement est une décision de portage, prise parce que 32px de\n * gouttière sur 375px laissent 311px de contenu à un bloc qui porte un\n * titre et un bouton. Si la DA la refuse, c'est cette règle qui saute —\n * pas le reste du bloc. */\n .zv-empty--page {\n padding: 40px 20px;\n }\n}\n\n\n/* ── navigation.css ── */\n/* Zevra UI — navigation : le chrome de l'applicatif\n * Colonne latérale, barre supérieure, tiroir, feuille de source citée, menu\n * déroulant, fil d'Ariane, pagination, mécanique de défilement — et, depuis\n * le 25/08/2026 (hors relevé, voir DECISIONS.md) : l'onglet souligné, le\n * popover et la barre d'onglets mobile.\n *\n * DÉPEND DE :\n * · tokens.css — neutres (--ink, --ink-2, --muted, --label, --line,\n * --hairline, --panel, --white), --accent / --accent-soft, échelles\n * --app-* et --mono-*, mesures --tap et --topbar-h, --shadow-menu ;\n * · base.css — reset box-sizing, :focus-visible, style des liens sans\n * classe, et la coupure globale prefers-reduced-motion (ce module ne la\n * repose donc pas) ;\n * · layout.css — l'inventaire des couches d'empilement, dont l'échelle\n * ci-dessous est le prolongement ;\n * · icons.css — .zv-icon (glyphes tracés) et .zv-icon-btn (bouton d'icône\n * nu, cible --tap), réemployés ici plutôt que redéfinis.\n * Ne repose ni le reset, ni le body, ni :root.\n *\n * NE REDÉFINIT RIEN de ce que les autres modules possèdent déjà :\n * .zv-navlink et .zv-nav__* (nav de vitrine, layout.css) ne sont pas touchés —\n * l'entrée applicative s'appelle .zv-sidelink ; le déclencheur du menu est\n * .zv-select et l'onglet en PUCES réemploie .zv-seg, tous deux dans\n * forms.css. L'onglet souligné, lui, a sa classe ici depuis l'arbitrage du\n * 25/08/2026.\n *\n * Ce module est un RELEVÉ de l'application Mémoire, en production, pas une\n * proposition de DA : chaque valeur vient de src/components/app-shell.tsx,\n * src/components/citation.tsx ou des écrans (chat, répertoire, fiche). Ce qui\n * manque à la production — focus, états désactivés, clavier — est signalé par\n * un ⚠️ à l'endroit exact plutôt que comblé en silence, et ce qui n'a AUCUNE\n * source est étiqueté HORS RELEVÉ ou DÉRIVÉ, jamais présenté comme relevé.\n *\n * ⚠️ SEUIL. Mémoire bascule à `lg` (1024px) ; le paquet n'a qu'un point de\n * rupture, 768px, et c'est celui-là qui s'applique. Ce qui est relevé, ce sont\n * les DEUX jeux de valeurs (doigt / souris), jamais la largeur où ils\n * basculent. Tout le responsive est en fin de fichier, sous la seule forme\n * employée par base.css et components.css : `max-width: 767px`. Les règles de\n * base décrivent donc l'écran large, et le bloc final ramène au doigt.\n *\n * ─── Empilement ────────────────────────────────────────────────────────\n * layout.css tient l'inventaire des couches du paquet et s'arrête à 50\n * (.zv-layer-top), avec la consigne « toute nouvelle couche s'arbitre par\n * rapport à celle-ci ». Les recouvrements applicatifs se rangent donc\n * AU-DESSUS, sans jamais se mettre à égalité avec une couche de vitrine : un\n * voile de tiroir et un header fixe départagés par l'ordre du DOM, ce n'est\n * pas une échelle. Le relevé donnait voile 40, menu 40, feuille 50, tiroir 60 ;\n * seuls les RAPPORTS comptent, et ils sont conservés — l'origine est décalée\n * en bloc pour dégager l'inventaire de vitrine :\n *\n * menu 60 · voile 60 · feuille de source 70 · tiroir 80\n *\n * L'égalité menu/voile est conservée parce qu'elle est assumée : les deux ne\n * coexistent jamais (ouvrir le tiroir ferme le menu). C'est autre chose qu'une\n * égalité subie entre deux pièces qui, elles, se croisent.\n * N'insérer aucune valeur intermédiaire sans rejuger l'échelle entière.\n *\n * ─── Gabarit attendu ───────────────────────────────────────────────────\n * Toutes ces pièces supposent une racine applicative en `height: 100dvh;\n * overflow: hidden; display: flex` — colonne sous le seuil, rangée au-dessus.\n * Sans elle, aucune zone défilante ne tient sa hauteur et c'est la page\n * entière qui défile, ce qui ruine la colonne comme la feuille. Cette racine\n * relève de la mise en page, pas de ce module (cf. notes).\n */\n\n/* ─── Colonne latérale ──────────────────────────────────────────────────\n * Une colonne, un filet, rien d'autre : ni ombre, ni rayon. Une ombre ferait\n * flotter une pièce qui ne bouge jamais, et le rayon appartient à l'action\n * (--r-btn) ou à la carte (--r-card), pas au chrome. */\n\n.zv-sidebar {\n display: flex;\n width: 224px;\n /* Sans `flex: none`, une entrée de nav un peu longue élargit la colonne et\n * décale le contenu : la largeur est une constante, pas un résultat. */\n flex: none;\n flex-direction: column;\n /* La colonne défile pour son propre compte : une nav longue ne doit jamais\n * entraîner le contenu, ni l'inverse. */\n overflow-y: auto;\n border-right: 1px solid var(--line);\n background: var(--panel);\n}\n\n.zv-sidebar__head {\n border-bottom: 1px solid var(--line);\n padding: 20px 18px 14px;\n}\n\n/* `margin-top: auto` colle le pied en bas quand la nav est courte et le laisse\n * suivre le contenu quand elle est longue. Un `position: sticky` aurait\n * recouvert la dernière entrée dès que la colonne défile. */\n.zv-sidebar__foot {\n margin-top: auto;\n border-top: 1px solid var(--line);\n}\n\n/* Le même balisage sert à l'aside et au panneau du tiroir. Dans le tiroir, la\n * croix se pose en absolu par-dessus la tête : sans cette réserve, un logo\n * client large passe dessous et devient incliquable. */\n.zv-drawer .zv-sidebar__head > :first-child {\n padding-right: 46px;\n}\n\n/* ─── Groupes de navigation ─────────────────────────────────────────────\n * Nommés .zv-sidegroup et non .zv-navgroup : layout.css possède déjà .zv-nav,\n * .zv-nav__logo, .zv-nav__links et .zv-navlink pour la nav de VITRINE. Un\n * .zv-navgroup posé à côté se lirait comme une pièce de cette famille-là. La\n * colonne applicative a sa propre lignée : sidebar › sidegroup › sidelink. */\n\n.zv-sidegroup {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 12px 10px;\n}\n\n.zv-sidegroup__title {\n padding: 2px 10px 8px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* À partir du deuxième groupe, c'est le blanc au-dessus de l'intitulé qui\n * sépare — pas un filet. La colonne en porte déjà deux (tête et pied) ; un\n * troisième trait la transformerait en tableau. */\n.zv-sidegroup + .zv-sidegroup .zv-sidegroup__title {\n padding-top: 14px;\n}\n\n/* ─── Entrée de barre latérale ──────────────────────────────────────────\n * `.zv-navlink` appartient déjà à la navigation de vitrine (layout.css) :\n * l'entrée applicative s'appelle `.zv-sidelink`, et les deux ne doivent\n * jamais se recouvrir. */\n\n.zv-sidelink {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n font-family: var(--font-text);\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink-2);\n text-decoration: none;\n}\n\n/* ⚠️ CONTRE-ORDRE, pas un style. base.css repeint les liens nus en accent, et\n * l'application hôte va souvent plus loin (`a { color: accent }`,\n * `a:hover { text-decoration: underline }`). Sans ces deux déclarations\n * d'apparence redondante, l'entrée de nav vire au bleu souligné dès qu'elle\n * tombe dans une page qui applique le style éditorial. */\n.zv-sidelink:hover {\n background: var(--hairline);\n color: var(--ink-2);\n text-decoration: none;\n}\n\n/* `aria-current=\"page\"` est ajouté au relevé : la production ne marque\n * l'entrée courante que visuellement, ce qui la rend muette au lecteur\n * d'écran. Les deux sélecteurs valent la même chose pour que le balisage\n * correct ne coûte rien. */\n.zv-sidelink--on,\n.zv-sidelink[aria-current=\"page\"] {\n background: var(--accent-soft);\n font-weight: 600;\n color: var(--accent);\n}\n\n/* Au survol de l'entrée courante, le fond change mais le texte reste accent :\n * sinon la page où l'on se trouve « s'éteint » sous le pointeur. */\n.zv-sidelink--on:hover,\n.zv-sidelink[aria-current=\"page\"]:hover {\n background: var(--hairline);\n color: var(--accent);\n}\n\n/* Le contour de base.css déborde de 2px ; dans une colonne en `overflow-y:\n * auto` il est rogné par le bord du scroller. On le rentre plutôt que d'en\n * redessiner un autre. */\n.zv-sidelink:focus-visible {\n outline-offset: -2px;\n}\n\n/* Largeur fixe : le rang mono et le glyphe ⚙ du panneau admin doivent tomber\n * sur la même colonne, sinon les libellés se décalent d'un groupe à l'autre.\n * La production écrit 10px en dur (app-shell.tsx:160) ; c'est exactement\n * --mono, donc le token remplace la valeur sans rien déplacer. */\n.zv-sidelink__num {\n width: 16px;\n flex: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n color: var(--label);\n}\n\n/* ─── Barre supérieure ──────────────────────────────────────────────────\n * Elle remplace la colonne sous le seuil : masquée au-dessus, elle n'apparaît\n * que dans le bloc mobile de fin de fichier. */\n\n.zv-topbar {\n display: none;\n /* --topbar-h nomme la HAUTEUR DE LA BARRE, et elle seule : 52px. Le filet\n * qui la termine est un pixel de plus, et c'est pour cela que la feuille de\n * source commence à 53 (cf. .zv-cite-panel dans le bloc mobile). Les deux\n * valeurs de la production sont justes et se répondent — les unifier sur 52\n * ferait remonter la feuille par-dessus le filet.\n * ⚠️ Nuance à connaître avant d'y toucher : base.css impose box-sizing:\n * border-box, donc ce `border-bottom` est peint À L'INTÉRIEUR des 52px. Le\n * modèle « 52 + 1 » n'est littéral que si le filet sort de la boîte —\n * une ligne suffirait (`box-shadow: 0 1px 0 var(--line)` au lieu du\n * `border-bottom`), mais c'est un écart au relevé : à arbitrer, pas à\n * décider ici. */\n height: var(--topbar-h);\n flex: none;\n align-items: center;\n gap: 10px;\n border-bottom: 1px solid var(--line);\n background: var(--panel);\n padding-inline: 16px;\n}\n\n/* Le logo cède AVANT le nom : un logo client très large ne doit jamais\n * pousser la barre hors de l'écran. */\n.zv-topbar__logo {\n min-width: 0;\n overflow: hidden;\n}\n\n.zv-topbar__title {\n min-width: 0;\n flex: 1 1 0%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app-sm);\n font-weight: 600;\n color: var(--ink);\n}\n\n/* Le hamburger n'est PAS un bouton propre à ce module : c'est le .zv-icon-btn\n * d'icons.css (cible --tap, sans cadre, couleur --muted) posé dans la barre.\n * Le balisage est donc `class=\"zv-icon-btn zv-topbar__burger\"`, et seul le\n * décalage optique appartient à la barre : la marge négative aligne le GLYPHE\n * sur le bord de la barre alors que la boîte garde ses 40px de cible tactile —\n * sans elle, il faut choisir entre l'alignement optique et le doigt.\n * Le relevé ne donne AUCUN état au hamburger ; le survol d'icons.css peint\n * --panel, qui est exactement le fond de cette barre — donc invisible ici.\n * Seule la couleur du glyphe s'assombrit d'un cran, ce qui vient du composant\n * partagé et non d'une décision de ce module. */\n.zv-topbar__burger {\n margin-right: -8px;\n}\n\n/* ─── Barre d'onglets mobile ────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Mémoire navigue au tiroir ;\n * une application à trois ou quatre sections navigue plus vite par une\n * barre basse. CINQ entrées au maximum — au-delà, c'est le tiroir.\n *\n * N'existe que SOUS le point de rupture : au-dessus, la colonne latérale\n * est là. Tout le dessin vit donc dans le bloc mobile de fin de fichier ;\n * ici, seulement l'effacement desktop.\n *\n * ⚠️ Elle réserve de la place : le gabarit qui la pose doit donner au\n * contenu un padding-bottom à sa hauteur, sinon la dernière ligne de la\n * page se cache dessous. */\n\n.zv-tabbar {\n display: none;\n}\n\n/* ─── Bouton de chrome (croix bordée) ───────────────────────────────────\n * ⚠️ RENOMMÉ : ce bouton s'appelait .zv-icon-btn, nom que icons.css possède\n * déjà pour tout autre chose (bouton nu de 40px, coins arrondis). Deux\n * géométries sous un seul nom, c'est la dernière qui charge qui gagne : la\n * croix bordée du chrome devient .zv-chrome-btn.\n * Un seul emploi : la croix de fermeture du tiroir et de la feuille de source.\n * Angles vifs — le rayon signale l'action, le chrome n'en est pas une. */\n\n.zv-chrome-btn {\n display: flex;\n width: 26px;\n height: 26px;\n flex: none;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n /* base.css ne reset pas les boutons : sans police ni fond explicites, un\n * <button> retombe sur ceux du navigateur et détonne. Même précaution que\n * .zv-btn dans components.css. */\n font-family: var(--font-text);\n font-size: var(--app);\n line-height: 1;\n color: var(--muted);\n cursor: pointer;\n}\n\n/* ⚠️ Divergence relevée : la croix du tiroir n'a AUCUN état en production,\n * celle de la feuille de source porte ce survol. Retenu : le survol partout,\n * un bouton sans réaction se lit comme un décor. */\n.zv-chrome-btn:hover {\n background: var(--hairline);\n}\n\n.zv-chrome-btn:focus-visible {\n outline-offset: -2px;\n}\n\n/* ⚠️ HORS RELEVÉ : aucun bouton de chrome n'est jamais désactivé en\n * production. Posé parce qu'un bouton grisé sans style se lit comme un bouton\n * actif ; aucune couleur nouvelle n'est introduite. */\n.zv-chrome-btn[disabled] {\n color: var(--label);\n cursor: default;\n}\n\n.zv-chrome-btn[disabled]:hover {\n background: var(--white);\n}\n\n/* Glyphes relevés, à reproduire tels quels :\n * · hamburger — SVG 18×12 (viewBox \"0 0 18 12\"),\n * <path d=\"M0 1h18M0 6h18M0 11h18\" stroke=\"currentColor\" stroke-width=\"1.5\"/> :\n * trois traits pleine largeur, entraxe 5px, extrémités franches. Ce n'est\n * PAS une icône 24×24 d'une bibliothèque, et une icône ronde tirée d'un\n * jeu tiers casserait la géométrie de la barre. Il ne suit donc pas la\n * grille de 24 de icons.css : il se pose en SVG nu, pas en .zv-icon.\n * · fermeture — le caractère ✕ (U+2715) posé en texte, d'où le\n * `font-size` ci-dessus qui le dimensionne. */\n\n/* ─── Feuille de source citée ───────────────────────────────────────────\n * ⚠️ RENOMMÉE : cette pièce s'appelait .zv-sheet. layout.css possède déjà\n * .zv-sheet-section (section de PLANCHE de documentation, avec son __title et\n * son __body) : côte à côte, .zv-sheet__title et .zv-sheet-section__title\n * donnent à lire un bloc et son modificateur là où il y a deux objets sans\n * rapport. Elle prend le nom de ce qui l'ouvre — un clic sur une citation :\n * .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\n * ne se confond pas avec .zv-src (la PUCE de source, components.css).\n *\n * Colonne au-dessus du seuil, feuille quasi plein écran en dessous. Trois\n * zones : tête fixe, corps défilant, pied d'actions. */\n\n.zv-cite-panel {\n display: flex;\n height: 100dvh;\n width: 392px;\n flex: none;\n flex-direction: column;\n border-left: 1px solid var(--line);\n /* Blanc, et non --panel : c'est une surface de lecture, pas du chrome. */\n background: var(--white);\n}\n\n.zv-cite-panel__head {\n display: flex;\n /* `flex-start` et non `center` : un titre de document sur trois lignes ne\n * doit pas faire descendre la croix au milieu du bloc. */\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n border-bottom: 1px solid var(--line);\n padding: 18px 20px 14px;\n}\n\n.zv-cite-panel__title {\n font-family: var(--font-display);\n font-size: var(--app-h3);\n line-height: 1.35;\n font-weight: 500;\n /* Un nom de fichier sans espace déborderait la colonne et pousserait la\n * croix hors de l'écran. */\n overflow-wrap: break-word;\n}\n\n/* Le corps est la seule zone qui défile : le fond gris l'enfonce d'un cran\n * pour que la page citée s'y détache comme une feuille posée. */\n.zv-cite-panel__body {\n flex: 1 1 0%;\n overflow-y: auto;\n background: var(--hairline);\n padding: 18px;\n}\n\n.zv-cite-panel__foot {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border-top: 1px solid var(--line);\n padding: 14px 20px;\n}\n\n/* Fermeture par Échap, par le voile, par la croix, et automatiquement à chaque\n * navigation. AUCUNE transition d'entrée ni de sortie, contrairement au\n * tiroir : la feuille répond à un clic sur une citation, elle doit être là au\n * moment où le regard y va.\n * ⚠️ NON LIVRÉ : le kicker « SOURCE CITÉE » de la tête (citation.tsx:104) n'est\n * dans aucun inventaire, et il n'est pas tranché s'il relève de la typographie\n * (.zv-kicker, base.css) ou de ce module. Rien n'est inventé en attendant. */\n\n/* ─── Menu déroulant ────────────────────────────────────────────────────\n * Ancré sous son déclencheur (.zv-select, forms.css), qui DOIT porter\n * `position: relative`, sans quoi le menu se colle au coin de la page.\n * L'enveloppe .zv-select-anchor (forms.css) le porte déjà : c'est elle qu'on\n * pose autour d'un déclencheur .zv-select, et non le déclencheur lui-même. */\n\n.zv-menu {\n position: absolute;\n top: calc(100% + 5px);\n left: 0;\n z-index: 60;\n display: flex;\n flex-direction: column;\n min-width: 230px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n padding: 5px;\n /* Seule ombre du chrome applicatif, et elle est justifiée : un menu flotte\n * vraiment au-dessus de la page, contrairement à la colonne ou au tiroir.\n * --shadow-card est trop courte pour ce tirage, --shadow-panel trop indigo :\n * d'où --shadow-menu, qui existe pour ce seul usage. */\n box-shadow: var(--shadow-menu);\n}\n\n.zv-menu__item {\n display: block;\n /* Les quatre déclarations qui suivent existent pour que l'item tienne aussi\n * bien en <button> qu'en <div> : sans elles, le navigateur impose sa police,\n * son fond gris, son cadre et un texte centré. */\n width: 100%;\n border: 0;\n background: none;\n text-align: left;\n padding: 8px 11px;\n font-family: var(--font-text);\n font-size: var(--app-sm);\n font-weight: 400;\n color: var(--ink);\n cursor: pointer;\n}\n\n.zv-menu__item:hover {\n background: var(--hairline);\n}\n\n/* ⚠️ CONTRE-ORDRE. Une entrée de menu rendue en <a> (une navigation, par\n * opposition à une action) hérite du soulignement par défaut du navigateur,\n * là où la même entrée en <button> n'a rien : dans un même panneau,\n * « Mon compte » se soulignait et « Se déconnecter » non. Les deux doivent\n * se rendre à l'identique — c'est la BALISE qui change, pas l'apparence. */\na.zv-menu__item,\na.zv-menu__item:hover {\n text-decoration: none;\n}\n\n/* ⚠️ Option SURVOLÉE AU CLAVIER — ajoutée le 25/08/2026 avec la sélection\n * dessinée. Ce n'est ni :hover (la souris n'est pas dessus) ni :focus (le\n * focus reste sur le déclencheur, c'est aria-activedescendant qui porte\n * l'annonce). Sans cette classe, parcourir la liste aux flèches ne se voit\n * pas : le lecteur d'écran suit, l'œil non. */\n.zv-menu__item--survol {\n background: var(--hairline);\n}\n\n.zv-menu__item:focus-visible {\n outline-offset: -2px;\n}\n\n.zv-menu__item--on,\n.zv-menu__item[aria-selected=\"true\"] {\n background: var(--accent-soft);\n color: var(--accent);\n font-weight: 600;\n}\n\n/* ⚠️ Relevé tel quel : le survol ÉCRASE le fond de l'item courant\n * (accent-soft → hairline), seul le texte reste accent. Volontaire ou non,\n * c'est ce qui tourne en production — signalé plutôt que corrigé en silence. */\n.zv-menu__item--on:hover,\n.zv-menu__item[aria-selected=\"true\"]:hover {\n background: var(--hairline);\n}\n\n/* ⚠️ HORS RELEVÉ : aucun item désactivé n'existe en production. Même\n * traitement que le bouton de chrome, sans couleur nouvelle. */\n.zv-menu__item[disabled],\n.zv-menu__item[aria-disabled=\"true\"] {\n color: var(--label);\n cursor: default;\n}\n\n.zv-menu__item[disabled]:hover,\n.zv-menu__item[aria-disabled=\"true\"]:hover {\n background: none;\n}\n\n/* ⚠️ MANQUENT en production et NON comblés ici, car ce sont des comportements,\n * pas du style : navigation au clavier (flèches, Origine/Fin), fermeture au\n * clic extérieur, fermeture à Échap. Le CSS ci-dessus est prêt à les recevoir\n * (aria-selected, :focus-visible) ; le composant doit les écrire. */\n\n/* ─── Entrées à menu déroulant de la barre vitrine ──────────────────────\n *\n * ⚠️ CE BLOC DOIT RESTER APRÈS `.zv-menu`, et donc dans CE fichier. Le\n * panneau d'une entrée porte les deux classes ; `.zv-navdd__menu` reprend à\n * `.zv-menu` son display, sa position et son ancrage, à spécificité égale —\n * c'est l'ORDRE qui tranche. Posé dans layout.css, qu'index.css importe\n * AVANT navigation.css, il perdait : les trois menus d'une barre\n * s'affichaient en même temps, ouverts en permanence.\n *\n * Une nav de vitrine qui porte l'architecture d'un site — des familles,\n * pas quatre liens — a besoin de déplier. Ouverture au survol ET au focus\n * (`:focus-within`) : en CSS seul, donc accessible au clavier sans une\n * ligne de JavaScript et sans état à tenir.\n *\n * Le panneau est le .zv-menu du paquet, simplement ancré sous l'entrée. */\n\n.zv-navdd {\n position: relative;\n display: inline-flex;\n}\n\n/* Le bouton porte .zv-navlink : il doit se rendre EXACTEMENT comme un lien\n * simple de la barre. Piège à éviter — un `font: inherit`, réflexe habituel\n * pour dénuder un bouton, remet la fonte du CONTENEUR (16px/400) là où\n * .zv-navlink pose 14px/500 : les entrées à menu deviennent plus grosses\n * que les autres. On ne reprend donc du bouton que ce qui doit disparaître. */\n.zv-navdd__btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 0;\n border: 0;\n background: none;\n cursor: pointer;\n font-family: inherit;\n line-height: inherit;\n}\n\n.zv-navdd__chevron {\n width: 12px;\n height: 12px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n /* Bouts francs : le trait carré du jeu d'icônes (25/08/2026). */\n stroke-linecap: square;\n stroke-linejoin: miter;\n transition: transform 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-navdd:hover .zv-navdd__chevron,\n.zv-navdd:focus-within .zv-navdd__chevron {\n transform: rotate(180deg);\n}\n\n/* Le pont invisible (::before) couvre l'interstice entre l'entrée et son\n * panneau : sans lui, le survol se rompt en descendant vers le menu et le\n * menu se referme sous le curseur. */\n.zv-navdd__menu {\n position: absolute;\n top: 100%;\n left: 50%;\n translate: -50% 0;\n padding-top: 14px;\n display: none;\n min-width: 250px;\n z-index: 50;\n}\n\n.zv-navdd__menu::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 14px;\n}\n\n.zv-navdd:hover .zv-navdd__menu,\n.zv-navdd:focus-within .zv-navdd__menu {\n display: flex;\n flex-direction: column;\n}\n\n.zv-navdd__sep {\n display: block;\n height: 1px;\n margin: 6px 0;\n background: var(--hairline);\n}\n\n/* ─── Popover ───────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Le menu impose ses items ;\n * il n'existait aucune surface ancrée LIBRE — aide contextuelle,\n * mini-formulaire de filtre. Même ancre que le menu (.zv-select-anchor,\n * forms.css), même surface (filet --line, ombre --shadow-menu), padding de\n * panneau au lieu d'items. Fermeture par clic extérieur et Échap : au\n * composant, comme pour le menu.\n * ⚠️ Ce n'est PAS une infobulle : le popover s'ouvre au CLIC et peut porter\n * de l'interactif ; l'infobulle (feedback.css) se montre au survol et n'en\n * porte jamais. */\n\n.zv-popover {\n position: absolute;\n top: calc(100% + 5px);\n left: 0;\n z-index: 60;\n min-width: 230px;\n max-width: 320px;\n border: 1px solid var(--line);\n border-radius: 0;\n background: var(--white);\n box-shadow: var(--shadow-menu);\n padding: 14px 16px;\n}\n\n/* ─── Fil d'Ariane ──────────────────────────────────────────────────────*/\n\n.zv-crumbs {\n display: block;\n margin-bottom: 14px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n /* Le fil porte des noms de dossier entiers : il a le droit de passer à la\n * ligne, pas celui d'élargir la page. */\n overflow-wrap: break-word;\n color: var(--label);\n text-decoration: none;\n cursor: pointer;\n}\n\n/* Même contre-ordre que .zv-sidelink : sans lui, le fil vire au bleu souligné\n * dans toute page qui applique le style de lien éditorial. */\n.zv-crumbs:hover {\n color: var(--label);\n text-decoration: none;\n}\n\n.zv-crumbs__sep {\n margin-inline: 4px;\n}\n\n/* Le dernier segment est plus sombre que le fil : c'est lui qu'on lit, le\n * reste n'est qu'un chemin. Aucun état « segment courant » n'existe au relevé. */\n.zv-crumbs__seg {\n color: var(--ink-2);\n}\n\n/* ─── Pagination ────────────────────────────────────────────────────────\n * Deux formes relevées, et deux seulement : le rang centré d'un panneau\n * (‹ p. 12 / 31 ›) et le pied de tableau (compte à gauche, « ‹ Précédent ·\n * Suivant › » à droite).\n *\n * ⚠️ DOUBLON À TRANCHER, et il est tranché ici : data.css pose .zv-table-foot\n * pour le MÊME nœud du DOM (repertoire-client.tsx:165) avec les mêmes valeurs.\n * La pagination est de la navigation, pas de la donnée : la forme reste dans ce\n * module, sous .zv-pager--foot, et data.css doit abandonner .zv-table-foot.\n * .zv-pager--foot est un MODIFICATEUR : il se pose avec .zv-pager, jamais seul. */\n\n.zv-pager {\n display: flex;\n justify-content: center;\n gap: 14px;\n /* Décolle le rang des boutons du pied de feuille, où il est immédiatement\n * suivi de deux actions pleine largeur. */\n padding-bottom: 2px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n color: var(--muted);\n}\n\n/* `font: inherit` plutôt qu'une police redite : les trois éléments cliquables\n * de la pagination sont des <button>, et sans lui le navigateur y impose sa\n * police système. Hériter reprend exactement la mono du rang — aucune valeur\n * nouvelle n'entre par cette porte. */\n.zv-pager__arrow,\n.zv-pager__rank,\n.zv-pager__step {\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n padding: 0;\n cursor: pointer;\n}\n\n/* ⚠️ HORS RELEVÉ. La production n'a AUCUN état désactivé : les flèches sont\n * identiques en première et en dernière page. Posé ici parce qu'une flèche\n * qui ne fait rien et ne le dit pas se lit comme une panne. `pointer-events`\n * n'est volontairement pas coupé : en aria-disabled, l'élément doit rester\n * atteignable au clavier pour être annoncé. */\n.zv-pager__arrow[disabled],\n.zv-pager__arrow[aria-disabled=\"true\"] {\n color: var(--label);\n cursor: default;\n}\n\n/* Pied de tableau : le compte à gauche, un SEUL élément cliquable pour les\n * deux sens à droite — pas de numéros de page, pas de page courante. Le\n * `padding` en raccourci annule le `padding-bottom: 2px` du rang de panneau,\n * qui n'a pas de sens ici. */\n.zv-pager--foot {\n justify-content: space-between;\n align-items: center;\n gap: 0;\n padding: 12px 4px;\n color: var(--label);\n}\n\n/* ⚠️ TROU ASSUMÉ, et il dépasse le style : il n'existe nulle part de page\n * courante, de numéros, ni de sens séparés. Une pagination digne de ce nom\n * (précédent et suivant distincts, désactivables, rang courant) est une\n * DÉCISION de conception, pas un relevé — cf. notes. */\n\n/* ─── Onglets ───────────────────────────────────────────────────────────\n * ⚠️ AUCUNE MATIÈRE PREMIÈRE : pas un seul jeu d'onglets dans les écrans\n * relevés. La rangée de PUCES se construit toujours avec .zv-seg-group et\n * .zv-seg (forms.css), qui portent déjà la grammaire d'un état actif —\n * c'est le motif des FILTRES, qui restreignent une collection.\n *\n * L'onglet SOULIGNÉ, longtemps « à faire », est ARBITRÉ le 25/08/2026\n * (DECISIONS.md) : trait actif de 2 px — l'épaisseur des filets dessinés du\n * système (filet de spectre, rail d'étapes) — en --accent, la couleur de\n * l'action, comme l'entrée de nav courante. PAS le spectre : un quatrième\n * emploi n'existe pas. C'est le motif des VUES d'un même objet (Aperçu /\n * Sources) ; pour filtrer, prendre les puces. */\n\n.zv-tabs-line {\n display: flex;\n gap: 22px;\n /* La rangée ne s'enroule pas — des onglets sur deux lignes ne se lisent\n * plus comme des onglets : elle déborde dans son propre cadre, jamais\n * dans la page. */\n overflow-x: auto;\n /* La ligne de la rangée est une ombre INTERNE, pas une bordure : les\n * onglets (étirés à la hauteur de la rangée) peignent leur trait\n * par-dessus. Le montage classique — bordure de rangée + marge basse de\n * -1px sur l'onglet — fait déborder l'onglet d'un pixel dans un conteneur\n * en overflow, et cet unique pixel suffit à faire naître un ascenseur\n * vertical. */\n box-shadow: inset 0 -1px 0 var(--line);\n}\n\n/* L'onglet se pose SUR la ligne : étiré à la hauteur de la rangée (le\n * défaut flex), son trait de 2 px partage son bord bas avec elle. Inactif,\n * le trait est transparent et laisse voir la ligne ; actif, il la couvre. */\n.zv-tabs-line__tab {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n flex: none;\n border: 0;\n border-bottom: 2px solid transparent;\n background: none;\n padding: 0 2px 9px;\n font-family: var(--font-text);\n font-size: var(--app);\n font-weight: 500;\n white-space: nowrap;\n color: var(--ink-2);\n cursor: pointer;\n transition:\n color 140ms cubic-bezier(0.2, 0.7, 0.2, 1),\n border-color 140ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-tabs-line__tab:hover {\n color: var(--ink);\n}\n\n/* Le contour de base.css serait rogné par l'overflow de la rangée : il\n * rentre, comme dans la colonne latérale. */\n.zv-tabs-line__tab:focus-visible {\n outline-offset: -2px;\n}\n\n/* La grammaire d'état de .zv-seg : 500 → 600, la graisse distingue l'actif\n * avant la couleur. Le sélecteur suit l'attribut autant que la classe pour\n * que le balisage tablist correct ne coûte rien. */\n.zv-tabs-line__tab--on,\n.zv-tabs-line__tab[aria-selected='true'] {\n border-bottom-color: var(--accent);\n color: var(--accent);\n font-weight: 600;\n}\n\n/* Au survol de l'onglet actif, le trait reste : la vue où l'on se trouve ne\n * s'éteint pas sous le pointeur — même règle que l'entrée de nav courante. */\n.zv-tabs-line__tab--on:hover,\n.zv-tabs-line__tab[aria-selected='true']:hover {\n color: var(--accent);\n}\n\n/* Le compte de l'onglet — mêmes mesures que celui de la puce (.zv-seg__count,\n * forms.css) mais SANS sa marge : ici c'est la gouttière de l'onglet qui\n * espace, et réemployer la classe de la puce doublait l'écart (6 + 7 px). */\n.zv-tabs-line__count {\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n}\n\n/* ─── Défilement ────────────────────────────────────────────────────────\n * La mécanique, elle, est relevée et livrée : `min-height: 0` ET\n * `min-width: 0`. Sans ces deux-là, un enfant flex refuse de rétrécir en\n * dessous de son contenu — la zone déborde, et c'est la page entière qui part\n * en défilement horizontal. C'est la faute la plus fréquente de ce gabarit. */\n\n.zv-scroll-y {\n min-height: 0;\n min-width: 0;\n flex: 1 1 0%;\n overflow-y: auto;\n}\n\n/* ⚠️ HABILLAGE DES BARRES : trou franc, vérifié deux fois. `grep -rn scrollbar`\n * sur tout le source de Mémoire ne renvoie AUCUNE occurrence — les cinq zones\n * défilantes utilisent la barre native du système. Il n'existe donc pas de\n * valeur relevée pour la largeur, le pouce ou le rail, et ce module n'en\n * applique aucune par défaut.\n * L'habillage ci-dessous est OPT-IN et n'emploie que des mots-clés plus un\n * token existant : aucune dimension inventée. ::-webkit-scrollbar est écarté\n * pour deux raisons — il exigerait une largeur en pixels sortie de nulle part,\n * et il remplace les barres flottantes de macOS/iOS par des barres permanentes\n * qui mangent la colonne. */\n.zv-scrollbar {\n scrollbar-width: thin;\n scrollbar-color: var(--line) transparent;\n}\n\n/* ─── Mobile ────────────────────────────────────────────────────────────\n * Point de rupture unique du paquet. Les deux jeux de valeurs sont relevés\n * (Mémoire les fait basculer à 1024px) ; seul le seuil est une convention.\n * Sous ce seuil, tout ce qui se touche fait au moins --tap. */\n\n@media (max-width: 767px) {\n\n /* Le fil ENTIER est un seul lien, et il n'avait aucune hauteur de cible :\n * à 11px de texte il passe sous le minimum tactile. On déploie la cible\n * sans toucher au corps. */\n .zv-crumbs {\n display: inline-flex;\n align-items: center;\n min-height: var(--tap);\n }\n /* 224px de colonne à côté du contenu sur un écran de 375px ne laisse rien à\n * lire : la colonne cède la place au couple barre + tiroir. */\n .zv-sidebar {\n display: none;\n }\n\n .zv-topbar {\n display: flex;\n }\n\n .zv-sidelink {\n min-height: var(--tap);\n padding-block: 11px;\n font-size: var(--app-lg);\n }\n\n .zv-chrome-btn {\n width: var(--tap);\n height: var(--tap);\n }\n\n /* DÉRIVÉ, pas relevé, et c'est une conséquence de la ligne au-dessus : la\n * croix passe de 26 à 40px et son bord gauche remonte à 48px du bord droit,\n * au-delà de la réserve de 46px de la tête. 40 (bouton) + 8 (son décalage)\n * + 8 (garde) : le logo cesse de passer dessous. */\n .zv-drawer .zv-sidebar__head > :first-child {\n padding-right: calc(var(--tap) + 16px);\n }\n\n /* La feuille se cale À RAS sous la barre.\n *\n * La production pose top: 53px pour une barre de 52 (citation.tsx:101\n * contre app-shell.tsx:268), en supposant que le filet s'ajoute à la\n * hauteur. Il ne s'y ajoute pas : box-sizing: border-box le peint À\n * L'INTÉRIEUR des 52px. Ce pixel n'empile donc rien, il ouvre une couture\n * par laquelle le fond de page transparaît — et la feuille portant en\n * plus son propre filet haut, le trait apparaissait doublé.\n *\n * Corrigé ici : la feuille commence à 52, et n'a pas de filet propre —\n * celui de la barre les sépare déjà. C'est un écart ASSUMÉ au relevé, sur\n * un artefact et non sur une intention. */\n .zv-cite-panel {\n position: fixed;\n inset-inline: 0;\n top: var(--topbar-h);\n bottom: 0;\n z-index: 70;\n height: auto;\n width: auto;\n border-left: 0;\n border-top: 0;\n }\n\n .zv-cite-panel__body {\n padding: 12px;\n }\n\n /* Le menu s'ouvre sous un déclencheur qui peut être près du bord droit :\n * le plafond en vw l'empêche de sortir de l'écran et d'emporter la page. */\n .zv-menu {\n max-width: calc(100vw - 40px);\n }\n\n /* Même garde-fou pour le popover, qui partage l'ancre du menu. */\n .zv-popover {\n max-width: calc(100vw - 40px);\n }\n\n /* L'onglet souligné devient une cible tactile ; le trait descend avec le\n * padding, la géométrie ne change pas. */\n .zv-tabs-line__tab {\n min-height: var(--tap);\n padding-bottom: 12px;\n }\n\n /* ─── Barre d'onglets mobile — le dessin ──────────────────────────────\n * z-index 50 : le SOMMET des couches de page (l'étage de .zv-layer-top,\n * layout.css), et SOUS tous les recouvrements applicatifs (60 et plus) —\n * le voile doit voiler la barre, le tiroir passer devant. C'est du chrome\n * permanent, pas un enrobage : il ne monte pas dans leur échelle. */\n .zv-tabbar {\n position: fixed;\n inset-inline: 0;\n bottom: 0;\n z-index: 50;\n display: flex;\n border-top: 1px solid var(--line);\n background: var(--panel);\n /* L'encoche basse d'iOS : sans cette réserve, la rangée tombe sous la\n * barre système du téléphone. Mot-clé d'environnement, pas une mesure\n * inventée. */\n padding-bottom: env(safe-area-inset-bottom);\n }\n\n .zv-tabbar__item {\n flex: 1 1 0%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 3px;\n min-height: calc(var(--tap) + 8px);\n padding: 6px 4px;\n border: 0;\n background: none;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--muted);\n text-decoration: none;\n cursor: pointer;\n }\n\n /* Même contre-ordre que .zv-sidelink : sans lui, l'entrée vire au bleu\n * souligné dans une page qui applique le style de lien éditorial. */\n .zv-tabbar__item:hover {\n color: var(--muted);\n text-decoration: none;\n }\n\n .zv-tabbar__item:focus-visible {\n outline-offset: -2px;\n }\n\n /* aria-current=\"page\" est le marquage attendu ; la classe existe pour que\n * l'état se pose aussi sans routeur. */\n .zv-tabbar__item--on,\n .zv-tabbar__item[aria-current='page'] {\n color: var(--accent);\n }\n\n .zv-tabbar__item--on:hover,\n .zv-tabbar__item[aria-current='page']:hover {\n color: var(--accent);\n }\n\n .zv-menu__item {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n }\n\n /* Les chevrons sont des glyphes d'une dizaine de pixels : sans ce retrait,\n * la cible reste celle du caractère, impossible à viser au doigt. */\n .zv-pager__arrow {\n padding: 8px;\n }\n\n /* La cible tactile, que le padding seul ne donne pas : un chevron de dix\n * pixels entouré de huit reste sous --tap. `.zv-pager__step` la portait\n * déjà ; ses deux voisines l'attendaient. */\n .zv-pager__arrow,\n .zv-pager__rank {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: var(--tap);\n }\n\n .zv-pager__rank {\n padding-block: 8px;\n }\n\n .zv-pager--foot {\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n }\n\n .zv-pager__step {\n display: flex;\n align-items: center;\n min-height: var(--tap);\n }\n}\n\n/* ─── Lanceur d'applications, et zone de compte ─────────────────────────\n *\n * ⚠️ CE BLOC AUSSI DOIT RESTER APRÈS `.zv-menu`, pour la même raison que\n * `.zv-navdd` : les deux panneaux portent `.zv-menu` et lui reprennent son\n * display et son ancrage à spécificité égale — c'est l'ORDRE qui tranche.\n *\n * Les deux vivent À DROITE de la barre, côte à côte : le lanceur pour passer\n * d'un produit à l'autre, le compte pour savoir qui l'on est. C'est la\n * convention que tout le monde connaît, et ce n'est pas le moment d'en\n * inventer une autre.\n *\n * ─── L'ÉTAT ────────────────────────────────────────────────────────────\n * Ouverts au survol ET au focus (`:focus-within`), en CSS seul : la barre\n * reste rendue au serveur, sans une ligne à hydrater — même mécanique que\n * `.zv-navdd`. Une app qui préfère l'ouverture au CLIC (la convention des\n * lanceurs) pose `.zv-lanceur--ouvert` / `.zv-compte--ouvert` : la prop\n * `ouvert` du composant les rend. C'est la règle du dépôt — un état se dit\n * en prop, il ne se peint pas depuis l'app.\n */\n\n.zv-lanceur,\n.zv-compte {\n position: relative;\n display: inline-flex;\n flex: none;\n}\n\n/* Le déclencheur du lanceur : la cible tactile du paquet, sans cadre — le\n * chrome n'est pas une action. */\n.zv-lanceur__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tap);\n height: var(--tap);\n padding: 0;\n border: 0;\n background: none;\n color: var(--ink-2);\n cursor: pointer;\n transition: color 140ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));\n}\n\n.zv-lanceur:hover .zv-lanceur__btn,\n.zv-lanceur:focus-within .zv-lanceur__btn {\n color: var(--accent);\n}\n\n/* Le déclencheur du compte : la pastille d'initiales, cliquable. Elle garde\n * son carré — le rond ici serait un usage de réseau social, et la pastille\n * d'initiales du paquet est carrée depuis le premier jour (data.css). */\n.zv-compte__btn {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 0;\n border: 0;\n background: none;\n font-family: inherit;\n cursor: pointer;\n}\n\n/* Le nom, à côté de la pastille. Il tombe sous le point de rupture : une\n * barre de téléphone n'a pas la place d'un nom, et la pastille suffit. */\n.zv-compte__nom {\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink-2);\n white-space: nowrap;\n}\n\n/* ─── Les deux panneaux ─────────────────────────────────────────────────\n * Ancrés à DROITE et non centrés : ils vivent au bord droit de la fenêtre,\n * et un panneau centré sur son bouton en sortirait. Le pont invisible\n * (::before) couvre l'interstice, sans quoi le survol se rompt en descendant\n * vers le panneau et celui-ci se referme sous le curseur. */\n\n.zv-lanceur__panneau,\n.zv-compte__panneau {\n position: absolute;\n top: 100%;\n right: 0;\n left: auto;\n translate: none;\n padding-top: 12px;\n display: none;\n z-index: 60;\n}\n\n/* ⚠️ Le `.zv-menu` intérieur est LUI-MÊME en position absolue (il est fait\n * pour s'ancrer seul sous un déclencheur). Imbriqué dans un panneau qui\n * porte déjà l'ancrage, il se positionnait une SECONDE fois — à partir du\n * bord droit du panneau, donc vers la droite, et la grille sortait de\n * l'écran sur téléphone. Sur grand écran la place restante le masquait :\n * c'est le rendu à 390px qui l'a montré. Ici le panneau ancre, le menu ne\n * fournit plus que sa surface (filet, fond, ombre). */\n.zv-lanceur__panneau .zv-menu,\n.zv-compte__panneau .zv-menu {\n position: static;\n}\n\n.zv-lanceur__panneau::before,\n.zv-compte__panneau::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 12px;\n}\n\n.zv-lanceur:hover .zv-lanceur__panneau,\n.zv-lanceur:focus-within .zv-lanceur__panneau,\n.zv-lanceur--ouvert .zv-lanceur__panneau,\n.zv-compte:hover .zv-compte__panneau,\n.zv-compte:focus-within .zv-compte__panneau,\n.zv-compte--ouvert .zv-compte__panneau {\n display: flex;\n flex-direction: column;\n}\n\n/* ─── La grille d'applications ──────────────────────────────────────────\n * Trois par rangée : au-delà, les noms se coupent ; en deçà, le panneau\n * s'allonge en colonne et cesse de se lire d'un coup d'œil. */\n\n.zv-lanceur__apps {\n display: grid;\n grid-template-columns: repeat(3, 92px);\n gap: 4px;\n padding: 6px;\n}\n\n.zv-lanceur__app {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n padding: 14px 6px;\n text-align: center;\n text-decoration: none;\n color: var(--ink-2);\n}\n\n/* Même contre-ordre que .zv-sidelink : sans lui, la tuile vire au bleu\n * souligné dans une page qui applique le style de lien éditorial. */\n.zv-lanceur__app:hover {\n background: var(--hairline);\n color: var(--ink-2);\n text-decoration: none;\n}\n\n.zv-lanceur__app:focus-visible {\n outline-offset: -2px;\n}\n\n/* L'application COURANTE reste marquée : sans elle, on ne sait pas d'où\n * l'on part. aria-current=\"page\" est le marquage attendu ; la classe existe\n * pour que l'état se pose aussi sans routeur. */\n.zv-lanceur__app--on,\n.zv-lanceur__app[aria-current='page'] {\n background: var(--accent-soft);\n color: var(--accent);\n}\n\n/* Le glyphe du produit. 28px : la taille de .zv-icon--xl, pour qu'une tuile\n * à image et une tuile à icône tracée se posent sur la même ligne. */\n.zv-lanceur__glyphe {\n display: block;\n width: 28px;\n height: 28px;\n object-fit: contain;\n}\n\n.zv-lanceur__nom {\n font-size: var(--app-sm);\n font-weight: 500;\n line-height: 1.3;\n /* Un nom de produit trop long ne doit pas élargir la colonne : il se coupe\n * plutôt que de déformer la grille. */\n overflow-wrap: anywhere;\n}\n\n/* ─── L'en-tête du panneau de compte ────────────────────────────────────\n * Qui l'on est, avant ce qu'on peut faire. Non connecté, il porte l'invite\n * de connexion : la zone de compte existe TOUJOURS, connecté ou non — une\n * barre où l'on ne peut pas se connecter est une barre où l'on ne revient\n * pas. */\n\n.zv-compte__tete {\n display: flex;\n flex-direction: column;\n gap: 2px;\n border-bottom: 1px solid var(--hairline);\n padding: 12px 14px;\n margin-bottom: 4px;\n}\n\n.zv-compte__tete-nom {\n font-size: var(--app);\n font-weight: 600;\n color: var(--ink);\n}\n\n.zv-compte__tete-mail {\n font-family: var(--font-mono);\n font-size: var(--mono);\n color: var(--label);\n overflow-wrap: anywhere;\n}\n\n.zv-compte__panneau .zv-menu {\n min-width: 240px;\n}\n\n/* Le pied du panneau : la déconnexion, séparée de ce qui précède. Elle ne\n * prend PAS l'encre d'erreur — se déconnecter n'est pas un incident. */\n.zv-compte__sortie {\n border-top: 1px solid var(--hairline);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n@media (max-width: 767px) {\n /* Le nom tombe : la pastille suffit à dire qui l'on est, et la barre n'a\n * plus la place. */\n .zv-compte__nom {\n display: none;\n }\n\n /* Les deux panneaux sont plafonnés à l'écran, comme le menu et le\n * popover : au bord droit d'un téléphone, un panneau de 300px pousserait\n * la page en défilement horizontal. */\n .zv-lanceur__panneau .zv-menu,\n .zv-compte__panneau .zv-menu {\n max-width: calc(100vw - 32px);\n }\n\n .zv-lanceur__apps {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n}\n\n\n/* ── data.css ── */\n/* Zevra UI — données : tableaux, listes, cellules, métadonnées, citations\n *\n * CE QUE PORTE LE MODULE\n * le cadre de tableau et sa grille (.zv-table, .zv-table__head, .zv-row),\n * les trois registres de cellule, le dégroupage mobile (.zv-unwrap), la\n * liste sans colonnes, la métadonnée mono, le couple clé-valeur, la\n * pastille d'initiales, les deux utilitaires de troncature, l'étiquette de\n * catégorie, la puce de citation et la vignette de format — et, depuis le\n * 25/08/2026 (hors relevé, voir DECISIONS.md) : la rangée dépliante, la\n * touche de clavier et le code, en ligne comme en bloc.\n *\n * DÉPENDANCES\n * tokens.css — pour les seules variables qui y sont déclarées ;\n * base.css — reset (box-sizing), body, et le :focus-visible global, dont\n * ce module ne redéfinit ni la couleur ni l'épaisseur.\n * Ni le reset, ni body, ni :root ne sont reposés ici.\n *\n * CE QUE LE MODULE NE PORTE PAS\n * la pagination (.zv-pager--foot, navigation.css) : le pied de tableau et\n * le pied de pagination sont LE MÊME nœud dans la source (un seul\n * conteneur flex, compteur à gauche, flèches à droite). La version de ce\n * module a été supprimée, elle faisait doublon — les emboîter donnait deux\n * space-between imbriqués et 24px de padding vertical.\n * Aucun contrôle de formulaire non plus : [disabled] et [aria-invalid]\n * n'ont pas d'objet ici, ils vivent dans forms.css.\n *\n * ÉCHELLES — comment lire les tailles ci-dessous\n * La source emploie des demi-pixels (9,5 / 10,5 / 11,5) que le paquet ne\n * nomme pas. Deux rabattements, appliqués systématiquement :\n * — registre TEXTE : plancher --app-sm (12px). Les paliers 10,5 et 11,5\n * relevés y remontent ; c'est une décision, pas un relevé.\n * — micro-labels et glyphes (label de colonne, initiales, vignette) :\n * l'échelle mono (--mono-lg 11 / --mono 10 / --mono-sm 9), employée là\n * comme MESURE, la famille restant celle que la règle déclare.\n * ⚠️ Toutes les tailles et tous les interlettrages mono de la source ont\n * été mesurés sur Menlo (chasse fixe). Le paquet rend la mono en Space\n * Grotesk, de largeur et de hauteur d'x différentes : ces valeurs ne sont\n * donc plus des relevés, ce sont des transpositions à revérifier à l'œil.\n *\n * ÉTATS\n * Le seul état interactif de la source est le survol (.zv-row, .zv-cite).\n * Ni sélection, ni ligne active, ni tri : c'est dit à chaque endroit\n * concerné plutôt que comblé par une valeur inventée.\n */\n\n/* ─── Cadre de tableau ──────────────────────────────────────────────────\n * Le tableau scrolle DANS son cadre : sans ce confinement, une colonne trop\n * large fait scroller la page entière et le rail de navigation part avec.\n * Angle vif et sans ombre : un tableau est une surface de travail, pas une\n * carte éditoriale. Aucun rayon n'est posé — la valeur par défaut est déjà 0,\n * il n'y a rien à annuler tant qu'on ne compose pas .zv-table avec .zv-card\n * (ce que ce module déconseille : ce sont deux intentions différentes). */\n\n.zv-table {\n overflow-x: auto;\n border: 1px solid var(--line);\n background: var(--white);\n\n /* --zv-cols est un PARAMÈTRE de grille, pas un token de marque : il n'a\n * rien à faire dans tokens.css et n'y est pas. Il est déclaré ici, sur la\n * classe qui l'expose, avec une valeur de repli ; l'appelant l'écrase en\n * style inline sur ce même élément (le style inline l'emporte), et les\n * enfants en héritent. Une seule déclaration pour l'en-tête ET les lignes :\n * recopié aux deux endroits, on l'oublie à l'un des deux et les intitulés\n * se décalent des valeurs sans que rien ne casse visiblement.\n *\n * Repli à une colonne souple bornée à 0 : sans gabarit, la rangée se\n * dégrade en pile dont les cellules tronquent, au lieu de pousser le cadre.\n *\n * Gabarits relevés, à reprendre tels quels :\n * bibliothèque : 26px minmax(150px, 1fr) 78px 92px 170px 128px\n * répertoire : minmax(190px, 1.4fr) 108px 118px minmax(150px, 1fr) 74px 96px\n * Les colonnes souples sont en minmax(0|150px, …) : sans borne basse à 0 ou\n * sans min-width sur la cellule, la troncature ne se déclenche jamais. */\n --zv-cols: minmax(0, 1fr);\n}\n\n.zv-table__head {\n display: grid;\n /* Le repli est répété dans le var() : .zv-table__head et .zv-row servent\n * aussi hors de .zv-table (rangée seule dans une carte), où rien ne\n * déclarerait le paramètre. */\n grid-template-columns: var(--zv-cols, minmax(0, 1fr));\n align-items: center;\n gap: 10px;\n /* Filet plein (--line) contre le cheveu (--hairline) des lignes : c'est le\n * seul repère qui sépare les intitulés des données, il doit être plus\n * marqué qu'une séparation de lignes. */\n border-bottom: 1px solid var(--line);\n background: var(--panel);\n padding: 9px 14px;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n /* 0,14em et non les 0,2em de .zv-mono : c'est l'interlettrage que le paquet\n * emploie déjà pour les capitales denses (.zv-tag, .zv-src). À 0,2em, six\n * intitulés de colonne ne tiennent plus dans leurs pistes. */\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* En-tête d'un tableau posé DANS une carte : la carte porte déjà son filet,\n * un second trait au-dessus ferait doublon. */\n.zv-table__head--inset {\n border-bottom: 0;\n padding: 7px 10px;\n}\n\n/* ⚠️ Aucun tri : les intitulés sont inertes, sans indicateur ni gestionnaire.\n * Rien dans la source ne dit ce que trierait un clic ni comment se marque la\n * colonne active — un en-tête triable serait une décision, pas un style. */\n\n/* ─── Ligne de données ─────────────────────────────────────────────────\n * Une seule écriture de balisage pour deux mises en page : grille au-dessus\n * du seuil, carte empilée en dessous (voir .zv-unwrap et le bloc responsive).\n * Aucun zébrage nulle part dans la source — le survol est le seul repère de\n * ligne, ce qui laisse le fond blanc porter la lecture. */\n\n.zv-row {\n display: grid;\n grid-template-columns: var(--zv-cols, minmax(0, 1fr));\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--hairline);\n text-decoration: none;\n cursor: pointer;\n}\n\n/* La dernière ligne colle au cadre : son filet doublerait le trait de bordure\n * et épaissirait le bas du tableau. C'est déjà ce que fait la source côté\n * bibliothèque et côté import (`last:border-b-0`) ; seul le répertoire garde\n * le filet. Le module tranche pour le retrait — ce n'est donc pas une\n * correction contre la production, c'est l'alignement sur sa majorité. */\n.zv-row:last-child {\n border-bottom: 0;\n}\n\n.zv-row:hover {\n /* --panel sur --white : l'écart est faible (2 %), c'est le seul fond de\n * survol dont dispose un système clair sans introduire un troisième gris.\n * Ne pas y accrocher un signal qui doit être vu de loin. */\n background: var(--panel);\n /* La ligne est un lien : sans cette annulation, le survol souligne d'un\n * coup tout le contenu de la rangée. */\n text-decoration: none;\n}\n\n/* Le cadre du tableau est en overflow auto : un contour de focus posé à\n * l'extérieur de la ligne y serait rogné sur les bords. On le rentre.\n * (La couleur et l'épaisseur restent celles de base.css.) */\n.zv-row:focus-visible {\n outline-offset: -2px;\n}\n\n/* ⚠️ Ni état sélectionné, ni :focus-within, ni ligne active : la source n'en\n * contient aucun, et les inventer ferait apparaître un fond de plus dans un\n * système qui n'en a que deux (blanc, survol). */\n\n/* ─── En-tête triable ──────────────────────────────────────────────────\n * ⚠️ HORS RELEVÉ — ajouté le 25/08/2026 avec l'adaptateur TanStack Table.\n * Aucun tableau de la production n'est triable ; l'intitulé y est du texte\n * inerte. Un en-tête qui trie DOIT être un <button> — c'est la seule façon\n * qu'il soit atteignable au clavier et annoncé comme actionnable.\n *\n * `font: inherit` plutôt qu'une police redite : l'en-tête a déjà la sienne\n * (mono, capitales, interlettrée), et un <button> retomberait sur celle du\n * navigateur. Hériter ne fait entrer aucune valeur nouvelle. */\n\n.zv-sort {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n border: 0;\n background: none;\n padding: 0;\n font: inherit;\n letter-spacing: inherit;\n text-transform: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n.zv-sort:hover {\n color: var(--accent);\n}\n\n.zv-sort:focus-visible {\n /* L'en-tête colle au bord du cadre : l'anneau de base.css y serait rogné. */\n outline-offset: -2px;\n}\n\n/* La marque de tri reste TOUJOURS posée, même sur une colonne non triée\n * (elle vaut alors ↕) : si elle n'apparaissait qu'au tri, la largeur de la\n * colonne changerait au clic et toute la ligne d'en-tête danserait. */\n.zv-sort__mark {\n font-size: var(--mono-sm);\n color: var(--label);\n}\n\n.zv-sort:hover .zv-sort__mark {\n color: var(--accent);\n}\n\n/* ─── Cellules ─────────────────────────────────────────────────────────\n * Trois registres seulement : le texte courant, le nombre, et l'intitulé qui\n * remplace l'en-tête quand celui-ci disparaît. */\n\n.zv-cell {\n /* Sans min-width: 0, une cellule de grille ne descend pas sous la largeur\n * de son contenu : la troncature ne se déclenche pas et la ligne pousse le\n * tableau au lieu de couper. C'est la panne la plus fréquente ici. */\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app-sm);\n color: var(--ink-2);\n}\n\n/* Cellule d'identité (nom de document, nom d'entité) : c'est elle qu'on lit\n * en premier, elle monte d'un cran en taille, en graisse et en contraste.\n * Sans cette variante, .zv-cell rendrait le titre au même poids que ses\n * métadonnées et la ligne perdrait son point d'entrée. */\n.zv-cell--name {\n font-size: var(--app);\n font-weight: 500;\n color: var(--ink);\n}\n\n/* Nombres et dates, alignés à droite.\n *\n * ⚠️ CORRECTION DE FOND. La version précédente comptait sur --font-mono pour\n * aligner les colonnes de chiffres. Dans la source, --font-mono valait\n * `ui-monospace, Menlo` — une vraie chasse fixe. Dans CE paquet, --font-mono\n * vaut « Space Grotesk », une grotesque PROPORTIONNELLE : le 1 y est plus\n * étroit que le 8, les unités ne tombent pas les unes sous les autres et\n * l'alignement annoncé n'existe pas.\n *\n * Ce qui aligne réellement, c'est font-variant-numeric: tabular-nums : la\n * fonte sert alors ses chiffres à chasse égale, quelle que soit sa famille.\n * La famille mono est conservée ici pour le REGISTRE (valeur technique), pas\n * pour l'alignement — les deux ne sont plus confondus. */\n.zv-cell--num {\n /* Sans effet dans la grille ; utile sous 768px, où la cellule se retrouve\n * enfant d'un .zv-unwrap devenu flex et ne doit pas se laisser comprimer. */\n flex: none;\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n color: var(--muted);\n}\n\n/* Intitulé porté par la valeur : quand la ligne devient une carte, l'en-tête\n * de colonnes n'existe plus et « 12 » ne veut plus rien dire. Il disparaît\n * dès que la colonne reprend son titre (bloc responsive). */\n.zv-cell__label {\n display: none;\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* Même logique pour l'unité en clair — « 42 docs » en carte, « 42 » en\n * tableau. Reste INLINE : elle appartient à la valeur, pas à sa ligne. */\n.zv-cell__unit {\n display: none;\n}\n\n/* ─── Dégroupage ───────────────────────────────────────────────────────\n * La primitive structurelle du module : un groupe qui rassemble deux ou trois\n * cellules pour l'empilement mobile, puis s'efface (display: contents) et\n * rend ses enfants DIRECTEMENT à la grille du tableau. C'est ce qui permet\n * d'écrire le balisage une seule fois pour deux mises en page — sans lui, il\n * faudrait dupliquer la rangée et les deux copies divergeraient.\n *\n * Quatre enveloppes de ce type dans la source, sur trois écrans indépendants :\n * c'est une primitive, pas une astuce locale.\n *\n * ⚠️ Ce que .zv-unwrap NE fait PAS : la fiche client emploie le mouvement\n * inverse (`contents` sur mobile, flex au-dessus du seuil) pour défaire ses\n * colonnes en pile ordonnée. Cette classe-ci pose `contents` en base : elle\n * ne peut pas exprimer ce cas, et le prétendre serait faux. */\n\n.zv-unwrap {\n display: contents;\n}\n\n/* ─── Liste sans colonnes ──────────────────────────────────────────────\n * Même cadre que le tableau, mais rien à aligner : pas d'en-tête, pas de\n * gabarit. Sert dès que les rangées n'ont pas de colonnes comparables. */\n\n.zv-list {\n border: 1px solid var(--line);\n background: var(--white);\n}\n\n.zv-list__row {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 13px 18px;\n border-bottom: 1px solid var(--hairline);\n}\n\n.zv-list__row:last-child {\n border-bottom: 0;\n}\n\n/* Rangée « envoi en cours ».\n * ⚠️ La source porte ici un papier CHAUD (#FDFCF8) : une température, pas un\n * accent. Le paquet n'a pas de fond chaud, et le plus proche (--panel) est à\n * 2 % du blanc de la liste : ce fond est, en pratique, INVISIBLE. Il est\n * conservé comme rappel discret, mais le signal « quelque chose est en train\n * de se passer » doit être porté par la jauge (.zv-progress) ou par le\n * libellé de la rangée — surtout pas par ce fond. */\n.zv-list__row--upload {\n background: var(--panel);\n}\n\n.zv-list__main {\n /* Colonne, et non bloc : le nom surmonte sa métadonnée. Sans direction\n * explicite, les deux se suivaient sur une même ligne et la ligne de\n * liste se lisait comme une phrase. */\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 0%;\n}\n\n.zv-list__name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: var(--app);\n font-weight: 500;\n}\n\n/* ⚠️ Aucun état interactif dans la source : ni survol de rangée, ni sélection,\n * ni focus. Une liste de fichiers n'y est pas cliquable — si elle le devient,\n * c'est .zv-row qu'il faut employer, pas cette rangée-ci. */\n\n/* ─── Métadonnée mono ──────────────────────────────────────────────────\n * Second registre mono du système, en BAS DE CASSE : taille de fichier, date,\n * horodatage, compteur, latence. Ni capitales ni interlettrage — c'est\n * exactement ce qui le distingue de .zv-tag et .zv-mono (capitales,\n * 0,14–0,2em) : une date ou un nom de fichier passés en capitales espacées\n * deviennent illisibles et s'alignent mal.\n *\n * tabular-nums pour la même raison qu'en .zv-cell--num : Space Grotesk est\n * proportionnelle, et une colonne d'horodatages y danse d'une ligne à\n * l'autre sans cette propriété. */\n\n.zv-meta {\n font-family: var(--font-mono);\n font-size: var(--mono);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n color: var(--muted);\n}\n\n/* La source échelonne 10,5 / 10 / 11px. Les deux premiers se rabattent sur le\n * même palier du paquet (--mono, 10px) : la variante basse est re-fondée sur\n * --mono-sm (9px, plancher du paquet) pour garder un écart perceptible.\n * C'est une décision assumée, pas une mesure. */\n.zv-meta--sm {\n font-size: var(--mono-sm);\n}\n\n.zv-meta--md {\n font-size: var(--mono-lg);\n}\n\n/* Métadonnée de second plan (taille d'un fichier en cours d'envoi, compteur\n * d'en-tête) : elle recule d'un cran pour ne pas concurrencer le nom. */\n.zv-meta--quiet {\n color: var(--label);\n}\n\n/* Plusieurs métadonnées sur une même ligne : le retour à la ligne est autorisé\n * dès l'origine, une date et une taille ne tiennent pas côte à côte à 320px. */\n.zv-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: 10px;\n row-gap: 4px;\n}\n\n/* ─── Clé-valeur ───────────────────────────────────────────────────────\n * ⚠️ RÔLES REMIS DANS LE SENS DE LA SOURCE. La version précédente mettait la\n * mono à droite et la césure à gauche ; la fiche client fait l'inverse : la\n * CLÉ est en mono, en accent, et se coupe ; la VALEUR est en texte courant,\n * en gris de label, et ne se coupe jamais. Inverser les deux change ce que\n * l'œil balaie en premier dans une colonne de métadonnées. */\n\n.zv-kv {\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-size: var(--app-sm);\n font-weight: 500;\n color: var(--ink-2);\n}\n\n.zv-kv__row {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n}\n\n.zv-kv__key {\n /* Une clé longue doit se couper plutôt que pousser la valeur hors de la\n * carte : ici on préfère la césure à la troncature, un libellé amputé de sa\n * fin ne se devine pas. */\n min-width: 0;\n overflow-wrap: break-word;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n /* L'accent, parce que la clé est le point d'entrée de la ligne. Les alias\n * sémantiques de citation (--cite-ink & co.) n'existent pas dans ce paquet :\n * c'est --accent, directement. */\n color: var(--accent);\n}\n\n.zv-kv__val {\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n color: var(--label);\n}\n\n/* Variante en pied de carte : le filet supérieur détache le récapitulatif du\n * contenu qu'il résume. Valeurs de la fiche client (mt 12 / pt 10 / corps\n * 11px / graisse 400). Le corps 11px n'a pas de palier texte dans le paquet,\n * dont l'échelle s'arrête à --app-sm (12px) : rabattu, et dit. */\n.zv-kv--foot {\n margin-top: 12px;\n border-top: 1px solid var(--hairline);\n padding-top: 10px;\n font-size: var(--app-sm);\n font-weight: 400;\n color: var(--muted);\n}\n\n/* Variante en grille (métadonnées d'une source citée) : deux colonnes des DEUX\n * côtés du seuil — ce sont quatre valeurs courtes, les empiler ferait une\n * colonne inutilement longue. Le libellé passe au-dessus de la valeur. */\n.zv-kv--grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n column-gap: 14px;\n row-gap: 8px;\n font-size: var(--app-sm);\n}\n\n.zv-kv--grid .zv-kv__row {\n display: block;\n min-width: 0;\n overflow-wrap: break-word;\n}\n\n/* En grille, la clé redevient un label de colonne : capitales, plus petit,\n * gris de label — et non plus l'accent de la variante en ligne. */\n.zv-kv--grid .zv-kv__key {\n display: block;\n margin-bottom: 2px;\n font-size: var(--mono-sm);\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--label);\n}\n\n/* La valeur peut se couper ici : dans une case de 50 % de largeur, un\n * nowrap déborderait de sa colonne. */\n.zv-kv--grid .zv-kv__val {\n white-space: normal;\n}\n\n/* ─── Pastille d'initiales ─────────────────────────────────────────────\n * CARRÉE, jamais ronde : le rond est un usage de réseau social, l'encre et le\n * papier posent des blocs. La teinte dit la NATURE de l'entité, elle n'est pas\n * décorative — la changer, c'est changer le sens.\n *\n * 9,5px dans la source. Le paquet ne descend sous 12px que sur son échelle\n * mono : --mono-sm (9px) est employé ici comme MESURE, la pastille restant\n * rendue dans la famille de texte héritée. */\n\n.zv-avatar {\n display: flex;\n width: 26px;\n height: 26px;\n flex: none;\n align-items: center;\n justify-content: center;\n font-size: var(--mono-sm);\n font-weight: 600;\n}\n\n/* Puce d'organisation, dans le chrome : une taille en dessous pour ne pas\n * peser plus que le nom qu'elle accompagne. */\n.zv-avatar--sm {\n width: 22px;\n height: 22px;\n}\n\n.zv-avatar--accent {\n background: var(--accent);\n color: var(--white);\n}\n\n.zv-avatar--ink {\n background: var(--ink-2);\n color: var(--white);\n}\n\n/* Défaut (société, confrère) : sans aplat, pour que les pastilles pleines\n * restent l'exception et gardent leur valeur de signal. */\n.zv-avatar--soft {\n border: 1px solid var(--line);\n background: var(--hairline);\n color: var(--ink-2);\n}\n\n/* Règles d'initiales, à respecter côté appelant. Il y en a DEUX, et les\n * confondre change le rendu :\n * — .zv-avatar (26px, entité du répertoire) : préfixes « M. | Me | SARL |\n * SCI | SA » retirés, découpe sur [espace ou &], AUCUN repli ;\n * — .zv-avatar--sm (22px, organisation dans le chrome) : deux mots au plus,\n * mots-outils écartés (« cabinet », « étude », « office », « selarl »,\n * « scp », « et », « de », « du », « des », « la », « le », « les »),\n * repli sur « · » quand rien ne sort — sans ce repli, une pastille vide\n * passe pour un défaut d'affichage.\n * Prescrire la seconde pour la première change les initiales du répertoire. */\n\n/* ─── Troncature ───────────────────────────────────────────────────────\n * Deux comportements, jamais interchangeables : couper, ou revenir à la ligne. */\n\n/* Couper : réservé à ce qui reste identifiable amputé (titres, noms d'entité)\n * ET à ce qui est récupérable ailleurs. ⚠️ Ne marche QUE si le parent porte\n * min-width: 0 (ou si la colonne est en minmax(0, 1fr)) : sans cela, la boîte\n * ne descend pas sous son contenu, rien n'est coupé et la grille est poussée.\n * Livré seul, c'est un utilitaire qui ne marche pas. */\n.zv-trunc {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Revenir à la ligne : tout ce qui ne se devine pas coupé — adresses e-mail,\n * noms de fichiers, jetons, messages d'erreur. Perdre la fin d'un message\n * d'erreur, c'est perdre l'incident. */\n.zv-break {\n min-width: 0;\n overflow-wrap: break-word;\n}\n\n/* ─── Étiquette de catégorie ───────────────────────────────────────────\n * Étiquette NON porteuse d'alerte : nature d'entité, matière, mot-clé. À ne\n * pas confondre avec le badge d'état (feedback.css), qui porte une couleur\n * sémantique, ni avec .zv-tag (components.css : mono capitales, sans fond) qui est\n * un kicker éditorial. Le fond hairline suffit : une catégorie n'a pas à\n * crier.\n *\n * 10,5 puis 11px dans la source : deux micro-labels, rabattus sur --mono et\n * --mono-lg pris comme mesures (la famille reste celle du texte). */\n\n.zv-chip {\n display: inline-block;\n /* Même défense que .zv-filetype : blockifiée comme enfant flex, une\n * étiquette étirée cesse de se lire comme une étiquette. */\n align-self: start;\n background: var(--hairline);\n padding: 3px 9px;\n font-size: var(--mono);\n font-weight: 600;\n color: var(--ink-2);\n}\n\n/* Valeur technique dans une étiquette (date normalisée, référence) : la mono\n * sans capitales, comme .zv-meta. */\n.zv-chip--mono {\n font-family: var(--font-mono);\n font-variant-numeric: tabular-nums;\n}\n\n/* Variante à contour : employée quand les étiquettes sont posées sur une carte\n * blanche, où le fond hairline ne se détacherait plus assez. */\n.zv-chip--outline {\n border: 1px solid var(--line);\n background: none;\n padding: 4px 10px;\n font-size: var(--mono-lg);\n font-weight: 500;\n}\n\n.zv-chip-row {\n display: flex;\n flex-wrap: nowrap;\n gap: 6px;\n}\n\n/* ─── Citation ─────────────────────────────────────────────────────────\n * Référence cliquable, insérée dans le fil du texte ou groupée en pied de\n * réponse ; elle ouvre la feuille de source. Fond, filet et encre sont ceux\n * de l'accent (--accent-soft / --accent-line / --accent) : le paquet ne\n * déclare pas d'alias sémantique de citation, et en inventer un ferait un\n * token de plus qu'aucun test n'accepterait. */\n\n.zv-cite {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 100%;\n border: 1px solid var(--accent-line);\n background: var(--accent-soft);\n color: var(--accent);\n padding: 2px 8px;\n /* La puce est insérée au fil d'un paragraphe : sans ce recalage elle pend\n * sous la ligne de base et hache l'interligne. */\n vertical-align: 1px;\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n /* nowrap l'emporte tant qu'on est au-dessus du seuil ; overflow-wrap est\n * posé d'avance pour le passage en white-space: normal du bloc mobile. */\n overflow-wrap: break-word;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.zv-cite:hover {\n border-color: var(--accent);\n}\n\n.zv-cite-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n}\n\n/* ⚠️ Aucun état « source déjà ouverte » : la puce ne se marque pas quand sa\n * feuille est affichée, on ne sait pas laquelle on lit dès qu'il y en a\n * plusieurs. À trancher, pas à combler au hasard.\n * ⚠️ En production c'est un <span onClick> : inatteignable au clavier. Le\n * rendre focusable côté appelant (<button>) suffit à lui donner le contour de\n * base.css — c'est la correction à faire, pas un style à ajouter ici.\n * ⚠️ Recoupe .zv-src (components.css : mono capitales, fond blanc, filet\n * --accent-line, rayon 4px) : même rôle éditorial, aucune valeur commune. La\n * version applicative est remplie et à angle vif. Fusion ou dédoublement\n * assumé : à arbitrer, ce module ne tranche pas seul. */\n\n/* ─── Vignette de format ───────────────────────────────────────────────\n * ⚠️ HORS CHARTE, ET ASSUMÉ : --fmt-pdf et --fmt-docx sont le rouge Acrobat\n * et le bleu Word, pas --err ni --accent. Deux relevés indépendants les codent\n * à l'identique — c'est une intention (reconnaître le format d'un coup d'œil),\n * pas une dérive ; tokens.css les déclare et n'en autorise pas d'autre usage.\n * ⚠️ La source pose 7,5px, sous le plancher du paquet (--mono-sm, 9px).\n * Rabattu sur ce plancher : la vignette grossit d'un pixel et demi, ce qui\n * n'est PAS le relevé — à revoir à l'œil sur une rangée dense. Réservé à deux\n * ou quatre caractères en capitales, jamais à du texte.\n * L'interlettrage reste à 0,06em et non aux 0,14em du reste des capitales :\n * sur quatre caractères dans 30px de large, 0,14em déborde de la vignette.\n * Règle de choix relevée : DOCX si le type MIME contient « word », PDF sinon. */\n\n.zv-filetype {\n flex: none;\n /* ⚠️ Une vignette posée dans une colonne flex (.zv-list__main) s'étire sur\n * toute la largeur si on ne l'en empêche pas : elle cesse alors d'être une\n * vignette et se lit comme une bande de couleur. Même raison que le\n * `justify-self: start` de .zv-badge en grille — vu au rendu, pas à la\n * relecture. */\n align-self: start;\n padding: 2px 5px;\n text-align: center;\n font-family: var(--font-mono);\n font-size: var(--mono-sm);\n font-weight: 700;\n letter-spacing: 0.06em;\n color: var(--white);\n}\n\n.zv-filetype--pdf {\n background: var(--fmt-pdf);\n}\n\n.zv-filetype--docx {\n background: var(--fmt-docx);\n}\n\n/* ─── Rangée dépliante ─────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La FAQ est le pliage de la\n * vitrine ; l'applicatif n'en avait aucun — options avancées, détail d'une\n * ligne de journal. Construite sur <details> NATIF : le clavier, l'annonce\n * et l'état ouvert viennent du navigateur, aucun JavaScript.\n * Plusieurs rangées se suivent bord à bord : le filet mitoyen tombe. */\n\n.zv-fold {\n border: 1px solid var(--line);\n background: var(--white);\n}\n\n.zv-fold + .zv-fold {\n border-top: 0;\n}\n\n.zv-fold__titre {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 16px;\n font-size: var(--app);\n font-weight: 600;\n color: var(--ink);\n cursor: pointer;\n /* Le marqueur natif du <summary> tombe : le chevron du composant fait le\n * même travail sans imposer le triangle du navigateur. */\n list-style: none;\n}\n\n.zv-fold__titre::-webkit-details-marker {\n display: none;\n}\n\n.zv-fold__titre:hover {\n background: var(--panel);\n}\n\n/* Le chevron porte sa classe : cibler tout svg du titre attraperait aussi\n * une icône posée dans le libellé, qui se verrait rétrécie et retournée à\n * l'ouverture. */\n.zv-fold__chevron {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--label);\n /* `rotate` et non `transform` : la propriété indépendante, comme partout\n * où le paquet anime. Coupée par prefers-reduced-motion (base.css). */\n transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-fold[open] > .zv-fold__titre {\n border-bottom: 1px solid var(--hairline);\n}\n\n.zv-fold[open] > .zv-fold__titre .zv-fold__chevron {\n rotate: 180deg;\n}\n\n.zv-fold__corps {\n padding: 12px 16px;\n font-size: var(--app);\n line-height: 1.6;\n color: var(--muted);\n}\n\n/* ─── Touche de clavier ────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026). Une documentation d'interface cite des\n * raccourcis ; le paquet n'avait rien pour les poser. Le bas de 2 px fait\n * le relief de la touche — géométrie, pas ombre de charte, comme l'anneau\n * du radio. */\n\n.zv-kbd {\n display: inline-block;\n padding: 2px 6px;\n border: 1px solid var(--line);\n border-bottom-width: 2px;\n background: var(--panel);\n font-family: var(--font-mono);\n font-size: var(--mono-lg);\n font-weight: 600;\n white-space: nowrap;\n color: var(--ink-2);\n}\n\n/* ─── Code ─────────────────────────────────────────────────────────────\n * HORS RELEVÉ (25/08/2026, voir DECISIONS.md — c'est l'entrée qui a créé\n * --font-code). --font-mono (Space Grotesk) est PROPORTIONNELLE : elle\n * porte le registre des labels, pas l'alignement, et tabular-nums n'aligne\n * que les chiffres. Du code exige la chasse fixe entière : --font-code, la\n * pile système, sans fonte chargée.\n * Aucune coloration syntaxique : elle imposerait une palette entière que la\n * charte n'a pas. Le code se lit à l'encre sur panneau, comme un extrait. */\n\n/* En ligne, dans une phrase : `pnpm build`. Taille RELATIVE au texte qui\n * l'accueille — le même extrait sert dans un corps 15 et une note 12,5. */\n.zv-code {\n padding: 1px 5px;\n border: 1px solid var(--hairline);\n background: var(--panel);\n font-family: var(--font-code);\n font-size: 0.85em;\n overflow-wrap: break-word;\n color: var(--ink-2);\n}\n\n/* En bloc. Le cadre défile en son sein, comme le tableau : une ligne longue\n * ne pousse jamais la page en défilement horizontal. */\n.zv-codeblock {\n overflow-x: auto;\n margin: 0;\n border: 1px solid var(--line);\n background: var(--panel);\n padding: 14px 16px;\n font-family: var(--font-code);\n font-size: var(--app-sm);\n line-height: 1.7;\n tab-size: 2;\n color: var(--ink);\n}\n\n/* Le <code> intérieur n'apporte que la sémantique : sans ce rappel, il\n * retomberait sur la mono du navigateur, à une autre taille. */\n.zv-codeblock code {\n font: inherit;\n}\n\n/* ─── Sous 768px ───────────────────────────────────────────────────────\n * Point de rupture unique de la charte. Les lignes cessent d'être des lignes :\n * une grille de six colonnes à 375px produit six colonnes illisibles, ou un\n * débordement horizontal de la page. Le balisage, lui, ne change pas — c'est\n * .zv-unwrap qui rend l'inversion possible.\n *\n * ⚠️ La source coupe à 1024px (lg de Tailwind), pas à 768 : entre les deux,\n * l'application affichera des cartes là où la source affiche encore un\n * tableau. C'est la charte qui tranche, mais les gabarits de colonnes ont été\n * réglés pour 1024 et méritent une vérification à 800px. */\n\n@media (max-width: 767px) {\n /* Des intitulés de colonnes sans colonnes ne veulent rien dire : ce sont les\n * valeurs qui reprennent leur étiquette (.zv-cell__label). */\n .zv-table__head {\n display: none;\n }\n\n /* stretch, pas flex-start : c'est ce que fait la source (aucun items-* au\n * répertoire, items-stretch à l'import). Avec flex-start, tout enfant\n * direct qui n'est pas une enveloppe se rétracte à sa largeur de contenu. */\n .zv-row {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 7px;\n padding-block: 12px;\n }\n\n /* Le groupe redevient une boîte : ses deux ou trois cellules tiennent sur\n * une même ligne au lieu de s'empiler chacune sur la sienne.\n * flex-wrap: wrap n'est pas optionnel — deux des quatre enveloppes de la\n * source passent à la ligne, et sans lui la rangée taille + date + statut\n * déborde de la carte à 320px, c'est-à-dire exactement le défaut que le\n * passage en carte devait supprimer.\n * ⚠️ La gouttière est arbitrée : la source en emploie trois (6, 10, 12px).\n * 10px est la valeur médiane et la plus fréquente, pas une mesure unique. */\n .zv-unwrap {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n column-gap: 10px;\n row-gap: 4px;\n min-width: 0;\n width: 100%;\n }\n\n .zv-cell__label {\n display: inline-block;\n }\n\n /* La source ne pose PAS de marge sur l'étiquette : elle rend la CELLULE\n * flex, intitulé à gauche et valeur à droite\n * (tableau-de-bord/page.tsx:120). Sans cela, « TAILLE » vient se coller à\n * « 2,4 Mo » et les deux se lisent comme un seul mot.\n * `:has()` cible les seules cellules qui portent une étiquette — les\n * autres gardent leur flux. */\n .zv-cell:has(.zv-cell__label) {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n }\n\n /* Inline, et pas inline-block : « 42 docs » doit rester un seul mot visuel\n * collé à sa valeur. */\n .zv-cell__unit {\n display: inline;\n }\n\n .zv-list__row {\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 12px 14px;\n }\n\n /* Clé et valeur empilées : côte à côte à 320px, elles se chevauchent dès que\n * le libellé dépasse trois mots. */\n .zv-kv__row {\n flex-direction: column;\n gap: 2px;\n }\n\n /* Un libellé long ne tient pas sur une ligne à cette largeur : les\n * étiquettes passent en plusieurs rangées plutôt que de déborder. */\n .zv-chip-row {\n flex-wrap: wrap;\n }\n\n /* Une référence complète ne tient pas en une ligne sur un écran de 320px :\n * sans césure autorisée, la puce déborde de sa colonne. */\n .zv-cite {\n white-space: normal;\n }\n\n /* L'en-tête du dépliant devient une cible tactile. */\n .zv-fold__titre {\n min-height: var(--tap);\n padding-block: 10px;\n }\n\n /* 16px de gouttière interne sur 375px déjà bordés par la page : le bloc\n * se resserre, comme les bandeaux. */\n .zv-codeblock {\n padding: 12px;\n }\n}\n";
8
8
  /** Les polices ne sont PAS dans CSS_COMPLET : fonts.css appelle Google Fonts,
9
9
  * et une app qui charge déjà ses polices (next/font) doublerait le
10
10
  * téléchargement. À injecter séparément quand le document isolé n'a pas les