/**
 * Camboyer — atomes (couche 2 du design system)
 *
 * Ce fichier ne contient QUE des classes `.cb-*` sur du markup SÉMANTIQUE
 * NEUTRE — c'est la définition de la couche 2 dans `design-system/AGENTS.md`.
 * Zéro connaissance de l'outil d'édition (Elementor) ici — AGENTS.md le dit
 * tel quel : un sélecteur qui nommerait le DOM de cet outil dans ce fichier
 * serait une erreur. Sa traduction vit dans le fichier « pont » de couche 3
 * (pas encore construit — lié à `H01`, qui doit relever ce DOM sur le
 * serveur avant qu'on l'écrive).
 *
 * Ticket : DS14 — les tout premiers atomes du système. Trois atomes, tous
 * des compositions titre Fonseca + accent DianaWebber Script, spécifiées
 * dans `docs/lockup-titres.md` (relevé Figma, ticket DS03b) :
 *   - `.cb-title-lockup` (+ `--left` / `--center`) → title/lockup-left,
 *     title/lockup-center — titre 2 lignes, script juste dessous, sans gap.
 *   - `.cb-title-inline` → title/inline-accent — titre + script CÔTE À CÔTE,
 *     gap explicite.
 *
 * Convention posée ici pour la suite du système (BEM `.cb-<bloc>` /
 * `.cb-<bloc>__<élément>` / `.cb-<bloc>--<variante>`), reprise de l'usage
 * déjà en place dans `legacy/` (`.cb-script--lg`, `.cb-script--on-dark`) :
 * le nom du bloc CSS reprend le premier segment de l'identifiant affiché sur
 * la page de revue (`title/…` → `.cb-title-…`), la variante le second
 * segment quand il en distingue plusieurs.
 *
 * Chaque valeur ci-dessous cite sa source en commentaire de fin de ligne —
 * jamais recopiée à l'œil. Une valeur qui ne se rattache à aucun token
 * `--cb-*` est marquée `(à arbitrer)` : c'est le cas des deux tailles de
 * titre (36px, 48px), absentes de l'échelle `--cb-text-*` (voir le rapport
 * du ticket DS14 pour la question à trancher).
 */

/* =====================================================================
 * .cb-title-lockup — title/lockup-left, title/lockup-center
 * Titre Fonseca (2 lignes) + script DianaWebber juste dessous, SANS gap :
 * les deux boîtes se touchent exactement (docs/lockup-titres.md §2.1/§2.2).
 * Le script est plus large que le titre : le débordement horizontal qui en
 * résulte (asymétrique à gauche, symétrique au centre) est une
 * caractéristique VOULUE de la spec, pas un bug — il sort naturellement de
 * `align-items` ci-dessous, aucune largeur n'est figée pour l'obtenir.
 * ================================================================== */
.cb-title-lockup{
  display: flex;
  flex-direction: column;
  gap: var(--cb-flow-lockup); /* 0 mesuré : relation explicite partagée, docs/lockup-titres.md §2.1/§2.2 — DS62 */
}
.cb-title-lockup--left{
  --cb-lockup-title-size: var(--cb-text-lockup-compact);
  --cb-lockup-script-ratio: var(--cb-ratio-lockup-script-compact);
  align-items: flex-start;
  text-align: left;
}
.cb-title-lockup--center{
  --cb-lockup-title-size: var(--cb-text-lockup-display);
  --cb-lockup-script-ratio: var(--cb-ratio-lockup-script-display);
  align-items: center;
  text-align: center;
}

.cb-title-lockup__title{
  margin: 0;
  font-family: var(--cb-font-caps);
  font-weight: var(--cb-weight-title); /* [Q27 : 200 — tout le titrage, décidé le 2026-08-12] Fonseca **Thin** servie ici, là où le relevé dit Light 3/3 sur les lockups (docs/lockup-titres.md) : écart DÉCIDÉ, pas subi. Réserve de rendu : voir design-system/index.html, section Atomes */
  font-size: var(--cb-lockup-title-size);
  line-height: 1.5; /* docs/lockup-titres.md §2.1 et §2.2, identique dans les deux */
  color: var(--cb-lockup-titre);
}

.cb-title-lockup__script{
  margin: 0;
  font-family: var(--cb-font-script);
  font-weight: 400; /* seule graisse existante de DianaWebber Script */
  font-size: calc(var(--cb-lockup-title-size) * var(--cb-lockup-script-ratio));
  line-height: 1; /* leading-none, docs/lockup-titres.md §2.1/§2.2 */
  color: var(--cb-terracotta); /* accent EMPILÉ = terracotta, jamais autre chose — docs/lockup-titres.md §3 */
}

/* DS12 — envisagé un `.cb-title-lockup--on-dark` pour card/overlay (les 4
   cartes-hébergement homepage sont bien un uael-advanced-heading, DS26 +
   reference/cartographie-homepage.txt conteneur 12c5a5ce "Charme"). PAS
   ajouté : aucun texte de SCRIPT sourcé ne confirme qu'une de ces 4 cartes
   porte réellement un sous-titre DianaWebber (le relevé confirme le widget
   et le titre, jamais le contenu du script pour cette occurrence précise) —
   fabriquer un script de démo aurait été inventer un contenu. La carte
   d'overlay compose `.cb-heading--on-dark` (titre seul, ci-dessous) à la
   place. Voir tickets/journal/DS12.md. */

/* =====================================================================
 * .cb-title-inline — title/inline-accent
 * Titre Fonseca + mot-accent script CÔTE À CÔTE, gap explicite 24px —
 * contrairement à `.cb-title-lockup`, jamais de terracotta ici : le script
 * reprend la couleur du titre à 80 % d'opacité (docs/lockup-titres.md
 * §2.3/§3, charte.md §3.3 complétée en conséquence par ce ticket).
 *
 * La gravure et la ligne de tags visibles dans la maquette relevée (§2.3)
 * ne font PAS partie de cet atome : hors périmètre typographique de ce
 * ticket (image → DS08, tags → DS07) — et aucun asset gravure vérifié ne
 * correspond au nœud Figma (`assets/CATALOG.md` ne couvre pas les gravures ;
 * les fichiers disponibles sont `clef-1-A.png`/`clef-2-A.png`, aucun ne
 * s'appelle "clef 3 A"). Ne pas ajouter une image non vérifiée pour "faire
 * complet".
 * ================================================================== */
.cb-title-inline{
  --cb-lockup-title-size: var(--cb-text-lockup-display);
  --cb-lockup-script-ratio: var(--cb-ratio-lockup-script-display);
  display: flex;
  align-items: center; /* centrage de boîte, pas calage sur la ligne de base — docs/lockup-titres.md §2.3 */
  gap: var(--cb-space-gap-mobile); /* 24px, confirmé = --cb-space-gap-mobile — docs/lockup-titres.md §2.3 et §6 pt.7 */
}
/* DS29 — sous viewport_md (768px, docs/spacing.md §1.2, seuil déjà établi par
   le système, pas choisi ici), le mot-accent sortait ENTIÈREMENT du cadre :
   mesuré par DS02d (conteneur 298px, composition 500px nécessaires, "Charme"
   positionné 304→501px, donc scriptVisible:false — perte de contenu, pas un
   défaut esthétique). Correction retenue : wrap contrôlé — empiler titre puis
   script, même stratégie que `.cb-title-lockup` — plutôt qu'une taille de
   script réduite, qui créerait un troisième rapport hors contrat DS32 et
   dégraderait la lisibilité du script au moment où l'écran est le plus petit.
   align-items:flex-start (pas de variante --left/--center pour cet atome) :
   la seule maquette relevée (§2.3) ne documente aucun alignement, et l'emploi
   constaté (fiche produit) est un bloc de texte, pas un titre de page centré. */
@media (max-width: 767px){
  .cb-title-inline{
    flex-direction: column;
    align-items: flex-start;
  }
}
.cb-title-inline__title{
  margin: 0;
  font-family: var(--cb-font-caps);
  font-weight: var(--cb-weight-title); /* [Q27 : 200 — tout le titrage, décidé le 2026-08-12] Fonseca Thin, même écart assumé et même réserve de rendu que .cb-title-lockup__title */
  font-size: var(--cb-lockup-title-size);
  line-height: 1.5;
  color: var(--cb-lockup-titre);
}
.cb-title-inline__script{
  margin: 0;
  font-family: var(--cb-font-script);
  font-weight: 400;
  font-size: calc(var(--cb-lockup-title-size) * var(--cb-lockup-script-ratio));
  line-height: 1;
  /* accent EN LIGNE = couleur du titre à 80 %, jamais terracotta — dérivée de
     --cb-lockup-titre via color-mix() plutôt que de recopier le rgba(24,28,24,.8)
     de la maquette : une seule source pour la couleur, l'opacité est la seule
     valeur propre à cette variante (docs/lockup-titres.md §2.3/§3). */
  color: color-mix(in srgb, var(--cb-lockup-titre) 80%, transparent);
}

/* =====================================================================
 * .cb-title-accent — title/accent-italic (DS27)
 *
 * La composition du HERO, relevée sur la frame validée (tickets/journal/
 * H01a-releve.md § 5) : « hôtel & Événements » en Fonseca Light, suivi SUR
 * LA MÊME LIGNE de « d'Exception » en Addington CF Regular Italic, **même
 * couleur pleine** que le titre (pas de terracotta, pas de 80 %), le tout
 * aligné à droite sur le fond sombre de la vidéo.
 *
 * POURQUOI UN TROISIÈME ATOME DE TITRE, et pas une variante des deux autres.
 * Aucun des deux ne couvre ce cas, et les trois écarts se cumulent :
 *   - `.cb-title-lockup`  : accent EMPILÉ, en DianaWebber, TOUJOURS terracotta ;
 *   - `.cb-title-inline`  : accent côte à côte, en DianaWebber, couleur du
 *                           titre à 80 % — et surtout deux BOÎTES flex ;
 *   - ici                 : accent dans le FLUX du texte, en Addington CF
 *                           italique, couleur du titre à 100 %.
 * En faire un modificateur de `.cb-title-inline` aurait obligé à rendre
 * optionnelles la famille, la couleur ET la structure — c'est-à-dire à ne
 * plus rien partager qu'un nom.
 *
 * CE QUI EST STRUCTUREL ICI, et c'est le point : ce n'est PAS un flex de
 * deux boîtes, c'est UN SEUL texte avec un changement de style de caractère
 * à l'intérieur. Le relevé Figma est formel — un seul nœud texte
 * (`40000003:17426`), pas deux widgets adjacents — et le widget réel de la
 * prod (`cfa85f6`, un `heading` Elementor) porte lui aussi une seule chaîne.
 * Conséquence pratique : le mot-accent enroule avec le reste de la phrase,
 * il ne peut pas sortir du cadre en étroit — le défaut que DS29 a dû
 * corriger sur `.cb-title-inline` ne peut pas se produire ici, sans qu'aucun
 * point de rupture n'ait à être décidé (Q20 ne s'applique donc pas à cet
 * atome).
 *
 * LA COULEUR EST DÉCLARÉE UNE SEULE FOIS, sur la composition. Le mot-accent
 * n'en déclare aucune : il HÉRITE. C'est la traduction littérale de la
 * spécification (« même couleur que le titre ») — la recopier sur l'accent
 * en ferait deux valeurs à maintenir, donc deux valeurs à faire diverger.
 *
 * LA TAILLE DE L'ACCENT EST UN RAPPORT, pas un palier : `1em` × le token
 * `--cb-ratio-accent-italic` (1,25 — mesuré 40/32 sur la frame). L'accent
 * suit donc automatiquement la taille du titre, quelle qu'elle devienne
 * quand Q13/DS32 trancheront les tailles absolues.
 * ================================================================== */
.cb-title-accent{
  margin: 0;
  font-family: var(--cb-font-caps);
  font-weight: var(--cb-weight-title); /* [Q27 : 200 — tout le titrage, décidé le 2026-08-12] Fonseca Thin, même graisse que les 2 autres titres de lockup. Une face réellement servie depuis S08 (avant, elle retombait en Regular 400 sans le dire) — à revérifier au CSS servi, c'est le piège de DS00b. */
  font-size: var(--cb-text-title-hero); /* (à arbitrer) — 32px mesuré sur la frame validée du hero. H01 : le littéral passe en token, PARTAGÉ avec .cb-heading--h1 parce que la frame met les deux titres du hero à la même taille (voir tokens.css). Rejoint le périmètre de DS32. */
  line-height: 1;   /* CORRIGÉ par H01. DS27 avait posé 1.5 « aligné sur les 2 autres lockups », en notant que « la frame ne documente aucun interligne » — c'était vrai de la frame qu'il avait lue. La frame du hero reworké (`40000182:18462`), elle, le documente : `lineHeight` = 100 % sur les DEUX segments (Fonseca 32px et Addington 40px). Ce n'est donc plus un alignement par défaut, c'est une mesure — et elle serre volontairement les deux lignes du titre. */
  color: var(--cb-lockup-titre);
}
/* alignement à droite : la seule occurrence relevée (le hero) est alignée à
   droite, mais l'alignement est une propriété de POSE, pas de composition —
   il se demande, il ne s'impose pas. Même parti que --left/--center sur
   `.cb-title-lockup`. */
.cb-title-accent--right{ text-align: right; }

/* fond sombre : même mécanisme que `.cb-heading--on-dark` (DS12) — un texte
   posé DIRECTEMENT sur une image ou une vidéo, hors de toute section. Dans
   une `.cb-section--dark`, ne PAS poser ce modificateur : la section corrige
   déjà ses enfants (règle plus bas), et le doubler serait le doublon que
   `master-instructions.md` proscrit. Consommateur réel et prévu : le hero
   sur la vidéo (H01c). */
.cb-title-accent--on-dark{ color: var(--cb-ivoire); }

.cb-title-accent__mot{
  font-family: var(--cb-font-accent);
  font-style: italic;  /* Q18/Q22 — la voix d'accent EST l'italique ; Addington CF droite n'est pas ce qui a été décidé */
  font-weight: 400;    /* Addington CF Regular Italic, relevé sur la frame (H01a § 5) — la famille en sert 3 graisses italiques, c'est bien la Regular */
  font-size: calc(1em * var(--cb-ratio-accent-italic));
  /* AUCUNE couleur : elle hérite du titre. C'est la spécification, pas un oubli. */
}

/* =====================================================================
 * .cb-interactive — système de hover / focus / actif / désactivé (DS11)
 *
 * PAS un atome bouton. `.cb-interactive` ne pose ni padding de marque au-delà
 * de ce que DS01 a déjà réservé pour "bouton" dans tokens.css, ni typo, ni
 * la forme finale d'un composant : ça viendra avec l'atome qui l'emploiera
 * (le futur DS-boutons, un lien, une carte…). Ce bloc pose UNIQUEMENT le
 * système d'état — transition, focus-visible, actif, désactivé, icône —
 * qu'un futur atome composera avec ses propres classes.
 *
 * Trois "recettes" de survol, sourcées sur l'usage déjà vérifié en prod/
 * legacy (design-system/legacy/camboyer-design-system.css § .cb-btn) — pas
 * inventées :
 *   - `--tone`         : le fond bascule d'une teinte à une autre (ex.
 *     `.cb-btn--primary`, cta → encre).
 *   - `--fill`         : le fond, transparent au repos, se remplit au
 *     survol (ex. `.cb-btn--outline`, transparent → encre).
 *   - `--fill-on-dark` : même recette, EXCEPTION sur fond sombre — le
 *     remplissage passe à l'ivoire à faible opacité plutôt qu'à l'encre,
 *     sinon le survol devient illisible sur un fond déjà sombre (ex.
 *     `.cb-btn--outline-dark`).
 * C'est la règle générale (le fond ou la bordure change de teinte) + ses
 * deux exceptions autorisées — voir tickets/journal/DS11.md.
 *
 * Interdit absolu du projet, respecté ici à la lettre : AUCUNE transformation
 * d'échelle, AUCUN zoom sur `.cb-interactive` lui-même (master-instructions.md,
 * AGENTS.md, docs/charte.md, docs/prod.md § 4.5). ⚠️ Depuis le 2026-08-12 la
 * règle est PLUS STRICTE que ça : il n'y a plus aucun mouvement du tout au
 * survol, pas même sur une icône enfant — le micro-déplacement de 2px a été
 * retiré sur décision d'Alexandre. Un survol change des couleurs.
 *
 * `[data-demo-state="…"]` : hameçon de démo pour la page de revue UNIQUEMENT
 * — posé sur les MÊMES sélecteurs que les vraies pseudo-classes (jamais une
 * règle dupliquée ailleurs), pour qu'un état puisse être forcé et prouvé par
 * une capture statique sans jamais risquer de diverger de ce que fait
 * réellement `:hover`/`:focus-visible`/`:active`/`:disabled` dans un
 * navigateur. Voir build-review.py, section « Interaction ».
 * ================================================================== */
.cb-interactive{
  display: inline-flex;
  align-items: center;
  gap: var(--cb-space-sm); /* "gap icône/texte", tokens.css */
  padding: var(--cb-space-sm) var(--cb-space-md); /* paliers déjà réservés "bouton" par DS01 */
  border: 1px solid transparent;
  border-radius: var(--cb-radius-sm);
  cursor: pointer;
  font: inherit;
  transition:
    background-color var(--cb-interaction-duration-base) var(--cb-interaction-ease),
    border-color var(--cb-interaction-duration-base) var(--cb-interaction-ease),
    color var(--cb-interaction-duration-base) var(--cb-interaction-ease);
}

/* focus-visible : jamais optionnel — accessibilité, master-instructions.md. Un ring
   visible, réutilise --cb-cta plutôt qu'une nouvelle couleur de marque. */
.cb-interactive:focus-visible,
.cb-interactive[data-demo-state="focus-visible"]{
  outline: var(--cb-interaction-focus-ring-width) solid var(--cb-cta);
  outline-offset: var(--cb-interaction-focus-ring-offset);
}

/* actif/pressé : un assombrissement (filter), jamais un déplacement ni une échelle du
   composant entier — c'est la limite explicite et non négociable du projet. */
.cb-interactive:active,
.cb-interactive[data-demo-state="active"]{
  filter: brightness(var(--cb-interaction-active-dim));
}

/* désactivé : `:disabled` couvre les éléments de formulaire réels (`<button>`),
   `[aria-disabled]` les cas où `.cb-interactive` est posé sur un élément qui n'a pas cet
   état natif (ex. un futur lien stylé comme un bouton). */
.cb-interactive:disabled,
.cb-interactive[aria-disabled="true"],
.cb-interactive[data-demo-state="disabled"]{
  opacity: var(--cb-interaction-disabled-opacity);
  cursor: not-allowed;
  pointer-events: none;
}

.cb-interactive--tone{
  background: var(--cb-cta);
  border-color: var(--cb-cta);
  color: var(--cb-ivoire);
}
/* DS63 — le primaire s'APPROFONDIT, il ne change pas de teinte.
   Était `--cb-encre`, c'est-à-dire la destination de survol de `--fill` : les
   deux recettes devenaient indiscernables au survol. Le primaire garde
   désormais sa couleur de marque et gagne en lisibilité (6,12:1 contre 5,17
   au repos). Le secondaire reste seul à atterrir sur l'encre — c'est son
   identité, l'inversion contour → aplat. */
.cb-interactive--tone:hover,
.cb-interactive--tone[data-demo-state="hover"]{
  background: var(--cb-cta-hover);
  border-color: var(--cb-cta-hover);
}

.cb-interactive--fill{
  background: transparent;
  border-color: var(--cb-encre);
  color: var(--cb-encre);
}
.cb-interactive--fill:hover,
.cb-interactive--fill[data-demo-state="hover"]{
  background: var(--cb-encre);
  color: var(--cb-ivoire);
}

/* ---- --solid-light (DS28) — la 4ᵉ recette : un APLAT CLAIR OPAQUE, posé sur
 * un fond qu'on ne maîtrise pas (vidéo du hero, photo). Les 3 recettes de
 * DS11 ne la couvraient pas : `--tone` est le CTA (terracotta), `--fill` et
 * `--fill-on-dark` sont contourées et laissent voir le fond au travers.
 *
 * Relevée sur les 2 boutons du header (H01a), mesurée au rendu le 2026-08-11 :
 * fond `#FFFDF0` = `--cb-ivoire` EXACT, texte et icône `#4B6146` = `--cb-olive`
 * EXACT. Aucune couleur neuve : la recette n'ajoute qu'une combinaison.
 *
 * POURQUOI CETTE RECETTE TIENT SUR UNE VIDÉO, et c'est tout son intérêt : le
 * fond est OPAQUE, donc le contraste du texte (6,64:1, calculé) ne dépend pas
 * une seconde de ce qui passe derrière. Même argument que `--tone` (DS17).
 *
 * ⚠️ CE QUI NE TIENT PAS, mesuré : le BORD. `--cb-ivoire` contre une frame
 * vidéo blanche donne **1,02:1** — le bouton disparaît dans l'image. Contre
 * une frame noire, 20,56:1. La prod ne règle pas le problème : son `border`
 * de 2px est de la MÊME couleur que le fond, il ne définit aucun contour. Le
 * texte reste lisible, mais la BOÎTE peut s'évanouir. Non tranché ici (aucune
 * source ne dit ce que fait le bouton sur une frame claire, et inventer un
 * contour serait une décision de design non sourcée) → `(à arbitrer)`, `DS50`.
 *
 * ⚠️ ET CE QUE LE RELEVÉ APPREND À `H01b` : en prod, cet aplat clair n'est
 * PAS un réglage du widget. Le widget est réglé en `rgba(255,253,240,0)`,
 * donc TRANSPARENT ; l'ivoire est forcé en `!important` par le snippet
 * `header-scroll-effect`, et seulement tant que le header n'est pas en mode
 * sticky (`:not(.elementor-sticky--effects)`). C'est un ÉTAT, pas un style.
 * L'atome, lui, décrit l'état au repos — `H01b` (qui édite le snippet, Q19)
 * décidera de ce qui se passe au défilement.
 *
 * Survol : inversion de la même paire (fond olive, texte ivoire) — 6,64:1
 * dans les deux sens, et les deux états restent opaques, donc indépendants du
 * fond. `(parti pris)` : le survol réel de la prod est piloté par le même
 * snippet et sera réécrit par `H01b` ; l'inversion est la réponse la plus
 * sobre et la plus cohérente avec `--fill`, qui inverse déjà. ------------- */
.cb-interactive--solid-light{
  background: var(--cb-ivoire);
  border-color: var(--cb-ivoire);
  color: var(--cb-olive);
}
.cb-interactive--solid-light:hover,
.cb-interactive--solid-light[data-demo-state="hover"]{
  background: var(--cb-olive);
  border-color: var(--cb-olive);
  color: var(--cb-ivoire);
}

.cb-interactive--fill-on-dark{
  background: transparent;
  border-color: var(--cb-ivoire);
  color: var(--cb-ivoire);
}

/* ---- --ghost-on-dark (H01) — la 5ᵉ recette, et l'écart avec la 4ᵉ tient en
 * un mot : PAS DE BORDURE.
 *
 * Pourquoi elle est nécessaire. La frame du hero reworké compte **12 libellés
 * de contrôle** sur fond sombre — « Français », le téléphone, « Contact », et
 * les 7 items de nav — et aucun ne porte de contour. Ils ont pourtant la boîte
 * d'un bouton : 42px de haut, 16px de padding horizontal, un rayon de 4px
 * (lu sur `40000182:18607`, `:18617`, `:18640`…). Ce sont des boutons dont on
 * ne voit que le libellé jusqu'à ce qu'on les survole.
 *
 * `--fill-on-dark` ne pouvait pas servir : il pose `border-color: --cb-ivoire`,
 * donc un contour permanent — 12 rectangles dessinés en travers du header.
 * `.cb-link` ne pouvait pas servir non plus : il ANNULE le box-model (padding 0,
 * pas de rayon), donc plus de surface de survol ni de zone cliquable de 42px.
 *
 * Ce qu'elle ne réinvente pas : la destination du survol est **la même** que
 * `--fill-on-dark` (ivoire à 15 %). Une seule façon de s'allumer sur fond
 * sombre dans tout le système — c'est l'argument de convergence de DS18, tenu
 * ici aussi. La bordure reste déclarée `transparent` (héritée de
 * `.cb-interactive`) et non `none` : la boîte garde ainsi exactement la même
 * hauteur que le « RÉSERVER » bordé posé à côté d'elle. ------------------- */
.cb-interactive--ghost-on-dark{
  background: transparent;
  border-color: transparent;
  color: var(--cb-ivoire);
}
.cb-interactive--ghost-on-dark:hover,
.cb-interactive--ghost-on-dark[data-demo-state="hover"]{
  background: color-mix(in srgb, var(--cb-ivoire) 15%, transparent); /* même destination que --fill-on-dark : une seule façon de s'allumer sur fond sombre */
}
.cb-interactive--fill-on-dark:hover,
.cb-interactive--fill-on-dark[data-demo-state="hover"]{
  /* --cb-ivoire à 15 % plutôt qu'un rgba() recopié de legacy : une seule source pour la
     couleur — même parti pris que .cb-title-inline__script ci-dessus. */
  background: color-mix(in srgb, var(--cb-ivoire) 15%, transparent);
  border-color: var(--cb-ivoire);
}

