/* ==========================================================================
   AlyCare — Page d'accueil (styles cinématiques)
   Tout est piloté par les variables de css/variables.css : le widget de
   theming continue donc de fonctionner sur l'ensemble de ces sections.
   ========================================================================== */

/* ---------- Tokens dérivés ---------- */
:root {
  --ease-out: cubic-bezier(0.22, 0.7, 0.24, 1);
  --ease-soft: cubic-bezier(0.65, 0, 0.35, 1);
  --dur: 0.9s;
}

/* ==========================================================================
   1. PRÉLOADER
   ========================================================================== */
/* Thème sombre (2026-08-21) : le préloader était l'un des rares blocs
   déjà sombres du site clair, et il obtenait ce fond en détournant
   var(--color-text) (l'encre) comme aplat. Les deux tokens ayant changé
   de valeur, on repasse sur leur usage normal — fond var(--color-bg),
   encre var(--color-text) — pour le même rendu qu'avant, mais cette fois
   en continuité directe avec le reste de la page. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 900;
  /* Tokens dédiés (voir variables.css). Depuis le 2026-08-25 ils SUIVENT
     LE THÈME : crème + encre brun-noir en clair, charbon + encre crème en
     sombre. Le logo manuscrit qui s'y écrit a ses couleurs figées dans son
     SVG, il existe donc en deux fichiers (choisis dans index.html). */
  background: var(--preloader-bg);
  color: var(--preloader-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: var(--space-md);
}

/* Logo manuscrit qui s'écrit (2026-08-24) — remplace l'ancien
   .preloader__word (le mot "AlyCare" en typo condensée, qui montait
   depuis le bas). L'animation vit ENTIÈREMENT dans le fichier SVG
   logo-anime-clair.svg en thème clair, logo-anime.svg en sombre — tout y
   est commenté) : rien à piloter ici, ni en CSS ni en JS. Les deux
   fichiers ont la même géométrie, seules les encres changent, et donc le
   même minutage. La durée d'écriture est de 2,3 s ; la barre de
   progression juste en dessous est calée dessus dans js/home.js §1.
   La marge basse compense le vide sous la baseline dans le SVG, pour que
   le bloc logo + barre reste optiquement centré. */
.preloader__logo {
  width: min(620px, 78vw);
  height: auto;
  display: block;
  margin-bottom: calc(var(--space-md) * -1);
}

.preloader__bar {
  width: min(240px, 50vw);
  height: 2px;
  background: var(--preloader-rail);
  overflow: hidden;
}

.preloader__bar i {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--preloader-gauge);
  /* Plus de `transition: width .3s` depuis le 2026-08-24 : la largeur est
     désormais posée à chaque frame par js/home.js (§1), la transition ne
     ferait que traîner de 0,3 s derrière le compteur et derrière la
     plume. */
}

.preloader__pct {
  font-family: var(--font-body);
  font-size: var(--font-size-sm);
  letter-spacing: 0.2em;
  opacity: 0.55;
  font-variant-numeric: tabular-nums;
}

.preloader.is-done {
  transform: translateY(-100%);
  transition: transform 1s var(--ease-out);
}

/* Le contenu attend la fin du préloader */
body.is-loading { overflow: hidden; }

/* ==========================================================================
   2. GRAIN + CURSEUR + BARRE DE PROGRESSION
   ========================================================================== */
.grain {
  position: fixed;
  inset: 0;
  z-index: 400;
  pointer-events: none;
  /* PERF-2026-08-27 — PLUS DE mix-blend-mode ICI, ET C'EST VOLONTAIRE.
     Ce calque est fixe, plein écran, au-dessus de tout (z-index 400). Un
     élément en mix-blend-mode force le navigateur à RE-MÉLANGER tout le
     contexte d'empilement sous lui dès que quoi que ce soit y change : il
     multipliait donc le coût de toutes les autres animations de la page,
     et il apparaissait comme co-responsable des DEUX moments qui ramaient
     (relevé du préloader et réduction de la vidéo). Mesuré : le passer en
     rendu normal donne exactement le même gain que le masquer entièrement
     — c'est le blend qui coûte, pas la texture.

     L'opacité NE CHANGE PAS en thème clair, et ce n'est pas un oubli :
     sur un fond proche du blanc, poser du bruit gris à 4,5 % et le
     multiplier à 4,5 % donnent presque le même résultat. Relevé sur une
     zone plate de la page (niveau 241, écart-type 0,18 sans grain) :
        multiply 0,045  → niveau 239,419 · écart-type 0,690
        normal   0,045  → niveau 239,352 · écart-type 0,689
     soit 0,07 niveau de luminance d'écart, pour un grain d'amplitude
     identique. En thème sombre le calcul est différent — voir la surcharge
     en fin de fichier, dont l'opacité, elle, a été recalée. */
  opacity: 0.045;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* Curseur personnalisé (.cursor / .cursor-ring) : déplacé dans
   css/components.css le 2026-08-13 — main.css met `cursor: none` sur tout
   le site (pas seulement l'accueil), ces règles doivent donc être chargées
   partout, pas uniquement via home.css qui n'est lié que sur index.html.
   Voir js/components/cursor.js (déplacé pour la même raison depuis
   home.js §2). */

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: var(--color-primary-dark);
  z-index: 300;
}

/* ==========================================================================
   3. RÉVÉLATIONS AU SCROLL
   ========================================================================== */
[data-reveal] {
  opacity: 0;
  transform: translateY(34px);
  transition: opacity 1s var(--ease-out), transform 1s var(--ease-out);
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
}

[data-reveal-delay="1"] { transition-delay: 0.1s; }
[data-reveal-delay="2"] { transition-delay: 0.2s; }
[data-reveal-delay="3"] { transition-delay: 0.3s; }

/* Colonne "Réservation" : apparition depuis le haut plutôt que depuis le
   bas (défaut ci-dessus), pour contraster avec "Nous contacter" à côté.
   Règle de reset dédiée (spécificité plus élevée que .is-in seul) : sans
   elle, une fois .is-in ajoutée, les deux sélecteurs à spécificité égale
   ([data-reveal].is-in et [data-reveal].booking-x__col--from-top)
   entraient en conflit et celui-ci gagnait par ordre de source — le
   transform restait bloqué à -34px, d'où l'opacité qui apparaît sans
   aucun mouvement. */
[data-reveal].booking-x__col--from-top {
  transform: translateY(-90px);
}

[data-reveal].booking-x__col--from-top.is-in {
  transform: none;
}

/* Colonne "Nous contacter" : même principe, mais point de départ plus bas
   que le défaut du site (translateY(34px)) pour que le trajet soit aussi
   long que celui de "Réservation" ci-dessus. */
[data-reveal].booking-x__col--from-bottom {
  transform: translateY(90px);
}

[data-reveal].booking-x__col--from-bottom.is-in {
  transform: none;
}

/* Révélation d'image par masque */
.reveal-img {
  position: relative;
  overflow: hidden;
}

.reveal-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.18);
  transition: transform 1.5s var(--ease-out);
}

.reveal-img::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-bg-alt);
  transform-origin: 100% 50%;
  transition: transform 1.1s var(--ease-out);
  z-index: 1;
}

.reveal-img.is-in img { transform: scale(1); }
.reveal-img.is-in::after { transform: scaleX(0); }

/* ==========================================================================
   4. HERO
   Historique (2026-08-10) : le texte était ancré bas (align-items:
   flex-end) avec un padding-bottom en vh pour un effet cinématique. Deux
   essais de plafond différents ont donné des résultats opposés selon
   l'écran (trop bas sur un 32", trop haut sur un 27") : un simple offset
   par rapport au bas, aussi généreux soit-il, reste sensible à des facteurs
   qui varient d'un poste à l'autre (mise à l'échelle Windows/macOS, barre
   des tâches, hauteur réelle du viewport...) et ne peut pas être fiable à
   100%.
   Solution robuste retenue : centrage vertical pur (align-items: center).
   Le centrage est par nature proportionnel — aucun seuil à caler, donc
   aucun risque de "trop haut" / "trop bas" selon l'écran. */
.hero-x {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  /* Encre dédiée à la photo du hero (voir variables.css). Claire jusqu'au
     2026-08-25 parce qu'un voile sombre couvrait la photo ; SOMBRE depuis
     que ce voile a été retiré et que le cliché est un intérieur très
     clair. Elle ne suit pas le thème : c'est la photo qui commande. */
  color: var(--color-on-light);
  /* Air minimal en haut/bas pour les très petites hauteurs de viewport
     (mobile en paysage, fenêtre réduite) où le contenu peut approcher les
     bords une fois centré. */
  padding: clamp(1.5rem, 6vh, 4rem) 0;
}

/* inset: 0 et non plus "-8% 0 0 0", et plus de will-change : ce débord en
   haut et cette promesse de transformation servaient la parallaxe, retirée
   le 2026-08-25 (voir js/home.js §5). La photo tient maintenant exactement
   dans le hero et n'est plus déplacée par le scroll. */
.hero-x__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}

/* scaleX(-1) : la photo est RETOURNÉE gauche-droite (2026-08-25, demande
   de Rom). C'est un miroir CSS et non un second fichier — le JPEG source
   n'est pas touché (pas de ré-encodage, pas de perte) et il suffit de
   retirer le facteur pour revenir en arrière. À GARDER EN TÊTE le jour où
   la cliente fournit son cliché : le miroir s'appliquera à lui aussi.

   PLUS DE ZOOM D'ARRIVÉE (2026-08-25). L'image partait à scale(1.08) et
   revenait à 1 en 14 s (une animation et ses keyframes, supprimées ici).
   Rom l'a fait retirer parce qu'elle saccadait : 8 % d'échelle étalés sur
   14 s, c'est de l'ordre du dixième de pixel par image, et à cette
   finesse le rendu avance par paliers plutôt qu'il ne glisse. Une mise
   sur couche (will-change + backface-visibility) a été essayée d'abord,
   sans succès — inutile de la retenter, elle a été jugée pire. La photo
   est donc parfaitement fixe : plus rien ne l'anime, ni au chargement ni
   au scroll (la parallaxe a été retirée juste avant, voir js/home.js §5),
   et elle n'a plus besoin de sa propre couche de composition. */
.hero-x__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}

/* Praticienne ET client mieux visibles sur écran 13 pouces (2026-09-02,
   demande de Rom). La carte de texte a une largeur FIXE (.hero-x__card,
   max-width: 44rem = 704px, + le padding-left plafonné à 10.5rem de
   .hero-x__inner, donc ~872px au total dès que la fenêtre dépasse ~1400px
   de large) alors que la photo, elle, est en cover et suit la largeur de
   la fenêtre : sur un grand écran la carte n'est qu'une fraction de la
   largeur et le client (allongé, soigné par la praticienne) reste visible
   à droite ; sur un 13-14 pouces (1200-1600 px de large environ) cette
   même carte fixe mange une part bien plus grande de l'écran et son bord
   droit tombe pile sur le visage du client, qui disparaît derrière elle
   (vérifié : à 1512×820 et 1366×606, seul son menton dépassait).
   Solution : zoom + décalage HORIZONTAL SEUL sur cette tranche de largeur,
   pour révéler une portion plus à droite de la photo (donc plus loin du
   bord de la carte) sans toucher au cadrage vertical (qui, lui, dépend de
   la hauteur d'écran et est déjà correct à toutes les hauteurs via
   object-fit: cover — voir [[project_alycare_photos]] §6 sur la toile
   élargie de hero_wide.webp, qui gère le cadrage horizontal par défaut).
   `scaleX(-1.28)` et non `scale(1.28)` : un zoom UNIFORME (scale() sur les
   deux axes) avait aussi recadré la hauteur visible et coupait le haut du
   visage de la praticienne sur les fenêtres les plus courtes (1366×606) —
   scaleX seul zoome uniquement l'axe horizontal (le signe négatif combine
   le zoom ET le miroir existant en une seule fonction, sans toucher à Y).
   Bornes 1200-1600px choisies pour couvrir les résolutions 13"/14" déjà
   identifiées dans [[project_alycare_laptop]] (1280, 1366, 1440, 1470,
   1512, 1600) sans toucher aux grands écrans (1920+, déjà corrects) ni aux
   petites fenêtres/mobile (où le hero a une tout autre mise en page).
   Revérifier au pixel avec Rom si la largeur de la carte (.hero-x__card)
   change un jour : ces valeurs sont calées sur SA largeur actuelle. */
@media (min-width: 1200px) and (max-width: 1600px) {
  .hero-x__bg img {
    transform: scaleX(-1.28) translateX(-130px);
  }
}

/* PLUS AUCUN VOILE SUR LA PHOTO (2026-08-25, demande de Rom : "pas de
   masque de couleur rose ou noir, simplement l'image de base"). La règle
   .hero-x__bg::after qui vivait ici posait un dégradé charbon sur toute
   l'image, à la fois pour la lisibilité du texte et pour fondre le bas de
   la photo dans la page. Les deux fonctions ont été rendues autrement :
   la lisibilité par l'encre sombre (--color-on-light), le raccord du bas
   par le "store" à lames qui recouvre le hero au scroll de toute façon. */

/* Le hero ne suit PAS la gouttière du reste du site (2026-08-25) : Rom a
   demandé la carte plus à gauche. .hero-x__inner porte quand même la
   classe .container — on neutralise donc son max-width et sa gouttière
   symétrique ici (même spécificité, mais home.css est chargé après
   main.css, donc c'est cette règle qui gagne). La marge de droite reste
   celle du site, pour que la carte ne touche pas le bord sur mobile. */
.hero-x__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: none;
  /* Réglé à l'œil avec Rom, dans cet ordre : 144 px (la gouttière du site,
     le point de départ) → 64 px ("plus à gauche") → 101 px ("un peu trop à
     gauche") → 120 px → 144 px → 168 px, la valeur retenue. On a donc
     dépassé la marge de départ, et de toute façon ce n'est PAS le même
     mécanisme : la gouttière du .container est calculée sur un contenu de
     1180 px centré, donc elle s'élargit sans fin sur un grand écran (près
     de 400 px en 1920) ; ce padding-là, plafonné à 10,5rem, garde la carte
     à distance constante du bord quelle que soit la largeur. C'est ce qui
     change tout sur les grands écrans. */
  padding-left: clamp(1.5rem, 12vw, 10.5rem);
  padding-right: var(--space-md);
}

/* ---- La carte de verre (2026-08-25) ----
   Demandée par Rom après le retrait du voile : "le texte sur le héro n'est
   pas vraiment visible, j'aimerais rajouter une card pour faire sortir le
   texte et les boutons". Elle épouse le bloc de texte, calée à gauche, et
   laisse toute la moitié droite de la photo — donc le visage — dégagée.
   Elle SUIT LE THÈME (crème en clair, charbon en sombre) : c'est la seule
   surface du hero qui le fasse, tout le reste (en-tête transparent, flèche
   "Découvrir", badge Instagram) est posé sur la photo, qui est claire dans
   les deux thèmes et garde donc son encre --color-on-light.
   Le fond plein ci-dessous est le REPLI pour les navigateurs sans
   backdrop-filter ; le verre dépoli est ajouté juste après, dans le
   @supports — même construction que la barre du haut au scroll. */
.hero-x__card {
  max-width: 44rem;
  padding: clamp(1.6rem, 1rem + 2vw, 3rem);
  border-radius: var(--radius-lg);
  /* Repli plein pour les navigateurs sans backdrop-filter : le BLANC en
     dur, pas var(--color-surface), pour la même raison que les tokens
     ci-dessous — la carte est claire dans les deux thèmes. */
  background: #ffffff;
  border: 1px solid var(--hero-card-border);
  box-shadow: var(--hero-card-shadow);
  color: var(--color-on-light);
  opacity: 0;
  transform: translateY(18px);
  /* Voir le @supports juste en dessous : c'est cette transition qui fait
     passer la carte de l'aplat blanc au verre dépoli sans à-coup. */
  transition: background-color 0.7s var(--ease-out);
}

/* PERF-2026-08-27 — LE VERRE DÉPOLI N'ARRIVE QU'APRÈS LE PRÉLOADER.
   Mesuré : pendant que le rideau du préloader se relève (translateY sur
   1 s) PAR-DESSUS cette carte, le navigateur doit relire le fond derrière
   elle et le reflouter à chaque frame. Résultat, 33,3 ms par frame (30 fps)
   et 58 à 70 % de frames longues. Sans le backdrop-filter : 16,7 ms, ZÉRO
   frame longue.
   Ce n'est ni le rayon du flou (le ramener à 10 px ne gagne rien) ni un
   défaut de promotion en calque (`will-change` ne gagne rien) : c'est la
   présence même d'un backdrop-filter sous un calque plein écran qui
   s'anime. AU REPOS il est gratuit — d'où ce simple report.

   `body.is-intro-done` est posée par js/home.js §1 au moment où le
   préloader est retiré du DOM. Deux conséquences voulues :
   - avant cette classe, c'est le repli `background: #ffffff` ci-dessus qui
     s'applique — exactement ce que voient déjà les navigateurs sans
     backdrop-filter, donc un état déjà dessiné et validé ;
   - le passage de l'un à l'autre est porté par la transition de
     background-color ci-dessus, PAS par une transition du flou : le flou
     apparaît instantanément mais reste invisible tant que le fond est
     opaque, puis se révèle à mesure que le fond devient translucide.
     Animer le flou lui-même coûterait précisément ce qu'on cherche à
     éviter.
   Si le JS ne tourne pas du tout, la carte reste en aplat blanc : lisible,
   sans le verre dépoli. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  body.is-intro-done .hero-x__card {
    background: var(--hero-card-bg);
    /* Flou monté avec la transparence (2026-08-25) : moins la carte
       couvre, plus c'est le flou qui doit calmer ce qu'il y a derrière
       pour que le texte reste lisible. */
    -webkit-backdrop-filter: blur(26px) saturate(1.2);
    backdrop-filter: blur(26px) saturate(1.2);
  }
}

/* SEO-2026-09-08 : ces 6 règles étaient posées sur body.is-ready (classe
   ajoutée par js/home.js SEULEMENT après l'écriture complète du logo,
   ~2,8 s). Le H1/carte/CTA restaient donc invisibles (translateY/opacity 0)
   jusque-là, retardant d'autant le LCP réel — mesuré, pas juste soupçonné :
   le rideau du préloader (z-index 900, opaque, voir §1) masque de toute
   façon tout par-dessus, donc rien n'empêche ces animations de jouer en
   pur CSS dès le chargement, SANS attendre le JS ni le préloader. Rendu
   final identique à l'écran (le rideau se lève sur un hero déjà stabilisé,
   comme avant), mais peint ~1,5 s plus tôt au lieu de ~4 s. Le repli
   prefers-reduced-motion (§14) n'est pas affecté : il force déjà
   opacity/transform à leur état final, quelle que soit la règle qui
   déclenche l'animation.
   La carte arrive AVANT son contenu (0,15 s, contre 0,5 et 0,62 s pour le
   texte et le bouton) : le cadre se pose, puis le texte s'y écrit. */
