/*!
 * ═══════════════════════════════════════════════════════════════════════════
 *  ADMINISPHÈRE — Site vitrine
 *  Assistante administrative & RH externalisée pour TPE/PME
 * ───────────────────────────────────────────────────────────────────────────
 *  Fichier        : assets/css/adminisphere.v1.9.0.3.css
 *  Rôle           : Mise en page, composants, sections et animations.
 *                   Toutes les valeurs proviennent de `tokens.v1.9.0.3.css`.
 *  Dépendance     : tokens.v1.9.0.3.css (à charger AVANT ce fichier)
 *  Développé par  : Adminisphère
 *  Version        : 1.9.0.3 (tablette portrait, 48 rem à 1000 px : les
 *                   photos des services, de la FAQ et du contact passent
 *                   à leur tour en bandeau pleine largeur — tokens
 *                   1.9.0.3)
 *                   1.9.0.2 : (tablette portrait, 48 rem à 1000 px : « À
 *                   propos » et « La différence » en une colonne, photo en
 *                   bandeau 16:9 au-dessus du texte ; héros moins haut.
 *                   Toutes largeurs : libellé court « Réserver » dans la
 *                   pastille d'en-tête là où la barre le loge, bouton
 *                   pause des bandes défilantes en verre rond (48 rem et
 *                   plus), mention de réassurance du héros en blanc plein,
 *                   trait du sur-titre calé sur sa première ligne ;
 *                   téléphone : apparitions moins amples, parcours d'« À
 *                   propos » replié derrière un résumé — tokens 1.9.0.2)
 *                   1.9.0.1 : (page « Conseils » sur téléphone : sommaire
 *                   du héros retiré, redondant avec les cartes ; cartes
 *                   allégées de leur « Lire l'article », rembourrage et
 *                   écart resserrés. Tablette et bureau inchangés)
 *                   1.9.0 : (version mobile de l'accueil : services et
 *                   témoignages en carrousel à points, photos recadrées ou
 *                   en médaillon, appels en double masqués, pied de page
 *                   sur deux colonnes ; bureau : photo des services
 *                   alignée sur la grille)
 *                   1.8.0.2 : (la carte de service ne reste plus levée au
 *                   retour arrière : seul le focus clavier la soulève,
 *                   `:has(:focus-visible)` au lieu de `:focus-within` ;
 *                   les ronds flottants prennent leur propre voile, plus
 *                   transparent, --glass-bg-frosted-sm)
 *                   1.8.0.1 : (verre toujours clair : carte et ronds prennent
 *                   le voile givré, AA sur tout fond, et l'anneau de focus des
 *                   ronds est doublé d'un intervalle blanc — tokens 1.8.0.1 ;
 *                   le lien de retour des articles ne partage plus sa ligne
 *                   avec le sur-titre au-delà de 768 px)
 *                   1.8.0 : (matériau verre commun à la carte, au retour en
 *                   haut et aux raccourcis flottants .quick — tokens 1.8.0)
 *                   1.7.0 : (carte d'invitation aux Conseils, en verre
 *                   « liquid glass » quasi incolore, fumé sur fond
 *                   sombre au prorata de --invite-tone — tokens 1.7.0)
 *                   1.6.0.1 : (vitrine « Conseils » à cinq cartes : trois
 *                   puis deux centrées en bureau, la dernière carte seule
 *                   centrée sur tablette ; pastille seule du sommaire en
 *                   pleine ligne sur téléphone)
 *                   1.6.0 : page « Conseils » : articles repliables en
 *                   cartes illustrées, corps d'article, signature d'Ana —
 *                   tokens 1.6.0.
 *                   1.5.0.1 : les pastilles du sommaire de la page des
 *                   questions fréquentes prennent l'aspect du bouton
 *                   fantôme du héros de l'accueil.
 *                   1.5.0 : barre « Réserver un appel » collante sur
 *                   téléphone ; héros de la FAQ recalé sur sa colonne.
 * ═══════════════════════════════════════════════════════════════════════════
 *
 *  ORGANISATION EN COUCHES (@layer)
 *  L'ordre déclaré ci-dessous fixe la priorité, quelle que soit la position
 *  des règles dans le fichier. Un utilitaire l'emportera toujours sur un
 *  composant, sans jamais recourir à `!important`.
 *
 *  reset      → remise à zéro du navigateur
 *  base       → éléments HTML nus (titres, liens, images)
 *  layout     → conteneurs, grilles, rythme des sections
 *  components → briques réutilisables (bouton, carte, champ…)
 *  sections   → habillage propre à chaque section de la page
 *  animations → apparitions au défilement et effets
 *  utilities  → classes d'appoint ponctuelles
 *
 * ───────────────────────────────────────────────────────────────────────────
 *  CONVENTION — CE QUI PEUT RESTER ÉCRIT EN CLAIR ICI
 *  Aucune valeur de STYLE n'est admise dans ce fichier : ni couleur, ni
 *  taille de composant, ni durée, ni courbe, ni police, ni ombre. Tout
 *  provient de `tokens.v1.9.0.3.css`.
 *  Ne subsistent en littéral que les valeurs STRUCTURELLES, celles qui ne
 *  relèvent d'aucune décision graphique et qu'un changement de charte ne
 *  toucherait jamais :
 *    • les neutres          → `0`, `none`, `auto`, `transparent`
 *    • les proportions      → `100%`, `50%`, `80%`, `1fr`, `2fr`, `0.85fr`
 *    • les angles de forme  → `45deg`, `-135deg` (une croix, un chevron)
 *    • les repères de sortie d'écran → `-9999px`, `±120%`, le `1px` de la
 *      classe `.visually-hidden`
 *    • les seuils de `@media` et de `@container`, que la syntaxe CSS
 *      interdit d'exprimer avec une variable
 *  Toute autre valeur qui apparaîtrait ici est un token manquant.
 *
 *  Contrôle : `grep -nE '#[0-9a-fA-F]{3,8}|rgba?\(|[0-9]+ms' ce fichier`
 *  ne doit remonter que des zéros.
 * ───────────────────────────────────────────────────────────────────────────
 */

