@zevra/ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalogue.d.ts.map +1 -1
- package/dist/catalogue.js +24 -1
- package/dist/catalogue.js.map +1 -1
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/select-menu.d.ts +25 -0
- package/dist/select-menu.d.ts.map +1 -0
- package/dist/select-menu.js +157 -0
- package/dist/select-menu.js.map +1 -0
- package/package.json +1 -1
- package/react/catalogue.tsx +37 -1
- package/react/css-inline.ts +1 -1
- package/react/index.ts +1 -0
- package/react/select-menu.tsx +250 -0
- package/src/base.css +3 -1
- package/src/components.css +54 -3
- package/src/forms.css +137 -12
- package/src/navigation.css +9 -0
- package/src/tokens.css +21 -5
- package/tokens/index.js +10 -4
package/dist/css-inline.js
CHANGED
|
@@ -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 /* 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, wordmarks 600 */\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\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 */\n --r-btn: 5px;\n --r-pill: 3px;\n --r-panel: 10px;\n --r-card: 16px;\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@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}\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 border-radius: 1px;\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 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}\n\n.zv-btn {\n background: linear-gradient(180deg, #2b2bb4 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/* 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.zv-btn[disabled],\n.zv-btn--ghost[disabled],\n.zv-btn--ink[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.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, stroke currentColor, épaisseur 2, bouts arrondis. */\n\n.zv-link-arrow {\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}\n\n.zv-link-arrow:hover {\n color: var(--accent-deep);\n}\n\n.zv-link-arrow svg {\n width: 15px;\n height: 15px;\n flex: none;\n}\n\n.zv-link-arrow--on-dark {\n color: var(--white);\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 border-radius: 4px;\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.zv-faq {\n background: var(--white);\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 border-bottom: 1px solid var(--hairline);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zv-faq__q svg {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--accent);\n}\n\n.zv-faq__a {\n padding: 14px 24px;\n font-size: 13px;\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: 2px;\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 .zv-btn--sm {\n height: 36px;\n padding: 0 14px;\n font-size: 13px;\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\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}\n\n/* Blocs centrés : hero, CTA, manifeste du prototype. */\n.zv-gutter--wide {\n padding-left: 120px;\n padding-right: 120px;\n}\n\n/* Chrome haut du prototype. */\n.zv-gutter--chrome {\n padding-left: 56px;\n padding-right: 56px;\n}\n\n/* Variante en marge, pour les blocs détachés du bord (grille produits,\n * vitrine, pied de page). */\n.zv-inset-x {\n margin-left: 72px;\n margin-right: 72px;\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/* ─── 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/* 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 stroke-linecap: round;\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/* Header fixe du prototype : voile dégradé plutôt qu'un fond plein, pour\n * que le contenu s'y fonde au défilement. */\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 background: linear-gradient(180deg, rgba(245, 246, 250, 0.92), rgba(245, 246, 250, 0));\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\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/* ─── 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-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\n/* ── surfaces.css ── */\n/* Zevra UI — surfaces : halos, sols quadrillés, voiles, bandeau manifeste\n * 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 * Toutes ces surfaces sont DÉCORATIVES : elles se posent en absolu derrière\n * le contenu, ne captent jamais le pointeur, et n'ont pas à être lues par un\n * lecteur d'écran (les poser en aria-hidden).\n *\n * Elles supposent toutes un parent `position: relative; overflow: hidden`.\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/* 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/* ─── 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. 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. Les animations d'entrée ne vivent que dans\n * le bloc @supports, si bien qu'un navigateur sans `animation-timeline`\n * affiche simplement le contenu en place plutôt qu'un écran vide.\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 pilotées par le défilement ────────────────────────────────\n * Hors de ce bloc, aucune de ces classes ne produit d'effet : l'élément\n * reste dans son état final. C'est voulu. */\n\n@supports (animation-timeline: view()) {\n /* 3 · Entrée standard de tout contenu texte et panneau.\n * Stagger de 120 ms entre les éléments d'un même groupe. */\n @keyframes zv-rise {\n from {\n opacity: 0;\n transform: translateY(32px);\n }\n }\n\n .zv-anim-rise {\n animation: zv-rise var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 10% contain 45%;\n }\n\n /* 4 · Entrée des glyphes produit dans leur scène : montée et grossissement. */\n @keyframes zv-glyph-in {\n from {\n opacity: 0;\n transform: translateY(40px) scale(0.7);\n }\n }\n\n .zv-anim-glyph-in {\n animation: zv-glyph-in var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 0% contain 45%;\n }\n\n /* 5 · La signature de la page d'accueil : les glyphes convergent vers le\n * Z depuis les quatre coins. La direction se choisit par modificateur —\n * c'est la seule entrée latérale du système, et elle est 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 .zv-anim-converge {\n animation: zv-converge-tl var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 0% contain 55%;\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 /* 6 · Badges, jetons d'entités, éléments de preuve.\n * En cascade : six vagues espacées de 6 % de défilement. */\n @keyframes zv-pop {\n from {\n opacity: 0;\n transform: scale(0.6);\n }\n }\n\n .zv-anim-pop {\n animation: zv-pop var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 30% contain 70%;\n }\n\n /* Les six vagues. Décaler la plage, et non le délai : un délai en secondes\n * n'a aucun sens sur une timeline de défilement. */\n .zv-anim-pop--2 { animation-range: contain 36% contain 76%; }\n .zv-anim-pop--3 { animation-range: contain 42% contain 82%; }\n .zv-anim-pop--4 { animation-range: contain 48% contain 88%; }\n .zv-anim-pop--5 { animation-range: contain 54% contain 94%; }\n .zv-anim-pop--6 { animation-range: contain 60% contain 100%; }\n\n /* 7 · Le filet de spectre se dessine à l'entrée de chaque en-tête de\n * section. On anime bien la largeur, et non une mise à l'échelle :\n * le dégradé doit se révéler, pas s'étirer. */\n @keyframes zv-filet {\n from {\n width: 0;\n }\n }\n\n .zv-anim-filet {\n animation: zv-filet var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 5% contain 25%;\n }\n\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, bouts et\n * jonctions arrondis, sur une grille de 24. Elles héritent de la couleur du\n * texte — c'est ce qui leur permet de fonctionner sur papier comme sur la\n * 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 stroke-linecap: round;\n stroke-linejoin: round;\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/* 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\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.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.\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}\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.zv-search:focus-within {\n border-color: var(--accent-line);\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 stroke: var(--label);\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/* ─── 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.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. */\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='round' stroke-linejoin='round'%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.zv-switch {\n position: relative;\n display: inline-block;\n width: 28px;\n height: 16px;\n flex: none;\n background: var(--accent-line);\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: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch--on {\n background: var(--accent);\n}\n\n.zv-switch__knob {\n position: absolute;\n inset-block-start: 2px;\n inset-inline-start: 2px;\n width: 12px;\n height: 12px;\n border-radius: 9999px;\n background: var(--white);\n transition: inset-inline-start 150ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n\n.zv-switch--on .zv-switch__knob {\n inset-inline-start: 14px;\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/* ─── 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 * stroke-linecap=\"round\" — et PAS de stroke-linejoin, le tracé n'a qu'un\n * angle franc et la source ne le pose pas :\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 .zv-input--bare {\n min-height: var(--tap);\n font-size: var(--app-lg);\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 .zv-search__input {\n padding-block: 12px;\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 .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\n/* ── feedback.css ── */\n/* Zevra UI — retours d'état : bandeaux, étiquettes, pastilles, absence,\n * travail en cours, curseur de flux.\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 * 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 * Rien ici n'est interactif : pas de bouton de fermeture, pas de survol, pas\n * de champ. Les états :hover / :focus-visible / [disabled] / [aria-invalid]\n * n'ont donc aucune cible dans ce module — ils appartiennent aux boutons\n * (components.css) et aux champs (forms.css). Le focus clavier global de\n * base.css suffit au bandeau qu'on rend focusable pour l'annoncer.\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é.\n * ⚠️ NON ARBITRÉ EN DA, et le module ne tranche pas à sa place : le\n * `border-radius: 0` posé ici et sur .zv-badge est la valeur relevée, pas\n * une « signature de la couche applicative ». Il entre en contradiction\n * directe avec .zv-empty__action, qui renvoie au .zv-btn de\n * components.css et donc à --r-btn (5px) : un écran vide affichera un\n * bouton arrondi au milieu de blocs à angle vif. La règle doit monter\n * dans DECISIONS.md ou tomber — elle ne peut pas rester implicite dans\n * quatre modules. */\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; /* même arbitrage non tranché que .zv-alert, voir plus haut */\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 — et\n * porte donc --r-btn, voir la contradiction signalée sur .zv-alert. */\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é — et les\n * @keyframes du paquet (motion.css) sont réservées à la vitrine, pilotées au\n * défilement. Ne pas confondre `.zv-dot` avec `.zv-anim-dot-active` de\n * motion.css : ce dernier anime un point de méthode, 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 * ─── Manques assumés : roue, squelette, infobulle ───────────────────────\n * AUCUNE MATIÈRE PREMIÈRE dans les cinq écrans relevés. Ni .zv-spinner, ni\n * .zv-skeleton, ni .zv-tooltip ne sont écrits ici : les livrer supposerait\n * d'inventer une durée, une amplitude et une géométrie que personne n'a\n * arbitrées, et un système ment moins en affichant un trou qu'une valeur\n * plausible. Rien n'appelle d'infobulle et rien n'en propose : le nom de\n * document tronqué n'a même pas d'attribut title, les barres des graphiques\n * n'affichent aucune valeur au pointeur.\n *\n * Contraintes à respecter le jour où la DA les tranche : pas de dégradé\n * spectre (réservé au filet de section, à la barre de progression et aux\n * points de méthode) — donc un squelette en --hairline, pas un balayage\n * spectral ; et une infobulle en petit panneau blanc, filet --line, ombre\n * --shadow-menu, texte --app-sm / --ink-2, qui est la seule combinaison déjà\n * présente ailleurs dans le système. */\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 /* ⚠️ 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.\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 réemploie .zv-seg, tous deux dans forms.css.\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/* ─── 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.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/* ─── 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. Ce module ne livre donc RIEN ici.\n *\n * Une rangée d'onglets se construit avec .zv-seg-group et .zv-seg\n * (forms.css), qui portent déjà la grammaire d'un état actif. Ce module a\n * d'abord défini une .zv-tabs — display flex, wrap, gouttière 6px — qui\n * était la copie exacte de .zv-seg-group : deux noms pour le MÊME nœud du\n * DOM, et c'est l'ordre de chargement qui aurait tranché. Elle est retirée.\n *\n * Un onglet SOULIGNÉ, lui, reste à faire : il suppose d'arbitrer la couleur\n * et l'épaisseur du trait actif, ce qu'aucune source ne donne.\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 /* 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 .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 .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\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.\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/* ─── 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/* ─── 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";
|
|
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 /* 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, wordmarks 600 */\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\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@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}\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 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}\n\n.zv-btn {\n background: linear-gradient(180deg, #2b2bb4 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/* 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.zv-btn[disabled],\n.zv-btn--ghost[disabled],\n.zv-btn--ink[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.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, stroke currentColor, épaisseur 2, bouts arrondis. */\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 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.zv-faq {\n background: var(--white);\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 border-bottom: 1px solid var(--hairline);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zv-faq__q svg {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--accent);\n}\n\n.zv-faq__a {\n padding: 14px 24px;\n font-size: 13px;\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 .zv-btn--sm {\n height: 36px;\n padding: 0 14px;\n font-size: 13px;\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\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}\n\n/* Blocs centrés : hero, CTA, manifeste du prototype. */\n.zv-gutter--wide {\n padding-left: 120px;\n padding-right: 120px;\n}\n\n/* Chrome haut du prototype. */\n.zv-gutter--chrome {\n padding-left: 56px;\n padding-right: 56px;\n}\n\n/* Variante en marge, pour les blocs détachés du bord (grille produits,\n * vitrine, pied de page). */\n.zv-inset-x {\n margin-left: 72px;\n margin-right: 72px;\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/* ─── 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/* 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 stroke-linecap: round;\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/* Header fixe du prototype : voile dégradé plutôt qu'un fond plein, pour\n * que le contenu s'y fonde au défilement. */\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 background: linear-gradient(180deg, rgba(245, 246, 250, 0.92), rgba(245, 246, 250, 0));\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\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/* ─── 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-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\n/* ── surfaces.css ── */\n/* Zevra UI — surfaces : halos, sols quadrillés, voiles, bandeau manifeste\n * 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 * Toutes ces surfaces sont DÉCORATIVES : elles se posent en absolu derrière\n * le contenu, ne captent jamais le pointeur, et n'ont pas à être lues par un\n * lecteur d'écran (les poser en aria-hidden).\n *\n * Elles supposent toutes un parent `position: relative; overflow: hidden`.\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/* 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/* ─── 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. 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. Les animations d'entrée ne vivent que dans\n * le bloc @supports, si bien qu'un navigateur sans `animation-timeline`\n * affiche simplement le contenu en place plutôt qu'un écran vide.\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 pilotées par le défilement ────────────────────────────────\n * Hors de ce bloc, aucune de ces classes ne produit d'effet : l'élément\n * reste dans son état final. C'est voulu. */\n\n@supports (animation-timeline: view()) {\n /* 3 · Entrée standard de tout contenu texte et panneau.\n * Stagger de 120 ms entre les éléments d'un même groupe. */\n @keyframes zv-rise {\n from {\n opacity: 0;\n transform: translateY(32px);\n }\n }\n\n .zv-anim-rise {\n animation: zv-rise var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 10% contain 45%;\n }\n\n /* 4 · Entrée des glyphes produit dans leur scène : montée et grossissement. */\n @keyframes zv-glyph-in {\n from {\n opacity: 0;\n transform: translateY(40px) scale(0.7);\n }\n }\n\n .zv-anim-glyph-in {\n animation: zv-glyph-in var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 0% contain 45%;\n }\n\n /* 5 · La signature de la page d'accueil : les glyphes convergent vers le\n * Z depuis les quatre coins. La direction se choisit par modificateur —\n * c'est la seule entrée latérale du système, et elle est 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 .zv-anim-converge {\n animation: zv-converge-tl var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 0% contain 55%;\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 /* 6 · Badges, jetons d'entités, éléments de preuve.\n * En cascade : six vagues espacées de 6 % de défilement. */\n @keyframes zv-pop {\n from {\n opacity: 0;\n transform: scale(0.6);\n }\n }\n\n .zv-anim-pop {\n animation: zv-pop var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 30% contain 70%;\n }\n\n /* Les six vagues. Décaler la plage, et non le délai : un délai en secondes\n * n'a aucun sens sur une timeline de défilement. */\n .zv-anim-pop--2 { animation-range: contain 36% contain 76%; }\n .zv-anim-pop--3 { animation-range: contain 42% contain 82%; }\n .zv-anim-pop--4 { animation-range: contain 48% contain 88%; }\n .zv-anim-pop--5 { animation-range: contain 54% contain 94%; }\n .zv-anim-pop--6 { animation-range: contain 60% contain 100%; }\n\n /* 7 · Le filet de spectre se dessine à l'entrée de chaque en-tête de\n * section. On anime bien la largeur, et non une mise à l'échelle :\n * le dégradé doit se révéler, pas s'étirer. */\n @keyframes zv-filet {\n from {\n width: 0;\n }\n }\n\n .zv-anim-filet {\n animation: zv-filet var(--zv-easing) both;\n animation-timeline: view();\n animation-range: contain 5% contain 25%;\n }\n\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, bouts et\n * jonctions arrondis, sur une grille de 24. Elles héritent de la couleur du\n * texte — c'est ce qui leur permet de fonctionner sur papier comme sur la\n * 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 stroke-linecap: round;\n stroke-linejoin: round;\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/* 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\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.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.\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/* ─── 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. */\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='round' stroke-linejoin='round'%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/* ─── 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 * stroke-linecap=\"round\" — et PAS de stroke-linejoin, le tracé n'a qu'un\n * angle franc et la source ne le pose pas :\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 .zv-input--bare {\n min-height: var(--tap);\n font-size: var(--app-lg);\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 .zv-search__input {\n padding-block: 12px;\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 .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\n/* ── feedback.css ── */\n/* Zevra UI — retours d'état : bandeaux, étiquettes, pastilles, absence,\n * travail en cours, curseur de flux.\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 * 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 * Rien ici n'est interactif : pas de bouton de fermeture, pas de survol, pas\n * de champ. Les états :hover / :focus-visible / [disabled] / [aria-invalid]\n * n'ont donc aucune cible dans ce module — ils appartiennent aux boutons\n * (components.css) et aux champs (forms.css). Le focus clavier global de\n * base.css suffit au bandeau qu'on rend focusable pour l'annoncer.\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é.\n * ⚠️ NON ARBITRÉ EN DA, et le module ne tranche pas à sa place : le\n * `border-radius: 0` posé ici et sur .zv-badge est la valeur relevée, pas\n * une « signature de la couche applicative ». Il entre en contradiction\n * directe avec .zv-empty__action, qui renvoie au .zv-btn de\n * components.css et donc à --r-btn (5px) : un écran vide affichera un\n * bouton arrondi au milieu de blocs à angle vif. La règle doit monter\n * dans DECISIONS.md ou tomber — elle ne peut pas rester implicite dans\n * quatre modules. */\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; /* même arbitrage non tranché que .zv-alert, voir plus haut */\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 — et\n * porte donc --r-btn, voir la contradiction signalée sur .zv-alert. */\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é — et les\n * @keyframes du paquet (motion.css) sont réservées à la vitrine, pilotées au\n * défilement. Ne pas confondre `.zv-dot` avec `.zv-anim-dot-active` de\n * motion.css : ce dernier anime un point de méthode, 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 * ─── Manques assumés : roue, squelette, infobulle ───────────────────────\n * AUCUNE MATIÈRE PREMIÈRE dans les cinq écrans relevés. Ni .zv-spinner, ni\n * .zv-skeleton, ni .zv-tooltip ne sont écrits ici : les livrer supposerait\n * d'inventer une durée, une amplitude et une géométrie que personne n'a\n * arbitrées, et un système ment moins en affichant un trou qu'une valeur\n * plausible. Rien n'appelle d'infobulle et rien n'en propose : le nom de\n * document tronqué n'a même pas d'attribut title, les barres des graphiques\n * n'affichent aucune valeur au pointeur.\n *\n * Contraintes à respecter le jour où la DA les tranche : pas de dégradé\n * spectre (réservé au filet de section, à la barre de progression et aux\n * points de méthode) — donc un squelette en --hairline, pas un balayage\n * spectral ; et une infobulle en petit panneau blanc, filet --line, ombre\n * --shadow-menu, texte --app-sm / --ink-2, qui est la seule combinaison déjà\n * présente ailleurs dans le système. */\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 /* ⚠️ 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.\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 réemploie .zv-seg, tous deux dans forms.css.\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/* ─── 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/* ⚠️ 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/* ─── 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. Ce module ne livre donc RIEN ici.\n *\n * Une rangée d'onglets se construit avec .zv-seg-group et .zv-seg\n * (forms.css), qui portent déjà la grammaire d'un état actif. Ce module a\n * d'abord défini une .zv-tabs — display flex, wrap, gouttière 6px — qui\n * était la copie exacte de .zv-seg-group : deux noms pour le MÊME nœud du\n * DOM, et c'est l'ordre de chargement qui aurait tranché. Elle est retirée.\n *\n * Un onglet SOULIGNÉ, lui, reste à faire : il suppose d'arbitrer la couleur\n * et l'épaisseur du trait actif, ce qu'aucune source ne donne.\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 /* 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 .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 .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\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.\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/* ─── 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/* ─── 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";
|
|
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
|