.hero-x__card { animation: fadeUp 1s var(--ease-out) 0.15s forwards; }

.hero-x__eyebrow {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  /* --color-primary et non --color-eyebrow : ce dernier s'éclaircit en
     thème sombre (#ff6f92), or la carte, elle, reste claire — le rose
     éclairci y perdrait tout contraste. Le rose vif, lui, est identique
     dans les deux thèmes. */
  color: var(--color-primary);
  /* Explicite depuis que le sur-titre vit DANS le h1 (2026-09-08) : sans
     cette ligne il hériterait du line-height 0.92 de .hero-x__title au
     lieu du 1.6 du corps de texte, et la bande rose remonterait. */
  line-height: var(--line-height-base);
  margin-bottom: var(--space-md);
}

.hero-x__eyebrow::before {
  content: '';
  width: 40px;
  height: 1px;
  background: var(--color-primary);
}

/* Mélange vw + vh (au lieu de vw seul) : sur un viewport bas mais large
   (mobile en paysage, fenêtre de navigateur réduite en hauteur), la taille
   du titre suit aussi la hauteur d'écran, pas uniquement la largeur — évite
   un titre disproportionné qui déborderait verticalement une fois le hero
   centré (voir .hero-x, align-items: center). */
.hero-x__title {
  font-family: var(--font-heading);
  font-size: clamp(2.2rem, 3vw + 4vh, 8.5rem);
  line-height: 0.92;
  letter-spacing: -0.035em;
  font-weight: 500;
  margin: 0 0 var(--space-md);
  /* --color-on-light, pas --color-text : la carte reste claire même en
     thème sombre, où --color-text virerait au crème — donc invisible.
     Volontairement pas --color-heading (le vert des titres) non plus : le
     hero n'a jamais été vert, et Rom veut le vert avec parcimonie. */
  color: var(--color-on-light);
}

.hero-x__title .line {
  display: block;
  overflow: hidden;
}

.hero-x__title .line > span {
  display: block;
  transform: translateY(110%);
}

.hero-x__title .line > span {
  animation: lineUp 1.15s var(--ease-out) forwards;
}

.hero-x__title .line:nth-child(2) > span { animation-delay: 0.1s; }
.hero-x__title .line:nth-child(3) > span { animation-delay: 0.2s; }

@keyframes lineUp {
  to { transform: translateY(0); }
}

.hero-x__title em {
  font-style: italic;
  /* Même raison que l'eyebrow : rose vif, seul rose qui tienne sur un
     fond clair. */
  color: var(--color-primary);
}

.hero-x__lede,
.hero-x__actions {
  opacity: 0;
  transform: translateY(18px);
}

.hero-x__lede { animation: fadeUp 1s var(--ease-out) 0.5s forwards; }
.hero-x__actions { animation: fadeUp 1s var(--ease-out) 0.62s forwards; }

@keyframes fadeUp {
  to { opacity: 1; transform: none; }
}

/* ==========================================================================
   3bis. TITRES DE SECTION — MÊME EFFET D'APPARITION QUE LE HERO
   Réutilise exactement le principe de .hero-x__title (lignes masquées,
   texte qui glisse du bas vers le haut), mais déclenché au scroll plutôt
   qu'au chargement : le balisage .line > span est identique, seul le
   déclencheur change (classe .is-in posée par l'IntersectionObserver
   existant sur [data-reveal], voir home.js §8 — aucun JS supplémentaire).
   À poser sur n'importe quel titre de section (h2, .manifesto__quote…)
   à l'intérieur d'un ancêtre [data-reveal]. ========================== */
.reveal-title .line {
  display: block;
  overflow: hidden;
}

.reveal-title .line > span {
  display: block;
  transform: translateY(110%);
  transition: transform 1.15s var(--ease-out);
}

.reveal-title .line:nth-child(2) > span { transition-delay: 0.1s; }
.reveal-title .line:nth-child(3) > span { transition-delay: 0.2s; }

[data-reveal].is-in .reveal-title .line > span,
[data-reveal] .reveal-title.is-in .line > span {
  transform: translateY(0);
}

.hero-x__lede {
  max-width: 46ch;
  font-size: clamp(1rem, 0.94rem + 0.3vw, 1.2rem);
  /* Même logique que le titre : l'encre de la carte, allégée pour rester
     secondaire. Pas --color-text-light, qui s'éclaircit en thème sombre. */
  color: color-mix(in srgb, var(--color-on-light) 72%, transparent);
  margin-bottom: var(--space-lg);
}

/* ---- Le « … » dépliant du texte du hero (2026-08-26) ----
   Sur téléphone, la carte doit tenir dans l'écran : la phrase s'arrête à
   « au cœur de Toulouse », la suite se déplie au toucher.
   Progressivement amélioré, dans ce sens-là : par DÉFAUT tout est visible
   et le bouton est masqué (c'est l'état desktop, et l'état sans JS). C'est
   js/home.js qui pose .is-lede-collapsible sur le paragraphe, et seule
   cette classe — combinée à la media query mobile — replie la phrase. Si le
   JS ne tourne pas, le visiteur lit le texte entier au lieu de tomber sur
   un « … » mort. */
.hero-x__lede-toggle {
  display: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  line-height: inherit;
  color: var(--color-primary);
  letter-spacing: 0.08em;
  /* L'espace avant le crochet ouvrant est une MARGE, pas une espace écrite
     dans le HTML (2026-08-26) : une vraie espace survivrait au masquage du
     bouton une fois le texte déplié, et on lirait « Toulouse . Épilation
     laser ». La marge, elle, disparaît avec l'élément. */
  margin-left: 0.35em;
  /* Le point d'appui de la zone tactile ci-dessous. */
  position: relative;
}

@media (max-width: 760px) {
  .hero-x__lede.is-lede-collapsible .hero-x__lede-toggle {
    display: inline;
  }

  /* Une inline de trois points fait ~20 × 20 px : bien trop peu pour un
     doigt. Ce pseudo-élément agrandit la SURFACE CLIQUABLE à ~48 px de
     haut sans rien changer au rendu ni pousser les lignes voisines (il est
     hors flux). Bornes latérales serrées (-10px) pour ne pas couvrir le
     mot précédent. */
  .hero-x__lede.is-lede-collapsible .hero-x__lede-toggle::after {
    content: "";
    position: absolute;
    inset: -14px -10px;
  }

  .hero-x__lede.is-lede-collapsible .hero-x__lede-more {
    display: none;
  }

  .hero-x__lede.is-lede-collapsible.is-lede-open .hero-x__lede-more {
    display: inline;
    /* display ne se transitionne pas : c'est une animation, jouée une fois
       à l'apparition, qui adoucit le dépliement. */
    animation: heroLedeReveal 0.5s var(--ease-out) both;
  }

  /* Une fois déplié, le « … » n'a plus de sens au milieu de la phrase : il
     disparaît. Le dépliement est donc à sens unique — pour un texte de
     trois lignes, un bouton « Réduire » permanent coûterait plus qu'il ne
     rapporte. */
  .hero-x__lede.is-lede-open .hero-x__lede-toggle {
    display: none;
  }
}

@keyframes heroLedeReveal {
  from { opacity: 0; }
  to { opacity: 1; }
}

.hero-x__actions {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* Badge rotatif — plafond du bottom repoussé (voir .hero-x plus haut) pour
   éviter le même effet de plateau sur les écrans à grande hauteur de
   viewport, et rester cohérent avec la position du titre. */
/* position: fixed (et non absolute) depuis le 2026-08-13 : le badge doit
   rester visible à l'écran pendant tout le scroll (haut comme bas), pas
   seulement tant que le hero est visible — d'où aussi le z-index relevé
   (300, au-dessus du header sticky à 100 et des sections, mais sous le
   curseur custom à 500+ et le widget dev à 500). */
.hero-x__badge {
  position: fixed;
  right: clamp(1.5rem, 5vw, 5rem);
  bottom: clamp(6rem, 18vh, 22rem);
  z-index: 300;
  /* Diamètre proportionnel à la hauteur d'écran depuis le 2026-08-27 :
     à 132 px fixes il occupait 10 % de la hauteur sur un 27 pouces mais
     21 % sur un 13 pouces. Plafonné à 132 px dès 892 px de haut, donc
     inchangé sur tout écran de bureau. Voir variables.css § ÉCHELLE
     LAPTOP. */
  width: var(--badge-size);
  height: var(--badge-size);
  /* right ci-dessus est pensé en vw : sur un grand écran (27"/32"), 5vw
     dépasse vite le plafond de 5rem et le badge reste bien collé au bord.
     Sur un petit écran (13", ~1280-1440px de large), 5vw reste EN DESSOUS
     de ce plafond (ex. 64-72px) — visuellement, avec une largeur de
     viewport plus étroite, ce même écart en px "mange" une part bien plus
     grande de l'écran et le badge paraît trop loin du bord droit. Voir le
     correctif en media query plus bas (2026-08-13). */
  display: grid;
  place-items: center;
  text-decoration: none;
}

/* Écrans "13 pouces" et similaires (laptop, viewport plus étroit) : right
   fixe plus petit, sans dépendance au vw, pour coller davantage le badge
   au bord droit — voir commentaire ci-dessus (2026-08-13). Les grands
   écrans (27"/32", au-delà de 1600px) gardent le calcul en clamp/vw
   d'origine, inchangé. */
@media (max-width: 1600px) {
  .hero-x__badge {
    right: 1.75rem;
    bottom: clamp(9rem, 24vh, 22rem);
  }
}

.hero-x__badge svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  animation: spin 18s linear infinite;
}

.hero-x__badge text {
  fill: var(--color-on-dark);
  font-family: var(--font-body);
  font-size: 9.6px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  font-weight: 700;
  transition: fill 0.25s ease;
}

.hero-x__badge-core {
  /* 56/132 et 17.6/132 : les proportions d'origine, rapportées au
     diamètre courant du badge (voir --badge-size). Le SVG du texte
     tournant, lui, a un viewBox et se met à l'échelle tout seul. */
  width: calc(var(--badge-size) * 0.4242);
  height: calc(var(--badge-size) * 0.4242);
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-text-on-primary);
  display: grid;
  place-items: center;
  font-size: calc(var(--badge-size) * 0.1333);
  /* Le halo rose (une lueur qui fait exister la pastille comme une source
     lumineuse) n'a de sens que sur fond sombre : voir les surcharges en fin
     de fichier. La transition le prévoit dans les deux thèmes. */
  transition: transform 0.4s var(--ease-out), background-color 0.3s ease, box-shadow 0.3s ease;
}

.hero-x__badge:hover .hero-x__badge-core {
  transform: scale(1.14);
  background: var(--color-primary-dark);
}

/* Aiguille de boussole : le centre de .hero-x__badge-core ne bouge jamais,
   seule cette flèche pivote pour pointer vers le curseur — piloté par
   js/home.js § "Badge magnétique" (rotateZ appliqué à chaque frame, en plus
   du scale ci-dessus qui reste géré en CSS pur sur l'élément parent, donc
   aucun conflit de transform entre les deux). Le glyphe "↗" pointe
   naturellement à -45° (haut-droite) : c'est la référence à partir de
   laquelle le JS calcule l'angle à ajouter. */
.hero-x__badge-arrow {
  display: grid;
  place-items: center;
  will-change: transform;
}

.hero-x__badge-arrow svg {
  /* Reset : .hero-x__badge svg (règle générique pour le grand cercle de
     texte tournant, ci-dessus) matche AUSSI cette icône et lui impose
     position:absolute + inset:0 — sans ce reset, elle sort du flux et se
     cale en haut à gauche de tout le badge (132px) au lieu d'être centrée
     dans le petit cercle (.hero-x__badge-core, 56px). */
  position: static;
  display: block;
  width: 18px;
  height: 18px;
  /* Encre de l'icône Instagram : NOIRE en thème clair, blanche en sombre
     (2026-08-26, demande de Rom). Le token porte toute la bascule — voir
     sa note dans variables.css. Ne pas revenir à --color-text-on-primary
     ici : ce token-là est blanc dans les deux thèmes et sert aux CTA. */
  color: var(--badge-icon-ink);
  transition: color 0.25s ease;
}

/* Historique : en thème clair, le fond derrière le badge devenait blanc
   pendant l'animation de store (et sous les sections claires), donc ses
   couleurs claires d'origine y étaient invisibles — d'où ce passage au
   noir. En thème sombre (2026-08-21) il n'y a plus AUCUN fond clair sur
   le site : le badge doit rester clair partout. Le mécanisme (classes
   is-store-animating / is-badge-light-bg posées par js/home.js à partir
   de data-badge-theme dans le HTML) est conservé tel quel, mais il pointe
   maintenant sur l'encre claire — si une section claire réapparaît un
   jour, il suffira de remettre une couleur sombre ici. */
/* Deux situations qui se ressemblent mais n'appellent PAS la même encre —
   elles ont été séparées le 2026-08-25, quand la photo du hero est
   devenue claire.
   1. is-store-animating : le badge est au-dessus du FOND DE PAGE (le
      blanc/charbon des lames du store pendant leur fermeture). L'encre du
      fond de page, c'est --color-text : sombre en clair, crème en sombre.
   2. is-badge-light-bg : le badge est au-dessus d'un fond CLAIR — une
      section normale en thème clair, ou la photo du hero, claire dans les
      DEUX thèmes. Ici --color-text donnerait du crème sur crème en thème
      sombre (bug constaté sur la nouvelle photo) : il faut l'encre des
      fonds clairs, --color-on-light, qui ne bouge pas avec le thème. */
html.is-store-animating .hero-x__badge text {
  fill: var(--color-text);
}

html.is-badge-light-bg .hero-x__badge text {
  fill: var(--color-on-light);
}

/* L'icône, elle, vit À L'INTÉRIEUR de la pastille rose vif, et ne dépend
   donc PAS du fond survolé : ni is-store-animating ni is-badge-light-bg ne
   la concernent. Son encre est purement affaire de thème et vit dans
   --badge-icon-ink (voir .hero-x__badge-arrow svg plus haut).
   Historique : elle avait été passée au noir SUR FOND CLAIR (mécanisme
   is-badge-light-bg), puis remise en blanc partout le 2026-08-25. Le noir
   est revenu le 2026-08-26, mais cette fois indexé sur le THÈME et non sur
   le fond survolé — ce qui est plus stable : le badge ne change plus de
   couleur en cours de scroll. */

@keyframes spin {
  to { transform: rotate(360deg); }
}

.hero-x__cue {
  position: absolute;
  left: 50%;
  bottom: 1.4rem;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.62rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  /* Texte "Découvrir" en blanc depuis le 2026-09-03 (demande de Rom),
     même détournement de --color-on-dark que le bandeau top ci-dessus
     (voir body.at-top site-header .site-header__link) : cet élément ne
     vit que dans le hero, pas besoin de le conditionner à .at-top. Le
     trait vertical juste en dessous (.hero-x__cue i) garde son encre
     d'origine — seul le texte a été demandé. */
  color: color-mix(in srgb, var(--color-on-dark) 62%, transparent);
}

.hero-x__cue i {
  width: 1px;
  height: 44px;
  background: color-mix(in srgb, var(--color-on-light) 25%, transparent);
  position: relative;
  overflow: hidden;
}

.hero-x__cue i::after {
  content: '';
  position: absolute;
  left: 0;
  top: -100%;
  width: 100%;
  height: 100%;
  /* Rose vif : la goutte qui descend le long du trait doit se voir sur un
     fond clair, le rose pâle s'y effaçait. */
  background: var(--color-primary);
  animation: cue 2s ease-in-out infinite;
}

@keyframes cue {
  0% { top: -100%; }
  55% { top: 0; }
  100% { top: 100%; }
}

/* ==========================================================================
   4bis. HERO → MANIFESTE — effet "store" (lames qui se ferment), avec
   #manifeste EN CALQUE À L'INTÉRIEUR DU PIN (2026-08-11)
   -----------------------------------------------------------------------
   Reprend le principe déjà utilisé pour les cartes "Les soins" (§6 plus
   bas) : pas de position:sticky calée en pixels bruts — ici on l'utilise
   uniquement pour garder le hero plein écran pendant que les lames se
   ferment dessus (§"Hero → Manifeste" dans home.js), mais la progression
   de l'animation elle-même est calculée en fraction de la hauteur d'écran
   (getBoundingClientRect), donc cohérente sur toutes les tailles d'écran.
   .hero-store réserve une hauteur supplémentaire (100svh de hero + 40svh
   de "couloir" de scroll) pendant laquelle .hero-store__pin reste collé en
   haut du viewport. La part de ce couloir réellement consacrée à la
   fermeture des lames est réglée en JS (zone morte de départ, voir
   HERO_STORE_START_DEADZONE dans home.js §6ter).
   IMPORTANT : #manifeste n'est plus une section normale plus bas dans la
   page — elle est posée en calque (position:absolute) par-dessus les
   lames, à l'intérieur même du pin. Son apparition (fondu) n'est PAS
   pilotée en continu par la molette comme les lames : une fois le couloir
   de scroll entièrement parcouru (lames closes), la classe
   .is-store-open est ajoutée une seule fois (voir home.js) et un simple
   transition CSS fait apparaître #manifeste automatiquement, SANS scroll
   supplémentaire — c'est la consigne du 2026-08-11 ("qui suis-je doit
   s'afficher une fois le fond blanc affiché, sans avoir à scroller").
   Une fois le couloir terminé, le pin se libère et tout l'ensemble (hero +
   lames + manifeste, empilés) défile normalement vers le haut, laissant
   place à la section suivante — #manifeste ne réapparaît pas plus bas.
   ========================================================================== */
.hero-store {
  position: relative;
  /* Couloir de scroll (au-delà des 100svh du hero). Historique, parce que
     la valeur a fait des allers-retours : 40svh à l'origine, allongé à
     65svh le 2026-08-11 (les lames se fermaient trop vite), ramené à 40svh
     le 2026-08-25 en même temps que la quasi-suppression de la zone morte
     de départ. À l'époque des 65svh, 60 % du couloir ne servait à RIEN
     qu'à attendre : il fallait scroller ~39svh avant de voir bouger quoi
     que ce soit. Désormais les lames répondent au premier cran et se
     ferment sur ~37svh — soit à peu près la même vitesse ressentie
     qu'avant, mais sans le temps mort. Les deux réglages vont ensemble :
     changer cette hauteur sans revoir HERO_STORE_START_DEADZONE (home.js
     §6ter) change la vitesse de fermeture. */
  height: 140svh;
}

.hero-store__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.hero-store__pin .hero-x {
  height: 100%;
  min-height: 0;
}

/* Couche de fond pleine, SANS découpe en lames, posée juste sous
   .hero-store__blinds (z-index 49 < 50) et opacifiée en phase avec elles
   (voir layoutHeroStore en JS). Corrige un artefact de rendu observé sur
   certains écrans haute résolution/scaling non-entier (32", etc.) : de
   fines lignes de démarcation sous-pixel apparaissaient entre les lames
   flex, même une fois "fermées". Cette couche garantit qu'aucune ligne ne
   peut jamais apparaître, quel que soit l'écran : tout interstice entre
   les lames révèle cette même teinte pleine, pas le hero en dessous. */