@layer reset, base, layout, components, sections, animations, utilities;

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  POLICE — Inter variable, auto-hébergée                                 ║
   ║  Auto-hébergée volontairement : aucune requête vers un tiers, aucun      ║
   ║  cookie, et un rendu plus rapide qu'un CDN de polices.                  ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800; /* police variable : une seule requête, 5 graisses */
  font-display: swap;   /* le texte s'affiche immédiatement en police système */
  src: url('../fonts/inter-latin.woff2') format('woff2');
  /* Sous-ensemble latin : couvre l'intégralité du français (47 Ko) */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  1. RESET                                                                ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  /* Marges par défaut du navigateur : on repart de zéro et on redonne
     un rythme explicite dans la couche `base`. */
  body,
  h1, h2, h3, h4, h5, h6,
  p, figure, blockquote, dl, dd {
    margin: 0;
  }

  ul[class],
  ol[class] {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  img,
  picture,
  svg,
  video {
    display: block;
    max-width: 100%;
  }

  /* Complément indispensable de `max-width: 100%` : sans lui, une image dont
     le CSS ne fixe que la largeur garde la hauteur de son attribut HTML et
     se retrouve écrasée. Les attributs width/height du balisage restent
     nécessaires — ils réservent la place et évitent les sauts de mise en
     page — mais c'est bien le ratio qui doit gouverner le rendu.
     Les images qui doivent remplir un cadre (`.hero__image`, `.figure img`,
     `.cta-band__image`) redéfinissent `height` dans une couche supérieure :
     l'ordre des couches leur donne toujours raison. */
  img,
  video {
    height: auto;
  }

  input,
  button,
  textarea,
  select {
    font: inherit;
    color: inherit;
  }

  /* Le navigateur n'ajoute pas de marge sous une image dans un lien */
  a {
    color: inherit;
    text-decoration: none;
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  2. BASE — éléments HTML nus                                             ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer base {
  html {
    scroll-behavior: smooth;
    /* Les ancres s'arrêtent sous l'en-tête collant */
    scroll-padding-top: calc(var(--header-height) + var(--space-md));
    -webkit-text-size-adjust: 100%;
    /* Débordement PASSAGER : une apparition `data-reveal` part décalée
       sur le côté, et la page gagnait de 8 à 46 px de largeur tant
       qu'elle n'était pas arrivée. `clip` et non `hidden` : il ne crée pas
       de conteneur de défilement, l'en-tête reste donc `sticky`. Ce
       rognage ne masque aucun contenu : les débordements PERMANENTS sont
       corrigés à la source (voir .split) et mesurés nuls à 320, 390 et
       768 px. */
    overflow-x: clip;
  }

  body {
    background-color: var(--surface-page);
    color: var(--text-body);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Empêche un débordement horizontal dû aux éléments décoratifs.
       `clip` et non `hidden` : `hidden` fait de l'élément un conteneur de
       défilement, ce qui détourne les timelines de défilement vers lui. */
    overflow-x: clip;
  }

  h1, h2, h3, h4 {
    color: var(--text-strong);
    font-weight: var(--weight-bold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
    /* Équilibre les retours à la ligne des titres courts */
    text-wrap: balance;
  }

  h1 { font-size: var(--text-4xl); }
  h2 { font-size: var(--text-3xl); }
  h3 { font-size: var(--text-xl); }
  h4 { font-size: var(--text-lg); }

  p {
    /* Évite les lignes orphelines en fin de paragraphe */
    text-wrap: pretty;
  }

  strong, b { font-weight: var(--weight-semibold); }

  /* Exposants et indices (« 1er ») : sans hauteur de ligne propre, ils ne
     repoussent plus l'interligne du paragraphe qui les porte. Le décalage
     vertical natif (`super` / `sub`) est conservé. */
  sup, sub { line-height: 0; }

  /* Rythme vertical : tout élément qui suit un frère reçoit une marge haute.
     Une seule règle plutôt qu'une marge répétée sur chaque composant. */
  :is(h1, h2, h3, h4) + * { margin-top: var(--space-sm); }
  p + p,
  p + ul,
  ul + p { margin-top: var(--space-sm); }

  /* --- Accessibilité clavier ------------------------------------------- */
  :focus-visible {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
    border-radius: var(--radius-xs);
  }
  /* Pas d'anneau au clic souris, uniquement au clavier */
  :focus:not(:focus-visible) { outline: none; }

  /* Blocs sombres : l'anneau de focus bascule sur son ton clair.
     On réaffecte le token sur le conteneur, et tout ce qu'il contient en
     hérite — aucun composant n'a besoin de savoir sur quel fond il vit.

     ⚠ CE QUI DÉCIDE, C'EST LE FOND SUR LEQUEL L'ANNEAU EST PEINT, PAS LA
     COULEUR DE L'ÉLÉMENT. `outline-offset` étant positif, l'anneau se pose
     DEHORS, dans l'écart de --focus-ring-offset : il touche donc l'ARRIÈRE-
     PLAN de l'élément, jamais son propre aplat. C'est pour cela que la
     règle porte sur des conteneurs et vaut pour leurs DESCENDANTS.
     Un composant à l'aplat sombre posé sur la page claire — .button--dark
     — n'est PAS dans ce cas : son anneau est peint sur le
     blanc de la page, où le ton foncé donne 6.31:1. Le basculer y ferait
     tomber l'anneau à 1.71:1. Mesuré, pas supposé.

     .skip-link fait exception et rejoint donc la liste : c'est le seul
     élément focalisable qui se pose PAR-DESSUS un bloc sombre sans en être
     un descendant. Déployé, il recouvre la barre de contact ; son anneau
     était peint sur le bleu nuit de celle-ci, où le ton foncé ne donnait que
     2.86:1 — sous le seuil de 3:1 de SC 1.4.11, et invisible précisément
     pour la personne à qui ce lien est destiné. Comme il déborde aussi sur
     le blanc de l'en-tête, son anneau est en plus ramené à l'intérieur de
     son propre aplat : voir .skip-link dans la couche `components`. */
  .topbar,
  .hero__stage,
  .hero__card,
  .figure__overlay,
  .cta-band,
  .footer,
  .skip-link {
    --focus-ring-color: var(--focus-ring-color-on-dark);
  }

  /* Sélection de texte aux couleurs de la marque */
  ::selection {
    background-color: var(--brand-mint-500);
    color: var(--text-on-accent);
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  3. LAYOUT — conteneurs et rythme                                        ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer layout {
  /* Conteneur central unique. Toute section l'utilise : la largeur du site
     se règle depuis --width-page dans tokens.v1.9.0.3.css. */
  .wrap {
    width: min(100% - (var(--space-gutter) * 2), var(--width-page));
    margin-inline: auto;
  }

  .section { padding-block: var(--space-section); }
  .section--alt { background-color: var(--surface-alt); }
  /* Le bandeau d'appel à l'action arrondit son angle bas de fuite
     (--radius-sweep) : l'angle laisse voir ce qui est DERRIÈRE lui, le fond
     de page blanc — pas la section qui suit. Quand celle-ci est alternée
     (#contact depuis septembre 2026), un coin blanc de 154 px à 1280 se
     glissait entre le bleu nuit et le vert pâle. La section projette son
     fond vers le haut, sous le bandeau (positionné, donc peint au-dessus) :
     seul le coin laissé libre par l'arrondi le montre. */
  .cta-band + .section--alt {
    box-shadow: 0 calc(var(--radius-sweep) * -1) 0 var(--surface-alt);
  }
  .section--flush-top { padding-top: 0; }

  /* Pas de `scroll-margin` sur les sections : c'est volontaire.
     `scroll-padding-top` sur <html> (couche base) suffit à garder une ancre
     sous l'en-tête collant, et il s'applique à TOUS les défilements —
     fluides, instantanés, `scrollIntoView`, déplacement du focus au clavier.
     Une marge de défilement posée ici S'ADDITIONNAIT à cette valeur au lieu
     de la compléter : la section arrivait à 196 px du haut, au-delà de la
     ligne du suivi de défilement (alors fixée à 120 px), et le menu allumait
     le lien de la section précédente. Sans elle, l'arrivée est à 98 px. */

  /* Grille deux colonnes qui se replie seule sous le seuil --grid-min-xl.
     Aucun media query nécessaire.

     LE SEUIL NE DÉPASSE JAMAIS LA PLACE DISPONIBLE — `min(…, 100%)`.
     Une piste `minmax(320px, 1fr)` ne descend pas sous 320 px, même quand
     le conteneur n'en offre que 280 : à 320 px de fenêtre, la colonne
     sortait de 20 px à droite (mesuré sur #a-propos, 340 > 320), et seul
     le rognage de `body` le cachait. Plafonné à 100 %, le seuil décide
     toujours du nombre de colonnes, mais une colonne seule prend la
     largeur du conteneur. Même correction sur toutes les grilles
     `auto-fit` du fichier. */
  /* Gouttière commune à toutes les mises en colonnes de la page : voir la
     note de --space-fluid-lg. Cette division employait la plus large des
     trois valeurs en circulation (64 px) — c'est elle que le client voyait
     en premier. */
  .split {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min-xl), 100%), 1fr));
    gap: var(--space-fluid-lg);
    align-items: center;
  }

  /* Grille de cartes auto-adaptative */
  .grid-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--width-card-min), 100%), 1fr));
    gap: var(--space-md);
  }

  /* ── Texte d'un côté, image de l'autre ───────────────────────────────
     Deux colonnes inégales : le texte porte le propos, l'image l'appuie.
     Les proportions ne sont pas écrites ici mais dans le design system
     (--side-cols-*), parce qu'elles sont PARTAGÉES avec la section des
     services : une seule décision, un seul endroit.

     DEUX RAPPORTS, ET C'EST UNE DISTINCTION, PAS UN OUBLI. La règle
     générale (0.85 contre 2) vaut quand les deux colonnes ont des hauteurs
     comparables. Quand la colonne d'appui porte une PHOTO face à une
     colonne trois fois plus haute — le cas de la FAQ, seul porteur de
     --media-end — la même largeur produit une vignette. C'est le second
     rapport, et sa mesure est consignée sur --side-cols-media-tall.

     AUCUN `order`, ET C'EST UNE DÉCISION, PAS UN OUBLI.
     Le côté de l'image est décidé par le BALISAGE : la figure est écrite
     avant le texte pour la poser à gauche, après pour la poser à droite.
     Le modificateur ne fait qu'accorder la grille à cet ordre-là. Un
     `order` CSS ferait diverger l'ordre vu de l'ordre lu — au clavier et
     au lecteur d'écran, on se retrouverait à parcourir la page dans un
     sens qui n'est pas celui qu'on voit. C'est un défaut d'accessibilité
     connu (WCAG 1.3.2), et il est gratuit à éviter : il suffit d'écrire le
     balisage dans le bon ordre, ce qui est fait.

     La colonne d'image ne descend pas sous --grid-min-md : en dessous,
     une photo cesse d'être lisible et le texte se retrouve en colonnes de
     journal. C'est le palier de repli ci-dessous qui prend le relais. */
  .side-layout {
    display: grid;
    /* Gouttière commune à toutes les paires colonne-texte / colonne-image
       de la page : voir la note de --space-fluid-lg dans tokens.v1.9.0.3.css. */
    gap: var(--space-fluid-lg);
    /* Chaque colonne commence en haut : la colonne de texte est deux à
       trois fois plus haute que l'image, les centrer ferait flotter la
       photo au milieu d'un vide. */
    align-items: start;
  }
  /* ⚠ --media-end EST AUJOURD'HUI SYNONYME DE « LA FAQ », et le fichier le
     suppose déjà ailleurs : c'est ce modificateur qui déclenche le format
     portrait et le collage de la figure, plus bas dans `components`. Le
     rapport large est posé au même endroit que ces deux-là, pour qu'ils
     restent solidaires. Le jour où une seconde section emploiera
     --media-end sans avoir de colonne haute en face, ce sont les trois
     règles qu'il faudra déplacer derrière un modificateur propre — pas
     celle-ci toute seule. */
  .side-layout--media-end {
    grid-template-columns:
      var(--side-cols-text) minmax(var(--grid-min-md), var(--side-cols-media-tall));
  }
  .side-layout--media-start {
    grid-template-columns:
      minmax(var(--grid-min-md), var(--side-cols-media)) var(--side-cols-text);
  }
  /* ⚠ LES DEUX COLONNES DOIVENT DÉMARRER SUR LA MÊME LIGNE.
     Une grille n'a pas de « bloc suivant », elle a des colonnes : le
     rythme de flux de la couche `base` (`p + p`, `:is(h1…h4) + *`) n'y a
     plus aucun sens, et pose pourtant sa marge haute sur tout enfant qui
     suit un frère. C'est exactement ce qui décalait le bandeau
     d'engagements de 16 px, et le piège se rejoue ici dès qu'une colonne
     commence par un titre ou un paragraphe.
     On coupe la fuite à la racine, sur les enfants directs. Les marges
     VOULUES passent, elles, par les utilitaires `mt-*` : ils vivent dans
     la dernière couche et l'emportent sur cette règle — ce qui est
     précisément la distinction qu'on cherche, entre une marge décidée et
     une marge héritée par accident.

     ⚠ `min-width: 0` N'EST PAS UNE PRÉCAUTION DE STYLE, C'EST LA
     CORRECTION D'UN DÉBORDEMENT MESURÉ. Le minimum automatique d'une piste
     `1fr` est le contenu minimal de la colonne, et la liste de la FAQ
     porte `contain-intrinsic-size: auto 640px` : tant que le navigateur en
     diffère la mise en page, elle déclare 640 px de large. À 390 px de
     fenêtre, la colonne se calait donc sur 640 px pour une page qui n'en
     offre que 350, et la figure sortait de l'écran — invisible, puisque
     `body` porte `overflow-x: clip` et le rognait en silence. */
  .side-layout > * {
    margin-block-start: 0;
    min-width: 0;
  }

  /* Pile verticale à espacement régulier */
  .stack {
    display: grid;
    gap: var(--space-sm);
    align-content: start;
  }
  .stack--lg { gap: var(--space-md); }

  /* Rangée horizontale qui passe à la ligne */
  .cluster {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    align-items: center;
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  4. COMPONENTS — briques réutilisables                                   ║
   ║  Chaque composant est autonome : il ne dépend que des tokens.            ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer components {

  /* ───────────────────────────────────────────────── Lien d'évitement ──
     Premier élément focusable de la page : permet de sauter la navigation
     au clavier. Invisible tant qu'il n'a pas le focus. */
  .skip-link {
    position: absolute;
    inset-inline-start: var(--space-sm);
    top: calc(var(--space-xl) * -1);
    z-index: var(--layer-skip-link);
    padding: var(--space-xs) var(--space-md);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    background-color: var(--surface-inverse);
    color: var(--text-on-dark);
    font-weight: var(--weight-semibold);
    transition: top var(--duration-fast) var(--ease-out);
  }
  .skip-link:focus { top: 0; }
  /* Seul élément du site à CHEVAUCHER deux fonds : déployé à top: 0, il
     mesure 50 px pour une barre de contact qui n'en fait que 42. Son anneau,
     posé dehors comme partout ailleurs, se retrouvait donc à cheval sur le
     bleu nuit ET sur le blanc de l'en-tête — aucune des deux teintes de
     --focus-ring-color ne tient sur les deux à la fois : le ton foncé donne
     2.86:1 sur le bleu, le ton clair 1.71:1 sur le blanc. Le haut de
     l'anneau, lui, sortait carrément de l'écran.
     On le pose donc à l'INTÉRIEUR, sur le seul fond qui court sous la
     totalité de son pourtour : l'aplat du lien lui-même. Le ton clair y
     donne 10.55:1 sur tout le pourtour, quoi qu'il y ait derrière, et
     l'anneau redevient un cadre entier au lieu d'un « U » rogné.
     C'est la seule dérogation à l'anneau extérieur du site, et elle tient à
     une géométrie, pas à un goût : rétablir l'écart positif ici, c'est
     rouvrir l'un des deux défauts. */
  .skip-link:focus-visible {
    outline-offset: calc(var(--focus-ring-offset) * -1);
  }

  /* ─────────────────────────────────────────────────────── Sur-titre ──
     Le petit libellé vert précédé d'un trait, au-dessus des titres.
     Le trait se cale sur la PREMIÈRE ligne (1.9.0.2) : centré sur le
     bloc, il tombait entre les deux lignes d'un sur-titre long à 375 px.
     Sur une ligne, sa position est la même qu'en centrage. */
  .eyebrow {
    display: inline-flex;
    align-items: flex-start;
    gap: var(--space-2xs);
    color: var(--text-accent);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
  }
  .eyebrow::before {
    content: '';
    width: var(--eyebrow-rule-width);
    height: var(--stroke-base);
    margin-block-start: var(--eyebrow-rule-offset);
    border-radius: var(--radius-pill);
    background-color: var(--brand-mint-500);
    /* Le trait se dessine quand la section apparaît */
    transform-origin: left center;
    transition: transform var(--duration-slow) var(--ease-out);
  }
  /* Le trait ne se dessine que s'il a une arrivée à accompagner : tant que
     le bloc n'est pas révélé il est rétracté, ailleurs il est simplement là. */
  [data-reveal]:not(.is-revealed) .eyebrow::before { transform: scaleX(0); }
  .is-revealed .eyebrow::before,
  .eyebrow--static::before { transform: scaleX(1); }

  /* ─── Illustration d'article (1.6.0) ─────────────────────────────────
     Dessins au trait faits main, en ligne dans le HTML, décoratifs
     (aria-hidden). Aucune couleur n'est écrite dans le SVG : chaque groupe
     reçoit son rôle ici, depuis les tokens --illustration-*. Le cadre est
     un aplat vert clair aux arrondis doux, celui de l'image de partage.
     Groupes disponibles : __sol, __eteint, __alerte, __accent, __succes,
     __aplat, __aplat-fort, __aplat-clair, __aire (surface sans contour). */
  .article-illustration {
    display: block;
    padding: var(--illustration-marge);
    border-radius: var(--illustration-rayon);
    background-color: var(--illustration-fond);
  }
  .illustration {
    display: block;
    inline-size: 100%;
    block-size: auto;
    aspect-ratio: var(--illustration-ratio);
    color: var(--illustration-trait);
    stroke-width: var(--illustration-trait-largeur);
  }
  .illustration__sol { stroke: var(--illustration-sol); }
  .illustration__eteint { stroke: var(--illustration-eteint); }
  .illustration__alerte { stroke: var(--illustration-alerte); fill: var(--illustration-aplat-clair); }
  .illustration__accent { stroke: var(--illustration-accent); }
  .illustration__succes { stroke: var(--illustration-trait); }
  .illustration__succes circle { fill: var(--illustration-accent); stroke: none; }
  .illustration__aplat { fill: var(--illustration-aplat); }
  .illustration__aplat-fort { fill: var(--illustration-aplat-fort); }
  .illustration__aplat-clair { fill: var(--illustration-aplat-clair); }
  .illustration__aire { fill: var(--illustration-aplat); stroke: none; }
  .reveal .eyebrow::before { transform: scaleX(0); }
  .reveal.is-revealed .eyebrow::before { transform: scaleX(1); }

  /* ───────────────────────────────────────────────────────── Chapô ──── */
  .lead {
    max-width: var(--measure);
    color: var(--text-muted);
    font-size: var(--text-lg);
  }
  .center .lead,
  .lead--center { margin-inline: auto; }

  /* ─────────────────────────────────────────── Code d'erreur ────────── */
  /* Le grand nombre qui ouvre la page 404. Il SIGNALE, il ne PARLE pas :
     c'est le <h1> juste dessous qui dit ce qui se passe, en toutes lettres
     et pour tout le monde. Le balisage l'écarte de l'arbre d'accessibilité
     (`aria-hidden`) ; ce fichier doit tenir la même ligne côté visuel, et
     donc lui donner UNE seule domination — la taille — en le mettant en
     retrait sur les deux autres axes.

     POURQUOI PAS `.metric` PLUS UN MODIFICATEUR. `.metric` est un composant
     de DONNÉE : il porte un halo animé (`.metric::after`), un
     `font-variant-numeric` qui n'existe que pour empêcher le chiffre de
     sauter pendant le comptage, une graisse `--weight-black` — et il ne va
     jamais sans son `.metric__label`, qui nomme ce qui est mesuré. Ici il
     n'y a rien à mesurer et rien à nommer. On aurait écrit plus de lignes
     pour éteindre `.metric` que pour poser cette règle-ci, et la classe
     aurait fini par vouloir dire deux choses.
     (Le compteur, lui, ne serait pas parti : le script vise `[data-metric]`,
     pas `.metric`. Ce n'est donc pas ce qui décide.) */
  .error-code {
    /* DEUX FOIS le token des grands chiffres, et non le token seul.
       --text-display plafonne à 3.4rem quand --text-4xl, la taille du
       <h1>, plafonne à 3.3rem : à taille nue, le nombre mesurerait 54.4 px
       contre 52.8 px au titre — 1.6 px d'écart, soit aucun signal. Le
       facteur est un RAPPORT, pas une valeur de style : la fluidité
       continue de venir du clamp() du token, qui reste la seule source.
       Mesuré : 80.8 px à 390 px de large (2.3× le <h1>), 108.8 px à
       1440 px (2.1×). */
    --_taille: calc(var(--text-display) * 2);
    /* Gris de commande, déjà tenu à 3.49:1 sur le fond de page — au-dessus
       des 3:1 exigés d'un grand texte. En retrait franc du <h1>, qui est en
       --text-strong. Le vert de marque a été écarté : à 2.42:1 sur blanc il
       est trop pâle, et il est réservé à ce qui s'actionne — ici le seul
       bouton de la page. */
    --_couleur: var(--brand-grey-400);

    margin-bottom: var(--space-2xs);
    color: var(--_couleur);
    font-size: var(--_taille);
    /* Un cran SOUS le <h1> (--weight-bold) : le nombre est le plus grand
       élément de l'écran, il ne doit pas être en plus le plus appuyé. */
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
  }

  /* ══════════════════════════════════════════════════════════ Bouton ══
     Un seul composant, décliné par modificateur. Toutes les variantes
     partagent la même géométrie : seules les couleurs changent. */
  .button {
    --_bg: transparent;
    --_fg: var(--text-strong);
    --_border: transparent;
    --_bg-hover: transparent;
    --_shadow: none;

    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2xs);
    padding: var(--button-padding-y) var(--button-padding-x);
    border: var(--button-border-width) solid var(--_border);
    border-radius: var(--radius-signature);
    background-color: var(--_bg);
    color: var(--_fg);
    box-shadow: var(--_shadow);
    font-size: var(--button-font-size);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    text-align: center;
    cursor: pointer;
    overflow: clip;
    transition: var(--transition-colors), var(--transition-transform),
      var(--transition-shadow);
  }

  /* Reflet qui balaie le bouton au survol.
     Les ±120% sont structurels : ils garantissent que le reflet part et
     finit hors du bouton, quelle que soit sa largeur. */
  .button::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--button-sheen);
    transform: translateX(-120%);
    transition: transform var(--duration-slow) var(--ease-out);
    pointer-events: none;
  }
  .button:hover::after,
  .button:focus-visible::after { transform: translateX(120%); }

  /* Le bouton se soulève ET vient à la rencontre du curseur ; au clic il
     s'enfonce franchement. Le retour est immédiat, jamais différé. */
  .button:hover,
  .button:focus-visible {
    transform: translateY(var(--button-lift)) scale(var(--button-hover-scale));
  }
  .button:active { transform: translateY(0) scale(var(--scale-press)); }

  /* La flèche optionnelle avance au survol */
  .button__arrow {
    width: var(--icon-size-inline);
    height: var(--icon-size-inline);
    transition: var(--transition-transform);
  }
  .button:hover .button__arrow { transform: translateX(var(--shift-xs)); }

  /* --- Variantes ---------------------------------------------------------
     Les teintes de survol sont dérivées de la marque dans tokens.v1.9.0.3.css : on
     ne fige plus ici une seconde couleur à maintenir en parallèle. */
  .button--primary {
    --_bg: var(--brand-mint-500);
    --_fg: var(--text-on-accent);
    --_bg-hover: var(--surface-accent-hover);
    --_shadow: var(--shadow-accent);
  }
  .button--primary:hover { background-color: var(--_bg-hover); }

  .button--dark {
    --_bg: var(--surface-inverse);
    --_fg: var(--text-on-dark);
    --_bg-hover: var(--surface-inverse-hover);
    --_shadow: var(--shadow-sm);
  }
  .button--dark:hover { background-color: var(--_bg-hover); }

  .button--light {
    --_bg: var(--brand-white);
    --_fg: var(--text-strong);
    --_bg-hover: var(--surface-accent-soft);
    --_shadow: var(--shadow-sm);
  }
  .button--light:hover { background-color: var(--_bg-hover); }

  /* Le contour est la seule chose qui signale ce bouton : il relève de
     SC 1.4.11 et prend donc le trait de commande, pas le filet décoratif. */
  .button--outline {
    --_border: var(--border-control);
    --_fg: var(--text-strong);
    --_bg-hover: var(--surface-accent-soft);
  }
  .button--outline:hover {
    border-color: var(--border-accent);
    background-color: var(--_bg-hover);
  }

  /* Contour clair, pour les fonds sombres */
  .button--ghost {
    --_border: var(--border-on-dark);
    --_fg: var(--text-on-dark);
    --_bg-hover: var(--overlay-white-soft);
  }
  .button--ghost:hover { background-color: var(--_bg-hover); }

  .button--block { width: 100%; }

  /* ════════════════════════════════════════════════════════════ Carte ══
     La carte est déclarée conteneur : sa mise en page interne réagit à SA
     propre largeur, pas à celle de l'écran. Une même carte peut donc vivre
     dans une grille à trois colonnes ou dans une colonne pleine largeur
     sans qu'aucun media query ne s'en mêle. */
  .card {
    container-type: inline-size;
    container-name: card;

    position: relative;
    display: flex;
    flex-direction: column;
    padding: var(--card-padding);
    border: var(--stroke-hairline) solid var(--border-subtle);
    border-radius: var(--card-radius);
    background-color: var(--surface-raised);
    transition: var(--transition-transform), var(--transition-shadow),
      border-color var(--duration-fast) var(--ease-out);
  }
  /* La carte se lève au survol et au focus CLAVIER d'un de ses liens —
     pas au focus laissé par un clic. Avec `:focus-within`, le lien cliqué
     gardait le focus ; au « Précédent », le navigateur restaurait la page
     depuis son cache avant/arrière avec ce focus, et la carte restait
     levée jusqu'au clic suivant. `:is()` tolère un `:has()` inconnu : un
     navigateur qui l'ignore garde le survol, et le lien son anneau de
     focus (`:focus-visible`, couche base). */
  .card:is(:hover, :has(:focus-visible)) {
    transform: translateY(var(--card-lift)) scale(var(--card-hover-scale));
    border-color: transparent;
    box-shadow: var(--shadow-md);
  }

  /* Halo vert du survol. C'est un calque à part dont seule l'OPACITÉ varie :
     une ombre animée obligerait le navigateur à la recalculer image par
     image, là où un fondu reste une simple composition. */
  .card::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: var(--card-glow);
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-out);
    pointer-events: none;
  }
  .card:is(:hover, :has(:focus-visible))::after { opacity: 1; }

  .card__title { margin-bottom: var(--space-3xs); }
  .card__text {
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }

  /* Liste à puces vertes, utilisée dans les cartes de services */
  .card__list {
    display: grid;
    gap: var(--space-3xs);
    margin-top: var(--space-xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
  .card__list li {
    position: relative;
    padding-inline-start: var(--space-sm);
    transition: transform var(--duration-fast) var(--ease-out);
  }
  .card__list li::before {
    content: '';
    position: absolute;
    inset-inline-start: 0;
    top: var(--card-bullet-offset);
    width: var(--card-bullet-size);
    height: var(--card-bullet-size);
    border-radius: var(--radius-pill);
    background-color: var(--brand-mint-500);
    transition: var(--transition-transform);
  }
  /* Au survol, la liste se décale en cascade et les puces s'ouvrent : la
     carte donne l'impression de se déplier plutôt que de simplement bouger. */
  .card:is(:hover, :has(:focus-visible)) .card__list li { transform: translateX(var(--card-list-shift)); }
  .card:is(:hover, :has(:focus-visible)) .card__list li::before { transform: scale(var(--card-bullet-hover-scale)); }
  .card:is(:hover, :has(:focus-visible)) .card__list li:nth-child(2) { transition-delay: calc(var(--card-list-stagger) * 1); }
  .card:is(:hover, :has(:focus-visible)) .card__list li:nth-child(3) { transition-delay: calc(var(--card-list-stagger) * 2); }
  .card:is(:hover, :has(:focus-visible)) .card__list li:nth-child(4) { transition-delay: calc(var(--card-list-stagger) * 3); }
  .card:is(:hover, :has(:focus-visible)) .card__list li:nth-child(5) { transition-delay: calc(var(--card-list-stagger) * 4); }

  /* Carte large : la liste de prestations se met sur deux colonnes plutôt
     que de s'étirer en lignes trop longues. */
  @container card (min-width: 26rem) {
    .card__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }

  /* ═══════════════════════════════════════════════ Citation attribuée ══
     L'IDIOME DE CITATION DU SITE, ÉCRIT UNE FOIS POUR DEUX EMPLACEMENTS.

     Le site cite quelqu'un à deux endroits : le cartouche posé sur la photo
     de la section « Une interlocutrice » (.pull-quote) et les cartes du
     bandeau de témoignages (.card--quote). Ce qui se partage n'est PAS le
     cadre — l'un est un cartouche détouré et positionné, l'autre une carte
     dans une bande qui défile — mais la façon de SIGNER : le nom détaché
     sous la citation, la fonction sous le nom, plus discrète et jamais
     décorative. C'est cette partie-là, et elle seule, qui est mise en
     commun ; factoriser le cadre reviendrait à lier deux composants qui
     n'ont ni la même place ni le même rôle.

     Elle vit dans `components`, alors que .pull-quote est déclaré dans
     `sections` : l'ordre des couches fait que le cartouche surcharge
     naturellement ce socle là où il doit s'en écarter, sans un point de
     spécificité en plus.

     ⚠ `font-size: inherit` sur le <small> N'EST PAS DÉCORATIF. Sans lui,
     le navigateur applique son `smaller` par défaut : la ligne de fonction
     tombait à 11,7 px sur les 14 px du nom — sous tout plancher de
     lisibilité, pour la ligne qui porte précisément ce qui rend un
     témoignage vérifiable.
     Contrastes (rôles du §2 de tokens.v1.9.0.3.css, sur le blanc des deux cadres) :
       nom ......... text-strong sur surface-raised .... 18.04:1  (AAA)
       fonction .... text-muted sur surface-raised .....  5.46:1  (AA) */
  :is(.pull-quote, .card--quote) cite {
    display: block;
    margin-top: var(--space-2xs);
    color: var(--text-strong);
    font-size: var(--text-sm);
    font-style: normal;
    font-weight: var(--weight-bold);
  }
  :is(.pull-quote, .card--quote) cite small {
    display: block;
    color: var(--text-muted);
    font-size: inherit;
    font-weight: var(--weight-regular);
  }

  /* ─── Carte de témoignage ────────────────────────────────────────────
     Variante de .card : même silhouette, même contour, même relief au
     survol. Trois écarts seulement, tous motivés.

     ① LA CITATION PASSE AU CORPS DE TEXTE COURANT. C'est le rang 1 de la
        hiérarchie voulue : la preuve se lit, elle ne se survole pas. Le
        cartouche de la photo, lui, reste en --text-sm — c'est une légende
        posée sur une image, pas le contenu d'une section.
     ② UN FILET VERT À GAUCHE, repris du cartouche. Seul ornement de la
        carte, et il dit ce qu'elle contient : sans lui, six cartes de
        citations et six cartes de services se ressemblent. Il est peint par
        un calque et non par une bordure, parce qu'une bordure de 4 px se
        déformerait dans les deux grands rayons du contour signature.
        .card::after porte déjà le halo de survol ; ::before était libre.
     ③ RIEN NE SE TRONQUE. Aucune hauteur imposée, aucun `line-clamp`,
        aucune ellipse : une citation coupée est une citation altérée. Si un
        extrait ne tient pas, c'est la phrase qui raccourcit à l'écriture,
        jamais l'affichage qui coupe. C'est la carte qui grandit.

     Pas d'italique, contrairement au cartouche : la police du site n'a
     qu'une coupe romaine (une seule @font-face, `font-style: normal`), le
     navigateur oblique donc les caractères par calcul. Le procédé passe
     inaperçu sur les trois lignes d'une légende ; il se voit et se paie en
     lisibilité sur six lignes de texte courant. */
  .card--quote {
    /* Le filet occupe la gouttière de gauche : le texte s'en écarte d'un
       cran pour ne pas le toucher. */
    padding-inline-start: calc(var(--card-padding) + var(--space-2xs));
  }
  .card--quote::before {
    content: '';
    position: absolute;
    inset-block: var(--quote-rule-inset);
    inset-inline-start: 0;
    width: var(--stroke-thick);
    border-radius: var(--radius-pill);
    background-color: var(--brand-mint-500);
  }
  /* text-body sur surface-raised : 16.26:1 (AAA) */
  .card--quote p {
    color: var(--text-body);
    font-size: var(--text-base);
    text-wrap: pretty;
  }

  /* ═════════════════════════════════════════════════════ Pastille icône ══
     Le carré vert arrondi qui porte une icône. Taille pilotée par --size. */
  .icon-badge {
    --size: var(--icon-badge-size);
    position: relative;
    /* Contexte d'empilement : l'anneau passe DERRIÈRE le glyphe plutôt que
       de le voiler au moment où il part. */
    z-index: var(--layer-base);
    display: grid;
    place-items: center;
    flex: none;
    width: var(--size);
    height: var(--size);
    border-radius: var(--radius-sm) var(--radius-md) var(--radius-sm) var(--radius-md);
    background-color: var(--surface-accent);
    color: var(--text-on-accent);
    transition: var(--transition-transform);
  }
  .icon-badge svg {
    width: var(--icon-badge-glyph-ratio);
    height: var(--icon-badge-glyph-ratio);
  }
  .icon-badge--sm { --size: var(--icon-badge-size-sm); }
  .icon-badge--plain {
    background-color: transparent;
    color: var(--text-accent);
  }
  /* Anneau qui s'échappe de la pastille. Absolument positionné, il n'est
     pas un élément de la grille et ne déplace donc pas l'icône. */
  .icon-badge::after {
    content: '';
    position: absolute;
    z-index: -1;
    inset: 0;
    border-radius: inherit;
    background: var(--halo-accent);
    opacity: 0;
    pointer-events: none;
  }

  /* L'icône bascule quand on survole sa carte parente, et l'anneau part
     une seule fois — un accusé de réception, pas une boucle. */
  .card:is(:hover, :has(:focus-visible)) .icon-badge,
  .channel:hover .icon-badge,
  .channel:focus-visible .icon-badge {
    transform: rotate(var(--tilt-angle)) scale(var(--scale-tilt));
  }
  .card:hover .icon-badge::after,
  .channel:hover .icon-badge::after {
    animation: badge-ring var(--duration-slow) var(--ease-out);
  }

  /* ═══════════════════════════════════════════════════════════ Icônes ══
     Toutes les icônes du site proviennent d'un sprite SVG unique placé en
     haut du <body>. On les appelle avec <svg class="icon"><use href="#…">.
     Elles héritent de la couleur du texte. */
  .icon {
    width: var(--icon-size);
    height: var(--icon-size);
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke-width);
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .icon--solid { fill: currentColor; stroke: none; }

  /* Une icône posée directement dans un flux de texte doit rester SUR la
     ligne. Le reset met tous les <svg> en display:block — ce qui convient
     aux conteneurs flex, où l'icône est un élément à part entière, mais
     coupe un paragraphe en deux. Le recalage compense l'écart entre la
     ligne de base du texte et le centre optique du pictogramme. */
  p > .icon {
    display: inline-block;
    vertical-align: var(--icon-inline-baseline);
    margin-inline-end: var(--space-3xs);
  }

  /* ═════════════════════════════════════════════════ Modale (dialog) ══
     S'appuie sur l'élément natif <dialog> : gestion du focus, de la touche
     Échap et de l'accessibilité assurée par le navigateur. */
  .modal {
    width: min(100% - (var(--space-md) * 2), var(--modal-width));
    padding: 0;
    border: none;
    border-radius: var(--radius-signature-lg);
    background-color: var(--surface-raised);
    box-shadow: var(--shadow-lg);
    color: var(--text-body);
  }
  .modal::backdrop {
    background-color: var(--modal-backdrop);
    backdrop-filter: blur(var(--modal-backdrop-blur));
  }
  /* Animation d'ouverture (@starting-style est ignoré si non supporté) */
  .modal[open] {
    animation: modal-in var(--duration-base) var(--ease-spring);
  }
  .modal[open]::backdrop {
    animation: fade-in var(--duration-base) var(--ease-out);
  }
  .modal__body {
    display: grid;
    justify-items: center;
    gap: var(--space-xs);
    padding: var(--space-xl) var(--space-md) var(--space-md);
    text-align: center;
  }
  .modal__icon {
    --size: var(--icon-badge-size-lg);
    margin-bottom: var(--space-2xs);
    /* Battement lent pour attirer l'œil sans agresser */
    animation: pulse var(--duration-pulse) var(--ease-in-out) var(--modal-icon-pulses);
  }
  .modal__title { font-size: var(--text-xl); }
  .modal__text {
    max-width: var(--measure-narrow);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }
  .modal__actions {
    display: grid;
    gap: var(--space-2xs);
    width: 100%;
    margin-top: var(--space-2xs);
  }

  /* ══════════════════════════════════════════ Fenêtre de recherche ══
     Le contrat DOM est documenté en tête de `assets/js/recherche.v1.2.0.js`, § 2.
     Rien n'est écrit ici qui ne s'y trouve : ce fichier ne fait qu'habiller
     ce que le module fabrique.

     LA FENÊTRE EST HAUTE DANS L'ÉCRAN, PAS CENTRÉE. On tape puis on lit
     vers le bas ; une fenêtre centrée verticalement remonterait sous les
     doigts à chaque résultat trouvé. Elle est donc ancrée près du bord haut
     et grandit vers le bas jusqu'à sa hauteur maximale.

     ⚠ `display` N'EST POSÉ QUE SUR `[open]`, ET C'EST OBLIGATOIRE.
     La feuille du navigateur masque un <dialog> fermé par
     `dialog:not([open]) { display: none }`. Une déclaration d'auteur
     l'emporte TOUJOURS sur celle du navigateur, quelle que soit la couche :
     écrire `.modal--search { display: flex }` afficherait la fenêtre fermée
     en bas de chacune des quatre pages. La restreindre à `[open]` couvre
     aussi le chemin de repli du contrôleur, qui pose l'attribut à la main
     quand `showModal()` n'existe pas. */
  .modal--search[open] {
    display: flex;
    flex-direction: column;
    /* Plus large que la modale d'information : elle porte une liste, pas un
       message. L'écart au bord est réduit pour qu'un téléphone y gagne de
       la place — mais il n'est pas supprimé : le voile est la SEULE façon
       de refermer au doigt, le balisage ne prévoyant aucun bouton de
       fermeture. Il doit rester atteignable. */
    width: min(100% - (var(--space-sm) * 2), var(--search-modal-width));
    /* Repli d'abord, unité dynamique ensuite : sur un navigateur sans
       `dvh`, la seconde déclaration est ignorée et la première tient. */
    max-height: calc(100vh - (var(--search-modal-offset-block) * 2));
    max-height: calc(100dvh - (var(--search-modal-offset-block) * 2));
    /* La marge basse reste `auto` : c'est elle qui, avec l'`inset: 0` de la
       feuille du navigateur, plaque la fenêtre en haut au lieu de la
       centrer. Les marges latérales restent `auto` et centrent. */
    margin-block: var(--search-modal-offset-block) auto;
  }

  /* ── Le verrou de défilement ────────────────────────────────────────────
     MESURÉ, PAS SUPPOSÉ : `showModal()` rend le reste du document inerte au
     clic et au clavier, mais PAS à la molette ni au doigt. Relevé au
     navigateur, molette posée sur le voile à 1440 × 900 : la page passait
     de 0 à 600 px pendant que la fenêtre, elle, restait immobile. On ferme
     la recherche, et l'on n'est plus là où l'on était.

     `overscroll-behavior` sur la liste ne couvre PAS ce cas : il empêche le
     défilement de se propager depuis la liste, il ne dit rien d'un geste
     fait à côté d'elle. Les deux réglages sont complémentaires, aucun ne
     remplace l'autre.

     `scrollbar-gutter: stable` EST LA MOITIÉ DU RÉGLAGE, PAS UN BONUS.
     Sur les systèmes à ascenseur classique (Windows, Linux), passer la
     racine en `overflow: hidden` escamote l'ascenseur et la page derrière
     s'élargit d'un coup de sa largeur — un saut latéral parfaitement
     visible sous un voile translucide. Réservée pendant le seul verrou, la
     gouttière garde la page exactement à sa place. Sur un système à
     ascenseur flottant (macOS, tous les téléphones), elle ne réserve rien :
     la déclaration est sans effet là où elle serait inutile.

     Le verrou ne vaut QUE pour la recherche. La fenêtre « en construction »
     porte trois lignes et un bouton : on n'y défile pas, et lui poser un
     verrou reviendrait à traiter un symptôme qu'elle n'a pas. */
  @supports selector(:has(*)) {
    html:has(.modal--search[open]) {
      overflow: hidden;
      scrollbar-gutter: stable;
    }
  }

  /* Le corps hérite de `.modal__body` (grille centrée, texte au milieu,
     grande marge haute) : rien de tout cela ne convient à une liste, on
     reprend donc la mise en page à zéro sur ce modificateur. */
  .modal--search .search {
    display: grid;
    grid-template-columns: 1fr;
    /* DEUX RANGÉES DÉCLARÉES, LES AUTRES LAISSÉES À L'AUTOMATISME.
       Rangée 1 : le champ ET le bouton d'effacement, superposés (voir plus
       bas). Rangée 2 : la liste, seule à recevoir `1fr`, donc seule à
       absorber ou à céder de la hauteur quand la fenêtre atteint sa borne.
       Le bloc « rien trouvé » et la ligne d'aide se placent tout seuls dans
       les rangées suivantes — ce qui est LA raison de ne pas les déclarer :
       le bloc « rien trouvé » est masqué la plupart du temps, et une rangée
       déclarée mais vide continuerait de réclamer son `gap`. */
    grid-template-rows: auto minmax(0, 1fr);
    gap: var(--space-sm);
    /* Sans `min-height: 0`, le corps refuse de se comprimer sous sa hauteur
       naturelle et la liste déborde la fenêtre au lieu de défiler. C'est le
       réglage qui fait tenir tout le reste. */
    min-height: 0;
    padding: var(--space-md);
    text-align: start;
  }

  /* ── Le champ ───────────────────────────────────────────────────────────
     La première chose que l'œil doit trouver : pleine largeur, trait de
     commande épaissi, coins généreux. Aucune icône décorative n'y est
     ajoutée — le balisage n'en fournit pas, et un pictogramme dessiné en
     CSS serait un dessin sans nom qui vieillirait mal.

     `--text-lg` A UNE RAISON TECHNIQUE EN PLUS DE SA RAISON GRAPHIQUE : son
     plancher vaut 1rem. Sous 16 px, Safari iOS zoome la page à la mise au
     point d'un champ et ne la dézoome jamais — la fenêtre de recherche
     sortirait de l'écran dès la première frappe.

     Les tokens employés ici dormaient depuis le retrait du formulaire de
     contact : --text-placeholder et --border-control-focus retrouvent leur
     emploi, avec les contrastes déjà mesurés dans tokens.v1.9.0.3.css. */
  .search__input {
    grid-area: 1 / 1;
    width: 100%;
    /* De la place à droite pour le bouton d'effacement, qui se superpose */
    padding: var(--space-xs) calc(var(--search-clear-size) + var(--space-2xs))
      var(--space-xs) var(--space-sm);
    border: var(--stroke-base) solid var(--border-control);
    border-radius: var(--radius-md);
    background-color: var(--surface-raised);
    color: var(--text-strong);
    font-size: var(--text-lg);
    font-weight: var(--weight-medium);
    line-height: var(--leading-snug);
    transition: var(--transition-colors);
  }
  /* L'opacité par défaut de Firefox tirerait la teinte sous son ratio
     annoncé : on la remet à plat et c'est le token qui décide. */
  .search__input::placeholder {
    color: var(--text-placeholder);
    opacity: 1;
  }
  .search__input:focus { border-color: var(--border-control-focus); }

  /* AU REPOS, LE CHAMP RÉCUPÈRE LA PLACE DU BOUTON D'EFFACEMENT.
     Le bouton n'apparaît qu'une fois quelque chose de tapé (`hidden` posé
     par le module) : tant qu'il n'est pas là, réserver ses 44 px revient à
     rétrécir le seul texte visible de la fenêtre — l'invite — pour un
     élément absent. Mesuré à 390 px : l'invite réclame 242 px et n'en
     recevait que 217, elle se coupait au milieu d'un mot alors que le
     champ paraissait à moitié vide.
     Rien ne saute à la première frappe : le texte est aligné à gauche, et
     c'est la place LIBRE à droite qui change, pas la position des signes.
     Sans `:has()`, la marge reste celle du cas avec bouton : l'invite est
     à l'étroit, jamais recouverte. */
  @supports selector(:has(*)) {
    .search:has(.search__clear[hidden]) .search__input {
      padding-inline-end: var(--space-sm);
    }
  }

  /* ── Le bouton d'effacement ─────────────────────────────────────────────
     Il se superpose au champ, calé sur son bord droit. Le balisage ne
     fournit qu'un libellé pour lecteurs d'écran : la croix est donc
     dessinée ici, par deux barres croisées — le même idiome que le burger,
     et aucune image à charger.

     LA HAUTEUR N'EST PAS UNE VALEUR, C'EST CELLE DU CHAMP — et c'est
     pourquoi le bouton n'est pas posé en `absolute`. Il partage la RANGÉE
     du champ dans la grille du corps : `align-self: stretch` lui donne
     exactement la hauteur du champ, quelle que soit la police, quel que
     soit le zoom du texte demandé par le visiteur. Un `inset-block` en
     pixels aurait recopié une hauteur qui bouge, et la croix aurait glissé
     hors de l'axe au premier agrandissement.
     La marge d'un trait le pose à l'intérieur de la bordure du champ.
     Cible obtenue : --search-clear-size de large (44 px) sur la hauteur du
     champ moins deux traits, soit davantage. */
  .search__clear {
    grid-area: 1 / 1;
    justify-self: end;
    align-self: stretch;
    display: grid;
    place-items: center;
    width: var(--search-clear-size);
    margin: var(--stroke-base);
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background-color: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: var(--transition-colors);
  }
  .search__clear::before,
  .search__clear::after {
    content: '';
    grid-area: 1 / 1;
    width: var(--search-clear-glyph);
    height: var(--stroke-base);
    border-radius: var(--radius-pill);
    background-color: currentColor;
  }
  /* Les 45° sont la géométrie de la croix, pas un réglage de charte */
  .search__clear::before { transform: rotate(45deg); }
  .search__clear::after { transform: rotate(-45deg); }
  .search__clear:hover {
    background-color: var(--surface-alt);
    color: var(--text-strong);
  }

  /* ── La liste ───────────────────────────────────────────────────────────
     C'est ELLE qui défile, pas la fenêtre : elle occupe la rangée `1fr` du
     corps, `min-height: 0` l'autorise à se comprimer sous sa hauteur
     naturelle, et `overscroll-behavior: contain` empêche la molette de
     repartir dans la page dès qu'on touche le bas de la liste.

     `:empty` PLUTÔT QUE L'ÉTAT — le module vide la liste avant de la
     reconstruire, à chaque frappe. Un conteneur vide qui garderait sa marge
     ferait sauter le bas de la fenêtre entre deux lettres. La règle vaut
     donc quel que soit `data-search-state`, y compris pendant le chargement
     de l'index et après une erreur réseau. */
  .search__results {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Le module fait défiler la liste au clavier : on lui laisse un peu
       d'air pour que l'option active ne colle pas au bord. */
    scroll-padding-block: var(--space-2xs);
  }
  .search__results:empty { display: none; }

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

  /* Le libellé de section est repris de la surtitraille de la page : c'est
     un repère, pas un titre. Il reste donc discret — et collant, pour qu'on
     sache toujours dans quelle partie du site on lit, même après quelques
     crans de molette. */
  .search__group-title {
    position: sticky;
    inset-block-start: 0;
    z-index: var(--layer-base);
    padding-block: var(--space-3xs);
    background-color: var(--surface-raised);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
  }

  /* ── Un résultat ────────────────────────────────────────────────────────
     Titre puis extrait, serrés : la liste doit se parcourir d'un coup d'œil.

     L'EXTRAIT N'EST PAS TRONQUÉ, ET C'EST UNE DÉCISION MESURÉE, PAS UN
     OUBLI. Une limite à deux lignes avait été posée d'abord, pour la
     densité. Relevée au navigateur sur cinq recherches et neuf largeurs,
     elle CACHAIT le mot trouvé — c'est-à-dire la seule chose qui distingue
     une recherche d'un sommaire :

         panneau           2 lignes        3 lignes       sans limite
         280 px .......  0 mot sur 15   10 sur 15        15 sur 15
         320 px .......  7 sur 15       12 sur 15        15 sur 15
         390 px ....... 10 sur 15       15 sur 15  (*)   15 sur 15
         640 px ....... 15 sur 15       15 sur 15        15 sur 15

     (*) tenu de justesse : « congés » et « paie » passaient à la ligne
     suivante dès qu'un signe s'ajoutait au titre.

     ET ELLE NE RAPPORTAIT RIEN LÀ OÙ ELLE FONCTIONNAIT. À 640 px de
     panneau, un extrait de 150 signes (`search.extraitLongueur`) occupe
     déjà deux lignes tout seul : la limite n'écourtait qu'un extrait sur
     cinq, et de 20 px. Une règle qui ne sert que là où elle ne sert à rien,
     et qui nuit partout ailleurs, se retire.

     CE QUI RÈGLE VRAIMENT LA DENSITÉ SUR TÉLÉPHONE, C'EST LA LONGUEUR DE
     L'EXTRAIT, ET ELLE N'EST PAS ÉCRITE ICI. À 390 px, la ligne tient 43
     signes : 150 signes font donc quatre lignes, et huit résultats mesurent
     environ 1 000 px pour 600 px de liste visible. Le levier est
     `search.extraitLongueur` dans config.v1.4.0.js, pas une troncature en CSS. */
  .search__result {
    padding: var(--space-2xs) var(--space-xs);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .search__result-title {
    display: block;
    color: var(--text-strong);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
  }
  .search__result-excerpt {
    margin-top: var(--space-3xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
  }

  /* ── L'option active ────────────────────────────────────────────────────
     LE SEUL INDICE QUI DISE « ENTRÉE M'EMMÈNE LÀ ». Le focus du clavier ne
     quitte jamais le champ (motif combobox) : sans repère ici, la flèche
     bas n'aurait aucun effet visible.

     DEUX SIGNES PLUTÔT QU'UN, ET CE N'EST PAS UNE COQUETTERIE. L'aplat vert
     pâle ne contraste que de 1.11:1 avec le blanc de la fenêtre : à lui
     seul, il ne satisferait pas SC 1.4.11. Le filet, lui, est peint à la
     teinte de l'anneau de focus, dont le banc mesure et surveille déjà les
     6.31:1 sur fond clair.

     La souris désigne la même option que le clavier — le module s'en charge
     au `mousemove` — d'où l'absence de règle `:hover` distincte : deux
     repères simultanés à l'écran désigneraient deux destinations pour une
     seule touche Entrée.

     Le fond ne se fond PAS : une option qui se colore en retard traîne
     derrière la flèche du clavier. */
  .search__result[aria-selected='true'],
  .search__result.is-active {
    background-color: var(--surface-accent-soft);
    box-shadow: inset var(--search-active-bar) 0 0 var(--focus-ring-color);
  }

  /* ── Le mot trouvé ──────────────────────────────────────────────────────
     Le <mark> du navigateur est un surligneur jaune : il n'a rien à faire
     dans la charte, et il tomberait sous le seuil de lisibilité dès qu'une
     option devient active. On le remplace par une teinte et une graisse.

     LA COULEUR N'EST PAS SEULE À PORTER L'INFORMATION (SC 1.4.1) : la
     graisse passe à `bold` là où le titre est déjà `semibold` et l'extrait
     `regular`. Et la teinte retenue est mesurée sur LES DEUX fonds que le
     mot rencontre — 6.31:1 sur le blanc de la fenêtre, 5.70:1 sur l'aplat
     de l'option active — deux paires déjà surveillées par le banc. */
  .search__mark {
    background-color: transparent;
    color: var(--text-accent);
    font-weight: var(--weight-bold);
  }

  /* ── Rien trouvé ────────────────────────────────────────────────────────
     Ce bloc ne remplace pas la liste : le module la vide, et `:empty` la
     retire. Il prend donc naturellement sa place. */
  .search__empty {
    display: grid;
    justify-items: start;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-xs) var(--space-2xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }

  /* ── Résultats partiels ─────────────────────────────────────────────────
     Posée par le module en tête de la liste quand la recherche a dû
     laisser des mots de côté. Même voix que le bloc « rien trouvé » :
     une remarque, pas une alerte. */
  .search__partial {
    padding: 0 var(--space-xs) var(--space-2xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }

  /* ── La ligne d'aide ────────────────────────────────────────────────────
     Un pied de fenêtre, séparé par un filet : il décrit des touches, il ne
     s'adresse donc qu'à qui en a. Voir le palier `any-hover: none` en fin
     de fichier — c'est le seul endroit du site où l'on interroge le
     dispositif de pointage, et la raison y est écrite. */
  .search__hint {
    padding-top: var(--space-2xs);
    border-top: var(--stroke-hairline) solid var(--border-subtle);
    color: var(--text-muted);
    font-size: var(--text-xs);
  }

  /* ════════════════════════════════════════ Barre de lecture (scroll) ══
     Fine jauge verte en haut de page indiquant la progression de lecture.
     `--progress` est déclarée via @property : le navigateur sait désormais
     l'interpoler, ce qui lisse le rendu même si le script met du temps à
     renvoyer une nouvelle valeur. */
  .reading-progress {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: var(--layer-overlay);
    height: var(--progress-height);
    background-color: var(--brand-mint-500);
    transform: scaleX(var(--progress, 0));
    transform-origin: left center;
    /* La valeur est poussée par le JS ; la transition lisse le rendu */
    transition: transform var(--duration-instant) var(--ease-linear);
    pointer-events: none;
  }

  /* ═══════════════════════════ Matériau verre « liquid glass » (1.7.0) ══
     UNE RECETTE, QUATRE PORTEURS : la carte « À lire ensuite », les
     raccourcis ronds, leurs pastilles de libellé et le bouton retour en
     haut. Les valeurs vivent dans les tokens --glass-* ; cette règle les
     applique. Un porteur ne choisit que son échelle (variables privées
     --_glass-*, redéfinies plus bas pour les ronds) et la façon dont il
     reçoit son ton --glass-tone (0 = clair, 1 = fumé) : écrit par le
     script sur les ronds, déduit de --invite-tone sur la carte.

     Les teintes passent par des variables privées : la fumée suit le ton
     linéairement, l'encre et l'anneau de focus basculent sur une plage
     courte centrée sur 0.5 (--glass-ink-switch-*), bornée à 0 % et 100 %
     pour que les états finis soient strictement les états mesurés.
     `isolation` garantit un contexte d'empilement même sans
     backdrop-filter : la fumée (::after) reste sous le contenu. */
  .invite__card,
  .quick__btn,
  .quick__label,
  .to-top {
    --_glass-bg: var(--glass-bg);
    --_glass-smoke: var(--glass-smoke);
    --_glass-gloss: var(--glass-gloss);
    --_glass-rim: var(--glass-rim);
    --_glass-shadow: var(--glass-shadow);
    --_glass-sheen: var(--glass-sheen);
    --_glass-sheen-dark: var(--glass-sheen-dark);
    --_glass-mix: clamp(0%, calc((var(--glass-tone, 0) - var(--glass-ink-switch-start))
      / var(--glass-ink-switch-span) * 100%), 100%);
    --_ink: color-mix(in oklab, var(--text-strong), var(--text-on-dark) var(--_glass-mix));
    --focus-ring-color: color-mix(in oklab, var(--text-accent),
      var(--focus-ring-color-on-dark) var(--_glass-mix));

    isolation: isolate;
    background-color: var(--_glass-bg);
    background-image: var(--_glass-gloss);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate))
      brightness(var(--glass-brightness));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate))
      brightness(var(--glass-brightness));
    box-shadow: var(--_glass-shadow), var(--_glass-sheen);
    color: var(--_ink);
  }

  /* Fumée de l'état sombre : un calque marine sous le contenu, dont
     l'opacité EST le ton. Il porte ses propres arêtes de lumière, pour que
     le verre fumé garde son épaisseur ; il recouvre le lustre et le halo
     clairs, qui n'ont donc pas à changer eux-mêmes. */
  :is(.invite__card, .quick__btn, .quick__label, .to-top)::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background-color: var(--_glass-smoke);
    box-shadow: var(--_glass-sheen-dark);
    opacity: var(--glass-tone, 0);
    pointer-events: none;
  }

  /* Liseré lumineux : un dégradé réduit à l'épaisseur d'un filet par un
     double masque (tout le calque, moins sa zone de contenu). Positionné
     après la fumée, il reste lumineux dans les deux états. */
  :is(.invite__card, .quick__btn, .quick__label, .to-top)::before {
    content: '';
    position: absolute;
    inset: 0;
    padding: var(--stroke-hairline);
    border-radius: inherit;
    background: var(--_glass-rim);
    -webkit-mask: linear-gradient(var(--brand-black) 0 0) content-box,
      linear-gradient(var(--brand-black) 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(var(--brand-black) 0 0) content-box exclude,
      linear-gradient(var(--brand-black) 0 0);
    pointer-events: none;
  }

  /* Échelle « rond » : mêmes calques, mêmes teintes — donc mêmes
     contrastes que la carte — mais reflets et ombres à la mesure d'un
     disque de 46 px (voir les variantes -sm dans tokens.css). */
  .quick__btn,
  .quick__label,
  .to-top {
    --_glass-gloss: var(--glass-gloss-sm);
    --_glass-rim: var(--glass-rim-sm);
    --_glass-shadow: var(--glass-shadow-sm);
    --_glass-sheen: var(--glass-sheen-sm);
    --_glass-sheen-dark: var(--glass-sheen-dark-sm);
  }

  /* Verre toujours clair : carte et ronds prennent le voile givré, dense
     assez pour rester AA au-dessus d'un fond sombre (mesures dans
     tokens.css, --glass-bg-frosted). Posé ici, AVANT les replis : sans
     backdrop-filter, l'aplat opaque l'emporte toujours. La pastille de
     libellé, opaque, garde son aplat (règle suivante). */
  .invite__card { --_glass-bg: var(--glass-bg-frosted); }

  /* Les ronds ne portent qu'une icône, jugée à 3 contre 1 (SC 1.4.11) et
     non à 4.5 : leur voile, plus léger, laisse deviner la page dessous
     (mesures dans tokens.css, --glass-bg-frosted-sm). */
  .quick__btn,
  .to-top { --_glass-bg: var(--glass-bg-frosted-sm); }

  /* Anneau de focus des ronds : peint HORS du rond, sur une page qui peut
     être claire ou sombre sous lui. L'ombre pleine remplit l'écart de
     --focus-ring-offset d'un intervalle blanc, sous l'anneau vert foncé :
     le double trait reste visible sur tout fond (mesures dans tokens.css,
     section « Anneau de focus »). Les ombres du verre suivent derrière.
     Les commandes de la carte reprennent le même anneau double (section
     de la carte, plus bas). */
  :is(.quick__btn, .to-top):focus-visible {
    box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-gap-color),
      var(--_glass-shadow), var(--_glass-sheen);
  }

  /* La pastille de libellé est un verre OPAQUE : elle vit dans le rond,
     qui porte déjà un backdrop-filter, et la spécification fait de lui sa
     « racine d'arrière-plan » — un flou imbriqué ne verrait que le rond,
     pas la page. Elle prend donc les aplats de repli de la recette (blanc,
     bleu nuit profond), les plus contrastés : c'est du texte posé sur du
     contenu quelconque, la lisibilité passe avant la transparence. */
  .quick__label {
    --_glass-bg: var(--glass-bg-fallback);
    --_glass-smoke: var(--glass-smoke-fallback);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* Sans backdrop-filter : deux aplats opaques, mêmes textes, mêmes
     contrastes que le verre au mieux de sa forme. */
  @supports not ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
    .invite__card,
    .quick__btn,
    .to-top {
      --_glass-bg: var(--glass-bg-fallback);
      --_glass-smoke: var(--glass-smoke-fallback);
    }
  }

  /* Sans color-mix() : retour à la bascule franche. Le script pose
     `.is-on-dark` dès que le ton atteint 0.5 ; l'encre change d'un bloc et
     la fumée s'anime en fondu. Les teintes propres à la carte suivent dans
     sa section. */
  @supports not (color: color-mix(in oklab, red, blue)) {
    .invite__card,
    .quick__btn,
    .quick__label,
    .to-top {
      --_ink: var(--text-strong);
      --focus-ring-color: var(--text-accent);
    }
    .invite.is-on-dark .invite__card,
    .quick__btn.is-on-dark,
    .quick__btn.is-on-dark .quick__label,
    .to-top.is-on-dark {
      --_ink: var(--text-on-dark);
      --focus-ring-color: var(--focus-ring-color-on-dark);
    }
    :is(.invite__card, .quick__btn, .quick__label, .to-top)::after {
      opacity: 0;
      transition: opacity var(--glass-tone-fallback-duration) var(--ease-out);
    }
    :is(.invite.is-on-dark .invite__card, .quick__btn.is-on-dark,
      .quick__btn.is-on-dark .quick__label, .to-top.is-on-dark)::after {
      opacity: 1;
    }
  }

  /* ══════════════════════════════════════════════ Bouton retour en haut ══
     UN BOUTON INVISIBLE DOIT ÊTRE INATTEIGNABLE, PAS SEULEMENT TRANSPARENT.
     Une opacité nulle ne retire rien de l'ordre de tabulation. Le scénario
     se reproduisait à chaque usage : on activait le bouton, la page
     remontait, la classe `is-visible` tombait — et le focus restait sur un
     bouton devenu invisible. Plus aucun repère à l'écran, alors que c'est
     exactement ce que SC 2.4.7 (focus visible, AA) demande de garantir.

     On reprend l'idiome du panneau de navigation mobile, à l'identique :
     `visibility` sort réellement l'élément du parcours clavier, et son
     basculement est RETARDÉ de la durée du fondu pour ne pas escamoter
     l'animation de sortie (--_hide-delay, remis à zéro à l'arrivée).
     `pointer-events` reste utile pendant ce délai : le bouton s'efface
     encore, il ne doit déjà plus se cliquer.
     Sans script la classe n'arrive jamais : le bouton reste hors du
     parcours au lieu d'y traîner une commande sans effet.

     Verre adaptatif depuis 1.7.0 (matériau ci-dessus), ton écrit par le
     script dans --glass-tone — désactivé depuis 1.8.0.1 : le ton reste à
     0. L'anneau de focus est peint HORS du rond, sur la page, claire ou
     sombre : il est donc doublé d'un intervalle blanc (règle « Anneau de
     focus des ronds », plus haut), visible sur les deux.
     Deux transformations distinctes : `transform` pour l'entrée, que le
     script ne mesure pas ; `translate` pour toute remontée (barre d'appel),
     qu'il lit pour calculer le ton. `scale` porte le survol. */
  .to-top {
    --_hide-delay: var(--duration-base);
    position: fixed;
    inset-block-end: var(--to-top-offset);
    inset-inline-end: var(--to-top-offset);
    z-index: var(--layer-sticky);
    display: grid;
    place-items: center;
    width: var(--to-top-size);
    height: var(--to-top-size);
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(var(--shift-md)) scale(var(--scale-enter-deep));
    pointer-events: none;
    transition: opacity var(--duration-base) var(--ease-out),
      transform var(--duration-base) var(--ease-spring),
      translate var(--duration-base) var(--ease-out),
      scale var(--duration-fast) var(--ease-out),
      --glass-tone var(--glass-tone-duration) var(--glass-tone-easing),
      visibility 0s var(--ease-linear) var(--_hide-delay);
  }
  .to-top.is-visible {
    /* À l'arrivée, rien n'attend : le bouton est atteignable dès l'instant
       où il commence à paraître. */
    --_hide-delay: 0s;
    opacity: 1;
    visibility: visible;
    transform: none;
    pointer-events: auto;
  }

  /* Glyphe commun aux trois ronds : même taille partout, la pile reste
     alignée au pixel sur le bouton. Encre du verre : bleu nuit en clair
     (18.04 contre 1 sur blanc, 13.4 au pire, sur du noir pur sous le
     voile givré), blanc en sombre (8.76 contre 1 au pire, sur du blanc pur sous
     la fumée) — au-dessus des 3 contre 1 de SC 1.4.11 dans tous les cas. */
  :is(.to-top, .quick__btn) .icon {
    inline-size: var(--to-top-icon-size);
    block-size: var(--to-top-icon-size);
  }

  /* Survol, au pointeur fin seulement : sur écran tactile, un survol
     « collé » après le toucher laisserait le rond grossi. */
  @media (hover: hover) {
    .to-top:hover,
    .quick__btn:hover { scale: var(--round-hover-scale); }
  }

  /* ═══════════════════════════════════ Raccourcis flottants (1.7.0) ══
     Conseils et prise de rendez-vous, empilés au-dessus du bouton retour
     en haut, une fois la carte « À lire ensuite » vue. Même rond, même
     axe, une seule gouttière (--quick-gap) entre les ronds et jusqu'au
     bouton : la pile est ancrée par le bas, à une hauteur DÉDUITE du
     bouton (--quick-stack-offset), si bien qu'un rond retiré ne laisse
     aucun trou.

     Le « quand » appartient au script (module quickLinks) : il retire
     `hidden`, pose `.is-visible` à l'image suivante, et attend la fin de
     la transition d'OPACITÉ de `.quick` lui-même pour remettre `hidden`.
     Le conteneur ne peint rien et ne capte aucun clic : une pile sans
     rond visible (téléphone, pages Conseils) n'a ni ombre ni zone morte.

     Entrée en cascade : la pile entière fond, chaque rond monte en
     léger rebond, celui du haut une marche plus tard. En mouvement réduit,
     déplacement et cascade tombent à zéro : il ne reste que le fondu. */
  .quick {
    position: fixed;
    inset-block-end: var(--quick-stack-offset);
    inset-inline-end: var(--to-top-offset);
    z-index: var(--layer-sticky);
    display: flex;
    flex-direction: column;
    gap: var(--quick-gap);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--quick-duration) var(--ease-out),
      translate var(--duration-base) var(--ease-out);
  }
  .quick.is-visible { opacity: 1; }

  .quick__btn {
    --_enter-delay: 0s;
    position: relative;
    display: grid;
    place-items: center;
    inline-size: var(--to-top-size);
    block-size: var(--to-top-size);
    border-radius: var(--radius-pill);
    text-decoration: none;
    transform: translateY(var(--shift-md)) scale(var(--scale-enter-deep));
    transition: transform var(--quick-duration) var(--ease-spring) var(--_enter-delay),
      scale var(--duration-fast) var(--ease-out),
      --glass-tone var(--glass-tone-duration) var(--glass-tone-easing);
  }
  .quick.is-visible .quick__btn {
    transform: none;
    pointer-events: auto;
  }
  /* Le rond du haut arrive une marche après celui du bas — à l'arrivée
     seulement : à la sortie, la pile part d'un bloc. */
  .quick.is-visible .quick__btn:nth-last-child(2) { --_enter-delay: var(--quick-stagger); }
  .quick.is-visible .quick__btn:nth-last-child(3) {
    --_enter-delay: calc(var(--quick-stagger) * 2);
  }

  /* Libellé : masqué à la vue mais présent dans l'arbre d'accessibilité
     (découpé à rien par clip-path, jamais display: none) — c'est le nom
     du lien. Au survol ou au focus clavier, il paraît en pastille à
     gauche du rond, centrée sur lui, et glisse depuis lui. La découpe ne
     s'anime pas : elle s'ouvre d'un coup à l'arrivée, et ne se referme
     qu'une fois le fondu de sortie terminé. Une ligne jusqu'à
     --quick-label-max, au-delà le libellé passe à la ligne. */
  .quick__label {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-end: calc(100% + var(--quick-label-gap));
    display: flex;
    align-items: center;
    inline-size: max-content;
    max-inline-size: var(--quick-label-max);
    min-block-size: var(--quick-label-height);
    padding-inline: var(--quick-label-padding-x);
    border-radius: var(--radius-pill);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
    text-wrap: balance;
    opacity: 0;
    transform: translate(var(--quick-label-shift), -50%);
    clip-path: inset(50%);
    pointer-events: none;
    transition: opacity var(--duration-fast) var(--ease-out),
      transform var(--duration-fast) var(--ease-out),
      clip-path 0s var(--ease-linear) var(--duration-fast),
      --glass-tone var(--glass-tone-duration) var(--glass-tone-easing);
  }
  .quick__btn:focus-visible .quick__label {
    opacity: 1;
    transform: translate(0, -50%);
    clip-path: inset(calc(var(--quick-label-clip-reach) * -1));
    transition-delay: 0s;
  }
  @media (hover: hover) {
    .quick__btn:hover .quick__label {
      opacity: 1;
      transform: translate(0, -50%);
      clip-path: inset(calc(var(--quick-label-clip-reach) * -1));
      transition-delay: 0s;
    }
  }

  /* ═════════════════════════════════════ Barre d'appel collante (1.5.0) ══
     Prend le relais de la pastille d'appel de l'en-tête, qui disparaît sous
     480 px (voir le palier 480, section RESPONSIVE). D'où le même seuil :
     au-dessus, l'en-tête collant porte déjà l'appel, et une barre ferait
     doublon. En bureau, elle n'existe pas du tout (`display: none`).

     Le « quand » appartient au module `ctaBar` du script : il pose
     `.is-visible` sur la barre et `has-cta-bar` sur <html>. Le « comment »
     est ici. Même idiome que .to-top : masquée, la barre est en
     `visibility: hidden`, donc hors du parcours clavier ; le basculement
     est retardé de la durée du fondu pour ne pas escamoter la sortie.
     Sans script, la classe n'arrive jamais : la barre reste masquée et
     l'appel demeure dans le héros et le bandeau. */
  .cta-bar { display: none; }

  @media (max-width: 480px) {
    .cta-bar {
      position: fixed;
      inset-inline: 0;
      inset-block-end: 0;
      z-index: var(--layer-sticky);
      display: block;
      padding: var(--cta-bar-padding-y) var(--space-gutter);
      /* Zone sûre du bas d'écran (barre d'accueil des iPhone) : vaut 0 tant
         que la page ne déclare pas `viewport-fit=cover`. */
      padding-block-end: calc(var(--cta-bar-padding-y) + env(safe-area-inset-bottom));
      border-block-start: var(--stroke-hairline) solid var(--cta-bar-border);
      background-color: var(--cta-bar-bg);
      box-shadow: var(--cta-bar-shadow);
      opacity: 0;
      visibility: hidden;
      transform: translateY(var(--cta-bar-enter-shift));
      pointer-events: none;
      transition: opacity var(--duration-base) var(--ease-out),
        transform var(--duration-base) var(--ease-out),
        visibility 0s var(--ease-linear) var(--duration-base);
    }
    .cta-bar.is-visible {
      opacity: 1;
      visibility: visible;
      transform: none;
      pointer-events: auto;
      transition-delay: 0s;
    }
    /* Le bouton garde sa géométrie et ses couleurs ; seule sa hauteur est
       fixée, pour que la barre mesure exactement --cta-bar-height. */
    .cta-bar__button {
      display: flex;
      width: min(100%, var(--cta-bar-max-width));
      min-block-size: var(--cta-bar-button-height);
      margin-inline: auto;
      padding-block: 0;
    }

    /* Ce que la barre impose au reste de la page, tant qu'elle est là.
       1. Le focus clavier et les ancres s'arrêtent au-dessus d'elle : un
          élément focalisé n'est jamais masqué par elle (WCAG 2.4.11). */
    html.has-cta-bar {
      scroll-padding-bottom: calc(var(--cta-bar-height) + env(safe-area-inset-bottom));
    }
    /* 2. Le bouton retour en haut et la pile de raccourcis remontent
          ensemble au-dessus d'elle, sans aucun recouvrement. Par
          `translate`, propriété distincte du `transform` d'apparition : la
          remontée s'anime sans toucher à la mise en page, et le script la
          lit pour calculer le ton du verre (il ignore `transform`). */
    html.has-cta-bar :is(.to-top, .quick) {
      translate: 0 calc((var(--cta-bar-height) + env(safe-area-inset-bottom)
        + var(--cta-bar-to-top-gap) - var(--to-top-offset)) * -1);
    }

    /* 3. Le rond « rendez-vous » ferait doublon : sur une page qui a une
          barre d'appel, soit la barre est là, soit la page montre déjà son
          propre bouton de rendez-vous (bandeau d'appel, contact, pied).
          Retiré du flux, il ne laisse aucun trou : la pile est ancrée par
          le bas. Sans :has(), repli sur l'état de la barre — le rond ne
          disparaît alors que pendant qu'elle est affichée. Deux règles
          distinctes : un :has() inconnu invaliderait toute la liste. */
    html.has-cta-bar [data-quick-booking] { display: none; }
    html:has([data-cta-bar]) [data-quick-booking] { display: none; }
  }

  /* ═══════════════════════════ Invitation à lire les Conseils (accueil) ══
     Carte flottante en verre « liquid glass », proposée une fois quand le visiteur a
     dépassé la FAQ. NON bloquante : aucun voile, aucun verrou de défilement,
     la page reste lisible et cliquable autour d'elle.

     Le « quand » appartient au script : il retire `hidden`, pose
     `.is-visible` à l'image suivante, et `has-invite` sur <html> tant que
     la carte est affichée. À la fermeture, il retire `.is-visible` et
     attend la fin de la transition d'OPACITÉ de `.invite` pour remettre
     `hidden` (les boutons internes ont leurs propres transitions, qui
     remontent aussi : filtrer sur la cible et sur `propertyName`).

     Placement sans media query : largeur et marges se déduisent de
     l'écran. Sur téléphone la carte occupe toute la largeur moins 16 px
     de chaque côté ; à partir de 408 px de large, elle plafonne à 360 px
     et se range dans le coin bas-droit. */
  .invite[hidden] { display: none; }

  .invite {
    position: fixed;
    inset-inline-end: var(--invite-offset);
    inset-block-end: calc(var(--invite-offset) + env(safe-area-inset-bottom));
    z-index: var(--layer-floating);
    inline-size: min(100% - var(--invite-offset) * 2, var(--invite-width));
    /* État de départ : légèrement plus bas et plus petit, transparent */
    opacity: 0;
    transform: translateY(var(--invite-shift)) scale(var(--invite-scale));
    transform-origin: right bottom;
    pointer-events: none;
    /* --invite-tone, écrit par le script, est transitionné ici, là où il
       est posé : ses descendants héritent de la valeur interpolée. */
    transition: opacity var(--invite-duration) var(--ease-out),
      transform var(--invite-duration) var(--ease-out),
      translate var(--duration-base) var(--ease-out),
      --invite-tone var(--glass-tone-duration) var(--glass-tone-easing);
  }
  .invite.is-visible {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  /* La carte : le verre partagé (« Matériau verre », plus haut), coins
     signature. Elle verse son --invite-tone (nom historique, écrit par le
     script sur .invite et interpolé là) dans --glass-tone : la recette
     commune ne lit qu'une variable. Repli à 0 si --invite-tone n'est pas
     encore écrit et que @property manque (sinon couleurs invalides).
     Ses teintes propres basculent sur la même plage courte que l'encre
     (--_glass-mix). Seuls les états finis (0 et 1) sont mesurés AA ; le
     script ne laisse jamais la carte au repos entre les deux. */
  .invite__card {
    --glass-tone: var(--invite-tone, 0);
    --_accent: color-mix(in oklab, var(--text-accent), var(--brand-mint-300) var(--_glass-mix));
    --_dot: color-mix(in oklab, var(--brand-mint-500), var(--brand-mint-300) var(--_glass-mix));
    --_dot-halo: color-mix(in oklab, var(--invite-dot-halo-color),
      var(--invite-dot-halo-color-dark) var(--_glass-mix));
    --_close-hover: color-mix(in oklab, var(--invite-close-hover),
      var(--invite-close-hover-dark) var(--_glass-mix));

    position: relative;
    display: grid;
    gap: var(--space-2xs);
    padding: var(--invite-padding);
    border-radius: var(--invite-radius);
  }

  /* Sur-titre : capitales accentuées précédées d'une pastille.
     Marge à droite pour ne jamais passer sous le bouton de fermeture. */
  .invite__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding-inline-end: var(--invite-close-size);
    color: var(--_accent);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
  }
  .invite__eyebrow::before {
    content: '';
    flex: none;
    inline-size: var(--invite-dot-size);
    block-size: var(--invite-dot-size);
    border-radius: var(--radius-pill);
    background-color: var(--_dot);
    box-shadow: 0 0 0 var(--invite-dot-halo-spread) var(--_dot-halo);
  }

  /* L'accroche : ce qui se lit en une seconde. Corps du chapô, jamais
     réduit pour faire tenir une phrase (voir capacité dans le rapport). */
  .invite__hook {
    margin: 0;
    color: var(--_ink);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
  }

  /* Les deux issues : l'article (bouton), ou la vitrine (lien discret).
     Elles passent l'une sous l'autre d'elles-mêmes si la place manque.
     Le bouton reste vert de marque dans les deux états. */
  .invite__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs) var(--space-md);
    margin: var(--space-2xs) 0 0;
  }
  .invite__cta {
    padding: var(--button-padding-y-sm) var(--button-padding-x-sm);
  }
  .invite__all {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--invite-close-size);
    color: var(--_accent);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    text-decoration: underline;
    text-decoration-thickness: var(--stroke-hairline);
    text-underline-offset: var(--space-3xs);
  }
  .invite__all:hover { color: var(--_ink); }

  /* Fermer : cible de 44 px dans le coin, croix au ton du titre — le plus
     contrasté des deux états. */
  .invite__close {
    position: absolute;
    inset-block-start: var(--space-2xs);
    inset-inline-end: var(--space-2xs);
    display: grid;
    place-items: center;
    inline-size: var(--invite-close-size);
    block-size: var(--invite-close-size);
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background-color: transparent;
    color: var(--_ink);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-out);
  }
  .invite__close:hover { background-color: var(--_close-hover); }
  .invite__close:focus-visible { border-radius: var(--radius-pill); }

  /* Anneau double, comme les ronds. Sur le verre, l'anneau vert foncé
     suffirait ; mais ceux de la croix et du lien « Tous les conseils »
     débordent des grands coins arrondis de la carte, sur la page — qui
     peut être le pied de page ou une photo. Mesuré : sans intervalle,
     coin de l'anneau du lien sur la barre d'appel bleu nuit (390 px),
     moins de 2 contre 1. L'intervalle blanc ne se voit pas sur le verre,
     presque blanc ; il ne travaille que là où l'anneau sort. Le bouton
     garde son ombre propre derrière. */
  :is(.invite__all, .invite__close):focus-visible {
    box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-gap-color);
  }
  .invite__cta:focus-visible {
    box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-gap-color),
      var(--_shadow);
  }
  .invite__close .icon {
    inline-size: var(--icon-size-xs);
    block-size: var(--icon-size-xs);
  }

  /* Sans color-mix() : retour à la bascule franche. Le script pose
     `.is-on-dark` dès que le ton atteint 0.5 ; l'encre et la fumée sont
     traitées par le matériau commun, les teintes propres à la carte ici.
     Chacune change d'un bloc et s'anime en fondu, comme la fumée. Placé
     après toutes les règles de la carte : à spécificité égale, il
     l'emporte sur elles. */
  @supports not (color: color-mix(in oklab, red, blue)) {
    .invite__card {
      --_accent: var(--text-accent);
      --_dot: var(--brand-mint-500);
      --_dot-halo: var(--invite-dot-halo-color);
      --_close-hover: var(--invite-close-hover);
    }
    .invite.is-on-dark .invite__card {
      --_accent: var(--brand-mint-300);
      --_dot: var(--brand-mint-300);
      --_dot-halo: var(--invite-dot-halo-color-dark);
      --_close-hover: var(--invite-close-hover-dark);
    }
    .invite__eyebrow,
    .invite__hook,
    .invite__all {
      transition: color var(--glass-tone-fallback-duration) var(--ease-out);
    }
    .invite__eyebrow::before {
      transition: background-color var(--glass-tone-fallback-duration) var(--ease-out),
        box-shadow var(--glass-tone-fallback-duration) var(--ease-out);
    }
    .invite__close {
      transition: color var(--glass-tone-fallback-duration) var(--ease-out),
        background-color var(--duration-fast) var(--ease-out);
    }
  }

  /* Cohabitation avec les autres éléments fixés en bas d'écran.
     1. Le bouton retour en haut occupe le même coin : il s'efface tant
        que la carte est là, et revient dès qu'elle est fermée. Même
        idiome que son propre masquage (visibility retardée). */
  html.has-invite .to-top {
    --_hide-delay: var(--duration-base);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }

  /* 2. Téléphone : la barre d'appel occupe le bas de l'écran. La carte
        remonte au-dessus d'elle par `translate` (distinct du `transform`
        d'apparition), qui s'anime si la barre paraît ou s'efface pendant
        que la carte est affichée. Seuil apparié à celui de .cta-bar. */
  @media (max-width: 480px) {
    html.has-cta-bar .invite {
      translate: 0 calc((var(--cta-bar-height) + var(--cta-bar-to-top-gap)
        - var(--invite-offset)) * -1);
    }
  }

  /* ═══════════════════════════════════════ Pastilles de carrousel ═════
     Sous une rangée de cartes qu'on fait défiler (services, témoignages,
     sur téléphone) : où l'on en est, et un accès direct à chaque carte.
     Créées par le script (module `carousel`), jamais écrites dans le
     balisage : sans script, la rangée reste parcourable au doigt, et il
     n'y a pas de pastilles mortes.

     CONTRAT AVEC LE SCRIPT
       .carousel-dots ......... conteneur, posé juste après la rangée
       .carousel-dots__dot .... un <button> par carte
       [aria-current='true'] .. la carte affichée
       .is-near ............... ses deux voisines

     UNE CIBLE DE 44 PX, UN DISQUE DE 6 À 12 PX. Le bouton garde la cible
     tactile minimale ; le disque est dessiné par ses pseudo-éléments. Deux
     calques superposés, gris dessous, vert dessus : la taille passe par
     `scale`, la couleur par l'opacité du calque vert — rien d'autre ne
     s'anime. Contrastes des deux tons : voir --carousel-dot-color. */
  .carousel-dots {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    /* La cible déborde du disque de (44 − 12) / 2 = 16 px de chaque côté.
       On la remonte d'autant : l'écart VISIBLE entre les cartes et les
       pastilles reste celui que la rangée laisse après elle, et la cible
       empiète sur ce blanc, jamais sur les cartes. */
    margin-block-start: calc((var(--carousel-dot-size) - var(--carousel-dot-hit)) / 2);
  }
  .carousel-dots__dot {
    display: grid;
    place-items: center;
    width: var(--carousel-dot-hit);
    height: var(--carousel-dot-hit);
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    cursor: pointer;
  }
  .carousel-dots__dot::before,
  .carousel-dots__dot::after {
    content: '';
    grid-area: 1 / 1;
    width: var(--carousel-dot-size);
    height: var(--carousel-dot-size);
    border-radius: 50%;
    transform: scale(var(--_scale, var(--carousel-dot-scale-rest)));
    transition: transform var(--duration-fast) var(--ease-out),
      opacity var(--duration-fast) var(--ease-out);
  }
  .carousel-dots__dot::before { background-color: var(--carousel-dot-color); }
  .carousel-dots__dot::after {
    background-color: var(--carousel-dot-color-active);
    opacity: 0;
  }
  .carousel-dots__dot.is-near { --_scale: var(--carousel-dot-scale-near); }
  .carousel-dots__dot[aria-current='true'] { --_scale: 1; }
  .carousel-dots__dot[aria-current='true']::after { opacity: 1; }

  /* Anneau double des ronds flottants : l'intervalle blanc sous l'anneau
     vert foncé. Ici la pastille est toujours posée sur un fond clair, mais
     c'est le même objet — un rond qu'on vise — et il garde le même
     anneau. Le contour suit la cible de 44 px, pas le disque dessiné. */
  .carousel-dots__dot:focus-visible {
    border-radius: 50%;
    box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-gap-color);
  }

  /* Mouvement réduit : la pastille change d'état sur place, sans
     transition. Le token de durée tombe déjà à 1 % ; on retire la
     transition elle-même, comme le demande la règle du lot. */
  @media (prefers-reduced-motion: reduce) {
    .carousel-dots__dot::before,
    .carousel-dots__dot::after { transition: none; }
  }

  /* Hors téléphone, les cartes ne sont plus en rangée : des pastilles n'y
     désigneraient rien. Le script ne les crée pas ; ceci couvre le passage
     d'une largeur à l'autre sans rechargement. */
  @media (min-width: 48rem) {
    .carousel-dots { display: none; }
  }

  /* ═════════════════════════════════════════════════ Motif décoratif ══
     Trame de points vert pâle, en fondu sur les bords, avec une lente
     dérive. Purement décoratif : `aria-hidden` côté HTML. */
  .dot-pattern {
    position: absolute;
    inset: -10%;
    background-image: var(--dots-pattern);
    background-size: var(--dots-size) var(--dots-size);
    opacity: var(--dots-opacity);
    -webkit-mask-image: var(--dots-mask);
    mask-image: var(--dots-mask);
    animation: dot-drift var(--dots-drift-duration) var(--ease-linear) infinite;
    pointer-events: none;
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  5. SECTIONS — habillage spécifique                                      ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer sections {

  /* ─────────────────────────────────────────────── Barre de contact ──── */
  .topbar {
    background-color: var(--surface-inverse);
    color: var(--text-on-dark-muted);
    font-size: var(--text-sm);
  }
  .topbar__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    min-height: var(--topbar-height);
  }
  .topbar__mail {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    transition: var(--transition-colors);
  }
  .topbar__mail:hover { color: var(--brand-mint-300); }

  .socials { display: flex; gap: var(--space-3xs); }
  .socials__link {
    display: grid;
    place-items: center;
    width: var(--social-size);
    height: var(--social-size);
    border-radius: var(--radius-pill);
    transition: var(--transition-colors), var(--transition-transform);
  }
  .socials__link:hover {
    background-color: var(--overlay-white-hover);
    color: var(--brand-mint-300);
    transform: translateY(calc(var(--shift-3xs) * -1));
  }
  .socials__link .icon { width: var(--icon-size-xs); height: var(--icon-size-xs); }

  /* ────────────────────────────────────────────────────── En-tête ────── */
  .header {
    position: sticky;
    inset-block-start: 0;
    z-index: var(--layer-sticky);
    background-color: var(--header-bg);
    backdrop-filter: saturate(var(--header-saturate)) blur(var(--header-blur));
    -webkit-backdrop-filter: saturate(var(--header-saturate)) blur(var(--header-blur));
    border-bottom: var(--stroke-hairline) solid transparent;
    transition: border-color var(--duration-base) var(--ease-out),
      box-shadow var(--duration-base) var(--ease-out);
  }
  /* En vue bureau le bouton porte son libellé en clair : l'icône ferait
     doublon et volerait de la place à un en-tête déjà chargé. */
  .header__cta-icon { display: none; }

  /* Classe posée par le JS dès que la page défile */
  .header.is-stuck {
    border-bottom-color: var(--border-subtle);
    box-shadow: var(--shadow-sm);
  }
  /* Menu déroulé en haut de page : l'en-tête prend le même relief, sinon le
     panneau semble flotter sans attache. Déduit de l'état du menu, sans
     classe supplémentaire à poser côté script. */
  @supports selector(:has(*)) {
    .header:has(.nav.is-open) {
      border-bottom-color: var(--border-subtle);
      box-shadow: var(--shadow-sm);
    }
  }
  .header__inner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    min-height: var(--header-height);
  }
  /* Un logo ne se comprime pas : la marge automatique pousse le reste vers
     la droite, et `flex: none` garantit qu'en cas de place comptée c'est la
     navigation qui se resserre, jamais l'identité. */
  .header__brand { flex: none; margin-inline-end: auto; }
  /* Une seule dimension imposée : la largeur. La hauteur vient du ratio du
     fichier via le `height: auto` du reset, et `object-fit` n'a donc plus
     rien à corriger — la boîte épouse déjà l'image. Les attributs
     width/height du balisage continuent de réserver la place avant le
     chargement. */
  .header__brand img {
    width: var(--logo-width);
    transition: var(--transition-transform);
  }
  .header__brand:hover img { transform: scale(var(--scale-hover)); }

  /* Le <nav> n'est qu'une enveloppe : son rôle est de porter le repli en
     panneau sous le palier mobile. C'est la LISTE qui aligne les liens, et
     c'est donc elle qui porte l'espacement entre eux — en vue bureau comme
     dans le panneau déroulé. */
  .nav { display: flex; align-items: center; }
  .nav__list {
    display: flex;
    align-items: center;
    gap: var(--space-fluid-sm);
  }
  /* Le balisage contient deux appels à l'action. Celui-ci n'existe que pour
     le panneau mobile ; en vue bureau il est masqué et c'est celui de
     l'en-tête qui porte le libellé.

     ⚠ CE COMMENTAIRE A DIT « JAMAIS LES DEUX À LA FOIS » JUSQU'À
     AUJOURD'HUI, ET C'ÉTAIT FAUX DEPUIS 2b530b9. Ce commit a remplacé le
     masquage du bouton d'en-tête sous 861 px par une réduction en pastille
     ronde : celui-ci ne disparaît donc plus, et menu OUVERT les deux sont
     visibles ensemble. Mesuré à 390 px : la pastille 44 × 44 dans la barre,
     le bouton 350 × 45,7 dans le panneau. Ils portent le MÊME nom
     accessible, « Réserver un appel découverte », qu'un lecteur d'écran
     annonce donc deux fois dans le même « banner ».
     Ce n'est pas un manquement — 2.4.4 est satisfait, le nom décrit bien la
     destination — mais c'est une redondance, et elle est assumée ici tant
     qu'aucune décision n'a été prise. Ne pas réécrire ce paragraphe en
     « jamais les deux » sans avoir d'abord mesuré, menu ouvert. */
  .nav > .button { display: none; }
  .nav__link {
    position: relative;
    padding-block: var(--space-3xs);
    color: var(--text-body);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    white-space: nowrap;
    transition: var(--transition-colors);
  }
  /* Soulignement qui se déploie depuis la gauche */
  .nav__link::after {
    content: '';
    position: absolute;
    inset-inline-start: 0;
    inset-block-end: 0;
    width: 100%;
    height: var(--stroke-base);
    border-radius: var(--radius-pill);
    background-color: var(--brand-mint-500);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--duration-fast) var(--ease-out);
  }
  /* Lien actif : `true` est posé par le suivi de section de l'accueil,
     `page` par une page autonome qui se désigne elle-même (la FAQ).
     Le sélecteur reste limité à `.nav__link` : les liens du pied de page
     qui portent aussi `aria-current="page"` gardent leur rendu courant. */
  .nav__link:hover,
  .nav__link:is([aria-current='true'], [aria-current='page']) { color: var(--text-accent); }
  .nav__link:hover::after,
  .nav__link:is([aria-current='true'], [aria-current='page'])::after { transform: scaleX(1); }

  .header .button {
    flex: none;
    padding: var(--button-padding-y-sm) var(--button-padding-x-sm);
    font-size: var(--text-sm);
  }

  /* ── La loupe ───────────────────────────────────────────────────────────
     UNE COMMANDE NEUTRE, DE LA MÊME FAMILLE QUE LE BURGER. Les deux sont
     blanches, cernées d'un trait de commande et de même taille : elles se
     lisent comme une paire d'outils, face à la pastille verte qui, elle,
     porte l'action commerciale. Le rayon est celui du burger, pas le rond
     de la pastille — c'est ce qui les range du bon côté.

     `flex: none` POUR LA MÊME RAISON QUE SUR LE BURGER : sans lui, la loupe
     serait le seul élément compressible de la ligne et paierait pour tout
     le monde. Mesuré avant réglage, la loupe non stylée occupait 16,5 px de
     large à 861 px de fenêtre — elle avait déjà cédé.

     Le libellé est un `.visually-hidden` posé dans le balisage : il ne
     participe donc pas à la boîte, mais reste annoncé. La grille centre
     l'icône sans que ce libellé ne la décale. */
  .header__search {
    flex: none;
    display: grid;
    place-items: center;
    width: var(--header-search-size);
    height: var(--header-search-size);
    padding: 0;
    border: var(--stroke-hairline) solid var(--border-control);
    border-radius: var(--radius-sm);
    background-color: var(--surface-raised);
    color: var(--text-strong);
    cursor: pointer;
    transition: var(--transition-colors), var(--transition-transform);
  }
  .header__search .icon {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }
  .header__search:hover {
    border-color: var(--border-accent);
    color: var(--text-accent);
  }
  /* La loupe s'enfonce au clic — même retour que les autres commandes */
  .header__search:active { transform: scale(var(--scale-press)); }
  /* Fenêtre ouverte : la loupe reste allumée. Sans cela, rien ne relie plus
     la fenêtre au bouton qui l'a ouverte une fois le voile posé. */
  .header__search[aria-expanded='true'] {
    border-color: var(--border-accent);
    color: var(--text-accent);
    background-color: var(--surface-accent-soft);
  }

  /* Bouton menu mobile — masqué au-dessus du point de rupture */
  /* Le bouton est une grille de trois rangées serrées au centre.
     `align-content: center` est indispensable : sans lui les rangées
     s'étirent pour remplir le bouton et l'écart réel n'a plus aucun rapport
     avec le token — c'était le défaut d'origine. */
  .burger {
    display: none;
    /* UNE LARGEUR POSÉE N'EST PAS UNE LARGEUR TENUE.
       `--burger-size` vaut 44 px, la cible tactile visée par le projet,
       mais cette valeur n'est qu'une BASE tant qu'un enfant de flex peut
       se comprimer. Le logo et les boutons de l'en-tête portent `flex:
       none` ; le burger ne l'avait pas, il était donc le seul élément de la
       ligne capable de céder, et il cédait pour tout le monde. Mesuré sur
       les quatre pages : 38 × 44 à 320 px, et 22 × 44 à 300 px — sous les
       24 px exigés par WCAG 2.5.8, sur le seul bouton qui ouvre le menu.
       Le zoom à 400 %, que WCAG 1.4.10 impose de servir, ramène un écran
       de 1280 px à exactement 320 px de large : ce cas n'a rien d'un cas
       limite.
       CE RÉGLAGE NE VA PAS SEUL. Rendre le burger incompressible ne crée
       pas la place qui manque, il déplace seulement le débordement vers le
       document. C'est le palier « petit mobile » plus bas dans ce fichier
       qui la libère, en resserrant l'écart de l'en-tête. Retirer l'un des
       deux ramène le défaut sous une autre forme. */
    flex: none;
    place-items: center;
    align-content: center;
    gap: var(--burger-bar-gap);
    width: var(--burger-size);
    height: var(--burger-size);
    padding: 0;
    /* Contour de commande : c'est lui qui délimite la cible tactile */
    border: var(--stroke-hairline) solid var(--border-control);
    border-radius: var(--radius-sm);
    background-color: var(--surface-raised);
    cursor: pointer;
  }
  .burger__bar {
    display: block;
    width: var(--burger-bar-width);
    height: var(--stroke-base);
    border-radius: var(--radius-pill);
    background-color: var(--text-strong);
    transition: var(--transition-transform), opacity var(--duration-fast) var(--ease-out);
  }
  /* Transformation en croix. Les 45° sont la géométrie de la croix
     elle-même, ils ne relèvent d'aucun réglage de charte.

     ⚠ On compte parmi les BARRES, pas parmi les enfants du bouton : le
     premier enfant est le libellé destiné aux lecteurs d'écran. Un simple
     :nth-child(1) désignait ce libellé, et les trois règles se décalaient
     d'un cran — le trait du haut disparaissait, celui du milieu partait en
     diagonale et celui du bas ne bougeait pas. Il n'y avait pas de croix
     du tout. `of .burger__bar` ancre le comptage sur les traits. */
  .burger[aria-expanded='true'] .burger__bar:nth-child(1 of .burger__bar) {
    transform: translateY(var(--burger-bar-shift)) rotate(45deg);
  }
  .burger[aria-expanded='true'] .burger__bar:nth-child(2 of .burger__bar) {
    opacity: 0;
  }
  .burger[aria-expanded='true'] .burger__bar:nth-child(3 of .burger__bar) {
    transform: translateY(calc(var(--burger-bar-shift) * -1)) rotate(-45deg);
  }

  /* ──────────────────────────────────────────────────────── Héros ────── */
  .hero { position: relative; }
  .hero__stage {
    position: relative;
    display: grid;
    align-items: center;
    min-height: var(--hero-min-height);
    border-end-end-radius: var(--radius-sweep);
    background-color: var(--surface-inverse-deep);
    overflow: clip;
  }
  .hero__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--hero-focal-point);
    /* Très lent zoom arrière : donne de la vie sans distraire */
    animation: slow-zoom var(--duration-hero-zoom) var(--ease-out) forwards;
  }
  .hero__stage::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--hero-overlay);
  }
  .hero__content {
    position: relative;
    z-index: var(--layer-raised);
    max-width: var(--width-hero-content);
    padding-block: var(--space-2xl);
    color: var(--text-on-dark);
  }
  .hero__content h1 { color: var(--text-on-dark); }
  .hero__content .eyebrow { color: var(--brand-mint-300); }
  .hero__content .eyebrow::before { background-color: var(--brand-mint-300); }
  .hero__text {
    max-width: var(--measure);
    margin-block: var(--space-sm) var(--space-md);
    color: var(--text-on-dark-muted);
    font-size: var(--text-lg);
  }

  /* Mention de réassurance, sous la rangée d'appels à l'action.
     Elle ne s'adresse qu'aux boutons qu'elle qualifie : elle reprend donc
     l'écart INTERNE de la rangée (le `gap` de .cluster) plutôt que le rythme
     de paragraphe de la couche `base`, et se lit comme la dernière ligne du
     groupe au lieu de flotter en paragraphe autonome. Sans cette marge elle
     serait collée aux boutons — aucune règle de rythme ne s'applique après
     un <div>.
     Plus petite taille du texte courant, interligne resserré : moins
     appuyée que les boutons, plus discrète que l'accroche, lue sans effort.
     ⚠ EN BLANC PLEIN (--text-on-dark), PAS AU TON DU CHAPÔ — 1.9.0.2. Le
     ton atténué (--text-on-dark-muted) descendait à 4,38:1 sur 1,2 % de la
     photo à 375 px : sous le seuil AA pour un texte de 14 px. La discrétion
     vient de la taille, plus de la couleur. */
  .hero__reassurance {
    margin-block-start: var(--space-xs);
    color: var(--text-on-dark);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
  }

  /* Carte de chiffres qui chevauche le bas du héros.
     Elle est déclarée conteneur : ses chiffres se réorganisent selon la
     place dont ELLE dispose (le chevauchement négatif et la marge du
     gabarit la rendent souvent plus étroite que l'écran ne le laisse
     croire). */
  .hero__card {
    container-type: inline-size;
    container-name: hero-card;

    position: relative;
    z-index: var(--layer-raised);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-fluid-lg);
    max-width: var(--width-hero-card);
    margin-block-start: var(--hero-card-overlap);
    padding: var(--space-fluid-md);
    border-radius: var(--radius-signature-lg);
    background-color: var(--surface-inverse);
    box-shadow: var(--shadow-lg);
    color: var(--text-on-dark);
  }
  .hero__figure { text-align: center; }
  .hero__figure .metric { color: var(--brand-mint-300); }
  .hero__divider {
    align-self: stretch;
    width: var(--stroke-hairline);
    background-color: var(--border-on-dark);
  }
  .hero__claim {
    flex: 1 1 var(--hero-claim-min);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    text-wrap: balance;
  }
  /* Carte étroite : le filet séparateur disparaît et l'accroche prend
     toute la largeur plutôt que de se réduire à deux mots par ligne. */
  @container hero-card (max-width: 30rem) {
    .hero__divider { display: none; }
    .hero__claim { flex-basis: 100%; }
  }

  /* ─── Héros compact (pages secondaires) ──────────────────────────────
     Le héros de l'accueil, ramené à sa hauteur de contenu — environ
     300 px à 1280 — au lieu des 460 à 680 px de --hero-min-height. Même
     photo plein cadre, même arrondi de fuite ; seuls changent la hauteur,
     le voile (plus dense, voir --hero-compact-overlay) et la mise au point,
     calée sur le visage dans une bande désormais très basse. */
  .hero--compact .hero__stage { min-height: 0; }
  .hero--compact .hero__stage::after { background: var(--hero-compact-overlay); }
  .hero--compact .hero__image { object-position: var(--hero-compact-focal-point); }
  .hero--compact .hero__content { padding-block: var(--hero-compact-padding-y); }
  /* Le chapô et le sommaire forment un seul bloc : l'écart interne d'un
     groupe (16 px) et non celui qui précède des boutons (24 px). Ces 8 px
     comptent : à 375 × 667, la première question n'a que 13 px de marge
     avant la ligne de flottaison. (Son apparition au défilement, elle, a
     été retirée dans le HTML : aucun écart ne suffisait à la déclencher
     sans geste — voir questions-frequentes.html, premier groupe.) */
  .hero--compact .hero__text { margin-block-end: var(--space-sm); }
  /* La section qui suit reprend un écart de bloc, pas un rythme de section
     entier : à 375 px, c'est ce qui garde la première question au-dessus
     de la ligne de flottaison. */
  .hero--compact + .section { padding-block-start: var(--hero-compact-follow-gap); }

  /* En bureau (1.5.0, piste A) : le héros retrouve une hauteur de héros
     (--hero-compact-min-height), et son contenu s'aligne sur la colonne
     des questions — même bord gauche, même largeur maximale que `.faq`.
     Le titre, le chapô et le sommaire tombent ainsi à l'aplomb de ce
     qu'ils annoncent. Seuil apparié à celui des étapes (861 px) : sous
     lui, rien ne change, et la marge de 13 px mesurée à 375 × 667 reste
     intacte. */
  @media (min-width: 861px) {
    .hero--compact .hero__stage { min-height: var(--hero-compact-min-height); }
    .hero--compact .hero__content {
      max-width: var(--width-faq);
      margin-inline: auto;
    }
  }

  /* ─── Sommaire des thèmes ─────────────────────────────────────────────
     Quatre liens portés par le composant `.button.button--ghost` — le même
     que « Découvrir mes services » sur l'accueil : aspect, survol, focus,
     enfoncement et reflet viennent de lui, rien n'est redéfini ici. Ce
     bloc ne règle que la DISPOSITION : deux par ligne tant que le sommaire
     est étroit — jamais trois et une orpheline —, puis en rangée à leur
     largeur naturelle dès qu'il a la place. C'est SA largeur qui décide,
     pas celle de l'écran : d'où le conteneur. */
  .theme-nav {
    container-type: inline-size;
    container-name: theme-nav;
  }
  .theme-nav__list {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--theme-nav-gap);
  }
  /* Chaque bouton remplit sa cellule et garde la cible tactile minimale.
     Sur deux rangées (téléphone), il prend le rembourrage vertical compact
     du même bouton dans l'en-tête : à pleine hauteur, les deux rangées
     repoussaient la première question sous la flottaison à 375 × 667
     (marge mesurée à −4 px, 6 px avec ce réglage). En rangée unique, il
     retrouve exactement la géométrie de l'accueil. */
  .theme-nav__link {
    display: flex;
    min-block-size: var(--theme-nav-link-height);
    padding-block: var(--button-padding-y-sm);
  }
  /* Nombre impair de pastilles (1.6.0.1) : la dernière, seule sur sa
     rangée, la prend en entier plutôt que de laisser une case vide. */
  .theme-nav__list > :nth-child(odd):last-child { grid-column: 1 / -1; }
  @container theme-nav (min-width: 34rem) {
    .theme-nav__list { display: flex; flex-wrap: wrap; }
    .theme-nav__link { padding-block: var(--button-padding-y); }
  }

  /* ────────────────────────────────────────── Chiffre mis en avant ──── */
  .metric {
    position: relative;
    /* Contexte d'empilement propre : le halo peut passer derrière le chiffre
       sans disparaître sous le fond de la section. */
    z-index: var(--layer-base);
    display: block;
    font-size: var(--text-display);
    font-weight: var(--weight-black);
    line-height: 1;
    letter-spacing: var(--tracking-tight);
    /* Empêche le texte de sauter pendant le comptage animé */
    font-variant-numeric: tabular-nums;
  }
  /* Halo du chiffre : posé derrière, il s'ouvre une seule fois à l'arrivée.
     Le nombre lui-même est compté par le script, on ne touche pas au texte. */
  .metric::after {
    content: '';
    position: absolute;
    z-index: -1;
    /* Cercle centré sur le chiffre, dimensionné en em. `translate` est une
       propriété distincte de `transform` : le centrage ne gêne donc pas la
       mise à l'échelle animée par les images-clés. */
    top: 50%;
    left: 50%;
    width: var(--metric-halo-size);
    height: var(--metric-halo-size);
    translate: -50% -50%;
    border-radius: 50%;
    background: var(--halo-accent);
    opacity: 0;
    pointer-events: none;
  }
  .metric__suffix {
    font-size: var(--metric-suffix-scale);
    vertical-align: super;
  }
  .metric__label {
    display: block;
    margin-top: var(--space-3xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    text-wrap: balance;
  }
  .hero__card .metric__label { color: var(--text-on-dark-muted); }

  /* ──────────────────────────────────────────── Photo encadrée ──────── */
  .figure {
    position: relative;
    aspect-ratio: var(--figure-ratio);
    border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-xl);
    background-color: var(--surface-alt);
    box-shadow: var(--shadow-md);
    overflow: clip;
  }
  .figure img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--duration-slower) var(--ease-out);
  }
  .figure:hover img { transform: scale(var(--scale-hover)); }

  /* ── Photo à la hauteur de son texte (septembre 2026) ─────────────────
     Depuis l'ajout du parcours, la colonne de texte de « À propos » mesure
     916 px à 1280 px quand la photo, tenue par --figure-ratio, n'en
     faisait que 631. La grille étire désormais ses deux colonnes à la même
     hauteur et la photo occupe toute la sienne ; `object-fit: cover`
     recadre sur le visage (--figure-about-focus).
     Contrairement à la FAQ, rien ne s'ouvre ni ne se ferme dans ce texte :
     un étirement suffit, sans hauteur figée. En pile (mobile), la rangée
     prend la hauteur de la figure elle-même, qui garde donc son format. */
  .split--stretch { align-items: stretch; }
  .split--stretch > .split__media {
    display: grid;
    /* La largeur de la colonne ne doit rien devoir à la photo : sans cette
       ligne, le format de la figure, appliqué à 916 px de haut, réclamait
       831 px de large et poussait le texte hors de la page. */
    contain: inline-size;
  }
  /* Le format passe de la figure à l'image. Posé sur la figure, il se
     reportait sur sa largeur dès qu'on lui imposait une hauteur (831 px
     réclamés à 1280). Sur l'image, il ne sert plus que de plancher : en
     pile, c'est lui qui fait la hauteur, comme avant ; en grille, la
     figure prend la rangée entière et l'image la remplit. */
  .split--stretch .figure {
    display: grid;
    aspect-ratio: auto;
    min-height: 100%;
  }
  .split--stretch .figure img {
    aspect-ratio: var(--figure-ratio);
    object-position: var(--figure-about-focus);
  }

  /* ─── Image d'appui ──────────────────────────────────────────────────
     DEUXIÈME GABARIT DE FIGURE, ET C'EST TOUT LE SUJET.
     .figure décrivait jusqu'ici une image PORTEUSE : une colonne entière,
     un cartouche à fond plein posé dessus, un argument à défendre. Les
     photos d'appui reprenaient ce gabarit faute d'en avoir un autre — même
     emprise, même poids, mais rien à porter. Une image qui occupe la place
     d'un argument sans en être un se lit comme une image plantée là.

     Quatre écarts, un seul principe : moins d'emprise, plus d'égards.
     ① UNE LARGEUR QUI NE CONCURRENCE PLUS LE TEXTE. Au plus 420 px, soit
        un peu plus du tiers de la page — contre 1 180 px auparavant, dix
        fois la surface. En colonne étroite (l'introduction des services)
        le `min()` la laisse simplement remplir sa colonne : une seule
        règle couvre les deux situations, sans media query.
     ② UN CARRÉ PLUTÔT QUE LE FORMAT DES FIGURES PORTEUSES. Les sources
        sont carrées : le gabarit épouse le fichier au lieu de le recadrer,
        donc ne coupe plus aucun visage.
     ③ ELLE EST DÉCALÉE, JAMAIS CENTRÉE. Une image centrée dans une colonne
        de texte en devient le sujet et coupe la lecture en deux ; poussée
        contre le bord de fuite, elle accompagne. C'est aussi le sens de
        lecture du site : les angles de la marque sont asymétriques, une
        image d'appui l'est aussi.
     ④ MOINS DE RELIEF. L'ombre passe à celle des blocs posés (--shadow-sm)
        et le contour prend la combinaison signature large : elle a l'air
        posée sur la page, pas détourée au-dessus.

     Ce que cette règle NE fait pas, et qui demanderait du balisage : mettre
     l'image À CÔTÉ de son texte. Les deux blocs qui l'entourent en pleine
     largeur sont des colonnes de prose centrées à 720 px sur 1 180 : il ne
     reste 230 px de chaque côté, pas de quoi loger 420. Un chevauchement
     mettrait donc du texte sur la photo hors de tout cartouche — ce qui est
     précisément ce qu'on s'interdit. Voir le rapport : la vraie intégration
     passe par .split, qui existe déjà. */
  .figure--solo {
    width: min(100%, var(--width-figure-solo));
    margin-inline-start: auto;
    aspect-ratio: var(--figure-solo-ratio);
    border-radius: var(--radius-signature-lg);
    box-shadow: var(--shadow-sm);
  }
  /* Image posée à GAUCHE : elle se cale sur le bord extérieur de la page,
     pas sur la gouttière intérieure — donc l'inverse de la règle générale.
     Cette règle vit ici et non auprès de .side-layout dans `layout` : une
     couche supérieure l'emporte sur la spécificité, et le
     `margin-inline-start: auto` ci-dessus l'écrasait silencieusement. La
     figure se retrouvait centrée dans sa colonne, à 143 px du bord au lieu
     de 0 — mesuré, pas supposé. */
  .side-layout--media-start > .figure--solo { margin-inline: 0 auto; }

  /* ── L'image accompagne une colonne beaucoup plus haute qu'elle ──────
     Demande du client : « pour la FAQ, ça doit être de la même hauteur que
     les questions ». Prise à la lettre, elle produit l'inverse de ce
     qu'elle vise : la colonne des questions mesure près de 2 100 px pour
     330 de large, et étirer une source carrée sur ce format ne montrerait
     plus qu'une lamelle de 141 px agrandie deux fois et demie — floue, et
     qui ne montre plus rien de reconnaissable.

     Ce que la demande vise, c'est que les deux colonnes SE RÉPONDENT au
     lieu qu'une petite image se perde en haut d'une grande colonne. On
     l'obtient autrement, et mieux : format portrait, puis l'image
     ACCOMPAGNE le défilement des questions. Elle occupe alors la colonne
     sur toute sa hauteur, sans un pixel de recadrage absurde.

     ── L'ARBITRAGE EST PROLONGÉ, PAS RÉVISÉ ────────────────────────────
     Neuvième question ajoutée, et le client revient : « l'image est un peu
     petite à droite ». Il a raison, et ce n'est pas ce format-ci qui est
     en cause — c'est la LARGEUR de la piste. Le format ne fait que
     traduire une largeur en hauteur : à 342 px de piste, 3/4 donne 457 px
     de haut, et rien d'autre ne pouvait en sortir.
     Trois pistes ont été mesurées avant de choisir.
     ① ÉTIRER LA PHOTO SUR LA COLONNE (height: 100% + cover). C'est la
        lecture littérale, déjà écartée plus haut, et la neuvième question
        l'a rendue PIRE : avec un seul dépli la colonne passe à 890 px pour
        une piste de 342, soit un rapport 0,38 — il ne resterait que 38 %
        de la largeur de la source. Surtout, le recadrage dépendrait de
        l'état des `details` : la photo se recadrerait sous les yeux à
        chaque question ouverte. Non.
     ② RELEVER --width-figure-solo. Sans effet ici, et c'est mesurable :
        le plafond de 420 px n'était pas atteint (piste 342), donc pas
        contraignant. Le relever n'aurait agrandi que la figure EMPILÉE
        sous 1002 px, la seule qui l'atteignait — soit l'inverse du besoin.
     ③ ÉLARGIR LA PISTE, ce qui est fait : --side-cols-media-tall, 1.15
        contre 2. La photo passe de 342 × 457 à 419 × 559 (+49 % de
        surface), le format 3/4 ne bouge pas, le recadrage reste celui du
        premier arbitrage — 12,5 % de chaque côté d'une source carrée, le
        sujet est à 43 % de la largeur conservée, il ne s'approche d'aucun
        bord. Repliée, la colonne mesure 570 px et la photo 559 : la
        demande d'origine, « la même hauteur que les questions », est
        satisfaite à 98 % sans qu'on ait eu à l'appliquer à la lettre.

     ⚠ LE PIÈGE DU COLLAGE NE S'APPLIQUE PAS ICI, et c'est vérifiable.
     Il avait mordu dans les services parce qu'un TROISIÈME enfant tombait
     en deuxième rangée de la colonne collante, sous un bloc qui ne le
     voyait pas. Ici la colonne d'image n'a qu'un seul enfant, et la
     colonne voisine appartient à une autre piste de la grille : rien ne
     peut se ranger dessous. Un élément collant est par ailleurs retenu par
     sa ZONE DE GRILLE, qui court sur toute la hauteur de la rangée — c'est
     ce qui lui donne sa course, malgré `align-items: start`. */
  .side-layout--media-end > .figure--solo {
    aspect-ratio: var(--figure-solo-ratio-tall);
    position: sticky;
    inset-block-start: calc(var(--header-height) + var(--space-lg));
  }
  /* ── Même hauteur que les questions repliées (septembre 2026) ────────
     Le 3/4 laissait la photo 105 px plus courte que la liste fermée
     (559 contre 664 à 1280 px). Octobre 2026 : la liste passe de douze à
     quatre questions, le jeton est remesuré (762 px de photo face à 253
     de questions avant correction) ; la photo devient paysage, la mise au
     point est donc désormais VERTICALE. Elle prend désormais la hauteur de la
     liste REPLIÉE, figée par un jeton : ouvrir une question allonge la
     liste, jamais la photo — ni saut ni recadrage sous les yeux, et le
     collage ci-dessus la garde en vue pendant la lecture des réponses.
     `object-fit: cover` (hérité de .figure img) recadre sans déformer ;
     la mise au point tient le visage à l'écart des bords.
     Le plafond en `svh` évite qu'une fenêtre basse rende la photo collée
     plus haute que l'espace sous l'en-tête ; la première déclaration sert
     de repli aux navigateurs qui ignorent cette unité. Le palier de
     repli (section 8) rend le format carré en pile. */
  .side-layout--media-end > .figure--solo {
    aspect-ratio: auto;
    height: var(--figure-solo-height-tall);
    height: min(var(--figure-solo-height-tall),
      calc(100svh - var(--header-height) - var(--space-lg) * 2));
  }
  .side-layout--media-end > .figure--solo img {
    object-position: var(--figure-solo-focus-tall);
  }

  /* Carte « nos valeurs » posée dans un angle de la photo */
  .figure__overlay {
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    width: min(80%, var(--width-figure-overlay));
    padding: var(--space-sm) var(--space-md);
    border-start-start-radius: var(--radius-lg);
    background-color: var(--surface-inverse);
    color: var(--text-on-dark);
  }
  /* Le titre du cartouche s'écrit de deux façons dans la page, et les deux
     sont justes. Un <h3> quand le cartouche prolonge le propos de sa
     section — c'est le cas de la carte de valeurs. Un <p class="…-title">
     quand il n'est qu'une mention posée sur une image d'appui : y mettre un
     titre de niveau 3 insérerait un intertitre dans le plan du document
     pour un contenu qui n'ouvre aucune partie, et un lecteur d'écran
     l'annoncerait comme une section à part entière.
     L'APPARENCE, elle, ne dépend pas de ce choix : elle est décrite une
     fois pour les deux. C'est la seule façon d'éviter qu'un cartouche sur
     deux ne se retrouve en texte courant sans que rien ne le signale —
     ce qui était le cas avant cette règle. */
  /* ⚠ AUCUNE MARGE BASSE ICI, ET C'EST LA CORRECTION D'UN DÉFAUT RÉEL.
     Elle y était, et elle ne servait à rien : entre le titre et la liste
     qui le suit, le rythme de flux de la couche `base` (`p + ul`,
     `:is(h1…h4) + *`) pose déjà 16 px, et les deux marges se fondent en
     une seule — la plus grande gagnait, la marge basse ne se voyait
     jamais. Elle ne se voyait que dans le cas où elle était fautive :
     titre SEUL dans le cartouche, ses 8 px pendaient sous la dernière
     ligne et déséquilibraient le rembourrage, 16 px en haut contre 24 en
     bas. On la retire plutôt que de l'annuler au cas par cas : une marge
     qui n'a d'effet que lorsqu'elle est de trop n'a pas à exister. */
  .figure__overlay :is(h3, .figure__overlay-title) {
    color: var(--text-on-dark);
    font-size: var(--text-base);
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
  }
  .value-list { display: grid; gap: var(--space-3xs); }
  /* La coche se cale sur la PREMIÈRE ligne, pas au milieu du bloc : ces
     entrées ne sont plus des mots isolés (« Organisation ») mais des
     phrases qui passent sur deux lignes, et une coche centrée se retrouvait
     entre les deux, à côté de rien. */
  .value-list li {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2xs);
    color: var(--text-on-dark-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }
  .value-list .icon {
    flex: none;
    width: var(--icon-size-xs);
    height: var(--icon-size-xs);
    margin-block-start: var(--value-list-icon-offset);
    color: var(--brand-mint-300);
  }

  /* Citation détourée, posée sur la photo */
  .pull-quote {
    position: absolute;
    inset-block-end: var(--pull-quote-inset-block);
    inset-inline-end: 0;
    width: min(80%, var(--width-pull-quote));
    padding: var(--space-sm) var(--space-md);
    border-inline-start: var(--stroke-thick) solid var(--brand-mint-500);
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    background-color: var(--surface-raised);
    box-shadow: var(--shadow-md);
  }
  /* La signature — nom puis fonction — n'est plus décrite ici : elle est
     commune aux deux citations du site et vit désormais dans `components`,
     sous « Citation attribuée ». Ne subsiste ci-dessous que ce qui est
     PROPRE à ce cartouche : la taille de légende de son texte, plus petite
     que celle des cartes de témoignages, parce qu'il est posé sur une image
     et non lu comme un contenu de section. */
  .pull-quote p {
    color: var(--text-body);
    font-size: var(--text-sm);
    font-style: italic;
  }

  /* ── Habillage d'une image d'appui ───────────────────────────────────
     Les deux cartouches du site — la carte de valeurs et la citation —
     restent les seuls habillages admis sur une photo : ils ont tous deux
     un FOND PLEIN, et c'est non négociable. Un texte posé à même une image
     ne peut pas voir son contraste garanti : il dépend du pixel qui se
     trouve dessous, que le banc de contrastes ne sait pas mesurer.
     Sur une image d'appui, ils se resserrent simplement : à 310 et 320 px
     ils couvriraient les trois quarts d'une image de 420. La citation
     abandonne aussi son débord — mais cela vaut maintenant pour les deux
     cartouches du site : voir la note de --pull-quote-offset.

     LA PROPORTION D'ABORD, LE PLAFOND ENSUITE. Un cartouche se juge à ce
     qu'il RECOUVRE, pas à sa largeur : c'est la surface cachée que l'œil
     perçoit. Les deux bornes et leur histoire sont documentées sur
     --figure-solo-overlay-part. */
  .figure--solo :is(.figure__overlay, .pull-quote) {
    width: min(var(--figure-solo-overlay-part), var(--width-figure-solo-overlay));
  }

  /* Le cartouche se pose du côté du BORD EXTÉRIEUR de la page, comme la
     figure elle-même. Ce n'est pas un réglage propre à une photo : c'est la
     composition qui le dicte — le cartouche s'adosse à la marge, et la
     photo s'ouvre du côté du texte auquel elle répond. Accessoirement,
     c'est ce qui libère le sujet, qui se tient rarement du côté du vide. */
  .side-layout--media-start .figure__overlay {
    inset-inline: 0 auto;
    border-start-start-radius: 0;
    border-start-end-radius: var(--radius-lg);
  }

  /* ─────────────────────────────────────────────── Bloc « piliers » ─── */
  .pillars {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min-xs), 100%), 1fr));
    gap: var(--space-md);
    margin-top: var(--space-md);
  }
  .pillar__title {
    display: block;
    margin-block: var(--space-2xs) var(--space-3xs);
    color: var(--text-strong);
    font-weight: var(--weight-semibold);
  }
  .pillar p { color: var(--text-muted); font-size: var(--text-sm); }

  /* ───────────────────────────────────────────────────── Services ───── */
  /* `clip` plutôt que `hidden` : voir le commentaire sur body. Un `hidden`
     ici rendait inerte toute animation pilotée par le défilement placée
     dans la section, la timeline s'accrochant à ce conteneur immobile. */
  .services { position: relative; overflow: clip; }
  /* La trame de points se resserre et se révèle quand la section entre dans
     l'écran. L'état vient de l'intérieur du bloc (l'introduction révélée),
     ce qui évite d'avoir à marquer la section elle-même dans le balisage. */
  @supports selector(:has(*)) {
    .services .dot-pattern {
      opacity: 0;
      transform: scale(var(--dots-enter-scale));
      transition: opacity var(--duration-slower) var(--ease-out),
        transform var(--duration-slower) var(--ease-out);
    }
    .services:has(.services__intro.is-revealed) .dot-pattern {
      opacity: var(--dots-opacity);
      transform: none;
    }
  }
  .services__layout {
    position: relative;
    display: grid;
    grid-template-columns:
      minmax(var(--grid-min-md), var(--side-cols-media)) var(--side-cols-text);
    /* Même gouttière que les autres mises en colonnes */
    gap: var(--space-fluid-lg);
    align-items: start;
  }
  .services__intro { position: sticky; top: calc(var(--header-height) + var(--space-lg)); }

  /* ── Ce qui se range SOUS la grille à deux colonnes ────────────────────
     UN BLOC COLLANT NE VOIT PAS CE QUI PASSE SOUS LUI. Celui-ci est retenu
     par son conteneur — la grille entière, pas sa seule rangée — il survole
     donc tout ce qui se range en deuxième rangée de SA colonne. La phrase
     de clôture y tombait par simple flux automatique : mesurée à 1648 px,
     elle était recouverte sur 75 de ses 79 px de haut, du début de la
     deuxième rangée jusqu'à la fin de la section. Invisible.

     Le remède n'est pas de l'écarter de quelques pixels — la collision
     reviendrait au premier paragraphe ajouté. Il est de RANGER CE TEXTE OÙ
     IL PARLE : une phrase qui répond aux cartes appartient à la colonne des
     cartes, pas à celle de l'introduction. La colonne du bloc collant lui
     est alors laissée libre, et il n'y a plus de collision possible, quel
     que soit le nombre de cartes ou de paragraphes.

     La règle vise TOUT paragraphe posé directement dans la grille, pas
     celui-ci en particulier : c'est la place de ce genre de texte qui est
     décidée ici, une fois pour toutes.

     Mieux encore, si le balisage peut bouger : sortir ce paragraphe de la
     grille pour le poser sous elle, dans la section. La grille n'aurait
     alors plus qu'une rangée, le collage se relâcherait à la fin des cartes
     — et l'appel à l'action de l'introduction quitterait l'écran avant que
     la clôture n'arrive, au lieu de dire la même chose à côté d'elle.
     Cette règle-ci devient sans objet ce jour-là : plus aucun <p> ne sera
     enfant direct de la grille. */
  .services__layout > p { grid-column: 2; }

  /* ── Colonne d'introduction au ras de la grille (1250 px et plus) ──────
     Mesuré avant correction à 1280 px : l'introduction et sa photo
     s'arrêtaient bien avant les six cartes, et le bloc collant laissait
     sous lui une colonne vide d'environ la hauteur d'une carte. La photo
     prend désormais la hauteur restante : la colonne s'étire sur la rangée
     de la grille, la figure en occupe le bas, et l'image la remplit en
     `cover` autour de --services-figure-focus. Les deux colonnes finissent
     sur la même ligne.
     Le collage disparaît avec : une colonne aussi haute que la grille n'a
     plus aucune course, et la garder collante n'aurait rien fait qu'à
     contretemps.
     Image en `absolute` : sa hauteur ne doit RIEN à la source, sans quoi le
     format du fichier pèserait sur la hauteur de la colonne qu'elle est
     censée remplir. La figure garde son carré comme plancher. */
  @media (min-width: 1250px) {
    .services__intro {
      position: static;
      align-self: stretch;
      display: flex;
      flex-direction: column;
    }
    .services__intro .figure--solo { flex: 1 0 auto; }
    .services__intro .figure--solo img {
      position: absolute;
      inset: 0;
      object-position: var(--services-figure-focus);
    }
  }

  /* ═══════════════════════════════════════ Bandeau de témoignages ═════
     SIX CITATIONS QUI DÉFILENT — ET QUI SE PARCOURENT AUSSI À LA MAIN.

     POURQUOI UNE BANDE ET NON UN CARROUSEL À FLÈCHES.
     Un carrousel range cinq témoignages sur six derrière une minuterie et
     deux boutons que presque personne n'actionne. Or toute la valeur d'un
     témoignage est d'être VU. La bande, elle, ne cache rien : tout finit
     par passer, et ce qui n'est pas passé se rattrape au doigt.
     Elle rend aussi un service de rythme : la section suit une grille de
     six cartes de services ; une bande horizontale après une grille qui
     empile change de forme au bon moment, et le lecteur ne voit que deux
     cartes à la fois là où une grille lui en poserait six d'un coup.

     TROIS MÉCANISMES, INDÉPENDANTS L'UN DE L'AUTRE — c'est ce qui fait
     qu'aucun ne peut emporter les deux autres en tombant :
       1. un conteneur de défilement natif (`overflow-x: auto`) : le doigt,
          la molette et les flèches du clavier fonctionnent, sans une ligne
          de JavaScript, et le balisage le rend focalisable (`tabindex`,
          `role=region`, `aria-label` — trois attributs à ne pas retirer) ;
       2. une animation CSS qui translate le rail : elle non plus ne dépend
          d'aucun script ;
       3. l'arrêt au survol et au focus, par `animation-play-state` — et,
          avec JavaScript, par un bouton qui le rend durable (voir
          .quote-track__control plus bas).
     Si l'un manque, la bande reste une bande de citations lisible et
     parcourable. Elle ne devient jamais un bloc vide.

     PREMIÈRE CARTE ENTIÈRE, COUPE AU BORD DE FUITE.
     La bande démarre à l'origine, donc sur une première carte complète :
     c'est la seule que verront ceux qui ne s'arrêtent pas. La coupe se
     produit à droite, où elle est justement le signal qui dit « ça
     continue ». Rien n'est tronqué pour autant : ce qui est coupé est le
     bord d'une carte, jamais une phrase — voir .card--quote. */
  .quote-track {
    /* ── RÉGLAGES DU MÉCANISME ─────────────────────────────────────────
       Le même mécanisme porte deux bandes : les témoignages, et les logos
       d'outils (.quote-track--logos). Tout ce qui les distingue passe
       par ces variables privées ; la variante ne redéfinit qu'elles, et
       jamais les trois mécanismes décrits plus haut. */
    --_item-width: var(--quote-card-width);
    --_gap: var(--quote-gap);
    --_loop-shift: var(--quote-loop-shift);
    --_loop-duration: var(--quote-loop-duration);
    --_reserve-block-end: var(--card-glow-reach-block-end);
    /* État du mouvement. Survol, focus et bouton pause l'écrivent ; seul
       le rail le lit. Trois raisons d'arrêter, une seule règle qui arrête. */
    --_play-state: running;

    /* Conteneur de dimension : la carte tire sa largeur de la bande, et la
       boucle sa distance. Tout le composant s'adapte sans media query. */
    container-type: inline-size;

    /* Ancrage doux du défilement manuel. Il est neutralisé pendant que la
       bande avance : deux mécanismes qui décident de la même position se
       tirent dessus, et c'est le lecteur qui voit le tremblement. */
    --_snap: x proximity;

    overflow-x: auto;
    /* Un conteneur de défilement l'est toujours dans les deux sens : on
       ferme explicitement le second. `clip` se résout ici en `hidden`,
       puisque l'autre axe défile. */
    overflow-y: clip;
    /* La bande arrivée en butée ne pousse pas la page — ni le geste de
       retour arrière des navigateurs à défilement horizontal. */
    overscroll-behavior-inline: contain;
    scroll-snap-type: var(--_snap);

    /* ── RÉSERVE DU HALO AU BAS DE LA BANDE ────────────────────────────
       Le halo de survol d'une carte descend de --card-glow-reach-block-end
       sous elle. La bande n'en offrait que --space-md : le halo était donc
       tranché net alors qu'il valait encore un cinquième de son intensité,
       et la coupe se lisait comme un trait blanc rectiligne sous la bande.
       Mesuré à 1280 px, au survol : marche de 63/765 au bord du rognage
       (rgb 223,241,238 d'un côté, blanc pur de l'autre) ; 101/765 en
       mouvement réduit, où la carte ne se soulève pas et perd donc les
       14 px d'air que lui donnait --card-lift.

       POURQUOI PAS `overflow-clip-margin`, QUI EST FAIT POUR ÇA.
       Parce qu'il ne s'applique qu'aux axes réellement en `clip`. Ici
       `overflow-x: auto` force `overflow-y` à se résoudre en `hidden`
       (vérifié : la propriété se calcule bien à 70px et ne produit rien).
       La seule surface de rognage négociable reste la boîte de padding.

       POURQUOI UNE MARGE NÉGATIVE.
       Parce que la place à prendre est une place de RENDU, pas une place
       de mise en page : le rythme de la section n'a aucune raison de
       changer pour qu'un halo tienne. On agrandit donc la boîte de
       rognage vers le bas, puis on reprend exactement autant en marge. La
       boîte de mise en page est au pixel près celle d'avant — la bande,
       le bouton qui suit et la hauteur de la section ne bougent pas.
       Seul le haut garde --space-md : le halo n'y remonte que de 10 px,
       auxquels --card-lift ajoute les 14 px que cette réserve couvre. */
    padding-block: var(--space-md) var(--_reserve-block-end);
    margin-block-end: calc(var(--space-md) - var(--_reserve-block-end));
  }

  .quote-track__rail {
    display: flex;
    /* Toutes les cartes prennent la hauteur de la plus haute : une bande à
       fond de cartes ragged se lirait comme un défaut, pas comme un choix.
       C'est le blanc SOUS les citations courtes qui absorbe l'écart de
       longueur — 102 à 170 signes — et il se lit comme de la respiration. */
    align-items: stretch;
    gap: var(--_gap);
    /* Le rail fait exactement la largeur de son contenu : c'est ce qui
       donne au conteneur une plage de défilement juste. */
    width: max-content;
  }

  .quote-track__rail > li {
    display: grid;      /* la carte remplit sa case, en largeur et hauteur */
    flex: none;         /* une largeur fixe : c'est elle qui fait le rythme */
    width: var(--_item-width);
    scroll-snap-align: start;
  }

  /* ── Le mouvement ────────────────────────────────────────────────────
     LA BOUCLE SUPPOSE UN SECOND JEU DE CARTES, CLONÉ DANS LE BALISAGE.
     Une bande de six cartes qui avance d'un jeu se retrouve, à l'arrivée,
     entièrement sortie de l'écran : il faut qu'un jeu identique occupe
     déjà la place pour que la reprise ne se voie pas. Les clones portent
     `aria-hidden="true"` — un lecteur d'écran doit continuer d'annoncer
     six témoignages, sinon le décompte du titre devient incompréhensible.

     C'est cette présence que le sélecteur ci-dessous vérifie. Tant que les
     clones ne sont pas là, la bande ne bouge pas : elle reste une bande
     lisible et parcourable au doigt. Une condition non remplie ne produit
     donc jamais un bandeau vide — elle produit l'état statique, qui est
     exactement l'état du mouvement réduit. */
  @media (prefers-reduced-motion: no-preference) {
    .quote-track__rail:has(> li[aria-hidden='true']) {
      animation: quote-drift var(--_loop-duration) var(--ease-linear) infinite;
      /* APRÈS le raccourci, qui remettrait sinon l'état à `running`. */
      animation-play-state: var(--_play-state);
      /* Seule exception du site à la règle « will-change pendant l'effet,
         puis on le retire » : l'effet, ici, ne s'arrête pas. Le poser
         permet au compositeur de garder la bande sur son propre calque. */
      will-change: transform;
    }
    .quote-track:has(> .quote-track__rail > li[aria-hidden='true']) {
      --_snap: none;
      /* Une translation vers la gauche rétrécit la zone défilable d'autant :
         la barre de défilement respirerait en continu sous la bande, ce qui
         se lit comme un défaut d'affichage. La coupe au bord de fuite et le
         mouvement lui-même disent déjà que la bande continue. Elle revient
         dès que la bande s'immobilise (mouvement réduit, ou clones
         absents), là où plus rien d'autre ne le dirait. */
      scrollbar-width: none;
    }
    .quote-track:has(> .quote-track__rail > li[aria-hidden='true'])::-webkit-scrollbar {
      display: none;
    }
  }

  /* Une citation qu'on commence à lire ne s'échappe pas. `:focus-within`
     couvre les deux cas d'un seul geste : la bande elle-même reçoit le
     focus (elle est focalisable), et n'importe quel élément qu'elle
     contiendrait un jour aussi. */
  .quote-track:hover,
  .quote-track:focus-within,
  .quote-track[data-paused] {
    --_play-state: paused;
  }

  /* ── Le bouton pause / lecture ───────────────────────────────────────
     Le survol n'existe pas au doigt, et le focus s'en va dès qu'on touche
     ailleurs : sur écran tactile, seul ce bouton arrête la bande pour de
     bon (WCAG 2.2.2). Il est créé par le script — module `marquee` — qui
     pose `data-paused` sur la bande ; sans JavaScript, il n'existe pas.
     Il suit IMMÉDIATEMENT la bande dans le balisage : c'est ce qui permet
     à l'icône de refléter l'état par le sélecteur `+`.
     Discret à dessein : la loupe de l'en-tête, sans son cadre au repos. */
  .quote-track__control {
    /* La bande des témoignages rend en marge négative la réserve de son
       halo : le bouton empiète donc sur cette boîte. Positionné, il est
       peint APRÈS le rail transformé, et reste au-dessus de lui. */
    position: relative;
    display: flex;
    justify-content: flex-end;
    margin-block-start: var(--space-2xs);
  }
  .quote-track__toggle {
    display: grid;
    place-items: center;
    width: var(--track-toggle-size);
    height: var(--track-toggle-size);
    padding: 0;
    border: var(--stroke-hairline) solid transparent;
    border-radius: var(--radius-sm);
    background-color: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: var(--transition-colors), var(--transition-transform);
  }
  .quote-track__toggle .icon {
    grid-area: 1 / 1;
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }
  .quote-track__toggle:hover {
    border-color: var(--border-accent);
    color: var(--text-accent);
  }
  .quote-track__toggle:active { transform: scale(var(--scale-press)); }

  /* Une seule icône à la fois : pause tant que la bande avance, lecture
     une fois qu'on l'a arrêtée. */
  .quote-track__icon-play { visibility: hidden; }
  .quote-track[data-paused] + .quote-track__control .quote-track__icon-play { visibility: visible; }
  .quote-track[data-paused] + .quote-track__control .quote-track__icon-pause { visibility: hidden; }

  /* Mouvement réduit : l'amplitude est déjà retombée à zéro dans
     tokens.v1.9.0.3.css, la bande ne bouge donc plus. Restent les clones, qui
     n'ont plus aucune raison d'être : à la main, ils feraient lire deux
     fois les mêmes six citations. On les retire de la page — donc aussi
     de l'arbre d'accessibilité — plutôt que de les laisser en doublon. */
  @media (prefers-reduced-motion: reduce) {
    .quote-track__rail > li[aria-hidden='true'] { display: none; }
    /* Une bande immobile n'a pas besoin d'un bouton pour l'arrêter. */
    .quote-track__control { display: none; }
  }

  /* Bouton réservé aux appareils sans survol : téléphone, tablette.
     Avec une souris, deux mécanismes arrêtent déjà la bande : le survol
     et le focus clavier de la région (`:hover` et `:focus-within`, plus
     haut). Ce sont eux qui satisfont le critère WCAG 2.2.2, et le bouton
     n'y ajoutait rien. Media query assumée : la capacité de survol ne se
     lit ni dans la largeur du conteneur ni dans une fonction de taille.
     Un appareil hybride (écran tactile et souris) déclare le plus souvent
     `hover: hover` sur son pointeur principal : il perd le bouton mais
     garde le survol, le focus clavier et le défilement au doigt. */
  @media (hover: hover) and (pointer: fine) {
    .quote-track__control { display: none; }
  }

  /* ════════════════════════════════════════════ Bandeau d'outils ═════
     LES LOGICIELS DU CLIENT, SUR LE MÊME MÉCANISME QUE LES TÉMOIGNAGES.
     Défilement natif, boucle CSS, arrêt au survol et au focus, retrait
     des clones en mouvement réduit : tout est hérité de .quote-track. La
     variante ne change que ses réglages privés.

     LA DISTANCE DE BOUCLE NE PEUT PAS SE COMPTER EN CARTES.
     Les logos n'ont pas de largeur commune : chacun garde les proportions
     de son dessin. La distance est donc lue sur le rail lui-même — la
     moitié de sa largeur, plus une demi-gouttière (voir le token
     --tool-loop-shift). Ajouter ou retirer un logo ne demande aucun
     recalcul, à condition de le faire dans les deux jeux.

     PAS DE HALO À RÉSERVER : un logo n'a ni ombre ni soulèvement. La
     réserve basse revient donc à l'espace ordinaire de la bande. */
  .quote-track--logos {
    --_item-width: auto;
    --_gap: var(--tool-gap);
    --_loop-shift: var(--tool-loop-shift);
    --_loop-duration: var(--tool-loop-duration);
    --_reserve-block-end: var(--space-md);
  }

  /* Un outil : son logo, son nom, ou les deux sur une ligne. Tous se
     calent sur une hauteur commune, et sur la teinte courante — c'est ce
     qui rend le bandeau monochrome, et qui suivra un futur thème sombre
     sans une ligne de plus : les tracés du sprite sont en currentColor.
     Contraste : text-muted sur surface ........ 5.46:1 (AA, texte courant ;
     les logos seuls relèvent du 3:1 des éléments graphiques). */
  .tool-mark {
    display: inline-flex;
    align-items: center;
    gap: var(--tool-inner-gap);
    color: var(--tool-color);
    white-space: nowrap;
  }

  /* Hauteur réglée, largeur déduite du viewBox recopié sur chaque <svg> :
     un logo large reste large, un symbole carré reste carré.
     État de base, et repli des navigateurs sans `pow()` ni `:has()` :
     tous à la hauteur de référence, comme avant l'équilibrage. */
  .tool-mark__logo {
    --_ratio: 1;
    block-size: var(--tool-logo-height);
    inline-size: auto;
    fill: currentColor;
  }

  /* ── Surface égale ───────────────────────────────────────────────────
     Chaque logo prend la hauteur qui lui donne à peu près la même surface
     que ses voisins : formule, pivot et bornes sont expliqués sur
     --tool-logo-exponent dans tokens.v1.9.0.3.css. Le nom écrit ne bouge
     pas : dans un couple symbole + nom, seul le symbole s'ajuste, et
     .tool-mark le recentre sur la ligne du nom.

     LE RAPPORT EST LU SUR LE SYMBOLE APPELÉ, pas sur une classe ajoutée :
     l'identifiant du fragment est déjà l'identité du logo, et la CSP du
     site (style-src 'self') interdit de le poser en attribut `style`.
     Un logo absent de cette liste reste carré (rapport 1).

     LA BOUCLE SUIT SANS RECALCUL : sa distance est lue en pourcentage du
     rail (--tool-loop-shift), qui s'élargit ou se resserre avec eux. */
  @supports (scale: pow(2, 0.5)) and selector(:has(> use)) {
    .tool-mark__logo {
      block-size: clamp(
        var(--tool-logo-height-min),
        var(--tool-logo-height) *
          pow(var(--tool-logo-pivot) / var(--_ratio), var(--tool-logo-exponent)),
        var(--tool-logo-height-max)
      );
    }
    .tool-mark__logo:has(> use[href$='#logo-payfit'])       { --_ratio: var(--logo-ratio-payfit); }
    .tool-mark__logo:has(> use[href$='#logo-pennylane'])    { --_ratio: var(--logo-ratio-pennylane); }
    .tool-mark__logo:has(> use[href$='#logo-qonto'])        { --_ratio: var(--logo-ratio-qonto); }
    .tool-mark__logo:has(> use[href$='#logo-tiime'])        { --_ratio: var(--logo-ratio-tiime); }
    .tool-mark__logo:has(> use[href$='#logo-indy'])         { --_ratio: var(--logo-ratio-indy); }
    .tool-mark__logo:has(> use[href$='#logo-notion'])       { --_ratio: var(--logo-ratio-notion); }
    .tool-mark__logo:has(> use[href$='#logo-swile'])        { --_ratio: var(--logo-ratio-swile); }
    .tool-mark__logo:has(> use[href$='#logo-odoo'])         { --_ratio: var(--logo-ratio-odoo); }
    .tool-mark__logo:has(> use[href$='#logo-alan'])         { --_ratio: var(--logo-ratio-alan); }
    .tool-mark__logo:has(> use[href$='#logo-boondmanager']) { --_ratio: var(--logo-ratio-boondmanager); }
    .tool-mark__logo:has(> use[href$='#logo-docusign'])     { --_ratio: var(--logo-ratio-docusign); }
    .tool-mark__logo:has(> use[href$='#logo-youtrust'])     { --_ratio: var(--logo-ratio-youtrust); }
    .tool-mark__logo:has(> use[href$='#logo-hiway'])        { --_ratio: var(--logo-ratio-hiway); }
    .tool-mark__logo:has(> use[href$='#logo-stripe'])       { --_ratio: var(--logo-ratio-stripe); }
    .tool-mark__logo:has(> use[href$='#logo-openai'])       { --_ratio: var(--logo-ratio-openai); }
  }

  /* Le nom écrit, seul ou à côté d'un symbole : même teinte, et un corps
     choisi pour que sa hauteur de capitale rejoigne celle des logos. */
  .tool-mark__name {
    font-family: var(--font-sans);
    font-size: var(--tool-name-size);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
  }

  /* ────────────────────────────────────────── Bandeau de chiffres ─────
     Trois affirmations posées côte à côte : elles se lisent d'un seul
     balayage, donc leurs chiffres doivent tomber sur UNE ligne, pas sur
     trois hauteurs voisines.

     LE BLOC SE DÉTACHE DE SA SECTION, QUELLE QU'ELLE SOIT.
     Il portait le gris des sections alternées. Posé dans une section
     alternée — ce qu'il est aujourd'hui en fin de « Méthode » — il avait
     exactement la couleur de son fond et cessait d'exister en tant que
     bloc. On le pose donc sur la surface des éléments POSÉS, avec le filet
     et l'ombre discrète des cartes : il se détache du gris comme du blanc,
     sans qu'il faille se demander dans quelle section il a été rangé.
     Contrastes (les rôles sont ceux du §2 de tokens.v1.9.0.3.css, sur du blanc) :
       chiffre ..... text-strong sur surface-raised .... 18.04:1  (AAA)
       suffixe ..... text-accent sur surface-raised .....  6.31:1  (AA)
       libellé ..... text-muted sur surface-raised ......  5.46:1  (AA) */
  /* UNE ou TROIS colonnes, jamais deux. `auto-fit` ne sait pas sauter le
     palier intermédiaire : il le traverse, et trois éléments y laissent le
     dernier seul, calé à gauche, la moitié droite du cartouche vide.
     Constaté entre 496 et 713 px — ce sont les « données pas jolies ».
     Le seuil est un littéral : une condition de @media ne peut pas lire une
     variable CSS. C'est une limite du langage, pas un oubli. Il vaut trois
     pistes de --grid-min-2xs plus les gouttières et le rembourrage. */
  .metrics-band {
    display: grid;
    grid-template-columns: 1fr;
    /* Alignement sur la première LIGNE DE BASE, pas sur le haut des boîtes.
       C'est ce qui rend l'alignement indépendant du contenu : une colonne
       peut gagner un suffixe, passer d'un chiffre à deux ou voir son libellé
       repasser sur deux lignes, les chiffres restent sur la même ligne. */
    align-items: baseline;
    gap: var(--space-md);
    padding: var(--space-fluid-2xl);
    border: var(--stroke-hairline) solid var(--border-subtle);
    border-radius: var(--radius-signature-lg);
    background-color: var(--surface-raised);
    box-shadow: var(--shadow-sm);
    text-align: center;
  }
  /* Une grille gouverne ses écarts par `gap` : elle n'a pas de « paragraphe
     suivant », elle a des colonnes. Le rythme de flux de la couche `base`
     (`p + p`) posait pourtant 16 px de marge haute sur les colonnes 2 et 3,
     laissant la première seule en avance sur la rangée. Le défaut n'était
     pas dans le contenu des colonnes — il aurait suivi n'importe quel
     texte. */
  @media (min-width: 45rem) {
    .metrics-band { grid-template-columns: repeat(3, 1fr); }
  }
  .metrics-band > * { margin: 0; }
  .metrics-band .metric { color: var(--text-strong); }
  .metrics-band .metric__suffix { color: var(--text-accent); }

  /* ──────────────────────────────────────────────────── Méthode ─────── */
  /* Même remède que la bande de chiffres : entre 596 et 915 px, l'étape 03
     se retrouvait seule sur sa rangée, et le filet de liaison de l'étape 02
     partait vers la droite sans rien rejoindre. */
  .steps {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-fluid-lg);
    margin-top: var(--space-xl);
    counter-reset: step;
  }
  /* Le seuil est le complément EXACT de celui qui masque le filet de
     liaison (`@media (max-width: 860px)`, plus bas) : les deux réglages
     décrivent le même passage — la série d'étapes se lit en ligne, ou
     elle s'empile. Les désaccorder ferait tracer des traits qui ne
     relient rien, ce qui était le défaut d'origine sur une grille à deux
     colonnes. Ne changer l'un qu'en changeant l'autre. */
  @media (min-width: 861px) {
    .steps { grid-template-columns: repeat(3, 1fr); }
  }
  .step { position: relative; padding-top: var(--space-xl); }
  /* Numéro en contour, généré automatiquement : ajouter une étape ne
     demande aucune modification du HTML au-delà du bloc lui-même. */
  .step::before {
    counter-increment: step;
    content: '0' counter(step);
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    color: transparent;
    font-size: var(--step-number-size);
    font-weight: var(--weight-black);
    line-height: 1;
    -webkit-text-stroke: var(--stroke-thin) var(--border-subtle);
    /* Le numéro glisse depuis la gauche à l'arrivée de son étape, puis
       grossit légèrement au survol : il devient la poignée du bloc. */
    transform: translate3d(calc(var(--step-number-shift) * -1), 0, 0);
    transform-origin: left center;
    opacity: 0;
    transition: -webkit-text-stroke-color var(--duration-base) var(--ease-out),
      transform var(--duration-reveal) var(--ease-out),
      opacity var(--duration-reveal) var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .step.is-revealed::before { transform: none; opacity: 1; }
  .step:hover::before { -webkit-text-stroke-color: var(--brand-mint-500); }
  .step.is-revealed:hover::before { transform: scale(var(--step-number-hover-scale)); }

  /* Filet reliant les étapes entre elles : il se trace de gauche à droite,
     juste après l'arrivée du numéro, ce qui donne à la série son sens de
     lecture. */
  .step::after {
    content: '';
    position: absolute;
    inset-block-start: var(--step-rule-offset-block);
    inset-inline-start: var(--step-rule-offset-inline);
    inset-inline-end: calc(var(--space-md) * -1);
    height: var(--stroke-hairline);
    background-color: var(--border-subtle);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--duration-trace) var(--ease-out);
    transition-delay: calc(var(--reveal-delay, 0ms) + var(--duration-fast));
  }
  .step.is-revealed::after { transform: scaleX(1); }
  .step:last-child::after { display: none; }
  .step p { color: var(--text-muted); font-size: var(--text-sm); }

  /* ──────────────────────────────────────────── Comparatif avant/après ─ */
  .compare {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min-sm), 100%), 1fr));
    gap: var(--space-sm);
    margin-block: var(--space-md);
  }
  .compare__item {
    padding: var(--space-sm);
    border: var(--stroke-hairline) solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-raised);
  }
  .compare__item--positive {
    border-color: var(--border-accent);
    background-color: var(--surface-accent-soft);
  }
  .compare__tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3xs);
    margin-bottom: var(--space-2xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
  }
  .compare__item--positive .compare__tag { color: var(--text-accent); }
  .compare__item strong { display: block; color: var(--text-strong); }
  .compare__item p { color: var(--text-muted); font-size: var(--text-sm); }

  /* ──────────────────────────────────────────────────────── FAQ ─────── */
  .faq {
    max-width: var(--width-faq);
    margin-inline: auto;
  }

  /* ─── Rendu différé, mais SEULEMENT tant que le bloc n'a pas été vu ───
     Bloc long, situé bas dans la page : son contenu n'est mis en page qu'à
     l'approche de l'écran, ce qui est autant de travail en moins au premier
     affichage. La taille réservée évite tout saut d'ascenseur.

     ⚠ POURQUOI CETTE CONDITION, ET PAS UN `content-visibility: auto` NU.
     Un sous-arbre sauté par `content-visibility: auto` n'est plus rendu du
     tout — et une transition, qui est une interpolation dans le TEMPS, ne
     progresse que tant que l'élément est rendu. Les neuf `.faq__item`
     portent `data-reveal` : leur fondu d'apparition démarre au moment
     précis où le bloc franchit le bord de l'écran, c'est-à-dire au moment
     précis où ce même bord le fait sauter. Sur un défilement rapide, le
     fondu se figeait donc en cours de route et n'en repartait jamais :
     mesuré à 639×900, page arrivée en bas, neuf éléments porteurs de
     `is-revealed` bloqués à 0,99 / 0,97 / 0,94 / 0,88 / 0,78 / 0,67 puis
     0,50 — et toujours à ces valeurs-là à la première image du retour en
     vue, donc bien visibles par qui remonte la page.
     Les deux réglages se disputaient la même frontière, celle du bord de
     l'écran. On les sépare dans le TEMPS plutôt que dans l'espace : le
     rendu n'est différé que tant qu'aucun élément du bloc n'a été révélé,
     c'est-à-dire exactement pendant la phase où l'optimisation sert (le
     premier affichage). Dès la première apparition, le bloc redevient un
     bloc ordinaire et plus aucune animation ne peut s'y figer. La bascule
     ne déplace rien : à cet instant le bloc est à l'écran, donc déjà mis
     en page à sa taille réelle.

     Sur un navigateur sans `:has()`, la règle entière est invalide et le
     rendu différé ne s'applique jamais : on perd l'optimisation, jamais
     l'affichage. C'est le bon sens de dégradation.

     ⚠ JAMAIS SUR UNE PAGE DONT LA FAQ PORTE DES ANCRES.
     Un bloc différé occupe la taille réservée (640 px), pas sa taille
     réelle. Sur la page des questions fréquentes, quatre blocs de 190 à
     400 px se suivent et chaque intertitre de thème est une ancre (liens
     de `llms.txt`, résultats de la recherche) : le navigateur visait la
     position calculée avec les tailles réservées, puis les blocs se
     dépliaient à leur vraie hauteur. Mesuré le 18/10/2026 en Chrome, à
     l'arrivée sur `#theme-prix` / `#theme-donnees` : 736 / 514 px trop bas
     à 1280 px, 628 / 871 px à 375 px. Sans rendu différé : 0 px d'écart.
     La condition se lit donc au niveau de la page : dès qu'un élément
     d'une FAQ porte un `id` (posé dans le HTML ou par la recherche),
     aucun bloc n'est différé. L'accueil, dont la FAQ n'a pas d'ancre
     interne, garde l'optimisation. */
  body:not(:has(.faq [id])) .faq:not(:has(.is-revealed)) {
    content-visibility: auto;
    contain-intrinsic-size: var(--intrinsic-section-size);
  }
  .faq__item { border-bottom: var(--stroke-hairline) solid var(--border-subtle); }
  .faq__question {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding-block: var(--space-sm);
    color: var(--text-strong);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    list-style: none;
    transition: var(--transition-colors);
  }
  .faq__question::-webkit-details-marker { display: none; }
  .faq__question:hover { color: var(--text-accent); }
  /* Chevron pivotant. Les angles (45°, -135°) dessinent la flèche ; le
     recalage vertical la recentre optiquement après rotation. */
  .faq__question::after {
    content: '';
    flex: none;
    width: var(--faq-chevron-size);
    height: var(--faq-chevron-size);
    border-inline-end: var(--stroke-base) solid var(--brand-mint-500);
    border-block-end: var(--stroke-base) solid var(--brand-mint-500);
    transform: rotate(45deg) translateY(calc(var(--faq-chevron-nudge) * -1));
    /* Même courbe que le dépliage de la réponse : le chevron pivotait avec
       un rebond pendant que le panneau, lui, glissait sans rebond. Deux
       gestes simultanés qui ne se répondaient pas. */
    transition: transform var(--duration-base) var(--ease-out);
  }
  /* Le chevron grossit à l'approche puis pivote avec un léger rebond :
     l'ouverture se sent avant même que la réponse ne se déplie. */
  .faq__question:hover::after,
  .faq__question:focus-visible::after {
    transform: rotate(45deg) translateY(calc(var(--faq-chevron-nudge) * -1))
      scale(var(--faq-chevron-hover-scale));
  }
  .faq__item[open] .faq__question::after {
    transform: rotate(-135deg) translateY(calc(var(--faq-chevron-nudge) * -1));
  }
  .faq__item[open] .faq__question:hover::after {
    transform: rotate(-135deg) translateY(calc(var(--faq-chevron-nudge) * -1))
      scale(var(--faq-chevron-hover-scale));
  }
  /* Réponse : glissement d'ouverture piloté par une grille 0fr → 1fr */
  .faq__answer {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--duration-base) var(--ease-out);
  }
  .faq__item[open] .faq__answer { grid-template-rows: 1fr; }
  .faq__answer > div { overflow: hidden; }
  /* `:is(p, ul, ol)` et non `p` seul : une réponse peut ÉNUMÉRER. Mesuré
     avant d'élargir la règle — un <ul> laissé au style du navigateur
     s'affichait à 16 px en --text-body, soit PLUS GROS ET PLUS FONCÉ que
     le paragraphe qu'il détaille, décalé de 40 px, et immobile pendant que
     le texte montait au dépliage. Une liste qui crie plus fort que sa
     réponse est un contresens. */
  .faq__answer :is(p, ul, ol) {
    padding-block-end: var(--space-sm);
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
  }
  /* Le retrait vient du site, pas du navigateur : le reset ne vise que
     `ul[class]`, et 40 px n'est tokenisé nulle part. Et pas de second
     écart en tête — le paragraphe qui précède a déjà posé le sien. */
  .faq__answer ul {
    margin-block-start: 0;
    padding-inline-start: var(--space-md);
  }
  .faq__answer li + li { margin-block-start: var(--space-3xs); }
  /* La réponse ne se contente pas d'être démasquée par la grille : elle
     monte en même temps, ce qui rend le dépliage nettement plus lisible.
     La liste doit monter AVEC : sans elle dans le sélecteur, le paragraphe
     montait et la liste restait figée à côté. */
  .faq__item[open] .faq__answer :is(p, ul, ol) {
    animation: rise-in var(--duration-base) var(--ease-out) backwards;
  }

  /* ─── Carte d'Ana, en fin de questions ───────────────────────────────
     Une .card ordinaire, alignée sur la colonne des questions. Photo et
     texte côte à côte tant que le texte garde plus de la moitié de la
     carte (--ask-card-body-min) ; en deçà, la photo passe au-dessus en
     pleine largeur. Le basculement vient du retour à la ligne de la boîte
     flexible, sans media query ni requête de conteneur. */
  .card--ask {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md);
    max-width: var(--width-faq);
    margin-inline: auto;
  }
  .card--ask .card__media {
    flex: 1 1 var(--ask-card-media-basis);
    aspect-ratio: var(--ask-card-media-ratio);
    border-radius: var(--radius-signature);
    background-color: var(--surface-alt);
    overflow: clip;
  }
  .card--ask .card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--ask-card-focus);
  }
  .card--ask .card__body {
    flex: 999 1 0;
    min-inline-size: var(--ask-card-body-min);
  }
  /* Titre de carte, pas intertitre de thème : un cran sous les h2 qui
     ouvrent les groupes de questions, pour ne pas se lire comme un
     cinquième thème. */
  .card--ask .card__title { font-size: var(--text-2xl); }

  /* ─── Rubrique « Conseils » : cartes-liens (1.6.0) ────────────────────
     Une .card ordinaire, rendue entièrement cliquable par UN SEUL lien —
     celui du titre — dont le pseudo-élément couvre la carte. Le lecteur
     d'écran n'entend donc qu'un lien par article, nommé par son titre ;
     le « Lire l'article » final n'est qu'un indice visuel, pas un second
     lien. L'anneau de focus est porté par ce pseudo-élément : il entoure
     la carte entière, sans dépendre de `:has()`.
     Disposition de base (navigateur sans requête de conteneur, et grille
     compacte « Autres conseils ») : autant de colonnes que la largeur
     minimale d'une carte en autorise. */
  .article-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--article-card-min), 100%), 1fr));
    gap: var(--article-card-gap);
    max-width: var(--width-faq);
    margin-inline: auto;
    container-type: inline-size;
    container-name: article-grid;
  }

  /* Vitrine (1.6.0.1) — trois cartes par ligne en bureau, deux sur
     tablette, une sur téléphone, et une dernière ligne incomplète
     CENTRÉE, ses cartes gardant la largeur des autres (5 cartes : 3 + 2
     en bureau, 2 + 2 + 1 sur tablette). auto-fit ne sait pas centrer
     une ligne : la grille passe à douze pistes égales, et chaque carte
     en couvre six (deux par ligne) ou quatre (trois par ligne). Une
     ligne incomplète commence donc une demi-carte plus loin : piste 4
     pour une carte seule sur deux colonnes ; pistes 3 puis 5 pour deux
     ou une carte sur trois colonnes. Les sélecteurs repèrent la
     première carte de la dernière ligne par son rang (nth-child) et le
     nombre de cartes qui la suivent (nth-last-child) : la règle tient
     quel que soit le nombre d'articles.
     Seuils (littéraux : une requête de conteneur n'accepte pas var()) :
     41.5rem = 2 × --article-card-min + --article-card-gap, seuil déjà
     employé par la carte compacte ; 63rem = 3 × --article-card-min +
     2 × --article-card-gap. La grille s'élargit à --width-page pour que
     trois cartes y tiennent.
     Sans @container, la disposition de base ci-dessus s'applique. */
  @supports (container-type: inline-size) {
    .article-grid:not(.article-grid--compact) {
      grid-template-columns: repeat(12, minmax(0, 1fr));
      max-width: var(--width-page);
    }
    .article-grid:not(.article-grid--compact) > .article-card { grid-column: 1 / -1; }
  }
  @container article-grid (width >= 41.5rem) {
    .article-grid:not(.article-grid--compact) > .article-card { grid-column: span 6; }
    .article-grid:not(.article-grid--compact) > .article-card:nth-child(odd):last-child { grid-column: 4 / span 6; }
  }
  @container article-grid (width >= 63rem) {
    .article-grid:not(.article-grid--compact) > .article-card,
    .article-grid:not(.article-grid--compact) > .article-card:nth-child(odd):last-child { grid-column: span 4; }
    .article-grid:not(.article-grid--compact) > .article-card:nth-child(3n+1):nth-last-child(2) { grid-column: 3 / span 4; }
    .article-grid:not(.article-grid--compact) > .article-card:nth-child(3n+1):last-child { grid-column: 5 / span 4; }
  }
  .article-grid--compact {
    grid-template-columns: repeat(auto-fit, minmax(min(var(--article-card-min-compact), 100%), 1fr));
  }
  .article-card > * + * { margin-block-start: var(--space-2xs); }
  .article-card__theme {
    color: var(--text-accent);
    font-size: var(--article-theme-size);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
  }
  .article-card__title {
    font-size: var(--article-card-title-size);
    transition: var(--transition-colors);
  }
  .article-grid--compact .article-card__title { font-size: var(--article-card-title-size-compact); }
  .article-card:hover .article-card__title { color: var(--text-accent); }
  .article-card__link::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: var(--layer-base);
    border-radius: var(--card-radius);
  }
  .article-card__link:focus-visible { outline: none; }
  .article-card__link:focus-visible::after {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
  }
  .article-card__resume { color: var(--text-muted); }
  /* Poussé en bas de carte : les « Lire l'article » d'une même rangée
     s'alignent, quelle que soit la longueur du résumé. */
  .article-card__more {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    margin-block-start: auto;
    padding-block-start: var(--space-xs);
    color: var(--text-accent);
    font-weight: var(--weight-semibold);
  }
  /* Sur un intitulé de plusieurs lignes (cartes de services de l'accueil),
     la flèche ne doit pas céder sa largeur au texte. */
  .article-card__more > .icon { flex-shrink: 0; }
  /* Quand l'indice est un vrai lien (cartes de services), son survol se
     distingue de celui de la carte qui le porte. */
  a.article-card__more:hover {
    text-decoration-line: underline;
    text-decoration-thickness: var(--article-link-underline-thickness);
    text-underline-offset: var(--article-link-underline-offset);
  }

  /* Carte compacte — dès que la grille n'a plus qu'une colonne.
     Une grande illustration au-dessus de chaque titre faisait de la
     vitrine un long défilement sur téléphone : l'illustration se réduit
     en vignette, à gauche du thème et du titre ; résumé et « Lire
     l'article » passent dessous, sur toute la largeur.
     La carte ne peut pas se restyler depuis sa propre requête : c'est la
     GRILLE qui est interrogée, et ses seuils sont ceux où elle bascule à
     une colonne (2 × --article-card-min + gap, 2 × --article-card-min-
     compact + gap). Les deux requêtes ne font que poser les variables
     privées --_compacte et --_vignette-* ; le placement, écrit une seule
     fois, est inerte tant que la carte reste en flex (bureau, ou
     navigateur sans @container), et chaque variable absente rend la
     valeur d'origine.
     Le lien étiré et son anneau de focus ne changent pas : ils couvrent
     toujours la carte entière. */
  @container article-grid (width < 41.5rem) {
    .article-grid:not(.article-grid--compact) > .article-card {
      --_compacte: grid;
      --_vignette-espace: 0;
      --_vignette-marge: var(--article-thumb-marge);
      --_vignette-rayon: var(--article-thumb-rayon);
      --_vignette-calage: start;
    }
  }
  @container article-grid (width < 27.5rem) {
    .article-grid--compact > .article-card {
      --_compacte: grid;
      --_vignette-espace: 0;
      --_vignette-marge: var(--article-thumb-marge);
      --_vignette-rayon: var(--article-thumb-rayon);
      --_vignette-calage: start;
    }
  }
  .article-card {
    display: var(--_compacte, flex);
    grid-template-columns: var(--article-thumb-size) minmax(0, 1fr);
    column-gap: var(--article-thumb-gap);
    align-content: start;
  }
  .article-card > * { grid-column: 1 / -1; min-width: 0; }
  .article-card > :is(.article-card__theme, .article-card__title) { grid-column: 2; }
  .article-card > .article-card__theme { margin-block-start: var(--_vignette-espace, var(--space-2xs)); }
  .article-card > .article-illustration {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: var(--_vignette-calage, auto);
    margin-block-end: var(--_vignette-espace, var(--space-xs));
    padding: var(--_vignette-marge, var(--illustration-marge));
    border-radius: var(--_vignette-rayon, var(--illustration-rayon));
  }

  /* ─── Page d'article (1.6.0) ──────────────────────────────────────────
     Pas de héros photo : l'article commence par ce qu'il promet. Une
     seule colonne de lecture (--article-body-width), du lien de retour à
     la signature. */
  .article-page { padding-block: var(--article-header-pad-top) var(--space-section); }
  .article-page__inner {
    max-inline-size: var(--article-body-width);
    margin-inline: auto;
  }
  /* Le lien de retour occupe sa propre ligne à toute largeur : en
     inline-flex, il partageait sa ligne avec le sur-titre (lui-même en
     inline-flex) dès que la place le permettait. Boîte de bloc, mais
     limitée à son contenu pour que la zone cliquable ne s'étire pas. */
  .article-page__back {
    display: flex;
    inline-size: fit-content;
    align-items: center;
    gap: var(--space-2xs);
    min-block-size: var(--article-back-height);
    color: var(--text-accent);
    font-weight: var(--weight-semibold);
    transition: var(--transition-colors);
  }
  .article-page__back:hover { color: var(--text-strong); }
  .article-page__header { margin-block-end: var(--space-xl); }
  .article-page__header .eyebrow { margin-block-start: var(--space-md); }
  .article-page__title { font-size: var(--article-h1-size); }
  .article-page__chapo {
    color: var(--text-strong);
    font-size: var(--text-lg);
  }
  .article-page__header .article-illustration { margin-block-start: var(--space-lg); }
  .article-page__byline {
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
  .article-page__header > .article-page__byline { margin-block-start: var(--space-sm); }

  /* Corps : le rythme vertical de la couche base, plus les intertitres
     espacés pour qu'on voie les sections en faisant défiler. */
  .article-page__body > * + * { margin-block-start: var(--space-sm); }
  .article-page__body > h2 {
    margin-block-start: var(--article-heading-gap);
    font-size: var(--article-h2-size);
  }
  .article-page__body :is(p, li) { font-size: var(--article-body-size); }
  .article-page__body :is(ul, ol) { padding-inline-start: var(--article-list-indent); }
  .article-page__body li + li { margin-block-start: var(--space-2xs); }
  .article-page__body strong { color: var(--text-strong); }
  /* Liens dans le texte : soulignés, sinon rien ne les distingue (le
     reset retire le soulignement). Les boutons gardent leur aspect. */
  .article-page__body a:not(.button) {
    color: var(--text-accent);
    text-decoration-line: underline;
    text-decoration-thickness: var(--article-link-underline-thickness);
    text-underline-offset: var(--article-link-underline-offset);
    transition: var(--transition-colors);
  }
  .article-page__body a:not(.button):hover { color: var(--text-strong); }
  .article-page__body .article-page__notice {
    margin-block-start: var(--space-lg);
    padding: var(--space-sm) var(--space-md);
    border-inline-start: var(--stroke-thick) solid var(--border-accent);
    border-radius: var(--radius-xs);
    background-color: var(--article-notice-bg);
    color: var(--text-muted);
    font-size: var(--text-sm);
  }

  /* Signature d'Ana, en fin d'article */
  .article-page__body .article-page__signature {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-block-start: var(--space-xl);
    padding-block-start: var(--space-md);
    border-top: var(--stroke-hairline) solid var(--border-subtle);
  }
  .article-page__signature img {
    flex: none;
    inline-size: var(--article-signature-photo-lg);
    block-size: var(--article-signature-photo-lg);
    border-radius: 50%;
    object-fit: cover;
    object-position: var(--article-signature-focus);
  }
  .article-page__signature p { color: var(--text-muted); }
  .article-page__signature p + p { margin-block-start: 0; }
  .article-page__signature strong { color: var(--text-strong); }

  /* « Autres conseils » : une .section--alt ordinaire, cartes compactes */
  .article-more__title {
    max-width: var(--width-faq);
    margin-inline: auto;
    margin-block-end: var(--space-lg);
    font-size: var(--article-h2-size);
  }

  /* ─────────────────────────────────────────── Bandeau d'appel (CTA) ── */
  .cta-band {
    position: relative;
    padding-block: var(--space-band-y);
    border-end-end-radius: var(--radius-sweep);
    background-color: var(--surface-inverse-deep);
    color: var(--text-on-dark);
    text-align: center;
    overflow: clip;
  }
  .cta-band__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: var(--cta-image-opacity);
    /* Léger défilement différentiel, valeur poussée par le JS */
    transform: translate3d(0, var(--parallax, 0px), 0) scale(var(--scale-cover));
    will-change: transform;
  }
  .cta-band__inner {
    position: relative;
    max-width: var(--width-prose);
    margin-inline: auto;
  }
  .cta-band h2 { color: var(--text-on-dark); }
  .cta-band p { color: var(--text-on-dark-muted); }
  /* Le bandeau arrive d'un bloc, mais son contenu se dépose en trois temps :
     le titre, la promesse, puis l'appel à l'action. */
  .cta-band__inner.is-revealed > * {
    animation: rise-in var(--duration-slower) var(--ease-out) backwards;
  }
  .cta-band__inner.is-revealed > :nth-child(1) { animation-delay: calc(var(--stagger-step) * 1); }
  .cta-band__inner.is-revealed > :nth-child(2) { animation-delay: calc(var(--stagger-step) * 2); }
  .cta-band__inner.is-revealed > :nth-child(3) { animation-delay: calc(var(--stagger-step) * 3); }

  /* ──────────────────────────────────────────── Canaux de contact ───── ═
     Ce composant a remplacé le formulaire. Ce ne sont plus des lignes
     d'information à lire mais des appels à l'action à toucher : d'où une
     hauteur minimale généreuse, un contour de commande et un survol qui
     engage. Trois entrées, une seule mise en avant.
     Ils reprennent .icon-badge tel quel, animation comprise. */
  .channels {
    container-type: inline-size;
    container-name: channels;

    display: grid;
    gap: var(--space-sm);
    max-width: var(--width-prose);
    margin-inline: auto;
  }

  /* ── Les deux plafonds de lecture sautent en colonne ─────────────────
     --width-faq (820 px) et --width-prose (720 px) sont des MESURES DE
     LECTURE : elles bornent un bloc posé en pleine largeur, pour qu'une
     ligne ne s'étire pas sur tout l'écran. Dans une colonne déjà bornée
     par la grille, elles bornent une seconde fois — un bloc centré dans un
     bloc, avec sa propre gouttière par-dessus celle de la grille.
     Sur .channels, c'est plus qu'inélégant : ces trois liens sont les
     commandes de conversion de la page, et le plafond leur retirait 74 px
     de cible tactile pour rien. La règle du projet est explicite : si les
     canaux se trouvent comprimés, c'est le plafond qui saute, pas la
     colonne qui rétrécit.
     Les plafonds sont rendus au palier de repli (section 8) : là, les
     blocs redeviennent pleine largeur et la mesure de lecture reprend tout
     son sens.

     ⚠ `width: 100%` EST INDISPENSABLE, ET IL RESTE VRAI AU PALIER DE
     REPLI. Ces deux blocs se centrent par `margin-inline: auto`. Dans un
     flux normal cela ne change pas leur largeur — un bloc de largeur
     `auto` remplit son parent. Dans une GRILLE, des marges automatiques
     empêchent l'étirement : la boîte retombe alors sur sa largeur de
     contenu. Mesuré : les trois canaux du contact tombaient à 447 px pour
     une colonne de 706 — soit exactement la compression que la règle du
     projet interdit, et elle ne venait même pas du plafond. */
  .side-layout :is(.faq, .channels) {
    width: 100%;
    max-width: none;
  }

  .channel {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: var(--channel-min-height);
    padding: var(--channel-padding-y) var(--channel-padding-x);
    border: var(--stroke-hairline) solid var(--border-control);
    border-radius: var(--channel-radius);
    background-color: var(--surface-raised);
    transition: var(--transition-colors), var(--transition-transform),
      var(--transition-shadow);
  }
  /* Le corps prend toute la place restante ; min-width évite qu'un libellé
     long ne pousse la flèche hors de la carte. */
  .channel__body {
    display: grid;
    gap: var(--space-3xs);
    flex: 1 1 auto;
    min-width: 0;
  }
  /* UNE ADRESSE NE SE COUPE PAS.
     « contact@adminisphere.fr » est un seul mot : 200 px en --text-lg à
     320 px de fenêtre. En ligne, badge, flèche et rembourrage ne laissent
     au libellé que 134 px à 320, 174 à 360, 204 à 390 (2 px de marge) :
     l'adresse se coupait en « contact@admini / sphere.fr ».
     Réduire le corps ne suffisait pas (il aurait fallu descendre sous
     12 px) : c'est la carte qui se réorganise, voir la requête `channels`
     plus bas. `anywhere` reste en filet pour ce que la mise en page ne
     peut pas prévoir — texte agrandi par le lecteur, écran sous 320 px —
     et retire le mot du calcul de largeur minimale, pour que la colonne
     ne se laisse jamais pousser hors de l'écran. */
  .channel__title {
    overflow-wrap: anywhere;
    color: var(--text-strong);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-snug);
    text-wrap: balance;
  }
  .channel__note {
    color: var(--text-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    text-wrap: pretty;
  }
  .channel__arrow {
    flex: none;
    width: var(--icon-size);
    height: var(--icon-size);
    color: var(--text-accent);
    transition: var(--transition-transform);
  }

  /* CARTE ÉTROITE : LE LIBELLÉ PREND TOUTE LA LARGEUR.
     Sous 24rem de liste (390 px de fenêtre et moins), le badge et la
     flèche montent sur une première ligne, le texte passe dessous et
     récupère leur largeur : 230 px à 320, 270 à 360, 300 à 390, pour une
     adresse de 200. Les trois canaux basculent ensemble, la liste reste
     homogène. La requête porte sur la liste et non sur l'écran : la même
     carte posée dans une colonne étroite se réorganise d'elle-même. */
  @container channels (max-width: 24rem) {
    .channel {
      display: grid;
      grid-template-columns: 1fr auto;
      grid-template-areas:
        "badge arrow"
        "body  body";
      align-items: center;
    }
    .channel > .icon-badge { grid-area: badge; }
    .channel__body { grid-area: body; }
    .channel__arrow { grid-area: arrow; }
  }

  /* Survol ET focus clavier traités ensemble : au clavier on doit voir
     exactement ce que voit la souris, en plus de l'anneau de focus. */
  .channel:hover,
  .channel:focus-visible {
    transform: translateY(var(--channel-lift));
    border-color: var(--border-accent);
    background-color: var(--surface-accent-soft);
    box-shadow: var(--shadow-md);
  }
  .channel:hover .channel__arrow,
  .channel:focus-visible .channel__arrow {
    transform: translateX(var(--channel-arrow-shift));
  }
  .channel:active { transform: translateY(0) scale(var(--scale-press)); }

  /* ── Canal mis en avant ──────────────────────────────────────────────
     Le bleu nuit plutôt qu'un aplat vert : la pastille d'icône est déjà
     verte et disparaîtrait sur un fond de même teinte. Le vert reste le
     levier — il porte la pastille, la flèche et le halo de l'ombre — mais
     c'est le contraste sombre/clair qui établit la hiérarchie entre les
     trois entrées. */
  .channel--primary {
    /* Fond sombre : l'anneau de focus bascule sur son ton clair */
    --focus-ring-color: var(--focus-ring-color-on-dark);
    border-color: transparent;
    background-color: var(--surface-inverse);
    box-shadow: var(--shadow-accent);
  }
  .channel--primary .channel__title { color: var(--text-on-dark); }
  .channel--primary .channel__note { color: var(--text-on-dark-muted); }
  .channel--primary .channel__arrow { color: var(--brand-mint-300); }
  /* Placé après la règle générale : c'est l'ordre qui tranche, les deux
     sélecteurs ayant la même spécificité. */
  .channel--primary:hover,
  .channel--primary:focus-visible {
    border-color: transparent;
    background-color: var(--surface-inverse-hover);
    box-shadow: var(--shadow-lg);
  }

  /* ────────────────────────────────────────────────── Pied de page ──── */
  .footer {
    padding-block: var(--space-footer-y) var(--space-md);
    background-color: var(--surface-inverse-deep);
    color: var(--text-on-dark-muted);
    font-size: var(--text-sm);
  }
  /* Dernier bloc de la page : mise en page différée jusqu'à l'approche.
     Même condition que la FAQ, et pour la même raison : le pied de page
     contient lui aussi des éléments `data-reveal` dont le fondu se figerait
     s'il était sauté en pleine apparition. Voir le commentaire de .faq. */
  .footer:not(:has(.is-revealed)) {
    content-visibility: auto;
    contain-intrinsic-size: var(--intrinsic-section-size);
  }
  .footer__highlights {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min-md), 100%), 1fr));
    gap: var(--space-sm);
    margin-bottom: var(--space-xl);
  }
  .footer__highlight {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2xs);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-signature);
    font-weight: var(--weight-semibold);
    text-wrap: balance;
    transition: var(--transition-transform), var(--transition-shadow);
  }
  .footer__highlight:hover {
    transform: translateY(calc(var(--shift-xs) * -1));
    box-shadow: var(--shadow-lg);
  }
  .footer__highlight--light { background-color: var(--brand-white); color: var(--text-strong); }
  .footer__highlight--accent { background-color: var(--brand-mint-500); color: var(--text-on-accent); }
  .footer__highlight .icon { width: var(--icon-size-sm); height: var(--icon-size-sm); flex: none; }

  /* Même défaut, même remède : « Informations légales » restait seule en
     bas à gauche entre 498 et 755 px. Une ou trois, jamais deux. */
  .footer__columns {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }
  @media (min-width: 48rem) {
    .footer__columns { grid-template-columns: repeat(3, 1fr); }
  }
  .footer__logo { width: var(--logo-width-sm); margin-bottom: var(--space-sm); }
  .footer__title {
    margin-bottom: var(--space-2xs);
    color: var(--text-on-dark);
    font-size: var(--text-base);
  }
  .footer__links { display: grid; gap: var(--space-2xs); }
  .footer__links a {
    display: inline-block;
    transition: var(--transition-colors), var(--transition-transform);
  }
  .footer__links a:hover {
    color: var(--brand-mint-300);
    transform: translateX(var(--shift-xs));
  }
  .footer .socials { margin-top: var(--space-sm); color: var(--text-on-dark); }
  .footer .socials__link {
    width: var(--social-size-lg);
    height: var(--social-size-lg);
    background-color: var(--overlay-white-faint);
  }
  .footer .socials__link:hover {
    background-color: var(--brand-mint-500);
    color: var(--text-on-accent);
  }
  .footer__bottom {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-2xs);
    margin-top: var(--space-lg);
    padding-top: var(--space-sm);
    border-top: var(--stroke-hairline) solid var(--border-on-dark);
    font-size: var(--text-xs);
    text-align: center;
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  6. ANIMATIONS                                                           ║
   ╚═════════════════════════════════════════════════════════════════════════╝

   PHILOSOPHIE — à lire avant d'ajouter quoi que ce soit ici
   ─────────────────────────────────────────────────────────────────────────
   Le mouvement de ce site répond à quatre principes. Ils ne sont pas
   décoratifs : ils expliquent pourquoi tel effet existe et pourquoi tel
   autre a été écarté.

   1. LE MOUVEMENT ACCOMPAGNE UN ÉVÉNEMENT, IL NE MEUBLE PAS.
      Chaque animation répond soit à l'arrivée d'un élément dans l'écran,
      soit à une intention de la personne (survol, focus, ouverture). Rien
      ne tourne en boucle sous les yeux du lecteur. Seule exception assumée :
      la dérive du motif de points, assez lente pour être ressentie sans
      être vue.

   2. RIEN NE RETARDE LA LECTURE.
      Une apparition dure au plus --duration-reveal, et la cascade d'un
      groupe reste courte. Un texte n'est jamais invisible plus d'une demi-
      seconde après être entré dans l'écran. Si le script ne s'exécute pas
      ou si le mouvement est réduit, tout s'affiche immédiatement.

   3. QUATRE PROPRIÉTÉS, PAS UNE DE PLUS.
      `transform`, `opacity`, `clip-path`, `filter`. Ce sont les seules que
      le navigateur peut animer sans recalculer la mise en page. Un halo, une
      ombre ou une bordure qui « s'allume » passent donc toujours par un
      calque dédié dont on fait varier l'opacité — jamais par l'ombre
      elle-même.

   4. L'AMPLEUR VIENT DE LA COMBINAISON, PAS DE LA DURÉE.
      Un élément qui arrive translate, bascule légèrement dans la profondeur
      et se déplie d'un cran d'échelle, le tout dans le même temps court.
      C'est ce qui donne l'impression qu'il « arrive » plutôt qu'il
      n'« apparaît ». Rallonger les durées ne rendrait le site que lent.

   Toutes les amplitudes, durées et échelles viennent de `tokens.v1.9.0.3.css`
   (sections 6 et 8) et retombent à l'identité sous `prefers-reduced-motion`.
   ═══════════════════════════════════════════════════════════════════════ */
