/* ==================================================================
   optifisc-tokens.css — SOURCE UNIQUE DES TOKENS (#693, étape 10).
   ------------------------------------------------------------------
   Toutes les décisions de design chiffrées du produit vivent ICI, et
   nulle part ailleurs : couleurs, typo, rythme, rayons, ombres, easing.

   POURQUOI CE FICHIER EXISTE

   Les tokens vivaient au milieu d'`optifisc.css`, entre les @font-face
   et 1 700 lignes de composants et de styles d'écran. Rien ne
   distinguait une DÉCISION (le laiton vaut #B5731E) d'une CONSÉQUENCE
   (le rail fiscal porte un liseré laiton). Une source unique se
   gouverne, se relit et se protège ; une source diffuse se duplique.

   RÈGLES, opposables en revue :

     - aucune redéclaration de ces tokens dans un CSS d'écran
       (actifs.css, budget.css, situation.css…) ;
     - aucun HEX sémantique dans le JavaScript — c'est déjà vrai,
       vérifié sur les 30 fichiers du front, commentaires retirés (#691) ;
     - aucun repli `var(--token, #hex)` (#559/L1) : ils rejouaient une
       palette d'une autre marque et faisaient fuir le contraste par la
       bande. Un token manquant doit CASSER VISIBLEMENT, pas retomber en
       douce sur une valeur étrangère ;
     - tout manifeste JSON se GÉNÈRE depuis ce fichier, jamais l'inverse.

   La garde `scripts/parite/audit-tokens.mjs` lit cette source et établit
   son écart avec le design system archivé. Elle constate — elle ne
   corrige pas : la correction est une décision de direction artistique.

   CHARGEMENT : lié AVANT `optifisc.css` dans les 23 pages. L'ordre
   importe peu pour la résolution (les custom properties se résolvent au
   calcul, pas au parse), mais il rend l'intention lisible.

   ⚠️ Ce fichier ne contient QUE des déclarations de tokens. Aucune règle
   de composant, aucun sélecteur d'écran. Si une valeur de design apparaît
   ailleurs dans le dépôt, c'est un bug — pas une exception locale.
   ================================================================== */