.hero-store__wash {
  position: absolute;
  inset: 0;
  z-index: 49;
  background: var(--color-surface);
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
}

.hero-store__blinds {
  position: absolute;
  inset: 0;
  z-index: 50; /* au-dessus du contenu du hero (max z-index: 3), sous #manifeste et l'en-tête */
  display: flex;
  pointer-events: none;
}

.hero-store__slat {
  flex: 1 1 0%;
  background: var(--color-surface);
  transform: scaleY(0);
  will-change: transform;
}

/* Origine alternée (haut / bas) : les lames ne se ferment pas toutes dans
   le même sens, pour un rendu moins mécanique. */
.hero-store__slat:nth-child(even) { transform-origin: top; }
.hero-store__slat:nth-child(odd) { transform-origin: bottom; }

/* #manifeste en calque plein écran par-dessus les lames (z-index 60, sous
   l'en-tête à 100). Fondu géré par .is-store-open (ajoutée une seule fois
   en JS quand le couloir est terminé) plutôt que par la progression du
   scroll : l'apparition est donc automatique, pas besoin de continuer à
   scroller une fois le fond blanc atteint. */
.hero-store__pin #manifeste {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  padding: clamp(1.5rem, 5vh, var(--space-2xl)) 0;
  overflow-y: auto;
  opacity: 0;
  transition: opacity 0.9s var(--ease-out) 0.05s;
  pointer-events: none;
}

.hero-store.is-store-open .hero-store__pin #manifeste {
  opacity: 1;
  pointer-events: auto;
}

/* Repli statique : plus de pin, plus de lames — le hero puis #manifeste
   s'affichent l'un après l'autre normalement, comme deux sections classiques.
   ÉTENDU AU MOBILE LE 2026-08-26 (≤ 760 px), après mesure. Le calque
   #manifeste est en position: absolute; inset: 0 DANS un pin de 100svh en
   overflow: hidden : sa hauteur est celle de l'ÉCRAN, jamais celle de son
   contenu. Or ce contenu (sur-titre, citation, deux paragraphes, signature,
   photo portrait) fait ~740 px même une fois resserré pour le mobile — il
   tenait sur un iPhone 14 (844) mais débordait de 178 px sur un iPhone SE
   (667) et de 119 px sur un Android de 740, le débord partant dans un
   overflow-y: auto, donc en SCROLL IMBRIQUÉ (un scroll dans le scroll, à
   proscrire au doigt).
   Aucune valeur ne pouvait corriger ça durablement : tant que le manifeste
   est un calque de la hauteur de l'écran, il faut soit rogner la photo, soit
   rapetisser le texte à chaque nouveau modèle de téléphone. On le remet donc
   dans le flux sur mobile — le hero occupe l'écran, puis le manifeste défile
   normalement en dessous, sans contrainte de hauteur.
   CE QUE ÇA COÛTE : sur téléphone, les lames du store ne se ferment plus (le
   pin n'existe plus, il n'y a plus de couloir à parcourir). C'est une DÉCISION
   À REVOIR AVEC ROM — il l'a explicitement mise de côté (« fais au mieux selon
   les standards mobile 2026 et on avisera »). Pour rétablir l'animation sur
   téléphone, il suffit de retirer `(max-width: 760px)` de la ligne ci-dessous
   ET la condition de largeur ajoutée le même jour à `heroStoreEnabled`
   (js/home.js §6ter) — les deux vont ensemble : sans le couloir CSS, le calcul
   de progression du JS prend la hauteur du manifeste pour un couloir de scroll
   et verrouille la page au premier geste. */
@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .hero-store { height: auto; }
  .hero-store__pin {
    position: relative;
    height: auto;
    overflow: visible;
  }
  .hero-store__pin .hero-x { height: 100svh; min-height: 100svh; }
  .hero-store__blinds { display: none; }
  .hero-store__pin #manifeste {
    position: static;
    z-index: auto;
    opacity: 1;
    transition: none;
    pointer-events: auto;
  }
}

/* ==========================================================================
   5. MANIFESTE
   ========================================================================== */
.manifesto {
  /* Fond dégradé léger (2026-08-20) : deux halos radiaux très doux (rose
     poudré en haut à gauche, teinte chaude à peine perceptible en bas à
     droite) qui se fondent dans le blanc — remplace l'aplat uni
     var(--color-surface). color-mix() garde l'effet cohérent avec les 6
     palettes du widget de dev (pas de hex en dur). */
  /* Sur fond sombre ces deux halos doivent être BEAUCOUP plus dilués (voir
     les surcharges en fin de fichier) : à 58%/32% le rose pâle virait à
     l'aplat laiteux et écrasait toute la section. */
  background:
    radial-gradient(62% 70% at 14% 10%, color-mix(in srgb, var(--color-secondary) 58%, transparent) 0%, transparent 74%),
    radial-gradient(58% 68% at 90% 96%, color-mix(in srgb, var(--color-primary) 32%, transparent) 0%, transparent 76%),
    var(--color-surface);
  overflow: hidden;
}

.manifesto__grid {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
  /* Texte + image descendus ensemble de 10 % (2026-08-11), sur demande. */
  transform: translateY(10%);
}

.manifesto__glyph {
  position: absolute;
  top: -4rem;
  left: -1.4rem;
  font-family: var(--font-heading);
  font-size: 12rem;
  line-height: 1;
  color: var(--color-primary);
  /* 0.1 en clair, le double en sombre où un rose à 10% disparaît
     complètement (voir les surcharges en fin de fichier). */
  opacity: 0.1;
  pointer-events: none;
  user-select: none;
}

.manifesto__col { position: relative; }

.manifesto__quote {
  position: relative;
  font-family: var(--font-heading);
  font-style: italic;
  font-size: clamp(1.5rem, 1rem + 2.1vw, 2.9rem);
  line-height: 1.24;
  letter-spacing: -0.015em;
  margin-bottom: var(--space-md);
  /* C'est un <p>, pas un h-tag : couleur de titre posée explicitement
     (2026-08-21) pour rester cohérente avec la règle h1-h4. */
  color: var(--color-heading);
}

.manifesto__rule {
  width: 68px;
  height: 2px;
  background: var(--color-primary);
  margin: var(--space-md) 0;
}

.manifesto__sub {
  color: var(--color-text-light);
  max-width: 50ch;
  font-size: 1.02rem;
}

.manifesto__sign {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: var(--space-md);
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-primary-dark);
  font-weight: 700;
}

.manifesto__sign::before {
  content: '';
  width: 28px;
  height: 1px;
  background: var(--color-primary);
}

.manifesto__media {
  position: relative;
  /* Ratio 670 × 1194, celui de assets/images/alyssa.webp. Il datait de la
     première photo (2026-08-11 : ratio natif gardé tel quel plutôt que le
     3/4 recadré d'origine, demande explicite de Rom). La photo a changé le
     2026-08-28 — un plan pied, néons roses, Alyssa assise, source
     alyssa.jpg en 1334 × 2000 — et c'est ELLE qui a été recadrée à ce
     ratio, pas l'inverse : ce nombre reste donc la référence, toute
     nouvelle photo se recadre dessus (sinon il faut aussi revoir les deux
     replis, max-width 340px sous 760 et height min(625px, …) en laptop
     bas, qui en dépendent). Voir le commentaire dans index.html. */
  aspect-ratio: 670 / 1194;
  /* Taille réduite de 20 % (2026-08-11) par rapport à la pleine largeur de
     sa colonne de grille — le ratio ci-dessus continue de piloter la
     hauteur, donc la photo rétrécit proportionnellement, sans déformation. */
  width: 80%;
  border-radius: var(--radius-md);
  transform: rotate(2.2deg);
  /* Ombre décalée "papier" : un brun très dilué en clair. Sur fond sombre
     il devient invisible et cède la place à une trace rose (voir les
     surcharges en fin de fichier) — la photo semble alors posée sur son
     propre reflet coloré. */
  box-shadow: 18px 22px 0 rgba(74, 59, 54, 0.07);
}

/* .manifesto__stamp ("Ex-infirmière") supprimé du HTML le 2026-08-11 —
   règle retirée avec lui (plus référencée nulle part sur le site actif). */

/* Spotlight au curseur (2026-08-11), inspiré de l'orbe de lumière colorée
   de clipdrop.co/relight : au survol de la photo, le curseur devient une
   source de lumière qui "éclaire" Alyssa en suivant sa position — teintée
   couleur accent de la palette (pas blanche/neutre), et SANS assombrir le
   reste de la photo (juste une zone plus lumineuse/contrastée, plus doux
   que l'effet spot-dans-le-noir de Clipdrop — choix du 2026-08-11).
   Fonctionnement : un calque plein (background: var(--color-accent)) est
   DÉCOUPÉ par un mask-image radial suivant le curseur (--x/--y posées en
   JS, voir home.js § "Spotlight..."), puis fondu avec la photo via
   mix-blend-mode — pas de dégradé rgba() à décomposer par palette, juste
   la couleur d'accent, définie une seule fois dans css/variables.css. */
.manifesto__spot {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--color-accent);
  opacity: 0;
  /* soft-light en clair ; en sombre il ne fait presque plus rien et cède
     la place à screen (voir les surcharges en fin de fichier). */
  mix-blend-mode: soft-light;
  -webkit-mask-image: radial-gradient(circle clamp(120px, 22vw, 240px) at var(--x, 50%) var(--y, 50%), #000 0%, transparent 70%);
  mask-image: radial-gradient(circle clamp(120px, 22vw, 240px) at var(--x, 50%) var(--y, 50%), #000 0%, transparent 70%);
  pointer-events: none;
  transition: opacity 0.35s ease;
}

.manifesto__media.has-spot .manifesto__spot {
  /* Baissé de 0.9 à 0.45 : en mode screen l'effet est bien plus franc,
     il faut donc beaucoup moins d'opacité pour le même ressenti. */
  opacity: 0.45;
}

@media (pointer: coarse), (prefers-reduced-motion: reduce) {
  /* Pas de souris (tactile) ou mouvement réduit : pas de spotlight, la
     photo garde son rendu normal. */
  .manifesto__spot { display: none; }
}

/* ==========================================================================
   5bis. INSTITUT — vidéo qui se réduit au scroll
   Inspiré de sly.clubop.site (section vidéo après "Three builds. One
   founder") : la vidéo occupe toute la largeur au chargement de la
   section, puis rétrécit et se cale à droite au scroll tandis que le
   texte apparaît à gauche.
   Mécanisme : .institut-video__row est une grille à 2 colonnes dont la
   largeur de la colonne de texte est pilotée par --p (0 → 1, posée en JS,
   voir home.js §"L'institut"). À p = 0 la colonne de texte fait 0 (donc
   invisible, opacity suit --p aussi) et la vidéo (1fr) occupe toute la
   largeur ; à mesure que --p augmente, la colonne de texte s'élargit,
   poussant mécaniquement la vidéo à rétrécir et se décaler à droite —
   pas de transform ni de calcul de position manuel, la grille fait tout.
   Pin en position:sticky pendant un couloir de scroll dédié, même
   principe que le Hero (§4bis) : progression calculée en fraction de la
   hauteur d'écran (pas en pixels bruts), lissée par lerp/rAF.
   ========================================================================== */
.institut-video {
  position: relative;
  height: 220svh;
  /* Même fond que "Qui suis-je" (2026-08-11) — donc texte foncé plutôt
     que blanc translucide comme sur l'ancien fond sombre. */
  background: var(--color-surface);
}

.institut-video__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  --p: 0;
  /* z-index < 300 (badge Instagram flottant, voir .hero-x__badge) : sticky
     crée toujours son propre stacking context, donc ce z-index se compare
     au badge au niveau racine du document — le badge doit rester au-dessus
     de la vidéo pendant cette section (demande du 2026-08-17, annule le
     comportement inverse du 2026-08-13). */
  z-index: 290;
  /* pointer-events: none (2026-09-03) — ce pin est TOUJOURS plein écran
     (100svh) et son z-index (290) passe au-dessus de <site-header> (100,
     lui aussi position: sticky/top: 0) : tant que la vidéo n'occupe pas
     toute la boîte (dès que --p > 0, elle rétrécit et se centre), la marge
     vide autour d'elle appartenait quand même à CE calque et interceptait
     les clics/survols destinés à la barre du dessous — y compris une fois
     la barre redevenue visible dans cette marge. Demande de Rom : le
     bandeau top doit redevenir hoverable (icône du curseur comprise) dès
     que la vidéo laisse apparaître de l'espace autour d'elle, pas
     seulement une fois le rétrécissement terminé. Le pin lui-même ne
     capte donc plus rien ; .institut-video__row ci-dessous non plus
     (même boîte pleine) ; seuls .institut-video__text et
     .institut-video__media-frame — qui épousent exactement ce qui est
     VISIBLEMENT la vidéo/le texte — le repassent à `auto`. */
  pointer-events: none;
}

.institut-video__row {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  /* Voir le commentaire pointer-events sur .institut-video__pin ci-dessus :
     cette rangée fait elle aussi 100% x 100% du pin, donc le même souci se
     reposerait si elle captait les clics. */
  pointer-events: none;
  /* Flex centré (texte + vidéo collés l'un à l'autre, pas de colonne
     réservée) : les deux blocs sont dimensionnés par leur propre largeur
     (voir width ci-dessous) et le flex les colle ensemble au centre, sans
     l'espace mort qu'on avait avec la colonne de grille à 42%. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--p) * clamp(1rem, 3vw, 2.5rem));
  /* Marges latérales proportionnelles à --p : à p = 0 (vidéo seule), plus
     aucune marge — elle occupe donc toute la largeur ET toute la hauteur
     de la section (voir .institut-video__media plus bas), pas seulement
     une bande centrée. */
  padding: 0 calc(var(--p) * clamp(1.5rem, 6vw, 5rem));
}

.institut-video__text {
  /* z-index (2026-08-26) : .institut-video__media-frame étant positionné,
     le fond décoratif se dessinait PAR-DESSUS les lettres du paragraphe
     quand ses courbes atteignaient la colonne de texte. */
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  width: calc(var(--p) * 30%);
  overflow: hidden;
  opacity: var(--p);
  transform: translateX(calc((1 - var(--p)) * -24px));
  color: var(--color-text);
  /* Repasse à `auto` : voir le commentaire pointer-events sur
     .institut-video__pin — cette boîte-ci épouse le texte réellement
     affiché, donc rien à laisser passer à travers elle. */
  pointer-events: auto;
}