@layer animations {

  /* ─── Apparitions au défilement ─────────────────────────────────────────
     Poser `data-reveal` sur un élément suffit : le script lui ajoute la
     classe `is-revealed` quand il entre dans l'écran.
     Variantes : data-reveal="up" | "down" | "left" | "right" | "zoom" | "fade".
     Cascade  : data-reveal-group sur le parent décale ses enfants.
     `--reveal-delay` est typée via @property : une valeur mal formée
     poussée par le script retombe sur 0ms au lieu d'invalider la règle.

     Chaque variante combine trois gestes — un déplacement, une bascule dans
     la profondeur, un retrait d'échelle — pour que l'élément vienne de
     quelque part au lieu de se contenter d'un fondu. */
  [data-reveal] {
    opacity: 0;
    transition: opacity var(--duration-reveal) var(--ease-out),
      transform var(--duration-reveal) var(--ease-out);
    /* Cascade amortie puis plafonnée : voir --reveal-delay-damping */
    transition-delay: min(
      calc(var(--reveal-delay, 0ms) * var(--reveal-delay-damping)),
      var(--reveal-delay-max)
    );
    will-change: opacity, transform;
  }
  [data-reveal='up'],
  [data-reveal=''] {
    transform: perspective(var(--reveal-perspective))
      translate3d(0, var(--reveal-shift), 0)
      rotateX(var(--reveal-tilt))
      scale(var(--reveal-zoom-soft));
  }
  [data-reveal='down'] {
    transform: perspective(var(--reveal-perspective))
      translate3d(0, calc(var(--reveal-shift) * -1), 0)
      rotateX(calc(var(--reveal-tilt) * -1))
      scale(var(--reveal-zoom-soft));
  }
  [data-reveal='left'] {
    transform: perspective(var(--reveal-perspective))
      translate3d(calc(var(--reveal-shift-x) * -1), 0, 0)
      rotateY(calc(var(--reveal-tilt) * -1))
      scale(var(--reveal-zoom-soft));
  }
  [data-reveal='right'] {
    transform: perspective(var(--reveal-perspective))
      translate3d(var(--reveal-shift-x), 0, 0)
      rotateY(var(--reveal-tilt))
      scale(var(--reveal-zoom-soft));
  }
  [data-reveal='zoom'] {
    transform: perspective(var(--reveal-perspective))
      scale(var(--reveal-zoom))
      rotateX(var(--reveal-tilt));
  }
  [data-reveal='fade']   { transform: none; }

  [data-reveal].is-revealed {
    opacity: 1;
    transform: none;
    /* L'accélération graphique n'est plus utile une fois l'effet terminé */
    will-change: auto;
  }

  /* ⚠ Cette règle vit dans `animations`, la couche la PLUS haute après les
     utilitaires. Sans le rattrapage ci-dessous, son `transform: none`
     écraserait silencieusement toutes les élévations au survol déclarées
     plus bas — `.card:hover` dans `components`, `.channel:hover` et
     `.footer__highlight:hover` dans `sections`. C'est exactement ce qui se
     produisait : les boutons se soulevaient, les cartes et les canaux non,
     sans qu'aucune règle n'ait l'air fautive.
     `revert-layer` rend la main aux couches inférieures le temps de
     l'interaction : chaque composant retrouve l'élévation qu'il a lui-même
     déclarée, et un élément sans règle de survol retombe simplement sur
     l'identité. */
  [data-reveal].is-revealed:where(:hover, :focus-visible, :has(:focus-visible)) {
    transform: revert-layer;
  }

  /* ─── Deux colonnes en vis-à-vis : l'arrivée ne DÉPLACE plus rien ───────
     Mesuré : les colonnes de .side-layout sont alignées au pixel en mise
     en page — `offsetTop` identique pour les deux, à toutes les largeurs.
     Ce qui se voyait à l'écran, ce sont les 60 px de l'état d'attente de
     `data-reveal="up"` : la colonne d'image porte l'attribut, la colonne
     de texte non. L'une arrivait, l'autre était déjà là — et pendant tout
     ce temps la composition n'était pas « en face ».

     Dans une composition à deux colonnes, un déplacement d'entrée est donc
     un défaut par construction : il désaligne exactement ce que la grille
     vient d'aligner. On garde le fondu, qui ne déplace rien, et l'on
     supprime le déplacement. Les deux colonnes ne peuvent plus diverger,
     quel que soit l'ordre dans lequel elles sont révélées.

     La règle ne vise que les ENFANTS de la grille : si le balisage pose un
     jour `data-reveal` sur .side-layout lui-même — ce qui serait mieux
     encore, le bloc arrivant alors d'un seul tenant — l'animation complète
     revient, et les colonnes bougent ensemble. */
  .side-layout > [data-reveal] { transform: none; }

  /* ─── Titres de section : balayage au lieu d'un simple fondu ─────────────
     Le titre se dévoile de la gauche vers la droite derrière un masque.
     `clip-path` ne déplace rien autour : le texte occupe sa place définitive
     dès le premier instant, seul le masque bouge. Le délai est hérité de
     l'ancêtre révélé, si bien qu'un titre dans une cascade reste en phase. */
  [data-reveal] h2 {
    clip-path: var(--clip-title-hidden);
    transition: clip-path var(--duration-sweep) var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
  }
  [data-reveal].is-revealed h2 { clip-path: var(--clip-title-shown); }

  /* ─── Visuels encadrés : le cliché se dévoile, son cartouche suit ───────
     La photo est balayée par le même masque que les titres ; la carte de
     valeurs et la citation arrivent un temps après, ce qui hiérarchise la
     lecture au lieu de tout faire surgir ensemble. */
  [data-reveal] .figure img {
    clip-path: var(--clip-wipe-hidden);
    transition: transform var(--duration-slower) var(--ease-out),
      clip-path var(--duration-sweep) var(--ease-out) var(--reveal-delay, 0ms);
  }
  [data-reveal].is-revealed .figure img { clip-path: var(--clip-wipe-shown); }

  [data-reveal] :is(.figure__overlay, .pull-quote) {
    transition: opacity var(--duration-reveal) var(--ease-out),
      transform var(--duration-reveal) var(--ease-spring);
    transition-delay: calc(var(--reveal-delay, 0ms) + var(--duration-fast));
  }
  [data-reveal]:not(.is-revealed) :is(.figure__overlay, .pull-quote) {
    opacity: 0;
    transform: translate3d(0, var(--reveal-shift), 0);
  }

  /* ─── Chiffres-clés ─────────────────────────────────────────────────────
     Le nombre est compté par le script ; on anime ce qui l'entoure. Un
     ressort à l'arrivée, un halo qui s'ouvre UNE fois, puis plus rien. */
  [data-reveal].is-revealed .metric {
    animation: metric-pop var(--duration-slower) var(--ease-spring) backwards;
  }
  [data-reveal].is-revealed .metric::after {
    animation: metric-halo var(--duration-metric-halo) var(--ease-out) backwards;
  }
  /* Dans un bandeau, les trois chiffres se déclenchent l'un après l'autre */
  .metrics-band.is-revealed > :nth-child(1) .metric,
  .metrics-band.is-revealed > :nth-child(1) .metric::after { animation-delay: calc(var(--stagger-step) * 1); }
  .metrics-band.is-revealed > :nth-child(2) .metric,
  .metrics-band.is-revealed > :nth-child(2) .metric::after { animation-delay: calc(var(--stagger-step) * 2); }
  .metrics-band.is-revealed > :nth-child(3) .metric,
  .metrics-band.is-revealed > :nth-child(3) .metric::after { animation-delay: calc(var(--stagger-step) * 3); }

  /* ─── Entrée du héros ───────────────────────────────────────────────────
     Séquence jouée au chargement, sans attendre le défilement. */
  .hero__content > * {
    animation: rise-in var(--duration-slower) var(--ease-out) backwards;
  }
  .hero__content > :nth-child(1) { animation-delay: calc(var(--stagger-step) * 1); }
  .hero__content > :nth-child(2) { animation-delay: calc(var(--stagger-step) * 2); }
  .hero__content > :nth-child(3) { animation-delay: calc(var(--stagger-step) * 3); }
  .hero__content > :nth-child(4) { animation-delay: calc(var(--stagger-step) * 4); }
  /* Un rang non déclaré ne retombe pas en fin de série : il prend le délai
     nul et PASSE DEVANT tout le reste. La mention de réassurance, cinquième
     enfant, arrivait ainsi avant le sur-titre. Tout enfant ajouté ici doit
     recevoir son cran. */
  .hero__content > :nth-child(5) { animation-delay: calc(var(--stagger-step) * 5); }
  /* Le titre du héros mérite mieux qu'une montée : il se dévoile derrière un
     masque tout en montant. Sa spécificité l'emporte sur la règle générale
     ci-dessus, mais pas sur le délai de cascade posé par :nth-child(2) —
     il reste donc à sa place dans la séquence. */
  .hero__content h1 {
    animation: hero-title var(--duration-sweep) var(--ease-out) backwards;
  }
  /* La carte ferme la séquence : elle passe au sixième cran depuis que la
     mention de réassurance occupe le cinquième. Partager un cran avec elle
     aurait fait trébucher la cascade — un bloc entier et une ligne de texte
     arrivant ensemble. Le coût est d'un cran, soit +90 ms sur une entrée qui
     s'achève à 1,49 s au lieu de 1,40 s. */
  .hero__card {
    animation: rise-in var(--duration-slower) var(--ease-out) backwards;
    animation-delay: calc(var(--stagger-step) * 6);
  }
  /* Héros compact : le sur-titre, le titre et le chapô sont là au premier
     rendu. Partir d'une opacité nulle retardait le plus grand affichage
     mesuré (LCP) de toute la durée de la cascade. Seul le sommaire monte
     encore, à son cran : il n'est pas candidat au LCP. */
  .hero--compact .hero__content > :not(.theme-nav) { animation: none; }

  /* ─── Images : fondu à la fin du chargement ─────────────────────────────
     La classe `is-loaded` est posée par le script une fois l'image décodée,
     ce qui évite l'apparition brutale d'une image lourde. */
  .fade-img {
    opacity: 0;
    transition: opacity var(--duration-slow) var(--ease-out);
  }
  .fade-img.is-loaded { opacity: 1; }

  /* ─── Vignettes rondes (téléphone) : un fondu au lieu du balayage ───────
     Le balayage des photos ne se lit plus sur un disque de 56 px, et la
     découpe en disque le remplace de toute façon (palier « Téléphone »,
     section 8). La vignette arrive donc en fondu, au même temps que le
     cartouche qu'elle accompagne — elle ne précède pas sa citation.
     Une fois révélée, c'est `.fade-img` qui reprend la main : la vignette
     attend toujours la fin de son chargement. Sans script, le filet des
     utilitaires (`.fade-img`) l'affiche d'office. */
  @media (max-width: 47.9375rem) {
    [data-reveal] :is(.figure--avatar-quote, .figure--avatar-card) img {
      transition: opacity var(--duration-reveal) var(--ease-out);
      transition-delay: calc(var(--reveal-delay, 0ms) + var(--duration-fast));
    }
    [data-reveal]:not(.is-revealed) :is(.figure--avatar-quote, .figure--avatar-card) img {
      opacity: 0;
    }
  }

  /* ─── Bibliothèque d'images-clés ────────────────────────────────────── */
  @keyframes rise-in {
    from { opacity: 0; transform: translate3d(0, var(--shift-xl), 0); }
    to   { opacity: 1; transform: none; }
  }

  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  @keyframes modal-in {
    from { opacity: 0; transform: translateY(var(--shift-lg)) scale(var(--scale-enter)); }
    to   { opacity: 1; transform: none; }
  }

  @keyframes slow-zoom {
    from { transform: scale(var(--scale-zoom-from)); }
    to   { transform: scale(1); }
  }

  @keyframes pulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(var(--scale-pulse)); }
  }

  @keyframes dot-drift {
    from { background-position: 0 0; }
    to   { background-position: var(--dots-size) var(--dots-size); }
  }

  /* Titre du héros : montée et balayage dans le même geste */
  @keyframes hero-title {
    from {
      opacity: 0;
      transform: translate3d(0, var(--shift-xl), 0);
      clip-path: var(--clip-title-hidden);
    }
    to {
      opacity: 1;
      transform: none;
      clip-path: var(--clip-title-shown);
    }
  }

  /* Chiffre-clé : arrivée en ressort, puis halo qui s'ouvre et s'efface */
  @keyframes metric-pop {
    from { opacity: 0; transform: scale(var(--metric-pop-from)); }
    to   { opacity: 1; transform: none; }
  }

  @keyframes metric-halo {
    from { opacity: 0; transform: scale(var(--metric-pop-from)); }
    35%  { opacity: var(--metric-halo-opacity); }
    to   { opacity: 0; transform: scale(var(--metric-halo-scale)); }
  }

  /* Bandeaux défilants (témoignages, outils) : la bande avance d'exactement
     un jeu — six cartes, ou la liste des outils — puis reprend à zéro. Comme le jeu suivant est identique, la
     reprise ne se voit pas — il n'y a ni saut ni vide, et aucune image-clé
     intermédiaire à tenir. L'amplitude vient du token : en mouvement réduit
     elle vaut 0 px et les deux images-clés se confondent. */
  @keyframes quote-drift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(calc(var(--_loop-shift) * -1), 0, 0); }
  }

  /* Pastille d'icône : un anneau part une fois au survol de la carte */
  @keyframes badge-ring {
    from { opacity: var(--icon-badge-ring-opacity); transform: scale(1); }
    to   { opacity: 0; transform: scale(var(--icon-badge-ring-scale)); }
  }

  /* ─── Amélioration : effets pilotés par le défilement ───────────────────
     Là où le navigateur sait lier une animation à la position dans la page,
     l'effet passe du fil d'exécution principal au compositeur : plus de
     calcul à chaque événement de défilement, donc un rendu qui reste fluide
     même pendant un chargement.
     Le script reste la voie principale et fonctionne seul : ces règles ne
     s'appliquent qu'en cas de prise en charge, et seulement si la personne
     n'a pas demandé à limiter les animations. Le comptage des chiffres et
     les apparitions `data-reveal` restent délibérément au JavaScript, pour
     n'avoir qu'un seul déclencheur et éviter les doubles animations. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      /* ─── Parallaxe du bandeau d'appel : RETIRÉE, volontairement ───────
         Une version pilotée par `view()` a été essayée ici. Mesurée, elle
         ne fonctionnait pas : la progression de la timeline restait très
         en deçà de sa plage et l'image demeurait clouée à son image-clé de
         départ. Pire, comme l'animation portait `fill: both`, elle
         écrasait le `transform` de base — donc elle DÉSACTIVAIT la
         parallaxe JavaScript qui, elle, fonctionne, et ce chez les ~85 %
         de visiteurs dont le navigateur gère `view()`.
         Un enhancement qui casse le chemin qu'il est censé enrichir n'est
         pas un enhancement. Le script reste seul maître de cet effet.
         Ne pas rétablir sans mesurer la progression réelle de la timeline,
         pas seulement sa présence. */

      /* Jauge de lecture : la barre suit exactement l'ascenseur, sans
         qu'aucune valeur n'ait besoin d'être recalculée en JavaScript.
         Celle-ci EST mesurée comme fonctionnelle : la mise à l'échelle
         suit la position de défilement. */
      .reading-progress {
        animation: read-progress var(--ease-linear) both;
        animation-timeline: scroll(root block);
        transition: none;
      }
      @keyframes read-progress {
        from { transform: scaleX(0); }
        to   { transform: scaleX(1); }
      }
    }
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  7. UTILITIES — classes d'appoint                                        ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@layer utilities {
  .center { text-align: center; }
  .center > .lead { margin-inline: auto; }

  .prose { max-width: var(--width-prose); }

  /* Mot composé insécable dans un titre (« faut-il », « Pré-comptabilité ») :
     le navigateur coupe volontiers après un trait d'union, et laissait
     « faut- » en fin de ligne à 375 px. Pas de `hyphens: auto` sur les
     titres : la césure y ajouterait des coupures, au lieu d'en retirer. */
  .nowrap { white-space: nowrap; }
  .prose--center { margin-inline: auto; }

  .mt-sm { margin-top: var(--space-sm); }
  .mt-md { margin-top: var(--space-md); }
  .mt-lg { margin-top: var(--space-lg); }
  .mt-xl { margin-top: var(--space-xl); }

  /* Visible uniquement pour les lecteurs d'écran.
     Le 1px et le -1px sont la technique elle-même, pas un réglage. */
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* Seul `!important` du site : l'attribut natif [hidden] doit l'emporter
     sur les `display` posés hors couche par les paliers responsive. */
  [hidden] { display: none !important; }

  /* Retiré sur téléphone (sous 48 rem), et seulement là. Posée dans le
     balisage plutôt que décrite par un sélecteur : c'est en lisant la page
     qu'on doit voir ce qui disparaît sur un petit écran. Sert aux appels
     en double de l'accueil et aux photos que le lot « accueil mobile » a
     retirées. `display: none` et non un masquage visuel : une image
     différée (`loading="lazy"`) ainsi masquée n'est pas téléchargée.
     Même seuil que le palier « Téléphone » de la section 8. */
  @media (max-width: 47.9375rem) {
    .hide-on-phone { display: none; }
  }

  /* ╔═══════════════════════════════════════════════════════════════════════╗
     ║  FILET DE SÉCURITÉ — une panne doit échouer en VISIBLE                ║
     ╚═══════════════════════════════════════════════════════════════════════╝
     Les apparitions au défilement reposent sur une classe posée par le
     script. Tant que l'état caché était le défaut, la moindre panne de
     JavaScript effaçait la page entière : plus de services, plus de FAQ,
     plus de formulaire de contact.

     Le rapport est donc inversé. `boot.v1.1.0.js` pose la classe `js` sur <html>
     avant le premier rendu ; c'est SA PRÉSENCE qui autorise l'état animé.
     Sans elle, tout état d'attente est annulé et la page s'affiche
     complètement, simplement sans mouvement.

     Ce filet vit dans `utilities`, la dernière couche déclarée : il
     l'emporte sur `components`, `sections` et `animations` par le seul jeu
     des couches, sans un seul `!important`.

     Ce qui n'est délibérément PAS révélé ici, parce que masqué est l'état
     JUSTE et non un état d'attente :
       · .to-top ............ commande sans effet en l'absence de script.
         Depuis qu'il porte `visibility: hidden`, il n'est même plus dans le
         parcours clavier : un bouton mort ne s'offre plus à la tabulation.
       · .card::after, .icon-badge::after, .metric::after ... halos de
         survol, dont le repos est précisément l'invisibilité
     ---------------------------------------------------------------------- */

  /* 1 — États d'attente complets : opacité, déplacement, découpe, animation */
  html:not(.js) [data-reveal],
  html:not(.js) [data-reveal] .figure__overlay,
  html:not(.js) [data-reveal] .pull-quote,
  html:not(.js) .eyebrow::before,
  html:not(.js) .step::before,
  html:not(.js) .step::after,
  html:not(.js) .hero__content > *,
  html:not(.js) .hero__content h1,
  html:not(.js) .hero__card,
  html:not(.js) .cta-band__inner > * {
    opacity: 1;
    transform: none;
    clip-path: none;
    animation: none;
    transition: none;
    will-change: auto;
  }

  /* 2 — Découpe seule : ces éléments gardent leurs effets de survol, qui
         ne dépendent d'aucun script. */
  html:not(.js) [data-reveal] h2,
  html:not(.js) [data-reveal] .figure img {
    clip-path: none;
  }

  /* 3 — Le fondu d'image attend `is-loaded`, posé par le script après
         décodage. Sans script, l'image s'affiche telle quelle. */
  html:not(.js) .fade-img { opacity: 1; }

  /* 4 — Le motif décoratif retrouve SON opacité propre, pas une opacité
         pleine qui le rendrait envahissant. */
  html:not(.js) .dot-pattern {
    opacity: var(--dots-opacity);
    transform: none;
    transition: none;
  }


  /* ─── Ceinture en plus des bretelles ────────────────────────────────────
     Si le navigateur bloque les scripts d'une façon qui empêche même
     `boot.v1.1.0.js` de s'exécuter, `scripting: none` le dit directement. Les
     déclarations sont l'écho exact du filet ci-dessus : toute modification
     de l'un doit être reportée sur l'autre. */
  @media (scripting: none) {
    [data-reveal],
    [data-reveal] .figure__overlay,
    [data-reveal] .pull-quote,
    .eyebrow::before,
    .step::before,
    .step::after,
    .hero__content > *,
    .hero__content h1,
    .hero__card,
    .cta-band__inner > * {
      opacity: 1;
      transform: none;
      clip-path: none;
      animation: none;
      transition: none;
      will-change: auto;
    }
    [data-reveal] h2,
    [data-reveal] .figure img { clip-path: none; }
    .fade-img { opacity: 1; }
    .dot-pattern {
      opacity: var(--dots-opacity);
      transform: none;
      transition: none;
    }
  }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  8. RESPONSIVE                                                           ║
   ║  Le gros du site s'adapte seul (grilles auto-fit, clamp et container).   ║
   ║  Ces trois paliers ne traitent que les cas qui l'exigent vraiment.       ║
   ║  Les largeurs sont écrites en clair : la syntaxe des media queries       ║
   ║  n'accepte pas de variable.                                              ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */

/* — Tablette large : la colonne d'introduction des services passe dessus — */
@media (max-width: 1000px) {
  /* Un seul seuil pour toutes les mises en colonnes de la page : sous ce
     palier, la colonne étroite ne loge plus une photo lisible et la
     colonne large tomberait en colonnes de journal. Partager le seuil
     avec les services évite que la page se replie en deux temps. */
  .side-layout { grid-template-columns: 1fr; }
  /* Les blocs redeviennent pleine largeur : leur mesure de lecture
     reprend tout son sens et leur plafond est rendu. Voir la note sur
     .side-layout :is(.faq, .channels) dans la couche `sections`. */
  .side-layout .faq { max-width: var(--width-faq); }
  .side-layout .channels { max-width: var(--width-prose); }
  /* En pile, l'image n'a plus de colonne à accompagner : elle resterait
     collée par-dessus les questions qu'elle est censée escorter. Elle
     reprend sa place dans le flux, et son format carré. */
  .side-layout--media-end > .figure--solo {
    /* `relative` sans décalage, PAS `static`. Les deux annulent le collage
       à l'identique — mais `static` retirait aussi à la figure son rôle de
       bloc conteneur, et l'encadré posé dessus (`.figure__overlay`, en
       `absolute`) se retrouvait sans ancêtre positionné : il se calait
       alors sur la page entière et débordait de la photo jusqu'à 251 px.
       Mesuré sur le site en ligne, de 501 à 1000 px — au-delà la figure
       est collante donc positionnée, en dessous l'encadré repasse lui-même
       en `static`. La bande cassée était exactement celle des tablettes. */
    position: relative;
    /* ⚠ LE DÉCALAGE DU COLLAGE DOIT ÊTRE RENDU, PAS SEULEMENT LE COLLAGE.
       `inset-block-start` sert d'ancre de repos à la figure collante du
       palier large. Sur un élément `relative` il ne s'annule pas tout
       seul : il devient un décalage de PEINTURE, qui déplace la figure
       vers le bas sans que la grille en sache rien. La rangée gardait donc
       sa hauteur d'origine et la photo en sortait par le bas de la valeur
       exacte de l'ancre — 106 px, soit --header-height + --space-lg — puis
       traversait la section pour venir toucher la bande d'appel qui suit.
       Mesuré du haut de la pile jusqu'à 1000 px. */
    inset-block-start: auto;
    aspect-ratio: var(--figure-solo-ratio);
    /* La hauteur figée du palier large (celle des questions repliées) n'a
       plus de colonne à égaler : le carré reprend la main. */
    height: auto;
  }
  .side-layout--media-end > .figure--solo img { object-position: center; }
  /* En pile, la figure n'accompagne plus aucune colonne : son calage sur un
     bord devient arbitraire et laisse jusqu'à 455 px de blanc à côté d'elle.
     Le palier remettait bien la photo à plat, mais oubliait sa marge — ce
     sont les « photos mal agencées » constatées entre 466 et 1001 px. */
  :is(.side-layout, .services__layout) .figure--solo { margin-inline: auto; }

  .services__layout { grid-template-columns: 1fr; }
  .services__intro { position: static; }
  /* Colonne unique : il n'y a plus de deuxième colonne où ranger la clôture,
     et la réclamer en créerait une implicite qui casserait la pile. Le bloc
     d'introduction ne colle plus non plus : le conflit n'existe pas ici. */
  .services__layout > p { grid-column: auto; }
}