:root{
  /* Encre & surfaces — VALEURS DE LA DA v1.1 (#763, commit 2).
     `DESIGN_TOKENS.md:36-43` (§2.3), repris à l'identique par `DIRECTIVE_DA_01.md:27-30`
     qui dit « reprendre §2.3 intégralement ». C'est cette pièce-là qui fait foi : quand le
     handoff (`design_handoff_optifisc_ui/README.md:91`) affecte les deux filets à des rôles
     INVERSES (`#E8EEF2` = filet de carte, `#D9E2E8` = filet de contrôle), c'est DESIGN_TOKENS
     qui gagne, parce que c'est la pièce que la directive nomme. Écart visuel ΔE ≈ 3, il se
     rattrapera écran par écran — mais il est SIGNALÉ, pas arbitré en silence.

     ⚠️ CE QUE CE CHANGEMENT DE VALEURS NE DOIT PAS PERDRE. `--ink-3` avait été assombri deux
     fois pour l'accessibilité (#8B929D → #68707C au #8, puis → #606874 au #438), parce que le
     calibrage #8 ne visait que les surfaces de carte et ÉCHOUAIT sur le fond de page (4.26:1).
     Q18 a levé l'accessibilité comme barre bloquante le 30/07, mais « contrastes lisibles »
     reste recommandé parce que ça sert l'utilisateur réel. Les cibles ont donc été MESURÉES
     avant d'être posées, sur les trois fonds cibles (milk-50 / blanc / surface-100) :

       --of-ink-950 #111A21 : 17,21 · 17,59 · 16,46
       --of-ink-650 #415361 :  7,80 ·  7,97 ·  7,45
       --of-ink-550 #606F79 :  5,08 ·  5,19 ·  4,85   ← le plus exposé, il passe partout

     Le recalibrage de #438 survit donc à la refonte. S'il avait chuté, la bonne réponse aurait
     été de le dire, pas de porter la valeur du dossier sans regarder. */
  --of-ink-950:#111A21; --of-ink-650:#415361; --of-ink-550:#606F79;
  /* #763 — `--muted` A DISPARU, et c'est un geste, pas un oubli. C'était un ALIAS PUR de
     `--ink-3` (`--muted: var(--ink-3)`) : deux noms vivants pour une notion, soit #592 en
     germe. Ses 26 sites sont repiqués sur `--of-ink-550`, qui porte la valeur.

     ⚠️ Ce que le remap a failli produire ici, et qui vaut d'être écrit : renommer
     mécaniquement `--muted` ET sa valeur `var(--ink-3)` donnait
     `--of-ink-550: var(--of-ink-550)` — une AUTO-RÉFÉRENCE, qui écrase la vraie valeur et
     rend le jeton invalide. Ni le filet de parité (il ne regarde pas les couleurs), ni la
     garde des jetons déclarés (le nom EST déclaré) ne pouvaient le voir. Trouvé par la
     comparaison valeur à valeur avant/après — c'est elle le vrai critère du commit 1.

     #559/L1 reste en vigueur : les REPLIS `var(--token, #hex)` ont tous été supprimés du
     dépôt. Ils rejouaient une palette Tailwind (slate #64748b ≈ 4,0:1, #9AA0A6 ≈ 2,6:1 sur
     blanc) et faisaient fuir le contraste par la bande, y compris là où le token avait été
     recalibré. Un token manquant doit casser visiblement, pas retomber en douce sur une
     valeur d'une autre marque. */
  /* `--of-surface-200` porte la valeur que le dossier nomme `--of-milk-50` (`DESIGN_TOKENS.md:18`,
     « Canvas clair »). Le NOM du dépôt est conservé : le commit 1 a figé les noms, en changer un
     ici rouvrirait le couplage nom↔valeur que le découpage en deux commits existe pour éviter.
     Deux noms pour une notion serait #592 ; il n'y en a qu'un de déclaré. */
  --of-surface-200:#FAFDFE; --of-white:#FFFFFF; --of-surface-100:#F4F8F9;
  --of-line-300:#D9E2E8; --of-line-200:#E8EEF2;

  /* Chrome & sémantique (sourde, jamais criarde) — valeurs `DESIGN_TOKENS.md:13-15,49-52,70`.

     ⚠️ LES VALEURS CHANGENT ICI, PAS LES RÔLES — et l'écart est volontaire. Le dossier réserve
     `--of-petrol-800` aux « états actifs, hover sombre » (`DESIGN_TOKENS.md:14`) et donne l'action
     primaire à `--of-teal-650` (`:15`, « CTA principal »). Le dépôt, lui, emploie `--of-petrol-800`
     comme couleur d'action sur 168 sites (boutons, chips actifs). BASCULER LE RÔLE est un geste
     de composant, pas de token : il appartient au shell (#765). Écrit ici pour qu'on ne lise pas
     le teal dans les jetons et le pétrole à l'écran en croyant l'un des deux en retard.

     `--of-teal-100` reste à sa valeur : le dossier NE LE NOMME PAS. Le handoff parle d'un « teal
     très pâle » (`README.md:74`) sans donner de hex sous ce rôle. Une valeur qu'on ne sait pas
     établir se dit — elle ne se déduit pas d'une teinte voisine (#575).

     Recalibrages AA antérieurs, revérifiés sur les cibles : `--of-positive-700` sur son fond pâle
     passe de 4,98:1 à 4,94:1, `--of-negative-700` de 4,88:1 à 4,53:1 — au-dessus de 4,5 tous les
     deux. Blanc sur `--of-teal-650` : 5,01:1 (était 5,75:1). Aucun ne décroche. */
  --of-petrol-800:#0E5353; --of-teal-650:#0F7C7B; --of-teal-100:#E1EEF0;
  --of-positive-700:#147557; --of-positive-100:#E4F3EC;
  --of-negative-700:#B84236; --of-negative-100:#F8E7E4;

  /* ==================================================================
     `--of-petrol-750` — LE PÉTROLE QUI NE VIT QU'EN TRANSPARENCE (22/08/2026).
     ------------------------------------------------------------------
     ⚠️ CE JETON EXISTE PARCE QUE `rgba(14,85,99,α)` N'EST **PAS** `--of-petrol-800`.

       le littéral       #0E5563   14, 85, 99
       --of-petrol-800   #0E5353   14, 83, 83     ← ΔE 9,4 (CIE76)
       --of-dataviz-1    #0E5563   identique au littéral

     Il vivait en dur sur cinq sites d'`optifisc.css` et `glidepath.css`, à quatre
     dosages et pour quatre rôles sans rapport : lavis de fond de page (7 %), cerne
     de focus (14 %), filet du bandeau d'information (22 %), ombre d'une carte
     primaire (42 %).

     LES DEUX REPIQUAGES ÉVIDENTS ONT ÉTÉ ÉCARTÉS, chacun pour sa raison :

       · sur `--of-petrol-800` — la bonne NOTION, mais le pixel bouge de ΔE 9,4 sur
         quatre surfaces, dont le fond de TOUTES les pages. Ce n'est plus un
         nettoyage, c'est une décision visuelle déguisée en refactor (#575).
       · sur `--of-dataviz-1` — le pixel EXACT, mais la notion fausse : une couleur
         de SÉRIE dit « voici la première courbe ». Accrocher un cerne de focus et
         un fond de page à la série 1 d'un graphique, c'est les faire bouger le jour
         où quelqu'un retouche une dataviz — un couplage qu'on ne découvre qu'en le
         subissant.

     D'où ce troisième terme : le jeton porte la valeur telle quelle, ZÉRO pixel
     déplacé aujourd'hui, la palette cesse d'être recopiée, et la question « faut-il
     rapprocher ce pétrole de `--of-petrol-800` ? » redevient une question SÉPARÉE,
     qu'on posera quand quelqu'un décidera de la regarder.

     ⚠️ IL NE S'EMPLOIE QU'EN ALPHA (`color-mix(… X%, transparent)`). En aplat il
     ferait un second pétrole opaque à côté du 800 — soit exactement #592.
     ================================================================== */
  --of-petrol-750:#0E5563;

  /* Extrémité CLAIRE des rampes de verdict — le haut d'un dégradé de barre, jamais
     un texte ni un aplat de statut. Elles vivaient en dur dans `.drift .real.over`
     et `.src-row .fill.hi/.lo`, où elles formaient une seconde palette de verdict
     que rien n'obligeait à suivre la première. */
  --of-positive-550:#2C9E75;
  --of-negative-550:#C8584A;
  --of-neutral-state:#606F79; /* variation nulle — aligné sur --of-ink-550 (DESIGN_TOKENS.md:70) */

  /* Marine du shell (#765) — la sidebar est une surface sombre dans un produit clair.
     ------------------------------------------------------------------
     `--of-ocean-950` est le fond de marque (`DESIGN_TOKENS.md:13`, « sidebar, hero
     sombre »), `--of-ocean-975` l'encart plus profond du pied (`handoff/README.md:108`)
     et `--of-aqua-500` l'accent qui vit UNIQUEMENT sur marine (`DESIGN_TOKENS.md:16`).
     Pourquoi l'aqua et pas `--of-teal-650` sur cette surface : le teal tombe à ≈2,3:1
     sur `--of-ocean-950`, l'aqua tient 5,51:1. Un focus qu'on ne voit pas n'est pas
     un focus — la barre d'accessibilité est levée (Q18 de #697), la lisibilité réelle
     de l'utilisateur, non.
     ⚠️ La planche du moodboard porte `#29E0C1` ; le jeton du dossier porte `#29C0C1`.
     Le jeton gagne (décision du 31/07) : deux valeurs pour une notion, c'est #592. */
  --of-ocean-950:#073B3F; --of-ocean-975:#05282B; --of-aqua-500:#29C0C1;
  /* Les deux arrêts INTERMÉDIAIRES du dégradé de la bande héros (`.hero`), qui part
     de `--of-petrol-800`. Ils vivaient en dur dans `optifisc.css` : trois valeurs de
     marque dans une déclaration, dont une seule était un jeton. */
  --of-hero-degrade-mi:#0A4751; --of-hero-degrade-fin:#08363E;

  /* Encres SUR marine — nommées par RÔLE, parce qu'elles n'existent que là.
     Contrastes mesurés sur `--of-ocean-950` : 8,64 · 6,27 · 5,29. */
  --of-sur-marine:#C8DCDD;         /* libellé d'entrée au repos */
  --of-sur-marine-titre:#9FBFC1;   /* intitulé d'intention, et sous-rang du chevron */
  --of-sur-marine-faible:#8FB0B2;  /* métadonnée (« Sécurité et accès ») */
  /* REVIEW 07/08 — deux valeurs vivaient EN DUR dans le JS (cockpit.js série
     « apports seuls », patrimoine-net.js eyebrow « dont » du héros sombre), en
     violation de « aucune valeur de design redéclarée dans le JavaScript ».
     Elles entrent ici, nommées par rôle, et le JS les référence par var(). */
  --of-sur-sombre-secondaire:rgba(255,255,255,.45); /* série secondaire d'un graphe sur fond sombre */
  --of-sur-sombre-70:rgba(255,255,255,.7);          /* eyebrow/mention sur héros sombre */

  /* ==================================================================
     LES QUATRE RANGS SUR FOND SOMBRE (22/08/2026, suite de la REVIEW ci-dessus).
     ------------------------------------------------------------------
     Il MANQUAIT une famille. `--of-sur-sombre-70` était le seul rang nommé, et
     il ne couvrait qu'un rôle sur quatre : faute de repère, les héros sombres
     ont inventé leurs nuances au fil des écrans. Relevé du 22/08 sur
     `backend/public/**` : .88 · .85 · .82 · .78 · .72 · .70 · .68 · .66 · .60,
     plus .28 / .5 / .55 dans `cockpit.css` — neuf à quinze valeurs pour QUATRE
     rangs de lecture réellement distincts. C'est #592 sur la typographie de
     l'ombre : autant de valeurs plausibles, rien qui exige leur égalité.

     Les quatre rangs, et le contraste MESURÉ de chacun (blanc composité sur
     `--of-ocean-950` #073B3F, puis sur `--of-petrol-800` #0E5353 qui ouvre le
     dégradé du héros — c'est le pire des deux fonds, il fait foi) :

       fort     .88   9,88:1 · 7,24:1   la VALEUR (le chiffre du héros)
       courant  .82   8,79:1 · 6,52:1   le texte suivi, la décomposition
       -70      .70   6,84:1 · 5,23:1   la mention, le libellé de terme, l'absence
       unité    .60   5,46:1 · 4,29:1   le symbole accolé à un chiffre

     ⚠️ `--of-sur-sombre-unite` NE PORTE JAMAIS UN TEXTE QUI SE LIT SEUL. À 4,29:1
     sur le pétrole il passe sous 4,5 — il est réservé au « € » collé au montant,
     que la valeur elle-même (9,88:1, juste à côté) rend redondant. L'employer
     pour une phrase serait fabriquer un texte illisible avec un jeton.

     ⚠️ DÉCISION 11 DU 22/08 (fiche #971, réponse A). « Palier non déclaré » et
     « Total non renseigné » REMPLACENT le seul chiffre de leur carte. Ils portent
     donc `--of-sur-sombre-fort`, PAS le rang de l'absence : atténuer le seul
     texte de la carte ferait lire « écran vide » là où il faut lire « donnée
     absente » (#575). L'absence ordinaire, elle — celle qui vit à côté d'autres
     chiffres — reste au rang `-70`.

     PIXELS DÉPLACÉS PAR CE REGROUPEMENT, déclarés (ΔE composité sur ocean-950) :
       .85 → .82  ΔE 2,26   `.h-compo`, la décomposition du héros
       .72 → .70  ΔE 1,54   `.hero .of-figure-sub`
       .68 → .70  ΔE 1,55   `.gp-repere dt`
       .66 → .70  ΔE 3,10   `.hero .of-nul` — l'absence devient LÉGÈREMENT plus
                            lisible (6,26:1 → 6,84:1), dans le sens de la
                            décision 11, jamais contre elle.
     Aucun de ces quatre ne franchit un seuil d'accessibilité, dans aucun sens.
     C'est un déplacement de pixel ASSUMÉ, pas un nettoyage : le dire est la
     condition pour avoir le droit de le faire (#575). */
  --of-sur-sombre-fort:rgba(255,255,255,.88);    /* la valeur, et l'absence QUI LA REMPLACE (décision 11) */
  --of-sur-sombre-courant:rgba(255,255,255,.82); /* texte suivi, décomposition, ligne de kv */
  --of-sur-sombre-unite:rgba(255,255,255,.6);    /* symbole accolé à un chiffre — jamais un texte seul */
  /* Filet d'un héros sombre. ⚠️ VOISIN de `--of-marine-filet` (.12) sans se
     confondre : celui-ci cerne le HÉROS, l'autre le SHELL. Deux valeurs écrites
     pour deux surfaces écrites — les fondre serait arbitrer en silence, exactement
     ce que les deux filets aqua ci-dessous refusent déjà. */
  --of-sur-sombre-filet:rgba(255,255,255,.14);

  /* Verdict SUR fond sombre — et c'est une vraie famille manquante, pas un doublon.
     `--of-positive-700` (#147557) tombe à 1,60:1 sur `--of-ocean-950` : sur le héros,
     le verdict de l'écran clair est ILLISIBLE. Ces deux-là vivaient en dur dans
     `optifisc.css` (`.hero .delta.pos/.neg`) faute d'être nommés.
     Contrastes mesurés — ocean-950 · petrol-800 : 7,98:1 · 5,72:1 et 6,36:1 · 4,56:1. */
  --of-sur-sombre-positif:#7FE3BD;
  --of-sur-sombre-negatif:#F4A79C;

  /* Surfaces et filets sur marine — en ALPHA, comme `--of-fiscal-line`, pour suivre
     le fond au lieu de le figer. `--of-marine-survol` unifie les deux alphas des
     maquettes (.06 desktop, .08 rail) : deux valeurs pour une notion de survol, c'est
     #592 appliqué à l'interaction. L'écart tient en ≤1 point de luminance.
     Les DEUX filets aqua restent DEUX jetons : deux rôles écrits, deux valeurs
     écrites (DA-18 ter est normatif sur le .32 du sous-menu). Les fondre serait
     arbitrer en silence. */
  --of-marine-survol:rgba(255,255,255,.07);
  --of-marine-filet:rgba(255,255,255,.12);
  --of-marine-pastille:rgba(255,255,255,.10);
  /* Dérivés d'`--of-aqua-500` PAR CALCUL, plus par recopie : `rgba(41,192,193,…)`
     ÉTAIT la valeur du jeton figée à la main, donc un second exemplaire de la teinte —
     déplacer l'aqua laissait ces deux-là sur l'ancienne. `color-mix` avec `transparent`
     rend exactement `rgba(<base>, X)` : à jeton égal, le pixel est identique. */
  --of-aqua-filet:color-mix(in srgb, var(--of-aqua-500) 28%, transparent);      /* anneau interne du rang actif */
  --of-aqua-filet-sous:color-mix(in srgb, var(--of-aqua-500) 32%, transparent); /* filet gauche du sous-menu (DA-18 ter) */

  /* Signature laiton — EXCLUSIVEMENT fiscal.
     ------------------------------------------------------------------
     #559/L1 — DOCTRINE DU LAITON, opposable en revue. Le laiton est le seul
     élément de marque vraiment distinctif du produit (README du design system) :
     il ne vaut que s'il ne dit QU'UNE chose. Voir du doré à l'écran doit
     signifier « ceci relève du fisc », sans exception à interpréter.

     Zones autorisées, liste FERMÉE :
       .rail / .rail-*            rail fiscal
       .fiscal-preview / .fp-*    aperçu fiscal avant écriture
       .gauge10k .track.over      franchissement du seuil 10 000 €
       .ofc-casc-*                cascade brut → net (prélèvements)
       .decl-code-todo, .decl-rappel, .pn-eta-opt   codes IPP / étalement
       .de-src.is-corrige/.is-placeholder, .of-preuve.manquant
                                  dérogation P4 sanctionnée : valeur
                                  fiscalement FRAGILE (provenance, pas état)

     ✅ #763 — LA DERNIÈRE DÉROGATION EST TOMBÉE. `--c-cash` (devenu
     `--of-classe-liquidites`) était le SEUL laiton hors zone fiscale du produit, exempté
     nommément ici parce que le design system archivé l'avait posé ainsi. L'addendum du
     31/07 attribue à la poche « liquidités » un gris ardoise `#596772`
     (`DESIGN_TOKENS.md:218`). La liste fermée n'a donc plus d'exception : voir du doré à
     l'écran signifie « ceci relève du fisc », sans exception à interpréter. C'est le
     meilleur gain de ce commit, et accessoirement le contraste monte de 3,86:1 à 5,82:1.

     ⚠️ L'AUTRE OR — `--of-solar-500 #D4AF37` (`DESIGN_TOKENS.md:25`, « logo, étoiles, phare
     et illustration uniquement ; solar ne porte jamais de statut »). Il est DISTINCT du
     laiton et il n'est PAS déclaré ici : il a zéro site dans `backend/public/`, et une ligne
     morte est une invitation à l'employer sans sa garde. Il entrera avec le shell (#765), et
     il lui faudra sa garde PROPRE — pas celle du laiton. La garde du laiton
     (`DesignSystemeFabriquesTest::test_le_laiton_ne_sort_pas_du_rail_fiscal`) est écrite sur
     le préfixe `--of-fiscal-` : le solaire n'en hérite pas, ce qui est correct ici mais
     signifie qu'il ne serait gardé par RIEN. Sa garde naturelle est l'inverse de celle du
     laiton — interdit dans tout le JS du front, parce qu'une couleur posée par du JS colore
     forcément une donnée, et que « solar ne porte jamais de statut ».

     Partout ailleurs, le laiton est INTERDIT. En particulier il ne sert plus à
     dire « attention » : le troisième terme d'une échelle (à surveiller / en
     retard / partiel / moyen / à risque) est NEUTRE — encre + FORME (pastille
     en anneau plutôt qu'en disque) + le MOT. Aucune 6e teinte n'entre dans la
     palette : ni ambre, ni orange, ni « vert vif / rouge doux ». Un état
     intermédiaire qui crie est une fausse alerte (cf. #555, T7.6). */
  --of-fiscal-650:#B5731E; --of-fiscal-750:#9A5E13; --of-fiscal-100:#F6ECDC;
  /* Nuances du rail : le laiton fondu sur blanc, exprimé en alpha plutôt qu'en
     hex figé, pour que le rail suive le token si celui-ci bouge. */
  /* #763 — `--of-fiscal-line-2` (rgba(181,115,30,.34)) est SUPPRIMÉ : zéro site dans
     `backend/public/`. Le commit 1 s'était donné la règle « pas de ligne morte » et l'avait
     appliquée aux 18 paliers typographiques ; elle vaut aussi pour ce qui est déjà là. Un
     jeton déclaré qu'on n'emploie pas se lit comme disponible et s'emploie un jour sans que
     personne ait vérifié qu'il a un sens — c'est la matière morte que `audit-tokens.mjs`
     recense depuis #679. */
  /* ⚠️ LE COMMENTAIRE CI-DESSUS PROMETTAIT UNE CHOSE QUE LE CODE NE FAISAIT PAS.
     « exprimé en alpha plutôt qu'en hex figé, POUR QUE LE RAIL SUIVE LE TOKEN SI CELUI-CI
     BOUGE » : `rgba(181,115,30,.26)` est un RVB écrit à la main. Il ne suit rien — c'est
     la valeur d'aujourd'hui de `--of-fiscal-650`, recopiée. Bouger le laiton laissait le
     rail sur l'ancien, en silence. Un commentaire faux est un bug (`CLAUDE.md`) : plutôt
     que de corriger la phrase, on rend le code conforme à ce qu'elle promettait. */
  --of-fiscal-line:color-mix(in srgb, var(--of-fiscal-650) 26%, transparent);
  --of-fiscal-wash:color-mix(in srgb, var(--of-fiscal-650) 6%, transparent);

  /* L'AUTRE OR — #769. `DESIGN_TOKENS.md:25` : « logo, étoiles, phare et illustration
     uniquement ; solar ne porte JAMAIS de statut ». Il est DISTINCT du laiton et il ne
     doit jamais pouvoir se confondre avec lui.

     #763 avait refusé de le déclarer, et le motif était juste : il n'avait aucun site,
     et « une ligne morte est une invitation à l'employer sans sa garde ». Les deux
     conditions qu'il posait sont réunies ici :

       1. il a un SITE — la galerie (`/design-systeme`) instancie les deux ors côte à
          côte avec leur règle écrite, ce qui est la seule façon de rendre la
          distinction opposable ailleurs qu'en prose ;
       2. il a sa GARDE PROPRE, et elle est l'INVERSE de celle du laiton. Le laiton est
          gardé sur les sélecteurs autorisés ; le solaire est interdit dans TOUT le
          JavaScript du front (`of-ui-verif.mjs`) — parce qu'une couleur posée par du JS
          colore forcément une donnée, et que le solaire ne porte jamais de statut. La
          garde du laiton est écrite sur le préfixe `--of-fiscal-` : le solaire n'en
          hérite pas, il serait donc gardé par RIEN sans cette ligne-là. */
  --of-solar-500:#D4AF37;

  /* Catégories d'actifs — valeurs de l'addendum du 31/07 (`DESIGN_TOKENS.md:214,218`).
     `--of-classe-crypto` NE BOUGE PAS, et c'est un manque, pas un choix : les six poches de
     l'addendum ne contiennent pas la crypto, et la seule pièce du dossier qui lui attribue une
     couleur (`CHARTE_DATAVIZ.md:288`) le fait avec `#D4AF37` — un DORÉ, rendu caduc par
     l'arbitrage Q2 du 01/08 (#775). Cette pièce-là n'a pas reçu l'amendement : c'est un reliquat
     d'archive, pas une décision vivante. Valeur cible NON ÉTABLIE → on garde l'existante et on
     le dit, plutôt que de déduire une teinte voisine (#575). Question posée dans #781. */
  --of-classe-actions:#0F7C7B; --of-classe-crypto:#7A4FB0; --of-classe-liquidites:#596772;

  /* Échelle CATÉGORIELLE de dataviz — #559/L1.
     ------------------------------------------------------------------
     Le design system ne nomme que 3 couleurs de classe ; les donuts et les
     courbes empilées en réclament jusqu'à 10. Ce manque était comblé, jusqu'ici,
     par des tableaux de #hex vivant DANS le JS (situation.js, cockpit.js) — dont
     trois ors et deux bruns qui rejouaient la teinte du laiton hors de toute
     zone fiscale, plus deux valeurs périmées (#1C7C5B, #68707C : les --pos et
     --ink-3 d'avant le recalibrage #438). C'est une échelle de dataviz, pas une
     sémantique : elle ne dit RIEN de l'état d'une donnée, elle ne fait que
     distinguer des séries — la légende et la table portent le sens (P8).

     Calibrage : 10 teintes réparties sur la roue, toutes ≥ 4,86:1 sur blanc
     (le seuil d'un objet graphique est 3:1 — on est au-dessus même pour du
     texte), écart de teinte minimal 7° entre les deux seules proches (--cat-4
     saturée / --cat-10 désaturée). La bande 22–52° est VIDE : c'est celle du
     laiton, elle reste au rail fiscal. Le rouge pur est écarté : il appartient
     à --neg. (L'exception --c-cash a disparu : voir le bloc laiton ci-dessus.)

     ⛔ #763 — LA PALETTE DU DOSSIER N'ENTRE PAS À CE COMMIT, ET C'EST UNE MESURE.

     `DESIGN_TOKENS.md:76` propose dix teintes et `:223` affirme, pour les six poches de
     l'addendum, une « séparation vérifiée : les six catégories sont deux à deux distantes de
     plus de 60° de teinte ou de 15 points de clarté ». Cette affirmation est FAUSSE à la
     mesure. En appliquant la règle telle que `CHARTE_DATAVIZ.md` §6 l'écrivait ALORS
     (≥ 60° de teinte OU ≥ 15 points de clarté), sur les six poches :

       P1 #0F7C7B / P4 #3F5A91 : Δteinte 40,8° · Δclarté 13,5 pt   ÉCHEC
       P1 #0F7C7B / P5 #596772 : Δteinte 27,0° · Δclarté 12,6 pt   ÉCHEC
       P4 #3F5A91 / P5 #596772 : Δteinte 13,8° · Δclarté  1,0 pt   ÉCHEC
       P4 #3F5A91 / P6 #7A56A8 : Δteinte 46,1° · Δclarté  9,0 pt   ÉCHEC
       P5 #596772 / P6 #7A56A8 : Δteinte 59,9° · Δclarté 10,0 pt   ÉCHEC (à 0,06° près)

     5 paires en échec sur 15. La contrainte opposable ne porte cependant que sur les segments
     ADJACENTS — on pourrait donc espérer s'en sortir par l'ordre. Énumération exhaustive des
     720 permutations : 24 ordres valides en CHAÎNE OUVERTE (légende, empilement), et
     **ZÉRO en ANNEAU** — parce qu'un anneau referme le dernier segment sur le premier.

     Or `cockpit.js:538` et `:2874` prennent exactement `--of-dataviz-1..6` comme palette
     d'anneau. Porter la palette du dossier produirait donc, dans un seul fichier et sans
     qu'aucun test ne rougisse, l'anneau que sa propre charte interdit — dont la paire
     `#0F7C7B` / `#24775D` que `CHARTE_DATAVIZ.md` §6 cite NOMMÉMENT comme cas d'école
     d'illisibilité, rencontrée le 30/07.

     S'y ajoutent quatre teintes du §3 hors poches (`#496FA8`, `#24775D`, `#83528A`, `#188E83`)
     dont aucune n'est employable en présence des six — et `#188E83` tombe à 4,01:1 sur blanc,
     sous le calibrage ≥ 4,86:1 que ce fichier s'était donné. Alors que `situation.js:25-26`
     consomme les DIX : n'en déclarer que six casserait quatre séries EN SILENCE. Deux voies,
     toutes deux muettes : dans une chaîne de style, `var(--of-dataviz-7)` non déclaré rend la
     propriété invalide à la valeur calculée et le remplissage retombe sur l'héritage ; dans le
     JS, `teinte()` (`of-chart.js:88-90`) replie sur `currentColor` — visible, mais identique
     pour les quatre. Rien ne lève. (Le commentaire d'`of-chart.js:113` dit « il renvoie la
     chaîne vide » : c'est le `|| 'currentColor'` de la ligne 90 qui a raison.)

     ── TRANCHÉ LE 02/08/2026 (#781) : ON ASSOUPLIT, ET L'ORDRE DEVIENT OPPOSABLE ───────────

     Le seuil de teinte de `CHARTE_DATAVIZ.md` §6 passe de 60° à 58° ; la clarté reste à
     15 points. Motif, en une phrase : à 60° il n'existe AUCUN ordre d'anneau valide sur 720,
     à 58° il en existe 24, soit deux anneaux distincts. Le chiffre ne vit plus dans un
     document mais dans `backend/tests/Feature/GardeSeparationDatavizTest.php`, qui refait
     la mesure — c'est faute d'être exécutée que la phrase « séparation vérifiée » a survécu.

     CE QUI SE DURCIT EN ÉCHANGE, et qui concerne DIRECTEMENT les trois lignes ci-dessous :

       1. l'ORDRE DE DÉCLARATION est l'ordre de l'anneau. `teintesCategorielles()` rend
          `--of-dataviz-1..n` dans l'ordre et ne permute rien ; le cycle 1→2→…→6→1, bouclage
          compris, est gardé par test. PERMUTER DEUX VALEURS CI-DESSOUS FAIT ROUGIR — même
          si les deux teintes sont belles, même si « le vert irait mieux avant le bleu » ;
       2. l'adjacence P4 `#3F5A91` / P5 `#596772` (13,8° / 1,0 pt) reste interdite NOMMÉMENT ;
       3. `#188E83` (4,01:1) reste hors palette ;
       4. la palette déclare DIX jetons. Pas six.

     ⚠️ CE VERROU NE COUVRE QUE LES SITES POSITIONNELS. `ovTypeColor` (`cockpit.js:1769`) lie
     la couleur à une CLÉ métier, pas à un rang : l'ordre du donut vient alors de la donnée, et
     n'importe quelle paire peut devenir voisine. Mesuré : 15 paires en échec sur 36, LE MÊME
     COMPTE À 58° QU'À 60° — le seuil n'y peut rien. C'est la branche 2 de la règle
     (`CHARTE_DATAVIZ.md` §6) : la couleur n'y est pas un canal porteur, la légende l'est. Le
     chiffre est gardé contre l'aggravation, et le correctif — re-mapper les 9 clés en jeu
     TOUTES-PAIRES — est en note d'écart n° 27.

     L'échelle ci-dessous est MAINTENUE, et ce n'est pas un statu quo par défaut : elle compte
     2 paires en échec sur 15 (1↔3 et 2↔4, NON adjacentes) contre 5 pour celle du dossier, et
     son anneau servi est conforme MÊME À 60°. Porter les six poches du dossier reste possible
     — l'assouplissement était là pour ça — mais c'est un geste à part : il réassigne la
     couleur de chaque série (`cockpit.js:538`, `:1771-1773`, `situation.js:25-26`) et il
     impose l'un des deux seuls ordres valides, `P1→P2→P4→P3→P5→P6` ou `P1→P3→P4→P2→P5→P6`,
     dont AUCUN n'est l'ordre naturel des poches. */
  --of-dataviz-1:#0E5563;  --of-dataviz-2:#7A4FB0;  --of-dataviz-3:#1A7355;  --of-dataviz-4:#3E6E9C;
  --of-dataviz-5:#A2506B;  --of-dataviz-6:#5F7A34;  --of-dataviz-7:#8A4A85;  --of-dataviz-8:#4A5490;
  --of-dataviz-9:#157B70;  --of-dataviz-10:#565E6A;

  /* Typo — LES TROIS FAMILLES DU DOSSIER SONT SERVIES depuis le 03/08 (#764).
     Cible atteinte : `CHARTE_TYPO.md:19-23` — Manrope (interface) / Newsreader (titres) /
     IBM Plex Mono (données). Les cinq `.woff2` vivent dans `backend/public/fonts/`, déclarés
     en `@font-face` dans `optifisc.css`, aucune requête externe (CSP `default-src 'self'`).

     CE QUI BLOQUAIT, et qui est levé : le commentaire précédent refusait de porter les noms
     tant qu'aucun fichier n'était au dépôt — porter un nom sans fichier sert la police de
     REPLI, donc Georgia sur les titres et `ui-monospace` sur les 143 sites de données. Le
     raisonnement était juste ; il attendait les fichiers, ils sont là.

     Le budget ne se devine plus : `GardePoidsPolicesTest` SOMME les octets servis. Les cinq
     nouvelles pèsent 128 136 o mesurés. Le vieux plafond « ~180 Ko » vivait dans dix documents
     et dans zéro mesure — il avait été franchi de 65 Ko sans que rien ne rougisse (#592, #572).

     ⚠️ Inter et JetBrains Mono restent en SECOND RANG, pas par prudence mais pour une raison
     précise : les sous-ensembles sont latins, et un caractère hors sous-ensemble doit retomber
     sur une police servie plutôt que sur une police système, qui ne serait pas la même d'une
     machine à l'autre. Elles sortiront quand la dernière vague d'écrans sera passée.

     Les 18 paliers typographiques du dossier ne sont toujours pas déclarés : ils n'ont aucun
     antécédent (207 `font-size:` en dur, aucun ne passe par un jeton), donc aucun site à
     repiquer. Ils entrent avec #768. */
  --of-font-ui:'Manrope','Manrope Repli','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  /* #763 déclarait ce jeton SANS l'employer, faute de serif servie. Newsreader est là : c'est
     la police des titres du dossier, et le passage d'une grotesque à une serif est le plus
     gros écart de caractère entre le rendu servi et les 29 maquettes. */
  --of-font-display:'Newsreader','Newsreader Repli',Georgia,'Times New Roman',serif;
  --of-font-data:'IBM Plex Mono','Plex Mono Repli','JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;

  /* Rythme —
     RAYONS : valeurs de `DESIGN_TOKENS.md:150-153` (« bouton/champ 10 · nested card 14 ·
     carte 18 »). C'est la réouverture annoncée de E5 (#697 §4) : E5 avait tranché « entériner
     l'app » POUR L'EXTRACTION du lot A de #693, en portant sa propre clause — « laisser la
     v1.1 rouvrir la question, l'authority va de toute façon redéfinir les formes ». #763 EST
     cette réouverture. La v1.1 est plus arrondie que l'app, qui l'était déjà plus que
     l'archive (6/10/14 → 8/12/16 → 10/14/18). */
  --of-radius-sm:10px; --of-radius-md:14px; --of-radius-lg:18px;

  /* #806 — DEUX PALIERS DE PLUS, ET TROIS QUI N'ENTRENT PAS. Lire les motifs :
     ils sont la moitié utile de ce bloc.
     ------------------------------------------------------------------
     Le constat de #806 : les 29 maquettes emploient SEPT paliers de rayon,
     mesurés — 10px (606) · 999px (564) · 18px (343) · 8px (321) · 12px (277) ·
     14px (159) · 9px (155) — et la source n'en déclarait que trois. Les quatre
     manquants s'écrivaient donc EN DUR, la pilule 30 fois dans `optifisc.css`
     à elle seule. Une source unique qui couvre 3 formes sur 7 est un début de
     source, pas une source.

     ⚠️ `--of-radius-xs` VAUT 6, PAS 8 — ET C'EST L'ORDRE DE VÉRITÉ QUI TRANCHE.
     #806 proposait `--of-radius-xs:8px`, mesuré sur les maquettes (321 sites).
     Mais `DESIGN_TOKENS.md` §6 écrit noir sur blanc « xs 6 ; sm 10 ; md 14 ;
     lg 18 ; xl 24 ; pill 999 », et `authority/TRUTH_ORDER.md` §1 range la Design
     Authority (rang 5) AU-DESSUS des maquettes validées (rang 6). Porter 8 sous
     le nom `xs` aurait donné DEUX valeurs vivantes pour un même nom d'échelle,
     l'une dans le dossier, l'autre dans le code — soit #592 sur l'échelle des
     formes. L'écart 6/8 est SIGNALÉ dans #806, il n'est pas arbitré ici.

     ✅ `--of-radius-bloc:12px` EST DÉCLARÉ DEPUIS LE 03/08, et il l'est parce
     qu'il a enfin des sites. Le refus initial n'était pas un désaccord sur la
     valeur : un jeton sans site est un point d'entrée mort (Q2 de #697), il se
     lit comme disponible et s'emploie un jour sans que personne ait vérifié
     qu'il a un sens. La condition écrite ici était « il entrera avec le premier
     écran qui pose vraiment un bloc à 12 » — c'est `budget.css`, qui en pose
     CINQ (tuile de KPI, ligne de mouvement, enveloppe, abonnement, et la
     bannière d'apprentissage qui écrivait 8px sans motif). Ils le consomment
     tous les cinq dans le même geste ; le jeton n'est jamais entré seul.
     ⚠️ `--of-radius-bloc` n'est PAS le rayon d'une sous-zone de carte : celle-ci
     reste à 14 (`--of-radius-md`), ce que `DESIGN_TOKENS.md` §6 valide (« nested
     card : au moins 4 px de moins que son parent » — 18 − 4 = 14). Le bloc, lui,
     est une surface de PLAIN-PIED (une ligne de liste, un bandeau d'état), pas
     une carte imbriquée. Les 4 littéraux restants de `situation.css` relèvent de
     sa feuille, pas d'ici.

     ⛔ `--of-radius-pastille:9px` non plus, et ce refus-là a une histoire courte
     qui vaut d'être écrite. #794 le demande pour la PASTILLE D'ICÔNE d'en-tête de
     carte (30×30, rayon 9, fond `--of-surface-100`, icône 17 px en teal), et pose
     la bonne condition : elle entre dans la galerie `/design-systeme` AVANT tout
     écran métier (Gate B de #693). Mais l'icône qu'elle porterait n'a pas de
     vocabulaire appelable : le catalogue du shell vit dans `optifisc-shared.js`
     sans être exporté, et `OF.ui.signe` (#795) est un SIGNE — sa règle n° 3 est
     « un signe par écran, un seul point d'ancrage », soit l'inverse d'une pastille
     répétée sur chaque carte de rang 1. Recopier un tracé ici créerait un
     TROISIÈME dialecte d'icône, ce que `of-ui-verif.mjs` interdit déjà pour les
     signes. Le rayon entrera avec la pastille, et la pastille avec son icône.

     ⛔ `--of-radius-xl:24px` non plus, pour la même raison : zéro site.

     Les rayons SOUS l'échelle (2, 3, 4px) restent littéraux et le resteront :
     ce sont des pastilles de série de 8 à 14 px de côté, pas des paliers de
     l'échelle des surfaces. Les maquettes les écrivent aussi en littéral
     (3px ×20, 2px ×15). Le dire ici évite qu'on les « corrige » un jour vers un
     palier qui les rendrait ronds. */
  --of-radius-xs:6px;        /* `DESIGN_TOKENS.md` §6 « xs 6 » — puce, mini-badge */
  --of-radius-bloc:12px;     /* bloc de plain-pied : ligne de liste, tuile, bandeau d'état */
  --of-radius-pilule:999px;  /* `DESIGN_TOKENS.md` §6 « pill 999 » — badge, compteur, jauge */

  /* ⚠️ REMIS ICI le 03/08 — ces deux jetons avaient atterri DANS le bloc
     `@media (max-width:560px)`, hors de tout sélecteur. Une déclaration qui ne
     vit pas dans un sélecteur n'est pas appliquée : les DEUX ombres du produit
     étaient mortes sur les 22 écrans, à toutes les largeurs, sans qu'aucune
     page n'ait l'air cassée — une carte sans ombre reste une carte.
     C'est la garde « la galerie rend TOUS les jetons de la source » qui l'a vu,
     et elle seule : la suite PHP était verte, le banc de parité aussi (il
     compare des chiffres, jamais une apparence). La galerie est l'instance
     opposable — ce qu'elle ne montre pas n'est gardé par personne. */
  /* ==================================================================
     `--of-shadow-encre` — L'ENCRE DES OMBRES, NOMMÉE AU LIEU D'ÊTRE RECOPIÉE.
     ------------------------------------------------------------------
     `rgba(20,25,31,α)` était écrit à la main sur SIX sites : les deux ombres
     ci-dessous, le voile de l'iroir (`.of-drawer-backdrop`, 38 %), le filet de la
     galerie (10 %) et l'ombre de l'infobulle de courbe. C'est une teinte de la
     palette recopiée — le défaut exact que le laiton et l'aqua ont corrigé.

     ⚠️ ELLE N'EST PAS `--of-ink-950` (#111A21), et on ne l'y aligne PAS. Écart
     mesuré : ΔE 1,75. L'encre des ombres a dérivé de l'encre des textes il y a
     longtemps ; les fondre changerait des pixels sous couvert de nettoyage (#575).
     Ce commentaire a longtemps SIGNALÉ l'écart sans pouvoir rien en faire — la
     nommer est la troisième voie : zéro pixel déplacé, et la valeur cesse d'avoir
     six exemplaires vivants qui peuvent diverger un par un.
     ================================================================== */
  --of-shadow-encre:#14191F;
  --of-shadow-1:0 1px 2px color-mix(in srgb, var(--of-shadow-encre) 4%, transparent), 0 8px 24px -14px color-mix(in srgb, var(--of-shadow-encre) 22%, transparent);
  --of-shadow-2:0 2px 4px color-mix(in srgb, var(--of-shadow-encre) 5%, transparent), 0 24px 48px -20px color-mix(in srgb, var(--of-shadow-encre) 30%, transparent);
  /* ✅ 25/08/2026 (#1010, Q14 de #987) — LA DETTE DES DEUX ÉLÉVATIONS EST SOLDÉE.
     `.evo-tip` portait un `0 4px 14px` qui lui était propre ; il a été remplacé
     par `--of-shadow-1`, que portaient déjà `.ofc-tip` et `.gp-tip`. Les trois
     infobulles de graphique partagent désormais une seule élévation et un seul
     rayon — la grammaire est écrite UNE fois, dans `optifisc.css`, au-dessus de
     `.evo-tip`, avec le test de fonction qui dit qui en fait partie.
     Ce commentaire a SIGNALÉ l'écart pendant trois jours sans pouvoir le
     réduire : une exception écrite est une exception qui reste. */
  --of-gap:18px; --of-pad:20px;

  /* ==================================================================
     LE PAPIER N'EST PAS L'ÉCRAN (22/08/2026 — décision 12 de la fiche #971).
     ------------------------------------------------------------------
     Quatre valeurs vivaient en dur dans les trois blocs `@media print` du front,
     et elles n'ont PAS de jeton d'écran équivalent — ce n'est pas un oubli, c'est
     un changement de support :

       · les encres d'écran portent une teinte pétrole (`--of-ink-950` #111A21).
         Une laser noir et blanc la tramerait ; sur un document qu'on range, le
         texte se pose en noir plein.
       · les filets d'écran sont calibrés pour un écran rétroéclairé.
         `--of-line-300` (#D9E2E8) donne **1,31:1 sur papier blanc** : à 1 px, il
         est quasi invisible une fois imprimé.

     ⚠️ LA CORRECTION DU 22/08, ET POURQUOI ELLE COMPTE. Le bloc `@media print`
     général d'`optifisc.css` posait la bordure de carte à `--of-line-300` — donc
     à 1,31:1 — alors que le commentaire juste au-dessus dit noir sur blanc à quoi
     elle sert : « une carte ne se coupe pas au milieu, c'est la première chose
     qu'on voit d'un document mal imprimé ». Une bordure invisible ne remplit pas
     ce rôle : le commentaire décrivait une intention que le pixel ne tenait pas.
     Elle rejoint le `#ccc` (1,61:1) que les DEUX autres blocs d'impression
     employaient déjà — on adopte la valeur majoritaire du dépôt, on n'en invente
     pas une troisième.

     ⚠️ DÉCISION 12 = A : LES DEUX GRIS RESTENT DEUX GRIS. Le clair cerne les
     cartes — c'est de la DÉCORATION, ça doit s'effacer. Le foncé (2,85:1) souligne
     les ABSENCES — c'est de l'INFORMATION, et c'est même la seule que le lecteur
     de la feuille ne peut aller vérifier nulle part ailleurs (#804). Les fondre
     effacerait un signal posé exprès.
     ================================================================== */
  --of-print-encre:#000000;         /* texte d'un document imprimé — noir plein, pas l'encre d'écran */
  --of-print-encre-faible:#555555;  /* URL dépliée derrière un lien (3,7:1) */
  --of-print-filet-decor:#CCCCCC;   /* bordure de carte — décoration, 1,61:1 */
  --of-print-filet-absence:#999999; /* filet d'un bloc d'absence — information, 2,85:1 */

  /* ==================================================================
     #806 — L'ANNEAU REMPLACE LA BORDURE. C'est une grammaire, pas un goût.
     ------------------------------------------------------------------
     Balayage des 29 maquettes : elles ne dessinent AUCUNE `border`. Toute
     surface y est cernée par un anneau intérieur en `box-shadow` — mesuré :

       inset 0 0 0 1px #E8EEF2 (305)  la carte          → --of-line-200
       inset 0 0 0 1px #D9E2E8 (217)  le contrôle       → --of-line-300
       inset 0 0 0 1px #0F7C7B (168)  contrôle actif    → --of-teal-650
       inset 0 0 0 2px #0F7C7B  (23)  sélection         → --of-teal-650

     LA RAISON TECHNIQUE, et elle est décisive : une `border` occupe le box
     model. Passer de 1 px à 2 px pour marquer la sélection décale le contenu
     d'un pixel — donc la production ne peut PAS reproduire le motif de
     sélection des maquettes sans compenser au `padding`, ce que personne ne
     fait. Un anneau `inset` ne décale rien : il se pose DANS la matière.

     ⚠️ UN ANNEAU DE SÉLECTION N'EST PAS UN ANNEAU DE FOCUS. Le focus clavier
     reste l'`outline` de `optifisc.css` (filet universel `:where(…)`). Les
     fondre ferait perdre le repère « où suis-je au clavier » sur un écran où
     une carte est par ailleurs sélectionnée. L'accessibilité n'est plus une
     barre bloquante (Q18 de #697), la lisibilité réelle de l'utilisateur, si.

     ⚠️ RETIRER UNE BORDURE DE 1 px SANS COMPENSER change la boîte de 2 px en
     hauteur ET en largeur. Sur une surface étirée par sa grille (le cas de
     `.card`), l'extérieur ne bouge pas et seul le contenu gagne 2 px ; sur une
     surface dimensionnée par son contenu (un bouton, une pilule), la boîte
     RÉTRÉCIT. C'est pourquoi les contrôles à fond plein gardent leur bordure
     ici : les convertir demande une compensation de `padding` contrôle par
     contrôle, et c'est le geste de leur écran, pas celui de la source.
     ================================================================== */
  --of-anneau:inset 0 0 0 1px var(--of-line-300);            /* contrôle au repos */
  --of-anneau-actif:inset 0 0 0 1px var(--of-teal-650);      /* survolé ou actif  */
  --of-anneau-selection:inset 0 0 0 2px var(--of-teal-650);  /* sélectionné       */
  /* LA CARTE EN UNE SEULE DÉCLARATION — élévation et cerne ensemble, comme les
     178 sites de la maquette. Deux propriétés pour une notion de surface, c'est
     ce qui a laissé `.card` vivre à DEUX endroits d'`optifisc.css` (§Cartes et
     §L2), chacun redéclarant `border` et `box-shadow` sans que rien n'exige leur
     égalité (#592). L'ombre est celle de la maquette (`0 1px 2px rgba(17,26,33,.05)`),
     l'anneau porte `--of-line-200` (« filets faibles »), pas `--of-line-300`. */
  /* `rgba(17,26,33,.05)` est `--of-ink-950` recopié à la main — même défaut que le
     laiton et l'aqua. ⚠️ `--of-shadow-1` et `--of-shadow-2`, juste au-dessus, emploient
     `rgba(20,25,31,…)` : ce n'est PAS `--of-ink-950` (17,26,33) mais une valeur voisine
     qui a dérivé. On ne les aligne pas ici — ce serait changer des pixels sous couvert de
     nettoyage (#575). L'écart est signalé, pas corrigé. */
  --of-surface-carte:0 1px 2px color-mix(in srgb, var(--of-ink-950) 5%, transparent), inset 0 0 0 1px var(--of-line-200);

  /* GÉOMÉTRIE DU SHELL (#765) — la sidebar remplace la topbar.
     ------------------------------------------------------------------
     ⚠️ `--of-maxw` et `--of-maxw-cockpit` sont RETIRÉS ici. Leur motif (#626) était
     littéralement : « la topbar consomme la MÊME largeur bornée que le contenu ».
     Cette logique tombe avec la topbar — une sidebar est un frère du contenu, elle
     ne partage plus son gabarit. Le cockpit perd du même coup son gabarit distinct :
     DA-14 fixe la même largeur utile pour tous les écrans, et il est postérieur à
     `DESIGN_TOKENS.md:175-176` (qui donnait 1680 au cockpit).

     `--of-contenu-max` est un PLAFOND, pas une cible. La largeur effective vaut
     min(plafond, canevas − nav − 2×gouttière). Arithmétique de DA-14, vérifiée :

     ⚠️ CE TABLEAU EST MESURÉ DANS LE NAVIGATEUR, pas calculé de tête (03/08/2026).
     La version calculée qu'il remplace se trompait à 1440 : elle y appliquait une
     gouttière de 32 alors que le palier 1180-1599 la redéfinit à 24 (voir plus bas).
     Elle annonçait donc 1160 là où le rendu donne 1176. Un commentaire faux est un
     bug — et celui-ci ne se voyait pas, puisqu'il tombait juste sur la valeur de
     DA-14. Mesure : `.wrap` et son padding, sur /cockpit, aux quatre largeurs.

       largeur   nav   gouttière   .wrap    contenu réel (hors padding)
        1920     248      32       1608 ←plafond atteint      1544
        1440     216      24       1224                       1176
        1024      80      20        944                        904
         412       0      16        412                        380

     ⚠️ DA-14 annonce 1608 / 1160 / 976 / 380. Deux de ces quatre chiffres ne
     correspondent à AUCUNE des deux colonnes mesurées, et le motif est plus
     intéressant qu'une erreur de calcul : DA-14 n'emploie pas la même définition
     de « contenu utile » d'une ligne à l'autre. À 412 il donne 380, soit le contenu
     hors padding ; à 1920 il donne 1608, soit la boîte padding compris. Claude Design
     a tranché **904** pour la tablette le 03/08 — c'est la définition « hors padding ».
     Appliquée aux quatre lignes, elle donne 1544 / 1176 / 904 / 380, et c'est ce que
     le code livre. Les deux lignes hautes de DA-14 restent à corriger côté dossier
     (demande B du retour de chantier) ; le code, lui, n'invente rien : il mesure.

     Un plafond non atteint ne ment pas : c'est exactement ce que DA-14 demande quand
     il écrit « un plafond de contenu (max-width sur le conteneur principal) ». */
  --of-contenu-max:1608px;
  --of-shell-nav-l:248px;          /* 216 @1180-1599 · 80 @561-1179 · 0 @≤560 */
  --of-shell-nav-l-deployee:216px; /* largeur du rail une fois déployé (1024) */
  --of-shell-rang-h:44px;          /* = la cible tactile, identique aux 4 largeurs */
  --of-shell-bas-h:68px;           /* nav basse 412 : 52 + 6 + 10 */
  --of-shell-gouttiere:32px;       /* redéfini par palier juste en dessous */
  /* ⚠️ PAS de `--of-rail-contextuel` ici. DA-14 le déduit à 336 px (800 + 24 + 336
     = 1160 à 1440), mais le rail du cockpit vaut 400 px dans le code et TROIS
     règles de repli lui sont adossées (#626, lot D). Le changer appartient à la
     migration du cockpit, pas au shell. Déclarer 336 sans l'employer donnerait
     une valeur morte dans la source unique — une invitation à la divergence. */

  /* Paliers typographiques CONSOMMÉS PAR LE SHELL (`CHARTE_TYPO.md` §2.2).
     Les 21 paliers du dossier n'entrent pas ici : on ne déclare que ce qu'on emploie,
     une ligne morte dans une source unique est une invitation à la divergence.
     ⚠️ `--of-eyebrow-s` et `--of-micro` sont dans le §2 de la charte et ABSENTS de son
     §4 : c'est un manque du document, pas une invention de ce commit.
     Le rang actif ne prend PAS un second jeton : `font:var(--of-nav)` puis
     `font-weight:650` — le palier « nav » est écrit « 550 au repos, 650 actif ». */
  --of-nav:550 14px/20px var(--of-font-ui);
  --of-label:650 13px/18px var(--of-font-ui);
  --of-caption:550 12px/17px var(--of-font-ui);
  --of-body-s:450 14px/20px var(--of-font-ui);
  --of-eyebrow-s:550 10px/14px var(--of-font-data); /* + letter-spacing .10em, capitales */
  --of-micro:650 10px/14px var(--of-font-ui);       /* compteur en pastille */

  /* — LES TROIS PALIERS ÉDITORIAUX (#793, #794) —
     ------------------------------------------------------------------
     #764 avait déjà fait passer le `h1` en Newsreader 42/48, mais EN DUR dans
     `optifisc.css` : la valeur vivait donc hors de la source unique, et son
     repli responsive tenait en une seule rupture à 820 px, inventée sur place.
     Elle vit ici, avec ses quatre paliers MESURÉS sur les quatre maquettes du
     cockpit (29 pouces / 15 pouces / tablette / Pixel 11) — voir les blocs de
     média plus bas, qui redéfinissent le jeton comme ils redéfinissent la
     géométrie du shell.

     `--of-card-title` : #794 mesure 189 titres de section dans les maquettes,
     dont 185 en Newsreader de 19 à 24 px (94 à 20 px, 64 à 22 px). La production
     servait `14.5px/600` en Manrope sur 140 points de montage — 140 étiquettes du
     poids du corps de texte, d'où la lecture « tableau de bord d'administration »
     au lieu de « document ».

     ⚠️ GRAISSE 400, PAS 600, et c'est contre-intuitif : un serif de 20 px en 400
     pèse déjà plus qu'une grotesque de 14,5 px en 600. Mettre 600 produit un titre
     LOURD, pas un titre premier. Seul le `h1` est en 500 (décision du 31/07,
     documentée dans `optifisc.css` §Titres).

     ⚠️ DEUX PALIERS DE CARTE, PAS TROIS. `BRAND_LANGUAGE.md` §1 interdit un
     troisième niveau de taille dans une même zone. `--of-card-title` est le
     défaut ; `--of-card-title-l` est réservé aux cartes de premier rang que la
     maquette de chaque écran nomme. */
  --of-display-l:500 42px/48px var(--of-font-display); /* + letter-spacing -.2px */
  --of-card-title:400 20px/26px var(--of-font-display);
  --of-card-title-l:400 22px/28px var(--of-font-display);
}