.institut-video__title {
  font-family: var(--font-heading);
  font-size: clamp(1.8rem, 1.1rem + 2.6vw, 3.4rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0 0 var(--space-sm);
}

.institut-video__title em {
  font-style: italic;
  color: var(--color-primary-dark);
}

.institut-video__lede {
  color: var(--color-text-light);
  max-width: 38ch;
}

/* Cadre (2026-08-20) : porte désormais le dimensionnement piloté par --p
   qu'avait .institut-video__media (width/height identiques à avant), pour
   pouvoir loger .institut-video__contours juste derrière la vidéo —
   centré et calé sur sa taille, pas sur toute la section. .institut-video__media
   redevient un simple habillage visuel (radius/ombre/clip) en 100%/100%
   à l'intérieur. */
.institut-video__media-frame {
  position: relative;
  flex: 0 0 auto;
  width: calc(100% - var(--p) * 55%);
  height: calc(100% - var(--p) * 58%);
  /* Repasse à `auto` : voir le commentaire pointer-events sur
     .institut-video__pin — cette boîte-ci épouse exactement la vidéo
     affichée, donc rien à laisser passer à travers elle. */
  pointer-events: auto;
}

/* ==========================================================================
   5bis. CONTOURS ORGANIQUES — apparaissent derrière l'encart vidéo réduit
   Remplace le CADRILLAGE d'origine (2026-08-20), retiré le 2026-08-26 :
   Rom a choisi cette proposition parmi sept (ondes concentriques, vagues
   horizontales, écho du cadre, trame de points, rayons…) — la grille de
   lignes droites jurait avec le vocabulaire arrondi du reste du site.

   Le principe de révélation, lui, est INCHANGÉ : réutilise --p (0 → 1,
   posé en JS sur .institut-video__pin, voir home.js §6quater) par simple
   héritage de variable CSS, aucune ligne de JS ajoutée. Fenêtre de
   déclenchement toujours resserrée sur la fin du couloir de scroll
   (0.82 → 1) pour que le motif n'apparaisse qu'une fois la vidéo à sa
   taille minimale. Toujours calé sur .institut-video__media-frame (donc
   centré sur la vidéo, pas sur la section) et toujours en
   var(--color-primary), le rose vif de la charte.

   Le motif : cinq courbes fermées irrégulières emboîtées, façon carte
   topographique. Ce sont de simples <span> avec un border-radius
   elliptique à huit valeurs — c'est ce qui casse le cercle et donne la
   forme organique — chacun tournant très lentement (64 s à 174 s le tour)
   et un rang sur deux à l'envers : les contours se déforment donc en
   permanence les uns par rapport aux autres sans jamais se répéter.
   Même famille visuelle que le blob rose de la section Réservation
   (voir §10), déjà dans le vocabulaire du site.

   PIÈGE À CONNAÎTRE (coûté une itération) : un masque ne peint QUE la
   boîte de son élément. Les contours devant déborder de la vidéo, ils ne
   peuvent pas être posés en inset négatif sur une couche calée à
   inset: 0 — tout ce qui sortirait de la boîte tomberait hors du masque
   et disparaîtrait purement et simplement. D'où la construction inverse :
   la couche prend d'emblée tout le débord (--bleed-x / --bleed-y) et
   chaque contour se pose avec un inset POSITIF DÉCROISSANT depuis ce bord.
   Le contour n° 1 est donc le plus petit (au ras de la vidéo), le n° 5 le
   plus grand. Ne pas "simplifier" en insets négatifs.
   ========================================================================== */
.institut-video__contours {
  position: absolute;
  /* Débord borné plutôt que fixe : à -185px constants, sur un écran de
     1100-1200px les courbes de gauche traversaient le paragraphe. Ces
     deux variables réduisent l'étalement quand la fenêtre se resserre (la
     colonne de texte et la vidéo se rapprochant d'autant) et le plafonnent
     sur grand écran. TOUT le reste du motif en découle. */
  --bleed-x: clamp(95px, 11vw, 185px);
  --bleed-y: clamp(100px, 19svh, 170px);
  inset: calc(-1 * var(--bleed-y)) calc(-1 * var(--bleed-x));
  z-index: 0;
  pointer-events: none;
  /* PERF3-2026-08-27 — APPARITION APRÈS COUP, PAS PENDANT LA RÉDUCTION.
     Avant, l'opacité ET une mise à l'échelle étaient pilotées par
     `--grid-p = (--p - 0.82) × 5.55`, donc recalculées à chaque frame sur
     les 18 derniers pour cent du couloir — pile au moment où le navigateur
     recalcule déjà la géométrie de la vidéo et du texte. Une couche masquée
     dont l'opacité ET la taille changent ne peut pas être simplement
     recomposée : elle est re-rastérisée à chaque frame, et le motif fait
     ~1270 × 980 px débord compris.
     Désormais l'apparition est déclenchée par `.is-corridor-closed`, posée
     par js/home.js §6quater À LA FIN de la réduction — donc plus rien à
     recalculer pendant le couloir, et un simple fondu d'opacité une fois
     que la vidéo ne bouge plus. Un fondu d'opacité seul sur une couche qui
     ne change pas par ailleurs, c'est du travail de compositeur pur.

     `visibility: hidden` tant que ce n'est pas le moment : avec la seule
     opacité à 0, la couche reste peinte et composée. La transition de
     visibility est retardée de la durée du fondu à la fermeture, et
     instantanée à l'ouverture — recette classique, elle évite que la couche
     disparaisse d'un coup avant la fin du fondu.

     `.is-corridor-closed` est un verrou à sens unique (voir plus bas) :
     une fois les contours apparus ils restent, y compris si on remonte —
     cohérent avec la vidéo, dont la réduction est elle aussi définitive. */
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.9s var(--ease-out), visibility 0s 0.9s;
  /* Vignette plus large que celle du cadrillage (62%) : un motif fait de
     traits fermés doit être vu en entier, sinon il se lit comme un trait
     coupé et non comme une courbe. */
  mask-image: radial-gradient(80% 80% at 50% 50%, black 62%, transparent 100%);
  -webkit-mask-image: radial-gradient(80% 80% at 50% 50%, black 62%, transparent 100%);
}

.institut-video.is-corridor-closed .institut-video__contours {
  opacity: 1;
  visibility: visible;
  /* Petit retard : la vidéo vient de se figer, on laisse l'œil s'y poser
     avant que le fond ne se révèle. */
  transition: opacity 0.9s var(--ease-out) 0.12s, visibility 0s;
}

/* Pas de repli `prefers-reduced-motion` à écrire ici : le bloc de repli plus
   bas (@media max-width: 760px, prefers-reduced-motion) met déjà toute la
   couche en `display: none`. Et `.is-corridor-closed` n'est jamais posée
   dans ces cas-là — `institutVideoEnabled` (js/home.js §6quater) exige
   min-width 761px ET l'absence de préférence « moins d'animations ». */

/* Les cinq contours se répartissent DANS le débord, en fraction de
   celui-ci : le n° 1 au ras de la vidéo, le n° 5 juste en dedans du bord
   de la couche (facteur ~0.11, soit une vingtaine de px de marge au
   maximum du clamp). Écrire ces pas en px fixes casserait le motif dès
   que le clamp se resserre — le n° 5 sortirait de la boîte, donc du
   masque, donc de l'écran. */
.institut-video__contours span {
  position: absolute;
  inset:
    calc(var(--bleed-y) * (1 - var(--i) / 5.7))
    calc(var(--bleed-x) * (1 - var(--i) / 5.6));
  border: 1px solid color-mix(in srgb, var(--color-primary) calc(62% - var(--i) * 9%), transparent);

  /* ===== ROTATION DÉSACTIVÉE LE 2026-08-27 — TEST DE PERFORMANCE =====
     Deuxième moitié du test : les cercles sont de retour dans index.html,
     mais figés. But : savoir si le coût vient de la ROTATION ou de la seule
     PRÉSENCE du motif (cinq bordures arrondies de ~900 × 640 px repeintes
     dans une couche masquée sous un calque plein écran).
     RIEN N'EST SUPPRIMÉ : pour rétablir le mouvement, effacer les deux
     lignes de commentaire qui encadrent les déclarations ci-dessous, ET
     celles du bloc :nth-child(odd) juste après la fermeture de cette règle.
     ATTENTION en éditant : l'accolade fermante de cette règle est
     VOLONTAIREMENT hors du commentaire — la mettre dedans laisserait le
     sélecteur ouvert et casserait toute la suite de la feuille.
  animation: institut-contour-turn linear infinite;
  animation-duration: calc(64s + var(--i) * 22s);
     ===== FIN DU BLOC DÉSACTIVÉ ===== */
}

/* ===== ROTATION DÉSACTIVÉE LE 2026-08-27 (suite du bloc ci-dessus) =====
.institut-video__contours span:nth-child(odd) { animation-direction: reverse; }
   ===== FIN DU BLOC DÉSACTIVÉ ===== */

/* PERF-2026-08-27 — LA ROTATION S'ARRÊTE PENDANT QUE LA VIDÉO SE RÉDUIT.
   Ces cinq contours apparaissent sur `--grid-p = (--p - 0.82) × 5.55`,
   donc sur les 18 derniers pour cent du couloir : exactement le moment
   décrit comme « ça rame quand j'ai fini de minimize la vidéo ». Ce sont
   cinq boîtes d'environ 900 × 640 px dont le border-radius à huit valeurs
   tourne en continu — une forme pareille se re-rastérise à chaque frame.
   Or c'est aussi la frame où le navigateur recalcule la largeur du texte
   et la géométrie de la vidéo.
   A/B mesuré sur la traversée du couloir (frames au-delà de 33 ms) :
        tel quel                              75–80 %
        contours masqués                      48 %
        contours gardés, ROTATION FIGÉE       39 %
        3 contours au lieu de 5               73 %
        masque du calque retiré               77 %
   Figer la rotation vaut donc presque autant que supprimer le motif, et le
   motif est conservé. Le nombre de contours et le masque, eux, ne coûtent
   rien : ne pas les toucher en croyant optimiser.
   La classe est posée par js/home.js §6quater, et seulement pendant la
   réduction : une fois le couloir refermé les contours retournent — au
   repos ils ne coûtent rien (16,7 ms mesurés, section à l'écran). Sans JS,
   la rotation tourne comme avant. */
html.is-institut-morphing .institut-video__contours span {
  animation-play-state: paused;
}

/* Huit valeurs par contour (les quatre coins horizontalement / les quatre
   verticalement) : c'est ce qui rend chaque courbe unique et asymétrique.
   Valeurs choisies à l'œil — les retoucher librement, rien n'en dépend. */
.institut-video__contours span:nth-child(1) { border-radius: 44% 56% 60% 40% / 55% 46% 54% 45%; }
.institut-video__contours span:nth-child(2) { border-radius: 58% 42% 39% 61% / 43% 57% 43% 57%; }
.institut-video__contours span:nth-child(3) { border-radius: 40% 60% 55% 45% / 60% 40% 60% 40%; }
.institut-video__contours span:nth-child(4) { border-radius: 52% 48% 44% 56% / 47% 53% 47% 53%; }
.institut-video__contours span:nth-child(5) { border-radius: 47% 53% 57% 43% / 52% 49% 51% 48%; }

@keyframes institut-contour-turn { to { transform: rotate(360deg); } }

.institut-video__media {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.35);
}

.institut-video__media video,
.institut-video__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---- Couloir refermé (2026-08-26) ----
   Le rétrécissement étant devenu un aller simple (verrou dans js/home.js
   §6quater), plus rien ne dépend du scroll dans cette section une fois la
   vidéo réduite. Or la section mesure 220svh pour un pin de 100svh : garder
   cette hauteur, c'est laisser 120svh de défilement pendant lesquels
   ABSOLUMENT RIEN ne bouge — ce que Rom a décrit comme « un lock au scroll,
   je dois forcer pour scroll up ». La section se replie donc sur un écran
   et le pin redevient statique : on entre et on sort de la section
   normalement. Le raccourcissement décale mécaniquement le scroll de
   120svh ; c'est compensé dans la même frame en JS, donc invisible. */
.institut-video.is-corridor-closed {
  height: 100svh;
}

.institut-video.is-corridor-closed .institut-video__pin {
  position: relative;
}

/* Repli mobile / mouvement réduit — RÉÉCRIT LE 2026-08-26.
   Ce qu'il y avait ici ne marchait plus : il datait de la version GRILLE
   de .institut-video__row (« grid-template-columns: 1fr ») et n'a pas été
   repris quand la rangée est passée en FLEX, le 2026-08-20, avec des
   largeurs pilotées par --p. Résultat sur téléphone : --p reste à 0 (le
   pin est désactivé en JS sous 761px, voir home.js §6quater), donc
   .institut-video__text mesurait `calc(0 * 30%)` = 0 de large, et
   .institut-video__media-frame `calc(100% - 0)` pour une hauteur calculée
   sur un pin qui n'existe plus. Toute la section « L'institut » — titre,
   paragraphe, vidéo — était INVISIBLE sur mobile, dans 1266 px de vide.
   La règle générale à retenir : dès qu'une géométrie est pilotée par une
   variable posée en JS, son repli doit REDONNER une valeur en dur à
   chacune des propriétés concernées (ici width ET height), pas seulement
   remettre l'opacité à 1. */
@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .institut-video {
    height: auto;
  }

  .institut-video__pin {
    position: relative;
    height: auto;
    overflow: visible;
    padding: var(--space-2xl) 0;
  }

  .institut-video__row {
    flex-direction: column;
    align-items: stretch;
    height: auto;
    gap: var(--space-lg);
    /* padding: 0 calc(var(--p) * ...) vaudrait 0 : on remet les marges
       latérales du site, cette rangée n'étant pas dans un .container. */
    padding: 0 var(--space-md);
  }

  .institut-video__text {
    width: 100%;
    opacity: 1;
    transform: none;
    overflow: visible;
  }

  /* Le <br> du titre (« Un cocon en plein / cœur de ville. ») est calé pour
     une colonne large ; sur 390 px il coupe au mauvais endroit, on laisse
     le texte se replier tout seul. Le ::before rétablit l'espace que le
     <br> fournissait : le HTML écrit « plein<br><em>cœur », sans espace
     autour — masquer le <br> seul recollait les mots en « pleincœur ». */
  .institut-video__title br {
    display: none;
  }

  .institut-video__title em::before {
    content: " ";
  }

  .institut-video__media-frame {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
  }

  /* Les contours décoratifs sont calibrés sur la vidéo RÉDUITE, et leur
     révélation dépend de --p (bloqué à 0 ici) : rien à afficher. */
  .institut-video__contours {
    display: none;
  }
}

/* ==========================================================================
   6. PRESTATIONS — RETOURNEMENT LIÉ AU SCROLL (sans pin)
   Adaptation de la section "THE SLY WAY — four moves" de sly.clubop.site.
   Structure de section tout à fait normale (.services-scroll = .section,
   voir main.css : padding-top/bottom var(--space-2xl), .section-title garde
   son margin-bottom var(--space-lg) par défaut) — PAS de position: sticky,
   PAS de hauteur artificielle. Les anciennes tentatives avec un scroll-pin
   (.svc-pin/.svc-sticky) ont été abandonnées : la distance de scroll à
   épingler était en pixels bruts, donc jamais cohérente d'un écran à
   l'autre malgré plusieurs réglages.
   À la place : js/home.js § "Cartes 'Les soins'" calcule la progression du
   flip directement à partir de la position de #svcTitle par rapport au HAUT
   DE L'ÉCRAN (getBoundingClientRect().top / window.innerHeight — donc en
   fraction de la hauteur d'écran, pas en pixels bruts). Résultat : le
   comportement est visuellement identique quelle que soit la taille
   d'écran, et le flip est terminé avant que le titre n'atteigne le haut de
   l'écran (consigne du 2026-08-10).
   Chaque carte est un vrai flip 3D à 2 faces empilées (comme une carte à
   jouer) : .svc-card (perspective) > .svc-card__flip (preserve-3d, tourné
   par JS) > .svc-card__face--front (visible au repos) +
   .svc-card__face--back (pré-tournée à 180°, donc face caméra une fois le
   flip terminé). backface-visibility: hidden fait disparaître chaque face
   quand elle est de dos.
   Sous 760px et en mouvement réduit : pas de flip, face "back" affichée
   directement en pile verticale. Contenu des cartes à affiner ensuite
   (icônes/description) — pour l'instant numéro + titre.
   ========================================================================== */
.services-scroll {
  background: var(--color-bg);
}

.svc-cards {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  width: 100%;
  /* Volontairement plus généreux que l'écart titre → contenu standard
     (.section-title { margin-bottom: var(--space-lg) } dans main.css) :
     les deux marges se "collapsent" (règle CSS standard entre éléments de
     bloc adjacents), donc c'est la plus grande des deux qui s'applique —
     ici var(--space-2xl), spécifique à cette section. */
  margin-top: var(--space-block-gap);
}

/* Format proche de sly.clubop.site (cartes compactes, presque carrées,
   centrées avec de l'air autour) plutôt que 3 cartes qui se partagent toute
   la largeur de l'écran. Bornes agrandies d'environ 15% par rapport à la
   version précédente, ratio largeur/hauteur conservé (~0,78-0,79). */
.svc-card {
  flex: 0 0 auto;
  width: clamp(210px, 18vw, 300px);
  height: clamp(270px, 23vw, 380px);
  /* Distance de la caméra virtuelle : plus la valeur est petite, plus le
     flip paraît "creusé" en 3D. */
  perspective: 1600px;
}

.svc-card__flip {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  will-change: transform;
  /* Tourné par js/home.js (0deg → 180deg) — voir layoutSvcCards(). */
  transform: rotateY(0deg);
}

.svc-card__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: var(--color-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-soft);
  padding: clamp(1.3rem, 3vw, 2rem);
  color: var(--color-text);
  backface-visibility: hidden;
  transition: box-shadow 0.4s ease;
}

/* Carte "Épilation laser" avec photo (2026-08-21, demande de Rom :
   utiliser assets/images/laser.webp sur cette carte), puis ajustée
   (même jour, retour de Rom) en deux temps forts distincts au lieu d'une
   photo + voile sombre + titre superposé sur les deux faces :
   - face avant (repos, avant survol/flip) : la photo SEULE, sans titre —
     .svc-card__face--photo.
   - face arrière (après le flip) : photo en haut + bandeau blanc en bas
     avec le titre — .svc-card__face--split, structure à deux blocs
     (.svc-card__photo-wrap / .svc-card__title-wrap) plutôt qu'un texte
     superposé sur l'image. */
.svc-card__face--photo {
  overflow: hidden;
  padding: 0;
}

.svc-card__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.svc-card__face--split {
  padding: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  overflow: hidden;
}

.svc-card__photo-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
}

.svc-card__photo-wrap .svc-card__photo {
  position: absolute;
  inset: 0;
}

.svc-card__title-wrap {
  flex: 0 0 auto;
  background: var(--color-surface);
  padding: clamp(1rem, 2.2vw, 1.4rem) clamp(1.3rem, 3vw, 2rem);
}

.svc-card__face--split .svc-card__title { margin: 0; }

.svc-card__face--back {
  /* Pré-tournée : une fois .svc-card__flip à 180°, cette face se retrouve
     face caméra (180 + 180 = 360°), lisible et non-inversée. */
  transform: rotateY(180deg);
}

.svc-card:hover .svc-card__face--front {
  text-decoration: none;
  /* Thème sombre : une ombre brune ne se voit pas. On empile une ombre
     noire profonde (le volume) et une lueur rose diffuse (l'accent de
     la charte, qui signale le survol). */
  box-shadow:
    0 26px 50px -22px rgba(0, 0, 0, 0.8),
    0 0 34px -6px color-mix(in srgb, var(--color-primary) 32%, transparent);
}

.svc-card__title {
  font-family: var(--font-heading);
  font-size: clamp(1.15rem, 0.9rem + 1.4vw, 1.7rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  margin: 0;
  /* Titres centrés sur les 3 cartes (2026-08-21, demande de Rom) — que la
     face soit blanche (Beauté du regard) ou le bandeau .svc-card__title-wrap
     sous une photo (.svc-card__face--split), le texte est toujours centré
     horizontalement dans son bloc. */
  text-align: center;
}

/* Sous 760px et en mouvement réduit : pas de flip, face "back" affichée
   directement en pile verticale (numéro "01 —"/"02 —"/"03 —" retiré le
   2026-08-21, demande de Rom — les faces "back" n'affichent plus que le
   titre, avec la photo pour la carte "Épilation laser" via
   .svc-card__face--split, dont le ratio 4/3 fournit une hauteur intrinsèque
   même ici où .svc-card__face--back passe en position: static). */
@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .svc-cards {
    flex-direction: column;
    padding: 0 var(--space-md);
  }

  .svc-card {
    flex-basis: auto;
    width: 100% !important;
    height: auto !important;
    min-height: 200px;
    perspective: none;
  }

  .svc-card__flip {
    position: static;
    height: auto;
    transform: none !important;
  }

  .svc-card__face--front { display: none; }

  .svc-card__face--back {
    position: static;
    transform: none !important;
  }
}

/* ==========================================================================
   8. CHIFFRES
   ========================================================================== */
/* Thème sombre (2026-08-21) : le bandeau était passé en pastel rose pour
   la version claire ; ce même color-mix sur fond sombre donnait un rose
   sale, ni clair ni sombre. Il redevient un bandeau à part entière —
   légèrement plus clair que la section précédente, teinté d'un souffle de
   rose, et détouré par deux filets pour le lire comme une bande pleine
   largeur plutôt que comme un simple changement de fond. */
.stats {
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--color-primary) 5%, var(--color-bg-alt)) 0%,
      var(--color-bg-alt) 100%);
  color: var(--color-text);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.stats__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

/* Filets verts fortement dilués (2026-08-21) : le vert lumineux du thème
   sombre en trait plein découpait le bandeau en quatre cases beaucoup trop
   marquées. */
.stats__item {
  padding: clamp(1.6rem, 3vw, 2.6rem);
  /* Le vert franc du thème clair ; en sombre il faut le diluer, un vert
     plein y accroche beaucoup trop l'œil (voir les surcharges en fin de
     fichier — pensez aussi aux deux règles de la media query 980px). */
  border-right: 1px solid var(--color-accent);
}

.stats__item:last-child {
  border-right: none;
}

.stats__num {
  font-family: var(--font-heading);
  font-size: clamp(2.2rem, 1.4rem + 2.6vw, 3.8rem);
  line-height: 1;
  color: var(--color-primary);
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: 0.5rem;
}

.stats__label {
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-light);
}

/* ==========================================================================
   9. AVIS
   ========================================================================== */
/* Fond BLANC en thème CLAIR (2026-08-24, validé par Rom) : le rose très
   pâle laissait la bande d'avis se confondre avec le crème du reste du
   site. Le blanc la détache nettement, comme une page de papier. Les
   cartes étant blanches elles aussi, on décale leur fond d'un souffle et
   on leur donne un filet — sans quoi elles disparaîtraient.
   En thème SOMBRE la section reprend le fond de section normal (voir les
   surcharges en fin de fichier) : une bande blanche au milieu du charbon
   avait été essayée le même jour, et écartée. */
.avis { background: var(--avis-bg); }