/* icône enfant — un simple porte-icône, qui n'anime plus RIEN.
   ⚠️ Il déplaçait l'icône de 2px au survol jusqu'au 2026-08-12. Retiré sur
   décision d'Alexandre, après l'avoir vu sur la carte du hero : « ce truc de
   mouvement d'icône au hover des boutons on peut le wipe complètement, ça n'a
   pas de sens. » Le défaut se voyait le mieux sur le chevron d'un item de nav,
   où le mouvement attirait l'œil sur le pictogramme au lieu du libellé.
   Ce n'était pas une idée du projet : le comportement venait de
   `design-system/legacy`, repris parce qu'il existait. Conséquence à assumer et
   à ne pas « re-corriger » : le système n'a PLUS AUCUN mouvement au survol,
   nulle part — un survol change des couleurs, point. C'est la lecture stricte
   de Q08 (« sobre, élégant »), pas un appauvrissement. */
.cb-interactive__icon{
  display: inline-flex;
}
/* L'icône occupe exactement un cadratin, et prend la couleur du texte. Une
   seule règle pour tout le système : bouton, lien, item de nav, bouton à icône
   seule. Elle était écrite en double (sur `.cb-btn--icon` et sur
   `.cb-nav__item`) — deux endroits pour une seule décision.
   `1em` et non une taille absolue : l'icône suit le palier de texte du bouton,
   donc elle reste juste si ce palier change. `currentColor` et non une couleur :
   l'icône suit la recette (--tone, --solid-light, --ghost-on-dark…) sans
   qu'aucune règle de couleur ne soit écrite pour elle.
   ⚠️ Ça suppose que l'icône soit un SVG INLINE. Un SVG posé en `<img src>`
   n'hérite d'aucune couleur — c'est pourquoi les icônes du projet sont inlinées
   (voir assets/icones/README.md). */
.cb-interactive__icon svg,
.cb-interactive__icon img{
  display: block;
  width: 1em;
  height: 1em;
  fill: currentColor;
}

/* =====================================================================
 * .cb-interactive__label / .cb-interactive--phrase — la CASSE DE PHRASE (C01)
 *
 * Demande d'Alexandre, 2026-08-18 : « on va mettre du css pour forcer de passer
 * de "RESERVER VOTRE BLABLA" à "Réserver votre blabla" si possible ». Les 33
 * libellés visés sont en capitales dans leur TEXTE, pas par une règle de style :
 * aucune couche n'y peut rien sans ce modificateur.
 *
 * ⚠️ POURQUOI PAS `text-transform: capitalize` : il donne « Demander Un Devis ».
 * Ce n'est pas du français. La casse de phrase demande donc deux gestes —
 * tout en bas de casse, puis la première lettre seule remontée.
 *
 * ⚠️ POURQUOI UN SPAN, ET PAS LA RÈGLE POSÉE SUR LE PORTEUR : `::first-letter`
 * ne s'applique QU'À UNE BOÎTE DE BLOC (block, inline-block, flow-root…) — ni à
 * un conteneur flex, ni à une boîte anonyme. Or `.cb-interactive` est
 * `inline-flex` (ci-dessus) et le libellé y est un nœud de texte NU, donc un
 * enfant anonyme du flex : il n'y a, en markup neutre, aucun nœud sur lequel la
 * règle puisse se poser. Passer `.cb-interactive` en `inline-block` pour
 * contourner marcherait sur un bouton sans icône et casserait tous les autres —
 * `gap` et `align-items` tombent avec le flex.
 * Ce span n'invente rien : il est le JUMEAU EXACT de ce que rend déjà Elementor
 * (`span.elementor-button-text`), donc la paire neutre/jumeau reste isomorphe et
 * le pont peint le même nœud des deux côtés.
 *
 * ⚠️ Il n'est OBLIGATOIRE que pour `--phrase`. Un bouton sans ce modificateur
 * garde son libellé en texte nu ; rien à migrer.
 *
 * ⚠️ CE MODIFICATEUR NE SE POSE JAMAIS GLOBALEMENT, et le chiffre est la raison :
 * sur les 24 pages publiées, 33 libellés sont tout en capitales contre **337 qui
 * portent une capitale interne VOULUE** (« 9 Hectares de Nature », « 45 min de
 * Toulouse », « Découvrir l'Espace Spa », « 4 Salles de Sous-commission »…). Une
 * règle globale détruirait les 337 pour en réparer 33. Il se pose nommément, sur
 * une liste relevée — `integration/scripts/mapping-c01-casse.py`.
 * ===================================================================== */
.cb-interactive__label{
  display: inline-block;
}
.cb-interactive--phrase .cb-interactive__label{
  text-transform: lowercase;
}
.cb-interactive--phrase .cb-interactive__label::first-letter{
  text-transform: uppercase;
}

/* prefers-reduced-motion : non négociable (ticket DS11). On ne SUPPRIME pas le
   changement d'état — l'utilisateur doit toujours voir qu'il a survolé/activé quelque
   chose — on neutralise seulement le TEMPS de transition (WCAG 2.3.3). */
@media (prefers-reduced-motion: reduce){
  /* `.cb-interactive__icon` retiré de cette liste avec son animation : il n'a
     plus de transition à neutraliser. La transition de COULEUR de
     `.cb-interactive`, elle, reste et reste neutralisée ici — on ne supprime
     pas le changement d'état, seulement son temps (WCAG 2.3.3). */
  .cb-interactive{
    transition-duration: var(--cb-interaction-duration-reduced);
  }
}

/* =====================================================================
 * .cb-btn / .cb-link — boutons et liens (DS06)
 *
 * Composent `.cb-interactive` (DS11, ci-dessus) plutôt que de réécrire un
 * système d'état : `.cb-btn`/`.cb-link` ne posent QUE la forme (rayon,
 * typo, box-model) — jamais une nouvelle transition, un nouveau
 * focus-ring, un nouvel état actif/désactivé. Les recettes de couleur
 * restent celles déjà posées par DS11 (`--tone`, `--fill`,
 * `--fill-on-dark`) — aucune nouvelle recette de survol n'était
 * nécessaire. C'est exactement l'usage que le commentaire d'en-tête de
 * `.cb-interactive` annonçait ("le futur DS-boutons, UN LIEN, une carte…").
 *
 * Composition attendue sur un widget :
 *   bouton primaire        → cb-interactive cb-interactive--tone cb-btn
 *   bouton secondaire clair → cb-interactive cb-interactive--fill cb-btn
 *   bouton secondaire sombre→ cb-interactive cb-interactive--fill-on-dark cb-btn
 *   lien sur fond clair     → cb-interactive cb-link cb-link--on-light
 *   lien sur fond sombre    → cb-interactive cb-link cb-link--on-dark
 * Icône (les deux, optionnelle) : <span class="cb-interactive__icon">…</span>
 * enfant — jamais une nouvelle classe d'icône, DS11 la fournit déjà.
 *
 * `DS17` (tickets/journal/DS06.md) — le contraste d'un bouton à fond plein
 * (primaire, `--tone`) se juge contre SON fond, jamais contre le fond de
 * section : `--cb-cta` en fond + `--cb-ivoire` en texte (5,17:1) ne varie
 * jamais, quelle que soit la section derrière. Le vrai risque touche les
 * DEUX cas où une couleur est exposée SANS fond opaque à elle : le bouton
 * secondaire (bordure + texte transparents) et le lien nu. Décision : ni
 * `--cb-cta` ni `--cb-terracotta` n'apparaissent jamais comme couleur de
 * texte/bordure exposée sur `.cb-link`/le secondaire — `--cb-cta` sur beige
 * échoue l'AA texte de peu (4,34:1 < 4,5) et `--cb-terracotta` sur beige
 * échoue même le seuil 3:1 (2,77:1, `docs/couleurs.md` § 3/§4). Ces deux
 * atomes réutilisent uniquement `--cb-encre`/`--cb-vert`/`--cb-ivoire`/
 * `--cb-sauge`, tous vérifiés AA texte normal sur ivoire ET beige.
 *
 * `DS18` (tickets/journal/DS06.md) — `docs/couleurs.md` proposait un hover
 * primaire cta→vert ; cette implémentation (DS11) fait déjà cta→encre. Les
 * deux passent l'AA — tranché en faveur d'encre, conservé tel qu'écrit :
 * c'est la MÊME destination que `--fill`/`--fill-on-dark` visent déjà au
 * survol, un seul point de convergence pour tout le système plutôt que
 * deux. `docs/couleurs.md` est corrigé en conséquence.
 *
 * Rayon — RECTANGLE ARRONDI (`--cb-radius-sm`, 4px), pas la pilule.
 * ⚠️ Corrigé le 2026-08-10 après signalement d'Alexandre. `DS06` avait posé
 * la pilule en croyant reprendre « la forme du système le plus visible » :
 * c'était une erreur de lecture. Les 1577 occurrences de `border-radius:50px`
 * relevées sur des sélecteurs de bouton dans le CSS compilé portent sur des
 * éléments de **29 px de haut** — ce sont les **pastilles/tags**, que le site
 * construit avec le widget `button` détourné (`docs/composants-prod.md`).
 * Sur elles, 50px produit bien une pilule, et c'est correct.
 *
 * Mesure au DOM réel (prod, `/spa/`, `getComputedStyle` via
 * `integration/scripts/cdp.py`) sur les vrais CTA :
 *   « Réserver en ligne »          h=39px  radius=4px  fond #4B6146
 *   « EN SAVOIR PLUS SUR LE YOGA » h=39px  radius=4px  fond #4B6146
 *   « DÉCOUVRIR nos massages »     h=39px  radius=4px  fond #4B6146
 *   « La brochure SPA »            h=42px  radius=4px  fond ivoire
 * (Le `2px` visible sur la bannière cookies vient d'un tiers, hors système.)
 *
 * Un bouton du site est donc un rectangle arrondi à 4px, et l'a toujours été
 * — sur l'ancien site comme sur le nouveau. `--cb-radius-sm` était bien
 * sourcé ; c'est son emploi ici qui était faux. Leçon : compter des
 * occurrences dans un CSS compilé ne dit pas sur QUOI elles portent — la
 * forme se mesure au rendu (`docs/verification.md`).
 * ================================================================== */
.cb-btn{
  font-family: var(--cb-font-body);     /* Teachers — labeur, docs/typographie.md §3 "boutons" */
  font-weight: var(--cb-weight-control); /* H01 — était HÉRITÉ, donc dépendant de ce qui entoure le bouton : un bouton dans un titre en gras sortait en gras. Désormais déclaré, et déclaré UNE fois pour boutons et liens. La valeur (400) est un écart assumé à la frame, qui dit Teachers Medium 500 — voir tokens.css § graisses. */
  font-size: var(--cb-text-base);       /* 16px — aucun palier de bouton distinct sourcé */
  border-radius: var(--cb-radius-sm);  /* 4px — rectangle arrondi, mesuré au DOM sur les vrais CTA de la prod (voir commentaire ci-dessus). La pilule reste réservée aux pastilles/tags. */
  text-decoration: none;                /* si le bouton est un <a>, jamais souligné */
  line-height: 1; /* DS40 — trouvé par compare-jumeaux.py : le pont posait
     `line-height:1` en se disant « reprise littérale d'atoms.css », alors que
     la ligne n'était PAS ici. Le jumeau rendait donc 1, le markup neutre 1.5
     (hérité du corps), et les deux colonnes mentaient l'une sur l'autre. La
     décision revient à cette couche : un libellé de contrôle tient sur une
     ligne, le centrage vient du padding, pas de l'interligne. */
  width: auto;                          /* ne s'étire jamais à la largeur du conteneur — même règle que le tag, docs/charte.md §6 */
}

/* ---- btn/icon (DS28) — le bouton qui ne porte QU'UNE icône.
 * `.cb-btn` est `width:auto`, pensé pour « texte + icône » : une icône seule y
 * donne une boîte plus large que haute. Relevé sur le bouton cadeau du header
 * (`1f9e470`, H01a) : 41×41, un carré.
 *
 * (parti pris) On ne recopie PAS le 41×41. Le carré de prod se compose d'une
 * icône de 25px, 6px de padding et 2px de bordure — et `6px` n'appartient à
 * aucune échelle du système (les paliers sont 8/12/16/20). Recopier 41
 * imposerait donc soit une valeur en dur, soit un token orphelin pour une
 * seule occurrence — les deux interdits par AGENTS.md.
 *
 * Ce qu'on fait à la place, et pourquoi c'est mieux : le bouton icône devient
 * le FRÈRE CARRÉ du bouton texte. Même padding (`--cb-space-sm`), même
 * bordure, même rayon — donc **exactement la même hauteur** que le « Réserver »
 * posé à côté de lui dans le header, ce que la prod obtient aujourd'hui par
 * coïncidence de réglages (41 et 41) plutôt que par construction. `aspect-
 * ratio:1` fait le reste : la boîte est carrée quoi qu'il arrive.
 *
 * Conséquence assumée à regarder en revue : l'icône y est plus petite qu'en
 * prod (taille de corps au lieu de 25px), donc le bouton respire davantage.
 * C'est le seul écart, il est visuel, et il se tranche à l'œil. -------------*/
.cb-btn--icon{
  /* ⚠️ `aspect-ratio: 1` ÉTAIT ÉCRIT ICI ET NE MARCHAIT PAS — mesuré au rendu le
     2026-08-12, sur signalement d'Alexandre (« bouton de type icône only n'a pas
     de ratio carré ») : la boîte faisait **35,8 × 42**. La cause est une règle
     du modèle de boîte, pas une faute de frappe : `aspect-ratio` n'a d'effet que
     si UNE des deux dimensions est définie. Ici largeur et hauteur étaient
     toutes deux `auto`, donc la boîte se dimensionnait sur son contenu dans les
     deux axes et le ratio était simplement ignoré. Il paraissait marcher tant
     qu'on ne mesurait pas.
     Le remède ne dépend plus du contenu : la largeur du CONTENU vaut un cadratin
     (`1em`, la même chose que la hauteur de ligne du bouton, `line-height: 1`),
     donc les deux axes valent `1em + 2 × --cb-space-sm + 2px de bordure` = 42px.
     Carré par construction, quelle que soit l'icône posée dedans — et exactement
     la hauteur du bouton texte voisin, ce qui était l'intention de DS28.
     `box-sizing: content-box` est OBLIGATOIRE ici et n'est pas une précaution :
     Elementor pose `box-sizing: border-box` globalement, sous lequel `width: 1em`
     vaudrait 16px TOTAL, bordure et padding compris — un bouton minuscule. */
  box-sizing: content-box;
  width: 1em;
  padding: var(--cb-space-sm); /* même palier vertical que .cb-interactive → même hauteur que le bouton texte voisin */
  justify-content: center;
  gap: 0; /* pas de texte à séparer de l'icône */
}

/* .cb-link — compose `.cb-interactive` pour l'engin d'état (transition,
   focus-visible, actif, désactivé via [aria-disabled], icône enfant), mais
   annule le box-model "bouton" (padding, bordure, fond, rayon) : un lien
   est du texte, pas une pilule. Les couleurs viennent des deux variantes
   ci-dessous, jamais de --tone/--fill (recettes réservées au bouton). */
.cb-link{
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  font-family: var(--cb-font-body);     /* Teachers — labeur, docs/typographie.md §3 */
  font-weight: var(--cb-weight-control); /* H01 — même raison que .cb-btn : la graisse était héritée, elle est désormais déclarée, et par le même token */
  font-size: var(--cb-text-base);
  text-decoration: none;                /* (parti pris) — l'icône + le changement de couleur au survol
                                            portent déjà l'affordance ; à reconsidérer si cet atome sert
                                            un jour à un lien isolé au milieu d'un paragraphe de prose
                                            (DS09), où WCAG 1.4.1 demande un indice non-couleur plus fort */
  line-height: 1; /* DS51, 2026-08-19 — EXACTEMENT LE DÉFAUT QUE DS40 A TROUVÉ SUR
     `.cb-btn`, resté ouvert sur l'atome frère : le pont posait `line-height:1` sur
     `.cb-link .elementor-button` en se disant « reprise littérale d'atoms.css »,
     alors que la ligne n'était pas ici. Le jumeau rendait 1 (16px), le markup
     neutre 1.5 (24px) — mesuré dès que les cartes `link/*` ont eu un jumeau.
     ⚠️ LA DÉCISION EST TRANCHÉE DU CÔTÉ DU PONT, ET C'EST DÉLIBÉRÉ : c'est ce que
     les 4 `.cb-link` déployés (tous sur `domaine-de-camboyer`, relevé du
     2026-08-19 sur les 161 posts) rendent DÉJÀ en ligne. Écrire la valeur ici
     aligne les deux couches sans changer un pixel du site ; la retirer du pont
     aurait, elle, changé le rendu servi pour refermer un écart d'outil.
     Même raison qu'en DS40 : un libellé de contrôle tient sur une ligne, le
     centrage vient du padding, pas de l'interligne. */
  width: auto;
}
.cb-link--on-light{
  color: var(--cb-vert); /* défaut — docs/couleurs.md § 2 couple n°3 : 10,20:1 ivoire / 8,57:1 beige */
}
.cb-link--on-light:hover,
.cb-link--on-light[data-demo-state="hover"]{
  color: var(--cb-encre); /* survol — voir DS18 ci-dessus : convergence vers encre, jamais --cb-cta ici (DS17) */
}

.cb-link--on-dark{
  color: var(--cb-ivoire); /* défaut — docs/couleurs.md § 2 couple n°4 : 10,20:1 vert / 14,93:1 encre */
}
.cb-link--on-dark:hover,
.cb-link--on-dark[data-demo-state="hover"]{
  color: var(--cb-sauge); /* survol — seul accent chromatique lisible en texte normal sur fond sombre (docs/couleurs.md § 3) */
}

/* =====================================================================
 * .cb-tag — tag/s, tag/m (DS07 — poste 8 du devis)
 *
 * Widget `button` détourné en prod pour porter les « arguments marketing »
 * des cartes, 3 à 10 par carte (docs/composants-prod.md § 1.1) : PAS un tag
 * natif, mais visuellement une pastille — jamais un lien réel dans le relevé
 * disponible (aucun href/action documenté sur ces boutons détournés).
 *
 * Forme mesurée au DOM sur la prod (`/spa/`) : hauteur 29px, `border-radius:
 * 50px` → une pilule (--cb-radius-pill — pas la même mesure que .cb-btn
 * ci-dessus, qui portait sur les VRAIS CTA à 4px : ici c'est bien la
 * pastille). Padding relevé 8px/20px = exactement --cb-space-xs / --cb-space-lg
 * — déjà réservés à cet usage par le commentaire de tokens.css ("padding
 * vertical compact (tag)" / "padding horizontal tag"). Fond --cb-tag-bg,
 * texte --cb-encre : 12,66:1 sur ivoire, 10,72:1 sur beige (composite déjà
 * calculé, docs/couleurs.md § 4.4) — largement AA.
 *
 * Deux tailles (BRIEF.md § 6 en demande au moins deux) : le padding VERTICAL
 * reste --cb-space-xs (8px, la valeur mesurée) pour les deux — la hauteur ne
 * change donc pas entre S et M, seule la densité horizontale change. Aucun
 * deuxième palier de taille de texte n'existe sous --cb-text-sm (docs/
 * typographie.md § 4.2 : un seul token pour le rôle tag/label) — baisser la
 * police pour "faire small" aurait été une valeur sans token. `tag/m` reprend
 * le padding horizontal mesuré (--cb-space-lg, 20px). `tag/s` réutilise
 * --cb-space-sm (12px, déjà nommé pour un autre usage dans tokens.css, mais
 * aucune nouvelle valeur créée) pour une pastille plus dense — (à arbitrer :
 * aucune deuxième mesure prod ne couvre ce second gabarit, voir tickets/
 * journal/DS07-DS15-DS22.md).
 *
 * Largeur intrinsèque non négociable (docs/charte.md § 6, bug déjà payé) :
 * jamais un tag ne s'étire à la largeur de son conteneur.
 *
 * PAS de .cb-interactive ici : décoratif, aucun rôle de lien ni de bouton
 * confirmé — donc aucun focus-ring, aucun survol, aucun état actif/désactivé.
 * Lui en inventer un serait styler un comportement que rien n'observe.
 * ================================================================== */
.cb-tag{
  line-height: 1; /* DS40 — trouvé par compare-jumeaux.py : le pont posait
     `line-height:1` en se disant « reprise littérale d'atoms.css », alors que
     la ligne n'était PAS ici. Le jumeau rendait donc 1, le markup neutre 1.5
     (hérité du corps), et les deux colonnes mentaient l'une sur l'autre. La
     décision revient à cette couche : un libellé de contrôle tient sur une
     ligne, le centrage vient du padding, pas de l'interligne. */
  display: inline-flex;
  align-items: center;
  width: auto;               /* ne s'étire jamais à la largeur du conteneur — même règle que .cb-btn */
  border-radius: var(--cb-radius-pill);
  background: var(--cb-tag-bg);
  color: var(--cb-encre);
  font-family: var(--cb-font-body); /* Teachers — labeur, docs/typographie.md § 4.2 */
  font-size: var(--cb-text-sm);
}
.cb-tag--m{
  padding: var(--cb-space-xs) var(--cb-space-lg); /* 8/20 — mesuré au DOM prod */
}
.cb-tag--s{
  padding: var(--cb-space-xs) var(--cb-space-sm); /* 8/12 — (parti pris) plus dense, même hauteur que tag/m */
}

/* .cb-tag--on-dark — DS12, ajout minimal. Les pastilles réelles apparaissent
   AUSSI sur card/overlay (4 hébergements homepage, docs/composants-prod.md
   §1.1-A "Pastilles (boutons): 4/3/5/4"), posées sur une photo + dégradé —
   --cb-tag-bg (translucide à 10% de --cb-vert, mesuré sur fond CLAIR,
   docs/couleurs.md §4.4) n'a jamais été vérifié là. Fond OPAQUE plutôt que
   translucide : la photo derrière n'est pas mesurée pixel à pixel, un fond
   translucide dépendrait d'un pixel qu'on ne connaît pas — un aplat clair
   rend le contraste indépendant de la photo. Même paire encre/ivoire déjà
   vérifiée (14,93:1, docs/couleurs.md §1), aucune nouvelle valeur. */
.cb-tag--on-dark{
  background: var(--cb-ivoire);
  color: var(--cb-encre);
}

/* =====================================================================
 * .cb-label — label/default (DS15)
 *
 * Petit texte NU, sans fond — distinct du tag (pilule à fond, ci-dessus) :
 * les deux formes co-existent en prod pour deux emplois différents (docs/
 * typographie.md § 4.2). Le libellé du bloc « arguments » à icônes est lui
 * aussi un widget `button` détourné, mais SANS lien ("le button ici sert de
 * libellé, pas de lien", docs/composants-prod.md § 2.2) — contrairement au
 * tag, jamais interactif : pas de .cb-interactive, pas de survol.
 *
 * --cb-text-sm (14px), Teachers — même rôle typographique que le tag (un
 * seul token pour "petit texte distinct", docs/typographie.md § 1/§ 4.2).
 * Couleur --cb-encre : 14,93:1 sur ivoire, 12,55:1 sur beige (docs/couleurs.md
 * § 3) — largement AA. Pas de variante fond sombre construite ici : aucun
 * relevé ne confirme le fond du bloc "arguments" (docs/composants-prod.md
 * § 2.2) — à ajouter le jour où un ticket le vérifie, pas anticipé.
 *
 * Même règle de largeur intrinsèque que .cb-tag : jamais étiré à la largeur
 * du conteneur (docs/charte.md § 6).
 * ================================================================== */
.cb-label{
  line-height: 1; /* DS40 — trouvé par compare-jumeaux.py : le pont posait
     `line-height:1` en se disant « reprise littérale d'atoms.css », alors que
     la ligne n'était PAS ici. Le jumeau rendait donc 1, le markup neutre 1.5
     (hérité du corps), et les deux colonnes mentaient l'une sur l'autre. La
     décision revient à cette couche : un libellé de contrôle tient sur une
     ligne, le centrage vient du padding, pas de l'interligne. */
  display: inline-flex;
  align-items: center;
  width: auto;
  margin: 0; /* DS42 — le label ne s'employait que sur un widget `button` détourné,
     donc sur un porteur sans marge d'agent utilisateur. Le relevé des cartes réelles
     l'a trouvé sur un widget `heading` (« Chambre »/« Équipements », 15 cartes des 4
     pages chambre) : là, le porteur est un `<h3>` qui apporte ses propres marges, et
     en markup neutre un `<p>`/`<hN>` fait pareil. Même raison que `.cb-heading{margin:0}` :
     un atome de texte ne laisse pas le navigateur décider de son rythme vertical. */
  background: none;
  color: var(--cb-encre);
  font-family: var(--cb-font-body);
  font-size: var(--cb-text-sm);
}