/* — LES QUATRE PALIERS DU SHELL (#765, DA-18 bis « où il s'applique ») —
   Les VALEURS vivent ici, avec le jeton qu'elles redéfinissent ; les RÈGLES qui
   les consomment vivent dans `optifisc.css`. Redéclarer un jeton dans une
   feuille d'écran est précisément ce que `tokens:audit` refuse — et le shell
   n'a aucune raison d'être la première exception à la source unique.
   Une seule rupture NEUVE entre dans le front : 1600. Les deux autres (1180,
   561) y sont déjà — seuil de bascule du cockpit, et seuil du reste du shell.

   ⚠️ LE TITRE DE PAGE SUIT LES MÊMES RUPTURES (#793). Ses quatre corps sont
   MESURÉS dans les maquettes du cockpit aux quatre largeurs de recette —
   42/48 à 1920, 38/44 à 1440, 34/40 à 1024, 28/34 à 412 — et non déduits d'une
   règle d'échelle. Ils vivent ici plutôt que dans `optifisc.css` pour la raison
   qui a produit ce fichier : une valeur de design est une décision, elle ne se
   dissémine pas dans les blocs de média d'une feuille de composants. */
@media (max-width:1599px){
  /* Portable 15 pouces : sidebar 216 px, permanente. */
  :root{ --of-shell-nav-l:216px; --of-shell-gouttiere:24px; --of-display-l:500 38px/44px var(--of-font-display) }
}
@media (min-width:561px) and (max-width:1179px){
  /* Tablette : rail de 80 px, déployable à 216 px AU CLIC. */
  :root{ --of-shell-nav-l:80px; --of-shell-gouttiere:20px; --of-display-l:500 34px/40px var(--of-font-display) }
}
@media (max-width:560px){
  /* Téléphone : ni rail ni sidebar. La piste de gauche s'effondre à 0 — c'est
     pour cela que la grille de `body` déclare `auto` et non une largeur fixe. */
  :root{ --of-shell-nav-l:0px; --of-shell-gouttiere:16px; --of-display-l:500 28px/34px var(--of-font-display) }
}