/* Marquee horizontal auto-défilant (2026-08-13, inspiré du bandeau
   "Observe → Enrich → Act" de spur.us) : .avis__marquee est la fenêtre de
   découpe (overflow hidden, pleine largeur — volontairement HORS de
   .container pour "border-to-border"), .avis__track est la piste que
   js/home.js translate en continu. Le fondu sur les bords (mask-image)
   évite que les cartes soient coupées net à l'entrée/sortie du cadre. */
.avis__marquee {
  width: 100%;
  overflow: hidden;
  margin-top: var(--space-lg);
  /* Pas de `cursor: grab` natif : remplacé par l'icône main custom
     (.cursor-hand, voir components.css + js/components/cursor.js) qui
     reprend la couleur du curseur du site et n'apparaît qu'au survol
     direct d'une carte (.avis__card), pas de tout le bandeau. */
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--space-lg),
    #000 calc(100% - var(--space-lg)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--space-lg),
    #000 calc(100% - var(--space-lg)),
    transparent 100%
  );
}

.avis__track {
  display: flex;
  align-items: stretch;
  gap: var(--space-lg);
  width: max-content;
  padding: 0.4rem var(--space-md);
  /* Position/translation gérées en JS (voir js/home.js §9bis) : pas de
     transition CSS ici, chaque frame pose une nouvelle valeur via rAF. */
  will-change: transform;
}

.avis__card {
  background: var(--avis-card-bg);
  border: 1px solid var(--color-border);
  box-shadow: 0 14px 30px -24px rgba(74, 59, 54, 0.5);
  border-radius: var(--radius-md);
  padding: var(--space-lg) var(--space-md) var(--space-md);
  position: relative;
  margin: 0;
  flex: 0 0 auto;
  width: min(460px, 82vw);
  /* Colonne flex : la mention "Avis Planity" (.avis__meta) est poussée
     tout en bas de la carte via margin-top: auto, quelle que soit la
     longueur du texte au-dessus. */
  display: flex;
  flex-direction: column;
  transition: transform 0.4s var(--ease-out), box-shadow 0.4s ease;
}

.avis__card:hover {
  transform: translateY(-8px);
  border-color: color-mix(in srgb, var(--color-primary) 35%, transparent);
  box-shadow: 0 28px 46px -26px rgba(74, 59, 54, 0.5);
}

.avis__stars {
  color: var(--color-accent);
  letter-spacing: 0.16em;
  font-size: 0.9rem;
  margin-bottom: var(--space-sm);
}

.avis__text {
  font-family: var(--font-heading);
  font-style: italic;
  /* 1.08rem → 1.24rem le 2026-08-25 (demande de Rom : les commentaires
     étaient trop petits). Seul le texte de l'avis grossit — les étoiles
     (.avis__stars) et la mention "Avis Planity" (.avis__meta) gardent leur
     taille, c'est ce décalage qui fait ressortir la citation. Attention si
     on monte encore : le troncage à 6 lignes ci-dessous se déclenche
     d'autant plus tôt, surtout sur mobile où la carte fait 86vw. */
  font-size: 1.24rem;
  line-height: 1.5;
  margin-bottom: var(--space-sm);
  /* Hauteur uniforme dans le marquee : les avis Planity ont des longueurs
     très variables, on tronque proprement (avec "…") au-delà de 6 lignes.
     -webkit-line-clamp couvre Chrome/Safari/Edge ; line-clamp (standard,
     sans préfixe) prend le relais sur les navigateurs plus récents/Firefox
     — display: -webkit-box reste requis dans les deux cas pour que le
     troncage s'applique. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  overflow: hidden;
  text-overflow: ellipsis;
}

.avis__meta {
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-light);
  margin-top: auto;
  padding-top: var(--space-sm);
}

/* ==========================================================================
   9bis. LE SALON
   .salon__media occupe TOUJOURS sa taille finale (toute la section, titre
   compris — top/height calculés avec --salon-title-h, mesuré en JS comme
   avant). Elle ne grandit donc jamais : c'est une fenêtre de découpe
   (clip-path: inset()) qui grandit à sa place, révélant progressivement le
   reste de la même photo, plutôt que de la faire grossir. Avant
   déclenchement, --salon-clip-* (calculées en JS depuis la taille réelle
   de l'écran, voir js/home.js) définissent une fenêtre centrée d'environ
   900px de large en 3/2. Au déclenchement, la classe .is-expanded ramène
   ce clip-path à inset(0) : toute la photo apparaît.
   ========================================================================== */
/* Espacements resserrés spécifiquement pour cette section : moins d'air
   entre l'eyebrow "L'institut" et la photo, et moins d'air entre la photo
   et la section suivante (Réservation). */
.salon .section-title {
  margin-bottom: var(--space-md);
}

.salon {
  padding-bottom: var(--space-md);
}

/* Plafonnée (clamp) plutôt que 80svh pur : sur les très grands écrans
   (moniteurs 32" et plus, où svh peut dépasser 900px), une hauteur non
   plafonnée laissait un vide important sous la petite photo centrée avant
   déclenchement — un padding-bottom à 0 sur .salon n'y changeait rien
   puisque ce vide vient de la hauteur du stage lui-même, pas d'un padding. */
.salon__stage {
  position: relative;
  height: clamp(460px, 80svh, 760px);
  overflow: visible;
}

.salon__media {
  position: absolute;
  top: calc(-1 * var(--salon-title-h, 0px));
  left: 0;
  width: 100%;
  height: calc(100% + var(--salon-title-h, 0px));
  z-index: 1;
  clip-path: inset(
    var(--salon-clip-top, 35%)
    var(--salon-clip-right, 35%)
    var(--salon-clip-bottom, 35%)
    var(--salon-clip-left, 35%)
    round var(--radius-md)
  );
  transition: clip-path 1.1s var(--ease-out);
}

.salon__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.salon__stage.is-expanded .salon__media {
  clip-path: inset(0 round 0);
}

/* Rétraction : au rescroll une fois le texte affiché, l'image ne revient pas
   à sa toute petite fenêtre de départ (le texte ne serait plus dessus) mais
   à une fenêtre intermédiaire, calculée en JS (salonRetract()) pour
   contenir confortablement le bloc de texte — voir js/home.js. Règle placée
   après .is-expanded : même spécificité, gagne par l'ordre des règles. */
.salon__stage.is-retracted .salon__media {
  clip-path: inset(
    var(--salon-retract-top, 10%)
    var(--salon-retract-right, 10%)
    var(--salon-retract-bottom, 10%)
    var(--salon-retract-left, 10%)
    round var(--radius-md)
  );
}

/* Dégradé de lisibilité + titre/sous-titre, réapparaissent par-dessus
   l'image une fois celle-ci pleinement révélée (classe .is-titled ajoutée
   en JS après la fin de l'animation de découpe — voir js/home.js). Même
   principe d'apparition que le hero : opacité + léger décalage vertical.
   ⚠️ Le même clip-path que .salon__media lui est appliqué (mêmes variables,
   mêmes états) : sans ça, ce dégradé — plaqué sur tout le cadre — assombrit
   aussi les petits triangles de fond visibles autour de l'image une fois
   rétractée (coins arrondis), ce qui donnait l'impression que la couleur de
   fond changeait légèrement après la rétractation. */
.salon__stage::after {
  content: '';
  position: absolute;
  top: calc(-1 * var(--salon-title-h, 0px));
  left: 0;
  width: 100%;
  height: calc(100% + var(--salon-title-h, 0px));
  z-index: 2;
  /* Même logique que le scrim du hero : assombrir la photo pour faire
     ressortir le titre posé dessus, sans lui ajouter de teinte. */
  background: linear-gradient(180deg, var(--scrim-salon-top) 0%, var(--scrim-salon-bottom) 100%);
  opacity: 0;
  clip-path: inset(
    var(--salon-clip-top, 35%)
    var(--salon-clip-right, 35%)
    var(--salon-clip-bottom, 35%)
    var(--salon-clip-left, 35%)
    round var(--radius-md)
  );
  transition: opacity 1s var(--ease-out), clip-path 1.1s var(--ease-out);
  pointer-events: none;
}

.salon__stage.is-titled::after {
  opacity: 1;
}

.salon__stage.is-expanded::after {
  clip-path: inset(0 round 0);
}

.salon__stage.is-retracted::after {
  clip-path: inset(
    var(--salon-retract-top, 10%)
    var(--salon-retract-right, 10%)
    var(--salon-retract-bottom, 10%)
    var(--salon-retract-left, 10%)
    round var(--radius-md)
  );
}

.salon__overlay {
  position: absolute;
  top: calc(-1 * var(--salon-title-h, 0px));
  left: 0;
  width: 100%;
  height: calc(100% + var(--salon-title-h, 0px));
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  pointer-events: none;
}

/* Wrapper interne : porte la mise en page du texte (colonne centrée) et
   sert de repère de mesure en JS pour dimensionner la fenêtre de
   rétraction (voir salonRetract() dans js/home.js). */
.salon__overlay-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.7rem;
}

.salon__overlay-eyebrow {
  display: block;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 1s var(--ease-out), transform 1s var(--ease-out);
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--color-secondary);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.35);
}

.salon__overlay-heading {
  margin: 0;
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 1.1s var(--ease-out) 0.12s, transform 1.1s var(--ease-out) 0.12s;
  font-family: var(--font-heading);
  font-size: clamp(2.4rem, 1.3rem + 5vw, 5rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--color-on-dark);
  text-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
}

.salon__overlay-text {
  max-width: 46ch;
  margin: 0;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 1s var(--ease-out) 0.24s, transform 1s var(--ease-out) 0.24s;
  font-size: clamp(1rem, 0.94rem + 0.3vw, 1.2rem);
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}

/* Plus d'air entre le titre et le premier paragraphe */
.salon__overlay-text:first-of-type {
  margin-top: var(--space-md);
}

/* Second paragraphe : révélé un peu après le premier */
.salon__overlay-text:nth-of-type(2) {
  transition-delay: 0.34s;
}

.salon__stage.is-titled .salon__overlay-eyebrow,
.salon__stage.is-titled .salon__overlay-heading,
.salon__stage.is-titled .salon__overlay-text {
  opacity: 1;
  transform: none;
}

@media (max-width: 760px), (prefers-reduced-motion: reduce) {
  .salon__stage::after,
  .salon__overlay {
    display: none;
  }

  .salon__stage {
    height: auto;
  }

  /* Pas de blocage/animation sous 760px ni en mouvement réduit (voir
     js/home.js) : la photo entière reste simplement affichée, en carte
     normale dans le flux — donc pas de clip-path ici. */
  .salon__media {
    position: static;
    width: 100%;
    height: auto;
    max-width: 100%;
    margin: var(--space-lg) 0 var(--space-md);
    clip-path: none;
    border-radius: var(--radius-md);
    overflow: hidden;
  }

  .salon__media img {
    aspect-ratio: 3 / 2;
  }
}

/* ==========================================================================
   10. RÉSERVATION
   ========================================================================== */
.booking-x {
  position: relative;
  /* Repassé en clair/pastel (2026-08-21, demande de Rom, même geste que
     .stats et site-footer) : fond pastel au lieu de l'aplat sombre
     d'origine — toutes les couleurs "blanc translucide" de cette section
     (texte, bordures, champs de formulaire) sont donc repassées sur des
     tokens clairs juste en dessous.
     Fond ensuite aligné sur celui de la section "Les soins" (.services-scroll,
     même jour, demande de Rom) : var(--color-bg) au lieu de var(--color-bg-alt)
     — les deux sections se répondent visuellement avec le même ton crème. */
  background: var(--color-bg);
  color: var(--color-text);
  overflow: hidden;
  text-align: center;
}

.booking-x .section-title__eyebrow {
  justify-content: center;
  /* text-align: center ajouté (2026-08-17) : .section-title__eyebrow est
     display: block (voir main.css), donc justify-content ci-dessus n'a
     aucun effet — c'est bien text-align qui centre "Réservation" et "Nous
     contacter", comme leur titre juste en dessous (.booking-x__col est
     passé à text-align: left le 2026-08-17, donc sans cette règle
     l'eyebrow suivait cet alignement à gauche). */
  text-align: center;
}

/* .container passe au-dessus des calques .booking-x__glow (voir plus bas,
   ce sont des enfants directs de <section>, pas de .container) : sans ce
   z-index, les cartes resteraient dans le flux normal (z-index: auto) et
   pourraient se retrouver sous des calques positionnés selon l'ordre du
   DOM/les règles d'empilement. */
.booking-x .container {
  position: relative;
  z-index: 1;
  /* Container élargi rien que pour cette section (2026-08-13) : cartes
     plus larges, sans toucher au gap/séparateur central déjà réglé. */
  max-width: 1360px;
}

/* Gouttière alignée sur le reste de la page (2026-08-27, constat 07 de
   l'audit laptop). Ce conteneur-ci est plus large que celui du site
   (1360 contre 1180) : sur un grand écran les deux sont loin de leurs
   bords et l'écart ne se voit pas, mais à 1280 px de large le contenu du
   site s'arrêtait à 74 px du bord quand cette section-ci descendait à
   24 px. Le min() ne mord qu'en dessous de 1460 px de large — au-delà,
   1360 px s'applique comme avant. Gardé sous min-width: 761px : sur
   téléphone le conteneur doit rester pleine largeur. */
@media (min-width: 761px) {
  .booking-x .container {
    max-width: min(1360px, calc(100vw - 100px));
  }
}

/* Réservation | Nous contacter — même largeur, empilées sous 900px.
   Chacune est une carte (2026-08-13) ET conserve le séparateur vertical
   central d'origine, repositionné pour tenir compte du nouveau gap
   (--space-xl au lieu de l'ancien clamp géant) : le trait reste exactement
   au milieu de l'espace entre les deux cartes. */
.booking-x__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: calc(var(--space-xl) + var(--space-md));
  align-items: stretch;
}

.booking-x__col {
  min-width: 0;
  position: relative;
  /* Aligné à gauche (2026-08-17) : override le text-align: center hérité
     de .booking-x pour tout le contenu des cartes SAUF les titres, qui
     restent centrés explicitement (voir .booking-x__title juste plus
     bas). */
  text-align: left;
  /* flex column (2026-08-13) : permet de pousser le bouton de chaque
     carte ("Réserver sur Planity" / "Envoyer") tout en bas via
     margin-top: auto, quelle que soit la hauteur du contenu au-dessus —
     et donc quelle que soit la taille d'écran. Les deux colonnes ont déjà
     la même hauteur grâce à align-items: stretch sur .booking-x__grid,
     donc les deux boutons se retrouvent alignés à la même hauteur. */
  display: flex;
  flex-direction: column;
  /* z-index: 1 : passe au-dessus des calques .booking-x__glow (voir plus
     bas) grâce au z-index posé sur .booking-x .container (leur ancêtre
     commun) — le blob reste donc toujours visuellement "sous" la carte.
     Le fond de la carte n'est que légèrement opaque (voir background
     ci-dessous, pensé pour ça) : la partie du blob qui se trouve juste
     sous la carte reste masquée, seules les parties qui dépassent de la
     carte, sur le fond de la section, restent visibles. */
  z-index: 1;
  /* Carte blanche opaque (2026-08-21, était un gris translucide dérivé du
     fond sombre) : cache proprement le blob derrière la carte, comme
     avant, mais avec le même traitement "carte blanche sur fond pastel"
     que le reste du site (.avis__card, .benefit-card...). */
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-md) var(--space-lg) var(--space-lg);
}

/* Calque du fond (2026-08-13) : enfant direct de <section> (PAS de
   .container/.booking-x__grid) pour couvrir toute la hauteur/largeur
   réelle de la section, pas juste la zone occupée par les cartes — sinon
   le blob restait cantonné à la taille des cartes et n'atteignait jamais
   le reste du fond de la section. Un seul calque désormais (2026-08-13,
   un seul blob rose plutôt que deux) : couvre toute la largeur, le blob
   peut donc circuler des deux côtés de la section — voir js/home.js §12,
   qui borne son déplacement à ce calque (donc à toute la section, dans
   les deux axes). Sous les cartes (z-index). */