/* =====================================================================
 * .cb-link--prose — link/prose (DS22, P0 — non-conformité corrigée)
 *
 * `.cb-link` (DS06, ci-dessus) est SANS soulignement par défaut — marqué à
 * l'époque comme un parti pris ("à reconsidérer si cet atome sert un jour à
 * un lien isolé au milieu d'un paragraphe de prose… où WCAG 1.4.1 demande
 * un indice non-couleur plus fort", commentaire de `.cb-link` ci-dessus).
 * Ce n'était pas un parti pris tenable : WCAG 1.4.1 exige un contraste ≥ 3:1
 * entre la couleur du lien et celle du texte environnant quand la couleur
 * est le SEUL indice — et aucune couleur du système ne passe à la fois ce
 * seuil ET l'AA texte/fond : --cb-vert (lien) contre --cb-encre (texte) =
 * 1,46:1 (confirmé, docs/couleurs.md § 3/§4.2, paire --cb-vert/--cb-encre
 * l'une sur l'autre) ; --cb-cta contre --cb-encre = 2,89:1 ; --cb-terracotta
 * contre --cb-encre = 4,53:1 (passe) mais --cb-terracotta contre --cb-beige
 * (fond) = 2,77:1 (échoue). Le soulignement est donc le seul moyen conforme.
 *
 * Toujours visible, y compris au repos — jamais seulement au survol : un
 * visiteur qui n'a pas encore survolé le lien doit déjà voir qu'il est
 * cliquable, sinon l'indice non-couleur n'existe que trop tard.
 *
 * Nuance du ticket, non perdue : un lien AUTONOME (`link/on-light`,
 * `link/on-dark` ci-dessus, seul sur sa ligne) n'a pas de texte adjacent
 * dont se distinguer — restent SANS soulignement, inchangés. `.cb-link--prose`
 * est un modificateur SUPPLÉMENTAIRE, à poser uniquement quand le lien est
 * entouré de texte courant.
 *
 * Compose avec --on-light/--on-dark existants (DS06) — même couleur, même
 * survol, seul le soulignement change. Doit suivre `.cb-link` dans ce
 * fichier pour gagner sur `text-decoration: none` à spécificité égale (une
 * seule classe chacune) — ne pas déplacer plus haut.
 * ================================================================== */
.cb-link--prose{
  text-decoration: underline;
}

/* =====================================================================
 * .cb-img — img/3-2, img/2-3 (DS25 — poste 9 du devis, docs/images.md)
 *
 * Deux ratios SEULEMENT (docs/images.md §1.1/§1.2, §7 interdit #6) : `3:2`
 * pour toute image-carte paysage, `2:3` pour un visuel d'ambiance vertical.
 * Chacun sourcé sur 3-4 occurrences prod indépendantes qui convergent déjà
 * dessus, sans qu'aucune règle commune ne l'impose aujourd'hui — pas un
 * troisième ratio spéculatif. Le hero reste HORS de cet atome : piloté par
 * le viewport (`vh`), jamais par un ratio W:H (docs/images.md §3) — ce n'est
 * pas un oubli, c'est le périmètre posé par la spec.
 *
 * `aspect-ratio` porte la règle centrale du document : JAMAIS une hauteur
 * fixée en pixels sur un widget média (docs/images.md §7 interdit #1) — le
 * vrai bug mesuré (deux cartes homepage structurellement identiques rendant
 * à 286px et 272px de haut en mobile, docs/images.md TLDR/§6.2) vient
 * exactement de l'absence de cette règle : `min_height_mobile:0` laissait le
 * texte voisin piloter la hauteur. `aspect-ratio` calcule la hauteur depuis
 * la largeur réelle de la colonne, quel que soit le nombre de colonnes ou la
 * longueur du texte voisin — jamais l'inverse.
 *
 * `object-fit: cover`, jamais `fill` (interdit #2, mesuré 3 fois en prod —
 * vignette blog, "salon-bar", "se-restaurer", docs/images.md §4.2) : `fill`
 * étire l'image hors de ses proportions natives sans qu'aucune erreur ne le
 * signale. `contain` n'a pas sa place ici : réservé aux pictogrammes/
 * illustrations (docs/images.md §1.4), qui ne sont pas des "images" au sens
 * de cet atome — jamais posé sur `.cb-img`. `object-position: center`
 * (défaut mesuré sur la majorité des cas, docs/images.md §3/§4.4) : une
 * image dont le sujet n'est pas centré se règle au cas par cas, par image —
 * jamais par une règle de site, donc jamais un modificateur `.cb-img--*`
 * pour ça.
 *
 * Radius --cb-radius-md (5px, tokens.css) — pas --cb-radius-sm (4px,
 * boutons) : DS08 a mesuré 5px EXACTEMENT sur 8 occurrences prod
 * indépendantes (docs/images.md §5.1), jamais 4 ni 6 — un écart réel et
 * stable, pas un bruit de mesure à arrondir sur le token d'un voisin. Décidé
 * ici (DS25) en suivant la proposition motivée de `docs/images.md` §5.2 :
 * pas un `(parti pris)`, un fait mesuré.
 *
 * Une seule classe, posée DIRECTEMENT sur l'élément `<img>` — pas de
 * wrapper : c'est déjà comme ça que le DOM réel du widget `image` se
 * présente (`img.attachment-*`, relevé DS24, voir le pont couche 3 § tête
 * de fichier), un wrapper supplémentaire n'apporterait rien que ce DOM n'a
 * pas déjà.
 * ================================================================== */
.cb-img{
  display: block;
  width: 100%;              /* remplit sa colonne, docs/images.md §2 — jamais une taille WP enregistrée */
  height: auto;              /* jamais une hauteur en dur — dérivée de l'aspect-ratio ci-dessous */
  object-fit: cover;         /* jamais fill — docs/images.md §4.2/§7 interdit #2 */
  object-position: center;   /* défaut — docs/images.md §3/§4.4, réglable par image si le sujet n'est pas centré */
  border-radius: var(--cb-radius-md);
}
.cb-img--3-2{ aspect-ratio: 3 / 2; } /* image-carte paysage — docs/images.md §1.1 */
.cb-img--2-3{ aspect-ratio: 2 / 3; } /* visuel d'ambiance portrait — docs/images.md §1.2 */
.cb-img--16-9{ aspect-ratio: 16 / 9; } /* média d'introduction — node Figma 40000117:16624 (742×414, arrondi au format éditorial standard) */
.cb-img--15-17{ aspect-ratio: 15 / 17; } /* média vertical de l'explorateur chambres — node 40000126:889 (600×680) */

/* ---- --duo : le média d'une section DEUX COLONNES image/texte — DS65,
 * tranché par Alexandre le 2026-08-18.
 *
 * C'est le seul modificateur de cette famille qui pose une HAUTEUR, et c'est
 * assumé : les quatre autres décrivent une FORME, celui-ci garantit une
 * COHÉRENCE. Le motif « image d'un côté, texte de l'autre » revient 145 fois
 * sur 21 pages, avec sept largeurs de colonne différentes — un ratio seul y rend
 * les formes cohérentes et les hauteurs divergentes (17 hauteurs distinctes
 * mesurées avant cette passe). Le raisonnement complet, et la dérogation à la
 * règle « jamais une hauteur en dur » ci-dessous, sont dans tokens.css.
 *
 * ⚠️ `block-size` et non `height`, `min-inline-size` logiques partout : c'est la
 * convention du fichier, pas une préférence.
 * ⚠️ La borne 1025 px n'est pas neuve : c'est celle du § 13 du pont et du
 * snippet `21-header-scroll-effect.php`. Sous ce seuil la section s'empile et la
 * colonne devient le viewport — on rend la main au ratio, couché. */
.cb-img--duo{
  aspect-ratio: var(--cb-img-duo-ratio-mobile);
  block-size: auto;
}
@media (min-width: 1025px){
  .cb-img--duo{
    /* ⚠️ C'EST LE RATIO QUI EST MAÎTRE, PAS UNE HAUTEUR — inversé le 2026-08-19.
       Avant, `block-size: clamp(420px, 78vh, 800px)` rendait la hauteur de
       l'image dépendante de la HAUTEUR DE LA FENÊTRE : la même page, sur le même
       Mac, servait une image de 702 px en fenêtre haute et de 420 en fenêtre
       basse, pendant que la colonne de texte ne bougeait pas d'un pixel. C'est
       le défaut qu'Alexandre a signalé. Un média de section se dimensionne sur
       sa COLONNE, jamais sur l'écran.
       Le ratio, lui, est dérivé de la largeur : il donne la même forme quelle
       que soit la hauteur de la fenêtre. Les deux bornes ne servent plus qu'à
       empêcher l'absurde aux extrêmes de largeur. */
    aspect-ratio: var(--cb-img-duo-ratio);
    block-size: auto;
    min-block-size: var(--cb-img-duo-h-min);
    max-block-size: var(--cb-img-duo-h-max);
  }
}

/* ---- --duo-dessin : l'exception d'Alexandre du 2026-08-18, et son critère est
 * MESURABLE — « exception au cas par cas, UNIQUEMENT pour les illustrations
 * style dessiné (très facile à reconnaître, souvent des png avec beaucoup de
 * transparence) ».
 *
 * Il a raison sur les deux points, et le corpus le prouve sans réglage de seuil.
 * Test sur les 30 images distinctes du motif deux colonnes :
 *
 *                     dessins (5)          photos (25)
 *     mode            RGBA ×5              RGB ×25
 *     transparence    54,8 % → 86,4 %      0,0 % PARTOUT
 *     teintes         1 → 36               103 → 289
 *
 * Il n'y a rien entre 0,0 % et 54,8 % : la séparation est nette, aucun cas
 * limite à arbitrer. Et les 5 sont exactement ceux qu'il redoutait en tranchant
 * le ratio — le dessin de `/hotel/` et les 4 de `/spa/massages-yoga/`, qui
 * perdaient jusqu'à 83 % de leur largeur.
 *
 * ⚠️ CE MODIFICATEUR NE COÛTE PAS LA COHÉRENCE DE HAUTEUR, et c'est ce qui le
 * rend acceptable : il GARDE la boîte de `--cb-img-duo-h` et change seulement le
 * mode de remplissage. Sur un dessin à fond transparent, `contain` ne produit
 * aucune bande visible — le fond de la section passe au travers. La rangée reste
 * donc alignée avec les photos voisines, et le dessin cesse d'être coupé.
 * C'est le prolongement de `docs/images.md` § 1.4, qui sort déjà les
 * illustrations du système de ratio : ici on ne les sort pas de la GRILLE. */
.cb-img--duo-dessin{
  object-fit: contain;
  object-position: center;
  /* ⚠️ AUCUN `background` déclaré, et c'est volontaire : une `<img>` n'en a pas
     par défaut, et `contain` n'en ajoute pas. Le fond de la section passe donc
     derrière le dessin — c'est exactement ce qui rend cette exception gratuite
     en cohérence de hauteur. Une déclaration ici n'aurait rien changé au rendu
     et aurait doublé la peinture avec la couche 3 (refusé par lint-couches). */
}

/* =====================================================================
 * LE RAIL DE PAGE — DS66
 *
 * Une section de premier niveau garde son fond à fond perdu et pose son
 * inset comme avant ; ce qui change au-delà du rail (`--cb-container-max`,
 * 1490px), c'est qu'elle recentre son CONTENU dans la même boîte que le
 * header et que les sections encore legacy, au lieu de continuer à s'étaler.
 * C'est la composition de 1440 qui se fige et se centre, pas une remise à
 * plat des insets : l'inset propre de chaque section s'applique À
 * L'INTÉRIEUR du rail, exactement comme le header y pose ses 32px.
 *
 * `max(0px, …)` fait que la gouttière vaut ZÉRO en dessous du rail : le
 * mobile et le 1440 ne connaissent pas cette règle, il n'y a pas un palier
 * de plus à tenir. Et `100%` se lit sur la largeur du CONTAINER de la
 * section — pour une section de premier niveau, le viewport : le même
 * repère que le rail Elementor auquel on s'aligne.
 *
 * Pourquoi une variable et pas `margin-inline:auto` + `max-width` sur
 * chaque section : le fond doit rester perdu (arbitrage du 2026-08-17), donc
 * la section ne peut PAS se rétrécir — seul son padding peut grandir. Et
 * `.cb-rooms` a besoin de la valeur en tant que nombre, pas en tant que
 * marge : sa couche média est positionnée en absolu sur ce même inset.
 * ================================================================== */
.cb-intro,
.cb-rooms-head,
.cb-rooms,
.cb-events,
.cb-section,
.cb-content-hero{
  --cb-rail-gutter: max(0px, (100% - var(--cb-container-max)) / 2);
}

/* =====================================================================
 * `.cb-intro` et `.cb-rooms` — compositions homepage (H02c / H03)
 *
 * Ces deux blocs n'ajoutent ni couleur, ni typo, ni recette de bouton : ils
 * organisent exclusivement des atomes existants. Les valeurs de Figma qui
 * divergent des tokens (graisse, taille des tags, couleur de fond) ne sont pas
 * recopiées ; le DS reste l'autorité.
 * ================================================================== */
.cb-intro{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:var(--cb-flow-block);
  padding:var(--cb-space-section-desktop) calc(var(--cb-rail-gutter) + var(--cb-page-inset)); /* DS66 — MÊME retrait que le header, à l'intérieur du rail */
  background:var(--cb-ivoire);
}
.cb-intro__content{
  display:flex;
  flex-direction:column;
  align-items:center;
  width:min(100%, var(--cb-container-max));
  gap:var(--cb-flow-block);
}
.cb-intro__lockup{ display:flex; flex-direction:column; align-items:center; gap:var(--cb-flow-lockup); width:100%; }
.cb-intro__details{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--cb-space-split-desktop); width:100%; }
.cb-intro__title,.cb-rooms-head__title,.cb-events__title{
  margin:0;
  color:var(--cb-lockup-titre);
  font-family:var(--cb-font-caps);
  font-size:var(--cb-text-h2);
  font-weight:var(--cb-weight-title);
  line-height:1.5;
  text-align:center;
  text-transform:uppercase;
}
.cb-intro__script,.cb-rooms-head__script,.cb-events__script{
  margin:0;
  color:var(--cb-terracotta);
  font-family:var(--cb-font-script);
  font-size:calc(var(--cb-text-h2) * var(--cb-ratio-lockup-script));
  font-weight:400;
  line-height:1;
  text-align:center;
}
.cb-intro__image{ width:min(100%, 742px); }
.cb-intro__copy{ max-width:var(--cb-measure); }
.cb-intro__tags{ display:flex; flex-direction:row; flex-wrap:wrap; align-content:start; justify-content:flex-start; gap:var(--cb-flow-inline); width:100%; }
/* ⚠️ `flex-direction:row` AFFIRMÉ, et ce n'est pas une redondance — c'est la
   régression REG01 § 4, payée deux fois. Ces atomes se posent sur un container
   Elementor, qui rend `flex-direction: var(--flex-direction)` ; tant que le
   widget portait son propre réglage `row`, l'omission ne se voyait pas. La
   migration du 2026-08-18 a vidé ce réglage — et le défaut d'un container
   Elementor est `column`. Six rangées de pastilles se sont empilées, dont celle
   de la section d'introduction de l'accueil. Un atome de rangée doit dire qu'il
   est une rangée. */
.cb-intro__actions{ display:flex; flex-direction:row; flex-wrap:wrap; justify-content:center; gap:var(--cb-flow-actions); width:100%; }
/* ⚠️ CENTRÉ DEPUIS LE 2026-08-20, ET C'EST LE SECOND ARBITRAGE D'ALEXANDRE SUR
   CETTE MÊME RANGÉE. Il faut lire les deux ensemble, sinon le prochain agent
   croira à une régression et le remettra à gauche.

   Le 2026-08-18, sur capture : « cette section a du texte centré et des boutons
   ferré à gauche, alors que c'est typiquement un cas de section image texte /
   texte image sur deux colonnes où la règle est que ça doit toujours être ferré
   à gauche. » Le ferrage a donc été posé — en appliquant sa RÈGLE.

   Le 2026-08-20, devant le résultat : « je préfèrerais centrer ces boutons
   complètement au milieu de l'écran. »

   ⚠️ Ce n'est pas une contradiction, et la mesure le montre : **ce bloc n'est
   pas une section deux colonnes.** Relevé au rendu, `.cb-intro__content` empile
   cinq enfants sur toute la largeur du rail — lockup 1376, IMAGE 742 **centrée**,
   prose 1376, actions 1376, brochure 1376. Sa règle du 18 vaut pour le motif
   qu'elle nomme (image | texte côte à côte) ; celui-ci n'en est pas un, et son
   seul média est déjà centré. Le centrage des actions suit donc l'axe du bloc.
   La règle du 2026-08-18 reste vraie et reste écrite : elle n'a simplement
   jamais eu à s'appliquer ici.

   ⚠️ Rayon : **un seul nœud sur le site**. `.cb-intro__actions` n'existe que
   dans le bloc d'introduction de l'accueil — vérifié page par page, 1 sur `/`,
   0 partout ailleurs. */
.cb-intro__brochure{ position:relative; isolation:isolate; display:flex; align-items:center; justify-content:flex-end; min-height:600px; width:100%; padding:var(--cb-flow-group); border-radius:var(--cb-radius-md); overflow:hidden; }
.cb-intro__brochure::before{
  content:"";
  position:absolute;
  z-index:0;
  inset:0;
  pointer-events:none;
  background:linear-gradient(
    to bottom,
    transparent var(--cb-brochure-overlay-start),
    color-mix(in srgb, var(--cb-scrim-color) calc(var(--cb-brochure-overlay-strength) * 100%), transparent) var(--cb-brochure-overlay-end)
  );
}
.cb-intro__brochure > *{ position:relative; z-index:1; }

.cb-rooms-head{ width:100%; padding:var(--cb-space-section-desktop) calc(var(--cb-rail-gutter) + var(--cb-page-inset)) 0; background:var(--cb-ivoire); } /* DS66 — retrait de page, pas un inset propre */
.cb-rooms-head__inner{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--cb-space-split-desktop); align-items:flex-start; width:100%; margin-inline:auto; }
.cb-rooms-head__title-group{ display:flex; flex-direction:column; align-items:flex-start; gap:var(--cb-flow-lockup); }
.cb-rooms-head__copy{ max-width:var(--cb-measure); margin:0; }
.cb-rooms{
  /* DS66 — l'inset porte le rail : il sert AUSSI de repère à la couche média
     positionnée en absolu (`right`, et la largeur de colonne calculée juste
     en dessous), donc la borne doit vivre dans cette valeur et nulle part
     ailleurs, sinon la photo et les accordéons se décalent l'un de l'autre. */
  --cb-rooms-inset:calc(var(--cb-rail-gutter) + var(--cb-page-inset));
  --cb-rooms-padding:var(--cb-flow-block) var(--cb-rooms-inset) var(--cb-space-section-desktop);
  position:relative;
  display:flex;
  flex-direction:column;
  gap:0;
  min-height:var(--cb-rooms-stable-height,var(--cb-rooms-media-min-height));
  width:100%;
  margin-inline:auto;
  padding:var(--cb-rooms-padding);
  background-color:var(--cb-ivoire);
}
.cb-rooms__media-layer{
  position:absolute;
  z-index:0;
  top:var(--cb-flow-block);
  right:var(--cb-rooms-inset);
  width:calc((100% - (var(--cb-rooms-inset) * 2) - var(--cb-space-split-desktop)) / 2);
  bottom:var(--cb-space-section-desktop);
  border-radius:var(--cb-radius-md);
  background-repeat:no-repeat;
  background-size:cover;
  opacity:0;
  pointer-events:none;
  transition:opacity var(--cb-rooms-motion-media) var(--cb-interaction-ease);
}
.cb-rooms__media-layer.is-visible{ opacity:1; }
.cb-rooms__item{
  box-sizing:border-box;
  position:relative;
  z-index:1;
  width:calc((100% - var(--cb-space-split-desktop)) / 2);
  min-height:0;
  padding-block-start:var(--cb-rooms-item-padding-start);
  padding-block-end:var(--cb-rooms-item-padding-end-closed);
  /* Le décalage de l'item actif est PRIS SUR LA BOÎTE, jamais ajouté à côté :
     le contenu vit dans 600 − 24 px et glisse dans la réserve. Sans cela, la
     ligne active dépasserait sa colonne (node 40000253:1341, x=24 w=552). */
  padding-inline-end:var(--cb-rooms-active-offset);
  border-bottom:1px solid color-mix(in srgb,var(--cb-encre) 9%,transparent);
  /* Le fond reste dans le JSON Elementor (source éditable) mais sa boîte est
     nulle : le JS le lit pour l'afficher une seule fois sur le panneau média
     de la liste. On ne duplique donc ni URL ni asset dans la feuille CSS. */
  background-size:0 0;
  background-repeat:no-repeat;
  cursor:pointer;
}
/* Filets INTERIEURS seulement : la liste n'est pas fermée en haut ni en bas,
   les traits ne servent qu'à séparer deux chambres. */
.cb-rooms__item--last{ border-bottom:0; }
.cb-rooms__item:focus-visible{ outline:2px solid var(--cb-cta); outline-offset:4px; }
/* Le dernier item absorbe la réserve de hauteur mesurée par rooms-explorer.js.
   La liste garde donc son rythme naturel, et le mou tombe sous le dernier
   filet — jamais entre le titre d'une chambre et son texte. */
.cb-rooms__item--last{ margin-block-end:auto; }
/* Seul le CONTENU glisse : les filets restent alignés à gauche d'une chambre
   à l'autre, comme dans la maquette (node 40000253:1337). */
.cb-rooms__label{ display:flex; align-items:center; gap:var(--cb-space-sm); transition:transform var(--cb-rooms-motion-shift) var(--cb-interaction-ease); }
.cb-rooms__item.is-active .cb-rooms__label,
.cb-rooms__item.is-active .cb-rooms__details{ transform:translateX(var(--cb-rooms-active-offset)); }
/* La maquette ne colore le numéro que sur la chambre ouverte : c'est le seul
   repère de position dans une liste sans puce. */
.cb-rooms__index{ color:var(--cb-lockup-titre); font-family:var(--cb-font-caps); font-size:var(--cb-text-h3); font-weight:var(--cb-weight-display); line-height:1; transition:color var(--cb-interaction-duration-base) var(--cb-interaction-ease); }
.cb-rooms__item.is-active .cb-rooms__index{ color:var(--cb-terracotta); }
.cb-rooms__index::after{ content:""; display:inline-block; width:4px; height:4px; margin-left:var(--cb-space-sm); border-radius:var(--cb-radius-pill); background:currentColor; vertical-align:middle; }
.cb-rooms__name{ color:var(--cb-lockup-titre); font-family:var(--cb-font-caps); font-size:var(--cb-text-h3); font-weight:var(--cb-weight-title); line-height:1.5; }
.cb-rooms__details{ display:grid; grid-template-rows:0fr; pointer-events:none; transition:grid-template-rows var(--cb-rooms-motion-reveal) var(--cb-interaction-ease),transform var(--cb-rooms-motion-shift) var(--cb-interaction-ease); }
.cb-rooms__source-heading{ display:none; }
/* Aucun padding ici : sur une piste de grille à 0fr, un padding tient la boîte
   ouverte (16 px fantômes sous chaque chambre fermée). L'écart au titre est
   donc porté par la marge du premier enfant, que `overflow:hidden` retient. */
.cb-rooms__details-inner{ display:flex; flex-direction:column; align-items:flex-start; gap:var(--cb-rooms-detail-gap); min-height:0; overflow:hidden; }
.cb-rooms__details-inner > :first-child{ margin-block-start:var(--cb-rooms-detail-gap); }
.cb-rooms__details-inner > *{ opacity:0; transform:translateY(var(--cb-rooms-reveal-rise)); transition:opacity var(--cb-rooms-motion-fade) var(--cb-interaction-ease),transform var(--cb-rooms-motion-fade) var(--cb-interaction-ease); }
.cb-rooms__item.is-active .cb-rooms__details{ grid-template-rows:1fr; pointer-events:auto; }
.cb-rooms__item.is-active .cb-rooms__details-inner > *{ opacity:1; transform:none; }
/* Texte, pastilles puis lien arrivent dans l'ordre de lecture. Le décalage est
   court : il donne le sens du mouvement sans faire attendre le dernier. */
.cb-rooms__item.is-active .cb-rooms__details-inner > :nth-child(2){ transition-delay:var(--cb-rooms-reveal-stagger); }
.cb-rooms__item.is-active .cb-rooms__details-inner > :nth-child(3){ transition-delay:calc(var(--cb-rooms-reveal-stagger) * 2); }
.cb-rooms__actions{ display:flex; flex-wrap:wrap; justify-content:flex-start; gap:var(--cb-flow-actions); width:calc((100% - var(--cb-space-split-desktop)) / 2); margin-top:var(--cb-flow-group); }
.cb-rooms-head__title,.cb-rooms-head__script{ text-align:left; }
.cb-rooms--measuring,.cb-rooms--measuring *{ transition:none !important; }

.cb-events{ padding:var(--cb-space-section-desktop) calc(var(--cb-rail-gutter) + var(--cb-page-inset)); } /* DS66 */
.cb-events__content,.cb-events__lead{ display:flex; flex-direction:column; align-items:center; gap:0; width:min(100%,var(--cb-container-max)); margin-inline:auto; }
.cb-events__lockup{ display:flex; flex-direction:column; align-items:center; gap:var(--cb-flow-lockup); }
.cb-events__title,.cb-events__script{ margin:0; text-align:center; }
.cb-events__actions{ display:flex; flex-direction:row; flex-wrap:wrap; justify-content:center; gap:var(--cb-flow-actions); margin-top:var(--cb-flow-group); }
/* ⚠️ DEUX classes, pas une, et c'est la CAUSE de l'écart nul mesuré par Alexandre le
   2026-08-18 (boutons → paragraphe = 0 px, les deux se touchent). `.cb-events__copy` et
   `.cb-paragraph` ont la MÊME spécificité (0-1-0) et le même porteur ; `.cb-paragraph` est
   déclaré 140 lignes PLUS BAS et sa `margin` est un raccourci 4 valeurs — il remettait donc
   `margin-top` à 0. Aucun token ne valait 0 : c'était une collision d'ordre dans la couche 2,
   pas une règle absente. Qualifier la composition par l'atome qu'elle modifie (0-2-0) dit ce
   qu'elle est ET la met hors d'atteinte du raccourci, quel que soit l'ordre du fichier. */