/* ==================================================================
   LA SIGNATURE DE MOUVEMENT (#355, #782) — DEUX COURBES, DEUX VERBES.

   Sa mise en œuvre — cascade d'entrée des cartes, remplissage des barres,
   tiroir, hover discret, le tout ENFERMÉ dans
   prefers-reduced-motion: no-preference — reste dans `optifisc.css` et
   `situation.css` : ici on ne déclare que les valeurs.

   CE QUI A PRODUIT CES DEUX JETONS. Le dépôt portait QUATRE courbes pour une
   seule notion, dont trois vivantes côte à côte sans rien qui exige leur
   égalité (#592 appliqué au mouvement) :
     · `cubic-bezier(.22, 1, .36, 1)` — l'ancien jeton `--of-ease`, 2 sites ;
     · `cubic-bezier(.2,.7,.2,1)`     — EN DUR 11 fois (`optifisc.css` ×9,
       `situation.css` ×2), dont la barre de dérive de poche, qui animait sa
       LARGEUR avec cette courbe et son ENTRÉE avec l'autre — deux courbes sur
       le même objet, à la même seconde ;
     · `cubic-bezier(.2,.7,.3,1)`  et  `cubic-bezier(.16,1,.3,1)` — les valeurs
       écrites du dossier v4, employées nulle part.
   Onze littéraux ne faisaient donc pas onze courbes : ils en faisaient UNE, et
   c'est la coexistence silencieuse avec le jeton qui était le défaut.

   POURQUOI DEUX ET PAS UNE. Le dossier v4 nomme les gestes, et il en distingue
   deux que le front réalise déjà :
     · « s'ouvre » — ce qui s'ÉTEND (une hauteur, une largeur, un tiroir qui
       se déplie). Décélère fortement, comme une page qu'on rabat.
       `design-system/BRAND_LANGUAGE.md:336`.
     · « émerge » — ce qui MONTE (opacité 0 → 1 + translation vers le haut).
       Départ franc, parce qu'une proposition se présente.
       `design-system/BRAND_LANGUAGE.md:337`, et la même courbe au même verbe
       dans `authority/DIRECTIVE_DA_02_NAVIGATION.md:87` et `:124` — rang
       supérieur dans l'ordre de lecture de `README_FIRST.md`.
   `design_handoff_optifisc_ui/README.md:250` dit, lui, « courbe unique
   `.2,.7,.3,1` ». C'est l'outlier : deux pièces contre une, dont l'une de rang
   supérieur, et README_FIRST avertit déjà que ce fichier porte des pièces
   périmées (#778). L'écart est signalé ici, pas arbitré en silence.

   LES QUATRE AUTRES VERBES DU DOSSIER NE SONT PAS DÉCLARÉS, ET C'EST VOULU.
   « se dépose » (`.33,1,.68,1`), « s'éteint » (`ease-out`), « respire »
   (`ease-in-out`) et « interrompt » (aucune transition) n'ont AUCUN site dans
   le front actuel. Un jeton sans site est un point d'entrée mort (Q2 de #697),
   et une quatrième valeur qui flotte est exactement ce que #782 corrige. Ils
   arrivent avec les écrans qui portent ces gestes (#765, #768).

   ⚠️ Aucun `cubic-bezier` littéral ne doit réapparaître ailleurs dans
   `backend/public/` : `GardeCourbeDeMouvementUniqueTest` rougit.
   ================================================================== */
:root{
  --of-ease-ouvre: cubic-bezier(.2, .7, .3, 1);   /* s'ouvre — ce qui s'étend */
  --of-ease-emerge: cubic-bezier(.16, 1, .3, 1);  /* émerge — ce qui monte   */
}