.booking-x__glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Blob lumineux rose qui suit la souris sur toute la section. Centré sur
   son point d'ancrage (translate -50/-50 posé en JS avec la position),
   flouté pour un effet "glow" doux plutôt qu'un cercle net.
   --blob-opacity (2026-08-13) : invisible tant que la souris n'est pas
   encore entrée dans la section (JS le passe à 0.55 au premier
   mousemove détecté à l'intérieur — voir js/home.js §12), avec un petit
   fondu plutôt qu'une apparition sèche. */
.booking-x__blob {
  position: absolute;
  top: 0;
  left: 0;
  width: 520px;
  height: 520px;
  max-width: 80%;
  max-height: 80%;
  border-radius: 50%;
  background: radial-gradient(circle, var(--color-primary) 0%, transparent 72%);
  opacity: var(--blob-opacity, 0);
  transition: opacity 0.35s ease;
  filter: blur(46px);
  will-change: transform, opacity;
}

.booking-x__col + .booking-x__col::before {
  content: '';
  position: absolute;
  top: 0.4em;
  bottom: 0.4em;
  left: calc((var(--space-xl) + var(--space-md)) / -2);
  width: 1px;
  background: var(--color-border);
}

@media (max-width: 900px) {
  .booking-x__grid {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }

  .booking-x__col + .booking-x__col::before {
    display: none;
  }

  /* Sous 900px les cartes sont empilées (pleine largeur, l'une sous
     l'autre) : la notion de "moitié gauche/droite" ne veut plus rien dire,
     et l'effet est de toute façon inactif hors pointeur fin (souris) —
     voir js/home.js §12. On masque simplement les calques pour éviter
     tout résidu visuel. */
  .booking-x__glow {
    display: none;
  }

  /* Carte Google Maps (2026-08-17) : sous 900px, les cartes ne sont plus
     étirées à une hauteur commune (grid empilée en une seule colonne,
     chaque carte garde sa hauteur de contenu naturelle) — flex: 1 n'aurait
     alors plus d'espace à absorber et .booking-x__map, qui ne contient
     qu'un iframe en position: absolute (donc hors du flux), s'effondrerait
     à 0px de haut. On repasse ici sur un ratio fixe, comme avant. */
  .booking-x__map {
    flex: 0 0 auto;
    aspect-ratio: 2 / 1;
  }

  /* Doublon invisible de la mention de consentement (2026-09-03,
     .booking-x__consent-spacer) : n'a de sens que pour compenser
     .booking-x__map en flex: 1 et garder deux boutons alignés sur une
     hauteur de carte commune. Sous 900px, les cartes sont empilées et
     gardent chacune leur hauteur naturelle (align-items: stretch n'a
     plus d'effet visible ici) : ce doublon ne ferait plus que rajouter un
     blanc inutile sous "Réserver sur Planity". */
  .booking-x__consent-spacer {
    display: none;
  }
}

.booking-x__title {
  font-family: var(--font-heading);
  font-size: clamp(1.9rem, 1.1rem + 2.6vw, 3rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
  /* Reste centré (2026-08-17) : seul le titre échappe à l'alignement à
     gauche posé sur .booking-x__col ci-dessus. */
  text-align: center;
  /* margin-bottom réduit à --space-xs (au lieu de --space-md) : compense
     l'ajout de .booking-x__subtitle juste en dessous (2026-08-17), pour
     que la carte "Nous contacter" ne s'agrandisse pas et reste alignée
     sur la hauteur de la carte "Réserver" (voir align-items: stretch sur
     .booking-x__grid). */
  margin-bottom: var(--space-xs);
  /* Vert, comme les autres titres du site (2026-08-21). */
  color: var(--color-heading);
}

.booking-x__title em { font-style: italic; color: var(--color-primary); }

/* Petite phrase ajoutée sous le titre (2026-08-17) : même taille que
   .booking-x__note ("Un bilan gratuit...", carte de gauche) — pas de
   font-size/line-height propres, hérite du corps de texte par défaut de
   la carte. Alignée à gauche (2026-08-17, revenu sur l'alignement à
   droite précédent), avec un espace au-dessus (sous le titre) conservé. */
.booking-x__subtitle {
  color: var(--color-text-light);
  text-align: left;
  margin: var(--space-sm) 0 var(--space-sm);
}

.booking-x__lede {
  color: var(--color-text-light);
  margin: 0;
}

/* Texte à gauche / bouton à droite, alignés à la même hauteur (2026-08-13) —
   override le text-align: center hérité de .booking-x pour ce bloc précis. */
.booking-x__lede-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  text-align: left;
  /* margin-top passé à --space-md (2026-08-17, était --space-sm) : espace
     entre le titre et ce texte agrandi d'autant que l'espace entre ce
     texte et .booking-x__note est réduit juste en dessous — pour que
     .booking-x__note ne bouge pas de position (voir son propre
     commentaire), seul l'écart ENTRE les deux phrases se resserre. */
  margin-top: var(--space-md);
  /* margin-bottom retiré (2026-08-17) : l'espace avant .booking-x__note
     vient désormais uniquement du margin-top de ce dernier (une seule
     source), pour être identique — pas juste égal par coïncidence de
     calcul — à l'espace entre l'iframe Google Maps et le bouton "Réserver
     sur Planity" (.booking-x__frame, padding-top: var(--space-md)). */
  margin-bottom: 0;
}

.booking-x__lede-row .booking-x__lede {
  flex: 1 1 50%;
}

@media (max-width: 640px) {
  .booking-x__lede-row {
    flex-direction: column;
    align-items: stretch;
    text-align: center;
  }
}

.booking-x__note {
  color: var(--color-text-light);
  /* margin-top réduit à --space-sm (2026-08-17, était --space-md) : écart
     resserré avec la phrase au-dessus. Compensé par le margin-top de
     .booking-x__lede-row, augmenté d'autant juste au-dessus — la SOMME
     des deux reste la même, donc .booking-x__note reste exactement à la
     même position sur la page, seul l'espace entre les deux phrases
     diminue. */
  margin: var(--space-sm) 0 0;
  text-align: left;
}

.booking-x__map {
  position: relative;
  width: 100%;
  /* flex: 1 (2026-08-17, remplace aspect-ratio: 2/1 en desktop) : la carte
     grandit désormais vers le bas pour absorber tout l'espace restant de
     la colonne — c'est elle qui pousse .booking-x__frame juste en dessous
     (qui n'a donc plus besoin de margin-top: auto, voir plus bas), au lieu
     de laisser un grand vide invisible avant le bouton. Le padding-top
     fixe de .booking-x__frame donne alors un écart identique à celui
     entre le champ "Message" et "Envoyer" côté formulaire de contact.
     Repasse à une hauteur fixe (aspect-ratio) sous 900px, où les cartes ne
     sont plus étirées à la même hauteur (grid_empilée, voir media query
     plus bas) — sans quoi la carte, sans hauteur de contenu propre
     (l'iframe est en position: absolute, donc hors du flux), s'effondrerait
     à 0px. */
  flex: 1 1 0;
  min-height: 0;
  margin-top: var(--space-md);
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--color-border);
}

/* Google Maps ne sert que sa version claire dans un iframe cross-origin :
   impossible de la styler, elle formait un rectangle blanc aveuglant au
   milieu de la section sombre. On la retourne donc côté navigateur —
   invert + hue-rotate(180deg) est la recette classique : l'inversion
   assombrit la carte, la rotation de teinte remet les couleurs (routes,
   eau, végétation) à l'endroit au lieu de les laisser en négatif.
   Au survol, le filtre est retiré : la carte redevient exactement celle de
   Google, fidèle et lisible, au moment précis où l'on cherche à la lire.
   (2026-08-21) */
.booking-x__map iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  /* Pas de filtre en clair : la carte Google est déjà claire, elle tombe
     juste. C'est en thème sombre qu'il faut la retourner (voir les
     surcharges en fin de fichier). */
  transition: filter 0.45s ease;
}

@media (prefers-reduced-motion: reduce) {
  .booking-x__map iframe { transition: none; }
}

.booking-x__frame {
  max-width: 100%;
  /* margin-top: auto retiré (2026-08-17) : .booking-x__map (flex: 1
     ci-dessus) absorbe désormais tout l'espace restant à sa place, donc ce
     bloc se retrouve déjà collé au bas de la colonne sans avoir besoin de
     se pousser lui-même. */
  /* padding-top remonté à --space-md (2026-08-17, était --space-sm juste
     avant, réaugmenté sur demande) : même écart que celui entre le champ
     "Message" et le bouton "Envoyer" côté formulaire de contact (voir
     .contact-form__field:last-of-type ci-dessous, même valeur). Le
     .booking-x__map en flex: 1 juste au-dessus rétrécit d'autant pour
     compenser — la carte peut aussi légèrement grandir si besoin, plus de
     contrainte de hauteur fixe ici. */
  padding-top: var(--space-md);
  text-align: left;
}

/* Repli affiché tant que la clé API Planity n'est pas renseignée
   (js/planity-config.js) — plus de cadre/carte (2026-08-13) : juste le
   bouton, aussi large que le titre au-dessus (voir .booking-x__frame--fallback
   .btn, width: 100% de la colonne, comme .booking-x__title). */
.booking-x__frame--fallback {
  display: flex;
}

.booking-x__frame--fallback .btn {
  width: 100%;
  text-align: center;
}

.booking-x__frame-text {
  color: var(--color-text-light);
  max-width: 32ch;
  margin: 0;
}

/* Une fois le widget Planity chargé (clé API renseignée), on laisse le
   widget gérer son propre habillage. */
.booking-x__frame--active {
  min-height: 0;
  padding: 0;
}

.booking-x__info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  margin: 0 auto var(--space-lg);
  font-size: 0.84rem;
  color: var(--color-text-light);
}

/* Déclinaison claire du rose (2026-08-21) : à cette taille (0.84rem), le
   rose vif de la charte manquait de contraste sur le fond sombre. */
.booking-x__info b { color: var(--color-primary-dark); }

.contact-form__body {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Aligné sur le style de .btn--primary (2026-08-21, demande de Rom :
   "le bouton Envoyer n'est pas rose clair + écriture noire sans bordure")
   — le bouton "Envoyer" utilisait la classe .btn--outline en HTML
   (contour rose/fond transparent), overridée ici en rose pâle plein,
   texte noir, sans bordure, cohérent avec le reste des CTA du site. */
/* Les deux états ont été ÉCHANGÉS le 2026-08-25 en même temps que
   .btn--primary (voir main.css) : cette règle n'existe que pour que
   "Envoyer" reste identique aux "Prendre rendez-vous", elle suit donc
   toutes leurs évolutions. Choix confirmé par Rom le jour même. */
.booking-x__contact-btn {
  margin-top: auto;
  background-color: var(--color-primary);
  color: var(--color-text-on-primary);
  border: none;
}

.booking-x__contact-btn:hover {
  /* Encre sombre sur l'aplat rose pâle (2026-08-21, thème sombre) :
     var(--color-text) y est devenu crème et le libellé disparaissait
     presque — d'où ce token dédié. */
  background: var(--color-secondary);
  color: var(--color-text-on-secondary);
  /* Même filet qu'au survol des .btn--primary (voir main.css pour le
     pourquoi de l'inset plutôt qu'une border). */
  box-shadow: inset 0 0 0 1px var(--color-text-on-secondary), var(--shadow-soft);
}

/* Mention de consentement sous "Envoyer" (2026-09-03, demande de Rom :
   "attention, il faut que les deux boutons reserver sur planity et
   envoyer soient toujours alignés"). PAS de margin-top: auto ici : c'est
   .booking-x__contact-btn juste au-dessus qui garde ce rôle. Cette mention
   s'ajoute donc APRÈS le bouton, dans le même flex column -- ce qui, à
   elle seule, pousserait "Envoyer" vers le haut (puisque le bouton n'est
   alors plus le dernier élément collé au bas de la carte) et casserait
   l'alignement avec "Réserver sur Planity", qui lui reste flush en bas de
   sa propre carte. Voir .booking-x__consent-spacer ci-dessous pour le
   correctif qui rétablit un alignement pixel pour pixel des deux boutons. */
.contact-form__consent {
  margin: 0.6rem 0 0;
  font-style: italic;
  font-size: 0.78rem;
  line-height: 1.4;
  text-align: center;
  color: var(--color-text-light);
}

/* Doublon invisible de .contact-form__consent, posé côté "Réservation"
   juste après .booking-x__frame (2026-09-03) -- CORRECTIF pour un
   alignement pixel pour pixel des deux boutons, pas juste des bas de
   carte. Explication : .booking-x__map est en flex: 1, seul élément
   flexible de sa colonne -- ajouter un élément après .booking-x__frame
   (comme ce doublon) réduit d'autant la hauteur que s'absorbe
   .booking-x__map, ce qui remonte .booking-x__frame ("Réserver sur
   Planity") EXACTEMENT de la même hauteur que celle dont
   .contact-form__consent fait remonter "Envoyer" côté formulaire (même
   texte, même largeur de colonne -> même hauteur une fois habillé). Les
   deux boutons remontent donc ensemble, du même nombre de pixels, et
   restent alignés -- alors que les bas des deux cartes (mention de
   consentement / doublon invisible) le sont toujours aussi, comme avant.
   `visibility: hidden` (pas `display: none`) : on veut conserver l'espace
   occupé, seul le rendu visuel disparaît -- `aria-hidden="true"` en HTML
   en plus, pour ne rien exposer aux lecteurs d'écran. Si la mention
   .contact-form__consent change de texte ou de taille un jour, reporter
   le même changement ici pour que les deux hauteurs restent identiques
   (ou repasser par un script qui les génère depuis la même source). Masqué
   sous 900px (voir la media query de .booking-x__grid) : les cartes n'y
   sont plus étirées à la même hauteur, l'alignement ne se pose plus. */
.booking-x__consent-spacer {
  margin: 0.6rem 0 0;
  font-style: italic;
  font-size: 0.78rem;
  line-height: 1.4;
  text-align: center;
  visibility: hidden;
}

/* ---- Formulaire de contact (colonne "Nous contacter") ---- */
.contact-form {
  margin-top: var(--space-lg);
  text-align: left;
  /* flex column + flex:1 : le formulaire remplit toute la hauteur restante
     de .booking-x__col (lui-même flex, voir plus haut), pour que le
     bouton "Envoyer" (margin-top: auto sur .contact-form__body ci-dessous)
     se retrouve poussé tout en bas, aligné avec le bouton "Réserver sur
     Planity" de la carte voisine. */
  display: flex;
  flex-direction: column;
  flex: 1;
}

.contact-form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
}

@media (max-width: 520px) {
  .contact-form__row {
    grid-template-columns: 1fr;
  }
}

.contact-form__field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: var(--space-sm);
  font-size: 0.82rem;
  color: var(--color-text-light);
}

/* Écart avant "Envoyer" agrandi (2026-08-17) : seul le dernier champ
   (Message) avant le bouton, pas les autres — même valeur que le
   padding-top de .booking-x__frame côté "Réserver sur Planity", pour un
   écart identique des deux côtés. */
.contact-form__body .contact-form__field:last-of-type {
  margin-bottom: var(--space-md);
}

.contact-form__field em {
  font-style: normal;
  color: color-mix(in srgb, var(--color-text-light) 75%, transparent);
}

.contact-form__field input,
.contact-form__field textarea {
  font-family: var(--font-body);
  font-size: var(--font-size-base);
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm, 6px);
  padding: 0.65em 0.8em;
  resize: vertical;
  transition: border-color var(--transition-base), background-color var(--transition-base);
}

.contact-form__field input::placeholder,
.contact-form__field textarea::placeholder {
  color: color-mix(in srgb, var(--color-text-light) 65%, transparent);
}

.contact-form__field input:focus,
.contact-form__field textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  background: var(--color-surface);
}

.contact-form .btn {
  /* Plus de margin-top ici (2026-08-17) : cette règle (2 classes, donc plus
     spécifique que .booking-x__contact-btn) écrasait le margin-top: auto
     posé sur .booking-x__contact-btn plus haut — le bouton "Envoyer" ne se
     retrouvait donc jamais réellement poussé en bas de la carte, contrairement
     à "Réserver sur Planity" (.booking-x__frame, margin-top: auto lui aussi,
     mais sans règle plus spécifique pour l'écraser). Sans cette ligne, les
     deux boutons se retrouvent enfin alignés en bas de leur carte, avec le
     même padding-bottom hérité de .booking-x__col pour les deux. */
  width: 100%;
  text-align: center;
}

.contact-form__success {
  display: none;
  color: var(--color-success);
  font-size: 0.9rem;
}

/* Erreur d'envoi (2026-09-09) : contact.php a répondu une erreur (ou la
   requête a échoué) — affichée sous le bouton "Envoyer", le formulaire
   reste visible pour permettre de réessayer (contrairement au succès qui
   bascule .is-sent et masque .contact-form__body). Toggle via l'attribut
   [hidden], pas de classe dédiée : pas de règle CSS à maintenir en double. */
.contact-form__error {
  margin: 0.5rem 0 0;
  color: var(--color-error);
  font-size: 0.82rem;
  line-height: 1.4;
  text-align: center;
}

.contact-form.is-sent .contact-form__body {
  display: none;
}

.contact-form.is-sent .contact-form__success {
  display: block;
}

/* ==========================================================================
   10bis. SÉPARATEUR SECTION/FOOTER
   Déplacé dans css/components.css + js/components/site-footer.js le
   2026-08-19 : ce séparateur vit maintenant DANS le web component
   <site-footer>, donc sur toutes les pages, pas seulement l'accueil. Voir
   [[project_alycare_tech_stack]].
   ========================================================================== */

/* ==========================================================================
   11. BOUTONS "MAGNÉTIQUES" + VARIANTES SOMBRES
   ========================================================================== */
.btn {
  position: relative;
  overflow: hidden;
  will-change: transform;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 0.8rem;
}

/* Ces deux variantes sont pensées pour être posées SUR UNE PHOTO (hero) :
   elles utilisaient var(--color-bg) comme "clair", ce qui n'a plus de sens
   en thème sombre — elles passent sur --color-on-dark (2026-08-21). Elles
   ne sont référencées nulle part dans le HTML actuel, mais restent
   disponibles et correctes si un CTA sur photo réapparaît. */
.btn--light {
  background: var(--color-on-dark);
  color: var(--color-text-on-secondary);
}

.btn--ghost {
  background: transparent;
  color: var(--color-on-dark);
  border: 1px solid rgba(255, 255, 255, 0.4);
}

.btn--ghost:hover {
  background: var(--color-on-dark);
  color: var(--color-text-on-secondary);
}

/* ==========================================================================
   12. EN-TÊTE TRANSPARENT SUR LE HERO
   ========================================================================== */
/* Sur l'accueil l'en-tête flotte au-dessus du hero plein écran */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

.site-header .site-header__bar {
  background: transparent;
  border-bottom-color: transparent;
  transition: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
}

/* Tant qu'on est en haut de page, l'en-tête flotte sur la PHOTO du hero.
   Encre BLANCHE depuis le 2026-09-03 (demande de Rom), via --color-on-dark
   — le token « toujours clair, quel que soit le thème » (voir
   variables.css), habituellement réservé aux photos sombres : ici détourné
   volontairement, la photo du hero restant claire (--color-on-light est
   l'encre "cohérente" avec elle, mais Rom veut du blanc pur). Une fois la
   barre décollée (body sans .at-top), on revient au rendu normal du site
   (--color-text) plus bas dans ce fichier — inchangé.
   Le logo n'est plus dans cette règle depuis le 2026-08-26 : ce n'est plus
   du texte mais une image, et c'est le choix du FICHIER (encre sombre /
   encre claire) qui joue ce rôle — voir components.css, où la règle du
   thème sombre est justement neutralisée par body.at-top. */
body.at-top .site-header .site-header__link {
  color: var(--color-on-dark);
}

body.at-top .site-header .site-header__toggle span { background: var(--color-on-light); }

/* Une fois décollé du haut, l'en-tête reprend le fond du site. En sombre,
   une ombre portée ne suffit plus à le détacher du contenu qui défile
   dessous : on lui ajoute un fond légèrement opacifié + flou d'arrière-plan
   (l'effet "verre" attendu sur ce genre de barre en thème sombre), avec
   repli sur l'aplat plein pour les navigateurs sans backdrop-filter. */
body:not(.at-top) .site-header .site-header__bar {
  background: var(--color-bg);
  border-bottom-color: var(--color-border);
  box-shadow: 0 6px 24px -18px rgba(74, 59, 54, 0.5);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  body:not(.at-top) .site-header .site-header__bar {
    background: color-mix(in srgb, var(--color-bg) 82%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.2);
    backdrop-filter: blur(14px) saturate(1.2);
  }
}

/* ---- MOBILE : la barre garde son fond DÈS LE HAUT DE PAGE (2026-08-26) ----
   Demande de Rom : « qu'il y ait toujours le background sur la top bar, même
   sur le hero ». Sur grand écran l'en-tête transparent sur la photo est un
   parti pris qui tient — la barre y est fine et le regard a de la place.
   Sur téléphone, la barre couvre toute la largeur, l'interrupteur et le
   burger sont posés directement sur la peau et le linge de la photo du hero,
   et le contraste change selon l'endroit où la photo est recadrée.
   On neutralise donc `at-top` sous 760 px : la barre est opaque tout de
   suite, exactement comme après le premier scroll.

   Les six déclarations ci-dessous sont les six effets de `.at-top`, repris
   un à un — si on en ajoute un ailleurs (une nouvelle couleur d'icône, par
   exemple), il faudra le neutraliser ici aussi :
     1. le fond et le filet de la barre (+ la variante floutée juste après) ;
     2. la couleur des liens de navigation ;
     3. les trois traits du burger ;
     4. l'encre de l'interrupteur de thème et de son rail (base dans
        components.css) ;
     5. le choix du FICHIER de logo en thème sombre — sans le point 5, on
        garderait le logo à encre sombre (choisi pour la photo claire du
        hero) sur une barre redevenue charbon : invisible ;
     6. le choix du FICHIER de logo en thème clair (2026-09-03) — depuis que
        le cas « accueil, tout en haut » affiche l'encre CLAIRE dans les
        deux thèmes (components.css), sans ce point 6 le logo resterait
        blanc sur la barre redevenue crème (thème clair, mobile) : invisible
        lui aussi, dans l'autre sens.
   Chaque sélecteur reprend `body.at-top` pour égaler la spécificité de la
   règle qu'il annule (une media query n'ajoute aucune spécificité) et gagner
   par l'ordre de source. Ne pas les « simplifier » en retirant `body`. */