/* ⚠️ `margin-inline: auto` N'EST PAS UN ORNEMENT ICI — régression signalée par
   Alexandre le 2026-08-19, et mesurée : le titre, le script et les deux boutons
   de ce bloc sont centrés à **x = 720** (le centre de 1440), et le paragraphe
   était à **x = 32, centre 390**. Cause : `max-width: var(--cb-measure)` est
   revenu avec `DS71` le 2026-08-18, et une boîte CONTRAINTE sans marge auto
   reste collée à gauche. Le texte, lui, était bien `center` — d'où des lignes
   centrées dans une boîte décalée, ce qui se voyait tout de suite.
   La règle générale, qui vaut au-delà de ce bloc : **une largeur maximale et un
   texte centré vont par paire avec `margin-inline: auto`.** Poser l'une sans
   l'autre décale la boîte sans décaler le texte. Vérifié sur 5 pages : c'est le
   SEUL paragraphe du site dans ce cas — les trois autres boîtes contraintes de
   l'accueil sont ferrées à gauche ET alignées à gauche, donc cohérentes. */
.cb-paragraph.cb-events__copy{ max-width:var(--cb-measure); margin:var(--cb-flow-group) auto 0; text-align:center; }

/* DS66 — l'exception « petit desktop » a disparu avec sa raison d'être. Elle
   ramenait le 80px de la composition Figma à 64 entre 768 et 1279px, pour que
   le texte ne touche pas le bord avant le basculement mobile. Le retrait
   horizontal d'une section vaut désormais `--cb-page-inset` (32px) à TOUTES
   les largeurs, comme celui du header : il n'y a plus de palier à rattraper,
   et une règle qui remettrait 64 ici casserait justement l'alignement
   demandé. Le 80px, lui, n'a pas disparu du bloc — il reste ce qu'il a
   toujours été, la gouttière ENTRE les deux colonnes
   (`--cb-space-split-desktop`), pas un retrait de bord. */

@media (max-width:767px){
  .cb-intro{ padding:var(--cb-space-section-mobile) var(--cb-page-inset); }
  .cb-intro__content{ gap:var(--cb-flow-group); }
  .cb-intro__details{ grid-template-columns:1fr; gap:var(--cb-flow-group); }
  .cb-intro__tags{ justify-content:flex-start; }
  .cb-intro__brochure{ min-height:auto; aspect-ratio:16 / 9; padding:var(--cb-flow-inline); }
  .cb-rooms-head{ padding:var(--cb-space-section-mobile) var(--cb-page-inset) 0; }
  .cb-rooms-head__inner{ grid-template-columns:1fr; gap:var(--cb-flow-group); }
  .cb-rooms-head__title-group{ align-items:center; }
  .cb-rooms-head__copy{ max-width:var(--cb-measure); margin-inline:auto; }
  .cb-rooms{ --cb-rooms-inset:var(--cb-page-inset); --cb-rooms-padding:var(--cb-flow-group) var(--cb-rooms-inset) var(--cb-space-section-mobile); min-height:0; padding:var(--cb-rooms-padding); }
  .cb-rooms__media-layer{ display:none; }
  .cb-rooms__item,.cb-rooms__actions{ width:100%; }
  /* Empilé, il n'y a plus de colonne à côté : ni réserve de décalage, ni
     glissement, ni hauteur à réserver — l'accordéon reprend sa hauteur libre. */
  .cb-rooms__item{ padding-inline-end:0; }
  .cb-rooms__item--last{ margin-block-end:0; }
  .cb-rooms__item.is-active .cb-rooms__label,
  .cb-rooms__item.is-active .cb-rooms__details{ transform:none; }
  .cb-events{ padding:var(--cb-space-section-mobile) var(--cb-page-inset); }
  .cb-events__actions{ justify-content:flex-start; }
  .cb-paragraph.cb-events__copy{ text-align:left; }
}

@media (prefers-reduced-motion: reduce){
  .cb-rooms__details,.cb-rooms__label,.cb-rooms__index,.cb-rooms__details-inner > *,.cb-rooms__media-layer{ transition-duration:var(--cb-interaction-duration-reduced); transition-delay:0s; }
}

/* =====================================================================
 * .cb-heading — heading/h2, heading/h3 (DS09)
 *
 * Le TITRE AUTONOME : un h2/h3 qui n'appartient à aucun lockup (pas de
 * DianaWebber Script accolé). Manque nommé noir sur blanc par `H02`
 * (tickets/journal/H02.md § « Hors périmètre déclaré ») : le widget
 * `heading` d876828 de la homepage (« BIENVENUE AU DOMAINE DE CAMBOYER »)
 * est resté non traité faute d'atome — la maquette montrait un
 * `.cb-title-lockup--center` canonique, mais le DOM réel intercale une
 * image entre le titre et son script (docs/prod.md § 13, H02), ce qui
 * exclut `.cb-title-lockup` sans restructurer l'arbre (hors mandat de ce
 * ticket). Le titre lui-même — Fonseca, capitales, seul — n'a besoin
 * d'aucune restructuration : c'est exactement ce que couvre `.cb-heading`.
 *
 * Échelle et traits déjà TRANCHÉS par `docs/typographie.md` (DS03), repris
 * ici sans nouvelle décision :
 *   - `--cb-text-h2`/`--cb-text-h3` (§ 1, l'échelle Astra plafonnée) ;
 *   - `uppercase` (§ 5 point 9, trois sources convergentes : Astra, le kit,
 *     le brand book — confirmé, pas un parti pris) ;
 *   - `line-height: 1.5` (§ 5 — recommandation pour LE KIT sur h1-h3, donc
 *     déjà générale au palier de titrage, pas limitée au lockup) ;
 *   - poids 300 (Fonseca Light) — même réserve de rendu que
 *     `.cb-title-lockup__title` (DS14) : c'est la graisse de charte, pas le
 *     repli 400 que `docs/typographie.md` § 5 propose UNIQUEMENT pour le
 *     kit Elementor (tant que le serveur ne sert pas Light, `DS00b`) — cet
 *     atome, comme les 3 atomes DS14 déjà en place, vise la graisse
 *     correcte, pas le repli technique d'un système différent.
 *   - couleur `--cb-encre`, PAS `--cb-lockup-titre` — ce dernier token est
 *     explicitement borné aux 3 lockups par son propre commentaire dans
 *     `tokens.css » (« portée strictement limitée au titre de ces 3
 *     lockups ») ; `docs/typographie.md` § 5 recommande `--cb-encre` pour
 *     tout le reste du titrage.
 *
 * h4/h5/h6 n'ont pas de modificateur ici et n'en auront jamais par cet
 * atome : ils sont aliasés au corps (`Q09`, docs/typographie.md § 2.2/§ 4),
 * un rôle que `.cb-paragraph` couvre déjà. Un h4/h5 de sous-section dans un
 * contenu long (article de blog, docs/typographie.md § 4.1) reste possible
 * en dehors de cet atome — mais aucun besoin réel de ce cas précis n'a été
 * trouvé dans `docs/composants-prod.md`/`reference/site-actuel/` : pas
 * construit ici (AGENTS.md, « seulement ce dont le site a besoin »).
 * ================================================================== */
.cb-heading{
  margin: 0;
  font-family: var(--cb-font-caps);
  font-weight: var(--cb-weight-title); /* [Q27 : 200 — tout le titrage, décidé le 2026-08-12] Fonseca Thin, même écart assumé et même réserve de rendu que .cb-title-lockup__title */
  text-transform: uppercase; /* confirmé, docs/typographie.md § 5 point 9 */
  line-height: 1.5; /* docs/typographie.md § 5 — recommandation h1-h3, pas seulement le lockup */
  color: var(--cb-encre); /* pas --cb-lockup-titre, portée limitée à son atome — docs/typographie.md § 5 */
}
.cb-heading--h2{ font-size: var(--cb-text-h2); }
.cb-heading--h3{ font-size: var(--cb-text-h3); }

/* ---- --on-dark : le même titre, sur une section sombre — REG01 § 1.
 *
 * Ce modificateur était NOMMÉ comme manquant depuis DS12 (voir en tête de
 * fichier : « un titre d'overlay compose `.cb-heading--on-dark` ») et n'avait
 * jamais été écrit, parce qu'aucun bloc n'en avait eu besoin. La migration du
 * 2026-08-18 en a créé le besoin en trois endroits : elle a vidé la couleur que
 * le widget portait, et le défaut qui a reprit la main est le `--cb-encre` de
 * `.cb-heading` — sur un fond encre. Trois titres ont donc disparu, dont un dans
 * un bloc global, c'est-à-dire sur toutes les pages du site.
 *
 * ⚠️ POURQUOI CE MODIFICATEUR ET PAS `.cb-section--dark` SUR LA SECTION. La
 * section sombre existe déjà comme composition, et elle aurait résolu le défaut.
 * Mais elle repeint AUSSI paragraphes, pastilles et labels, dont aucun n'est en
 * défaut ici — ils tiennent leur couleur de leur propre réglage. Un correctif ne
 * déborde pas de son défaut : c'est la même retenue que `DS37` sur card/overlay.
 * Le jour où une section entière se refait, `.cb-section--dark` reste la bonne
 * réponse et ce modificateur devient inutile sur elle — les deux ne se battent
 * pas, ils disent la même chose.
 *
 * La valeur est `--cb-ivoire` et non le blanc pur que sert la production
 * (`rgb(255,255,255)`) : le blanc pur n'est pas dans la palette (charte.md
 * § 2.2), et l'ivoire est ce que le système emploie partout ailleurs sur fond
 * sombre — `.cb-title-accent--on-dark` juste au-dessus, `--fill-on-dark`,
 * `--ghost-on-dark`. Écart assumé, ~2 % par canal. */
.cb-heading--on-dark{ color: var(--cb-ivoire); }

/* ---- --h1 (H01) — le h1 DU HERO, et il est plus discret que ses cadets.
 *
 * Le seul h1 que ce site ait à rendre aujourd'hui est celui du hero d'accueil
 * (« Le Domaine de Camboyer », widget `776e36e`) — `H01a` § 3. Il est resté
 * sans atome jusqu'ici : `DS09` a livré h2 et h3, `DS27` le titre d'accent,
 * personne le h1.
 *
 * TROIS ÉCARTS avec `.cb-heading`, tous les trois mesurés sur `40000182:18461`,
 * et le premier est celui qui compte :
 *   1. **la graisse** — Fonseca **Thin** (`--cb-weight-display`, 200), là où
 *      tout le titrage courant est en Light (300). Décision d'Alexandre :
 *      « c'est vrai que sur le DS c'était 300, on va passer le DS à 200 et
 *      répercuter proprement ». La portée retenue pour ce 200 est le palier
 *      display, pas tout le titrage — argument complet dans `tokens.css`
 *      § graisses, à confirmer en revue (`Q27`).
 *   2. **la taille** — `--cb-text-title-hero` (32px), PAS `--cb-text-h1`
 *      (clamp jusqu'à 64px). Ce h1 est volontairement petit : il partage sa
 *      taille avec le titre d'accent posé à l'autre bout de l'écran, dont il
 *      est le pendant. Un clamp à 64px le ferait dominer une composition
 *      construite sur l'équilibre des deux.
 *   3. **l'interligne** — 1,2 au lieu de 1,5. Mesuré (`lineHeight: 120 %`), et
 *      cohérent avec un titre sur deux lignes qui doit se lire comme un bloc.
 *
 * `text-transform: uppercase` est hérité de `.cb-heading` et NE fait rien de
 * visible ici : la frame le rend en capitales alors que son `textCase` vaut
 * `ORIGINAL` et que la chaîne est « Le Domaine\nde Camboyer » — c'est Fonseca
 * qui dessine des capitales dans les deux casses. On le laisse : il rend le
 * résultat indépendant de la police effectivement servie (si Fonseca ne charge
 * pas, le repli sans-serif rendrait sinon un titre en minuscules).
 *
 * La coupure en deux lignes est du CONTENU (un saut de ligne explicite dans le
 * nœud Figma comme dans le widget), pas une largeur à contraindre : aucun
 * `max-width` ici. ------------------------------------------------------- */
.cb-heading--h1{
  font-weight: var(--cb-weight-display);
  font-size: var(--cb-text-title-hero);
  line-height: 1.2;
}

/* .cb-heading--on-dark — DS12, ajout minimal. `.cb-heading` visait un fond
   clair par défaut (--cb-encre, "pas --cb-lockup-titre" ci-dessus) — jamais
   un fond sombre. card/overlay (agencement carte posé sur une photo,
   docs/composants-prod.md §1.1-A) en a besoin pour de vrai : la carte ne
   redéclare pas cette couleur en local, elle compose ce modificateur, sur
   le même modèle que .cb-link--on-light/--on-dark (DS06). Contraste au pire
   cas estimé (photo + dégradé --cb-olive à 80%) : voir tickets/journal/
   DS12.md. */
.cb-heading--on-dark{ color: var(--cb-ivoire); }

/* =====================================================================
 * .cb-paragraph — paragraph/body, paragraph/lead (DS09, absorbe DS19)
 *
 * Le PARAGRAPHE DE CORPS, manque nommé par `H02` (le text-editor 74814fbe
 * du même bloc Bienvenue, 3 paragraphes réels, resté non traité faute
 * d'atome de prose — tickets/journal/H02.md).
 *
 * `--lead` (20px, `--cb-text-lg`) : le paragraphe d'ACCROCHE. Pas un
 * nouveau parti pris — `tokens.css` réservait déjà ce rôle à ce token
 * (« paragraphe d'accroche / intro de bloc (hero, "Bienvenue" —
 * docs/blocs-homepage.md) ») et `docs/typographie.md` § 1 le classait
 * « hors périmètre » de `DS03`, renvoyé explicitement à `DS09`. L'emploi
 * réel visé est la première phrase du bloc Bienvenue elle-même :
 * « Poussez les grilles du Domaine et laissez-vous envelopper par la
 * quiétude du lieu. » (reference/site-actuel/homepage.md L226) — une seule
 * phrase courte, distincte des deux paragraphes de corps qui suivent.
 *
 * ⚠️ Largeur de mesure — RETIRÉE de cet atome le 2026-08-17. `DS19` posait
 * `max-width: var(--cb-measure)` (55ch) ici, donc sur tout paragraphe du
 * site ; Alexandre l'a tranchée (« je vois pas d'intérêt »). La prod ne
 * bornait aucun `<p>` non plus (CSS servi vérifié, `reference/site-actuel/
 * css/`). Le token survit pour les compositions qui la demandent
 * explicitement — `.cb-intro__copy`, `.cb-rooms-head__copy`,
 * `.cb-events__copy` — où la borne est un choix de bloc, pas un défaut.
 * Un paragraphe non borné prend donc la largeur de son container : c'est à
 * la composition de la limiter si elle le veut.
 *
 * Marge inter-paragraphes : un widget `text-editor` réel embarque souvent
 * PLUSIEURS `<p>` (le bloc Bienvenue en a 3 dans un seul widget — vérifié
 * sur le DOM homepage, `homepage.html` L1533). `--cb-space-sm` (12px,
 * déjà nommé pour un autre usage dans `tokens.css`, aucune valeur nouvelle
 * créée — même réemploi que `.cb-tag--s`, DS07) sépare les paragraphes
 * successifs ; le dernier n'ajoute pas de marge basse parasite (l'espace
 * après le widget est le rôle de `--cb-space-inset-*`, docs/spacing.md § 4,
 * pas de ce paragraphe).
 * ================================================================== */
.cb-paragraph{
  margin: 0 0 var(--cb-space-sm) 0; /* (parti pris, réemploi) — gap entre paragraphes successifs d'un même widget */
  max-width: var(--cb-measure); /* DS71 2026-08-18 — REVENUE, et plus large (80ch, cf. tokens.css). Retirée
     le 2026-08-17 « je vois pas d'intérêt » ; ce qui l'a rouverte est la mesure d'Alexandre :
     1376px sur le paragraphe du bloc Événements, ~154 caractères par ligne. ⚠️ Sur un widget
     Elementor cette ligne ne suffit PAS — `.e-con.e-con > .e-con-inner > .elementor-widget`
     (0-4-0) impose `max-width:100%` ; elle est mirroitée en couche 3. */
  text-wrap: pretty; /* DS42 — LIGNES VEUVES. Mesuré sur le contenu réel des cartes :
     « La Prada, n.f. Occitan : La Prairie. » laissait « Prairie. » seul sur sa 2ᵉ ligne
     (375 et 900px), le bloc Spa laissait « sérénité. » seul (900px) et « extérieure. »
     seul (1440px). Ce n'est pas un défaut de contenu — c'est le moteur de rendu qui
     coupe au plus court ; `pretty` lui demande d'éviter la dernière ligne à un mot.
     Aucune valeur, aucun token : une propriété que les navigateurs qui ne la
     connaissent pas ignorent sans rien casser. */
  font-family: var(--cb-font-body);
  font-size: var(--cb-text-base);
  line-height: 1.6; /* parti pris, docs/typographie.md § 5 point 8 — même valeur que le corps du kit */
  color: var(--cb-encre);
}
.cb-paragraph:last-child{ margin-bottom: 0; }
.cb-paragraph--lead{
  font-size: var(--cb-text-lg); /* 20px — tokens.css, déjà réservé à ce rôle */
}

/* --on-dark (H01) — le frère manquant de `.cb-heading--on-dark` (DS12) et de
   `.cb-tag--on-dark`. Même contrat, étroit : un paragraphe posé DIRECTEMENT sur
   une image ou une vidéo, hors de toute section — dans une `.cb-section--dark`
   la section corrige déjà ses enfants, et le doubler serait un doublon.
   Consommateur réel : le paragraphe du hero sur la vidéo (`40000182:18460`,
   ivoire mesuré). Il n'existait pas parce qu'aucun bloc n'avait encore posé de
   prose sur un média — le hero est le premier.
   ⚠️ Ce n'est pas ce modificateur qui rend le texte lisible : c'est le voile
   (`.cb-scrim`). Sans lui, ce même ivoire tombe à 1,25:1 sur la vidéo nue
   (docs/preuves/H01/mesure-scrim.py). */
.cb-paragraph--on-dark{ color: var(--cb-ivoire); }

/* =====================================================================
 * .cb-list — list/default (DS09)
 *
 * La LISTE éditoriale : un `<ul>`/`<li>` typé dans l'éditeur WYSIWYG d'un
 * widget `text-editor` — pas les pastilles-boutons détournées (`.cb-tag`,
 * DS07, déjà couvertes) ni l'`icon-list` natif d'Elementor Pro (widget à
 * part, hors mandat de cet atome, docs/composants-prod.md § 1.1). Contenu
 * de démo sourcé sur les pastilles RÉELLES de la homepage (vérifiées mot
 * pour mot, reference/site-actuel/homepage.md L390-398/L330-340/L354-366) :
 * Spa (4 items), Mariages (5 items), Séminaires (6 items) — exactement les
 * trois exemples nommés par le ticket (« prestations spa, preuves mariage,
 * chiffres séminaire »). Aucun de ces trois blocs ne rend AUJOURD'HUI ces
 * faits en `<ul>` réel (ils sont posés en boutons détournés, `.cb-tag`) —
 * cet atome ne prétend pas remplacer ce DOM existant, il donne au contenu
 * éditorial de prose (un widget `text-editor` isolé, un article, une page
 * de contenu long) l'alternative sémantique qui manque quand le contexte
 * n'est plus une carte compacte mais du texte courant — signalé tel quel
 * dans tickets/journal/DS09.md, pas caché.
 *
 * Puce native (`disc`), couleur héritée (`currentColor` via `::marker`
 * dans les navigateurs modernes) — aucune décoration de marque : la
 * sobriété du brief ne demande rien de plus qu'un point.
 * ================================================================== */
.cb-list{
  margin: 0;
  max-width: var(--cb-measure); /* DS71 2026-08-18 — revenue avec celle de `.cb-paragraph` : une liste
     éditoriale est de la prose, elle se lit à la même mesure. Mirroitée en couche 3 (même motif). */
  padding-left: var(--cb-space-lg); /* (parti pris, réemploi) — indentation de la liste, même logique de réemploi que .cb-tag--s (DS07) */
  font-family: var(--cb-font-body);
  font-size: var(--cb-text-base);
  line-height: 1.6;
  color: var(--cb-encre);
}
.cb-list li + li{
  margin-top: var(--cb-space-sm); /* (parti pris, réemploi) — gap entre items */
}

/* =====================================================================
 * .cb-card — card/overlay, card/split, card/stacked (DS12)
 *
 * UN composant, décliné en agencements — pas une famille (docs/composants-
 * prod.md § 1.1 : ~48 instances réelles sur 15 pages, un seul motif —
 * média + titre + pastilles-boutons + texte + CTA — recopié-collé et
 * réglé à la main à chaque usage). Cet atome ne redéclare AUCUNE des cinq
 * briques, toutes déjà en place plus haut dans ce fichier : le média est
 * `.cb-img`, le titre est `.cb-heading` OU `.cb-title-lockup` (les deux
 * widgets réels — `heading` et `uael-advanced-heading` — coexistent dans le
 * relevé, `reference/cartographie-homepage.txt` conteneurs `12c5a5ce`
 * "Charme" et `85f3224` "Spa & bien-être" ; DS26, elementor-bridge.css § 5),
 * les pastilles sont `.cb-tag`, le texte est `.cb-paragraph`, le CTA est
 * `.cb-btn`/`.cb-link`. Ce que la carte AJOUTE, et seulement ça : la boîte,
 * le rythme vertical/horizontal entre ses parties, et les agencements réels.
 *
 * Structure neutre — 4 classes de composition, jamais de style de marque
 * dessus (couleur/typo restent aux atomes qu'elles contiennent) :
 *   .cb-card(--overlay|--split|--stacked)
 *     .cb-card__media          → contient .cb-img (absent sur --overlay,
 *                                  voir plus bas)
 *     .cb-card__body           → contient le titre, .cb-card__tags,
 *                                  .cb-paragraph ×1-2, .cb-card__cta
 *       .cb-card__tags         → contient .cb-tag ×3-10
 *       .cb-card__cta          → contient .cb-btn/.cb-link ×0-2
 *
 * Aucun fond, aucun radius, aucun padding de CADRE sur --split/--stacked :
 * aucune occurrence prod ne mesure une couleur de fond ni un rayon PROPRES
 * au conteneur carte lui-même (seul le rayon de l'IMAGE est mesuré, déjà
 * porté par `.cb-img`, docs/images.md § 5.1) — en inventer un serait une
 * valeur en dur sans source. `--overlay` fait exception délibérée : SA
 * boîte a un rayon et un fond mesurés (voir plus bas), parce que sa boîte
 * EST la photo, pas un cadre autour d'elle.
 *
 * Rythme interne — deux tokens, deux rôles, jamais interchangés (la seule
 * lecture qui couvre les deux emplois, docs/spacing.md § 3/§ 4.2) :
 *   - `--cb-space-inset-*` : à L'INTÉRIEUR d'un groupe EMPILÉ — titre →
 *     pastilles → texte → CTA, cité littéralement par spacing.md § 4.2 ;
 *     et, par la même logique (empilé, pas côte à côte), entre le média et
 *     le corps quand l'un est SOUS l'autre (`--stacked`, et `--split`
 *     repassé en une colonne sous 768px) — (parti pris) cette dernière
 *     extension n'est pas nommée mot pour mot par spacing.md (qui ne cite
 *     que titre/pastilles/texte/CTA), mais suit la même règle générale.
 *   - `--cb-space-gap-*` : entre éléments CÔTE À CÔTE — la rangée de
 *     pastilles (spacing.md § 3, cité littéralement « ligne de tags »), et
 *     le média/corps de `--split` en 2 colonnes desktop — MESURÉ, pas
 *     arbitré : docs/images.md § 2 calcule la largeur d'image 2-up par
 *     `(1490 − 2×64 − 48) / 2`, où `48` est très précisément
 *     `--cb-space-gap-desktop`, l'écart entre les deux colonnes.
 * ================================================================== */
.cb-card{
  display: flex;
  /* PAS de `width` (DS56, 2026-08-12) — retiré après une régression réelle.
     Il valait `100%`, avec l'intention « remplit sa colonne ». Sur du DOM
     Elementor, la largeur de colonne est portée par le container lui-même
     (`width: 50%` sur les 4 salons de la page réception, soit une grille 2×2) :
     un `width:100%` en `!important` dans le pont l'écrasait et repliait la
     grille sur une seule colonne. Mesuré : 4 cartes à 1152px au lieu de 576.
     C'est le principe de DS12 qui était trahi — « une grille de cartes est une
     propriété de la SECTION, pas du composant ». Une carte remplit ce qu'on lui
     donne ; elle ne décide pas de sa largeur. Un élément de bloc/flex le fait
     déjà nativement, la déclaration n'apportait rien en markup neutre. */
}

.cb-card__media{
  display: block;
}

/* LE FLUX D'UN CORPS DE CARTE (DS55) — une règle PAR RELATION, pas un gap
 * uniforme. C'était tout le défaut : un `gap` unique traite « titre → texte »
 * et « texte → rangée de CTA » comme la même chose, alors que ce sont deux
 * frontières de nature différente. Avec 64px, le corps de carte se lisait
 * comme cinq blocs séparés au lieu d'un bloc articulé.
 *
 * Mécanique : `gap: 0` + une marge posée sur le SECOND de chaque paire
 * (`* + *`), puis des exceptions par type de voisin. Les règles se lisent de
 * la plus générale à la plus spécifique, comme la cascade les applique.
 *
 * ⚠️ `gap` ET marge ne doivent JAMAIS coexister ici : mesuré avant correctif,
 * le gap de 64px s'ajoutait à la marge basse des `<p>` pour donner **76px
 * réels**. C'est pourquoi le gap passe à 0 et que tout se joue en marges. */