/* — En-tête : le palier vient de la LARGEUR D'UN LIBELLÉ ————————————————
   ⚠ CE SEUIL N'EST PAS CELUI DES MISES EN COLONNES CI-DESSUS, ET IL NE DOIT
   PAS Y RETOURNER. Les deux blocs répondent à deux questions différentes :
   celui du dessus demande « à partir de quand une grille tient-elle sur
   deux colonnes », celui-ci demande « à partir de quand la ligne de
   l'en-tête peut-elle porter le libellé du bouton en clair ». Les avoir
   empilés sous un même 1000 px a coûté le défaut décrit juste après.

   LE CALCUL, parce qu'un seuil sans son calcul est un seuil que le prochain
   contributeur déplacera au hasard — c'est exactement ce qui s'est passé.
   Ce que la ligne réclame quand le bouton porte son libellé :

       logo ................................ 180,0 px  (--logo-width)
       navigation (sept liens) ............. 494,3 px
       bouton « Réserver un appel découverte » 249,0 px
       deux écarts de --space-md ............ 48,0 px
       ────────────────────────────────────────────────
       total incompressible ................ 971,3 px

   Tous ces éléments portent `flex: none` : aucun ne peut céder. Or `.wrap`
   n'offrait que 925 px à 1001 px de large — la gouttière fluide y vaut
   38 px de chaque côté. Il manquait donc 46,3 px, et le bouton sortait :
   8 px de débordement du document à 1001 px, décroissant jusqu'à 1 px à
   1009 px, puis la gouttière droite mangée jusqu'à 1054 px (11,8 px restant
   à droite contre 38,7 px à gauche à 1024 px — l'en-tête visiblement
   décentré). Constaté sur les QUATRE pages, l'accueil comprise.

   Premier palier réellement sain, mesuré au pixel : 1055 px. On prenait
   1070 px, soit 18,7 px de jeu — de quoi absorber deux caractères de plus
   au libellé ou un rendu de police un peu plus large sur un autre système.
   Au-delà de 1066,7 px la gouttière plafonne à 40 px, la place ne dépend
   donc plus que de la largeur de fenêtre.

   ⚠ 1070 EST DEVENU 1150 LE JOUR OÙ LA LOUPE EST ARRIVÉE. Un quatrième
   élément de 44 px, plus son écart de 24, ajoutent 68 px à un total qui
   n'en avait que 18,7 de marge. Mesuré au navigateur, la barre débordait de
   55 px à 1071 px, de 29 px à 1100 px, et redevenait juste à 1132 px. On
   prend 1150, soit 16 px de jeu — la même marge qu'auparavant.

   CE QUE CELA COÛTE, EN CLAIR : entre 1071 et 1150 px, l'appel à l'action
   se lit désormais en pastille au lieu de porter son libellé. Le libellé
   est ce qui décide : « Réserver un appel découverte » mesure 249 px pour
   28 signes. À 187 px — soit environ 21 signes — le seuil redescendrait à
   1070. C'EST UNE QUESTION POUR LE RÉDACTEUR, PAS UN RÉGLAGE À PRENDRE
   SEUL : on ne rétrécit pas un texte pour le faire entrer, on l'écrit plus
   court ou l'on change la mise en page.

   CE QUI FAIT BOUGER CE SEUIL : la longueur du libellé (mesuré, 249 px pour
   28 signes), le nombre de liens de navigation (494,3 px pour sept, un
   huitième le remonterait d'environ 70 px) et le nombre de commandes de la
   barre. Le seuil est un littéral : une condition de @media ne peut pas
   lire une variable CSS.

   ⚠ 1150 EST DEVENU 1216 AVEC LE HUITIÈME LIEN (« Outils »). Mesuré au
   navigateur, la barre débordait de 40 px à 1151 px et redevenait juste à
   1195 px. On prend 1216, soit 22 px de jeu. */