@media (max-width: 760px) {
  body.at-top .site-header .site-header__bar {
    background: var(--color-bg);
    border-bottom-color: var(--color-border);
    box-shadow: 0 6px 24px -18px rgba(74, 59, 54, 0.5);
  }

  body.at-top .site-header .site-header__link {
    color: var(--color-text);
  }

  body.at-top .site-header .site-header__toggle span {
    background-color: var(--color-text);
  }

  body.at-top .site-header .theme-switch {
    color: var(--color-text);
  }

  body.at-top .site-header .theme-switch__track {
    border-color: var(--color-border);
    background: color-mix(in srgb, var(--color-text) 6%, transparent);
  }

  :root[data-theme="dark"] body.at-top .site-header__logo-img--ink-dark {
    display: none;
  }

  :root[data-theme="dark"] body.at-top .site-header__logo-img--ink-light {
    display: block;
  }

  /* Point 6 : cas symétrique en thème clair — voir le commentaire plus
     haut. Spécificité identique aux deux règles de components.css qui
     affichent l'encre claire en body.at-top (2026-09-03) : ce fichier
     étant chargé après (voir index.html), il gagne à l'ordre de source. */
  body.at-top .site-header__logo-img--ink-dark {
    display: block;
  }

  body.at-top .site-header__logo-img--ink-light {
    display: none;
  }
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  @media (max-width: 760px) {
    body.at-top .site-header .site-header__bar {
      background: color-mix(in srgb, var(--color-bg) 82%, transparent);
      -webkit-backdrop-filter: blur(14px) saturate(1.2);
      backdrop-filter: blur(14px) saturate(1.2);
    }
  }
}

@media (max-width: 760px) {
  :root[data-theme="dark"] body.at-top .site-header .site-header__bar {
    box-shadow: 0 10px 30px -20px rgba(0, 0, 0, 0.9);
  }
}

/* ==========================================================================
   13. RESPONSIVE
   ========================================================================== */
@media (max-width: 980px) {
  .manifesto__grid { grid-template-columns: 1fr; }

  .stats__grid { grid-template-columns: 1fr 1fr; }
  .stats__item:nth-child(-n+2) { border-bottom: 1px solid var(--color-accent); }

  .avis__card { width: min(380px, 86vw); }

  .manifesto__media { max-width: 340px; margin: 0 auto; }
  .manifesto__glyph { font-size: 7rem; top: -2.6rem; }
}

@media (max-width: 640px) {
  .hero-x__badge { display: none; }
  .hero-x__cue { display: none; }
  .stats__grid { grid-template-columns: 1fr; }
  .stats__item { border-bottom: 1px solid var(--color-accent); }
  .hero-x__actions .btn { flex: 1 1 100%; text-align: center; }
}

/* --------------------------------------------------------------------------
   PASSE « VERSION MOBILE » (2026-08-26)
   Tout ce qui suit ne concerne QUE le téléphone (≤ 760 px, le seuil déjà
   utilisé partout dans le projet). Placé en fin de §13, donc APRÈS les
   media queries 980/640 ci-dessus : à largeur égale de sélecteur, c'est la
   dernière règle du fichier qui gagne, et c'est voulu — on corrige ici des
   replis écrits pour des écrans plus larges.
   Les grands écarts et les tailles de titre, eux, ne se règlent PAS ici
   mais dans css/variables.css (§ « ÉCHELLE MOBILE »), d'un seul geste pour
   tout le site.
   -------------------------------------------------------------------------- */
@media (max-width: 760px) {
  /* ---- Manifeste ----
     Redevenu une section normale sur mobile (voir §4bis, le repli du store) :
     il n'y a donc plus de hauteur d'écran à respecter, seulement une mise en
     page à une colonne. Le translateY(10%) de .manifesto__grid, lui, est
     pensé pour un écran large où le bloc flotte dans le vide — sur téléphone
     il ne fait que décoller le sur-titre « Qui suis-je » du haut de section. */
  .manifesto__grid {
    transform: none;
    gap: var(--space-md);
  }

  .manifesto__quote {
    font-size: clamp(1.3rem, 5.2vw, 1.7rem);
    margin-bottom: var(--space-sm);
  }

  /* Cadrage d'origine conservé (demande du 2026-08-11), simplement centré
     et borné : sur une colonne pleine largeur, la photo étant en portrait
     (670 × 1194), 100 % de large ferait plus de 600 px de haut. */
  .manifesto__media {
    width: min(62%, 230px);
    margin: 0 auto;
  }

  /* ---- « Les soins » ----
     .svc-cards est déjà dans un .container (24 px de marge) : les 24 px
     ajoutés par le repli de la §6 mettaient les cartes à 294 px de large
     au lieu de 342, sans raison. */
  .svc-cards {
    margin-top: var(--space-lg);
    padding: 0;
    gap: var(--space-md);
  }

  /* ---- Les chiffres ----
     En une seule colonne (repli 640 px ci-dessus), les quatre chiffres
     prenaient 680 px de haut, soit huit écrans de scroll pour quatre
     nombres. Deux colonnes suffisent, et les filets se lisent alors comme
     une croix — à condition de les redistribuer : la règle de base pose un
     border-right sur CHAQUE item (donc un trait vertical dans le vide à
     droite du dernier), et le repli 640 px un border-bottom sous tous. */
  .stats__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .stats__item {
    padding: var(--space-md) var(--space-sm);
    border-right: 0;
    border-bottom: 0;
  }

  .stats__item:nth-child(odd) {
    border-right: 1px solid var(--color-accent);
  }

  .stats__item:nth-child(-n + 2) {
    border-bottom: 1px solid var(--color-accent);
  }

  .stats__label {
    font-size: 0.7rem;
    letter-spacing: 0.1em;
  }

  /* ---- Avis ----
     Les cartes faisaient 82vw + 24 px de padding de piste : la carte
     « centrale » arrivait donc systématiquement coupée des deux côtés, et
     le fondu de 40 px mangeait le début de chaque phrase. On resserre
     l'ensemble pour qu'une carte entière tienne à l'écran, la suivante
     annoncée par un bord.
     LARGEUR PORTÉE À 88vw le 2026-08-26 (après mesure, choix de Rom). Le
     chiffre qui décide, sur un écran de 390 px et 3 lignes de texte : à
     78vw, 8 avis sur 13 sont coupés au « … » ; à 88vw, 4 sur 13. Entre 62
     et 78vw on ne gagne qu'un bout de carte suivante en plus et on perd de
     la lisibilité ; au-delà de 88vw on ne gagne plus rien et on perd le
     débord qui signale que ça défile (~12 %, soit 47 px ici).
     En vw et NON en px fixes : 88vw avec ~10 % de débord est la largeur
     canonique d'un carrousel mobile à scroll-snap, et le snap raisonne en
     proportion d'écran — le jour où ce marquee passe en défilement à pas
     (souhait de Rom), la largeur est déjà la bonne.
     Le plafond `min(460px, …)` sert aux écrans entre ~520 et 760 px, où
     88vw donnerait des cartes démesurées.
     HAUTEUR DIVISÉE PAR DEUX le 2026-08-26 (demande de Rom) : 311 px →
     ~165 px. Les cartes sont étirées à la même hauteur par le
     `align-items: stretch` de la piste, donc c'est le PLUS LONG avis
     (200 caractères, 6 lignes) qui imposait sa taille à tout le bandeau.
     On ne pose donc PAS de `height` en dur — on redescend le troncage de
     6 à 3 lignes et on resserre les écarts internes : la carte la plus
     haute mesure alors trois lignes, et toutes les autres suivent. Pas de
     risque de contenu coupé au pixel près comme avec une hauteur fixe, et
     ça reste juste si Rom rallonge le texte des avis.
     Le « … » de fin est déjà géré par line-clamp (voir .avis__text) — les
     avis trop longs sont coupés proprement, avec les points de suspension. */
  .avis__card {
    width: min(460px, 88vw);
    padding: var(--space-sm);
  }

  .avis__stars {
    font-size: 0.8rem;
    margin-bottom: var(--space-xs);
  }

  .avis__text {
    font-size: 1.05rem;
    line-height: 1.45;
    margin-bottom: var(--space-xs);
    -webkit-line-clamp: 3;
    line-clamp: 3;
  }

  .avis__meta {
    font-size: 0.7rem;
    padding-top: var(--space-xs);
  }

  .avis__track {
    gap: var(--space-md);
    padding-left: var(--space-sm);
    padding-right: var(--space-sm);
  }

  .avis__marquee {
    -webkit-mask-image: linear-gradient(
      to right,
      transparent 0,
      #000 var(--space-sm),
      #000 calc(100% - var(--space-sm)),
      transparent 100%
    );
    mask-image: linear-gradient(
      to right,
      transparent 0,
      #000 var(--space-sm),
      #000 calc(100% - var(--space-sm)),
      transparent 100%
    );
  }
}

/* ==========================================================================
   14. ACCESSIBILITÉ — MOUVEMENT RÉDUIT
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  [data-reveal] { opacity: 1; transform: none; }
  .reveal-img::after { transform: scaleX(0); }
  .reveal-img img { transform: none; }
  .hero-x__title .line > span { transform: none; }
  .reveal-title .line > span { transform: none; }
  .hero-x__card,
  .hero-x__lede,
  .hero-x__actions { opacity: 1; transform: none; }

  /* Séparateur section/footer : déplacé dans css/components.css (le
     composant <site-footer> vit sur toutes les pages, pas seulement
     l'accueil qui charge home.css) — voir la media query équivalente
     là-bas. */
}

/* ==========================================================================
   SURCHARGES DU THÈME SOMBRE
   ————————————————————————————————————————————————————————————————————
   Tout ce qui précède est écrit pour le THÈME CLAIR, en var(--…) : le
   passage en sombre est déjà assuré par les tokens de variables.css.
   Ne restent ici que les réglages qu'AUCUN token ne peut porter — modes de
   fusion, filtres, opacités, ombres — parce qu'ils ne dépendent pas d'une
   couleur mais de la façon dont la lumière se comporte sur un fond sombre.

   RÈGLE : si une surcharge que vous êtes sur le point d'ajouter ici est une
   simple COULEUR, c'est qu'il manque un token — l'ajouter dans les deux
   blocs de variables.css plutôt que d'allonger cette liste.
   ========================================================================== */

/* Le grain. En multiply il ne ferait qu'assombrir un fond déjà sombre :
   invisible. soft-light module dans les deux sens (il éclaircit aussi les
   grains clairs), la matière redevient perceptible — et on peut alors
   monter un peu l'opacité. */
:root[data-theme="dark"] .grain {
  /* PERF-2026-08-27 — soft-light retiré comme le multiply du thème clair
     (voir §2). Ici l'opacité DOIT être recalée : sur un fond charbon,
     soft-light module autour du niveau de base alors qu'un rendu normal
     ne fait que l'éclaircir. Relevé sur une zone plate à niveau 27 :
        soft-light 0,075 → niveau 28,087 · écart-type 0,398
        normal     0,075 → niveau 32,722  (voile gris, beaucoup trop)
        normal     0,020 → niveau 27,994 · écart-type 0,571
     0,020 rend donc le même éclaircissement à 0,09 niveau près, avec un
     grain un rien plus présent. Écart moyen sur tout l'écran : 0,25 niveau
     (contre 0,90 si on retirait le grain). */
  opacity: 0.02;
}

/* Le badge Instagram flotte au-dessus de sections toutes sombres : ce halo
   le fait exister comme une petite source lumineuse plutôt que comme une
   gommette posée. Sur fond clair il ne produirait qu'un cerne rose. */
:root[data-theme="dark"] .hero-x__badge-core {
  box-shadow: 0 0 34px -4px color-mix(in srgb, var(--color-primary) 70%, transparent);
}
:root[data-theme="dark"] .hero-x__badge:hover .hero-x__badge-core {
  box-shadow: 0 0 46px -2px color-mix(in srgb, var(--color-primary-dark) 75%, transparent);
}

/* Les deux halos du manifeste, très fortement dilués : à 58 %/32 % sur un
   fond sombre, le rose pâle vire à l'aplat laiteux et écrase la section.
   Sur un fond neutre, la moindre teinte se remarque bien davantage que sur
   un fond déjà coloré. Ils ne font plus que réchauffer deux coins. */
:root[data-theme="dark"] .manifesto {
  background:
    radial-gradient(62% 70% at 14% 10%, color-mix(in srgb, var(--color-secondary) 6%, transparent) 0%, transparent 74%),
    radial-gradient(58% 68% at 90% 96%, color-mix(in srgb, var(--color-primary) 11%, transparent) 0%, transparent 76%),
    var(--color-surface);
}

/* Le guillemet géant : un rose à 10 % disparaît complètement sur charbon. */
:root[data-theme="dark"] .manifesto__glyph { opacity: 0.2; }

/* L'ombre "papier" décalée sous la photo d'Alyssa : le brun très dilué du
   thème clair est invisible en sombre. Même décalage, mais la trace passe
   au rose de la charte — la photo semble posée sur son propre reflet. */
:root[data-theme="dark"] .manifesto__media {
  box-shadow: 18px 22px 0 color-mix(in srgb, var(--color-primary) 26%, transparent);
}

/* Le spot vert qui suit le curseur sur la photo : soft-light ne fait
   presque rien sur une photo entourée de sombre. screen n'éclaircit que —
   le curseur redevient une vraie source de lumière, sans jamais assombrir
   l'image. */
:root[data-theme="dark"] .manifesto__spot { mix-blend-mode: screen; }

/* Ombres portées : plus profondes, il faut plus de noir pour creuser un
   fond déjà sombre. */
:root[data-theme="dark"] .institut-video__media { box-shadow: 0 30px 70px rgba(0, 0, 0, 0.6); }
:root[data-theme="dark"] .salon__overlay-heading { text-shadow: 0 8px 30px rgba(0, 0, 0, 0.55); }
:root[data-theme="dark"] body:not(.at-top) .site-header .site-header__bar {
  box-shadow: 0 10px 30px -20px rgba(0, 0, 0, 0.9);
}

/* Les filets du bandeau chiffres : sur fond neutre, un vert plein accroche
   beaucoup trop l'œil pour un simple séparateur. */
:root[data-theme="dark"] .stats__item {
  border-right-color: color-mix(in srgb, var(--color-accent) 26%, transparent);
  border-bottom-color: color-mix(in srgb, var(--color-accent) 26%, transparent);
}

/* Le survol des cartes d'avis : en clair, une ombre chaude ; en sombre,
   du noir franc doublé d'une lueur rose, seule chose qui décolle vraiment
   une carte d'un fond sombre. */
:root[data-theme="dark"] .avis__card:hover {
  border-color: var(--color-border);
  box-shadow:
    0 26px 50px -22px rgba(0, 0, 0, 0.8),
    0 0 30px -8px color-mix(in srgb, var(--color-primary) 28%, transparent);
}

/* La carte Google est une iframe cross-origin : impossible de la styler,
   elle forme un rectangle blanc aveuglant au milieu d'une page sombre. On
   la retourne donc côté navigateur — invert + hue-rotate(180deg) est la
   recette classique : l'inversion assombrit la carte, la rotation de teinte
   remet routes, eau et végétation à l'endroit au lieu de les laisser en
   négatif. Le filtre est RETIRÉ au survol : au moment où on lit vraiment la
   carte, on retrouve la vraie. */
:root[data-theme="dark"] .booking-x__map iframe {
  filter: invert(0.92) hue-rotate(180deg) saturate(0.72) brightness(0.94) contrast(0.9);
}

/* À garder ICI et pas plus haut : la surcharge de thème ci-dessus est plus
   spécifique qu'un simple `.booking-x__map:hover iframe`, qui ne reprendrait
   donc jamais la main. */
:root[data-theme="dark"] .booking-x__map:hover iframe { filter: none; }

/* ==========================================================================
   17. ÉCHELLE LAPTOP (2026-08-27) — ce qui doit TENIR DANS UN ÉCRAN
   ————————————————————————————————————————————————————————————————————
   Les jetons de variables.css règlent le rythme général. Ce bloc-ci traite
   les trois compositions qui, elles, sont censées tenir dans la hauteur
   d'un écran et débordaient dès que celle-ci passait sous ~744 px : la
   carte du hero, le calque du manifeste, et le repère « DÉCOUVRIR ».

   LA REQUÊTE EST EN HAUTEUR, PAS EN LARGEUR — et c'est délibéré : à
   1440×900 comme à 1600×900, la fenêtre utile fait 738 px et les deux
   cassent de la même façon, alors que 1512×982 (plus étroit que le second)
   va parfaitement bien. `min-width: 761px` n'est PAS un second seuil de
   projet, seulement le garde-fou qui exclut le téléphone (un iPhone SE
   fait 667 px de haut : sans lui, tout ce bloc s'appliquerait au mobile,
   qui a déjà ses propres replis).

   Bornes mesurées : s'applique à 1366×768, 1280×800, 1440×900, 1600×900
   (viewports 606 à 738 px) ; ne s'applique pas à 1470×956, 1512×982,
   1680×1050, 1920×1080, 2560×1440 ni au-delà.
   ========================================================================== */