.cb-card__body{
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0; /* laisse le texte se compresser en flex plutôt que forcer la largeur du parent */
}
/* le pas par défaut : deux éléments de texte qui se suivent */
.cb-card__body > * + *{ margin-top: var(--cb-flow-text); }
/* une frontière de GROUPE : le texte cède la place à une rangée d'objets */
.cb-card__body > * + .cb-card__tags,
.cb-card__body > * + .cb-card__cta{ margin-top: var(--cb-flow-group); }
/* et symétriquement, ce qui suit une rangée d'objets repart au pas de groupe */
.cb-card__body > .cb-card__tags + *,
.cb-card__body > .cb-card__cta + *{ margin-top: var(--cb-flow-group); }
/* AUCUN enfant ne porte de marge basse : le flux est entièrement piloté par
   `margin-top`, et une marge basse s'y AJOUTERAIT. Mesuré après le premier
   correctif : la marge de 12px des `<p>` portait le pas de 16 à 28 et celui de
   32 à 44. Un flux mixte marge-haute/marge-basse ne se raisonne pas. */
.cb-card__body > *{ margin-bottom: 0; }

/* .cb-tag-row — LA RANGÉE DE PASTILLES, comme atome (DS69, 2026-08-18)
 *
 * Pourquoi elle manquait, et ce qu'elle corrige : les pastilles avaient un atome
 * (`.cb-tag`, DS07) mais leur RANGÉE n'en avait pas. Chaque container legacy
 * décidait donc seul de son alignement, et le relevé après migration l'a chiffré
 * sur `/hotel/` — les rangées des cartes portent `justify-content: center`
 * (containers `ffb6b97`, `220f74e`) là où notre `.cb-content-hero__tags` est en
 * `flex-start`. Deux dialectes sur la même page : dans une carte au texte ferré à
 * gauche, la rangée de pastilles partait du centre, et le bouton juste en dessous
 * repartait de la gauche. C'est exactement le défaut d'alignement signalé par
 * Alexandre le 2026-08-18.
 *
 * ⚠️ Un enfant ne peut pas corriger l'alignement décidé par son parent en CSS :
 * il fallait donc un atome POUR LE PARENT, et poser sa classe sur les containers
 * de rangée. Ce n'est pas un atome de confort, c'est la seule façon d'appliquer
 * la loi d'alignement de `DS66` à une rangée.
 *
 * Réemploie exactement les valeurs déjà mesurées par `.cb-intro__tags` (DS55) :
 * aucune valeur nouvelle, `--cb-flow-inline` (16px) mesuré dans les deux axes.
 */
.cb-tag-row{
  display: flex;
  flex-direction: row; /* affirmé, jamais supposé — cf. le pont : le défaut d'un container Elementor est `column` */
  flex-wrap: wrap;
  align-content: start;
  justify-content: flex-start; /* la rangée suit le ferrage du bloc — DS66 */
  gap: var(--cb-flow-inline);
  margin: 0;
  padding: 0;
  list-style: none;
}

.cb-tag-row--center{ justify-content: center; }

/* .cb-align--center — LE CENTRAGE EST UNE DÉCISION DE SECTION, pas un réglage de widget
 *
 * Demande d'Alexandre (2026-08-18), après que la migration a cassé le layout : « on
 * avait certaines sections avec une sous-section d'intro centrée […] on doit laisser
 * centré dans ces cas là les titres paragraphes boutons et stickers », et à l'inverse
 * « toutes les sections au format image texte / texte image, tout soit ferré à gauche
 * systématiquement ».
 *
 * ⚠️ **Ce que j'ai cassé et pourquoi, parce que la leçon est le vrai livrable** : la
 * migration a vidé `align` sur chaque titre et chaque bouton, au motif que « c'est la
 * composition qui décide ». Sauf que la composition ne portait PAS cette information :
 * la vider ne l'a pas déplacée, elle l'a **détruite**, et les sections centrées sont
 * reparties à gauche. Vider un champ local n'est légitime que si une couche supérieure
 * dit déjà la même chose.
 *
 * D'où cette classe, posée sur LE CONTAINER DE SECTION — une seule par section, au lieu
 * d'un champ sur chacun de ses widgets. `text-align` **hérite** en CSS : les titres et
 * la prose suivent donc sans rien porter. Seules les rangées en flex ont besoin d'être
 * nommées, parce que `justify-content` n'hérite pas.
 *
 * Le défaut du système reste le ferrage à gauche : une section sans cette classe est
 * ferrée à gauche, ce qui est exactement la règle demandée pour les sections
 * image/texte.
 *
 * ⚠️ ET `text-align` NE SUFFIT PAS, mesuré le 2026-08-18 : sur les widgets rendus
 * `display: flex` — ce que notre pont fait des widgets bouton pour `.cb-tag` —
 * `text-align` ne positionne RIEN, et 118 pastilles sont restées collées au bord
 * gauche de leur colonne (écart mesuré -39 à -516 px). La production, elle, laisse
 * ces wrappers en `display: block`, où l'ancre `inline-block` est bien centrée par
 * `text-align`. Il faut donc dire les DEUX choses : `text-align` pour le texte qui
 * hérite, `justify-content` pour les boîtes flex qui ne l'écoutent pas.
 */
/* .cb-flow--group — LE RYTHME DE GROUPE SUR UNE PILE QUELCONQUE (`REG11`).
 *
 * ⚠️ Pourquoi cet utilitaire existe, et ce n'est pas une préférence : relevé au
 * rendu sur 21 pages, **77 blocs composés** (titre → pastilles → prose → CTA)
 * posent leur écart vertical EN DUR dans Elementor, et **aucun ne tient d'un
 * token**. Deux valeurs cohabitent — 20 px sur 72 blocs, 40 px sur 2, dont le
 * bloc « avant-footer » qui est un modèle GLOBAL et se rend donc sur 18 pages.
 * C'est cette dernière qui se voit : le même motif respire deux fois plus large
 * d'une section à l'autre (signalé par Alexandre le 2026-08-19).
 *
 * ⚠️ La valeur retenue n'est pas un arbitrage neuf : le système a DÉJÀ tranché
 * ce cas pour les cartes. `.cb-card__body` pose `--cb-flow-group` (32) à chaque
 * frontière de rangée de pastilles ou de CTA, et `--cb-flow-text` (16) entre
 * deux textes. Ces 77 blocs sont la même forme sans la classe : les aligner sur
 * 32 les rend cohérents avec la règle que le système applique déjà ailleurs,
 * au lieu d'entériner un 20 que personne n'a choisi.
 * `(parti pris)` sur la VALEUR : `--cb-flow-group` est marqué non mesuré depuis
 * `DS55`. Si 32 est trop large à l'œil, c'est UNE ligne de token à changer, et
 * les 77 blocs suivent — c'est précisément ce que cet utilitaire achète.
 *
 * ⚠️ Ce n'est pas un atome et il n'a pas de carte de revue, au même titre que
 * `.cb-align--center` juste au-dessus : la règle « un atome non illustré n'existe
 * pas » vise les atomes, pas les utilitaires de mise en page. */
.cb-flow--group{ display: flex; flex-direction: column; gap: var(--cb-flow-group); }

/* ⚠️ LA NUANCE QUE LA TABLE EXIGE, et qui a demandé une seconde mesure : après
 * un TITRE, tout n'est pas une frontière de groupe. `docs/spacing.md` § 4bis dit
 * `--cb-flow-text` (16) entre un titre et le texte du même bloc, et
 * `--cb-flow-group` (32) seulement à la frontière d'une rangée de pastilles ou
 * de CTA — c'est exactement la règle que `.cb-card__body` applique déjà.
 * Mesuré : dans 9 de ces blocs, la rangée qui suit le titre est un SOUS-TITRE
 * (rendu en Teachers, vérifié au rendu — pas un script DianaWebber comme la
 * structure le laissait croire). Un gap uniforme de 32 l'écartait de son titre.
 * D'où le rattrapage ci-dessous, qui ne vise QUE ce cas : après un titre, une
 * rangée qui n'est pas une rangée de pastilles revient au pas de texte. */
.cb-flow--group > .cb-heading + *:not(.cb-tag-row):not(.cb-flow--lockup){
  margin-top: calc(var(--cb-flow-text) - var(--cb-flow-group));
}

/* .cb-flow--lockup — LE SCRIPT QUI COLLE AU TITRE QUI LE PRÉCÈDE (`REG13`).
 *
 * ⚠️ Ce cas était CONNU et laissé de côté : `mapping-reg11-flux-groupe.py` écrit
 * noir sur blanc que les blocs à écart 0 dont l'ordre est titre → prose →
 * pastilles « demandent leur propre règle, et le relevé ne dit pas encore
 * laquelle ». Alexandre l'a signalé trois fois le 2026-08-20 (« OFFREZ DU
 * BIEN-ÊTRE », « OFFREZ L'ÉVASION », « OFFREZ UN VOYAGE SENSORIEL ») : c'est
 * cette règle-là. Relevé au rendu sur les 21 pages : **25 blocs**, sur 17 pages,
 * enchaînent titre → script → prose [→ pastilles] avec **0 partout**.
 *
 * ⚠️ POURQUOI UNE CLASSE SUR LE SCRIPT ET PAS UN SÉLECTEUR MALIN. Le script
 * n'est reconnaissable au CSS par rien : ici c'est un CONTAINER Elementor qui
 * enveloppe un `heading`, sans classe à nous, et sa seule marque distinctive est
 * la police servie — que le CSS ne sait pas interroger. Une classe posée par la
 * table de mappage est donc la seule voie honnête ; elle dit ce qu'on sait, au
 * lieu de le deviner.
 *
 * ⚠️ Aucune valeur neuve, et surtout aucun arbitrage neuf : les trois paliers
 * sortent tels quels de `docs/spacing.md` § 4bis — titre → son script
 * `--cb-flow-lockup` (0, mesuré sur deux frames), script → prose
 * `--cb-flow-text` (16), prose → rangée de pastilles `--cb-flow-group` (32).
 * L'utilitaire ne fait que rendre au bloc le rythme que la table lui promet.
 *
 * ⚠️ Utilitaire de flux, pas atome : il ne peint rien (le script garde sa
 * police, sa taille et sa couleur d'où qu'elles viennent), il ne fait que
 * déclarer une adhérence. Même statut que `.cb-flow--group`. */
.cb-flow--group > .cb-flow--lockup{
  margin-top: calc(var(--cb-flow-lockup) - var(--cb-flow-group));
}
.cb-flow--group > .cb-flow--lockup + *:not(.cb-tag-row){
  margin-top: calc(var(--cb-flow-text) - var(--cb-flow-group));
}

/* .cb-flow--group--compact — LE MÊME BLOC, À L'ÉCHELLE D'UNE CARTE (`REG14`).
 *
 * Jugé à l'œil par Alexandre le 2026-08-20, trois captures à l'appui : « trop
 * d'espace à mon goût pour une petite card ». C'est le `(parti pris)` que
 * `REG11` avait explicitement laissé ouvert — « si 32 est trop large à l'œil,
 * c'est UNE ligne de token à changer, et les 84 blocs suivent ». Sa revue est
 * cette ligne-là ; simplement, elle ne vaut pas pour tous les blocs.
 *
 * ⚠️ POURQUOI UN MODIFICATEUR ET PAS UNE BAISSE DU TOKEN. Mesuré au rendu à
 * 1440 sur 18 pages, la largeur des 100 blocs `.cb-flow--group` servis est
 * franchement BIMODALE : **64 entre 384 et 576 px** (les colonnes d'une grille)
 * et **36 entre 714 et 1440** (les blocs de section) — aucun entre les deux.
 * Baisser le token aurait tassé les seconds avec les premiers : sur un bloc
 * large, la frontière de groupe passerait à 24 contre 16 pour le pas de texte,
 * et la hiérarchie que `docs/spacing.md` § 4bis construit cesserait de se lire.
 *
 * ⚠️ UNE SEULE LIGNE, ET C'EST VOULU : le modificateur ne redéclare aucune
 * règle, il REDÉFINIT LE TOKEN sur le bloc. Le `gap` de la couche 2, celui du
 * pont en `!important`, et les deux rattrapages de marge de `.cb-flow--lockup`
 * lisent tous `var(--cb-flow-group)` — ils suivent donc sans qu'on les touche,
 * y compris sous `.e-con-inner`, où la variable arrive par héritage. Écrire
 * quatre règles jumelles aurait été quatre occasions de diverger.
 *
 * ⚠️ Pas de container query, et ce n'est pas un oubli : essayé, mesuré, écarté.
 * Un `@container` interroge l'ANCÊTRE conteneur — un élément ne peut pas se
 * requêter lui-même. Poser `container-type` sur `.cb-flow--group` puis vouloir
 * changer SON `gap` ne marche pas (vérifié au rendu : 0 bloc affecté), et le
 * faire depuis le parent supposerait un parent qu'on ne contrôle pas. */
.cb-flow--group--compact{ --cb-flow-group: var(--cb-flow-group-compact); }

/* .cb-flow--actions — UNE RANGÉE DE CTA HORS CARTE (`REG13`, 2026-08-20).
 *
 * ⚠️ Pourquoi il manquait, alors qu'on croyait le cas couvert : le système a
 * TROIS rangées d'actions, et toutes les trois sont des **compositions**, donc
 * réservées à leur bloc — `.cb-intro__actions` (accueil), `.cb-events__actions`
 * (accueil), `.cb-card__cta` (dans une carte). Une section quelconque qui pose
 * deux boutons côte à côte n'avait rien à porter : elle retombait sur le défaut
 * d'un container Elementor, qui est `column`. Mesuré sur
 * `/domaine-mariage-toulouse/organisation-mariage/` (`044d22f`) : « Commencer ma
 * configuration » et « Plaquette d'information » empilés, signalé par Alexandre.
 *
 * ⚠️ Aucune valeur neuve : `--cb-flow-actions` (12) est le token déjà mesuré sur
 * Figma pour deux CTA d'une même rangée (`DS55`), et c'est exactement ce que
 * `.cb-card__cta` emploie. Le `wrap` est obligatoire, pas décoratif : deux CTA
 * longs sur un écran étroit doivent retomber l'un sous l'autre plutôt que
 * déborder — c'est le repli, pas la mise en page nominale.
 *
 * ⚠️ Utilitaire, pas atome : même statut que `.cb-align--center` et
 * `.cb-flow--group`, donc pas de carte de revue. Il n'invente aucune peinture —
 * ce sont les boutons qui la portent. */
.cb-flow--actions{
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cb-flow-actions);
}
/* La rangée suit le ferrage de son bloc : centrée dans un bloc centré, ferrée à
   gauche sinon. Écrit ici plutôt que dupliqué dans `.cb-align--center`, qui
   traite déjà `.cb-tag-row` de la même façon quelques lignes plus bas. */
.cb-align--center .cb-flow--actions{ justify-content: center; }

.cb-align--center{ text-align: center; }
.cb-align--center .cb-tag-row,
.cb-align--center .cb-card__tags{ justify-content: center; }

.cb-card__tags{
  display: flex;
  flex-direction: row; /* affirmé — REG01 § 4 : 6 rangées de cartes s'étaient empilées */
  flex-wrap: wrap;
  gap: var(--cb-flow-inline); /* 16px, MESURÉ sur Figma dans les DEUX axes (DS55) — était 24/48px, soit 3x trop : l'écart valait 77 % de la largeur d'une pastille */
  margin: 0;
  padding: 0;
  list-style: none;
}

.cb-card__cta{
  display: flex;
  flex-direction: row; /* affirmé — REG01 § 4 */
  flex-wrap: wrap;
  gap: var(--cb-flow-actions); /* 12px, MESURÉ sur Figma entre les 2 boutons de la frame validée (DS55) — était 24/48px */
  margin: 0;
}

/* ---- card/stacked — la base EXPLICITE : média au-dessus, corps dessous.
 * Sert de carte autonome ET d'unité répétée pour l'agencement C (grille
 * côte-à-côte, réception 4 salons / mariages 3 espaces, docs/composants-
 * prod.md § 1.1-C) — voir la décision sur (C) en pied de section : ce
 * n'est délibérément PAS un 3ᵉ modificateur séparé, `--stacked` suffit.
 * Nommé explicitement (plutôt que "juste ne pas mettre de modificateur")
 * pour rester repérable par un éditeur non technique dans Elementor — même
 * logique de nommage que `--split`/`--overlay`. --------------------------*/
.cb-card--stacked{
  flex-direction: column;
  gap: var(--cb-flow-group); /* média → corps : une frontière de groupe (DS55). Était --cb-space-inset-*, donc 64px en desktop — un padding de conteneur employé comme écart entre deux éléments. */
}

/* ---- card/split — agencement B, la forme DOMINANTE du site (mariages/
 * séminaires/spa/cuisine homepage, blocs secondaires hôtel/spa/réception/
 * restauration, sections d'équipement des 4 pages chambre — docs/
 * composants-prod.md § 1.1-B). Empilée sous 768px (une carte par
 * conteneur, jamais en grille — docs/images.md § 6.1 : les colonnes
 * passent en une colonne unique pleine largeur sous le breakpoint mobile),
 * 2 colonnes ÉGALES à partir de 768px — mesuré, pas arbitré (voir tête de
 * section, docs/images.md § 2). --------------------------------------- */
.cb-card--split{
  flex-direction: column;
  gap: var(--cb-flow-group); /* empilé sous 768px → même frontière de groupe que --stacked (DS55) */
}
@media (min-width: 768px){
  .cb-card--split{
    flex-direction: row;
    align-items: stretch;
    gap: var(--cb-space-gap-desktop); /* colonnes côte à côte → gap, mesuré docs/images.md § 2 */
  }
  .cb-card--split > .cb-card__media,
  .cb-card--split > .cb-card__body{
    flex: 1 1 0; /* deux colonnes égales — docs/images.md § 2, division stricte par 2 */
    min-width: 0;
  }
}

/* ---- card/overlay — RETIRÉ le 2026-08-11 (DS37), décision d'Alexandre.
 * NE PAS LE RECONSTRUIRE. Ce n'est pas un oubli, c'est un abandon décidé.
 *
 * L'atome existait (DS12) et relevait juste : les 4 cartes « NOS
 * HÉBERGEMENTS » de la homepage sont bien du texte sur photo avec un voile
 * dégradé, mesuré (`background_overlay` `#4B6146`, stop 60 %, opacité 0.8 —
 * docs/composants-prod.md § 1.1-A). Ce qui l'a tué, ce sont deux mesures
 * faites APRÈS, sur le contenu réel :
 *   - `H03` : la boîte ne tient pas le contenu. `aspect-ratio:3/2` +
 *     `overflow:hidden` donnent 377px de haut à la largeur réelle (566px) ;
 *     3 des 4 cartes réelles débordent, et le titre sort du cadre SANS
 *     erreur visible. Perte de contenu silencieuse.
 *   - `H03` encore : le contraste au pire pixel sur les 4 VRAIES photos —
 *     Charme 1,23:1 · Élégance 1,40:1 · Prestige 1,57:1 · Suite 2,82:1.
 *     Les quatre échouent même le seuil grand texte (3:1). DS12 n'avait
 *     mesuré qu'UNE photo (1,44:1 → corrigé à 4,77:1) et avait laissé la
 *     réserve écrite ; elle est levée, dans le mauvais sens.
 *
 * Conclusion : le voile ne peut pas rattraper 4 photos quelconques sans les
 * écraser, et un blindage plus dense avait déjà été écarté à l'œil par DS12
 * (« il coupait la photo d'une arête horizontale »). Donc on ne met plus de
 * texte sur photo : les 4 cartes chambres passent en `card/stacked` /
 * `card/split`, photo en vrai nœud média, texte en dessous → `DS41`.
 *
 * Le token `--cb-overlay-veil` part avec (tokens.css). Les 5 variantes
 * rendues et mesurées par DS12 restent dans `tickets/journal/DS12.md` : si
 * un jour un besoin de texte-sur-photo réapparaît, la mesure est là, mais
 * il faudra la refaire contre les photos de ce besoin-là.
 *
 * Conséquence à ne pas rater : ce retrait fait disparaître le SEUL
 * `linear-gradient` des 3 couches. Un dégradé qui réapparaîtrait dans le
 * design system est donc un accident, pas un héritage.
 * ---------------------------------------------------------------------- */

/* ---- (C) grille côte-à-côte — DÉCISION : PAS un 3ᵉ modificateur.
 * Le relevé (docs/composants-prod.md § 1.1-C) montre 2 endroits réels
 * (réception, 4 salons ; mariages, 3 espaces) où plusieurs cartes sont
 * posées côte à côte dans une grille — mais les deux grilles DIFFÈRENT
 * l'une de l'autre : réception a un `icon-list` natif et AUCUN CTA par
 * carte (un seul CTA partagé après la grille) ; mariages a un CTA PAR carte
 * et aucun `icon-list`. Ce ne sont pas deux réglages d'un même agencement,
 * ce sont deux blocs particuliers qui partagent seulement la mise en grille
 * — exactement l'avertissement d'AGENTS.md contre l'atome spéculatif : nommer
 * un `--grid`/`--side-by-side` ici obligerait à deviner LEQUEL des deux
 * gabarits divergents fait foi.
 * Ce que la grille (C) demande réellement, `card/stacked` ci-dessus le
 * fournit déjà sans rien ajouter : d'après `docs/images.md` § 2, l'image
 * d'une carte en grille 3-up occupe la largeur PLEINE de sa colonne
 * (422px, pas la moitié d'une colonne coupée en deux) — la carte y est donc
 * bien empilée (média en haut pleine largeur, corps dessous), pas coupée en
 * deux comme `--split`. Mettre plusieurs `.cb-card.cb-card--stacked` côte à
 * côte dans un conteneur `display:flex`/`grid` avec `gap: var(--cb-space-
 * gap-*)` est une composition de BLOC (grille de section), pas une
 * propriété de la carte — aucune classe supplémentaire n'est nécessaire ici,
 * conformément à `AGENTS.md` (« seulement ce dont le site a besoin »). Le
 * réglage un-CTA-partagé-vs-CTA-par-carte et l'`icon-list` restent des
 * différences de CONTENU du bloc, pas de l'atome carte. */

/* =====================================================================
 * .cb-cards-grid — LA GRILLE DE CARTES CÔTE À CÔTE (C03, 2026-08-19)
 *
 * ⚠️ CET ATOME REVIENT SUR LA DÉCISION ÉCRITE JUSTE AU-DESSUS (« grille
 * côte-à-côte — PAS un 3ᵉ modificateur … aucune classe supplémentaire n'est
 * nécessaire ici »), et il faut dire pourquoi, sinon une session future la
 * rétablira. Cette décision reposait sur une hypothèse : que le conteneur
 * porterait sa gouttière par son propre réglage de gap. MESURÉ le 2026-08-19
 * sur `/domaine-mariage-toulouse/espaces-de-reception/`, C'EST FAUX —
 * poser `column-gap: 48px` sur la section fait passer les 4 cartes sur
 * **4 rangées** au lieu de 2. Les cartes valent `width: 50%` : le retour à la
 * ligne se décide sur la taille HYPOTHÉTIQUE (570 + 48 + 570 > 1140), AVANT
 * tout rétrécissement — et le rétrécissement, lui, n'opère qu'À L'INTÉRIEUR
 * d'une ligne déjà formée. Ajouter `flex-shrink: 1` n'y change rien : essayé,
 * mesuré, toujours 4 rangées. Une gouttière ne peut donc pas être posée sur ce
 * conteneur sans que la largeur des colonnes en découle — c'est-à-dire sans un
 * atome qui tienne LES DEUX du même token.
 *
 * LE DÉFAUT QU'IL RÉPARE : la production donne `padding: 0 20px` à chacune des
 * 4 colonnes, ce qui fabrique 40px de gouttière visuelle entre deux textes et
 * entre deux images. En recevant `cb-card cb-card--stacked`, ces colonnes ont
 * vu leur champ `padding` vidé — à raison, c'est la règle des porteurs `.cb-*`
 * — et `.cb-card` ne déclare aucun padding, à raison aussi (« la carte ne
 * déclare aucune couleur ni aucune taille de texte »). Les deux règles sont
 * justes ; c'est la gouttière qui n'avait plus de propriétaire. La voici.
 *
 * ⚠️ CE QUE LA MESURE A CORRIGÉ DANS LE DIAGNOSTIC D'ORIGINE, et qui comptait :
 *   · « la prod est centrée, la préprod ferrée à gauche » → FAUX. `text-align`
 *     vaut `start` des DEUX côtés. Rien à recentrer.
 *   · « la gouttière est tombée de 40 à 3px » → FAUX aussi. Le `column-gap` de
 *     la section vaut **0 en production comme en préprod** ; les 40px n'ont
 *     jamais été un gap.
 * Ces deux points venaient d'une lecture de captures, pas d'un relevé.
 *
 * ⚠️ LA VALEUR VIENT DU TOKEN PRÉVU POUR ÇA, dont le commentaire nomme
 * littéralement ce bloc : `--cb-space-gap-*`, « gap d'une GRILLE DE CARTES
 * uniquement (chambres 2×2, **4 salons**, 3 espaces) ». Rien de neuf n'est
 * inventé, et la largeur se DÉDUIT de la gouttière au lieu d'être posée à côté
 * d'elle — c'est ce qui garantit qu'elles ne divergeront pas.
 * ===================================================================== */
