/**
 * Camboyer — design tokens (couche 1 du design system)
 *
 * Ce fichier ne contient QUE des variables `--cb-*` — c'est la définition de
 * la couche 1 dans `design-system/AGENTS.md`. Le `:root` ci-dessous est le
 * seul sélecteur admis : une propriété personnalisée CSS ne peut pas être
 * déclarée sans porteur. Aucune règle de style, aucun autre sélecteur.
 * Les classes qui consomment ces tokens vivent dans `atoms.css` (couche 2).
 *
 * Chaque groupe cite sa source : `docs/charte.md` (la charte), `docs/prod.md`
 * (constat serveur), ou « dérivé »/« parti pris » quand la charte ne dit rien
 * (elle ne couvre ni l'espacement, ni les rayons, ni l'échelle typographique —
 * charte.md § 6). Rien n'est inventé au sens d'un fait : ce qui n'est pas une
 * couleur/police de charte est une décision de design system, assumée comme
 * telle et documentée.
 *
 * Ticket : DS01. Ne rien recopier du legacy sans le requalifier — voir les
 * commentaires "requalifié vs legacy" ci-dessous.
 */

:root{

  /* ====================================================================
   * COULEURS — palette de travail, docs/charte.md § 2.2 (8 entrées)
   * C'est cette palette qui s'applique, pas le brand book seul (Q05,
   * tranchée). Trois de ces couleurs sont des ajouts hors brand book
   * (beige, olive, cta) — voir charte.md § 2.3, ce n'est pas une dette.
   * ================================================================== */
  --cb-ivoire: #FFFDF0;       /* brand book (Mineure #1) — fond clair dominant */
  --cb-beige: #E9EADD;        /* ajout — fonds de section/cartes secondaires, couleur la plus fréquente en prod (49 occ., charte.md § 2.3) */
  --cb-vert: #284634;         /* brand book (Dominante #1) — vert profond, sections sombres */
  --cb-encre: #212721;        /* brand book (Neutre) — texte et contours sur fond clair */
  --cb-olive: #4B6146;        /* ajout — vert secondaire, couleur "primary" Astra en prod */
  --cb-terracotta: #DF6A2E;   /* brand book (Déclinaison) — accent, texte script. Contraste 3,3:1 sur ivoire : sous le seuil AA, ne PAS l'utiliser pour du texte de bouton (charte.md § 2.3) */
  --cb-cta: #B64A14;          /* ajout — fond du bouton primaire, terracotta assombri pour atteindre 5,17:1 sur ivoire (AA) */
  /* DS63 — LE SURVOL DU BOUTON PRIMAIRE. Pas une couleur neuve : --cb-cta
     APPROFONDI vers l'encre, donc dérivé de la palette comme --cb-tag-bg ou
     --cb-univers-chambre-charme le sont déjà.
     ⚠️ POURQUOI IL EXISTE, et ce n'est pas une préférence : jusqu'ici le
     survol de --tone était `--cb-encre`… exactement là où --fill arrive aussi.
     Primaire et secondaire devenaient donc STRICTEMENT identiques au survol —
     même fond, même texte, même bordure — et rien ne distinguait plus l'action
     principale de la secondaire au moment précis où l'utilisateur vise.
     ⚠️ ET LE CONTRASTE N'ÉTAIT PAS LE JUGE UTILE : cta→encre valait 2,89:1
     d'écart, mais surtout ΔE **72** — un changement de TEINTE, pas d'intensité,
     qui effaçait la couleur de marque au survol. Le 85/15 retenu vaut ΔE 9,7 :
     nettement perceptible, teinte conservée. Et il AMÉLIORE la lisibilité —
     texte ivoire à **6,12:1** contre 5,17 au repos.
     Mesures et candidats écartés : tickets/journal/DS63.md. */
  --cb-cta-hover: color-mix(in srgb, var(--cb-cta) 85%, var(--cb-encre));
  --cb-sauge: #A2BD8C;        /* brand book (Déclinaison) — script sur fond sombre, fonds doux */
  --cb-tag-bg: rgba(40, 70, 52, .1); /* dérivé — --cb-vert (#284634) à 10 %, fond des pastilles/tags */
  /* --cb-overlay-veil : RETIRÉ le 2026-08-11 (DS37), décision d'Alexandre. NE PAS LE RECRÉER.
     Il n'existait que pour `card/overlay`, l'atome texte-sur-photo, lui-même abandonné : `H03` a
     mesuré le pire pixel derrière le titre sur les 4 VRAIES photos — 1,23 / 1,40 / 1,57 / 2,82:1,
     les quatre sous le seuil grand texte. Le voile ne rattrape pas 4 photos quelconques, et le
     durcir avait déjà été écarté à l'œil par DS12 (arête horizontale en travers de l'image).
     Les 5 variantes mesurées au pixel restent dans tickets/journal/DS12.md : la mesure est
     conservée, c'est son emploi qui disparaît. Raisonnement complet : atoms.css § card/overlay. */

  /* ajout DS14 — hors palette de travail à 8 entrées ci-dessus (charte.md § 2.2) : titre Fonseca des lockups titre+script (title/lockup-left, title/lockup-center, title/inline-accent), variable Figma "Black writing" confirmée 3/3 par get_variable_defs (docs/lockup-titres.md § 4). Distincte de --cb-encre (#212721) — proche (~9/255 par canal) mais aucune source n'établit de filiation entre les deux : ne pas fusionner, ne pas écraser --cb-encre. Portée strictement limitée au titre de ces 3 lockups, pas un remplacement général de --cb-encre sur les autres titres du site. */
  --cb-lockup-titre: #181C18;

  /* ====================================================================
   * UNIVERS — Q14/DS31 : la couleur d'accent suit l'univers de la section.
   * Table décidée par Alexandre (docs/couleurs.md § 6.2), noms rattachés
   * par Q22 (confirmé 2026-08-11). Coût réel : UN seul token neuf ici
   * (--cb-univers-spa) — les autres univers réemploient --cb-cta/
   * --cb-olive/--cb-sauge déjà déclarés plus haut ; le mapping complet
   * (quel univers → quel token) vit dans atoms.css § .cb-univers--*, pas
   * ici — ce fichier ne pose QUE les valeurs qui n'existaient pas encore.
   * ================================================================== */
  --cb-univers-spa: #417D72;  /* Q14 "dark blue" / Q22 — spa·détente·piscine·bien-être (fond clair) ET chambre Élégance, même valeur (docs/couleurs.md § 6.2). ⚠️ Q10 note un doublon volontaire avec #89B1AA (brand book "Bien-être", absent de la palette, Q10 non tranchée) — deux bleu-verts voisins, pour deux raisons différentes, signalé là-bas */
  --cb-univers-chambre-charme: color-mix(in srgb, var(--cb-lockup-titre) 80%, transparent); /* Q22 "black writing 80%" — dérivé de --cb-lockup-titre, MÊME formule que .cb-title-inline__script (atoms.css) : pas une couleur neuve, l'opacité 80% déjà en usage ailleurs dans le système. Calibré fond clair (composite #464943 sur ivoire, 8,96:1, docs/couleurs.md § 6.2) — jamais posé sur fond sombre (DS31, invisible : 1,11:1 sur encre) */
  /* Les autres univers de la table Q14 n'ont besoin d'AUCUN token supplémentaire —
     ils réemploient directement --cb-cta (dark orange : restauration, général hôtel
     fond clair, chambre Prestige, défaut clair), --cb-olive (medium green :
     parc & activités, chambre Suite Camboyer) et --cb-sauge (light green :
     général hôtel fond sombre, défaut sombre, ET — DS31 — TOUT univers sur fond
     sombre, voir atoms.css § .cb-univers--* pour l'argument). */

  /* ⚠️ CE TOKEN EST VOLONTAIREMENT SANS CONSOMMATEUR, ET IL NE FAUT PAS LE
     « NETTOYER » — DS30, 2026-08-19. Le commentaire qui vivait ici disait
     l'inverse (« ne pas introduire un token qu'aucun atome ne consomme
     aujourd'hui ») : il est périmé, `Q10` a été tranchée le 2026-08-11 en
     faveur de son ENTRÉE DANS LA PALETTE, même sans emploi identifié
     (docs/couleurs.md § 5). C'est un écart ASSUMÉ à la règle « rien de
     spéculatif » du projet, et l'écart a une raison : c'est la seule des 6
     couleurs du brand book absente du système, elle porte le pôle
     « Bien-être » et « Les salons » sur la planche d'applications (p. 9), et
     la faire disparaître du système reviendrait à décider en silence qu'elle
     n'existe plus. Une session future qui la trouverait inutilisée doit lire
     ceci avant de la retirer. */
  --cb-bien-etre: #89B1AA;  /* Charte, P 132-10 C — pôle « Bien-être » du brand book (p. 9), la 6ᵉ couleur, entrée dans la palette par Q10 le 2026-08-11. ⚠️ EMPLOI RESTREINT AU FOND SOMBRE, et le chiffre commande : 6,49:1 sur encre (AA texte normal, passe) mais 2,30:1 sur ivoire (échoue même le seuil 3:1 des grands textes). Comme la sauge, c'est une couleur de SURFACE sur clair, jamais de texte. ⚠️ Doublon volontaire de voisinage avec --cb-univers-spa (#417D72) : deux bleu-verts proches pour deux raisons différentes — celui-ci vient du brand book, l'autre de la table d'univers Q14. Ne pas fusionner. ⚠️ Aucune source n'établit de filiation avec le #78b8b2 relevé en dur en prod (à vérifier). */


  /* ====================================================================
   * TYPOGRAPHIE — familles, docs/charte.md § 3
   * ================================================================== */
  --cb-font-caps: 'Fonseca', sans-serif;         /* charte § 3.1/3.2 — titrage UNIQUEMENT. Fallback corrigé vs legacy : Fonseca est une géométrique sans-serif, le legacy déclarait `serif` (écart #13 charte.md § 5, "à corriger (mineur)") */
  --cb-font-script: 'DianaWebber Script', cursive; /* charte § 3.1/3.2 — sous-titrage et accents manuscrits uniquement */
  --cb-font-body: 'Teachers', sans-serif;        /* Q03 tranchée — labeur. Écart assumé vs Addington CF du brand book, ne pas "corriger" (charte.md § 5, écart #1) */
  /* DS27 — la QUATRIÈME voix, et elle n'entre pas en contradiction avec Q03 :
     Addington CF est écartée du LABEUR (--cb-font-body ci-dessus), elle est
     retenue comme ACCENT DE TITRE, en italique. Décidée par Q18/Q22 après
     révision : Q17 retire du serveur les italiques de Fonseca, donc l'option
     "accent en Fonseca italique" exigeait des .woff2 absents du dépôt, au
     risque d'une fausse italique synthétisée en silence — alors qu'Addington CF
     est déjà versionnée (assets/fonts/addington-cf/, 14 .woff2, italiques
     comprises) et installée par install-fonts.php. Servie sur la préprod
     depuis S08 (docs/prod.md § 12.6). Fallback aligné sur la déclaration
     serveur du script (Georgia, serif) — ne pas mettre 'cursive' ici : ce
     n'est pas une manuscrite, c'est un romain à empattements. */
  --cb-font-accent: 'Addington CF', Georgia, serif;

  /* ---- graisses — H01 -----------------------------------------------------
   * POURQUOI CES TROIS TOKENS EXISTENT. Jusqu'ici la graisse était écrite en
   * dur, quatre fois (`font-weight: 300` sur .cb-title-lockup__title,
   * .cb-title-inline__title, .cb-title-accent, .cb-heading) — donc quatre
   * valeurs à faire diverger, ce qu'`AGENTS.md` interdit (« aucune valeur en
   * dur »). Le déclencheur est la frame du hero reworké (40000182:18455) :
   * elle emploie DEUX graisses de Fonseca côte à côte, à 3 cm l'une de
   * l'autre, ce qu'aucun atome ne savait exprimer.
   *
   * ⚠️ LES DEUX VALEURS SONT MESURÉES, pas déduites — lecture des segments de
   * texte du fichier Figma (`getStyledTextSegments`) sur les deux nœuds :
   *   - `40000182:18461` (le h1 « Le Domaine de Camboyer ») → Fonseca **Thin**
   *   - `40000182:18462` (« hôtel & Événements d'exception ») → Fonseca **Light**
   * Le poids déclaré par Figma pour Thin est 250 ; la valeur retenue ici est
   * **200**, parce que c'est celle sous laquelle `install-fonts.php` sert
   * réellement la face `Fonseca-Thin` sur le serveur (§ 'Fonseca' ligne 39).
   * Demander 250 en CSS ne servirait aucune @font-face déclarée.
   *
   * ✅ `Q27` EST TRANCHÉE (Alexandre, 2026-08-12) : **TOUT LE TITRAGE**, pas le
   * seul h1. Sa phrase d'origine — « sur le DS c'était 300, on va passer le DS à
   * 200 et répercuter proprement » — se lisait des deux façons ; il a confirmé la
   * lecture large. Les deux paliers valent donc 200, et le découpage reste utile :
   * il garde deux leviers distincts si display et titrage doivent redivergent.
   *
   * ⚠️ CE QUE ÇA CONTREDIT, et il faut le savoir : la frame validée garde Fonseca
   * **Light 300** sur le titre d'accent posé à 3 cm du h1 (`40000182:18462`,
   * mesuré). Le titrage du site s'écarte donc sciemment de cette source-là. C'est
   * une décision, pas une dérive — ne pas « recorriger » vers 300 en relisant la
   * frame.
   *
   * ⚠️ RAYON RÉEL, mesuré et non supposé : le token n'est consommé que par 4
   * atomes de titre, dont les classes ne sont posées que sur les pages où un
   * mapping a été appliqué — `11316` (SPA, `P01`), `15972` (espaces de réception,
   * `P02`), `8422` (bloc Bienvenue, `H02b`). Ce n'est PAS « site entier » tant que
   * les autres blocs ne portent pas nos classes. Preuve avant/après aux deux
   * largeurs : `docs/preuves/Q27/`. */
  --cb-weight-display: 200;  /* Fonseca Thin — palier display : le h1 du hero, et lui seul aujourd'hui */
  --cb-weight-title: 200;    /* Fonseca Thin — titrage courant (3 lockups + heading/h2/h3). 300 → 200 par Q27, 2026-08-12. */
  --cb-weight-script: 400;   /* DianaWebber Script — MESURÉ en production le 2026-08-18 sur les trois
     accents de nom de chambre (`256cd6c` « Charme », `5b724ad4` « Prestige », `dd802b6` « Élégance ») :
     400 des trois côtés. La fonte n'a qu'une graisse ; ce token existe pour que le pont puisse RENDRE ce
     poids à un accent script que `.cb-heading` avait fait descendre à 200 (REG01 § 2) — pas pour offrir un
     réglage. ⚠️ Il ne remplace pas `--cb-weight-title` : celui-là est une décision (Q27), celui-ci est un
     fait de fonte. */
  --cb-weight-control: 400;  /* Teachers Regular — boutons, liens, items de nav.
     ⚠️ La frame du hero reworké dit Teachers **Medium 500** sur les 12 libellés
     de contrôle du header, et Alexandre a tranché : « teachers medium 500
     pourrait être employée pour tout ce qui est boutons et liens, mais pour
     l'instant, ok, on va rester sur 400 » — d'où ce token, qui existe pour
     rendre ce basculement possible en une ligne le jour où il se décide.
     La valeur est donc un ÉCART ASSUMÉ à la frame, pas une lecture de la frame. */

  /* DS27 — le rapport de taille accent ↔ titre, POUR L'ACCENT ITALIQUE
     seulement. Mesuré sur la frame validée du hero (tickets/journal/
     H01a-releve.md § 5) : titre Fonseca Light 32px, accent Addington CF
     Regular Italic 40px → 40/32 = 1,25. L'accent est TOUJOURS plus grand que
     le titre, jamais l'inverse — même loi que le script manuscrit, autre
     valeur. ⚠️ Ne pas confondre avec le rapport ≈ 1,8 du couple
     Fonseca ↔ DianaWebber (Q13, mesuré 1,78/1,83/1,83 sur 3 nodeId) : deux
     familles différentes, deux rapports, et c'est DS32 qui tokenisera le
     second. Une seule source par valeur. */
  --cb-ratio-accent-italic: 1.25;

  /* ---- échelle de tailles — base 16px (décision projet, charte § 3.3) --
   * DS01 pose l'échelle ; DS03 tranchera le rôle de chaque palier sur les
   * balises et arbitrera entre les deux échelles concurrentes en prod :
   * Astra (h1 5vw fluide, h2 38px, h4 25px, h6 14px — le vrai porteur de
   * charte, docs/prod.md § 5.2) et le kit Elementor (h1 176px, h2 52px,
   * h4/h6 15px — incohérent, aucune famille déclarée, docs/prod.md § 3.1).
   * Point de départ retenu ICI : Astra, pour son ordre de grandeur et sa
   * logique fluide — jamais le kit Elementor. Les `clamp()` reprennent
   * cette logique fluide tout en la plafonnant, pour rester sobres (brief :
   * la sobriété, jamais un effet qui attire l'œil sur lui-même) — contrairement
   * au h1 Astra en `5vw` pur, non plafonné, qui grossirait sans limite sur
   * un grand écran.
   */
  --cb-text-sm: 14px;      /* reprend la taille réelle du h6 Astra (docs/prod.md § 5.2) — affectée au rôle label/tag, jamais à un titre (Q09 : h4/h6 ne doivent plus servir de petit texte titré) */
  --cb-text-base: 16px;    /* corps de texte — charte § 3.3, confirmé déjà appliqué en prod */
  --cb-text-lg: 20px;      /* (parti pris) — paragraphe d'accroche / intro de bloc (hero, "Bienvenue" — docs/blocs-homepage.md), aucun fait prod ne fixe cette taille */
  --cb-text-h3: clamp(22px, 1.8vw, 28px);  /* (parti pris, logique Astra) — titre de niveau 3 */
  --cb-text-h2: clamp(28px, 2.6vw, 40px);  /* point de départ Astra (38px desktop réel) */
  --cb-text-h1: clamp(32px, 4.5vw, 64px);  /* point de départ Astra (5vw fluide), plafonné — le kit Elementor (176px) est écarté. ⚠️ AUCUN consommateur à ce jour (vérifié par grep sur atoms.css) : c'est le palier du h1 d'une PAGE, pas celui du hero, qui a le sien ci-dessous. Les deux se réconcilient à DS32, pas ici. */

  /* ---- le couple de titres du HERO — H01 ---------------------------------
   * UN SEUL palier pour DEUX atomes, et c'est le fait qui le justifie : sur la
   * frame validée (40000182:18455), le h1 (`40000182:18461`) et le titre
   * d'accent (`40000182:18462`) sont **à la même taille**, 32px tous les deux,
   * mesurés au segment de texte. Ce ne sont pas deux valeurs qui se
   * ressemblent, c'est un couple typographique : ils se lisent ensemble, en
   * haut du hero, de part et d'autre de l'écran.
   *
   * Ce token REMPLACE le `font-size: 32px` littéral que DS27 avait dû écrire
   * en dur dans `.cb-title-accent` (« aucun --cb-text-* ne vaut 32px fixe »,
   * pont § 2bis) — donc il retire une valeur en dur au lieu d'en ajouter une.
   *
   * `(à arbitrer)` — même statut et même destination que les 36px/48px des
   * deux autres lockups : une taille littérale relevée sur maquette, marquée
   * comme telle, qui rejoint le périmètre de DS32 (Q13). Ne PAS y substituer
   * `--cb-text-h1` : son clamp monte à 64px, soit le double, et la frame ne
   * montre nulle part un h1 de hero à cette taille. */
  --cb-text-title-hero: 32px;

  /* DS32 — un lockup ne porte pas deux tailles indépendantes. Les points
     d'entrée restent ceux relevés dans Figma (36px compact, 48px display),
     mais DianaWebber dérive toujours de Fonseca. Les trois relevés donnent
     1,78 / 1,83 / 1,83 : `--cb-ratio-lockup-script` documente la loi
     commune (≈1,8) ; les deux ratios de rendu conservent à l'octet les
     valeurs Figma 64px et 88px jusqu'à ce qu'une taille finale soit décidée.
     `section-accent` reprend la loi commune sur l'échelle h3 : c'est le
     parti pris explicite qui ferme son rendu 16px, pas une valeur locale. */
  --cb-text-lockup-compact: 36px;
  --cb-text-lockup-display: 48px;
  --cb-ratio-lockup-script: 1.8;
  --cb-ratio-lockup-script-compact: 1.7777777778; /* 64 / 36 */
  --cb-ratio-lockup-script-display: 1.8333333333; /* 88 / 48 */
  --cb-text-section-accent: calc(var(--cb-text-h3) * var(--cb-ratio-lockup-script));

  /* ---- média d'une section DEUX COLONNES image/texte — DS65, tranché par
   * Alexandre le 2026-08-18 : « une hauteur consistante et responsive, on veut
   * pas des monolithes sur petit écran, on vise le ratio 3/4 sur desktop et 4/3
   * sur mobile ».
   *
   * ⚠️ CE QUE SA DÉCISION REMPLACE, et il faut le dire : la proposition de
   * `DS65` recommandait `2/3`, sourcé sur 4 occurrences mesurées à 516×800
   * (`docs/images.md` § 1.2). Il tranche `3/4`. L'écart est faible et joue en
   * faveur de sa valeur : à 800 px de haut, `3/4` demande 600 px de large, et la
   * colonne dominante du site en sert **576** (64 des 145 médias) — ratio servi
   * 0,72 contre 0,75 visés. Les quatre ratios existants (`3-2`, `2-3`, `16-9`,
   * `15-17`) SURVIVENT : ils servent d'autres rôles, aucun n'est retiré.
   *
   * ⚠️ POURQUOI UNE HAUTEUR ICI, alors qu'`atoms.css` interdit « une hauteur en
   * dur ». Cette interdiction protège la FORME d'une image contre un pixel
   * arbitraire. Or ce qu'Alexandre demande — « des tailles 100 % cohérentes
   * d'une section à une autre » — un ratio ne peut PAS le donner : les colonnes
   * servies font 328, 438, 479, 552, 556, 576 et 1152 px, donc un ratio seul
   * rend les formes cohérentes et les hauteurs divergentes (17 hauteurs
   * distinctes mesurées aujourd'hui pour ce seul rôle). La hauteur est donc
   * maîtresse en desktop — mais FLUIDE, ce qui est l'autre moitié de sa demande.
   *
   * ⚠️ LE `78vh` N'EST PAS CHOISI, IL EST DÉRIVÉ : il donne 800 px sur le
   * viewport qu'il nomme (« un desktop qui fait 1440 x 1024 »), soit
   * 800 / 1024 = 0,781. Les deux bornes du `clamp` bornent la dérive : jamais
   * moins de 420 px sur un portable bas, jamais plus que sa valeur cible.
   * ------------------------------------------------------------------------ */
  --cb-img-duo-ratio: 3 / 4;        /* desktop — sa décision du 2026-08-18 */
  --cb-img-duo-ratio-mobile: 4 / 3; /* sous 1025 px, la section s'empile : la colonne DEVIENT le viewport,
     et une hauteur y produirait exactement le monolithe qu'il refuse. Le ratio reprend la main, couché. */
  /* ⚠️ CE TOKEN NE DÉPEND PLUS DE LA HAUTEUR DE FENÊTRE — corrigé le 2026-08-19
     sur signalement d'Alexandre (« pas hyper à l'aise avec la hauteur des images
     quand j'ai ma fenêtre en full largeur de mon mac mais avec une faible
     hauteur »). Il valait `clamp(420px, 78vh, 800px)`, donc l'image RÉTRÉCISSAIT
     quand la fenêtre était basse : mesuré 702 px sur une fenêtre de 900 (78 %),
     et jusqu'à 420 sur une fenêtre basse — pendant que la colonne de texte, elle,
     ne bougeait pas. D'où l'image « étouffée » à côté d'un texte plus haut.
     ⚠️ LE `78vh` N'ÉTAIT PAS UNE ERREUR DE VALEUR, c'était une erreur de
     DÉPENDANCE : une image de section ne se dimensionne pas sur l'écran, elle se
     dimensionne sur sa colonne. Le ratio (`--cb-img-duo-ratio`) le fait déjà, et
     il est dérivé de la LARGEUR — donc stable quelle que soit la hauteur de la
     fenêtre. Ce token ne sert plus que de PLANCHER et de PLAFOND, pour qu'une
     colonne très étroite ou très large ne produise pas une image absurde. */
  --cb-img-duo-h-min: 420px;
  --cb-img-duo-h-max: 800px;
  --cb-img-duo-vignette-h: 120px;   /* la BANDE DE VIGNETTES du carrousel en skin `slideshow` — MESURÉE
     en production le 2026-08-18 sur 8 bandes indépendantes (/hotel/ ×4, /hotel/chambres/ ×4) : 510×120 les
     huit fois. Ce n'est pas le média de la section et elle ne suit donc pas `--cb-img-duo-h` ; mais elle
     doit garder une hauteur, sinon ses vignettes sont des fonds sans dimension propre et la bande tombe
     à 0 — ce qui est arrivé, et se voit. ⚠️ Un token pour une valeur qu'on RESTAURE, pas qu'on décide :
     le jour où la bande se redessine, c'est `DS65` qui tranchera, pas ce token. */

  /* ---- largeur de mesure — DS09, absorbe DS19 ----------------------------
   * Trou signalé par docs/spacing.md § 7 : le container fait 1490px, le
   * contenu utile ~1360px après padding (--cb-space-inset-*) — largement
   * au-delà d'une longueur de ligne lisible pour un paragraphe. C'est une
   * propriété du PARAGRAPHE (docs/typographie.md), pas un palier
   * d'espacement de mise en page : ne rejoint donc pas les --cb-space-*
   * ci-dessous.
   * (parti pris) — tranché À L'ŒIL sur le texte réel « Nichée au cœur…
   * » du bloc Bienvenue (reference/site-actuel/homepage.md L226), rendu
   * en Teachers 16px/1.6 à pleine largeur de colonne (paragraph/body,
   * `"wide": True` sur la page de revue, pour que le token soit vraiment
   * la contrainte — pas la grille à 2 colonnes de la carte), aux 3
   * largeurs (375/900/1440). `65ch` essayé d'abord : ~80 caractères
   * réels par ligne au rendu (mesuré, `p.textContent.length /
   * nb-lignes`) — au-delà du haut de la fourchette usuelle, jugé trop
   * long à l'œil. `55ch` retenu : ~65-67 caractères réels par ligne
   * mesurés, confortable — voir tickets/journal/DS09.md pour les deux
   * captures comparées. `ch` plutôt qu'un px fixe : la mesure reste
   * correcte quelle que soit la taille de police réellement rendue
   * (repli système si Teachers ne charge pas, `docs/elementor.md` § 5)
   * — un px fixe aurait figé un nombre de caractères différent selon la
   * police servie.
   *
   * ⚠️ 2026-08-17 — LE TOKEN N'EST PLUS UN DÉFAUT DE LA PROSE. `DS19`
   * posait cette borne sur `.cb-paragraph`/`.cb-list` eux-mêmes, donc sur
   * TOUT paragraphe du site : retiré sur décision d'Alexandre (« je vois
   * pas d'intérêt »). La prod ne bornait rien non plus — aucune règle de
   * mesure sur un `<p>` dans le CSS servi (vérifié sur
   * `reference/site-actuel/css/`, Astra pose même `max-width:none` en
   * template page-builder). Le token reste, employé par les COMPOSITIONS
   * qui le demandent explicitement (`.cb-intro__copy`,
   * `.cb-rooms-head__copy`, `.cb-events__copy` — où il porte en plus le
   * centrage) : la mesure devient un choix de bloc, plus un défaut global.
   *
   * ⚠️ 2026-08-18 — REVENUE SUR LA PROSE, ET PLUS LARGE (`DS71`, décision
   * d'Alexandre : « la mesure était trop basse. tu peux la remettre mais
   * plus grande width qu'avant »). Ce qui a rouvert l'arbitrage est une
   * MESURE, pas un goût : le paragraphe du bloc « LE DÉCOR DE VOS
   * ÉVÉNEMENTS » sortait à **1376 px** au rendu 1440, soit ~154 caractères
   * par ligne. 55ch valait 492 px (1ch = 8,953 px en Teachers 16 px, mesuré
   * au rendu) — c'est cette valeur-là qu'il juge trop basse, pas le
   * principe. **80ch = 716 px** `(parti pris)` : la borne haute usuelle de
   * lisibilité, et le seul paragraphe du site qu'elle déplace est
   * justement celui de sa capture (les autres prose mesurées sont déjà à
   * 459/624/648 px, bornées par leur colonne — rayon vérifié au rendu
   * avant écriture, `docs/preuves/DS71/`).
   * ⚠️ Sur un widget Elementor, ce token ne peut PAS vivre en couche 2
   * seule : `.e-con.e-con > .e-con-inner > .elementor-widget{max-width:100%}`
   * (frontend.min.css, spécificité 0-4-0) écrase toute classe simple. La
   * mesure est donc mirroitée en couche 3 — `docs/pieges.md` § 14.
   * ================================================================== */
  --cb-measure: 80ch;

  /* ---- rail de page — DS66 ----------------------------------------------
   * La boîte dans laquelle le contenu d'une section de premier niveau doit
   * tenir, quelle que soit la largeur de l'écran. Ce n'est PAS la largeur
   * utile d'une composition (ça, c'est ce qui reste une fois l'inset de la
   * section retiré) et ce n'est pas non plus une largeur de fond : un fond
   * reste à fond perdu, seul le contenu se borne.
   *
   * 1490px, et la valeur ne vient pas de Figma — aucune frame ne dit ce que
   * devient la page au-delà de 1440. Elle vient de ce que TOUT LE RESTE DU
   * SITE fait déjà : `container_width: 1490px` dans le kit
   * (docs/spacing.md § 4), donc le header (tickets/journal/H01i.md, contenu
   * gardé à 1490 en connaissance de cause), le footer et chaque section
   * encore legacy. Se caler dessus, c'est aligner le design system sur le
   * repère existant plutôt que d'en créer un deuxième — arbitré par
   * Alexandre le 2026-08-17 devant les trois lois relevées (DS66).
   *
   * ⚠️ Ce token valait 1280px jusqu'au 2026-08-17, décrit comme « largeur
   * utile 1440 − 2×80, relevée sur les frames H02/H03 ». Le relevé était
   * juste, mais il décrivait la largeur utile À 1440 — le résultat de
   * `rail − 2 × inset`, pas le rail. En le portant comme rail, `.cb-intro`
   * et `.cb-events` bornaient leur contenu 16px plus tôt que leur propre
   * inset ne le demandait (bord à 73px au lieu de 64) et surtout ne
   * disaient rien des sections qui n'avaient pas de borne du tout. La
   * largeur utile reste dérivée, elle n'a pas à être écrite : à 1440 elle
   * vaut toujours 1440 − 2×64 = 1312 pour une section à l'inset système.
   * ================================================================== */
  --cb-container-max: 1490px;


  /* ====================================================================
   * ESPACEMENT — composant (fixe, ne varie pas selon le viewport)
   * Un bouton ou un tag n'a pas besoin de rétrécir : ce sont des paddings
   * internes courts. Requalifié vs legacy : le legacy déclarait 11 paliers
   * (4 à 120px) dont seuls 4 étaient réellement consommés par son propre
   * CSS (8, 12, 16, 20px) — les 7 autres, dont le palier `4px`, n'avaient
   * aucun consommateur. On ne recopie que ce qui sert.
   * ================================================================== */
  --cb-space-xs: 8px;   /* padding vertical compact (tag) */
  --cb-space-sm: 12px;  /* padding vertical bouton, gap icône/texte */
  --cb-space-md: 16px;  /* padding horizontal bouton */
  --cb-space-lg: 20px;  /* padding horizontal tag */

  /* ---- espacement — mise en page, mobile ET desktop (demande explicite,
   * BRIEF.md § 6 / tickets/DS01-tokens.md) --------------------------------
   * Logique qui relie les deux : le mobile vaut la moitié du desktop,
   * arrondi à la grille 4px. Ordre de grandeur confirmé par un fait prod
   * comparable (padding de conteneur : 6,67em desktop → 2,4em mobile,
   * reference/site-actuel/charte-graphique.md — compression du même ordre).
   * Trois paliers seulement : ce que les blocs identifiés (BRIEF § 6,
   * docs/blocs-homepage.md) réclament — gap entre éléments d'un groupe,
   * padding interne d'un bloc/carte, padding vertical d'une section.
   */
  --cb-space-gap-mobile: 24px;      --cb-space-gap-desktop: 48px;      /* gap d'une GRILLE DE CARTES uniquement (chambres 2×2, 4 salons, 3 espaces). ⚠️ NE PLUS employer pour une rangée de pastilles ou de CTA — voir § FLUX ci-dessous, DS55 */
  --cb-space-inset-mobile: 32px;    --cb-space-inset-desktop: 64px;    /* PADDING INTERNE d'un bloc/carte, et rien d'autre. ⚠️ NE PLUS employer comme écart entre widgets empilés — voir § FLUX ci-dessous, DS55 */
  --cb-space-section-mobile: 56px;  --cb-space-section-desktop: 112px; /* padding vertical (haut/bas) d'une section pleine largeur */
  --cb-space-split-desktop: 80px; /* Figma 40000126:889/497 — écart entre le contenu et le média des compositions éditoriales dissociées. Un rôle de grille, pas un padding de composant. */
  --cb-space-hero-top: 32px; /* Figma 40000182:17715 — écart entre le BAS DU HEADER et le premier contenu d'un hero de page. ⚠️ Ce n'est PAS --cb-space-section-* : celui-ci règle le RYTHME ENTRE deux sections, or un hero n'a rien au-dessus de lui que le header. Mesuré sur la frame : la section « Frame 106 » commence à y=148, soit exactement sous les 2 rangées du header (74+74), et son image est à y=32 dans cette section. Employé par .cb-content-hero, et par lui seul aujourd'hui. */
  /* ---- LE DÔME — état de départ du média vidéo (P03f) --------------------
   * Frame 40000272:2142, nœud `40000272:2247` : un carré de **616 × 616** posé
   * à x=380 / y=79 dans la boîte média de 1376 × 774. Donc CENTRÉ sur les deux
   * axes, et ce n'est pas une lecture à l'œil : 380 = 1376 − 380 − 616, et
   * 79 = 774 − 79 − 616.
   *
   * Les deux retraits sont exprimés en POURCENTAGE, et c'est ce qui les rend
   * justes à toutes les largeurs : la boîte média garde son 16:9, donc le carré
   * reste carré. En pixels, ils ne décriraient que la largeur 1376.
   *   - inline : 380 / 1376 = 27,616 %
   *   - block  :  79 /  774 = 10,207 %
   * (Le côté du dôme n'a pas son token : il EST ce que ces deux retraits
   * laissent. Le déclarer en plus donnerait deux sources pour un seul fait.)
   */
  /* ⚠️ RÉÉCRIT EN PIXELS DÉRIVÉS DE LA LARGEUR (4ᵉ passe, 2026-08-17), et ce
     n'est pas cosmétique : depuis qu'Alexandre a demandé le **plein cadre quitte
     à recadrer**, la boîte média n'est plus toujours en 16:9 — sa hauteur se
     borne à la bande disponible sous le header. Des retraits en pourcentage,
     calibrés sur un 16:9, y auraient donné un dôme APLATI : sur un écran de
     644px de haut, 616 × 407 au lieu de 616 × 616. Le côté du dôme est donc
     dérivé de la LARGEUR (le seul repère stable), et les retraits s'expriment
     par soustraction — l'inline se lit sur la largeur, le block sur la hauteur,
     donc la même écriture donne un CARRÉ dans les deux axes, à toute hauteur de
     boîte. `max(0px, …)` pour le cas où la boîte est plus courte que le dôme :
     l'arche cesse alors d'être rognée en haut et en bas, elle ne devient pas
     négative. */
  --cb-dome-cote: calc((min(100vw, var(--cb-container-max)) - 2 * var(--cb-page-inset)) * 0.44767); /* mesuré — 616 / 1376 */
  --cb-dome-inset: max(0px, calc((100% - var(--cb-dome-cote)) / 2));
  /* L'ARCHE. La frame pose `rounded-t-[999px]` sur le carré de 616 : 999 se
     rabat sur la moitié du côté, soit un rayon de **308** — un demi-cercle.
     Deux tokens et non un, parce que le rayon vit dans une forme dont les
     pourcentages se lisent sur la BOÎTE (1376 × 774) et non sur le carré : le
     même 308px vaut donc 22,384 % horizontalement et 39,793 % verticalement.
     ⚠️ ET CE N'EST PAS DU ZÈLE : `round 50%` — la formulation courte,
     tentante — donne une ellipse PLUS PLATE, mesurée au pixel sur un banc
     d'essai (largeur de l'arche à 80px de profondeur : 522px avec `50%`,
     **418px** avec ces deux tokens, contre 418 attendus pour un demi-cercle de
     308). Les deux formes donnent bien le même carré de 616 centré ; seule la
     courbe diffère, et c'est justement ce qu'Alexandre a montré. */
  --cb-dome-radius: calc(var(--cb-dome-cote) / 2); /* la moitié du côté = le demi-cercle de la frame (308 sur 616), et en PIXELS : le même rayon dans les deux axes, sans dépendre du rapport de la boîte */
  /* Plage de défilement sur laquelle le dôme s'ouvre, sur la timeline de vue du
     bloc média. `(parti pris)` — la frame donne deux états, pas leur rythme.
     ⚠️ CALIBRÉE AU RENDU, ET LA PREMIÈRE VALEUR ÉTAIT FAUSSE : à `entry 15 %`,
     l'ouverture démarrait quand **37px seulement** du dôme étaient au-dessus du
     pli (mesuré à 1440 × 900 : bloc à y=784, dôme à y=863). Le dôme s'ouvrait
     donc SOUS l'écran, et l'utilisateur n'en voyait jamais l'état de départ —
     ce qui vide le mouvement de son sujet. À `entry 75 %`, **81 %** du dôme est
     visible quand il commence à s'ouvrir (dôme à y=399), et il est entièrement
     visible bien avant la fin. La fin passe sur la plage `cover` : elle donne
     341px de défilement d'ouverture au lieu de 194, pendant lesquels le bloc
     occupe l'écran. Les deux valeurs restent à trancher à l'œil en revue. */
  --cb-dome-scroll-start: contain 40%;
  --cb-dome-scroll-end: contain 75%;
  /* ---- LA RETENUE (le « pin »), demandée par Alexandre le 2026-08-17 ------
   * « Il faut retenir l'user en centrant le dôme et la vidéo pour laisser
   * l'animation se faire. Ajouter de la friction pour profiter de l'animation
   * et rester centré sur la vidéo. » C'est `pin: true` de ScrollTrigger (GSAP),
   * et son `scrub` pour la progression — ici obtenus sans JavaScript : le bloc
   * média devient COLLANT, et la section gagne cette course de défilement en
   * padding bas. Pendant cette course, le média reste centré à l'écran et le
   * dôme s'ouvre ; ensuite la page reprend son cours.
   *
   * ⚠️ EN PIXELS, ET C'EST VOULU : c'est une distance de DÉFILEMENT, pas une
   * hauteur. En `svh`, la page de revue — dont le viewport de capture fait
   * 8 192px de haut — se serait vue ajouter 6 500px de vide.
   *
   * ⚠️ LA COURSE EST LA FRICTION. Plus elle est longue, plus l'ouverture demande
   * de défilement, donc plus elle est lente et posée. C'est le seul réglage de
   * « ressenti » du mouvement, et il est `(parti pris)` : 800px ≈ deux à trois
   * gestes de molette. Mobile réduit — sur 390px de large le média ne fait que
   * 183px de haut, le retenir aussi longtemps n'apporterait rien. */
  --cb-dome-course-mobile: 400px;   --cb-dome-course-desktop: 800px;
  /* Hauteur de la boîte média, RECALCULÉE pour pouvoir centrer un élément
     collant : `top` ne peut pas se référer à la hauteur de l'élément lui-même.
     Elle dérive du rail (DS66) et du ratio 16:9 de la boîte — donc des mêmes
     décisions, pas de valeurs neuves. ⚠️ `100vw` inclut la barre de défilement
     (pieges § 15) : la hauteur est donc surestimée d'environ 8px à 1440, soit
     4px de décentrage. Assumé — le corriger demanderait de connaître la largeur
     utile, ce que le CSS ne donne pas ici. */
  --cb-dome-boite-h-max: calc((min(100vw, var(--cb-container-max)) - 2 * var(--cb-page-inset)) * 9 / 16); /* la hauteur du média s'il occupe tout le rail. `-max` parce que la boîte se BORNE à la bande disponible sous le header (atoms.css § dôme) : c'est un plafond, pas une hauteur fixe. */
  /* ---- HAUTEUR DU HEADER QUAND IL EST COLLÉ ------------------------------
   * Signalé par Alexandre au rendu (2026-08-17) : « prends en compte le header
   * sticky et positionne au centre de l'écran MOINS la hauteur du header,
   * sinon ma vidéo est partiellement masquée ». Mesuré : le média épinglé
   * passait 5px sous le header à 1440 × 900, et **133px** à 1440 × 644.
   *
   * ⚠️ LE HEADER N'A PAS UNE HAUTEUR, IL EN A DEUX SÉRIES — et c'est mesuré, pas
   * supposé. Au repos : 134 (≥1440), 124 (768–1024), 57 (390). **Collé, il se
   * compacte**, et de façon IRRÉGULIÈRE selon la façon dont sa rangée
   * utilitaire se replie : 58 à 768 et 1024, **68 à 1440**, 105 à 1600, 93 à
   * 1800, 105 à 1920 et 2400.
   *
   * ⚠️ D'OÙ LE **MAXIMUM** ET NON UNE MOYENNE : sous-estimer remet la vidéo
   * sous le header (le défaut signalé) ; surestimer la descend de quelques
   * pixels, sans rien masquer. Une valeur par palier voudrait poursuivre le
   * repliement d'un plugin sur cinq breakpoints — fragile, pour un centrage.
   * Le prix assumé : jusqu'à ~37px de décentrage vers le bas à 1440.
   * ✅ `(à arbitrer)` LEVÉ le 2026-08-17 : `sentinelle-collant.js` publie la
   * hauteur mesurée en `--cb-header-h` (`ResizeObserver`), et les consommateurs
   * l'emploient avec ces deux valeurs en repli. Le maximum ci-dessous n'est donc
   * plus la règle mais le filet sans JS. */
  --cb-header-colle-mobile: 57px;   --cb-header-colle-desktop: 105px;

  /* ---- HAUTEUR DU HEADER **AU REPOS** ------------------------------------
   * Repli de `--cb-header-h-repos`, publié par `sentinelle-collant.js`.
   *
   * ⚠️ CE N'EST PAS UN DOUBLON DES DEUX VALEURS CI-DESSUS, et la différence se
   * mesure : au repos le header vaut **134 / 124 / 57**, collé il se compacte à
   * **68 à 1440**. Ce sont deux états du même élément, et deux consommateurs
   * opposés — le dôme de `P03f` veut la hauteur COURANTE (il centre pendant le
   * défilement), le bandeau de `P03e` veut celle du REPOS (sa marge négative est
   * lue en haut de page). Faire servir une seule variable aux deux ferait sauter
   * le bandeau de 66px sous le header au premier pixel de défilement.
   *
   * ⚠️ ET ICI LE MAXIMUM SERAIT FAUX, à l'inverse du cas collé : une marge
   * négative trop grande découvre du fond ivoire entre le header et la photo —
   * le défaut exact que `P03e` a corrigé (le hero legacy portait -120px partout,
   * calibré sur un header à une rangée : 14px d'ivoire à 1440, 63px de média
   * rogné à 390). D'où trois paliers mesurés, et non une valeur unique. */
  --cb-header-repos-mobile: 57px;
  --cb-header-repos-tablet: 124px;
  --cb-header-repos-desktop: 134px;

  --cb-bandeau-lockup: 136px; /* Hauteur MESURÉE du lockup de titre d'un hero à bandeau, prise au plus grand palier typographique : 134px à 1920 et au-delà (106 à 1024, 111 à 1280, 124 à 1440 — relevé au rendu sur /hotel/chambres/ le 2026-08-17), arrondie à 136. ⚠️ CE N'EST PAS UNE HAUTEUR IMPOSÉE AU LOCKUP : celui-ci reste dimensionné par son texte, et doit le rester (un titre de 3 lignes doit pouvoir grandir). Ce token sert UNIQUEMENT à RÉSERVER sa place sous le bandeau — voir le plafond de .cb-bandeau. C'est une valeur à revoir si l'échelle de titre change ; le jour où elle mentira, le titre passera sous le pli, pas ailleurs. */
  --cb-space-hero-intro: 80px; /* Figma 40000265:1653 — padding vertical de la SECTION D'INTRODUCTION d'un hero à bandeau : MESURÉ, la « Frame 115 » pose son contenu à y=80 et le referme 80 avant son bas. ⚠️ Ce n'est pas --cb-space-section-desktop (112px) : celui-là règle le rythme entre deux sections QUELCONQUES, et Alexandre a jugé 112 trop aéré ici (2026-08-17) là où la frame donne 80. Ce n'est pas --cb-flow-block (80px) non plus, MALGRÉ LA VALEUR IDENTIQUE : celui-là est un écart entre deux BLOCS d'une section, pas le padding de la section — deux rôles, deux tokens, sinon le jour où l'un des deux bouge on ne sait plus lequel on déplace. Le mobile n'est pas dessiné par la frame : il garde --cb-space-section-mobile. Employé par .cb-content-hero--bandeau, et par lui seul. */


  /* ====================================================================
   * FLUX — les écarts ENTRE éléments (DS55)
   *
   * POURQUOI CETTE FAMILLE EXISTE. Les 3 paliers ci-dessus portaient chacun
   * DEUX rôles sans rapport, et dans les deux cas le rôle « conteneur » a
   * mangé le rôle « élément » :
   *   - `--cb-space-inset-*` = padding interne d'une carte ET « marge titre →
   *     contenu ». Employé en `gap` sur le corps de carte, il mettait **64px
   *     entre un titre de 26px et son paragraphe de 16px**.
   *   - `--cb-space-gap-*` = gap d'une grille de cartes ET « ligne de tags ».
   *     Il mettait **48px entre deux pastilles larges de 62px** — l'écart
   *     valait 77 % de l'objet.
   * L'erreur est dans `docs/spacing.md` § 4.2 : elle range « widgets empilés
   * dans un même bloc » et « rangée de pastilles » avec les paliers de MISE EN
   * PAGE, donc responsives en 1:2. Un écart entre deux pastilles ne double pas
   * parce que l'écran s'élargit — c'est ce que dit déjà § 1.3 pour les paliers
   * composant, et personne ne l'avait appliqué ici.
   *
   * D'OÙ VIENNENT CES VALEURS. Des deux frames Figma **validées**, mesurées le
   * 2026-08-11 (`40000117:16624`, « Section Intro ») — `master-instructions.md`
   * tranche la question de la source : « une maquette donne la mise en forme »,
   * la prod ne gagne que sur le CONTENU.
   *
   * Ces 4 paliers sont FIXES : aucun rapport 1:2 mobile/desktop.
   * ================================================================== */
  --cb-flow-actions: 12px;  /* entre deux CTA d'une même rangée — MESURÉ : boutons à x=0 (l.168) et x=180, soit 12px (Figma 40000117:16639) */
  --cb-flow-inline: 16px;   /* entre deux pastilles d'une rangée — MESURÉ 4 fois horizontalement (16/16/16/16) ET verticalement entre les 2 rangées (y=50, hauteur 34). Même valeur dans les deux axes : c'est un fait, pas une symétrie décidée (Figma 40000117:16709) */
  --cb-flow-lockup: 0px;    /* entre le titre Fonseca et son script DianaWebber dans UNE même composition — MESURÉ : les deux frames homepage 40000245:1362 et 40000253:1436 posent les boîtes sans gap. L'espace perçu relève de l'interligne (1,5 puis 1), pas d'une marge locale → DS62. */
  --cb-flow-text: 16px;     /* (parti pris) titre → texte, texte → texte, dans un même bloc. AUCUNE frame validée ne contient de carte : ce palier n'est pas mesuré. Il reprend --cb-flow-inline plutôt que d'inventer une 5ᵉ valeur — un pas de flux court, cohérent avec le seul pas court que Figma donne. À trancher à l'œil en revue → DS55 */
  --cb-flow-group: 32px;    /* (parti pris) entre deux GROUPES d'un même bloc : texte → rangée de pastilles, texte → rangée de CTA. Non mesuré non plus. Vaut le double de --cb-flow-text, pour que la frontière de groupe se lise sans rivaliser avec le rythme entre blocs (80px, mesuré 3 fois) → DS55 */
  --cb-flow-group-compact: 24px; /* (parti pris) LE MÊME RÔLE, À L'ÉCHELLE D'UNE PETITE CARTE — jugé à l'œil par Alexandre le 2026-08-20 sur trois captures : « trop d'espace à mon goût pour une petite card ». ⚠️ Ce n'est pas une valeur neuve dans l'échelle : 24 est déjà `--cb-space-gap-mobile`. Ce qui est neuf, c'est le RÔLE — un bloc composé ne respire pas pareil selon sa largeur. Mesuré, la répartition est franchement bimodale et ne laisse pas de zone grise : sur 100 blocs `.cb-flow--group` servis, **64 font 384 à 576 px** (les colonnes d'une grille) et **36 font 714 à 1440** (les blocs de section) — aucun entre les deux. Le premier groupe prend ce token via `.cb-flow--group--compact`, le second garde `--cb-flow-group`. Comme son voisin, il n'est pas mesuré sur une frame : une ligne à changer si l'œil dit autre chose. */
  --cb-flow-block: 80px;    /* entre deux BLOCS d'une section (titre → image → pastilles → CTA au niveau section) — MESURÉ 3 fois, exactement 80 à chaque fois (Figma 40000117:16624). ⚠️ Tension non résolue avec --cb-space-section-desktop (112px) : voir DS55 */

  /* EXPLORATEUR CHAMBRES — H03 V2. Ces valeurs ne généralisent pas une
   * échelle de mouvement : elles décrivent le seul explorateur qui possède un
   * média partagé et un contenu à divulgation progressive. La source de
   * comportement est `camboyer-archive/designs/v2-ambitieuse.html`, jamais
   * une source de contenu ou d'assets. */
  --cb-rooms-detail-gap: 24px;              /* paragraphe → stickers → lien : décision Alexandre, distincte du 16px entre pastilles d'une même rangée. Porte aussi titre → paragraphe : MESURÉ 24px dans Figma 40000253:1341 (label bas 36, texte haut 60) */
  --cb-rooms-active-offset: 24px;           /* décalage du CONTENU de l'item actif — MESURÉ dans Figma 40000253:1341 : le bloc ouvert commence à x=24, les items fermés à x=0. Remplace le 20px repris de V2 (translateX(1.2rem)) */
  --cb-rooms-reveal-rise: 6px;              /* (parti pris) montée des trois blocs révélés — assez pour lire le sens du mouvement, trop court pour lire un déplacement */
  --cb-rooms-reveal-stagger: 70ms;          /* (parti pris) décalage entre texte, pastilles et lien : la révélation totale reste sous la durée d'ouverture (500ms) */
  --cb-rooms-item-padding-start: var(--cb-flow-text);
  --cb-rooms-item-padding-end-closed: var(--cb-rooms-detail-gap); /* état fermé : bas plus généreux que haut */
  --cb-rooms-media-min-height: 680px;       /* hauteur du panneau éditorial V2/Figma 40000126:889 */
  --cb-rooms-motion-shift: 350ms;
  --cb-rooms-motion-reveal: 500ms;
  --cb-rooms-motion-fade: 400ms;
  --cb-rooms-motion-media: 600ms;


  /* ====================================================================
   * RAYONS — sourcés au constat prod (reference/site-actuel/charte-graphique.md),
   * pas à la charte (elle ne couvre pas les rayons, charte.md § 6). Trois
   * paliers, chacun avec son propre rôle et sa propre source — jamais l'un
   * arrondi sur l'autre pour économiser un token (DS21, ouvert pour
   * consolider les petits rayons, puis CLOS : la mesure a montré que 4px
   * boutons / pilule pastilles / 5px images sont trois faits distincts, pas
   * une variance à corriger).
   * ================================================================== */
  --cb-radius-sm: 4px;      /* bouton anguleux (thème Astra) : "angles très légèrement arrondis, 3-4px" observé — borne haute retenue */
  --cb-radius-md: 5px;      /* image/carte (DS08→DS25) : `image_border_radius`/`border_radius` mesuré identique sur 8 occurrences prod indépendantes et sans rapport entre elles (homepage ×4, spa, gîte, réception ×3, restauration ×2) — jamais 4px ni 6px, docs/images.md §5.1. Écart réel avec --cb-radius-sm (4px, sourcé boutons) : pas un bruit de mesure à arrondir sur un token voisin. */
  --cb-radius-pill: 999px;  /* bouton pilule (homepage Elementor) : "pilule complète, border-radius 50px" observé — 999px plutôt que 50px fixe, pour s'adapter à n'importe quelle hauteur de bouton */

  /* ====================================================================
   * FILET — la ligne de séparation la plus fine du système (H01)
   *
   * Trou signalé par la relecture de la frame du hero reworké : elle pose une
   * séparation sous CHACUNE des deux rangées du header, et une séparation
   * verticale entre les groupes de la rangée utilitaire — et le système
   * n'avait aucun token de filet, seulement des rayons et des couleurs.
   *
   * MESURÉ, pas estimé : lecture des strokes du fichier Figma —
   *   - `40000182:18604` (rangée 1) et `40000182:18622` (rangée 2) :
   *     `strokeBottomWeight: 0.5`, `strokeAlign: INSIDE`, couleur
   *     `r:1 g:0.99215 b:0.94117` = **#FFFDF0**, soit `--cb-ivoire` exactement.
   *   - `40000182:18612` (séparateur vertical) : `strokeWeight: 0.5`,
   *     24px de haut, même ivoire.
   * Décision d'Alexandre sur le 0,5px : « 0.5 oui assumé, à inclure dans le
   * DS ». La couleur n'est PAS un token neuf — c'est l'ivoire de la palette à
   * pleine opacité ; seule l'épaisseur manquait.
   *
   * ⚠️ Ce que 0,5px veut dire au rendu, et il faut le savoir avant de juger :
   * sur un écran à 1 dpr le navigateur ne peut pas allumer un demi-pixel — il
   * rend une ligne d'1px atténuée. Le filet paraîtra donc plus clair que
   * l'ivoire plein, et légèrement différent entre un écran Retina et un écran
   * classique. C'est le comportement voulu (un filet doit s'effacer), pas un
   * défaut à corriger en montant à 1px.
   * ================================================================== */
  --cb-hairline: 0.5px;
  --cb-hairline-inset: 24px; /* longueur du séparateur VERTICAL de la rangée utilitaire — mesuré 24px sur `40000182:18612`. Pas un palier d'espacement : une longueur de trait. */
  /* ⚠️ LE FILET D'INDICATION DE NAV N'EST PAS UN FILET DE SÉPARATION, et il ne
     peut pas réemployer `--cb-hairline` (DS46b). Un séparateur doit s'effacer,
     donc 0,5px atténué est le bon comportement pour lui ; celui-ci doit se
     VOIR — c'est le seul retour visuel du survol d'un item de nav, et à 0,5px
     il serait plus discret que le trait qui ferme la barre juste à côté.
     Relevé au pixel sur la frame `40000272:1939` (le filet sous « Hôtel ») :
     une ligne blanche pleine à y=130 et une couverture d'environ 65 % à y=131,
     soit ~1,6px. Retenu 2px — un trait net à 1 dpr plutôt qu'un 1,6 qui
     rendrait flou. C'est le seul arrondi de cette passe, et il est déclaré. */
  --cb-header-filet-nav: 2px;

  /* ====================================================================
   * VOILE DE MÉDIA (scrim) — H01, et c'est le remède attendu par DS50
   *
   * CE QUE C'EST. Le dégradé posé PAR-DESSUS la vidéo du hero pour que le
   * header et les titres restent lisibles quel que soit l'instant de la
   * vidéo. Ce n'est pas une décoration : `DS50` a échantillonné la vidéo
   * image par image et mesuré, derrière le MÊME texte ivoire, un contraste
   * qui va de 10:1 à **1,37:1** selon la seconde (tickets/journal/
   * DS50-echantillonnage-video.md). Aucune couleur de texte ne rattrape ça ;
   * seul un voile le peut.
   *
   * ⚠️ VALEURS MESURÉES SUR LA FRAME, et elles ne sont pas celles annoncées.
   * Alexandre décrit « des gradients posés en haut et en bas qui vont en gros
   * de noir 40% à transparent ». La lecture du fichier Figma
   * (`40000182:18456`, 3ᵉ fill, `GRADIENT_LINEAR`) dit autre chose, et c'est
   * elle qui est reprise ici :
   *   - la `gradientTransform` [[0,1,0],[-1,0,1]] fait courir le dégradé du
   *     HAUT vers le BAS (position = y normalisé) ;
   *   - stop 0     → noir, alpha **0,6**
   *   - stop 12,5 % → noir, alpha **0,2**
   *   - stop 39,9 % → noir, alpha 0
   * Donc 60 % au bord, pas 40 %, et un coude à 12,5 % — un profil bien plus
   * dense en haut que la description ne le laissait croire. Sur les 821px du
   * rectangle : plein à y=0, 20 % à y≈103, éteint à y≈328 — soit juste sous
   * le bloc de titres, qui finit à y≈245.
   *
   * ⚠️ ET IL N'Y A PAS DE VOILE BAS DANS LA FRAME. Vérifié en listant tous les
   * nœuds porteurs d'un fill dans le sous-arbre `40000182:18455` : **un seul
   * gradient dans toute la frame**, celui du haut. Le paragraphe du bas est
   * donc posé sur la vidéo nue. Le voile bas ci-dessous est un `(parti pris)`
   * — il applique au bas la demande explicite d'Alexandre (« afin de rendre
   * lisible le header ET LE TEXTE EN BAS ») en MIROIR du profil mesuré en
   * haut, faute d'autre source. À juger à l'œil, et à vérifier au contraste
   * sur les deux instants extrêmes de `DS50` (34,59 s et 30,26 s).
   *
   * ⚠️ NOIR PUR, HORS PALETTE, ASSUMÉ. `--cb-scrim-color` vaut `#000` parce
   * que c'est ce que la frame mesure. Employer `--cb-encre` (#212721) à la
   * place tinterait le voile en vert sur toute la hauteur du hero. Un voile
   * est une DENSITÉ, pas une couleur de marque : il n'entre pas dans la
   * palette de `charte.md` § 2.2 et ne doit pas y être ajouté.
   * ================================================================== */
  --cb-scrim-color: #000;            /* mesuré (frame) — densité, pas couleur de marque */
  --cb-scrim-edge: .6;               /* opacité au bord de l'écran — mesuré */
  /* ⚠️ RELEVÉ À .2 SUR LA FRAME, PORTÉ À .25 PAR DÉCISION D'ALEXANDRE (2026-08-18).
     `Q31` a montré que le voile ne tenait pas la rangée du menu sur une photo
     claire : caler le coude sur la hauteur du header a réglé la rangée du haut
     (4,18 → 5,41:1) mais pas celle du menu (2,19 → 2,74:1), parce que déplacer
     un arrêt change OÙ le voile s'éclaircit, jamais À QUEL POINT il est sombre.
     Trois issues lui ont été soumises ; sa réponse : « trop sombre c'était pas
     terrible, on coupe la poire en deux : 25 % ». C'est donc un arbitrage entre
     lisibilité et présence de la photo, PAS une valeur mesurée — et il est
     cohérent avec `Q28`, qui avait retiré `.cb-scrim--dense` pour excès.
     ⚠️ Ce token sert les DEUX voiles, haut (`::before`) et bas (`::after`) : ils
     sont le miroir l'un de l'autre, une seule densité les décrit. Le voile bas
     n'existe que sur le hero d'accueil (mesuré : 1 post sur 16). */
  --cb-scrim-knee: .25;              /* décidé — voir ci-dessus ; relevé frame : .2 */
  --cb-scrim-knee-position: 12.5%;   /* mesuré */
  --cb-scrim-end-position: 40%;      /* mesuré 39,90 % — arrondi au dixième le plus proche, l'écart (0,8px sur 821) est sous le pixel */

  /* CTA brochure sur photo — Figma 40000253:1469. Le voile n'est présent
   * qu'au bas du média, derrière le contrôle : il garantit la lisibilité de
   * la recette `--fill-on-dark` sans assombrir la photographie entière. */
  --cb-brochure-overlay-start: 65.385%;
  --cb-brochure-overlay-end: 93.269%;
  --cb-brochure-overlay-strength: .4;

  /* ====================================================================
   * MÉGA-MENU DESKTOP — frame Figma 40000272:1939 (DS46b)
   *
   * Ces valeurs sont relevées sur la frame (`get_design_context` sur
   * `40000272:2013`), pas à l'œil, et elles ne servent QU'aux 4 panneaux du
   * méga-menu : c'est pourquoi elles portent leur propre préfixe au lieu
   * d'élargir une famille existante. Trois d'entre elles retombent
   * exactement sur l'échelle du système (gouttière 12 = `--cb-space-sm`,
   * rayon 4 = `--cb-radius-sm`, texte 16 = `--cb-text-base`) : elles ne sont
   * donc PAS redéclarées ici, le pont les consomme directement.
   *
   * ⚠️ `--cb-megamenu-voile-*` sont des OPACITÉS, appliquées à
   * `--cb-scrim-color` (noir pur, hors palette et assumé comme densité —
   * voir son propre commentaire). Le voile est en deux couches, comme sur la
   * frame : un aplat sur toute la carte PLUS un dégradé qui se densifie sous
   * le texte. C'est ce qui distingue cette proposition de l'atome
   * `card/overlay` retiré par `DS37` : celui-là n'avait qu'un aplat, et il
   * échouait le contraste sur les vraies photos. La réserve que `DS37`
   * laissait (« refaire la mesure contre les photos de ce besoin-là ») n'est
   * levée POUR CE COMPOSANT que tant que la mesure ci-dessous tient — elle se
   * refait à chaque fois que l'aplat bouge.
   *
   * 🚨 LES DEUX APLATS ONT ÉTÉ ÉCLAIRCIS LE 2026-08-17, sur les frames
   * `40000276:2360` (repos) et `40000276:2370` (survol), demandées par
   * Alexandre : « je veux éclaircir les cards du dropdown ». L'aplat passe de
   * 40 % à **20 %** au repos et le survol **ÉCLAIRCIT** désormais à **10 %**
   * au lieu de densifier à 52 %. Le dégradé de pied, lui, ne bouge pas : c'est
   * lui qui porte la lisibilité, l'aplat ne porte que l'ambiance. Les deux
   * frames sont identiques à l'aplat près — vérifié, pas déduit.
   * ⚠️ Le sens du survol s'inverse donc par rapport au 2026-08-17 matin
   * (« au survol on assombrit juste un peu plus l'overlay ») : c'est une
   * décision d'Alexandre qui remplace la précédente, pas une contradiction
   * qu'on aurait laissée passer. Conséquence assumée : le survol n'est plus
   * l'état le PLUS lisible de la carte, il est le moins lisible des deux.
   * C'est l'état survol qui devient le plancher de contraste à surveiller.
   *
   * Mesuré sur les 26 boîtes des 4 panneaux après éclaircissement, au pire
   * pixel, fond seul : repos **5,90:1**, survol **5,01:1**, 0 boîte sous
   * 4,5:1. L'éclaircissement ne coûte donc rien à la lisibilité — mais il ne
   * l'a pas fait tout seul : il a fallu raccourcir la rampe du dégradé de pied
   * (`--cb-megamenu-voile-bas-fin`, voir son commentaire, c'est là qu'est la
   * mesure qui a forcé la main).
   *
   * ⚠️ `--cb-megamenu-decalage` n'est pas une marge de composition, c'est une
   * correction de fait : ElementsKit ouvre le panneau à `y = 121` pour un
   * header qui finit à 134 (mesuré au survol réel — `docs/pieges.md` § 19).
   * Ses 13 premiers pixels passent derrière la barre. Invisible tant que le
   * panneau n'a pas de fond ; avec un sol, cette bande teinterait le header.
   * Ce token a DEUX consommateurs, et c'est volontaire : le raccord du panneau
   * (§ 13.1 du pont) et la position du filet sous l'item de nav (§ 13.7). Les
   * deux décrivent la même distance — le bas de l'item au bas du header — donc
   * elle est mesurée une fois. Le filet est ainsi l'arête haute du panneau
   * ouvert, par construction et non par réglage tenu à la main.
   * 🚨 ET IL SE POSE EN `padding`, JAMAIS EN `margin` : une marge décollerait
   * la boîte du panneau de l'item, et le survol se perdrait dans l'intervalle —
   * méga-menu inatteignable. Régression payée le 2026-08-17, voir § 13.1.
   * ================================================================== */
  --cb-megamenu-fond: .1;             /* verre : opacité de l'ivoire posé sur le panneau — frame (rgba(255,255,255,.1), ramené à l'ivoire de la palette) */
  --cb-megamenu-flou: 20px;           /* frame : backdrop-blur(20px) */
  --cb-megamenu-carte-hauteur: 310px; /* frame : cartes 344,5 × 310 (aujourd'hui 350) */
  --cb-megamenu-carte-inset: 24px 32px; /* frame : padding 24 vertical / 32 horizontal du bloc de texte (aujourd'hui 35 partout) */
  --cb-megamenu-carte-gap: 10px;      /* frame : gap titre → description */
  --cb-megamenu-voile-repos: .2;      /* frame 40000276:2360 : aplat noir 20 % sur toute la carte (40 % jusqu'au 2026-08-17) */
  --cb-megamenu-voile-bas: .6;        /* frame : le dégradé monte à 60 % en pied de carte */
  --cb-megamenu-voile-bas-depart: 50%;   /* frame : le dégradé démarre à mi-hauteur */
  /* 🚨 78 % ET NON LES 93,269 % DE LA FRAME, et c'est le seul écart assumé de
     la passe d'éclaircissement — il n'était pas demandé, il est SUBI. Mesuré
     sur les 26 boîtes des 4 panneaux : avec l'aplat descendu à 20 %, la frame
     telle quelle fait tomber **9 titres sur 26 sous 4,5:1 au repos et 3 sur 4
     au survol** (pire pixel 3,52 puis 2,84). Aucune description ne tombe.
     La cause est géométrique et se lit d'une ligne : le bloc de texte occupe
     73,9 → 92,3 % de la carte, identique sur les 26. À 73,9 % — le HAUT du
     titre — un dégradé qui finit à 93,269 % n'a monté qu'à 0,331 ; à 84,8 %
     — le haut de la description — il est déjà à 0,483. C'est cet écart de
     0,15 que l'aplat à 40 % payait, et que l'aplat à 20 % ne paie plus.
     Raccourcir la RAMPE règle ça sans toucher à ce que la frame dessine
     vraiment : même départ (50 %), même maximum (60 %), le dégradé atteint
     simplement son plein juste avant le titre au lieu de finir sa montée
     derrière lui. Sous 78 % c'est donc un socle plein, pas une rampe.
     Vérifié : repos 5,90:1 au pire pixel (le servi d'avant faisait 5,64),
     survol 5,01:1, 0 boîte sous 4,5:1 sur les 26 et sur les 4 survols.
     ⚠️ Ce token dépend de la position du texte dans la carte : il se remesure
     si `--cb-megamenu-carte-hauteur` ou `--cb-megamenu-carte-inset` bougent. */
  --cb-megamenu-voile-bas-fin: 78%;
  --cb-megamenu-voile-survol: .1;     /* frame 40000276:2370 : le survol ÉCLAIRCIT l'aplat, 20 % → 10 % (il le densifiait à 52 % jusqu'au 2026-08-17). L'affordance est un dévoilement de la photo, plus un assombrissement */
  /* DS46 — le voile du TIROIR mobile (< 1025px), pas celui du panneau desktop.
     Valeur relevée sur le rendu servi avant la passe (`#1B1812` à 50 % en
     `multiply`) et transférée telle quelle, à la couleur près : le legacy
     employait une couleur hors palette, on garde sa DENSITÉ et on prend le
     noir du système. Le desktop, lui, superpose DEUX couches
     (`--cb-megamenu-voile-repos` + `--cb-megamenu-voile-bas`) : le tiroir n'en
     a qu'une, parce que ses cartes font 100px de haut et qu'un dégradé de pied
     n'y aurait aucune place pour s'installer. */
  --cb-megamenu-tiroir-voile: .5;

  --cb-megamenu-decalage: 13px;       /* mesuré : panneau ouvert à y=121, bas du header à 134 */
  /* Durée du rideau qui remplace le fondu (§ 13.1ter du pont). Reprend la
     durée que le panneau avait DÉJÀ : `transition-duration: 0.4s` mesuré sur
     `.elementskit-megamenu-panel`, posé par ElementsKit et par le snippet 18.
     Ce n'est donc pas un tempo neuf — c'est le même mouvement, révélé
     autrement. Volontairement distinct de `--cb-interaction-duration-base`
     (250ms), qui est le tempo d'un survol de bouton, pas celui de l'ouverture
     d'un panneau plein écran. */
  --cb-megamenu-revelation: 400ms;
  /* Le frémissement de la photo au survol (§ 13.5 du pont). Demandé par
     Alexandre le 2026-08-17 — « vraiment léger et snappy ». 1,03 : sur une
     carte de 345px de large, le débord vaut ~5px de chaque côté, assez pour
     que l'œil sente la photo respirer, trop peu pour qu'il lise un zoom.
     ⚠️ À ne pas confondre avec le `scale(1.2)` d'Elementor Pro qu'on a
     neutralisé : 7 fois plus grand et toujours plus lent (1 500 ms). Ce token
     existe précisément pour que la nuance soit écrite quelque part.
     L'AMPLEUR ne bouge pas — « garde le zoom », 2026-08-17 après-midi. */
  --cb-megamenu-zoom-survol: 1.03;
  /* Sa DURÉE, elle, bouge : « ralentis-le » (Alexandre, 2026-08-17). Le
     `--cb-interaction-duration-base` de 250 ms est le tempo d'un survol de
     BOUTON — un changement d'état qu'on veut immédiat. Une photo qui respire
     n'est pas un changement d'état : à 250 ms le mouvement est fini avant que
     l'œil l'ait suivi, ce qui le fait lire comme un saut plutôt que comme une
     dérive. 700 ms, c'est assez long pour que le déplacement soit CONTINU sous
     le regard (les ~5px de débord se font en presque une seconde) et assez
     court pour que le retour au repos ne traîne pas quand on survole les 4
     cartes à la file.
     ⚠️ (parti pris) Alexandre a dit « ralentis », pas un chiffre : 700 ms est
     un choix, à confirmer à la revue. Volontairement pas mutualisé avec
     `--cb-megamenu-revelation` (400 ms) : celle-là est l'ouverture du panneau,
     un autre geste, et les aligner ferait démarrer les deux mouvements
     ensemble sans que rien ne le demande. */
  --cb-megamenu-zoom-duree: 700ms;

  /* ====================================================================
   * RETRAIT DE PAGE, puis les valeurs propres au header (H01, DS66)
   *
   * `--cb-page-inset` — le retrait OPTIQUE du bord de l'écran, 32px, mesuré
   * sur la frame du header (le logo commence à x=32). ⚠️ Il s'appelait
   * `--cb-header-inset` jusqu'au 2026-08-17 et il ne servait qu'au header :
   * les sections, elles, se retiraient de 64 ou 80px. Alexandre a demandé
   * l'alignement STRICT du contenu des sections sur celui du header
   * (DS66) — donc une seule valeur pour les deux, et le nom le dit.
   * L'alignement n'est plus le résultat de deux réglages qu'on maintient
   * égaux à la main : il est exact par construction, à toutes les largeurs.
   *
   * Il est le même sur les deux rangées du header, mais obtenu
   * différemment, et c'est ce qui a failli le faire passer pour deux
   * valeurs : la rangée 2 le porte en padding (le logo commence à x=32),
   * tandis que la rangée 1 n'en porte que 16 — parce que ses items sont des
   * boutons fantômes qui ajoutent leurs propres 16px de padding
   * (« Français » commence à x=16+16=32). Les atomes dérivent donc le
   * padding de la rangée 1 par `calc()` au lieu de déclarer un second token.
   * ⚠️ Conséquence assumée et visible : le bouton « RÉSERVER », lui, a un
   * fond opaque, donc son bord EST son bord optique — il tombe à 16px du
   * bord droit, pas 32. La frame est asymétrique, on la reproduit telle
   * quelle.
   *
   * ⚠️ Ne pas confondre avec `--cb-space-inset-*` (32/64px) : celui-là est
   * le PADDING INTERNE d'un bloc ou d'une carte. Qu'il vaille aussi 32 en
   * mobile est une coïncidence de valeur, pas de rôle — c'est d'ailleurs
   * pour ça que le mobile était déjà aligné et que personne ne voyait rien.
   *
   * `--cb-header-gap` — 40px entre le bloc de logos et la nav, mesuré
   * (logos finissent à x=172,671 ; la nav commence à x=212,671). Ni 32
   * (`--cb-flow-group`) ni 48 (`--cb-space-gap-desktop`) : une troisième
   * valeur, qu'on ne fait pas semblant d'arrondir sur un palier existant.
   * ================================================================== */
  --cb-page-inset: 32px;
  /* ⚠️ ALIAS DE COMPATIBILITÉ, pas un second token. Le snippet serveur
     `38-aggregateur-avis-google.php` (badge d'avis calé sur le 4ᵉ coin du
     hero, `tickets/journal/P03d.md`) écrit `var(--cb-header-inset, 32px)` :
     sans cette ligne il ne CASSERAIT pas — son repli vaut 32px, la même
     valeur — mais il cesserait de suivre le système, et le jour où le
     retrait de page bouge, le badge resterait seul en arrière sans que rien
     ne le signale. Le retirer suppose de reprendre le snippet côté serveur
     (procédure `docs/prod.md` § 16) : c'est une écriture serveur, elle
     n'appartient pas à un ticket de CSS. */
  --cb-header-inset: var(--cb-page-inset);
  --cb-header-gap: 40px;
  --cb-header-chevron-size: 8px; /* glyphe ElementsKit relevé sur les entrées de navigation ; le sélecteur WPML emploie le même format. Son rayon reste le token global --cb-radius-sm. */

  /* ====================================================================
   * INTERACTION — hover, focus, actif, désactivé (DS11)
   * Q08 tranchée (TODO.md, 2026-08-09) : sobre, élégant, micro-interaction —
   * micro-mouvements autorisés, JAMAIS de scale/zoom sur un composant entier.
   * C'est le pain point d'origine du projet : docs/prod.md § 4.5 a identifié
   * `hover_animation: "shrink"`, posé sur les boutons de 20 pages en prod,
   * qui applique `transform: scale(0.9)` au bouton ENTIER au survol — c'est
   * précisément ce que ce système remplace.
   *
   * Deux durées seulement, chacune avec un consommateur réel dans atoms.css
   * (pas une échelle "au cas où", cf. AGENTS.md). Une seule courbe : le
   * brief demande une sobriété qui exclut plusieurs "signatures" de
   * mouvement différentes sur un même site.
   * ================================================================== */
  /* --cb-interaction-duration-fast : RETIRÉ le 2026-08-12 avec le déplacement d'icône, son unique
     consommateur. Un token sans consommateur est précisément ce qu'AGENTS.md refuse. */
  --cb-interaction-duration-base: 250ms;   /* changement de couleur/fond/bordure au survol — reprend la valeur déjà en usage dans design-system/legacy/camboyer-design-system.css (`.cb-btn .elementor-button`, `.25s ease`), pas une nouvelle valeur inventée */
  --cb-interaction-ease: cubic-bezier(.4, 0, .2, 1); /* courbe "standard" (accélère puis décélère), sans rebond ni overshoot — seule courbe du système, utilisée par les deux durées ci-dessus, pour que tout accélère/décélère pareil sur le site */
  --cb-interaction-duration-reduced: .01ms; /* prefers-reduced-motion (non négociable, cf. atoms.css) : quasi-zéro plutôt que 0 — laisse le navigateur déclencher `transitionend`, idiome connu, pas une valeur choisie au hasard */

  /* --cb-interaction-icon-shift : RETIRÉ le 2026-08-12, décision d'Alexandre. NE PAS LE RECRÉER.
     « ce truc de mouvement d'icône au hover des boutons on peut le wipe complètement, ça n'a pas de
     sens. » Vu sur la carte du hero : le chevron d'un item de nav se déplaçait au survol, ce qui
     attire l'œil sur le pictogramme au lieu du libellé. Le token venait de `design-system/legacy`
     (`.cb-btn .elementor-button:hover svg{transform:translateX(2px)}`) — repris parce qu'il
     existait, jamais parce qu'un besoin l'avait demandé. Le survol reste un changement de couleur,
     et rien d'autre : c'est ce que « sobre, élégant » (Q08) voulait dire depuis le début. */
  --cb-interaction-active-dim: .92;        /* (parti pris) filter:brightness() de l'état pressé — un assombrissement, pas un déplacement ni une échelle du composant */
  --cb-interaction-disabled-opacity: .5;   /* (parti pris) opacité de l'état désactivé */

  --cb-interaction-focus-ring-width: 2px;
  --cb-interaction-focus-ring-offset: 2px; /* la couleur de l'anneau réutilise --cb-cta (pas un nouveau token couleur) — déjà validé AA à 5,17:1 sur --cb-ivoire, contraste non-texte largement au-dessus du seuil 3:1 (WCAG 2.4.11) */

}