@media (min-width: 761px) and (max-height: 760px) {

  /* ---- Carte du hero (constat 02) -------------------------------------
     .hero-x centre son contenu et ne réservait NULLE PART la hauteur de
     l'en-tête, qui est transparent et posé par-dessus la photo. Tant que
     la carte reste basse ça passe ; dès qu'elle occupe 83 % de la hauteur
     d'écran, elle remonte jusque derrière le logo manuscrit (mesuré :
     50 px de recouvrement à 1366×768). Deux gestes, il faut les deux :
     réserver la barre en haut, et resserrer la carte pour qu'elle tienne
     dans ce qui reste. 6rem = 96 px couvre les trois hauteurs d'en-tête
     observées (79, 82, 85 px) avec une dizaine de pixels d'air. */
  .hero-x {
    padding-top: 6rem;
    padding-bottom: clamp(1rem, 3svh, 2.5rem);
  }

  /* padding-block seul : la gouttière horizontale de la carte (clamp en vw)
     reste celle d'origine, c'est bien la hauteur qu'il faut récupérer. */
  .hero-x__card { padding-block: clamp(1.25rem, 3.4svh, 2rem); }
  .hero-x__eyebrow { margin-bottom: 1rem; }
  .hero-x__title { margin-bottom: 0.9rem; }
  .hero-x__lede { margin-bottom: 1.6rem; }

  /* Le repère « DÉCOUVRIR » est en absolute, calé sur le bas du hero et
     centré horizontalement : sur un écran court la carte descend jusque
     dessus et il s'imprimait EN PLEIN MILIEU d'elle. Masqué, comme il
     l'est déjà sous 640 px (§12) — l'invitation à défiler est de toute
     façon portée par la fermeture du store, qui démarre au premier cran
     de molette. */
  .hero-x__cue { display: none; }

  /* ---- Manifeste (constat 01) -----------------------------------------
     #manifeste est un calque `position: absolute; inset: 0` dans un pin de
     100svh : sa hauteur est celle de l'ÉCRAN, jamais celle de son contenu.
     Ce contenu faisait 695 px, dont 625 px de photo — et ces 625 px ne
     venaient ni de la hauteur d'écran ni d'une valeur en dur, mais de la
     LARGEUR de la colonne (donc du conteneur figé à 1180 px) multipliée
     par l'aspect-ratio 670/1194. La photo ne rétrécissait donc jamais, et
     le bas partait sous le bord de l'écran, dans un `overflow-y: auto`
     que la molette n'atteint pas (Lenis la capte pour la page).
     C'est le même bug qu'en mobile le 2026-08-26 ; là-bas il avait été
     réglé en sortant le manifeste du calque, ce qui coûtait l'animation du
     store. Ici on garde l'animation et on contraint la photo. */
  .hero-store__pin #manifeste {
    /* Le calque est en inset: 0, donc il commence SOUS l'en-tête : sans ce
       padding-top, le sur-titre « Qui suis-je » et le guillemet décoratif
       passaient derrière la barre pendant que 45 px de vide restaient en
       bas. Même valeur que le hero, pour la même raison. */
    padding-top: 6rem;
    padding-bottom: clamp(0.75rem, 3svh, 2rem);
  }

  /* Les 10 % de translation descendaient la grille de 62 px de plus, soit
     l'essentiel du débord à eux seuls. */
  .manifesto__grid {
    transform: none;
    gap: clamp(1.5rem, 4vw, 3rem);
  }

  /* height + width: auto : c'est l'aspect-ratio qui déduit la largeur, la
     photo n'est donc jamais déformée. Le min() garde la valeur d'origine
     tant qu'elle tient, et justify-self recentre la photo dans sa colonne
     une fois qu'elle a rétréci (sans quoi elle restait collée à gauche,
     avec tout le vide à droite). */
  .manifesto__media {
    /* 9.5rem = les 6rem d'en-tête réservés plus l'air haut et bas. Le min()
       garde la hauteur d'origine tant qu'elle tient (elle tient encore à
       760 px de viewport, la borne haute de ce bloc), donc le raccord avec
       la version grand écran se fait sans marche. */
    height: min(625px, calc(100svh - 9.5rem));
    width: auto;
    justify-self: center;
  }

  /* Le guillemet décoratif débordait par le haut d'un calque désormais
     bien plus serré. */
  .manifesto__glyph {
    font-size: 8rem;
    top: -1rem;
    left: -0.8rem;
  }

  /* ---- Réservation / Nous contacter (constat 03 bis) -------------------
     MARQUE : RESERVATION-LAPTOP-2026-08-27
     Les deux cartes faisaient 729 px de haut sur un écran de 638 : la
     section entière occupait 1,34 écran. Aucune valeur n'y était en cause
     toute seule, c'était l'addition — deux sur-titres manuscrits, deux
     titres, un sous-titre, quatre champs et leurs marges, le tout calibré
     pour un 27 pouces.

     ON NE TOUCHE QU'À LA COLONNE DE DROITE, et c'est voulu : la carte de
     gauche est en align-items: stretch et .booking-x__map est en flex: 1.
     C'est donc le formulaire qui impose la hauteur, et la carte s'ajuste
     toute seule — resserrer les deux ferait un double décompte et
     écraserait la carte Google deux fois.
     Le min-height du plan garde quand même un plancher lisible si le
     formulaire venait à raccourcir encore. */
  .booking-x__col {
    padding-top: var(--space-sm);
    padding-bottom: clamp(1rem, 3.5svh, 1.75rem);
  }

  .booking-x__col .section-title__eyebrow,
  .booking-x__title { margin-bottom: 0.25rem; }

  .booking-x__subtitle { margin: 0.6rem 0 0.5rem; }

  .booking-x__map { min-height: 180px; }

  .contact-form { margin-top: clamp(0.75rem, 2.6svh, 1.5rem); }
  .contact-form__field { margin-bottom: 0.6rem; }
  /* Deux classes : égale la spécificité de la règle d'origine
     (.contact-form__body .contact-form__field:last-of-type), sans quoi
     l'écart avant « Envoyer » ne bougerait pas. */
  .contact-form__body .contact-form__field:last-of-type { margin-bottom: 1rem; }

  .contact-form__field input,
  .contact-form__field textarea { padding: 0.5em 0.8em; }

  /* rows="4" dans le HTML donne ~115 px ; la hauteur CSS reprend la main
     sans toucher au balisage (donc sans rien changer sur les autres
     tailles d'écran, où cette règle ne s'applique pas). */
  .contact-form__field textarea { height: clamp(64px, 11svh, 100px); }
}

/* ==========================================================================
   17bis. BADGE INSTAGRAM — effacement en bas de page (2026-08-27, constat 03)
   ————————————————————————————————————————————————————————————————————
   Le badge est volontairement fixe et visible pendant tout le défilement
   (décision du 2026-08-13) — ça n'est pas remis en cause. Mais sur un
   viewport court il retombe pile sur le bouton « Envoyer » du formulaire
   puis sur les horaires du pied de page : il les masque, et avec son
   z-index 300 il intercepte le clic.
   La classe est posée par js/home.js § 15, sur une VRAIE intersection de
   rectangles (les deux axes) : sur un 27 pouces le badge est à 2 400 px
   du bord gauche quand ces blocs s'arrêtent à 1 936 px, l'intersection est
   donc vide et le badge ne s'efface jamais — comportement inchangé.
   ========================================================================== */
.hero-x__badge {
  transition: opacity 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}

html.is-badge-tucked .hero-x__badge {
  opacity: 0;
  transform: scale(0.86);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .hero-x__badge { transition: none; }
}

/* ==========================================================================
   « VENIR À L'INSTITUT » (2026-09-06) — bloc d'accès/localisation, entre
   « L'institut » et la réservation. 2e version : la première n'était qu'un
   texte et une carte, sans relief ni coupure nette avec la section
   suivante. Rom : « la section manque de whaou effect [...] et la
   séparation entre cette section et la section suivante n'est pas assez
   marquée ». Deux réponses, décidées avec lui :
     1. une BANDE pleine largeur (même procédé que .stats) et une
        respiration verticale généreuse, pour en faire un chapitre à part ;
     2. un ITINÉRAIRE qui se trace au scroll entre l'arrêt de métro et
        l'institut.

   Composant DÉDIÉ et non une reprise de .benefit-card : benefits.css et
   benefits.js ne sont pas chargés sur l'accueil, une .benefit-card y
   resterait à opacity:0 (sa valeur par défaut, que seul benefits.js
   relève). L'entrée des blocs passe par [data-reveal] de js/home.js.

   Tout est en tokens, donc le thème sombre suit sans surcharge.
   ========================================================================== */

.lieu {
  /* La bande : un aplat pastel un peu plus soutenu que le --color-bg-alt de
     la réservation qui suit, pour que la rupture se voie. Les deux filets
     reprennent le procédé de .stats. */
  background-color: color-mix(in srgb, var(--color-secondary) 42%, var(--color-bg));
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  padding-top: var(--space-2xl);
  padding-bottom: var(--space-2xl);
}

/* ---- L'itinéraire ---------------------------------------------------- */

.lieu-route {
  --p: 0;                                   /* progression 0→1, écrite par home.js */
  --p-mid: clamp(0, calc((var(--p) - 0.42) * 6), 1);
  --p-end: clamp(0, calc((var(--p) - 0.80) * 7), 1);
  --route-h: clamp(80px, 9vw, 120px);       /* hauteur du SVG, reprise plus bas */
  --dot-size: 3.25rem;
  display: flex;
  /* flex-start et non center : c'est la PASTILLE qui doit s'aligner sur la
     courbe, pas le bloc « pastille + nom + métro » dans son entier. Le
     décalage est repris sur .lieu-route__track juste en dessous. */
  align-items: flex-start;
  gap: var(--space-md);
  margin: var(--space-xl) 0;
}

.lieu-route__stop {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  flex: 0 0 auto;
  width: 11rem;
}

.lieu-route__dot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  margin-bottom: 0.7rem;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background-color: var(--color-surface);
  color: var(--color-primary-dark);
}

.lieu-route__dot svg { width: 1.5rem; height: 1.5rem; }

/* Le repère d'arrivée s'allume quand le tracé l'atteint. Pas de classe à
   basculer en JS : c'est --p-end qui pilote le remplissage, via color-mix.
   Une écriture DOM de moins par frame. */
.lieu-route__dot--brand {
  position: relative;
  border-color: color-mix(in srgb, var(--color-primary) calc(var(--p-end) * 100%), var(--color-border));
  background-color: color-mix(in srgb, var(--color-primary) calc(var(--p-end) * 100%), var(--color-surface));
  color: color-mix(in srgb, var(--color-text-on-primary) calc(var(--p-end) * 100%), var(--color-primary-dark));
}

.lieu-route__dot--brand::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1px solid var(--color-primary);
  opacity: calc(var(--p-end) * 0.5);
  transform: scale(calc(0.9 + var(--p-end) * 0.1));
}

.lieu-route__name {
  font-family: var(--font-heading);
  font-size: 1.05rem;
  letter-spacing: 0.01em;
  color: var(--color-heading);
}

/* Second arrêt possible (Canal du Midi, ajouté le 2026-09-15) : ligne
   secondaire sous le nom principal, dans la police du corps de texte et un
   ton plus clair, pour rester lisible sans concurrencer le nom. Neutre à
   dessein — aucun des deux arrêts n'est présenté comme le plus proche. */
.lieu-route__name-or {
  display: block;
  font-family: var(--font-body, inherit);
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--color-text-light);
  margin-top: 0.15rem;
}

.lieu-route__meta {
  font-size: 0.8rem;
  color: var(--color-text-light);
}

.lieu-route__track {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  /* Les extrémités de la courbe sont à mi-hauteur du SVG (y=60 sur 120) :
     on remonte le tracé de façon que cette mi-hauteur tombe pile au centre
     des pastilles, sinon la ligne flotte sous elles sans les rejoindre. */
  margin-top: calc(var(--dot-size) / 2 - var(--route-h) / 2);
  /* Et on la fait passer légèrement SOUS les pastilles, pour un raccord net
     plutôt qu'un trait qui s'arrête à quelques pixels du cercle. */
  margin-left: calc(var(--space-md) * -1 - 0.6rem);
  margin-right: calc(var(--space-md) * -1 - 0.6rem);
  z-index: 0;
}

.lieu-route__stop { position: relative; z-index: 1; }

.lieu-route__svg {
  display: block;
  width: 100%;
  height: var(--route-h);
  overflow: visible;
  fill: none;
  stroke-linecap: round;
}

/* Le tracé « pas encore parcouru » : il montre le chemin sans le révéler. */
.lieu-route__ghost {
  stroke: var(--color-border);
  stroke-width: 2;
  stroke-dasharray: 1 6;
}

/* pathLength="1" normalise la longueur du chemin à 1 : dasharray et
   dashoffset se pilotent alors en CSS pur depuis --p, sans que le JS ait
   besoin de mesurer getTotalLength(). */
.lieu-route__line {
  stroke: var(--color-primary);
  stroke-width: 2.5;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p));
}

.lieu-route__mid {
  position: absolute;
  left: 50%;
  /* SOUS l'arc, pas au-dessus : le sommet de la courbe frôle le haut du SVG,
     un libellé placé par-dessus déborderait sur le titre de section (et
     passait sous le bandeau du haut au scroll). Il se loge donc dans le
     creux formé par l'arc, et monte de 8 px en apparaissant. */
  top: 36%;
  transform: translate(-50%, calc(-50% + (1 - var(--p-mid)) * 8px));
  padding: 0.3rem 0.85rem;
  border-radius: 999px;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  font-size: 0.78rem;
  color: var(--color-text-light);
  white-space: nowrap;
  opacity: var(--p-mid);
}

/* ---- La carte pratique ----------------------------------------------- */

.lieu-card {
  position: relative;
  overflow: hidden;
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  padding: var(--space-lg);
}

/* Lueur qui suit le curseur — même principe que .benefit-card__spot des
   pages prestation (--mx/--my posées en JS, voir home.js § "Itinéraire"). */
.lieu-card__spot {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
  background: radial-gradient(260px circle at var(--mx, 50%) var(--my, 50%),
              color-mix(in srgb, var(--color-primary) 20%, transparent), transparent 70%);
}

.lieu-card.is-active .lieu-card__spot { opacity: 1; }

.lieu-card__list {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-md);
}

.lieu-card__label {
  display: block;
  margin-bottom: 0.25em;
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-light);
}

.lieu-card__value {
  display: block;
  color: var(--color-text);
  line-height: 1.5;
}

.lieu-card__cue {
  position: relative;
  display: inline-block;
  margin-top: var(--space-md);
  color: var(--color-primary-dark);
  font-weight: 600;
  text-decoration: none;
}

.lieu-card__cue svg {
  /* Dans le flux du texte plutôt qu'en flex : si le libellé passait à la
     ligne, une flèche en flex se retrouverait décrochée à droite. */
  vertical-align: -0.15em;
  margin-left: 0.4em;
  transition: transform 0.35s ease;
}

.lieu-card__cue:hover svg { transform: translateX(4px); }

/* ---- Texte ----------------------------------------------------------- */

.lieu__grid {
  /* Les deux colonnes démarrent en haut : la carte est plus courte que le
     texte, elle ne doit ni s'étirer ni se centrer verticalement. */
  align-items: start;
  gap: var(--space-lg);
}

.lieu__text p + p { margin-top: var(--space-sm); }

.lieu__text a {
  color: var(--color-primary-dark);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: color 0.3s ease;
}

.lieu__text a:hover { color: var(--color-primary); }

/* ---- Repli ----------------------------------------------------------- */

@media (max-width: 760px) {
  /* L'itinéraire bascule à la VERTICALE : une courbe horizontale de 300 px
     de large est illisible. Le SVG est masqué et le tracé redevient deux
     filets verticaux — le fantôme en pointillés, la progression par-dessus,
     pilotée par la même variable --p. */
  .lieu-route {
    flex-direction: column;
    /* Neutralise le flex-start du montage horizontal : en colonne, ce sont
       les pastilles ET le filet vertical qui doivent partager le même axe.
       Sans ça, chaque repère se cale à gauche sur sa propre largeur et les
       trois éléments se retrouvent décalés les uns des autres. */
    align-items: center;
    gap: var(--space-sm);
    margin: var(--space-lg) 0;
  }

  .lieu-route__stop { width: auto; }

  .lieu-route__track {
    /* Les décalages du montage horizontal n'ont plus lieu d'être. */
    margin: 0;
  }

  .lieu-route__svg { display: none; }

  .lieu-route__track {
    width: 2px;
    height: 5.5rem;
    flex: 0 0 auto;
    background-image: repeating-linear-gradient(to bottom,
      var(--color-border) 0 3px, transparent 3px 9px);
  }

  .lieu-route__track::after {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    width: 2px;
    height: calc(var(--p) * 100%);
    background-color: var(--color-primary);
  }

  .lieu-route__mid {
    top: 50%;
    left: calc(50% + 0.75rem);
    transform: translateY(-50%);
  }
}

@media (max-width: 900px) {
  .lieu__grid { gap: var(--space-md); }
}

@media (prefers-reduced-motion: reduce) {
  /* Le tracé s'affiche complet d'emblée : c'est js/home.js qui pose --p à 1
     (il ne s'abonne alors pas au scroll), la règle ci-dessous n'est qu'un
     filet si le script ne tourne pas du tout. */
  .lieu-route { --p: 1; }
  .lieu-card__cue svg,
  .lieu-card__spot { transition: none; }
}

/* ==========================================================================
   RENVOI VERS LES AVIS GOOGLE (2026-09-06) — sous le bandeau d'avis Planity.
   Voir le commentaire devant le bloc dans index.html : on n'affiche pas les
   avis Google (CGU), on envoie les lire à la source. Le clic alimente la
   fiche Google, ce qui sert le référencement local.
   Traité comme une pastille discrète et non comme un bouton : le CTA de la
   page reste « Prendre rendez-vous », celui-ci ne doit pas lui faire
   concurrence.
   ========================================================================== */

.avis__google {
  margin-top: var(--space-lg);
  text-align: center;
}

.avis__google-link {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.55rem 1.15rem;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background-color: var(--color-surface);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color 0.3s ease, transform 0.3s ease;
}

.avis__google-link:hover {
  border-color: var(--color-primary);
  transform: translateY(-2px);
}

.avis__google-score {
  font-family: var(--font-heading);
  font-size: 1.25rem;
  line-height: 1;
  color: var(--color-heading);
}

.avis__google-stars {
  color: var(--color-accent);
  letter-spacing: 0.06em;
}

.avis__google-text {
  font-size: 0.9rem;
  color: var(--color-text-light);
}

.avis__google-link svg {
  color: var(--color-primary-dark);
  transition: transform 0.35s ease;
}

.avis__google-link:hover svg { transform: translateX(3px); }

@media (max-width: 480px) {
  /* La pastille tient sur une ligne jusqu'à 480px ; en dessous on laisse la
     mention passer à la ligne plutôt que de rétrécir la note. */
  .avis__google-link { flex-wrap: wrap; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .avis__google-link,
  .avis__google-link svg { transition: none; }
}

/* ==========================================================================
   BLOC DE REMPLACEMENT DE LA CARTE (2026-09-06) — tant que le visiteur n'a
   pas accepté les contenus tiers. Voir js/consent.js et le commentaire
   devant le bloc dans index.html.
   ⚠️ inset: 0 et position: absolute : il remplit EXACTEMENT la place de
   l'iframe, dont la hauteur porte l'alignement au pixel des boutons de la
   section réservation. Ne pas lui donner de hauteur propre.
   ========================================================================== */

.map-consent {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: var(--space-md);
  text-align: center;
  background-color: var(--color-bg-alt);
}

.map-consent__icon {
  color: var(--color-primary-dark);
}

.map-consent__icon svg { width: 1.9rem; height: 1.9rem; }

.map-consent__addr {
  margin: 0;
  color: var(--color-text);
  line-height: 1.45;
}

.map-consent__note {
  margin: 0;
  font-size: 0.76rem;
  color: var(--color-text-light);
}

.map-consent__btn {
  margin-top: 0.35rem;
  padding: 0.45rem 1.1rem;
  font-size: 0.8rem;
}