.cb-cards-grid{
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--cb-space-gap-mobile);
  row-gap: var(--cb-space-gap-mobile);
}
/* Une seule colonne sous 768 : une carte y a besoin de toute la largeur. */
.cb-cards-grid > *{
  width: 100%;
}
@media (min-width: 768px){
  .cb-cards-grid > *{
    width: calc(50% - var(--cb-space-gap-mobile) / 2);
  }
}
@media (min-width: 1025px){
  .cb-cards-grid{
    column-gap: var(--cb-space-gap-desktop);
    row-gap: var(--cb-space-gap-desktop);
  }
  .cb-cards-grid > *{
    width: calc(50% - var(--cb-space-gap-desktop) / 2);
  }
}

/* =====================================================================
 * .cb-section — le commutateur clair/sombre + univers (DS31, décision Q14)
 *
 * DEUX livrables indissociables, un seul mécanisme :
 *   (1) `.cb-univers--*` — la couleur d'accent suit l'UNIVERS de la
 *       section (spa/bien-être, restauration, parc & activités, général
 *       hôtel, les 4 catégories de chambre), table docs/couleurs.md § 6.2,
 *       noms rattachés par Q22.
 *   (2) `.cb-section--light`/`--dark` — le FOND pilote titre, accent,
 *       bouton et son hover d'un seul coup : un widget ne les règle plus
 *       un par un, il se contente d'être posé DANS la bonne section.
 *
 * Comment les deux se combinent, sans combinatoire écrite à la main :
 * `.cb-univers--*` pose UNIQUEMENT `--cb-section-accent-light` (la valeur
 * pour un fond clair). `.cb-section--light` lit cette variable avec un
 * repli explicite (`var(--cb-section-accent-light, var(--cb-cta))`) — pas
 * d'univers déclaré = défaut clair de Q14, sans code séparé pour ce cas.
 * `.cb-section--dark` IGNORE délibérément `--cb-section-accent-light` et
 * fixe toujours `--cb-sauge` : c'est la décision de DS31 sur le trou de
 * `docs/couleurs.md` § 6.2 (voir tickets/journal/DS31.md pour l'argument
 * de rendu) — 3 des 4 couleurs d'univers échouent le contraste sur fond
 * sombre et la 4ᵉ (black writing) y est structurellement invisible ; les
 * variantes éclaircies testées (color-mix vers l'ivoire) convergent en un
 * gris ou un pastel à peine distinct de la sauge et entre elles — jugé AU
 * RENDU, pas sur le papier, et écarté. Le fond sombre NEUTRALISE donc
 * l'identité d'univers, pour TOUS les univers sans exception — c'est
 * `--cb-section-accent`, jamais une couleur d'univers en dur, que
 * `.cb-section__accent` (plus bas) consomme.
 *
 * Deux propriétés CSS personnalisées séparées (`--cb-section-accent-light`
 * vs `--cb-section-accent`) plutôt qu'une seule : elles n'appartiennent pas
 * à la même règle CSS (l'univers pose l'une, le fond pose l'autre) — les
 * fusionner obligerait soit une règle combinatoire par paire univers×fond
 * (8 univers × 2 fonds = 16 sélecteurs), soit un ordre de source fragile
 * entre deux classes de même spécificité posées sur le même élément. Deux
 * propriétés distinctes, chacune écrite par UNE seule famille de classes,
 * n'ont jamais à se disputer une priorité.
 *
 * Pourquoi ce n'est PAS un sélecteur nu sur `section`/`.elementor-section`
 * (AGENTS.md, couche 2 ignore Elementor) : le commutateur est une CLASSE
 * posée sur un container quelconque — même famille de piège/traduction que
 * `.cb-title-lockup`/`.cb-card`, couverte au pont (elementor-bridge.css).
 *
 * `.cb-section` ne force AUCUNE couleur de fond (ni ivoire/beige/vert/encre) :
 * le fond réel reste un choix d'auteur (background Elementor, ou un futur
 * atome de fond) — le switch ne fait que déclarer QUEL RÉGIME s'applique,
 * pas QUELLE couleur exacte. C'est voulu : ivoire et beige partagent le même
 * jeu de couleurs sûres (docs/couleurs.md § 3), vert et encre aussi — le
 * régime, pas le token de fond exact, est ce qui pilote le reste.
 * ================================================================== */
.cb-section{
  display: flex;
  flex-direction: column;
  gap: 0; /* DS55 — le flux se joue en marges, comme .cb-card__body : un `gap` uniforme mettait 64px entre un titre de section et son accent */
  padding: var(--cb-space-section-mobile) var(--cb-page-inset); /* DS66 — vertical : rythme de section ; horizontal : le retrait de PAGE, le même que le header. Même valeur qu'avant en mobile (32px), mais désormais pour la bonne raison. */
}
/* MÊME STRATÉGIE PAR RELATION que le corps de carte (DS55). Le pas par défaut
   d'une section est plus ample que celui d'une carte — c'est le seul palier
   que Figma mesure vraiment à ce niveau (80px, 3 occurrences). */
.cb-section > * + *{ margin-top: var(--cb-flow-group); }
.cb-section > * + .cb-card__tags,
.cb-section > * + .cb-card__cta,
.cb-section > .cb-card__tags + *,
.cb-section > .cb-card__cta + *{ margin-top: var(--cb-flow-group); }
/* le titre et son accent scripté forment UN lockup : ils se touchent, comme
   dans les 3 compositions de titre relevées (gap structurel 0, docs/lockup-titres.md § 2.1) */
.cb-section > .cb-heading + .cb-section__accent{ margin-top: 0; }
.cb-section > *{ margin-bottom: 0; }
@media (min-width: 768px){
  .cb-section{
    /* DS66 — le rail n'est ajouté qu'ici : la règle de base est mobile, et une
       gouttière y vaudrait zéro de toute façon (viewport < rail). */
    padding: var(--cb-space-section-desktop) calc(var(--cb-rail-gutter) + var(--cb-page-inset));
  }
}

.cb-section--light{
  color: var(--cb-encre); /* repli d'héritage pour tout texte direct sans classe propre */
  --cb-section-accent: var(--cb-section-accent-light, var(--cb-cta)); /* défaut explicite Q14 : dark orange si aucun univers déclaré */
}
.cb-section--dark{
  color: var(--cb-ivoire);
  --cb-section-accent: var(--cb-sauge); /* décidé DS31 : TOUJOURS sauge en sombre, univers ignoré — voir commentaire de tête de section */
}

/* .cb-univers--* — pose UNIQUEMENT la valeur CLAIRE de l'accent (voir
   pourquoi au commentaire de tête). Combinable avec .cb-section--light SUR
   LE MÊME élément (même convention que .cb-interactive--tone + .cb-btn) ;
   sans effet visible sous .cb-section--dark, qui l'ignore délibérément. */
.cb-univers--spa{ --cb-section-accent-light: var(--cb-univers-spa); }              /* spa · détente · piscine · bien-être */
.cb-univers--resto{ --cb-section-accent-light: var(--cb-cta); }                    /* restauration */
.cb-univers--parc{ --cb-section-accent-light: var(--cb-olive); }                   /* parc & activités */
.cb-univers--hotel{ --cb-section-accent-light: var(--cb-cta); }                    /* général hôtel, fond clair — même valeur que le défaut, nommé pour la lisibilité éditoriale (docs/couleurs.md § 6.2) */
.cb-univers--chambre-charme{ --cb-section-accent-light: var(--cb-univers-chambre-charme); }
.cb-univers--chambre-prestige{ --cb-section-accent-light: var(--cb-cta); }
.cb-univers--chambre-elegance{ --cb-section-accent-light: var(--cb-univers-spa); }
.cb-univers--chambre-suite{ --cb-section-accent-light: var(--cb-olive); }

/* .cb-section__accent — l'élément qui AFFICHE l'accent (le « highlight »
   nommé par Alexandre, Q14) : script DianaWebber, couleur pilotée par le
   switch+univers ci-dessus, jamais fixée en dur. Rôle distinct de
   `.cb-title-lockup__script`/`.cb-title-inline__script` (atoms.css, DS14) :
   ceux-là sont un accent FIXE (terracotta empilé / lockup-titre 80% en
   ligne), une décision antérieure et toujours valable pour ces 2 atomes —
   `.cb-section__accent` sert les FUTURS blocs dont l'accent doit suivre
   l'univers (Q14 « débloque la règle d'emploi finale du script pour
   DS09/DS13 »), sans réécrire les 3 lockups déjà tranchés. */
.cb-section__accent{
  margin: 0;
  font-family: var(--cb-font-script);
  font-weight: 400;
  font-size: var(--cb-text-section-accent);
  line-height: 1;
  color: var(--cb-section-accent);
}

/* Le TITRE, le TEXTE, les TAGS et le LIEN suivent le switch sans classe
   `--on-dark` dédiée : le widget déclare juste `.cb-heading`/`.cb-paragraph`/
   `.cb-tag`/`.cb-link` (bruts), et la section fait le reste. Spécificité
   2 classes (`.cb-section--dark .cb-heading`) bat toujours la règle 1 classe
   de l'atome nu, quel que soit l'ordre de source — pas de `!important`
   nécessaire ici (réservé au pont, où l'adversaire est Elementor). Le fond
   CLAIR n'a besoin d'AUCUNE règle : c'est déjà la couleur par défaut de ces
   atomes (encre/tag-bg) — seul le sombre est une déviation à corriger. */
.cb-section--dark .cb-heading,
.cb-section--dark .cb-paragraph,
.cb-section--dark .cb-list,
.cb-section--dark .cb-label{
  color: var(--cb-ivoire);
}
.cb-section--dark .cb-tag{
  background: var(--cb-ivoire); /* même choix que .cb-tag--on-dark (DS12) : opaque plutôt que --cb-tag-bg translucide, jamais vérifié sur un fond réellement sombre */
  color: var(--cb-encre);
}

/* Le LIEN nu (sans --on-light/--on-dark, DS06) suit aussi le switch — les
   deux variantes existantes restent utiles hors section (ex. un lien posé
   directement sur une photo, card/overlay) ; DANS une section, un simple
   `.cb-link` suffit. */
.cb-section--light .cb-link{ color: var(--cb-vert); }
.cb-section--light .cb-link:hover,
.cb-section--light .cb-link[data-demo-state="hover"]{ color: var(--cb-encre); }
.cb-section--dark .cb-link{ color: var(--cb-ivoire); }
.cb-section--dark .cb-link:hover,
.cb-section--dark .cb-link[data-demo-state="hover"]{ color: var(--cb-sauge); }

/* Le BOUTON SECONDAIRE (`.cb-interactive--fill`, DS11) : un widget qui pose
   cette seule classe (jamais besoin de choisir `--fill-on-dark`) devient
   automatiquement dark-safe sous `.cb-section--dark` — même recette de
   couleur que `--fill-on-dark`, appliquée par la section plutôt que par le
   widget. `--fill-on-dark` reste disponible pour un bouton hors section
   (ex. sur une photo). Le bouton PRIMAIRE (`--tone`) n'a besoin d'AUCUNE
   règle ici : fond opaque à lui, son contraste ne dépend jamais du fond
   derrière (DS17, déjà établi). */
.cb-section--dark .cb-interactive--fill{
  border-color: var(--cb-ivoire);
  color: var(--cb-ivoire);
}
.cb-section--dark .cb-interactive--fill:hover,
.cb-section--dark .cb-interactive--fill[data-demo-state="hover"]{
  background: color-mix(in srgb, var(--cb-ivoire) 15%, transparent);
  border-color: var(--cb-ivoire);
  color: var(--cb-ivoire);
}

/* =====================================================================
 * .cb-content-hero — hero éditorial des pages contenu (DS13 / poste 2)
 *
 * La frame Figma 40000182:17715 décrit une composition, pas une nouvelle
 * famille d'atomes : média à gauche, introduction à droite, puis — seulement
 * lorsque le hero d'origine est une vidéo — le film en pleine largeur sous
 * l'introduction. Le titre, l'accent, la prose, les tags, l'image et le régime
 * clair sont donc les atomes existants ; ce composant n'ajoute que leur grille.
 *
 * Écarts volontaires Figma → système :
 * - padding de section = --cb-space-section-* / --cb-space-inset-* plutôt que
 *   les 32px locaux de la frame ;
 * - séparation média/texte = --cb-space-split-desktop (80px) plutôt que 52px ;
 * - titre/accent = .cb-heading--h3 + .cb-section__accent, donc échelle et
 *   couleur d'univers du système plutôt que 20/48px et terracotta en dur ;
 * - image = ratio 3:2 et rayon image déjà décidés par DS25 ; vidéo = 16:9,
 *   seul ratio vidéo fourni par la frame elle-même (1920/1080).
 *
 * Aucun texte ni média n'est une décision de ce composant. Ils restent des
 * widgets Elementor natifs et éditables, déplacés sans être réécrits.
 * ================================================================== */
.cb-content-hero{
  background: var(--cb-ivoire);
  /* ⚠️ LE HERO NE PREND PAS LE RYTHME DE SECTION EN HAUT, et c'est mesuré, pas
     préféré. `.cb-section` pose --cb-space-section-desktop (112px) parce qu'une
     section ordinaire doit s'écarter de CELLE QUI LA PRÉCÈDE. Un hero n'a rien
     au-dessus de lui que le header : les 112px y deviennent une bande vide, ce
     qu'Alexandre a signalé au rendu le 2026-08-17 sur `/hotel/`. La frame
     40000182:17715 donne 32px. Le bas garde le rythme de section : lui a bien
     un bloc en dessous dont il doit s'écarter. */
  padding-top: var(--cb-space-hero-top);
}
.cb-content-hero__intro{
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  /* ⚠️ FERRAGE HAUT, PAS CENTRÉ — la frame 40000182:17715 pose le média ET le
     corps au MÊME y=32. `center` était un choix par défaut, pas un relevé : dès
     que le texte est plus haut que l'image (le cas de `/hotel/`, 874 caractères,
     le plus long des 15), il repoussait l'image de ~90px vers le bas et créait
     un bord haut en escalier — la moitié du « padding » signalé par Alexandre
     le 2026-08-17. Sur les pages où le texte est plus court, l'image reste plus
     haute que lui et c'est elle qui donne la hauteur : le ferrage haut est
     juste dans les deux sens. */
  align-items: start;
  gap: var(--cb-space-split-desktop);
  width: 100%;
}
/* `.cb-content-hero__media` retiré de cette liste le 2026-08-18 (`P03g`) : il
   portait le média DANS l'introduction, ce qui était propre à la composition
   écartée par `Q30`. Vérifié côté serveur avant de couper — la classe est
   présente sur **0 post**, là où `__intro` et `--bandeau` le sont sur 15. */
.cb-content-hero__body,
.cb-content-hero__copy,
.cb-content-hero__tags{
  min-width: 0;
  width: 100%;
}
.cb-content-hero__body{
  display: flex;
  flex-direction: column;
  gap: 0;
}
.cb-content-hero__body > * + *{ margin-top: var(--cb-flow-group); }
.cb-content-hero__body > .cb-heading + .cb-section__accent{ margin-top: 0; }
.cb-content-hero__body > *{ margin-bottom: 0; }
.cb-content-hero__tags{
  display: flex;
  flex-direction: row; /* affirmé — REG01 § 4 */
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--cb-flow-inline);
}
.cb-content-hero__motion{
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--cb-radius-md);
  background-position: center;
  background-size: cover;
}
.cb-content-hero__motion > video{
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 767px){
  .cb-content-hero__intro{
    grid-template-columns: minmax(0, 1fr);
    gap: var(--cb-flow-block);
  }
}

/* ---- .cb-content-hero--bandeau — LA MÊME INTRODUCTION, quand un bandeau la
 * précède (P03e, frame 40000265:1653). C'est la seconde composition proposée
 * pour le même rôle que `.cb-content-hero` : ici le média n'est plus DANS
 * l'introduction, il est un `.cb-bandeau` à fond perdu AVANT elle, et le texte
 * occupe les deux colonnes. Les deux compositions coexistent le temps de
 * l'arbitrage d'Alexandre ; l'une des deux disparaîtra.
 *
 * Deux différences avec la composition de base, et deux seulement :
 *
 * 1. LE RYTHME HAUT REDEVIENT CELUI D'UNE SECTION. `--cb-space-hero-top` (32px)
 *    n'existe que parce qu'un hero n'a QUE le header au-dessus de lui (P03c,
 *    signalé au rendu par Alexandre). Ici il a un bandeau : la section a de
 *    nouveau quelque chose dont s'écarter, donc le rythme de section reprend.
 *    Ce n'est pas un réglage de goût — c'est la raison d'être du token qui
 *    tombe.
 * 2. LA GRILLE PORTE DEUX COLONNES DE TEXTE, pas un média et un texte : le
 *    titre et son accent à gauche, la prose et les tags à droite. Mesuré sur la
 *    frame : 560 et 640, séparés de 80px — soit exactement
 *    `--cb-space-split-desktop`, déjà le gap de la composition de base, rien à
 *    ajouter. `7fr/8fr` conserve le rapport mesuré sans figer de largeur.
 *
 * Écarts volontaires avec la frame, identiques à ceux de la base et pour les
 * mêmes raisons : l'inset horizontal vient du rail de page (DS66, alignement
 * STRICT sur le header, 32px) et non des 80px locaux de la frame ; le padding
 * vertical vient de `--cb-space-section-*` et non des 80px de la frame ;
 * l'échelle du titre et la couleur de l'accent viennent du système
 * (`.cb-heading--h3` + `.cb-section__accent`) et non des 20/48px terracotta en
 * dur — c'est déjà l'arbitrage de DS13, la frame le repose à l'identique.
 * ------------------------------------------------------------------------- */
.cb-content-hero--bandeau{
  padding-top: var(--cb-space-section-mobile);
}
.cb-content-hero--bandeau .cb-content-hero__intro{
  grid-template-columns: minmax(0, 7fr) minmax(0, 8fr);
}
@media (min-width: 768px){
  /* 80px, pas les 112px du rythme de section : mesuré sur la frame et confirmé
     à l'œil par Alexandre (« 112 trop aéré », 2026-08-17). Le rôle est nommé
     par son token, il n'emprunte pas celui du flux. */
  .cb-content-hero--bandeau{ padding-block: var(--cb-space-hero-intro); }
}

/* ---- LE DÔME — l'ouverture du média vidéo au défilement (P03f)
 *
 * Frame `40000272:2142`, nœud `40000272:2247` : la vidéo part d'un **carré de
 * 616 × 616 centré** dans sa boîte de 1376 × 774, arche semi-circulaire en haut,
 * et s'ouvre jusqu'au 16:9 entier. Les deux retraits mesurés vivent dans
 * `tokens.css` ; le côté du dôme n'est pas déclaré, il EST ce qu'ils laissent.
 *
 * ⚠️ C'EST UNE FENÊTRE QUI S'OUVRE, PAS UNE VIDÉO QUI GRANDIT — un `clip-path`
 * sur la BOÎTE, et rien d'autre. Trois raisons, et la troisième est décisive :
 *   1. **rien ne bouge dans la mise en page.** La boîte garde sa hauteur finale
 *      du début à la fin. Un bloc qui grandirait en hauteur pousserait tout ce
 *      qui le suit à CHAQUE image de défilement — un reflow continu, exactement
 *      ce que le plafond du bandeau évite par ailleurs (P03e).
 *   2. **une seule propriété, un seul nœud, et le MÊME dans les deux DOM.** La
 *      boîte existe des deux côtés (`.cb-content-hero__motion`), donc le pont
 *      n'a rien à re-cibler ; et le `clip-path` rogne aussi le fond posé sur la
 *      boîte (l'image de repli qu'Elementor peint sous la vidéo), qui sinon
 *      resterait plein cadre DERRIÈRE le dôme et ruinerait l'effet.
 *   3. **la source est un 720p.** Faire tenir la vidéo entière dans un carré
 *      l'agrandirait encore (elle est déjà étirée à 1376 de large) : le dôme
 *      sortirait plus flou que l'état final, ce qui est le contraire de
 *      l'intention. Une fenêtre ne redimensionne rien.
 *   `(question ouverte)` — l'autre lecture possible de « extension » est une
 *   vidéo qui grandit dans l'arche. À trancher à la revue ; la fenêtre est le
 *   parti pris, pour les raisons ci-dessus.
 *
 * ⚠️ LE RECADRAGE PENDANT L'OUVERTURE EST INHÉRENT ET VOULU : une fenêtre carrée
 * sur une image 16:9 en montre le centre. Le « sans crop » demandé porte sur
 * l'état FINAL, où la fenêtre couvre exactement la boîte, elle-même au ratio de
 * la vidéo.
 *
 * Deux garde-fous, dans cet ordre :
 *   - `prefers-reduced-motion` : pas de mouvement demandé → aucun mouvement, et
 *     le média rend directement son état final, entier ;
 *   - `@supports (animation-timeline: view())` : là où le navigateur ne sait pas
 *     lier une animation au défilement, l'état servi est **l'état final**, pas
 *     le dôme figé. Un média coupé serait une régression, pas une dégradation.
 *
 * La timeline est NOMMÉE et portée par la BOÎTE, pas par la couche animée. Avec
 * un `view()` anonyme sur la couche, la timeline se calculerait sur la boîte que
 * l'animation est justement en train de déplacer : l'entrée piloterait sa propre
 * mesure. La boîte, elle, ne bouge pas — c'est le repère stable.
 * ------------------------------------------------------------------------- */
/* Les DEUX formes s'écrivent avec le MÊME nombre de composants — 2 retraits, 8
   rayons — parce que l'interpolation d'un `clip-path` exige des formes
   compatibles : une écriture courte d'un côté (`round 5px`) et longue de
   l'autre, et le navigateur SAUTE d'un état à l'autre sans animer. */
@keyframes cb-dome-ouverture{
  from{
    clip-path: inset(var(--cb-dome-inset) var(--cb-dome-inset)
      round var(--cb-dome-radius) var(--cb-dome-radius) var(--cb-radius-md) var(--cb-radius-md)
      / var(--cb-dome-radius) var(--cb-dome-radius) var(--cb-radius-md) var(--cb-radius-md));
  }
  to{
    clip-path: inset(0 0
      round var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md)
      / var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md));
  }
}
/* ---- LA RETENUE — « pin » (P03f, 2ᵉ passe)
 *
 * Demande d'Alexandre : « 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 en profiter et
 * rester centré sur la vidéo. » C'est `pin: true` + `scrub` de ScrollTrigger
 * (GSAP) — obtenus ici **sans JavaScript** : le bloc média devient COLLANT et
 * centré, et la section gagne la course de défilement en padding bas. Pendant
 * cette course, l'écran ne montre que le média, et le dôme s'ouvre.
 *
 * ⚠️ LA TIMELINE CHANGE DE PORTEUR, ET C'EST OBLIGATOIRE : elle passe de la boîte
 * à la SECTION. Une `view-timeline` mesure la position de son sujet dans le
 * viewport — un élément épinglé ne bouge plus, donc sa propre progression
 * **gèle**. Le dôme se serait figé à l'instant précis où il commence à être
 * regardé. La section, elle, continue de défiler : c'est le seul repère qui
 * avance pendant la retenue.
 *
 * ⚠️ ET LA PLAGE PASSE SUR `contain` : c'est, par définition, l'intervalle où le
 * sujet couvre entièrement le viewport — donc exactement la fenêtre pendant
 * laquelle la section est plus haute que l'écran et le média épinglé. Les 65 %
 * laissent la fin de la retenue sur un média entier, plutôt que de la faire
 * finir pile au relâchement.
 *
 * ⚠️ LE PIN VIT DANS LE MÊME `@media`/`@supports` QUE L'ANIMATION, et ce n'est
 * pas de la coquetterie : sans mouvement (mouvement réduit, ou navigateur sans
 * timeline de défilement), une retenue n'aurait plus rien à laisser voir — et
 * les 800px de course deviendraient une grande bande d'ivoire vide. Pas
 * d'animation, pas de retenue.
 * ------------------------------------------------------------------------- */
/* ⚠️ 32px DE MARGE SUR LES QUATRE CÔTÉS, ET LE PLEIN CADRE QUI PRIME — demandé
   par Alexandre au rendu (« une cohérence de padding autour de la vidéo : 32px de
   chaque côté ; là il en manque juste en dessous », puis « il faut forcer le full
   width quitte à cropper la vidéo »). Mesuré avant de corriger : il restait
   **10px** sous la vidéo à 1440 × 900, et elle **débordait de 42px** à 1440 × 800.

   Les 32px latéraux viennent du rail de page. En vertical, ils n'existent que si
   la boîte tient dans `écran − header − 2 × 32` : c'est ce plafond.

   ⚠️ ET LE PLAFOND EST SUR LA HAUTEUR, DONC IL RECADRE — c'est l'arbitrage
   explicite d'Alexandre, et il vaut d'être compris : un 16:9 plein cadre et 32px
   sur les quatre côtés ne coexistent que si l'écran a exactement le bon rapport.
   Borner la LARGEUR aurait gardé le ratio intact mais rétréci la vidéo (jusqu'à
   846px de large sur un écran de 644px de haut) ; borner la HAUTEUR garde le
   plein cadre et rogne le haut et le bas de l'image (`object-fit: cover` s'en
   charge). Le plein cadre a gagné. Écran haut : le plafond ne mord pas, la boîte
   reste en 16:9 exact. -------------------------------------------------------*/