@media (max-width: 1216px) {
  /* Le bouton ne disparaît pas : il se réduit à son icône. Il manquait
     entre 861 et 1000 px, là où le burger n'était pas encore apparu —
     l'en-tête n'avait alors aucun appel à l'action, sur la plage même des
     tablettes tactiles. Réduit, il tient à côté du burger sans rien
     comprimer, et il reste atteignable sans ouvrir le menu. */
  .header__cta {
    display: grid;
    place-items: center;
    width: var(--header-cta-compact-size);
    height: var(--header-cta-compact-size);
    padding: 0;
    border-radius: 50%;
  }
  .header__cta-icon { display: block; }
  /* Masqué à l'œil, conservé pour les lecteurs d'écran — mêmes propriétés
     que .visually-hidden, appliquées ici sans changer de classe. */
  .header__cta-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* — En-tête : la pastille reprend un mot, là où la ligne le loge (1.9.0.2) —
   Une icône de calendrier seule ne dit pas « réserver un appel » : à côté
   d'elle, un libellé court, « Réserver » (--header-cta-short-label). Le
   libellé complet du balisage reste masqué à l'œil et fait toujours le nom
   accessible : le mot affiché est un contenu généré à texte de
   remplacement vide (`/ ''`), qu'aucun lecteur d'écran n'ajoute au nom.

   DEUX PLAGES, ET CE N'EST PAS UN OUBLI. La pastille passe de 44 à 122 px.
   Mesuré au navigateur, place restante sur la ligne :
       481 px .......   4 px   (logo réduit, loupe, pastille, burger)
       500 px .......  22 px   ← début de la première plage
       561 px .......  49 px   (le logo reprend ses 180 px)
       980 px ....... 443 px   (dernier palier à burger)
       981 px ....... − 75 px  la navigation reparaît, huit liens en ligne
      1024 px ....... − 31 px
      1066 px .......   4 px
      1080 px .......  17 px   ← début de la seconde plage
   De 981 à 1079 px, la navigation en ligne prend la place : la pastille y
   reste ronde, comme sous 500 px. Allonger le libellé ou ajouter un lien
   déplace ces deux seuils : on remesure.

   Derrière `@supports` : un navigateur qui ignore le texte de remplacement
   lirait le mot deux fois (« Réserver un appel découverte Réserver ») ; il
   garde la pastille ronde, telle qu'avant. */
@media (min-width: 500px) and (max-width: 980px), (min-width: 1080px) and (max-width: 1216px) {
  @supports (content: 'x' / '') {
    .header__cta {
      display: inline-flex;
      width: auto;
      padding-inline: var(--space-sm);
      border-radius: var(--radius-signature);
    }
    /* `::before` et non `::after` : celui-ci porte déjà le reflet de
       survol des boutons. `order` le replace après l'icône. */
    .header__cta::before {
      content: var(--header-cta-short-label) / '';
      order: 1;
    }
  }
}

/* — Services : trois cartes réclament 806 px, il faut les leur laisser ———
   À 1002 px, .services__layout reprenait ses deux colonnes et ne laissait
   plus que 627 px aux cartes : elles retombaient de trois à deux par ligne.
   Élargir la fenêtre dégradait donc la page — le défaut le plus déroutant
   à l'usage. On prolonge la colonne unique jusqu'à ce que la colonne large
   reloge réellement trois cartes. */
@media (prefers-reduced-motion: reduce) {
  /* Le seul mouvement du site qui échappait encore à cette préférence, et
     c'est le plus susceptible de provoquer un malaise vestibulaire. */
  html { scroll-behavior: auto; }
}

@media (min-width: 1001px) and (max-width: 1249px) {
  .services__layout { grid-template-columns: 1fr; }
  .services__intro { position: static; }
  .services__layout > p { grid-column: auto; }
  /* La pile prolongée doit se comporter comme la pile qu'elle prolonge.
     Ce bloc recopiait trois des réglages du palier 1000 et oubliait le
     quatrième, le centrage de la photo : elle gardait le calage à droite
     des images d'appui, et laissait environ 520 px de blanc à sa gauche
     (mesuré à 1024 px). Le même centrage qu'en dessous de 1001 px, plutôt
     qu'une composition propre à cette plage de 250 px : une mise en page
     de plus pour une seule bande de largeurs serait une dette, pas un
     réglage. */
  .services__intro .figure--solo { margin-inline: auto; }
}

/* — Tablette / mobile : bascule vers le menu déroulant ———————————————————
   ⚠ CE PALIER VALAIT 860 PX ET IL A ÉTÉ SÉPARÉ EN DEUX, PAS DÉPLACÉ.
   860 répondait à DEUX questions à la fois, et le hasard voulait qu'elles
   aient la même réponse : « la barre porte-t-elle encore sept liens ? » et
   « la série d'étapes se lit-elle en ligne ? ». La loupe a déplacé la
   première sans toucher à la seconde ; les deux sont donc désormais
   écrites séparément, chacune avec son calcul.

   LE CALCUL DE CELUI-CI. Ce que la barre réclame, menu déplié :

       logo ......................... 180 px  (--logo-width)
       navigation (sept liens) ...... 398 px + 6 écarts fluides (1,6vw)
       loupe ......................... 44 px  (--header-search-size)
       pastille d'appel à l'action ... 44 px  (--header-cta-compact-size)
       trois écarts de --space-md .... 72 px

   Mesuré au navigateur : la barre débordait de 28 px à 861 px, de 12 px à
   880 px, et redevenait juste à 894 px. On prend 920, soit 22 px de jeu —
   la marge qu'avait 860 avant l'arrivée de la loupe (16 px). Aucun appareil
   courant ne vit dans l'intervalle 861-920 : la tablette la plus large en
   portrait mesure 820 px.

   L'AUTRE MOITIÉ DU PALIER — filets des étapes et citation détachée — est
   restée à 860 px, plus bas dans ce fichier. Elle est appariée au seuil de
   `.steps` (`@media (min-width: 861px)`), et cette paire-là n'a pas bougé :
   déplacer l'une sans l'autre ferait tracer des traits qui ne relient
   rien. C'est le défaut d'origine, on ne le rouvre pas. */
/* ⚠ 920 EST DEVENU 980 AVEC LE HUITIÈME LIEN (« Outils »). Mesuré au
   navigateur, la barre débordait de 30 px à 921 px et redevenait juste à
   957 px. On prend 980, soit 22 px de jeu. Le palier sans JavaScript,
   plus bas, suit la même valeur. */
@media (max-width: 980px) {
  .burger { display: grid; order: 3; }

  .nav {
    position: absolute;
    inset-inline: 0;
    inset-block-start: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-2xs) var(--space-gutter) var(--space-md);
    background-color: var(--surface-page);
    border-bottom: var(--stroke-hairline) solid var(--border-subtle);
    box-shadow: var(--shadow-md);
    /* Repli animé : le menu se déroule au lieu d'apparaître d'un coup */
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: hidden;
    transition: grid-template-rows var(--duration-base) var(--ease-out),
      opacity var(--duration-fast) var(--ease-out),
      visibility 0s var(--ease-linear) var(--duration-base);
  }
  .nav.is-open {
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
    transition-delay: 0s;
  }
  /* En panneau, les liens s'empilent et ce sont leurs filets qui les
     séparent : l'espacement de la vue bureau n'a plus lieu d'être. */
  .nav__list {
    display: grid;
    gap: 0;
    overflow: hidden;
  }
  /* En panneau, le lien occupe toute la colonne : son filet devient un vrai
     séparateur d'un bord à l'autre, et la cible tactile couvre la largeur
     entière plutôt que les seuls caractères. */
  .nav__link {
    display: block;
    padding-block: var(--space-xs);
    border-bottom: var(--stroke-hairline) solid var(--border-subtle);
    font-size: var(--text-base);
  }
  .nav__link::after { display: none; }
  /* L'appel à l'action du panneau reprend sa place, celui de l'en-tête
     étant masqué à ce palier. */
  .nav > .button {
    display: inline-flex;
    margin-top: var(--space-sm);
  }
}