.cb-content-hero--bandeau .cb-content-hero__motion{
  --cb-dome-sous-header: var(--cb-header-h, var(--cb-header-colle-mobile));
  --cb-dome-bande: calc(100svh - var(--cb-dome-sous-header) - 2 * var(--cb-page-inset));
  --cb-dome-boite-h: min(var(--cb-dome-boite-h-max), var(--cb-dome-bande));
  /* ⚠️ CETTE VARIABLE EXISTE POUR SURVIVRE AU MINIFIEUR, pas pour se relire plus
     facilement — et c'est une régression payée, pas une précaution (`REG13`,
     2026-08-20). Écrite en clair dans `top`, la formule sortait du minifieur de
     WP Rocket en `top:max(var(--cb-dome-sous-header)) calc(…))` : une virgule
     transformée en parenthèse fermante, donc une déclaration invalide, donc
     `top:auto` — et un `position:sticky` sans offset ne colle **jamais**. La
     vidéo s'ouvrait toujours (l'animation, elle, survivait) mais ne se retenait
     plus : exactement ce qu'Alexandre a vu. Le cache n'a rien cassé de notre
     mise en page, il a cassé UNE déclaration, et seulement celle-ci — mesuré,
     feuille minifiée contre feuille source, 0 autre écart sur la page.
     La valeur d'une propriété personnalisée n'est pas retouchée par le
     minifieur (`--cb-dome-boite-h` juste au-dessus en est la preuve servie) :
     y déplacer le terme composé rend `top` trivial — `max(var(), var())`, la
     forme que le minifieur traite déjà correctement ailleurs (`width:min(100%,
     var(--cb-container-max))`, servie intacte).
     ⚠️ Substitution : la valeur est résolue sur l'élément QUI LA DÉCLARE, avec
     la valeur CASCADÉE de `--cb-dome-sous-header` — donc l'override desktop
     ci-dessous s'y répercute sans qu'il faille redéclarer quoi que ce soit. */
  --cb-dome-top-centre: calc(var(--cb-dome-sous-header) + (100svh - var(--cb-dome-sous-header) - var(--cb-dome-boite-h)) / 2);
  max-height: var(--cb-dome-bande);
}
@media (min-width: 768px){
  .cb-content-hero--bandeau .cb-content-hero__motion{
    --cb-dome-sous-header: var(--cb-header-h, var(--cb-header-colle-desktop));
  }
}
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    .cb-content-hero--bandeau{
      view-timeline-name: --cb-dome;
      view-timeline-axis: block;
    }
    /* ⚠️ LA COURSE EST UN FRÈRE, PAS UN PADDING — et c'est une contrainte du
       modèle, trouvée au rendu, pas un choix d'écriture. Le rectangle de
       contrainte d'un élément COLLANT est la **boîte de contenu** de son bloc
       conteneur : un `padding-bottom` ne l'allonge donc pas d'un pixel. Mesuré
       sur la préprod avec 880px de padding bas : le média ne s'épinglait **pas
       du tout**, sa position suivait le défilement au pixel près, alors que
       `position: sticky` était bien calculé. Un frère qui SUIT le média, lui,
       allonge la boîte de contenu — d'où ce `::after`, qui est un vrai item de
       flux sans être un nœud du document (donc rien à écrire dans Elementor).
       ⚠️ ET IL NE S'AJOUTE QUE HORS ÉTAT FIGÉ : sur la page de revue, les cartes
       rendent le départ et l'arrivée, y ajouter 800px de course donnerait une
       bande d'ivoire vide sous chaque carte et une capture de référence qui
       décrit une page que personne ne verra.

       ⚠️ ET SEULEMENT S'IL Y A UN MÉDIA À ÉPINGLER — `:has(…__motion)`, ajouté
       le 2026-08-18. La condition d'origine n'excluait que l'état figé de la
       revue ; elle ne vérifiait jamais qu'une vidéo EXISTE. Tant que la seule
       page à bandeau était `/hotel/chambres/`, qui en a une, le défaut ne
       pouvait pas se voir. `P03b` a migré `/hotel/`, qui est une page IMAGE :
       Alexandre l'a vu au rendu (« tu as ajouté l'espace dédié pour la vidéo
       mais il n'y a pas de vidéo »), et c'est mesuré — 0 `<video>`, aucun
       `__motion`, et pourtant 800px de course. Une course sans rien à retenir
       n'est pas une retenue, c'est une bande d'ivoire vide.
       ⚠️ Ça concerne **9 des 15 pages** du poste 2 (`P03a` : neuf pages sont
       image seule, six ont une vidéo). */
    .cb-content-hero--bandeau:has(.cb-content-hero__motion):not(:has(.cb-content-hero__motion[data-demo-state]))::after{
      content: "";
      display: block;
      flex: 0 0 auto;
      width: 100%;
      height: var(--cb-dome-course-mobile);
    }
    .cb-content-hero--bandeau .cb-content-hero__motion{
      position: sticky;
      /* Centré dans la bande RÉELLEMENT disponible — l'écran MOINS le header
         collé, signalé par Alexandre : centré sur le viewport entier, le média
         passait sous le header (5px à 1440 × 900, 133px à 1440 × 644, mesurés).
         `top` ne peut pas se référer à la hauteur de l'élément, d'où la boîte
         recalculée en token.
         ⚠️ `max()` ET PAS SEULEMENT LE CENTRAGE : sur un écran trop court pour la
         boîte, le terme centré devient plus petit que le header et la vidéo
         repasserait dessous. Le `max()` fait alors basculer le débordement
         ENTIÈREMENT vers le bas — rien n'est jamais masqué par le header, ce qui
         est exactement la demande. */
      /* `--cb-header-h` est MESURÉ et publié par `sentinelle-collant.js` (le JS
         qui gère déjà l'état collé de ce header) : c'est la seule valeur juste,
         puisque la hauteur du header collé dépend du repliement de sa rangée
         utilitaire ET du contenu du menu, éditable par la cliente. Le token
         reste le repli sans JS — il ne masque rien, il décentre de quelques
         pixels. Les variables elles-mêmes sont posées par la règle de base
         ci-dessus, qui borne aussi la largeur : ici on ne fait que placer. */
      /* ⚠️ SUR UNE SEULE LIGNE, ET SANS `calc()` IMBRIQUÉ — voir le commentaire
         de `--cb-dome-top-centre` : la forme développée ne survit pas au
         minifieur de WP Rocket (`REG13`). */
      top: max(var(--cb-dome-sous-header), var(--cb-dome-top-centre));
      animation: cb-dome-ouverture linear both;
      animation-timeline: --cb-dome;
      animation-range: var(--cb-dome-scroll-start) var(--cb-dome-scroll-end);
    }
    @media (min-width: 768px){
      /* Même garde qu'au-dessus : pas de média, pas de course. */
      .cb-content-hero--bandeau:has(.cb-content-hero__motion):not(:has(.cb-content-hero__motion[data-demo-state]))::after{
        height: var(--cb-dome-course-desktop);
      }
    }
  }
}
/* L'ÉTAT DE DÉPART, FORCÉ — pour la page de revue, et pour elle seule. Un
   mouvement piloté par le défilement ne se voit sur aucune capture : sans cet
   état, le dôme serait un atome non illustré, ce qu'AGENTS.md interdit. Même
   convention que les survols forcés (`[data-demo-state="hover"]`), donc du CSS
   du système et non un style local sur la page. */
.cb-content-hero__motion[data-demo-state]{
  /* ⚠️ `animation: none` D'ABORD, ET CE N'EST PAS DÉFENSIF : une animation bat
     une déclaration normale dans le cascade. Sans ça, l'état forcé perd contre
     l'ouverture elle-même — mesuré sur la page de revue, qui rendait l'état
     d'arrivée dans les deux colonnes de la carte censée montrer le départ.
     `position: static` pour la même raison de surface : une page de revue ne
     peut pas montrer une retenue, et un bloc collant dans une carte ne
     décrirait rien de juste. */
  animation: none;
  position: static;
}
.cb-content-hero__motion[data-demo-state="dome"]{
  clip-path: inset(var(--cb-dome-inset) var(--cb-dome-inset)
    round var(--cb-dome-radius) var(--cb-dome-radius) var(--cb-radius-md) var(--cb-radius-md)
    / var(--cb-dome-radius) var(--cb-dome-radius) var(--cb-radius-md) var(--cb-radius-md));
}
.cb-content-hero__motion[data-demo-state="ouvert"]{
  clip-path: inset(0 0
    round var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md)
    / var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md) var(--cb-radius-md));
}

/* ⚠️ LE MÉDIA PLEINE LARGEUR EST UN BLOC DE LA SECTION, PAS UN GROUPE DE TEXTE
   — et c'est le défaut qu'Alexandre a vu tout de suite (« très peu d'espacement
   entre la zone de contenu et la vidéo », 2026-08-17). Rien ne l'avait décidé :
   `.cb-section > * + *` pose `--cb-flow-group` (32px), un `(parti pris)` NON
   MESURÉ dont le rôle est « texte → rangée de pastilles » à l'intérieur d'un
   bloc. Entre deux BLOCS d'une section, le système a une valeur MESURÉE trois
   fois : `--cb-flow-block`, 80px. La frame, elle, met 112px à cet endroit — 80
   de padding bas de son introduction plus les 32 d'inset de sa frame média —
   mais ces 112 sont la somme de deux frames voisines, pas un écart déclaré ;
   c'est l'écart de FLUX qu'on traduit ici, et le système le connaît.
   Vaut pour les DEUX compositions : la base n'a simplement jamais rendu ce
   container sur une page réelle (`/hotel/` est une page image). */
.cb-content-hero > .cb-content-hero__motion{
  margin-top: var(--cb-flow-block);
}

/* =====================================================================
 * .cb-hairline — rule/hairline (H01)
 *
 * LE FILET. La séparation la plus fine du système : un trait de
 * `--cb-hairline` (0,5px) en `--cb-ivoire`, posé sous une rangée de header ou
 * entre deux groupes d'une même rangée.
 *
 * Pourquoi un atome et pas un `border-bottom` écrit dans chaque composant :
 * la frame en pose TROIS (sous la rangée utilitaire, sous la rangée de nav,
 * et deux séparateurs verticaux entre les groupes de la rangée 1) et il n'y a
 * qu'une seule décision derrière — l'épaisseur et la couleur du trait. Trois
 * `border` recopiés = trois valeurs à faire diverger.
 *
 * DEUX FORMES, deux mécanismes différents, et ce n'est pas un caprice :
 *   - `--under` s'applique À l'élément qu'il souligne (une bordure basse) :
 *     c'est ce que fait la frame, dont les rangées portent un
 *     `strokeBottomWeight` et non un enfant séparateur ;
 *   - `--between` est un élément À PART, vertical, de longueur fixe
 *     (`--cb-hairline-inset`, 24px) : la frame l'implémente en nœud VECTOR
 *     autonome, parce qu'un trait de 24px au milieu d'une rangée de 42px n'est
 *     la bordure de personne.
 * `border-inline-start` plutôt que `border-left` : la seule propriété logique
 * du système, mais gratuite — le site a un jumeau anglais (Q11) et n'aura
 * jamais de RTL, donc c'est de la correction, pas de la précaution.
 *
 * `aria-hidden` attendu sur `--between` côté markup : un trait décoratif ne se
 * lit pas à voix haute. Ce n'est pas du style, donc pas ici — mais la page de
 * revue le pose, et le pont ne peut pas le poser à sa place.
 * ================================================================== */
.cb-hairline--under{
  border-bottom: var(--cb-hairline) solid var(--cb-ivoire);
}
.cb-hairline--between{
  align-self: center;
  width: 0;
  height: var(--cb-hairline-inset);
  border-inline-start: var(--cb-hairline) solid var(--cb-ivoire);
}

/* =====================================================================
 * .cb-nav — nav/bar, nav/item (H01)
 *
 * LA NAV PRINCIPALE, réduite à ce qu'elle est : une rangée de boutons
 * fantômes. Chaque item compose `.cb-interactive .cb-interactive--ghost-on-dark
 * .cb-btn` — il n'a donc AUCUNE couleur, AUCUNE transition, AUCUN focus-ring
 * qui lui soit propre. `.cb-nav__item` n'ajoute que les deux choses que la
 * frame demande et que le bouton générique ne donne pas.
 *
 * LES TROIS ÉCARTS ASSUMÉS PAR ALEXANDRE, tous les trois contre le rendu
 * ACTUEL du site, pas contre la frame — ils sont sa décision, écrite : « on
 * passe à 16px (la font size minimum site wide de base), et on enlève le
 * letter spacing horrible et on passe tous les boutons et liens en casse
 * normale. c'est assumé. »
 *   1. `16px` — vient de `.cb-btn` (`--cb-text-base`), rien à écrire ici ;
 *   2. `letter-spacing: normal` — écrit ici, parce qu'il faut ANNULER quelque
 *      chose : ElementsKit en pose un sur les items de menu ;
 *   3. `text-transform: none` — même raison.
 * Ces deux annulations sont bien une décision de design (couche 2), pas une
 * riposte de spécificité (couche 3) : c'est le pont qui devra les rendre
 * gagnantes contre ElementsKit, pas cet atome.
 *
 * `gap: --cb-space-xs` (8px) et non les 12px de `.cb-interactive` : mesuré 8px
 * entre le libellé et son chevron sur les 4 items qui en portent un
 * (`40000182:18641`, `:18646`, `:18651`, `:18656`), et 8px aussi entre l'icône
 * et le numéro de téléphone. Les 12px de `.cb-interactive` restent justes là
 * où la frame les mesure : le « RÉSERVER », dont l'icône fait 24px et non 16.
 * La règle qui relie les deux — petit pictogramme → petit écart — n'est pas
 * tokenisée : deux occurrences ne font pas une loi.
 *
 * ⚠️ CE QUE CET ATOME NE COUVRE PAS, volontairement : le panneau du méga-menu
 * qui s'ouvre au survol. Décision d'Alexandre : « niveau popover de mega menu
 * on ne touche pas pour l'instant ». Les 26 cartes de panneau restent celles
 * d'ElementsKit, et leur sort dépend de `Q25`.
 * ================================================================== */
.cb-nav{
  display: flex;
  align-items: center;
  /* Aucun `gap` : les items portent leur propre padding horizontal, et la
     frame les met bord à bord (item 1 finit à x=93, item 2 commence à x=93).
     L'écart visible entre deux libellés est donc 2 × 16px, par construction —
     ajouter un gap le porterait à 48 et décalerait toute la rangée. */
}
.cb-nav__item{
  gap: var(--cb-space-xs);
  letter-spacing: normal;
  text-transform: none;
  /* `whitespace-nowrap` est posé sur les 12 libellés de la frame, sans
     exception — ce n'est pas une précaution, c'est la spécification. Et c'est
     nécessaire : à la 1ʳᵉ capture, « Spa & bien-être » passait sur TROIS lignes
     et « Coffrets Cadeaux » sur deux, ce qui faisait grandir la rangée de 66 à
     plus de 90px. Un item de nav est un libellé, pas un paragraphe.
     ⚠️ Le revers, et il est connu du projet : un item qui ne peut plus enrouler
     DÉBORDE quand la place manque — c'est le défaut de 281px que `P05b` a mis
     une session à corriger. La place a été vérifiée : à 1440px la rangée
     mesure ~1104px (7 items + 2 logos + retrait), il reste ~336px de marge.
     C'est ce que la capture à 1830px de viewport montre ; la carte de revue,
     plus étroite qu'un vrai desktop, laisse volontairement voir le débordement
     plutôt que de le maquiller. */
  white-space: nowrap;
}
/* Le chevron ne tourne pas, ne bouge pas, ne rebondit pas : il change de
   couleur avec son parent, comme tout le reste. Sa taille et sa couleur sont
   posées une seule fois, sur `.cb-interactive__icon svg` (§ interaction) — la
   règle était écrite ici en double jusqu'au 2026-08-12. */

/* =====================================================================
 * .cb-headerbar — header/bar (H01)
 *
 * LA BARRE À DEUX RANGÉES. C'est le dessin qu'Alexandre a retravaillé : une
 * rangée utilitaire (langue · téléphone · contact … cadeau · réserver) et une
 * rangée de marque et de navigation (logos … nav), chacune soulignée d'un
 * filet, la seconde seule restant collée au défilement.
 *
 * ⚠️ CET ATOME NE PORTE PAS LE COMPORTEMENT COLLANT. Il pose la forme des deux
 * rangées, rien de plus. Le « seule la 2ᵉ rangée reste collée, et la 1ʳᵉ
 * réapparaît quand on remonte » est un comportement au défilement : il appartient
 * au snippet du serveur (`21-header-scroll-effect.php`, réécriture autorisée par
 * `Q19`), pas à une feuille de style — et surtout pas à la couche 2, qui ne sait
 * rien de l'outil d'édition.
 *
 * LE RETRAIT EST DÉRIVÉ, PAS RECOPIÉ. Les deux rangées visent le même retrait
 * optique de 32px (`--cb-page-inset`) mais ne portent pas le même padding :
 * la rangée utilitaire n'en porte que 16, ses items ajoutant les 16 autres.
 * D'où le `calc()` — l'alternative était deux tokens dont l'un aurait menti sur
 * son rôle. Argument complet et conséquence assumée (le « RÉSERVER » tombe à
 * 16px du bord droit, pas 32) : `tokens.css` § HEADER.
 * ================================================================== */
.cb-headerbar{
  /* ⚠️ CES DEUX LIGNES SONT UN CORRECTIF TROUVÉ À LA CAPTURE, pas à la lecture —
     et il a fallu DEUX prises pour le voir en entier (docs/preuves/H01/) :
     le voile de `.cb-scrim` est fait de deux pseudo-éléments absolus du hero,
     donc positionnés ; sans contre-mesure, ils se peignent PAR-DESSUS le
     contenu au lieu de passer derrière. 1ʳᵉ prise : le « Réserver » à fond
     ivoire OPAQUE rendait gris foncé et les libellés du header étaient
     assombris par le voile censé les rendre lisibles. 2ᵉ prise, une fois le
     header réparé : c'était le paragraphe du bas qui passait sous le voile bas
     (`::after` est le DERNIER enfant, donc peint après tout le reste) — mesuré,
     pas supposé : la zone du paragraphe passait de 35,8 à 19,1 de luminance
     moyenne entre les deux voiles, texte compris, ce qui ne pouvait vouloir
     dire qu'une chose. C'est l'inverse exact de la fonction du voile.
     `z-index: 1` plutôt qu'un `z-index: -1` sur les pseudo-éléments : ce
     dernier marcherait ici (fond CSS sur le hero) mais PAS en Elementor, où la
     vidéo de fond est un ENFANT positionné (`.elementor-background-video-
     container`) et non un fond CSS — le voile passerait derrière la vidéo.
     Remonter le contenu marche dans les deux mondes. */
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
}
/* --on-dark : la barre posée sur un média sombre. ⚠️ Elle n'est PAS redondante
   avec la recette des boutons, et c'est une capture qui l'a montré : les items
   de contrôle portent leur couleur eux-mêmes (`--ghost-on-dark`), mais les
   LOGOS ne sont pas des items — ils héritaient donc de la couleur de texte
   ambiante, c'est-à-dire `--cb-encre`, et rendaient en vert sombre sur un
   header sombre. Presque invisibles. Poser la couleur sur la barre la donne à
   tout ce qu'elle contient, logos compris, sans avoir à en habiller chacun. */
.cb-headerbar--on-dark{
  color: var(--cb-ivoire);
}
.cb-headerbar__row{
  display: flex;
  align-items: center;
  padding-block: var(--cb-space-sm); /* 12px — mesuré sur les deux rangées (py-12), qui font 66px de haut pour un contenu de 42 */
}
/* rangée utilitaire — les deux groupes se repoussent aux extrémités */
.cb-headerbar__row--utility{
  justify-content: space-between;
  padding-inline: calc(var(--cb-page-inset) - var(--cb-space-md));
}
/* rangée de marque + nav — les logos, puis la nav, collés à gauche */
.cb-headerbar__row--nav{
  gap: var(--cb-header-gap);
  padding-inline: var(--cb-page-inset);
}
/* ⚠️ SOUS 1025px LA NAV N'EST PLUS UNE LISTE, C'EST UN HAMBURGER DE 25px — et
   « collés à gauche » cesse alors d'avoir un sens : le bouton flottait à la
   suite du logo, sans rien pour le pousser au bord. Mesuré le 2026-08-19 sur
   `/hotel/` (REG04), le bord droit du hamburger vaut **238 à TOUTES les largeurs
   sous 1025** — soit 120px de retard sur le rail à 390, et **754 à 1024**. Le
   défaut n'est donc pas mobile, il est de tout le palier tablette compris ; la
   première mesure, faite à 390 seule, le sous-estimait d'un facteur six.

   ⚠️ CE N'EST PAS LA PRODUCTION QUI SERT DE CIBLE — le bord droit de SON
   hamburger est à 282 à 390, elle n'est pas ferrée non plus, et son rail n'est
   pas le nôtre (son lockup commence à x=10, le nôtre à 32). La cible est NOTRE
   rail : `innerWidth − var(--cb-page-inset)`, soit 358 à 390.

   ⚠️ `margin-inline-start: auto` sur la nav, et pas `justify-content:
   space-between` sur la rangée : la rangée n'a deux enfants qu'ici, et le
   `space-between` deviendrait faux le jour où la cliente en ajoute un — le
   troisième irait au milieu. Pousser la nav ne dépend d'aucun compte d'enfants.

   ⚠️ 1024 et pas 767 : c'est ElementsKit qui décide, et il rend le hamburger
   jusqu'à 1024 inclus (mesuré à 390/767/768/1024 : présent ; à 1025 : boîte
   0×0). Le seuil suit le plugin, il n'est pas choisi. */
@media (max-width: 1024px){
  .cb-headerbar__row--nav > .cb-nav{
    margin-inline-start: auto;
  }
}
/* les deux groupes de la rangée utilitaire. À gauche 16px entre chaque item
   (mesuré, `40000182:18606`) ; à droite 8px entre le bouton icône et le CTA
   (mesuré, `40000182:18619`) — deux écarts différents, deux mesures. */
.cb-headerbar__group{
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
}
.cb-headerbar__group--actions{
  gap: var(--cb-space-xs);
}
/* Les logos. Deux hauteurs, toutes deux MESURÉES sur la frame : le monogramme
   Camboyer à 32px (`40000182:18626`, 25,896 × 32) et le logo partenaire The
   Originals à 24px (`40000182:18635`, 98,775 × 24). Ce ne sont pas des paliers
   d'échelle et elles n'en rejoindront pas : la hauteur d'un logo est une
   propriété du logo.
   ⚠️ C'est le MONOGRAMME que la frame emploie, pas le logotype complet — l'erreur
   a été faite une fois (signalée par Alexandre : « les logos ne sont pas bons »),
   le fichier est `assets/logo-favicon.svg`, mal nommé mais catalogué comme le
   monogramme dans docs/charte.md § assets. `width: auto` pour que le rapport du
   tracé décide de la largeur, jamais la boîte. */
.cb-headerbar__logo{
  display: block;
  width: auto;
  height: 32px;
}
.cb-headerbar__logo--partenaire{
  height: 24px;
}

/* =====================================================================
 * .cb-scrim — media/scrim (H01) — et c'est la réponse à DS50
 *
 * LE VOILE POSÉ SUR LA VIDÉO DU HERO. Deux dégradés, un par bord, qui
 * assombrissent l'image là où du texte doit rester lisible et la laissent
 * intacte au milieu.
 *
 * POURQUOI CE N'EST PAS DE LA DÉCORATION. `DS50` a échantillonné la vraie
 * vidéo image par image : derrière le même texte ivoire, le contraste va de
 * 10:1 à **1,37:1** selon la seconde. `DS27` avait déjà buté dessus (« --on-dark
 * ne garantit rien contre un fond quelconque »), et c'est exactement ce qui a
 * fait RETIRER `card/overlay` du système (DS33/DS37) : un voile ne rattrape pas
 * une photo quelconque. La différence ici, et elle est décisive : le hero n'est
 * pas une carte de 350px, c'est un plein écran où le texte est confiné aux deux
 * bords — donc le voile peut être dense là où il sert et nul là où il ne sert
 * pas. C'est ce que le dégradé mesuré fait, et pourquoi il est acceptable ici
 * quand il ne l'était pas là-bas.
 *
 * `::before`/`::after` plutôt que deux éléments : le voile n'est pas du
 * contenu. `pointer-events: none` pour qu'il n'intercepte ni un clic sur le
 * header, ni la lecture de la vidéo. `z-index` volontairement absent — l'ordre
 * de peinture suffit tant que le texte du hero est un enfant positionné du même
 * conteneur ; c'est au pont de le vérifier sur le DOM réel, pas à cet atome de
 * l'imposer.
 *
 * ⚠️ LE VOILE HAUT EST MESURÉ, LE VOILE BAS EST UN PARTI PRIS. La frame ne
 * contient qu'UN gradient (vérifié en listant tous les nœuds porteurs de fill
 * du sous-arbre) : celui du haut. Le bas applique la demande explicite
 * d'Alexandre (« rendre lisible le header ET le texte en bas ») en miroir du
 * profil mesuré. À trancher à l'œil, et à vérifier au contraste sur les deux
 * instants extrêmes de `DS50`.
 * ================================================================== */
.cb-scrim{
  position: relative;

  /* ---- L'ARRÊT DU VOILE HAUT SUIT LE HEADER, PAS LE BLOC (`Q31`, 2026-08-17)
   * Le voile haut n'a qu'un seul travail : rendre le header lisible sur un
   * média. Son coude était exprimé en pourcentage du bloc (`12,5 %`), ce qui
   * marche tant que le bloc a la taille pour laquelle le pourcentage a été
   * mesuré — et cesse de marcher dès qu'un autre média l'emploie.
   *
   * ⚠️ MESURÉ, aux deux tailles de média en ligne, header de 134px à 1440 :
   *   hero d'accueil, 900px → coude à 113px ;
   *   bandeau `P03e`,  609px → coude à  76px, soit DANS la rangée 2 (67→134).
   * Contraste sous les 11 boîtes de texte réelles du header, avant → après :
   *   rangée 1  4,18 → 5,41 ✔ (part sous 4,5:1 : 12,3 % → 0 %)
   *   rangée 2  2,19 → 2,74   (78,8 % → 58,7 %) — améliore, NE SUFFIT PAS
   *   accueil   11,54 / 7,44 → 11,70 / 7,76 — aucune régression sur le poste 4
   *
   * ⚠️ CE N'EST PAS UNE DENSIFICATION, et donc ça ne rejoue pas `Q28` :
   * `--cb-scrim-edge` et `--cb-scrim-knee` sont INCHANGÉS. On déplace un arrêt,
   * on ne change aucune opacité. `.cb-scrim--dense`, retiré par `Q28`, le reste.
   *
   * ⚠️ La rangée 2 reste sous le seuil AA : le coude ne peut pas la sauver seul,
   * l'opacité au coude n'étant que de 0,2. C'est une décision d'Alexandre, pas
   * un réglage — elle est posée telle quelle dans `Q31`, avec ses chiffres.
   *
   * Le `min()` est un garde-fou, pas un réglage : si un média était plus court
   * que le header, le coude passerait derrière la fin du dégradé et l'ordre des
   * arrêts deviendrait invalide. Aucun média servi n'est dans ce cas
   * aujourd'hui (le plus court mesure 260px à 390, pour un header de 57).
   *
   * Les paliers existent parce que `--cb-header-h-repos` est publié par JS :
   * sans lui, le repli doit rester juste à chaque largeur — un maximum unique
   * découvrirait du fond au-dessus du média sur mobile. */
  --cb-scrim-knee-top: min(
    var(--cb-header-h-repos, var(--cb-header-repos-mobile)),
    var(--cb-scrim-end-position)
  );
}
@media (min-width: 768px){
  .cb-scrim{
    --cb-scrim-knee-top: min(
      var(--cb-header-h-repos, var(--cb-header-repos-tablet)),
      var(--cb-scrim-end-position)
    );
  }
}
@media (min-width: 1025px){
  .cb-scrim{
    --cb-scrim-knee-top: min(
      var(--cb-header-h-repos, var(--cb-header-repos-desktop)),
      var(--cb-scrim-end-position)
    );
  }
}
/* Les DEUX pseudo-éléments couvrent tout le bloc, et leurs arrêts sont exprimés
   en pourcentage DU BLOC — pas de leur propre hauteur. C'est ce qui rend les
   tokens lisibles tels quels (« 12,5 % du hero ») au lieu d'imposer une
   conversion (12,5/40 = 31,25 %) qu'un changement de l'autre token ferait
   mentir en silence.
   ⚠️ La contrainte que ça impose, et elle est réelle : `--cb-scrim-end-position`
   ne doit JAMAIS dépasser 50 %. Au-delà, les deux dégradés se recouvriraient au
   milieu et s'y AJOUTERAIENT (composition « over », pas maximum) — le bloc
   s'assombrirait au centre, là où le voile est justement censé disparaître, et
   la mesure de `docs/preuves/H01/mesure-scrim.py` cesserait de décrire ce que
   le navigateur peint. */
.cb-scrim::before,
.cb-scrim::after{
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* z-index EXPLICITE — trouvé au rendu (H01c, signalé par Alexandre : « le
     dégradé n'est pas correctement implémenté »). Sur la page de revue, `::before`
     est visuellement le seul enfant du porte-image (rien d'autre en `position:
     absolute` dedans) : l'ordre du DOM suffisait à le peindre au-dessus. Sur le
     hero réel, Elementor pose SA PROPRE couche vidéo en `position:absolute`
     (`.elementor-background-video-container`) — MESURÉ avec un `z-index:0`
     EXPLICITE, pas `auto` comme supposé une première fois. Elle arrive APRÈS
     `::before` dans l'arbre (le pseudo est conceptuellement le premier enfant),
     donc à z-index ÉGAL (0 contre 0), l'ordre du DOM tranche et elle peint
     PAR-DESSUS lui : `::before` restait invisible, caché SOUS la vidéo. `::after`
     marchait par accident (dernier dans l'arbre, donc déjà au-dessus À z-index
     ÉGAL). `z-index: 1` bat le `0` de la vidéo sans ambiguïté de DOM — et reste
     sous `.cb-hero__body` (z-index 2, plus bas). Vérifié en isolant chacun (fond
     plein rouge/bleu en test) : un seul restait invisible avant ce correctif,
     preuve, pas supposition — puis les deux confirmés visibles après. */
  z-index: 1;
}
.cb-scrim::before{
  /* ⚠️ Le coude du HAUT est en pixels (`--cb-scrim-knee-top`, calé sur la
     hauteur du header), là où celui du BAS reste en pourcentage du bloc. Ce
     n'est pas une incohérence : les deux voiles ne protègent pas la même chose.
     Le haut protège un élément de hauteur connue et FIXE — le header ; le bas
     protège du texte dont la place dépend de la composition, donc du bloc.
     Voir le raisonnement complet et les mesures au § `.cb-scrim` ci-dessus. */
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--cb-scrim-color) calc(var(--cb-scrim-edge) * 100%), transparent) 0%,
    color-mix(in srgb, var(--cb-scrim-color) calc(var(--cb-scrim-knee) * 100%), transparent) var(--cb-scrim-knee-top),
    transparent var(--cb-scrim-end-position)
  );
}
.cb-scrim::after{
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--cb-scrim-color) calc(var(--cb-scrim-edge) * 100%), transparent) 0%,
    color-mix(in srgb, var(--cb-scrim-color) calc(var(--cb-scrim-knee) * 100%), transparent) var(--cb-scrim-knee-position),
    transparent var(--cb-scrim-end-position)
  );
}
/* --top-only : le hero tel que la frame le dessine RÉELLEMENT, sans voile bas.
   Il existe pour que la revue puisse comparer les deux et trancher, pas pour
   offrir une option — l'un des deux disparaîtra. */
.cb-scrim--top-only::after{ content: none; }

/* ---- `Q28` EST TRANCHÉE : LA FIDÉLITÉ À LA FRAME, ET LES TITRES RESTENT PEU
 * LISIBLES. C'est une décision d'Alexandre du 2026-08-12, prise sur les chiffres
 * ci-dessous. Ce n'est pas un oubli, et ça ne se « corrige » pas.
 *
 * Un second profil existait — `.cb-scrim--dense` — qui passait l'AA partout. Il
 * a été RETIRÉ du système par cette décision (`Done =` de `Q28` : « un seul des
 * deux modificateurs survit »). Ne pas le remettre : il a été jugé, pas oublié.
 *
 * CE QUE LA MESURE DIT, et qu'on assume (docs/preuves/H01/mesure-scrim.py, qui
 * reste dans le dépôt — c'est la trace de l'arbitrage, 5 instants de la VRAIE
 * vidéo × 3 boîtes de texte, contraste de l'ivoire contre le fond composité) :
 *   - vidéo nue                        → pire pixel **1,00:1**
 *   - voile actuel de la prod (#151F15 à 15 %, multiply) → **1,35:1**
 *   - voile de la frame — CE QUI EST SERVI → **1,18:1**, et **29,5 %** de la
 *     boîte du h1 sous 4,5:1
 *   - le profil dense, retiré          → 3,97:1 au pire, 0,0 % sous le seuil
 *
 * ⚠️ CE QUE ÇA VEUT DIRE CONCRÈTEMENT : le voile servi traite le HEADER, pas les
 * titres. Ses 60 % se concentrent sur les 100 premiers pixels — la hauteur de la
 * barre, qu'ils traitent parfaitement. Les deux titres du hero sont à y=169-245,
 * où le dégradé est déjà retombé sous 0,13 d'opacité. Le voile de la frame sauve
 * le header ; il ne sauve pas les titres. C'est le dessin qui a été préféré à la
 * mesure, en connaissance de cause.
 *
 * CE QUI RESTE OUVERT, et ce n'est PAS la densité : le voile BAS. La frame ne
 * contient qu'un gradient, celui du haut ; le bas applique une demande verbale
 * d'Alexandre (« rendre lisible le header ET le texte en bas ») et reste un
 * `(parti pris)`. `--top-only` existe pour que la revue tranche ce point-là, et
 * lui seul. -------------------------------------------------------------- */

/* =====================================================================
 * .cb-bandeau — media/bandeau (P03e)
 *
 * LE MÉDIA PANORAMIQUE À FOND PERDU QUI OUVRE UNE PAGE CONTENU. Frame
 * 40000265:1653, « Frame 106 » : 1440 × 609 posée en x=0, y=0 — donc bord à
 * bord, et les deux rangées du header sont posées DESSUS, pas au-dessus de lui.
 * Il se compose donc avec `.cb-scrim--top-only` : sans voile, le header ivoire
 * tombe sur une photo quelconque, et c'est le même problème mesuré par DS50 sur
 * l'accueil.
 *
 * IL NE REMPLACE PAS `.cb-img`, et la différence n'est pas cosmétique : `.cb-img`
 * porte une image de CONTENU, avec son alt, dans le flux d'un bloc. Ici le média
 * est le décor de la page, cadré par recouvrement — exactement ce que fait déjà
 * `.cb-content-hero__motion` pour la vidéo, d'où la même peinture.
 *
 * ⚠️ LONGHANDS OBLIGATOIRES, JAMAIS LE RACCOURCI `background`. L'image vient
 * d'Elementor (`background_image` du container : une donnée éditable par la
 * cliente, pas un style), compilée en `.elementor-element-XXX{background-image}`
 * — (0,1,0), le même poids que `.cb-bandeau`, et notre feuille charge APRÈS la
 * sienne. Un raccourci gagnerait donc, et remettrait `background-image: none` :
 * le média disparaîtrait. Même famille de piège que DS61 (double peinture).
 *
 * ⚠️ AUCUNE HAUTEUR — et surtout pas le `min-height: 100vh` du hero legacy qu'il
 * remplace : la frame donne un RATIO, et un ratio se tient à toutes les
 * largeurs. En dessous de 768px le panoramique n'a plus de sens (165px de haut à
 * 390) : le repli prend le ratio paysage du système, 3:2 — qui est aussi celui
 * des covers réelles de la médiathèque (1400 × 933), donc la photo y rend
 * entière. `(parti pris)` : la frame ne dessine pas le mobile.
 *
 * ⚠️ MAIS UN RATIO SEUL NE SUFFIT PAS NON PLUS, et c'est mesuré : un ratio ne
 * connaît que la LARGEUR. Sur un écran large et bas — capture d'Alexandre du
 * 2026-08-17, 1404 × 644 — le bandeau seul faisait 594px et le titre tombait à
 * y=674, soit SOUS LE PLI : un hero dont le titre n'est pas visible n'est plus
 * un hero. Le plafond ci-dessous réserve donc, sur la hauteur du viewport, la
 * place du titre et de son accent, plus un pas de flux pour que le pli
 * n'affleure pas le texte et annonce qu'il y a une suite. En dessous de ce
 * plafond le ratio gouverne (c'est le cas dès ~1080px de viewport à 1440 de
 * large) ; au-dessus, la hauteur cède — et c'est l'IMAGE qui se recadre, jamais
 * le titre qui descend.
 *
 * `svh` et non `vh` : sur mobile la barre d'URL fait varier `vh`, et un titre
 * « visible » qui cesse de l'être au premier scroll ne compte pas.
 * ================================================================== */
.cb-bandeau{
  position: relative;
  width: 100%;
  aspect-ratio: 1440 / 609; /* mesuré sur la frame — « Frame 106 », 40000265:1654 */
  max-height: calc(100svh
    - var(--cb-space-hero-intro)   /* le padding haut de l'introduction */
    - var(--cb-bandeau-lockup)     /* le titre et son accent, hauteur mesurée */
    - var(--cb-flow-group));       /* (parti pris) le pli ne colle pas au texte */
  overflow: hidden;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
}
@media (max-width: 767px){
  .cb-bandeau{ aspect-ratio: 3 / 2; } /* le ratio paysage du système, cf. .cb-img--3-2 */
}

/* =====================================================================
 * .cb-hero — hero/full (H01)
 *
 * LA COMPOSITION DU HERO D'ACCUEIL : un média plein cadre, la barre de header
 * en haut, puis trois textes placés aux coins — le h1 en haut à gauche, le
 * titre d'accent en haut à droite, le paragraphe en bas à gauche.
 *
 * POURQUOI C'EST UN ATOME ET PAS DU STYLE LOCAL. Sans lui, la page de revue
 * devrait poser cette grille en `style=` sur la carte de démo, ce qu'`AGENTS.md`
 * interdit (« aucun style local sur la page ») — et le bloc réel devrait la
 * reposer en réglages Elementor, ce que la stratégie d'implémentation interdit
 * aussi. La mise en page d'un bloc identifié du site est du système.
 *
 * LA GRILLE EST À DOUZE COLONNES (grille Elementor classique), et c'est la
 * réponse à la question d'Alexandre. Il a écrit, à propos de la largeur du
 * paragraphe : « c'est un peu arbitraire ma valeur. on peut imaginer une
 * valeur desktop basée sur des colonnes (j'imagine qu'elementor fait ça)
 * genre 1/4 de rangée ? » — ⚠️ CORRIGÉ le 2026-08-14 : une première lecture
 * avait pris « 1/4 » au pied de la lettre et posé QUATRE colonnes ÉGALES (le
 * paragraphe en occupant une, soit 25 %). Alexandre a tranché ensuite :
 * « je veux 4 colonnes sur 12, soit 1/3 pour le paragraphe » — c'est la
 * grille à 12 qu'il visait depuis le début (« j'imagine qu'elementor fait
 * ça » = le système de colonnes natif d'Elementor, toujours en base 12), pas
 * une grille à 4 colonnes égales. Le paragraphe occupe donc **4 colonnes sur
 * 12** (33,3 %), le h1 et le titre d'accent en occupent **6 chacun** (50/50,
 * comme avant — la question ne portait que sur le paragraphe). À 1440px, une
 * colonne de 12 fait 114,7px, donc le paragraphe fait ~459px — la frame
 * plaçait le paragraphe à 323px, à la main : l'écart n'est plus le sujet
 * (`Alexandre a explicitement choisi la fraction plutôt que la frame`).
 * ⚠️ 2026-08-17 — il n'y a PLUS de second plafond. `--cb-measure` (55ch) a
 * été retirée de `.cb-paragraph` (voir § .cb-paragraph) : sur un très grand
 * écran, le lede du hero est borné par sa colonne (4/12) et par rien d'autre.
 * Si sa ligne devient trop longue à l'œil, c'est à cette composition-ci de
 * poser sa propre borne, pas à l'atome de prose.
 *
 * `padding` — `--cb-flow-group` (32px) en vertical et `--cb-page-inset`
 * (32px) en horizontal, contre 37px et 31px sur la frame. `(parti pris)` :
 * ces deux nombres-là sont des positions posées à la main dans une frame
 * statique (le paragraphe est à y=483, le h1 à y=37), pas une grille — les
 * accrocher à deux tokens existants à 1 et 5px près vaut mieux que d'ouvrir
 * deux tokens orphelins pour les recopier. Et le retrait horizontal devient
 * alors le MÊME que celui du header juste au-dessus, ce que la frame frôlait
 * sans l'atteindre (31 contre 32).
 *
 * `align-content: space-between` plutôt qu'une hauteur de rangée : le hero fait
 * 100vh en prod, et sa hauteur réelle dépend de l'écran. Les deux rangées se
 * repoussent donc aux extrémités du média, comme la frame les montre.
 *
 * AUCUNE HAUTEUR ICI. Le `min_height: 100vh` du hero est un réglage du
 * container Elementor qui existe déjà en prod (`0ce0045`, H01a § 3) : le
 * redéclarer ici en ferait une seconde source pour la même décision.
 *
 * En dessous de 768px, tout s'empile en une colonne. Le mobile est HORS
 * PÉRIMÈTRE de ce chantier (décision d'Alexandre : « niveau version mobile on
 * ne touche pas pour l'instant à l'existant ») — cette règle n'est donc pas une
 * proposition de dessin mobile, c'est le repli le plus sobre pour que l'atome
 * ne casse pas là où personne ne l'a encore dessiné.
 * ================================================================== */
.cb-hero{
  position: relative;
  display: flex;
  flex-direction: column;
}
.cb-hero__body{
  position: relative; /* au-dessus du voile — même raison et même correctif que `.cb-headerbar`, voir son commentaire */
  z-index: 2; /* > 1 (`.cb-scrim::before/::after`, H01c) : garantit le texte au-dessus du voile sans dépendre de l'ordre du DOM */
  /* !important — trouvé au rendu (H01c, 3ᵉ passage) : Elementor pose
     `.e-con.e-flex{--flex-grow:0}` sur tout container de ce type, 2 classes,
     qui bat `.cb-hero__body` (1 classe) et écrase notre `flex:1` en `flex:0` —
     le corps du hero ne s'étirait plus jusqu'au bas du média, le paragraphe
     ne restait pas ferré en bas. Même famille de riposte que le bouton
     (docs/elementor.md § 3.2). */
  flex: 1 !important;
  display: grid;
  align-content: space-between;
  row-gap: var(--cb-flow-block);
  padding: var(--cb-flow-group) var(--cb-page-inset);
}
@media (min-width: 768px){
  .cb-hero__body{
    grid-template-columns: repeat(12, 1fr);
    /* column-gap nul, volontairement : les douze colonnes sont une RÈGLE DE
       PLACEMENT, pas des boîtes à espacer — le h1 et le titre d'accent sont
       séparés par le vide de la composition (le premier est aligné à gauche, le
       second à droite), pas par une gouttière. Une gouttière rétrécirait la
       colonne du paragraphe sans rien apporter. */
    column-gap: 0;
  }
  /* h1 : colonnes 1-6 (moitié gauche) · accent : colonnes 7-12 (moitié droite)
     · paragraphe : colonnes 1-4, SOIT 4 SUR 12 = 1/3 (décision d'Alexandre,
     2026-08-14 — pas 1/4, voir commentaire de composition ci-dessus). */
  .cb-hero__title{  grid-area: 1 / 1 / 2 / 7; }
  .cb-hero__accent{ grid-area: 1 / 7 / 2 / 13; }
  .cb-hero__lede{   grid-area: 2 / 1 / 3 / 5; }
}

/* =====================================================================
 * .cb-reveal — APPARITION AU DÉFILEMENT (DS70)
 *
 * Demande d'Alexandre (2026-08-18) : « le `fadeIn` que je vois un peu partout
 * sur le site qui fait apparaître certains éléments au scroll : j'aime bien
 * cette idée, on peut l'appliquer sur les sections qu'on a modifiées en suivant
 * la logique déjà en place qui est il me semble composant par composant : à
 * analyser avant d'agir ».
 *
 * ⚠️ LE RELEVÉ CORRIGE L'INTUITION, et c'est ce qui fixe la règle. 281
 * animations servies sur 8 pages FR, dont **265 `fadeIn` et 258 sur des
 * `button`** : 92 % sur un seul type de widget. Il n'y a donc AUCUNE « logique
 * composant par composant » à imiter — il y a une habitude prise sur les
 * boutons, plus deux usages cohérents (9 `fadeInDown` sur les flèches de
 * scroll, 6 sur les vidéos) et 1 `fadeInUp` orphelin. La règle ci-dessous se
 * pose ; elle ne reproduit pas l'existant.
 *
 * LA RÈGLE, en trois lignes :
 *   1. L'unité qui s'anime est le **bloc** : une composition déclare que ses
 *      enfants directs apparaissent. Jamais un widget qui décide seul.
 *   2. L'ORDRE n'est pas réglé, il est **géométrique** : chaque enfant porte sa
 *      propre `view()` timeline, donc il se révèle quand IL entre dans le
 *      viewport. C'est ce qui donne la lecture « composant par composant »
 *      qu'Alexandre aime, sans 258 réglages locaux pour l'obtenir, et sans
 *      aucune valeur de décalage à choisir. `animation-delay` est de toute
 *      façon ignoré par une animation pilotée par le défilement.
 *   3. NE S'ANIMENT JAMAIS : le header (global et collant — `H01k` en avait
 *      déjà retiré une), le hero et son média (c'est le premier rendu de la
 *      page : le retarder, c'est retarder le contenu), et tout descendant d'un
 *      bloc qui s'anime déjà (pas d'apparition imbriquée). L'explorateur de
 *      chambres est exclu explicitement : ses états sont pilotés par du JS
 *      (`H03c`) et une apparition entrerait en concurrence avec eux.
 *
 * ⚠️ POURQUOI PAS LE MÉCANISME D'ELEMENTOR, et c'est la raison principale de ce
 * ticket : `elementor-invisible` pose l'élément à `opacity: 0` en CSS et ne le
 * révèle QUE par JavaScript. Si le JS ne s'exécute pas, le contenu reste
 * invisible — ce n'est pas une dégradation, c'est une perte de contenu. Ici la
 * relation est inversée : `opacity: 0` n'existe QUE dans le corps du
 * `@supports` + `@media`. Un navigateur qui ne connaît pas les timelines de
 * défilement, un utilisateur qui demande moins de mouvement, un JS en panne —
 * dans les trois cas l'élément est simplement VISIBLE. Le repli ne peut pas
 * cacher le contenu, par construction. `P03f` avait déjà prouvé le procédé sur
 * le dôme des heros.
 *
 * ⚠️ Aucune durée : une animation pilotée par le défilement n'en a pas — sa
 * progression EST la position de son sujet dans le viewport. Il n'y a donc rien
 * à prendre à `DS11` sur ce point, et surtout aucun token de durée à inventer.
 * Le déplacement réemploie `--cb-space-sm` (12px) plutôt qu'une 2ᵉ valeur.
 * `Q08` est respectée : opacité + micro-translation, jamais un scale.
 * ================================================================== */
@keyframes cb-reveal{
  from{ opacity: 0; transform: translateY(var(--cb-space-sm)); }
  to{   opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    .cb-reveal > *{
      animation: cb-reveal linear both;
      animation-timeline: view();
      /* `(parti pris)` — la révélation se joue sur la première moitié de
         l'entrée dans le viewport, pour être finie bien avant que l'élément
         soit au centre de l'écran. Deux pourcentages de plage, pas deux
         valeurs de style : il n'y a pas de token d'échelle de défilement dans
         le système, et en créer un pour deux emplois serait spéculatif. */
      animation-range: entry 15% entry 70%;
    }
    /* Ne s'animent jamais — cf. règle 3 ci-dessus. Un descendant d'un bloc qui
       s'anime déjà hériterait d'une seconde apparition : on la coupe. */
    .cb-reveal > * .cb-reveal > *,
    .cb-rooms *,
    .cb-hero *,
    .cb-headerbar *{
      animation: none;
    }
  }
}

/* LES COMPOSANTS QUI S'ANIMENT — la liste EST le rayon d'action du ticket, et
   elle se lit d'un coup d'œil au lieu de se deviner. Ce sont des CLASSES de
   composant, pas un `> *` : dans le DOM Elementor, la classe de composition est
   posée sur un `e-con` dont le vrai parent des composants est un `.e-con-inner`
   sans classe à nous, si bien qu'un `> *` révélerait le bloc entier d'un coup —
   exactement l'effet qu'Alexandre trouve incohérent. Nommer les composants rend
   la règle **isomorphe** entre le markup neutre et le jumeau Elementor (`DS39`),
   donc valable dans les deux couches sans la réécrire.
   Sont exclus les enveloppes de mise en page (`__content`, `__lead`, `__inner`,
   `__details`) et les enfants d'un lockup : le lockup se révèle d'un bloc, son
   titre et son script ne se séparent pas (`--cb-flow-lockup` vaut 0, `DS62`).
   ⚠️ `.cb-section` est volontairement ABSENTE : l'atome de section générique est
   servi sur les pages de contenu (2 nœuds sur `/hotel/`, mesuré) mais je n'ai
   pas établi lesquels — l'inclure élargirait le rayon au-delà du vérifié.
   ⚠️ `.cb-intro` et `.cb-rooms-head` ne sont PAS illustrées sur la page de revue
   (relevé du 2026-08-18) : leur apparition n'a donc pas de jumeau à comparer.
   Fait déclaré, pas corrigé ici — c'est le même trou que `DS51`/`DS48`. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    .cb-intro__lockup, .cb-intro__image, .cb-intro__copy,
    .cb-intro__tags, .cb-intro__actions, .cb-intro__brochure,
    .cb-rooms-head__title-group, .cb-rooms-head__copy,
    .cb-events__lockup, .cb-events__actions, .cb-events__copy,
    .cb-content-hero__body > *{
      animation: cb-reveal linear both;
      animation-timeline: view();
      animation-range: entry 15% entry 70%;
    }
    /* et les exclusions valent aussi pour elles */
    .cb-rooms *, .cb-hero *, .cb-headerbar *{ animation: none; }
  }
}