/* — Le même passage, mais pour le CONTENU : la série d'étapes s'empile ————
   Seuil apparié à celui de `.steps` (`@media (min-width: 861px)`, couche
   `sections`) et à lui seul. Il ne suit PAS le palier de la barre
   ci-dessus : voir la note qui ouvre celui-ci. */
@media (max-width: 860px) {
  /* Les filets entre étapes n'ont plus de sens en colonne unique */
  .step::after { display: none; }

  /* La citation sort de la photo et se place en dessous */
  .pull-quote {
    position: static;
    width: auto;
    margin-top: var(--space-sm);
    border-radius: var(--radius-md);
  }
}

/* — Sans JavaScript, sous le palier : le menu se déplie définitivement ——————
   Le panneau déroulant est commandé par le script. Sans lui, le bouton reste
   là mais ne répond plus : le visiteur clique, rien ne bouge, il en conclut
   que le site est cassé. Un bouton mort est pire qu'un bouton absent.

   On applique donc ici la même règle que pour les apparitions — une panne
   doit échouer en VISIBLE : le panneau redevient une simple liste posée sous
   le logo, et le bouton qui ne commande plus rien disparaît.

   Ce bloc est volontairement HORS COUCHE et placé après le palier ci-dessus :
   celui-ci est hors couche lui aussi, l'ordre des couches ne peut donc pas
   trancher. C'est la position dans le fichier et la spécificité qui le font,
   sans `!important`.

   ⚠ SON SEUIL EST CELUI DE LA BARRE (980), PAS CELUI DU CONTENU (860). Il
   annule des règles posées par le palier de la barre : les deux doivent
   couvrir exactement la même plage, sans quoi la bande 861-980 garderait un
   panneau replié que plus aucun bouton ne commande. */
@media (max-width: 980px) {
  /* L'en-tête ne peut plus coller : déplié, il mangerait tout l'écran */
  html:not(.js) .header { position: static; }

  /* Le logo garde sa ligne, la navigation passe dessous */
  html:not(.js) .header__inner { flex-wrap: wrap; }

  html:not(.js) .burger { display: none; }

  html:not(.js) .nav {
    position: static;
    flex-basis: 100%;
    grid-template-rows: 1fr;
    /* La gouttière latérale est déjà portée par .wrap : on ne la répète pas */
    padding: 0 0 var(--space-2xs);
    background-color: transparent;
    border-bottom: 0;
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    transition: none;
  }
  html:not(.js) .nav__list { overflow: visible; }
}

/* — Téléphone : la ligne de l'en-tête ne porte plus TROIS commandes ————————
   LE CALCUL, PARCE QUE C'EST LUI QUI TRANCHE, PAS LE GOÛT.
   Ce que la barre réclame une fois le menu replié, la loupe posée :

       logo ......................... 150 px  (--logo-width-sm, palier 560)
       loupe ......................... 44 px  (--header-search-size)
       pastille d'appel à l'action ... 44 px  (--header-cta-compact-size)
       burger ........................ 44 px  (--burger-size)
       trois écarts de --space-md .... 72 px
       ─────────────────────────────────────
       total ........................ 354 px

   Ce que la page offre : la largeur moins deux gouttières, soit W − 40 px
   sous 400 px de large et 0,94 × W − 16 au-delà. Les quatre éléments ne
   tiennent donc qu'à partir de 394 px — au MILIEU du parc de téléphones :
   un iPhone 15 (390) échouerait, un Pixel 8 (412) passerait. Un seuil posé
   là ferait deux en-têtes différents pour deux appareils identiques.

   CE QUI S'EFFACE, C'EST L'APPEL À L'ACTION, ET PAS LA LOUPE.
   L'appel a un DOUBLE dans le panneau du menu (`.nav > .button`, palier
   861), un autre dans le héros, un troisième dans le bandeau d'appel : le
   retirer de la barre coûte une tape. La loupe, elle, n'a aucun double —
   c'est une décision de balisage, prise pour ne pas faire annoncer deux
   fois le même nom accessible — et la retirer supprimerait la recherche
   sur téléphone, c'est-à-dire exactement là où l'on cherche.

   CE PALIER RÉPARE AUSSI UN DÉFAUT CONNU. La note de `.nav > .button`
   décrit la redondance mesurée à 390 px, menu ouvert : la pastille dans la
   barre ET le bouton du panneau portent le même nom accessible, annoncé
   deux fois dans le même « banner ». Sous 480 px, la pastille n'est plus
   là : la redondance disparaît avec elle.

   POURQUOI 480 ET NON 394, LE SEUIL EXACT. 480 px est la première valeur
   ronde au-dessus de TOUT le parc en portrait (430 px pour le plus large
   des iPhone). Tous les téléphones se comportent alors pareil, et il reste
   82 px de jeu à 481 px — de quoi absorber un logo plus large ou un
   libellé plus long sans rouvrir le sujet.

   CE QUE CE PALIER NE RÉPARE PAS, et c'est chiffré plutôt que masqué : la
   ligne tient jusqu'à 310 px (270 px réclamés pour 270 offerts, écart
   ramené à --space-sm par le palier 360). En dessous — 300 px, et 280 px
   pour un Galaxy Fold replié — un logo et deux commandes ne tiennent plus.
   C'est le défaut déjà consigné sur le palier 360 : la loupe ne l'aggrave
   pas, elle prend exactement la place que la pastille libère. */
@media (max-width: 480px) {
  .header__cta { display: none; }
}

/* — Petit mobile ————————————————————————————————————————————————————— */
@media (max-width: 560px) {
  .topbar .socials { display: none; }
  .hero__divider { display: none; }
  .hero__card { margin-block-start: calc(var(--space-lg) * -1); }
  .header__brand img { width: var(--logo-width-sm); }
  .figure__overlay { position: static; width: auto; border-radius: 0; }
}

/* — Tablette portrait : deux photos en bandeau, le texte dessous (1.9.0.2) —
   LOT « TABLETTE PORTRAIT », validé par Greg. Entre 768 et 1000 px, « À
   propos » et « La différence » gardaient leurs deux colonnes, chacune trop
   étroite pour son rôle. Mesuré à 768 px avant le lot :
       À propos ....... photo 338 × 1 253 px, texte sur 338 px de large
                        (une quarantaine de signes par ligne) ;
       La différence .. photo 338 × 371 px, centrée face à 924 px de texte.
   La photo devient un bandeau 16:9 sur toute la largeur du contenu
   (--photo-ratio-band, mises au point dans tokens), le texte et ses blocs
   passent dessous, à pleine mesure.

   POURQUOI CETTE PLAGE. Le bas suit le lot téléphone (47.9375rem), qui a
   ses propres formats ; le haut suit le palier 1000, celui de toutes les
   mises en colonnes de la page. Au-delà, l'iPad à l'horizontale
   (1024 px) garde les deux colonnes du bureau : 457 px chacune, une
   colonne de lecture correcte. Hors couche et placé après les paliers 1000
   et 860, dont il reprend des réglages : c'est la position qui tranche. */
@media (min-width: 48rem) and (max-width: 1000px) {
  .split { grid-template-columns: 1fr; }
  /* Le texte passe sous la photo, sur toute la largeur : 706 px à 768,
     924 px à 1000, soit 90 à 115 signes par ligne pour le corps de texte.
     Les paragraphes reprennent la mesure de lecture du chapô (--measure) ;
     titres, piliers et comparatif gardent la largeur, ils ne sont pas des
     lignes longues. Le paragraphe du parcours, logé dans son repli
     (.about-career), en fait partie. */
  .split > :not(.split__media) > p,
  .split .about-career > p { max-width: var(--measure); }

  /* À propos : le plancher de format de l'image devient le bandeau. La
     figure s'étire toujours sur sa rangée, qui n'est plus que la sienne.
     Le cartouche de valeurs reste posé dans l'angle : il occupe le bas
     droit du bandeau (310 × 148 px à 768), sous le visage, et le texte n'a
     pas de quoi l'accueillir mieux. */
  .split--stretch .figure img {
    aspect-ratio: var(--photo-ratio-band);
    object-position: var(--figure-about-focus-band);
  }

  /* La différence : le format passe de la figure au bandeau. */
  .figure--avatar-quote { aspect-ratio: var(--photo-ratio-band); }
  .figure--avatar-quote img { object-position: var(--figure-why-focus-band); }

  /* La citation signée reste sur la photo, ce que le palier 860 lui
     retirait : elle tombait sous l'image, au-dessus du titre de la
     section, et se lisait avant lui. Sur le bandeau, elle se pose à
     droite, sur les lumières du fond, loin du visage (à gauche de la
     source) : 320 × 179 px sur 706 × 397 à 768 px. On rend exactement la
     disposition du bureau. */
  .figure--avatar-quote .pull-quote {
    position: absolute;
    width: min(80%, var(--width-pull-quote));
    margin-top: 0;
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
  }

  /* Services, FAQ, contact : les trois dernières photos d'appui prennent le
     même bandeau (1.9.0.3). Greg, sur tablette : « pourquoi la photo d'Ana
     n'est pas comme les autres sections ». Mesuré avant, à 768 et 820 px :
     trois carrés de 420 × 420 centrés, 143 à 200 px de blanc de chaque
     côté, entre deux bandeaux pleine largeur.
     Le plafond --width-figure-solo tombe : la figure prend la largeur du
     contenu. Sélecteurs repris des paliers 1000 et 1001-1249, à
     spécificité égale : la position dans le fichier tranche. */
  :is(.services__intro, .side-layout) > .figure--solo {
    width: 100%;
    aspect-ratio: var(--photo-ratio-band);
  }
  .services__intro > .figure--solo img {
    object-position: var(--figure-services-focus-band);
  }
  .side-layout--media-start > .figure--solo img {
    object-position: var(--figure-contact-focus-band);
  }
  /* FAQ : bandeau plus bas, voir --photo-ratio-band-short. La carte
     « Posez-la-moi » garde sa place en bas à droite, sous la table, loin
     du visage. */
  .side-layout--media-end > .figure--solo {
    aspect-ratio: var(--photo-ratio-band-short);
  }
  .side-layout--media-end > .figure--solo img {
    object-position: var(--figure-faq-focus-band);
  }
}

/* — Bouton pause des bandes défilantes : un rond de verre (1.9.0.2) ————————
   Sur tablette, le bouton n'était qu'un glyphe « ‖ » sans cadre au repos :
   rien ne le désignait comme une commande. Il prend le matériau des ronds
   flottants (.to-top) : même taille, même voile givré léger, mêmes reflets
   — recette « Matériau verre » plus haut, dont il ne reprend que l'état
   clair : il vit sur les fonds clairs des sections, le ton sombre ne le
   concerne pas.
   48 rem et plus : sur téléphone, la bande d'outils garde son bouton
   discret, le lot mobile n'en a pas décidé autrement. Avec une souris, le
   bouton n'existe pas (voir la note de .quote-track__control). L'anneau
   de focus reste celui du site. */
@media (min-width: 48rem) {
  .quote-track__toggle {
    position: relative;
    isolation: isolate;
    width: var(--to-top-size);
    height: var(--to-top-size);
    border: none;
    border-radius: var(--radius-pill);
    background-color: var(--glass-bg-frosted-sm);
    background-image: var(--glass-gloss-sm);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate))
      brightness(var(--glass-brightness));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate))
      brightness(var(--glass-brightness));
    box-shadow: var(--glass-shadow-sm), var(--glass-sheen-sm);
    color: var(--text-strong);
  }
  /* Le liseré lumineux, même double masque que les ronds. */
  .quote-track__toggle::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    padding: var(--stroke-hairline);
    border-radius: inherit;
    background: var(--glass-rim-sm);
    -webkit-mask: linear-gradient(var(--brand-black) 0 0) content-box,
      linear-gradient(var(--brand-black) 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(var(--brand-black) 0 0) content-box exclude,
      linear-gradient(var(--brand-black) 0 0);
    pointer-events: none;
  }
  /* Sans backdrop-filter : l'aplat opaque de la recette. */
  @supports not ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
    .quote-track__toggle { background-color: var(--glass-bg-fallback); }
  }
}

/* — « Mon parcours » : un repli sur téléphone, rien au-delà (1.9.0.2) ————
   Le paragraphe du parcours d'Ana, dans « À propos », est enveloppé d'un
   <details data-fold-phone open>. Le script le referme au chargement sur
   téléphone et le garde ouvert au-dessus ; sans script, il reste ouvert.
   Sous 48 rem, le résumé devient une ligne à toucher (44 px de cible), son
   propre chevron — celui de la FAQ — et l'anneau de focus double des
   ronds. À partir de 48 rem, le résumé disparaît : le repli ouvert se lit
   exactement comme le paragraphe nu d'avant, au pixel près (mesuré à 768,
   1000 et 1280 px). Seuil apparié au lot téléphone et au script
   (`carousel.query`). */
@media (max-width: 47.9375rem) {
  .about-career > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    min-height: var(--fold-summary-min-height);
    /* L'écart que le paragraphe portait seul : il est désormais DANS le
       repli, le résumé le reprend au-dessus de lui. */
    margin-block-start: var(--space-md);
    padding-block: var(--space-xs);
    /* Le chevron se tient en retrait du bord : l'anneau de focus, posé à
       3 px dehors, venait sinon le toucher. Le libellé reste aligné sur
       la colonne. */
    padding-inline-end: var(--space-2xs);
    border-block: var(--stroke-hairline) solid var(--border-subtle);
    color: var(--text-accent);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    list-style: none;
    transition: var(--transition-colors);
  }
  .about-career > summary::-webkit-details-marker { display: none; }
  .about-career > summary::marker { content: ''; }
  .about-career > summary:hover { color: var(--text-strong); }
  /* Anneau double : l'intervalle blanc sous l'anneau du site, comme les
     ronds — visible quel que soit le fond. */
  .about-career > summary:focus-visible {
    box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-gap-color);
  }
  /* Chevron de la FAQ : mêmes jetons, même rotation à l'ouverture. */
  .about-career > summary::after {
    content: '';
    flex: none;
    width: var(--faq-chevron-size);
    height: var(--faq-chevron-size);
    border-inline-end: var(--stroke-base) solid var(--brand-mint-500);
    border-block-end: var(--stroke-base) solid var(--brand-mint-500);
    transform: rotate(45deg) translateY(calc(var(--faq-chevron-nudge) * -1));
    transition: transform var(--duration-base) var(--ease-out);
  }
  .about-career[open] > summary::after {
    transform: rotate(-135deg) translateY(calc(var(--faq-chevron-nudge) * -1));
  }
  @media (prefers-reduced-motion: reduce) {
    .about-career > summary::after { transition: none; }
  }
}
@media (min-width: 48rem) {
  .about-career > summary { display: none; }
}

/* — Téléphone : l'accueil en rangées, en vignettes, en rythme serré ————————
   LOT « ACCUEIL MOBILE » (octobre 2026), validé par Greg sur prototypes.
   Mesuré avant le lot à 375 px : 14 387 px de page, soit 21 écrans, dont
   sept photos carrées pleine largeur et onze appels à réserver.

   UN SEUL SEUIL POUR TOUT LE LOT : 47.9375rem (767 px). Il est partagé avec
   le script (`carousel.query` dans config) : les pastilles n'existent que
   là où les cartes sont en rangée. Les deux valeurs se déplacent ENSEMBLE,
   ou pas du tout. La tablette (768 px) et le bureau ne voient rien de ce
   bloc.

   HORS COUCHE, COMME LES AUTRES PALIERS DE CETTE SECTION, et placé après
   eux : il reprend des réglages qu'ils posent eux-mêmes hors couche (la
   figure de la FAQ au palier 1000, le cartouche au palier 560) — l'ordre
   des couches ne pourrait pas trancher. Ce qui relève d'un ÉTAT d'attente
   (le fondu des vignettes) reste, lui, dans `animations` : un état
   d'attente doit pouvoir être annulé par le filet de sécurité des
   utilitaires, ce qu'une règle hors couche empêcherait. */
@media (max-width: 47.9375rem) {

  /* ─── Rythme vertical, accueil seulement ──────────────────────────────
     Sections à 40 px au lieu de 56, grands écarts à 32 au lieu de 48,
     gouttières de colonnes à 24. Restreint à l'accueil (`.page-accueil`
     sur <body>) : c'est la page mesurée et validée ; l'étendre aux autres
     pages est une décision à part, pas un effet de bord. */
  .page-accueil .section { padding-block: var(--space-section-compact); }
  .page-accueil .mt-xl { margin-top: var(--space-lg); }
  .page-accueil :is(.split, .side-layout, .services__layout) { gap: var(--space-md); }

  /* Les divisions en deux colonnes s'empilent sur toute la plage. Leur
     grille auto-fit en logeait encore deux entre 729 et 767 px : les
     vignettes ci-dessous y auraient laissé une colonne presque vide face
     au texte. Un seul comportement pour tout le téléphone. */
  .split { grid-template-columns: 1fr; }

  /* ─── Carrousels : services et témoignages ────────────────────────────
     Une rangée qu'on fait défiler au doigt, la carte suivante amorcée sur
     le bord (voir --carousel-item-width). Ancrage `proximity` et non
     `mandatory` : un geste court peut s'arrêter entre deux cartes, et une
     carte plus haute que l'écran reste lisible jusqu'en bas sans que le
     navigateur la ramène de force.

     LA RANGÉE DÉBORDE DE LA COLONNE JUSQU'AUX BORDS DE L'ÉCRAN, PAS AU-DELÀ.
     Marges négatives d'une gouttière, rendues en rembourrage : les cartes
     partent de la colonne de texte et filent jusqu'au bord, sans que la
     page gagne un pixel de large. `scroll-padding` aligne l'ancrage sur la
     colonne, pas sur le bord de l'écran. */
  .grid-cards[data-carousel] {
    container-type: inline-size;
    display: flex;
    gap: var(--carousel-gap);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--space-gutter);
    scrollbar-width: none;
    margin-inline: calc(var(--space-gutter) * -1);
    /* Même réserve que la bande de témoignages, et pour la même raison :
       un conteneur qui défile rogne ce qui dépasse, donc le soulèvement
       et le halo d'une carte touchée. On agrandit la boîte de rognage, et
       l'on reprend exactement autant en marge : la mise en page ne bouge
       pas. Voir la note « réserve du halo » de .quote-track. */
    padding: var(--space-md) var(--space-gutter) var(--card-glow-reach-block-end);
    margin-block: calc(var(--space-md) * -1) calc(var(--card-glow-reach-block-end) * -1);
  }
  .grid-cards[data-carousel]::-webkit-scrollbar { display: none; }
  .grid-cards[data-carousel] > * {
    flex: none;
    width: var(--carousel-item-width);
    scroll-snap-align: start;
  }

  /* La bande de témoignages quitte son défilement automatique : sur
     téléphone, une seule carte se lit à la fois, et une carte qui part
     pendant qu'on la lit est exactement ce que la rangée corrige. Plus de
     mouvement, donc plus de copies de boucle (elles feraient lire deux fois
     les six citations) ni de bouton pause. Les réglages privés du
     composant suffisent à en faire une rangée identique à celle des
     services. La bande de logos ne porte pas `data-carousel` : elle n'est
     pas concernée. */
  .quote-track[data-carousel] {
    --_item-width: var(--carousel-item-width);
    --_gap: var(--carousel-gap);
    --_snap: x proximity;
    margin-inline: calc(var(--space-gutter) * -1);
    padding-inline: var(--space-gutter);
    scroll-padding-inline: var(--space-gutter);
  }
  .quote-track[data-carousel] > .quote-track__rail {
    animation: none;
    will-change: auto;
  }
  .quote-track[data-carousel] > .quote-track__rail > li[aria-hidden='true'] { display: none; }
  /* `~` et non `+` : les pastilles s'insèrent juste après la bande, et le
     bouton n'en est alors plus le voisin immédiat. */
  .quote-track[data-carousel] ~ .quote-track__control { display: none; }

  /* ─── Photos : un format par rôle ─────────────────────────────────────
     Voir « Photos sur téléphone » dans tokens. Les hauteurs viennent toutes
     d'un format ou d'une taille fixe, jamais du chargement de l'image : la
     page ne saute pas quand une photo arrive. */

  /* Présence : À propos, en bandeau 4:3. Son cartouche de valeurs est
     retiré dans le balisage (`hide-on-phone`) : posé sous une photo
     raccourcie, il en devenait la moitié. */
  .split--stretch .figure img {
    aspect-ratio: var(--photo-ratio-phone);
    object-position: var(--figure-about-focus-phone);
  }

  /* Présence : Contact, en bandeau 4:3. La figure perd son carré et
     laisse l'image faire sa hauteur, le cartouche passant dessous (palier
     560) ou dessus selon la largeur. */
  .side-layout--media-start > .figure--solo {
    aspect-ratio: auto;
    height: auto;
  }
  .side-layout--media-start > .figure--solo img {
    height: auto;
    aspect-ratio: var(--photo-ratio-phone);
    object-position: var(--figure-contact-focus-phone);
  }

  /* Vignettes : recadrage en disque, commun aux deux.
     L'image est agrandie de --avatar-zoom autour d'une origine choisie pour
     que le visage (fx, fy) tombe au centre de la boîte, puis découpée en
     disque centré sur lui. L'origine se déduit du point visé :
       o = (50 % − z × f) / (1 − z)
     Le découpage est une forme au repos, pas un état d'attente : il reste
     posé sans script. Le balayage d'apparition des photos ne s'y applique
     plus (la découpe ci-dessous le remplace) ; c'est un fondu qui prend le
     relais, dans `animations`. */
  :is(.figure--avatar-quote, .figure--avatar-card) img {
    --_z: var(--avatar-zoom);
    flex: none;
    aspect-ratio: 1;
    transform-origin:
      calc((50% - var(--_z) * var(--_fx)) / (1 - var(--_z)))
      calc((50% - var(--_z) * var(--_fy)) / (1 - var(--_z)));
    transform: scale(var(--_z));
    clip-path: circle(calc(50% / var(--_z)) at var(--_fx) var(--_fy));
  }

  /* Vignette à côté de la signature (Pourquoi) : la photo devient le
     portrait d'auteur de la citation. La figure perd son cadre ; la
     citation garde le sien. `overflow: visible` rend son ombre à la
     citation, que le cadre rognait. */
  .figure--avatar-quote {
    aspect-ratio: auto;
    border-radius: 0;
    background: none;
    box-shadow: none;
    overflow: visible;
  }
  .figure--avatar-quote .pull-quote { margin-top: 0; }
  .figure--avatar-quote img {
    --_fx: var(--avatar-face-why-x);
    --_fy: var(--avatar-face-why-y);
    position: absolute;
    z-index: var(--layer-base);
    /* Calée sur le rembourrage de la citation : son filet vert, puis son
       retrait intérieur. */
    inset-block-end: var(--space-sm);
    inset-inline-start: calc(var(--stroke-thick) + var(--space-md));
    width: var(--avatar-size-quote);
    height: var(--avatar-size-quote);
  }
  /* La signature se décale de la largeur du portrait et prend au moins sa
     hauteur : nom et fonction se centrent à côté du visage. */
  .figure--avatar-quote cite {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: var(--avatar-size-quote);
    padding-inline-start: calc(var(--avatar-size-quote) + var(--space-sm));
  }

  /* Vignette dans l'invitation (FAQ) : photo et phrase ne font plus
     qu'une carte sombre. Sélecteur à deux classes : il doit l'emporter sur
     le format carré que le palier 1000 rend à cette figure. */
  .side-layout > .figure--avatar-card {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    aspect-ratio: auto;
    height: auto;
    padding: var(--space-sm) var(--space-md);
    background-color: var(--surface-inverse);
  }
  .figure--avatar-card img {
    --_fx: var(--avatar-face-bureau-x);
    --_fy: var(--avatar-face-bureau-y);
    width: var(--avatar-size-card);
    height: var(--avatar-size-card);
  }
  .figure--avatar-card .figure__overlay {
    position: static;
    width: auto;
    padding: 0;
    border-radius: 0;
    background: none;
  }

  /* ─── Appels en double ─────────────────────────────────────────────────
     Les boutons retirés le sont dans le balisage (`hide-on-phone`). Reste
     ce que leur retrait dérange autour d'eux. */

  /* La section de contact projetait son fond vers le haut, sous l'angle
     arrondi du bandeau d'appel (couche `layout`). Bandeau retiré, l'ombre
     se peignait sur le bas de la FAQ : 48 px de gris sur une section
     blanche. */
  .cta-band.hide-on-phone + .section--alt { box-shadow: none; }

  /* Boutons du pied de page : sur l'accueil, la section de contact les
     précède immédiatement, avec les mêmes deux canaux. Les autres pages
     les gardent : elles n'ont pas de section de contact.
     Le pied de page ne peut pas porter la classe dans son balisage : il est
     recopié à l'identique sur toutes les pages (banc de concordance). */
  .page-accueil .footer__highlights { display: none; }
  /* ⚠ LE RENDU DIFFÉRÉ DU PIED DE PAGE DOIT ÊTRE RENDU EN MÊME TEMPS.
     `.footer:not(:has(.is-revealed))` (couche `sections`) ne cesse de
     différer le pied de page qu'à la première apparition d'un de ses
     enfants — et ses seuls enfants révélables sont ces deux boutons. Une
     fois masqués, plus rien n'est jamais révélé : le pied de page restait
     différé pour toujours, à sa taille réservée de 640 px tant qu'il est
     hors de l'écran. Rien à animer, donc rien à protéger : on le rend
     ordinaire. */
  .page-accueil .footer { content-visibility: visible; }

  /* ─── Page « Conseils » : la vitrine des articles (1.9.0.1) ───────────
     Le sommaire du héros est retiré dans le balisage (`hide-on-phone`) :
     sept pastilles dont deux passaient sur deux lignes, pour annoncer les
     sept cartes affichées juste dessous. Le chapô devient le dernier bloc
     visible du héros : il rend l'écart qu'il réservait au sommaire, et le
     héros se referme sur son seul rembourrage, symétrique du haut. Sans
     `:has()`, 16 px de plus, rien de cassé. */
  .hero--compact .hero__text:has(+ .theme-nav.hide-on-phone) { margin-block-end: 0; }

  /* Cartes de la vitrine (grille non compacte) seulement : la carte
     entière est déjà le lien, « Lire l'article » (aria-hidden) n'y est
     qu'un indice visuel. Les cartes de services de l'accueil, où l'indice
     est un vrai lien, et les « Autres conseils » des articles
     (`.article-grid--compact`) gardent le leur. Rembourrage et écart
     passent d'un cran sous leur valeur de bureau : la carte réduite à sa
     vignette, son titre et son résumé n'a plus besoin de 24 px d'air. */
  .article-grid:not(.article-grid--compact) { gap: var(--space-sm); }
  .article-grid:not(.article-grid--compact) > .article-card { padding: var(--space-md) var(--space-sm); }
  .article-grid:not(.article-grid--compact) > .article-card > .article-card__more { display: none; }

  /* ─── Pied de page : deux colonnes de liens, cibles de 44 px ──────────
     Commun à toutes les pages, puisque le pied de page l'est. Dix liens en
     une colonne faisaient une liste plus longue que l'écran ; en deux, la
     hauteur de chaque ligne devient la cible tactile, et l'écart entre
     lignes n'a plus de raison d'être. */
  .footer__links {
    grid-template-columns: repeat(2, 1fr);
    column-gap: var(--space-md);
    row-gap: 0;
  }
  .footer__links a {
    display: inline-flex;
    align-items: center;
    min-height: var(--footer-link-min-height);
  }

}

/* — Téléphone, 320 px et plus : bandeau d'engagements sur trois colonnes ——
   Les trois chiffres se lisent d'un balayage, comme en bureau, au lieu de
   trois blocs empilés. Rembourrage resserré pour laisser aux libellés la
   largeur de leur plus long mot.
   ⚠ LE SEUIL SUIT LES LIBELLÉS, ET IL A DÉJÀ BOUGÉ UNE FOIS. Les premiers
   (« interlocutrice, jamais un standard », « ouvrées pour une réponse »)
   imposaient 360 px : à 320, la colonne du milieu tombait à 54 px et son
   libellé passait sur quatre lignes. Raccourcis avec le rédacteur
   (« jour de formation », « ouvrées pour répondre », « seule
   interlocutrice »), ils tiennent à 320 px, mesuré : colonnes de 67, 67
   et 87 px (« interlocutrice » fixe la troisième), deux, trois et deux
   lignes, aucun mot coupé. À 375 px : trois colonnes égales de 92 px.
   Sous 320 px, rien n'a été mesuré : le bandeau garde sa pile d'origine.
   Allonger un libellé peut remonter ce seuil : on remesure à 320 px avant
   de valider le texte. */
@media (min-width: 20rem) and (max-width: 47.9375rem) {
  .metrics-band {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-xs);
    padding: var(--space-md) var(--space-sm);
  }
}

/* — Très petit mobile : l'écart de l'en-tête cède avant les cibles ————————
   SECONDE MOITIÉ DU RÉGLAGE POSÉ SUR `.burger` PLUS HAUT — les deux ne
   valent que pris ensemble. `flex: none` empêche le burger de se comprimer,
   mais ne crée aucune place : sans ce palier, le débordement quitte le
   bouton pour partir dans le document. C'est ici qu'on libère les pixels.

   LE CALCUL. Ce que la barre réclame une fois le menu replié :

       logo ......................... 150 px  (--logo-width-sm, palier 560)
       burger ........................ 44 px  (--burger-size)
       pastille d'appel à l'action ... 44 px  (--header-cta-compact-size)
       ─────────────────────────────────────
       éléments seuls ............... 238 px

   La gouttière vaut 20 px de chaque côté sous 400 px de large : à 320 px il
   reste 280 px, donc 42 px pour deux écarts. --space-md en réclame 48 : il
   en manquait 6, et c'est le burger qui les payait. À --space-sm les deux
   écarts tiennent en 32 px, avec 10 px de jeu.

   CE QUE CE PALIER COUVRE, MESURÉ : la ligne tient jusqu'à 310 px de large
   (270 px réclamés pour 270 offerts). En dessous — 300 px, et 280 px pour
   un Galaxy Fold replié — trois éléments de commande et un logo ne tiennent
   plus sur une ligne, quel que soit l'écart : il faudrait en retirer un.
   Décision non prise, défaut assumé et chiffré plutôt que masqué.

   POURQUOI 360 ET NON 326, LE SEUIL EXACT. Un seuil posé sur la valeur
   mesurée casse au premier changement de logo ou de libellé. 360 px laisse
   34 px de marge et ne touche que des cadres où la ligne est déjà serrée :
   à cette largeur, passer l'écart de 24 à 16 px ne se voit pas. */
@media (max-width: 360px) {
  .header__inner { gap: var(--space-sm); }
}

/* — Recherche : la ligne d'aide décrit des touches ————————————————————————
   `↑↓ naviguer · ↵ ouvrir · échap fermer` ne s'adresse qu'à qui a un
   clavier. Sur un téléphone, elle occupe une ligne et un filet au bas d'une
   fenêtre qui a besoin de sa hauteur pour montrer des résultats.

   LE CRITÈRE EST LE SURVOL, ET IL EST CHOISI. `any-hover: none` n'est vrai
   que si AUCUN dispositif de pointage disponible ne sait survoler : un
   téléphone, une tablette. Un portable à écran tactile a une souris, donc
   un survol, donc un clavier : la ligne y reste. C'est le seul endroit du
   site qui interroge le dispositif de pointage, parce que c'est la seule
   information qu'aucun `clamp()` ne peut déduire d'une largeur.

   LIMITE ASSUMÉE : un téléphone auquel on a branché un clavier Bluetooth
   n'a toujours pas de survol, et perd donc la ligne. La fenêtre reste
   entièrement pilotable au clavier — la ligne décrit, elle ne commande
   pas. */
@media (any-hover: none) {
  .search__hint { display: none; }
}

/* ╔═════════════════════════════════════════════════════════════════════════╗
   ║  9. IMPRESSION                                                           ║
   ╚═════════════════════════════════════════════════════════════════════════╝ */
@media print {
  .topbar,
  .header,
  .cta-band,
  .footer__highlights,
  .to-top,
  .quick,
  .cta-bar,
  .invite,
  .reading-progress,
  .dot-pattern { display: none; }

  body { color: var(--print-ink); background: var(--print-paper); }
  /* Sur papier, aucun état d'attente : ni fondu, ni décalage, ni masque */
  [data-reveal] { opacity: 1; transform: none; }
  [data-reveal] :is(h2, .figure img, .figure__overlay, .pull-quote) {
    clip-path: none;
    opacity: 1;
    transform: none;
  }
  .step::before,
  .step::after { opacity: 1; transform: none; }
  .faq__answer { grid-template-rows: 1fr; }
  /* Rien ne doit rester en rendu différé sur une page imprimée */
  .faq,
  .footer { content-visibility: visible; }
  /* Le parcours replié sur téléphone s'imprime quand même : le résumé, une
     commande, n'a pas sa place sur papier, et le contenu d'un <details>
     fermé est rendu visible là où le navigateur expose son contenant
     (::details-content). Ailleurs, un repli fermé reste fermé à
     l'impression. */
  .about-career > summary { display: none; }
  @supports selector(::details-content) {
    .about-career::details-content { content-visibility: visible; }
  }
}
