/* ============================================================
   Bump Studio — Site intermédiaire
   Toutes les valeurs proviennent du design system Bump Studio
   (tokens couleurs / typo / espacements importés via Claude Design).
   ============================================================ */

/* ---------------- Webfonts — Neue Haas Grotesk (Adobe Fonts) ----------------
   🚨 Refonte du 14/08/2026 : les fichiers locaux Neue Haas Display
   (Asset DA/TYPO/Haas-Neue/) ne sont PLUS chargés. Le kit Typekit
   « yrz1quz » (lié dans le <head> des 4 pages publiques) sert désormais les
   DEUX seules polices du site :
     - "neue-haas-grotesk-display", poids 200 (Thin) — réservée aux TITRES
       (h1-h6), toujours en MAJUSCULES.
     - "neue-haas-grotesk-text", poids 400 — tout le reste : texte courant,
       nav, footer, boutons, labels, formulaire.
   Aucune autre graisse n'est servie (plus de Light 300 / Medium 500 /
   italiques) : voir les tokens --weight-* ci-dessous, qui collapsent tout ce
   qui n'est pas un titre sur ce seul poids 400. Ne pas réintroduire de
   @font-face local ni de règle font-style: italic. */

/* ---------------- Tokens ---------------- */
:root {
  /* Brand base palette */
  --bump-charcoal: #282827;
  --bump-forest:   #162B1C;
  --bump-sage:     #D0D3C1;
  --bump-paper:    #F3F4EF;
  --bump-white:    #FFFFFF;

  /* Neutral steps.
     ⚠️ `--gray-500` EST le « Graphite » de la charte (#6D6D6C) et `--bump-sage`
     le « Papyrus » (#D0D3C1) : les deux ont un nom officiel côté client, un nom
     historique côté code. Les alias sémantiques plus bas font le pont. */
  --gray-700: #4d4d4c;
  --gray-500: #6d6d6c;
  --gray-300: #bbbaba;
  --gray-200: #d8d8d4;
  --gray-100: #e9eae5;

  /* Forest / sage tints */
  --forest-500: #234a31;
  --forest-300: #5a7563;
  --sage-soft:  #e1e3d8;
  --sage-deep:  #b3b89e;

  /* Semantic */
  --text-body: var(--bump-charcoal);
  --text-muted: var(--gray-500);
  --text-inverse: var(--bump-paper);
  --text-accent: var(--bump-forest);
  --surface-page: var(--bump-paper);
  --surface-card: var(--bump-white);
  --surface-sage: var(--bump-sage);
  --surface-sage-soft: var(--sage-soft);
  --surface-dark: var(--bump-charcoal);
  --surface-forest: var(--bump-forest);
  /* Graphite de la charte — le palier neutre de la page (section Résultats). */
  --surface-graphite: var(--gray-500);
  --border-subtle: rgba(40, 40, 39, 0.14);
  --border-medium: rgba(40, 40, 39, 0.28);
  --border-inverse: rgba(243, 244, 239, 0.24);
  --accent: var(--bump-forest);

  /* ---- Trame de repères ----
     L'encre de l'habillage « lignes de construction » : filets fins qui
     traversent l'écran, verticales au bord du conteneur, croix `+` aux
     intersections. Ces trois valeurs sont partagées par `.trame-lien` et
     `.trame-cadre` ET par la planche de logos `.lcloud`, qui portait le motif avant
     qu'il soit généralisé — c'est ce qui garantit que les deux dessinent
     exactement la même chose. Ne pas les redéclarer localement.
     Le filet est plus clair que la croix : c'est le repère qu'on doit voir,
     pas la ligne. */
  --trame-ligne: var(--border-subtle);
  --trame-croix: var(--border-medium);
  --trame-bras: 13px;
  /* Le motif du RACCORD entre deux cadres (`.trame-lien`). Trait un peu plus
     long que le blanc : sur un filet d'1 px, un pointillé à parts égales se lit
     comme une ligne grise et le traitillé ne se voit plus. */
  --trame-tiret: 7px;
  --trame-vide: 5px;
  /* 🚨 L'AIR entre la verticale et le bord du contenu (10/08/2026).
     Les verticales tombaient EXACTEMENT sur le bord de contenu : le portrait
     d'un fondateur et le chapô aligné à droite s'y collaient, et une ligne
     collée à un texte se lit comme une erreur de gabarit, pas comme un repère.
     Elles sont donc reculées vers l'extérieur de ce montant.
     ⚠️ Il est PRIS SUR LA GOUTTIÈRE : il doit rester nettement inférieur à
     `--gutter` (48 / 32 / 22 px), sinon la verticale sort de l'écran sur une
     fenêtre plus étroite que `--maxw-content`. D'où les trois paliers.
     🚨 `.lcloud` (planches clients et partenaires) se décale du MÊME montant
     par marge négative : c'est ce qui garde la planche à l'aplomb exact des
     verticales des sections voisines. Si l'un bouge, l'autre suit. */
  --trame-air: 28px;

  /* Typography — deux familles Adobe Fonts, aucune autre (14/08/2026).
     --font-sans reste le nom historique du token (des dizaines de règles le
     référencent) mais pointe désormais vers la famille TEXTE : c'est la
     police de tout ce qui n'est pas un titre. --font-display est réservée
     aux h1-h6 (règle plus bas), toujours en majuscules.
     --weight-light / --weight-medium n'existent plus comme graisses
     distinctes : le kit ne sert que 200 (Display, titres) et 400 (Text).
     Les deux tokens collapsent donc sur 400 pour ne pas casser les dizaines
     de règles qui les référencent encore ; l'emphase (`<em>`, `strong`) se
     fait uniquement par la couleur, plus par la graisse. */
  --font-sans: "neue-haas-grotesk-text", "Helvetica Neue", Arial, sans-serif;
  --font-display: "neue-haas-grotesk-display", "Helvetica Neue", Arial, sans-serif;
  --weight-text: 400;
  --weight-display: 200;
  --weight-light: 400;
  --weight-medium: 400;

  --leading-tight: 1.04;
  --leading-snug: 1.18;
  --leading-normal: 1.5;
  --leading-relaxed: 1.65;

  /* 🚨 Interlettrage resserré le 07/08/2026 — demande de charte.
     Les paliers étaient à 0,08 / 0,16 / 0,28em. Le plus large disloquait
     franchement les sur-titres (« AGENCE DE COMMUNICATION · FRIBOURG »,
     « EXPLOREZ PAR SECTEUR ») : « l'espacement entre les lettres est trop
     gros, ça fait pas partie de mon branding ».
     Neue Haas Display est déjà une grotesque large ; en capitales elle n'a
     pas besoin qu'on l'écarte pour rester lisible. Les valeurs restent
     croissantes, mais sur une amplitude deux fois plus courte.
     ⚠️ Ne pas remonter ces trois valeurs. */
  --tracking-tight: -0.01em;
  --tracking-wide: 0.05em;
  --tracking-wider: 0.09em;
  --tracking-widest: 0.12em;

  /* Spacing */
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;
  --space-10: 8rem;

  /* Respiration verticale d'une section (`.section`) — redescend d'un cran
     sous 760px.
     ⚠️ Passé sur la rampe fluide le 01/09/2026 (R = 128px, l'ancien
     `--space-10`) : l'espacement vertical était lui aussi figé, donc deux fois
     plus présent en proportion sur un 1080p que sur le 4K. Voir « ÉCHELLE
     FLUIDE » plus bas dans ce bloc.
     🚨 Les débords de la trame (`.trame-lien`, `.trame-lien--amorce`) sont
     exprimés EN FONCTION de ce token, jamais en dur : ils suivent donc la
     rampe tout seuls et la chaîne verticale reste continue. Vérifié. */
  --section-py: clamp(72px, 51.2px + 2vw, 128px);

  /* Radii — adoucis pour un rendu plus moderne */
  --radius-xs: 6px;
  --radius-sm: 10px;
  --radius-md: 14px;
  --radius-lg: 20px;
  --radius-card: 18px;
  --radius-pill: 999px;
  /* Rayon des couches empilées au scroll (hero → pont → reste de la page) */
  --radius-stack: clamp(28px, 5vw, 88px);

  /* Bande de recouvrement : hauteur sur laquelle le panneau « pont » remonte
     par-dessus le hero encore épinglé. Elle est ajoutée à .hero-pin ET retirée
     du calcul des 3 segments dans hero.js (via #heroBand) : la bascule entre
     secteurs garde donc exactement le même rythme qu'avant. Mise à 0 en
     mouvement réduit → la page redevient un défilement classique.

     ⚠️ Doit valoir EXACTEMENT la hauteur du hero (`100svh`). Le hero se
     dé-épingle à `pin_bottom - 100svh` ; le panneau atteint le haut de l'écran
     à `pin_bottom - --stack-band`. Si la bande est plus courte, le hero se
     libère AVANT que le panneau soit arrivé et se met à défiler à la même
     vitesse que lui : c'est le « coup sec » signalé par le client. Les deux
     valeurs doivent coïncider.
     La hauteur du panneau, elle, est libre (cf. .hero-outro). */
  --stack-band: 100svh;
  /* Course des 3 segments de secteur. Séparée de la bande pour que les deux se
     règlent indépendamment ; hero.js la mesure via #heroBand. */
  --pin-travel: 240vh;

  /* Shadows — soft, low, neutral. Never colored. */
  --shadow-xs: 0 1px 2px rgba(40, 40, 39, 0.06);
  --shadow-sm: 0 2px 8px rgba(40, 40, 39, 0.07);
  --shadow-md: 0 8px 28px rgba(40, 40, 39, 0.10);
  --shadow-lg: 0 20px 56px rgba(40, 40, 39, 0.14);

  /* Motion */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* courbe type iOS pour les dépliages */
  /* Sortie très longue : pour les gros déplacements (entrées de section,
     survols de carte). Démarre franc, s'arrête sans à-coup. */
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  /* Courbe des animations liées au scroll : symétrique, l'élément bouge peu
     aux extrémités et le plus au milieu de sa traversée. C'est ce qui donne un
     mouvement courbe plutôt que mécanique. */
  --ease-scroll: cubic-bezier(0.4, 0, 0.55, 1);
  /* Rideau : masque qui traverse une carte de haut en bas (prestations).
     Courbe en S franche — un volet démarre, accélère, puis se pose ; il ne
     surgit pas. C'est la seule courbe du site qui accélère au départ, et elle
     est réservée à ce geste : ne pas l'utiliser pour une micro-transition. */
  --ease-wipe: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 140ms;
  --dur: 240ms;
  --dur-slow: 420ms;
  --dur-enter: 760ms;

  /* Layout */
  /* 🚨 ÉCHELLE FLUIDE JUSQU'AU 4K (01/09/2026) — lire avant de toucher à une
     valeur plafonnée du site.

     Retour client : « sur mon écran 4K en 16/10 tout allait bien, mais sur un
     vrai 1080p il y a beaucoup de choses beaucoup trop grandes, je dois
     afficher en 60 % pour que ça y ressemble… il faut faire quelque chose de
     plus uniforme, sans toucher la version que j'avais ».

     CAUSE : tout le dimensionnement d'affichage du site (colonne de contenu,
     titres, planches, chiffres) était plafonné en PIXELS, et ces plafonds
     tombaient tous entre ~1150 et ~1550 px de large. Au-delà, plus rien ne
     grandissait. La composition a donc été jugée à l'œil sur un 4K, où ces
     valeurs figées n'occupent qu'un tiers de l'écran ; sur un 1080p elles en
     occupent les deux tiers. Mesuré : le titre du hero passe de 1,6 % de la
     largeur en 3840 à 3,1 % en 1920 (×1,9), la colonne de 33 % à 67 % (×2).
     Le zoom à 60 % que faisait le client rétablissait exactement ça
     (1920 ÷ 0,6 = 3200 px, donc les proportions du 4K).

     CORRECTIF : chaque valeur plafonnée devient une RAMPE qui atteint sa
     valeur actuelle à 3840 px (donc le rendu 4K est INCHANGÉ, au pixel) et
     décroît proportionnellement en dessous — 70 % de la référence à 1920.
     Pour une valeur de référence R :

         clamp(<plancher>, 0.4·R + (R/64)·1vw, R)

     soit exactement R à 3840 et 0,7·R à 1920. Le `min` du clamp sert de
     plancher de lisibilité sur les petits portables : en dessous de ~1100 px
     c'est lui qui reprend la main, donc la rampe ne rapetisse jamais le site
     au point de le rendre illisible.

     ⚠️ NE PAS remettre un plafond atteint tôt (`clamp(x, Nvw, max)` dont le
     max tombe vers 1400 px) sur une valeur d'affichage : c'est précisément ce
     qui a créé le problème. Si une nouvelle valeur doit être plafonnée, la
     calculer avec la formule ci-dessus.
     ⚠️ Le TEXTE COURANT (body, nav, labels, formulaire) est volontairement
     HORS de ce système : il reste à sa taille fixe. Le mettre à l'échelle le
     ferait tomber vers 10 px sur un 1080p — c'est l'arbitrage explicite du
     client (« rapprocher, sans casser la lisibilité »). */
  /* 🚨 LA COLONNE DE CONTENU RESTE FIGÉE À 1280px, ET C'EST LA SEULE VALEUR
     D'AFFICHAGE QUI ÉCHAPPE À LA RAMPE CI-DESSUS. Essayé le 01/09/2026 en
     `clamp(880px, 512px + 20vw, 1280px)` (donc 896px à 1920, soit la
     proportion du 4K), puis RETIRÉ : la colonne commande la largeur du
     footer, et les QUATRE colonnes de texte du footer ne tiennent pas sous
     ~1100px de largeur utile. C'est le seuil déjà documenté plus bas
     (« Quatre colonnes de texte ne tiennent pas sous ~1200 px »), sauf qu'en
     rétrécissant la colonne on le déclenche à 1920 au lieu de 1200. Mesuré :
     à 1920 le nombre de textes qui s'enroulent passe de 3 à 7 — l'adresse se
     coupe sur 8 lignes et les numéros de téléphone en 3. Pour que 4 colonnes
     tiennent, il faudrait `--maxw-content` ≥ 1200px, ce que la rampe
     n'atteint qu'au-delà de 3440px de large : autrement dit le footer
     passerait en 2×2 sur la totalité des écrans réels, ce qui défait la
     composition « 4 + 2n = 12 modules » voulue par le client.
     ⚠️ Conséquence assumée : la colonne reste à 67 % de la largeur en 1920
     contre 33 % en 3840. C'est le seul écart de proportion qui subsiste après
     la passe du 01/09/2026 ; le rapprocher demanderait de refondre d'abord la
     grille du footer, ce qui n'a pas été demandé. */
  --maxw-content: 1280px;
  --gutter: 48px;
  /* Hauteur RÉELLE du header sticky. Le calcul « padding 2×18 + logo 30 + bordure 1 »
     donnait 67px et oubliait que le bouton CTA (36px) est plus haut que le logo :
     le header fait 73px sur desktop et 77px sous 760px (la poignée de menu fait
     40px). Comme `.hero-pin` remonte de exactement `-1 × --header-h` pour filer
     sous le header, les 6px manquants laissaient une bande de papier visible tout
     en haut de la page — signalé par le client.
     Le script d'index.html remesure la valeur au chargement et au
     redimensionnement : ces deux valeurs sont donc le repli (pas de JS) et
     l'état du premier rendu, jamais une source de vérité à maintenir à la main. */
  --header-h: 73px;
}

/* ---------------- Reset / base ---------------- */
*, *::before, *::after { box-sizing: border-box; }

/* Lien d'évitement clavier : hors écran, révélé au focus */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 200;
  padding: 10px 18px;
  background: var(--bump-forest);
  color: var(--bump-paper);
  border-radius: var(--radius-xs);
  font-size: 0.9rem;
  text-decoration: none;
  transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus-visible { top: 12px; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-weight: var(--weight-light);
  font-size: 1rem;
  line-height: var(--leading-normal);
  color: var(--text-body);
  background: var(--surface-page);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* 🚨 Tout titre (h1-h6) est en Neue Haas Grotesk Display Thin, en MAJUSCULES
   (14/08/2026, demande client : « ses deux typo, aucune autre sur le site »).
   Le texte courant reste en casse normale — voir --font-sans plus haut. Ne
   pas mettre `text-transform: uppercase` ailleurs que sur un vrai titre : les
   labels/eyebrows/boutons déjà en capitales le sont par leur propre règle,
   pas par celle-ci. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  text-transform: uppercase;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 0.5em;
  text-wrap: balance;
}

p { text-wrap: pretty; margin: 0; }

a { color: var(--forest-500); text-decoration: none; }

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

::selection { background: var(--bump-forest); color: var(--bump-paper); }

/* ---- Visibilité par section, pilotée par le CMS (01/09/2026) ----
   `.masque-desktop` / `.masque-mobile` : posées par JS (index.html) et par
   `build.js` sur une `<section data-section-key>` et sur son lien de nav
   quand une section n'est masquée que sur UN SEUL des deux gabarits — voir
   « Visibilité des sections : desktop / mobile indépendants » dans CLAUDE.md.
   Coupure à 760px, la même que partout ailleurs sur le site.
   ⚠️ `!important` volontaire : un réglage de visibilité posé depuis le CMS
   doit gagner sur toute règle de mise en page existante, y compris celles à
   plus forte spécificité (`.presta`, `.results`, etc.). Une section masquée
   EN ENTIER (les deux cases décochées) n'utilise pas ces classes : elle
   reçoit l'attribut `hidden`, plus simple et suffisant dans ce cas. */
@media (min-width: 761px) {
  .masque-desktop { display: none !important; }
}
@media (max-width: 760px) {
  .masque-mobile { display: none !important; }
}

:focus-visible {
  outline: 2px solid var(--forest-500);
  outline-offset: 3px;
}

/* ---------------- Utilities ---------------- */
.shell {
  max-width: var(--maxw-content);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.eyebrow {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0;
}

/* `--section-py` passe par un token (et non `--space-10` en dur) pour qu'une
   section qui demande un peu plus d'air en haut puisse s'y ajouter sans
   redéclarer la valeur du palier mobile. Voir `#a-propos`. */
.section {
  padding-top: var(--section-py);
  padding-bottom: var(--section-py);
}

/* ============================================================
   Trame de repères — habillage « lignes de construction »

   🚨 DEUX CLASSES, DEUX RÔLES (refonte du 10/08/2026). Elles étaient une
   seule : `.trame` posait un cadre autour de la SECTION ENTIÈRE, titre
   compris. Retour client : « l'encadré doit commencer en dessous de la ligne
   des fondateurs, et tu dois en avoir une au-dessus qui s'arrête à la section
   du dessus… exactement comme la différence avec Ils nous font confiance ».

   La planche de logos avait raison depuis le début : son cadre commence SOUS
   le titre. C'est ce modèle qui est généralisé, pas l'inverse.

   | classe | où | dessin |
   |---|---|---|
   | `.trame-cadre` | sur le bloc de contenu | rails pleins + 2 verticales + 4 croix |
   | `.trame-lien`  | autour du bloc de titre | les 2 verticales, en TRAITILLÉ |

   Le titre vit donc DANS l'intervalle, entre le rail bas d'un cadre et le rail
   haut du suivant, et les verticales le traversent en pointillé : la ligne ne
   s'interrompt jamais du premier cadre au dernier. C'est le rythme de
   `.lcloud`, appliqué partout.

       <section class="section shell">
         <div class="trame-lien"><div class="section-head">…</div></div>
         <div class="trame-cadre">…</div>
       </section>

   🚨 `.lcloud` EST DÉJÀ un `.trame-cadre` : elle porte ses propres rails
   pleins, ses verticales (ce sont ses bordures) et ses croix de coin. Ne pas
   lui ajouter la classe, elle doublerait tout. Les deux consomment les mêmes
   jetons `--trame-*` : si l'encre change, elle change des deux côtés.

   ⚠️ Où NE PAS poser de `.trame-cadre` (audit du 07/08/2026) :
     · `.hero` / `.hero-outro` — le pont a sa composition, et `.after-hero` est
       explicitement « purement structurel » ;
     · `.presta` — la scène est épinglée et le rail traverse les bords du
       conteneur (`--rail-pad`) : les cartes passeraient sur les verticales.
       Elle prend en revanche le SÉPARATEUR, qui ne touche à rien ;
     · `.results` — trois systèmes de lignes s'y croisent déjà (bento à filets
       2 px, épure oblique du fond, et la rupture papier → graphite qui EST la
       ligne horizontale).
   ============================================================ */

/* ---- Le séparateur de section : un seul filet, traitillé ----
   🚨 UN SEUL RAIL, EN HAUT, et jamais en bas. La version précédente en posait
   deux par section en comptant sur le fait que les `.section` sont jointives
   pour que le rail bas de l'une et le rail haut de la suivante tombent au même
   pixel. Ça marchait, mais dès que le trait est traitillé la moindre section
   sans la classe rouvre un trou, et deux traitillés superposés dépendent d'une
   phase de motif identique.

   🚨 ET SURTOUT : LE TRAITILLÉ EST VERTICAL, PAS HORIZONTAL. Une version l'a
   posé en filet horizontal à la frontière des sections — contresens.
   Précision du client (10/08/2026) : « c'était pour dire un trait, pas
   horizontalement, mais verticalement, entre les deux lignes, entre les deux
   sections, de les relier avec des traits tirés, comme s'ils en faisaient
   qu'un. Depuis le début du site il y avait une seule ligne à partir du moment
   où il y a ces quadrillés. »

   Autrement dit : LES DEUX VERTICALES NE S'INTERROMPENT JAMAIS entre le
   premier cadre et le dernier. Elles sont pleines dans un cadre, traitillées
   dans l'intervalle qui porte le titre de la section suivante. Il n'y a donc
   plus aucun filet horizontal de frontière : les seules horizontales du
   système sont les rails des cadres.

   ⚠️ Le motif passe par un dégradé répété et NON par `border-style: dashed` :
   la longueur des tirets d'une bordure est laissée au moteur (~2 × l'épaisseur
   sous Chromium) et n'est pas réglable. Ici elle est écrite. */

/* ---- Le raccord traitillé entre deux cadres ----
   Se pose sur un ENVELOPPE autour du bloc de titre, jamais sur le titre
   lui-même : `.section-head` porte `.reveal` (opacité + `translateY(18px)`) et
   le raccord partirait alors 18 px trop bas avant de se mettre en place, donc
   désaligné de la verticale qu'il prolonge. L'enveloppe porte aussi la pleine
   largeur : `.section-head` est plafonné à `60ch` quand il n'a pas de chapô,
   et la verticale de droite tomberait au milieu de la section.

   🚨 LES DEUX DÉBORDS SONT DES CONSTANTES DE GABARIT, PAS DES VALEURS CHOISIES :
     · en haut, `2 × --section-py` — le padding bas de la section précédente
       (son rail est exactement `--section-py` au-dessus de sa fin, cf. la marge
       basse du cadre) plus le padding haut de celle-ci ;
     · en bas, `--space-8` — la marge basse de `.section-head`, qui est aussi la
       distance titre → rail du cadre.
   Si l'un de ces espacements bouge, le raccord se décroche des rails. Il est
   mesuré au banc (`scratchpad/trame-air.js`) : écart aux rails ≤ 0,5 px. */
.trame-lien {
  --tl-haut: calc(2 * var(--section-py));
  /* Débord BAS : la marge basse de `.section-head`, qui est aussi la distance
     du titre au rail du cadre. En variable parce que tous les gabarits n'ont
     pas de `.section-head` — l'en-tête de `projets.html` est suivi du terrain
     de jeu du sillage, bien plus haut (voir `.ptitle`). */
  --tl-bas: var(--space-8);
  position: relative;
  isolation: isolate;
}

/* ---- L'AMORCE de la chaîne (10/08/2026) ----
   Le premier raccord n'a pas de cadre au-dessus de lui : il ne monte donc pas
   dans la section précédente, il s'arrête au bord HAUT de sa propre section.
   Demande client : « rajouter les treillis sur le Nés à Fribourg, à gauche et à
   droite, vers le haut jusqu'à la section Ce que nos clients obtiennent, jusqu'à
   la partie grise, sans toucher à cette section-là. »

   🚨 La valeur est le PADDING HAUT DE `#a-propos`, pas `--section-py` : cette
   section en ajoute `--space-7` (voir sa règle plus bas). Les deux doivent
   rester d'accord — si le padding change, l'amorce déborde sur le graphite ou
   s'arrête avant lui. Mesuré : 0,0 px entre le haut du traitillé et le bas de
   la section grise. */
.trame-lien--amorce { --tl-haut: calc(var(--section-py) + var(--space-7)); }

.trame-lien::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: calc(-1 * var(--tl-haut));
  bottom: calc(-1 * var(--tl-bas));
  left: calc(-1 * var(--trame-air));
  right: calc(-1 * var(--trame-air));
  pointer-events: none;

  background-repeat: no-repeat;
  background-image:
    repeating-linear-gradient(
      to bottom,
      var(--trame-ligne) 0 var(--trame-tiret),
      transparent var(--trame-tiret) calc(var(--trame-tiret) + var(--trame-vide))
    ),
    repeating-linear-gradient(
      to bottom,
      var(--trame-ligne) 0 var(--trame-tiret),
      transparent var(--trame-tiret) calc(var(--trame-tiret) + var(--trame-vide))
    );
  background-size: 1px 100%, 1px 100%;
  background-position: left top, right top;
}

/* ---- Le cadre de contenu ----
   Se pose sur le bloc qui suit le titre. Il déborde de `--trame-air` sur ses
   QUATRE côtés : horizontalement ça met les verticales exactement où elles
   étaient (bord de contenu moins l'air, donc à l'aplomb des planches
   `.lcloud`), verticalement ça évite que le rail haut se pose sur le sommet
   des portraits.
   ⚠️ Le débord est en `top`/`bottom` négatifs et NON en `padding` : le cadre
   ne doit pas déplacer d'un pixel ce qu'il entoure. */
.trame-cadre {
  /* Décalage des verticales, mesuré depuis le bord du bloc — qui est déjà le
     bord de contenu, puisque le `.shell` porte la gouttière. Négatif : la
     ligne est DEHORS. */
  --tr-x: calc(-1 * var(--trame-air));
  /* Demi-bras de croix, corrigé du pixel du filet : c'est ce qui centre la
     croix SUR la ligne et non à côté. */
  --tr-demi: calc((var(--trame-bras) - 1px) / 2);

  /* 🚨 LE RAIL HAUT DOIT TOMBER À LA MÊME DISTANCE DU TITRE QUE CELUI D'UNE
     PLANCHE `.lcloud`, sinon les deux dispositifs ne battent pas la même
     mesure. La planche n'a pas d'air : son rail EST son bord haut, donc à
     `--space-8` du bloc de titre (la marge basse de `.section-head`). Le cadre,
     lui, remonte son rail de `--trame-air` — on lui rend donc exactement ça en
     marge. Idem en bas, pour que la distance du rail à la fin de section soit
     la même des deux côtés. Mesuré : 64 px partout.

     🚨 LA MARGE HAUTE EST ÉCRITE EN ENTIER (`--space-8` + l'air) À CAUSE DE LA
     FUSION DES MARGES. `.section-head` et le cadre sont des frères adjacents :
     leurs marges verticales fusionnent et c'est la PLUS GRANDE qui l'emporte.
     Un simple `margin-top: var(--trame-air)` (28 px) était donc absorbé par
     les 64 px du titre et ne déplaçait rien — constaté, le rail restait à
     36 px. En bas il n'y a pas de fusion (la section porte un padding), donc
     l'air seul suffit. */
  margin-top: calc(var(--space-8) + var(--trame-air));
  margin-bottom: var(--trame-air);

  position: relative;
  /* 🚨 Contexte d'empilement sans toucher au z-index du bloc : les deux
     calques passent en `z-index: -1`, donc DERRIÈRE le texte — sinon la
     verticale de gauche court sur la première colonne de lettres.
     ⚠️ `z-index: -1` seul ne suffirait pas : `.after-hero` porte un fond
     opaque et n'est pas un contexte d'empilement, les calques se peindraient
     dessous et seraient purement et simplement invisibles. */
  isolation: isolate;
}

/* Les deux rails du cadre, PLEINS (le traitillé est réservé aux frontières de
   section) et d'un bord à l'autre de la fenêtre, comme ceux de `.lcloud`. */
.trame-cadre::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: calc(-1 * var(--trame-air));
  bottom: calc(-1 * var(--trame-air));
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  border-top: 1px solid var(--trame-ligne);
  border-bottom: 1px solid var(--trame-ligne);
  pointer-events: none;
}

/* Les deux verticales + les quatre croix, en couches de `background` sur un
   seul calque. La croix est le même dessin que celle de `.lcloud` : deux
   traits d'1 px, aucune icône importée.

   🚨 LA BOÎTE DÉBORDE DU CADRE D'UN DEMI-BRAS SUR SES QUATRE CÔTÉS, et les
   lignes sont rentrées d'autant. Une première version calait la boîte
   exactement sur le cadre et sortait les croix en position négative : la
   moitié extérieure de chaque bras était alors DÉCOUPÉE (`background-clip`
   vaut `border-box` par défaut, une couche qui dépasse n'est pas peinte).
   Résultat constaté : des équerres `⌐` au lieu de croix, un bras vers la
   droite et un vers le bas. Tout se passe donc à l'intérieur de la boîte, et
   plus aucun décalage n'est négatif — d'où le `--tr-x` négatif ajouté au
   demi-bras plutôt que soustrait. */
.trame-cadre::after {
  content: "";
  position: absolute;
  z-index: -1;
  /* Les quatre côtés sont symétriques : le cadre déborde du bloc de
     `--trame-air`, et cette boîte-ci déborde du cadre d'un demi-bras. */
  top: calc(var(--tr-x) - var(--tr-demi));
  bottom: calc(var(--tr-x) - var(--tr-demi));
  left: calc(var(--tr-x) - var(--tr-demi));
  right: calc(var(--tr-x) - var(--tr-demi));
  pointer-events: none;

  background-repeat: no-repeat;
  background-image:
    /* les 2 verticales */
    linear-gradient(var(--trame-ligne) 0 0),
    linear-gradient(var(--trame-ligne) 0 0),
    /* les 4 bras horizontaux des croix */
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    /* les 4 bras verticaux des croix */
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0);
  background-size:
    1px calc(100% - 2 * var(--tr-demi)), 1px calc(100% - 2 * var(--tr-demi)),
    var(--trame-bras) 1px, var(--trame-bras) 1px,
    var(--trame-bras) 1px, var(--trame-bras) 1px,
    1px var(--trame-bras), 1px var(--trame-bras),
    1px var(--trame-bras), 1px var(--trame-bras);
  background-position:
    left var(--tr-demi) top var(--tr-demi),
    right var(--tr-demi) top var(--tr-demi),
    left 0 top var(--tr-demi),
    right 0 top var(--tr-demi),
    left 0 bottom var(--tr-demi),
    right 0 bottom var(--tr-demi),
    left var(--tr-demi) top 0,
    right var(--tr-demi) top 0,
    left var(--tr-demi) bottom 0,
    right var(--tr-demi) bottom 0;
}

/* Sur fond sombre. Le site n'a PAS de mode sombre (ni `prefers-color-scheme`,
   ni `data-theme`) : il a des SECTIONS sombres — `.contact` en forêt,
   `.results` en graphite. C'est donc un modificateur, pas un thème, et il
   reprend les encres claires déjà en service dans ces sections. */
.trame-lien--clair,
.trame-cadre--clair {
  --trame-ligne: var(--border-inverse);
  --trame-croix: rgba(243, 244, 239, 0.42);
}

.section-head {
  max-width: 60ch;
  margin-bottom: var(--space-8);
}
.section-head .eyebrow { margin-bottom: var(--space-5); }
.section-head h1,
.section-head h2 {
  font-size: clamp(30px, min(4vw, calc(18.4px + 0.71875vw)), 46px);
  margin: 0 0 var(--space-4);
}
.section-head .intro {
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
  max-width: 56ch;
}

/* ---- Le chapô passe À DROITE, aligné à droite (07/08/2026) ----
   Demande client : « s'il y a un texte quelque part, par exemple “ce que nos
   clients obtiennent” ou “ils nous font confiance”, j'aimerais mettre le
   texte à droite et centré à droite, pour équilibrer un peu le site s'il n'y
   a rien d'autre à droite. » Le titre restait seul à gauche et la moitié
   droite de la rangée était vide : le chapô vient la remplir.

   🚨 La condition est bien « rien d'autre à droite », d'où le `:has(> .intro)`
   sur un enfant DIRECT — et deux exclusions :
     - `.section-head--compte` (Projets) porte son compteur à droite ; son
       chapô est d'ailleurs imbriqué dans `.section-head__txt`, donc `>` ne
       l'attrape pas ;
     - `.presta .section-head` est la moitié gauche d'une rangée flex dont la
       droite porte le compteur `01 / 12` — repris en bloc plus bas.

   Les titres seuls (Les fondateurs, Nos partenaires) ne sont pas concernés :
   sans chapô, il n'y a rien à déplacer. */
.section-head:has(> .intro) {
  max-width: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-4) clamp(32px, 6vw, 96px);
  /* Les deux colonnes se calent sur leur BAS : le chapô finit donc sur la
     même ligne que la dernière ligne du titre. Alignés en haut, un titre de
     deux lignes laisserait le chapô flotter au milieu du vide. */
  align-items: end;
}
/* La marge basse du titre sauterait le calage : c'est le `gap` qui espace
   les deux blocs quand ils s'empilent. */
.section-head:has(> .intro) h1,
.section-head:has(> .intro) h2 { margin-bottom: 0; }
.section-head:has(> .intro) .intro {
  justify-self: end;
  text-align: right;
  max-width: 46ch;
}

/* Une colonne : le chapô repasse sous le titre et redevient aligné à gauche.
   Aligné à droite sur un écran étroit, il se lirait comme une légende. */
@media (max-width: 900px) {
  .section-head:has(> .intro) {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
  }
  .section-head:has(> .intro) .intro { justify-self: start; text-align: left; }
}

/* ---- Bloc de titre qui porte un compteur à l'autre bout de la rangée ----
   Le `max-width: 60ch` doit sauter : c'est la RANGÉE qui prend la largeur du
   contenu, et c'est la colonne de texte qui garde la mesure de lecture. */
.section-head--compte {
  max-width: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-7);
}
.section-head--compte .section-head__txt { max-width: 60ch; }

/* ---- Compteur à chiffres roulants (section Projets) ----
   Même vocabulaire que `.presta-count`, mais les chiffres ne sont pas
   remplacés : ils se DÉVIDENT. Chaque rang est un rouleau de 0 à 9 découpé sur
   une ligne, qu'on translate — donc rien à réécrire dans le DOM, et le
   navigateur anime un `transform`, sans repeindre.
   🚨 Seuls les chiffres bougent. Le « / 06 projets » est fixe : c'est ce qui
   donne l'échelle et ce qui fait lire les chiffres comme un rang. */
.pcount {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.pcount__now {
  display: inline-flex;
  font-size: clamp(30px, min(4vw, calc(18.4px + 0.71875vw)), 46px);
  font-weight: var(--weight-light);
  color: var(--text-body);
  line-height: 1;
}
.pcount__all {
  font-size: 0.8125rem;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  white-space: nowrap;
}
/* Un rang. ⚠️ `clip` et jamais `hidden` : pas de conteneur de défilement
   (piège transverse du site). `1em` de haut = exactement un chiffre, sinon le
   rouleau laisserait voir le suivant. */
.pcount__d {
  display: block;
  overflow: clip;
  height: 1em;
}
.pcount__s {
  display: block;
  /* Le retard croît de la droite vers la gauche : les rangs se posent l'un
     après l'autre au lieu de basculer d'un bloc. */
  transition: transform 560ms var(--ease-soft);
  transition-delay: calc(var(--r, 0) * 55ms);
  transform: translateY(calc(var(--v, 0) * -1em));
}
.pcount__s > span { display: block; height: 1em; }

@media (max-width: 760px) {
  /* En colonne étroite le compteur passerait sous une intro de trois lignes
     et perdrait son rapport au titre : il repasse juste sous lui. */
  .section-head--compte { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
}
@media (prefers-reduced-motion: reduce) {
  .pcount__s { transition: none; }
}

/* Buttons — sharp, uppercase, wide-tracked */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  font-family: var(--font-sans);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius-xs);
  cursor: pointer;
  white-space: nowrap;
  border: 1px solid transparent;
  padding: 13px 26px;
  font-size: 0.8125rem;
  /* Resserré avec les paliers de `--tracking-*` (07/08/2026). Un libellé de
     bouton est un mot qu'on lit d'un bloc : l'écarter le transforme en suite
     de lettres. Le petit et le grand gabarit suivent la même pente. */
  letter-spacing: 0.07em;
  transition: background var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
/* Appui : léger recul d'échelle plutôt qu'un déplacement d'un pixel. C'est la
   règle du projet (scale 0.96-0.97), elle n'était pas appliquée ici. */
.btn:active { transform: scale(0.97); }
.btn--sm { padding: 8px 16px; font-size: 0.75rem; letter-spacing: 0.08em; }
.btn--lg { padding: 17px 36px; font-size: 0.9375rem; letter-spacing: 0.06em; }

.btn--primary {
  background: var(--bump-charcoal);
  color: var(--bump-paper);
  border-color: var(--bump-charcoal);
}
.btn--primary:hover { background: var(--bump-forest); border-color: var(--bump-forest); }

.btn--forest {
  background: var(--bump-forest);
  color: var(--bump-paper);
  border-color: var(--bump-forest);
}
.btn--forest:hover { background: var(--forest-500); border-color: var(--forest-500); }

.btn--outline {
  background: transparent;
  color: var(--bump-charcoal);
  border-color: var(--border-medium);
}
.btn--outline:hover { border-color: var(--bump-charcoal); }

/* ---- La flèche des boutons « aller vers » (05/09/2026) ----
   Même dessin que `.pitem__go` (les lignes de `projets.html`) et que
   `.stat__arrow` (les sources de Résultats) : un seul tracé de flèche
   diagonale, réutilisé partout plutôt que réinventé pour chaque bouton — la
   demande client était justement une cohérence entre « toutes les flèches
   vers le haut ». `currentColor` : elle suit le texte du bouton dans tous ses
   états (repos, survol, sur fond sombre), sans réglage de couleur à part. */
.btn__fleche {
  width: 0.85em;
  height: 0.85em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 260ms var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) {
  .btn--fleche:hover .btn__fleche { transform: translate(2px, -2px); }
}

/* Variantes pour fonds sombres (hero vert forêt) */
.btn--sage {
  background: var(--bump-sage);
  color: var(--bump-charcoal);
  border-color: var(--bump-sage);
}
.btn--sage:hover { background: var(--bump-paper); border-color: var(--bump-paper); }

.btn--ghost {
  background: transparent;
  color: var(--bump-paper);
  border-color: rgba(243, 244, 239, 0.35);
}
.btn--ghost:hover { border-color: var(--bump-paper); }

.btn--full { width: 100%; }

/* Tag / badge */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-weight: var(--weight-medium);
  font-size: 0.6875rem;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  line-height: 1;
  white-space: nowrap;
}
.tag--forest { color: var(--bump-forest); border: 1px solid var(--forest-300); }
.tag--sage { background: var(--surface-sage); color: var(--bump-charcoal); }

/* ============================================================
   Header
   ------------------------------------------------------------
   🚨 Fond OPAQUE, jamais translucide + flou. Le « verre dépoli »
   (`rgba(243,244,239,.82)` + `backdrop-filter: blur(12px)`) a été
   retiré le 07/08/2026 à la demande du client. Une barre qui laisse
   deviner ce qui passe dessous fait bouger deux plans à la fois,
   alors qu'elle porte le logo et la navigation : elle doit être une
   surface franche, séparée du contenu par une arête d'1 px.
   Ne pas y remettre de flou.

   Seule exception : `.site-header--hero` avant scroll, où elle est
   ENTIÈREMENT transparente. Ce n'est pas un effet de verre mais un
   hero immersif à bord perdu — `.hero-pin` remonte de
   `-1 × --header-h` précisément pour filer dessous.
   ============================================================ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--surface-page);
  border-bottom: 1px solid var(--border-subtle);
}
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding-top: 18px;
  padding-bottom: 18px;
}
/* Conservé pour `projets.html`, qui pose explicitement le modificateur :
   depuis le retrait du verre dépoli, la barre est pleine par défaut et la
   règle ne fait plus que confirmer l'intention. */
.site-header--solide { background: var(--surface-page); }
.site-header__logo img { height: 30px; width: auto; }

/* CTA du header : pilule (touche moderne, cohérente avec le sélecteur) */
.site-header .btn { border-radius: var(--radius-pill); }

.nav {
  display: flex;
  align-items: center;
  gap: 38px;
}
.nav a {
  font-size: 12px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-body);
  position: relative;
  display: inline-block;
  padding: 4px 0;
}

/* ---- Pastille de section en cours ----
   Le seul repère permanent de la barre : la barre dit maintenant où l'on
   est, pas seulement où l'on peut aller. `is-active` est posé par nav.js
   (ancres de la home), `aria-current` vient du HTML (page projets). */
.nav a::before {
  content: "";
  position: absolute;
  left: 50%; bottom: -3px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--bump-forest);
  transform: translateX(-50%) scale(0);
  transition: transform var(--dur) var(--ease-out),
              background var(--dur-slow) var(--ease-out);
}
.nav a[aria-current]::before,
.nav a.is-active::before { transform: translateX(-50%) scale(1); }

/* ---- Survol : les lettres basculent, dans un ordre aléatoire ----
   Chaque libellé est doublé — la rangée du dessus sort par le haut, sa
   copie entre par le bas. Le désordre vient d'un délai tiré au sort PAR
   LETTRE (`--ld`, réécrit par nav.js à chaque survol) : c'est lui qui fait
   le grain, pas la durée. Deux survols du même lien ne se ressemblent donc
   jamais tout à fait.
   ⚠️ Le balisage est construit par nav.js, et seulement s'il y a un
   pointeur fin et pas de mouvement réduit. Partout ailleurs le lien reste
   du texte simple, et c'est le filet ci-dessous qui sert d'accroche —
   d'où le `:not(:has(.nav__swap))`, qui ne le sort que dans ce cas. */
.nav__swap {
  display: block;
  position: relative;
  /* `clip` et pas `hidden` : jamais de conteneur de défilement (piège
     transverse du site), et le découpage est exactement le même. */
  overflow: clip;
  line-height: 1.3;
}
.nav__row { display: flex; }
.nav__row--b { position: absolute; inset: 0; }
.nav__c { display: block; }
/* Repos de la copie : une hauteur de ligne plus bas, donc hors du cadre. */
.nav__row--b .nav__c { transform: translateY(100%); }

.nav a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--bump-charcoal);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 340ms var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) {
  .nav__c {
    transition: transform 460ms var(--ease-soft);
    transition-delay: var(--ld, 0ms);
  }
  .nav a:hover .nav__row--a .nav__c,
  .nav a:focus-visible .nav__row--a .nav__c { transform: translateY(-100%); }
  .nav a:hover .nav__row--b .nav__c,
  .nav a:focus-visible .nav__row--b .nav__c { transform: translateY(0); }

  /* Repli : sans le doublage, le survol reprend le filet historique. */
  .nav a:not(:has(.nav__swap)):hover::after { transform: scaleX(1); }
}

/* ---- Titres de section : mêmes lettres qui basculent (voir titres.js) ----
   Le geste de la nav, porté sur les grands titres, avec DEUX déclencheurs :
   les lettres montent en place à l'entrée dans le champ de vision, puis
   rebasculent au survol.

   🚨 Le découpage se fait par MOT et pas sur le titre entier. Un titre
   s'enroule sur plusieurs lignes ; un `overflow: clip` posé sur l'ensemble
   laisserait une lettre de la 1re ligne descendre en plein sur la 2e au lieu
   de la masquer. Chaque mot porte donc son propre cadre, à hauteur de ligne.
   ⚠️ `clip` et jamais `hidden` : pas de conteneur de défilement (piège
   transverse du site), et le découpage est exactement le même. */
.tsw__w {
  display: inline-flex;
  position: relative;
  overflow: clip;
  /* Le cadre suit la LIGNE, pas le glyphe : à `--leading-tight` (1,04) un
     jambage de « p » ou l'accent d'un « É » affleure le bord et se ferait
     raboter. On élargit le cadre de part et d'autre et on reprend la même
     valeur en marge négative — le titre garde donc exactement sa mise en page.
     ⚠️ `--tsw-air` sert AUSSI à recaler la copie (`inset` ci-dessous) : les
     deux doivent bouger ensemble. */
  --tsw-air: 0.16em;
  padding-block: var(--tsw-air);
  margin-block: calc(-1 * var(--tsw-air));
  /* Aligné par le haut : un élément dont l'`overflow` n'est pas `visible`
     prend son bord bas pour ligne de base, ce qui gonflerait l'interligne du
     titre. Tous les mots étant construits pareil, ils restent alignés entre
     eux. */
  vertical-align: top;
}
.tsw__r { display: flex; }
/* Le cadre de positionnement d'un absolu est la boîte de PADDING : sans ce
   décalage, la copie se poserait `--tsw-air` trop haut. */
.tsw__r--b { position: absolute; left: 0; right: 0; top: var(--tsw-air); }
.tsw__c {
  display: block;
  transition: transform 460ms var(--ease-soft);
  transition-delay: var(--ld, 0ms);
}
/* 🚨 La lettre de la rangée B est du CONTENU GÉNÉRÉ, pas un nœud texte. C'est
   ce qui garde le `textContent` du titre propre (« Nos prestations » et non
   « NosNos prestationsprestations ») : le contenu généré par CSS n'appartient
   pas au DOM, donc le doublage de l'animation reste invisible aux crawlers.
   Voir `rangee()` dans titres.js, qui pose le `data-c`.
   ⚠️ Sans cette règle, la rangée B est VIDE et l'animation de bascule ne
   montre plus rien : les deux moitiés vont ensemble. */
.tsw__r--b .tsw__c::after { content: attr(data-c); }
/* 🚨 La course vaut une hauteur de ligne PLUS `--tsw-air`, jamais 100 % tout
   court. Le cadre a été élargi de `--tsw-air` pour laisser passer accents et
   jambages : une lettre qui ne descend que de 100 % laisse donc dépasser ce
   même `--tsw-air` de son sommet sous la ligne — 7 px sur un titre de 46 px,
   parfaitement visible (constaté). Avec la course corrigée, le bord de la
   lettre tombe exactement sur le bord du cadre.
   ⚠️ Si `--tsw-air` change, ces quatre courses suivent toutes seules. */
.tsw__r--b .tsw__c { transform: translateY(calc(100% + var(--tsw-air))); }
/* Avant l'entrée dans le champ : la rangée visible attend elle aussi en bas.
   Elle remonte lettre par lettre quand `is-armed` tombe. */
.tsw.is-armed .tsw__r--a .tsw__c { transform: translateY(calc(100% + var(--tsw-air))); }

@media (hover: hover) and (pointer: fine) {
  /* `.is-in` : tant que le titre n'est pas entré, il n'a rien à rebasculer. */
  .tsw.is-in:hover .tsw__r--a .tsw__c { transform: translateY(calc(-100% - var(--tsw-air))); }
  .tsw.is-in:hover .tsw__r--b .tsw__c { transform: translateY(0); }
  [data-swap]:focus-visible .tsw__r--a .tsw__c { transform: translateY(calc(-100% - var(--tsw-air))); }
  [data-swap]:focus-visible .tsw__r--b .tsw__c { transform: translateY(0); }

  /* 🚨 LE TITRE DE `projets.html` NE REBASCULE PLUS AU SURVOL (01/09/2026).
     Retour client : « le hover sur le titre, sur cette page, je ne veux pas
     que ça le fasse, mais j'aimerais le garder sur le reste, sinon ça fait
     trop. » La page porte déjà un geste au pointeur — le sillage d'images qui
     suit le curseur (`.ptrail`) — et le second geste sur le titre en faisait
     un de trop, superposé au premier.
     ⚠️ L'ENTRÉE AU SCROLL N'EST PAS TOUCHÉE : elle est pilotée par
     `is-armed`/`is-in` (IntersectionObserver dans titres.js), jamais par
     `:hover` — ces deux règles n'annulent QUE le rebasculement au survol/
     focus, rien d'autre. `titres.js` n'a pas changé une ligne : le script
     continue de retirer un nouvel ordre de lettres à chaque `pointerenter`,
     il ne sert simplement plus à rien ici puisque le CSS ne montre plus ce
     tirage.
     ⚠️ Scopé à `.phead h1 .tsw`, le SEUL titre porté par `data-swap` sur
     cette page (`projets.html`) : les 8 `<h2>` de section de la home ne sont
     pas concernés et gardent le survol. Plus spécifique que la règle
     ci-dessus grâce au sélecteur de type `h1` en plus des 5 classes
     (spécificité 0,5,1 contre 0,5,0) : gagne quel que soit l'ordre dans la
     feuille, mais placé après par convention. */
  .phead h1 .tsw:hover .tsw__r--a .tsw__c,
  .phead h1[data-swap]:focus-visible .tsw__r--a .tsw__c { transform: none; }
  .phead h1 .tsw:hover .tsw__r--b .tsw__c,
  .phead h1[data-swap]:focus-visible .tsw__r--b .tsw__c { transform: translateY(calc(100% + var(--tsw-air))); }
}

/* 🚨 Le bloc qui porte un titre à lettres ne se DÉPLACE plus à l'apparition :
   sa montée, c'est désormais celle des lettres. Il ne lui reste que le fondu.
   Sans ça, deux déplacements verticaux se superposent (les 18 px du `.reveal`
   + la hauteur de ligne des lettres) et le geste devient mou. */
.reveal:has(.tsw),
.reveal:has(.tsw).is-in { transform: none; }

.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  width: 40px; height: 40px;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  width: 22px; height: 1.5px;
  background: var(--bump-charcoal);
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out),
              background var(--dur-slow) var(--ease-out);
}

/* ---- Header au-dessus du hero sombre : transparent, logo + liens clairs.
       Redevient papier dès que le hero est quitté (.is-scrolled) ou que le
       menu mobile est ouvert (.is-menu-open). ---- */
.site-header {
  transition: background var(--dur-slow) var(--ease-out),
              border-color var(--dur-slow) var(--ease-out);
}
.site-header__logo { position: relative; display: block; }
.site-header__logo img { transition: opacity var(--dur-slow) var(--ease-out); }
.site-header__logo-light { position: absolute; inset: 0; opacity: 0; }
.nav a { transition: color var(--dur-slow) var(--ease-out); }

.site-header--hero:not(.is-scrolled):not(.is-menu-open) {
  background: transparent;
  border-bottom-color: transparent;
}
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .site-header__logo-dark { opacity: 0; }
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .site-header__logo-light { opacity: 1; }
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .nav a { color: var(--bump-paper); }
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .nav a::after { background: var(--bump-paper); }
/* Sur le forêt, c'est la couleur claire qui marque l'état actif — même
   règle que les pilules de secteur du hero et l'onglet actif du contact. */
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .nav a::before { background: var(--bump-sage); }
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .nav-toggle span { background: var(--bump-paper); }
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .btn--primary {
  background: var(--bump-sage);
  color: var(--bump-forest);
  border-color: var(--bump-sage);
}
.site-header--hero:not(.is-scrolled):not(.is-menu-open) .btn--primary:hover {
  background: var(--bump-paper);
  color: var(--bump-forest);
  border-color: var(--bump-paper);
}

/* ============================================================
   Hero — immersif : vert forêt, épure architecturale animée,
   accroche centrée, cartes médias flottantes reliées par des
   ponts d'épure. Épinglé sur ~3 écrans : Immobilier →
   Construction → Hôtellerie au scroll (voir hero.js).
   Vidéos = images : poster, lecture au survol, aucun contrôle.
   ============================================================ */
.hero-pin {
  /* --pin-travel = 3 segments courts (bascule rapide entre secteurs).
     + --stack-band : un écran de plus pendant lequel le hero reste épinglé et
     le panneau « pont » remonte par-dessus. */
  height: calc(var(--pin-travel) + var(--stack-band));
  position: relative;
  margin-top: calc(-1 * var(--header-h)); /* file sous le header transparent */
  background: var(--surface-forest);
}
/* Zone « le hero est la couche visible » : tout le pin sauf la bande de
   recouvrement. Sert de cible d'observation (header, boucle d'animation) et de
   mesure du parcours des secteurs — jamais d'affichage. */
.hero-pin__band {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--stack-band);
  pointer-events: none;
}
.hero {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  background: var(--surface-forest);
  color: var(--text-inverse);
}
/* Profondeur : légère lumière haut-gauche, vert profond dans l'angle bas-droit.

   🚨 LE BORD BAS EST RAMENÉ AU VERT EXACT DE `--surface-forest`, et ce n'est
   pas cosmétique (corrigé le 01/09/2026).

   `.hero` fait `100svh` — la hauteur du viewport quand la barre d'URL du
   téléphone est DÉPLOYÉE. Dès qu'elle se replie, la zone visible grandit
   jusqu'à `100lvh` et l'on voit, sous le hero, une bande de `.hero-pin`
   (plat `--surface-forest`, rgb(22,43,28)). Or le radial bas-droit assombrit
   le bord bas du hero jusqu'à rgb(17,34,22) : mesuré, 5 points d'écart entre
   les deux verts, exactement à la couture. D'où la « zone de vert foncé qui
   n'a pas la même teinte que la hero » signalée par le client.

   La dernière couche est donc un aplat opaque de `--surface-forest` qui reprend
   la main sur les 14 derniers pourcents : le bord bas du hero vaut alors le
   fond du pin AU PIXEL, quelle que soit l'unité de viewport retenue par le
   navigateur. La profondeur reste entière là où on la voit.

   ⚠️ Corriger plutôt en passant le hero à `100lvh` obligerait à changer
   `--stack-band` du même montant (les deux DOIVENT coïncider, voir
   « Pont vers le contact ») et rejouerait toute la chorégraphie de l'épinglage.
   Ne pas y toucher : c'est le fond qu'on aligne, pas la géométrie. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background:
    linear-gradient(to bottom, transparent 86%, var(--surface-forest) 100%),
    radial-gradient(120% 90% at 22% 6%, rgba(243, 244, 239, 0.05), transparent 55%),
    radial-gradient(140% 110% at 88% 112%, rgba(15, 31, 20, 0.9), transparent 62%);
  pointer-events: none;
}

/* ---- Accroche centrée ---- */
.hero__center {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: calc(var(--header-h) + 16px) var(--gutter) 0;
  pointer-events: none; /* la souris passe aux cartes derrière */
}
.hero__center > * { pointer-events: auto; } /* titre + sélecteur cliquables ; le vide laisse passer vers les cartes */
.hero .eyebrow {
  color: var(--bump-sage);
  margin-bottom: clamp(14px, 2vh, 22px);
}
.hero h1 {
  font-size: clamp(30px, min(4.2vw, calc(24px + 0.9375vw)), 60px);
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin: 0;
  /* 🚨 Cette mesure est LARGE EXPRÈS : c'est elle qui rend les retours à la
     ligne du CMS (`data-cms-lignes`) décisifs. À `24ch` le titre se coupait
     déjà tout seul en 5 lignes, et un saut saisi s'AJOUTAIT à ces coupures au
     lieu de les remplacer — retour client du 15/08/2026. Mesuré : la bascule
     se fait à 28ch (5 lignes → 3) ; à partir de là deux sauts donnent
     exactement trois lignes, donc l'auteur commande.
     ⚠️ Ne pas monter au-delà de ~34ch : à 1280px, 38ch ne laisse plus que 9px
     de marge totale dans la colonne. Le `min(100%, …)` est le garde-fou qui
     tient les fenêtres plus étroites — ne pas le retirer.
     ⚠️ Sous 760px on RETOMBE à 24ch (voir le palier mobile) : là les sauts
     sont neutralisés, et c'est le rapport corps/mesure qui règle le pli. */
  max-width: min(100%, 34ch);
  color: var(--bump-paper);
}

/* ---- Sélecteur de secteur : au centre, sous le titre ---- */
.hero__switch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(10px, 1.6vh, 16px);
  margin-top: clamp(22px, 3.4vh, 40px);
}
.hero__switch-hint {
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: rgba(243, 244, 239, 0.5);
}
.hero__switch-pills {
  display: flex;
  gap: 8px;
}
/* 🚨 La pilule au repos est en PAPYRUS dilué, jamais en vert.
   Elle a été en `rgba(22,43,28,.5)` — du forêt sur du forêt : les deux
   secteurs non choisis se camouflaient dans le fond du hero et on ne voyait
   qu'un liseré (signalé par le client le 07/08/2026). Un bouton doit se voir
   avant qu'on le survole.
   L'échelle des trois états passe donc uniquement par la DOSE de papyrus —
   voile 16 % au repos, 26 % au survol, aplat plein à l'état actif — et jamais
   par un changement de teinte. Ne pas y remettre de vert. */
.hs-btn {
  display: inline-flex;
  align-items: center;
  padding: 9px;
  background: rgba(208, 211, 193, 0.16);
  border: 1px solid rgba(208, 211, 193, 0.52);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.hs-btn:active { transform: scale(0.96); }
.hs-btn:hover {
  background: rgba(208, 211, 193, 0.26);
  border-color: rgba(208, 211, 193, 0.8);
}
.hs-btn__ico { position: relative; width: 26px; height: 26px; flex: none; }
.hs-btn__ico img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: opacity var(--dur) var(--ease-out);
}
.hs-btn__pd { opacity: 0; } /* picto sombre, visible sur fond sage actif */
.hs-btn__label {
  display: block;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateX(-6px);
  white-space: nowrap;
  font-size: 0.875rem;
  font-weight: var(--weight-medium);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--bump-paper);
  transition: max-width 0.5s var(--ease-drawer),
              opacity 0.3s var(--ease-out),
              transform 0.5s var(--ease-drawer),
              padding 0.5s var(--ease-drawer);
}
.hs-btn:hover .hs-btn__label,
.hs-btn.is-active .hs-btn__label {
  max-width: 180px;
  opacity: 1;
  transform: none;
  padding-left: 10px;
  padding-right: 8px;
}
.hs-btn.is-active { background: var(--bump-sage); border-color: var(--bump-sage); }
.hs-btn.is-active .hs-btn__label { color: var(--bump-charcoal); }
.hs-btn.is-active .hs-btn__pv { opacity: 0; }
.hs-btn.is-active .hs-btn__pd { opacity: 1; }

/* ---- Cartes médias flottantes (construites par hero.js) ---- */
.hero__cards {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none; /* seules les cartes captent la souris */
}
.hm-set { display: none; }
.hm-set.is-on { display: block; }
.hm-card {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y; /* le scroll vertical reste fluide sur tactile */
  will-change: transform;
}
/* 🚨 Carte écartée de la sélection mobile, posée par hero.js (`selectionne()`).
   Masquée et non retirée du DOM : le seuil des 760 px se franchit dans les deux
   sens à la rotation de l'écran, et reconstruire les sets à chaque passage
   relancerait le chargement des vidéos. */
.hm-card--off { display: none; }

.hm-card:hover,
.hm-card.is-drag { z-index: 5; }
@media (hover: hover) and (pointer: fine) {
  .hm-card { cursor: grab; }
  .hm-card.is-drag { cursor: grabbing; }
}
/* ---- Échelle du hero ----
   La composition doit avoir LA MÊME allure sur un portable Full HD et sur un
   écran 4K : les cartes sont donc dimensionnées en unités d'écran, sans plafond
   en pixels (un plafond ferait rétrécir la composition sur les grands écrans).
   L'unité est bornée par la HAUTEUR autant que par la largeur (le hero fait
   100svh) : rien ne déborde verticalement sur un écran large et court.
   1.6 = rapport de référence 1440 × 900, où l'échelle vaut exactement 1vw.
   ⚠️ sizePx() dans hero.js et sizePxRef() dans admin.html sont les miroirs
   exacts de ces valeurs — les trois doivent rester synchronisés. */
:root { --hero-u: min(1vw, 1.6svh); }

.hm-card--sm { width: max(120px, calc(11.5 * var(--hero-u))); }
.hm-card--md { width: max(148px, calc(14   * var(--hero-u))); }
.hm-card--lg { width: max(176px, calc(16.5 * var(--hero-u))); }
.hm-card--xl { width: max(200px, calc(18.5 * var(--hero-u))); }

/* La carte : rotation de repos, redressée + agrandie au survol.
   L'entrée en cascade (hmIn, backwards) se rejoue à chaque bascule
   de secteur, car le set repasse par display:none. */
.hm-card__in {
  border-radius: var(--radius-md);
  overflow: hidden;
  isolation: isolate;
  border: 1px solid rgba(243, 244, 239, 0.12);
  box-shadow: 0 18px 44px rgba(15, 31, 20, 0.45);
  background: var(--bump-forest);
  transform: rotate(var(--r, 0deg));
  transition: transform 0.4s var(--ease-out), box-shadow 0.4s var(--ease-out);
  animation: hmIn 0.7s var(--ease-out) calc(var(--i, 0) * 50ms) backwards;
}
@keyframes hmIn {
  from { opacity: 0; transform: translateY(20px) scale(0.93) rotate(var(--r, 0deg)); }
}
@media (hover: hover) and (pointer: fine) {
  .hm-card:hover .hm-card__in {
    transform: rotate(0deg) scale(1.045);
    box-shadow: 0 26px 60px rgba(15, 31, 20, 0.55);
  }
}
.hm-set.is-out .hm-card__in {
  animation: none;
  opacity: 0;
  transform: translateY(-12px) scale(0.96) rotate(var(--r, 0deg));
  transition: opacity 0.19s ease, transform 0.19s var(--ease-out);
  transition-delay: calc(var(--i, 0) * 12ms);
}
.hm-card img,
.hm-card video {
  display: block;
  width: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
}
.hm-card--port img, .hm-card--port video { aspect-ratio: 4 / 5; }
.hm-card--land img, .hm-card--land video { aspect-ratio: 4 / 3; }

/* ---- Ponts d'épure entre les cartes (endpoints suivis par hero.js) ---- */
.hero__bridges {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.hero__bridges.is-dim { opacity: 0; }
.hero__bridges line {
  stroke: var(--bump-sage);
  stroke-opacity: 0.2;
  stroke-width: 1;
  stroke-dasharray: 4 7;
  animation: bridgeDash 80s linear infinite;
}
.hero__bridges circle { fill: var(--bump-sage); opacity: 0.3; }
@keyframes bridgeDash { to { stroke-dashoffset: -440; } }

/* ============================================================
   Pont vers le contact — le panneau qui remonte sur le hero

   Recouvrement au scroll, tout en CSS, UNE SEULE FOIS :
     .hero        épinglé (sticky) dans .hero-pin
     .hero-outro  remonte par-dessus (margin-top négatif = la bande de
                  recouvrement --stack-band), coins hauts arrondis + ombre
                  portée vers le haut → on le voit glisser sur le hero
   Une fois le panneau passé, le site reprend son défilement normal : les
   sections suivantes ne repassent PAS par-dessus lui. Un seul effet, sinon
   la page donne l'impression de se répéter.
   ============================================================ */
.hero-outro {
  position: relative;
  z-index: 2;
  margin-top: calc(-1 * var(--stack-band));
  /* Hauteur libre, pilotée par le contenu : le panneau est plus court qu'un
     écran, et la bande de recouvrement vaut un écran entier. Le bas du panneau
     tombe donc encore dans .hero-pin, où le hero est sticky donc positionné —
     c'est `.after-hero` (position: relative) qui remet la suite du site
     au-dessus de lui. Sans ce conteneur, le vert du hero recouvre les sections
     suivantes, qui ne sont pas positionnées. */
  display: flex;
  align-items: center;
  /* ⚠️ `clip` et non `hidden` : `overflow: hidden` ferait de ce panneau un
     conteneur de défilement, et les `animation-timeline: view()` de ses enfants
     se calculeraient alors par rapport à LUI (qui ne défile jamais) au lieu de
     la page — les animations resteraient figées. `clip` découpe à l'identique
     sans créer de conteneur. Ne pas revenir à `hidden`. */
  overflow: clip;
  /* le padding haut dégage le header sticky (67px) */
  padding-top: clamp(88px, 11vh, 132px);
  padding-bottom: clamp(56px, 8vh, 88px);
  border-radius: var(--radius-stack) var(--radius-stack) 0 0;
  /* Le dégradé repart vers le papier en bas : la section suivante est du papier,
     donc la jonction ne fait aucune marche. Le sage vit au milieu du panneau.

     🚨 LA PREMIÈRE COUCHE EST UN APLAT DE PAPIER, ET ELLE EST INDISPENSABLE
     (corrigée le 01/09/2026). Le dégradé de dessous est incliné (172deg) : sa
     ligne n'est donc PAS horizontale, et son arrêt « papier 100 % » n'est
     atteint qu'à un seul coin. Mesuré à 1440 juste au-dessus de la frontière,
     le bord bas du panneau allait de rgb(231,232,223) à gauche à
     rgb(242,243,238) à droite — 11 points de rampe le long de l'arête — pendant
     que `.after-hero` posait un rgb(243,244,239) parfaitement uniforme en
     dessous. La jonction montrait donc une marche à gauche et pas à droite :
     ni une vraie séparation, ni un dégradé propre (retour client).

     Un aplat vertical de `--bump-paper` sur les 56 derniers pixels, fondu sur
     les 112 précédents, rend l'arête RIGOUREUSEMENT égale à `--surface-page`
     sur toute la largeur — donc invisible, ce qui est le parti retenu ici
     (uniformiser plutôt que séparer : c'est le pont qui doit glisser sur le
     hero, pas se détacher de la suite du site).

     ⚠️ `--bump-paper` et `--surface-page` doivent rester le même jeton des deux
     côtés : c'est ce qui garantit l'égalité. Si `.after-hero` change de fond,
     changer cet arrêt avec lui. */
  background:
    linear-gradient(to top, var(--bump-paper) 0 56px, transparent 168px),
    linear-gradient(172deg,
      var(--bump-paper) 0%, var(--sage-soft) 60%, var(--bump-paper) 100%);
  /* L'étalement négatif rétrécit le rectangle d'ombre pour qu'il ne dépasse
     QUE par le haut. Sans lui, un flou de 64px bavait sous le panneau et
     dessinait un liseré sombre au-dessus de la section suivante.

     🚨 -30px NE SUFFISAIT PAS, et le reliquat se voyait (corrigé le
     01/09/2026). Le calcul : le rectangle d'ombre est la boîte décalée de
     `offset` puis rétrécie de `|spread|`, et la traîne du flou porte encore
     ~`blur` au-delà de son arête. En bas cela donnait
     `bas - 34 - 30 + 72 = bas + 8` : l'ombre débordait donc bien SOUS le
     panneau. Mesuré à 1440, une bande de ~29px à rgb(238,240,234) sur le
     rgb(243,244,239) de `.after-hero` — c'est elle qui, avec la rampe du
     dégradé incliné, faisait une jonction « ni franche ni propre ».
     ⚠️ `.hero-outro` porte `z-index: 2` et `.after-hero` aucun : l'ombre se
     peint donc PAR-DESSUS la section suivante, elle ne passe pas dessous.

     🚨 LES DEUX VALEURS ONT BOUGÉ ENSEMBLE, ET C'EST VOLONTAIRE : l'aspect
     vers le HAUT est inchangé au pixel. Arête haute du rectangle
     = `haut - 44 + 40 = haut - 4`, exactement comme avant (`haut - 34 + 30`),
     donc la même portée vers le hero. En bas : `bas - 44 - 40 + 72 = bas - 12`,
     l'ombre s'arrête désormais AVANT l'arête. Si l'une des trois valeurs
     change, refaire les deux calculs. */
  box-shadow: 0 -44px 72px -40px rgba(15, 31, 20, 0.55);
}
/* Halo sage très doux en haut à gauche : introduit la couleur de charte sans
   rien ajouter au contenu. */
.hero-outro::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(72% 88% at 6% 0%, rgba(208, 211, 193, 0.5), transparent 62%);
  pointer-events: none;
}
.hero-outro > .shell {
  position: relative;
  z-index: 1;
  width: 100%;
}
/* Au-delà de 1600px, le panneau s'élargit et le titre continue de grandir :
   sans ça, la composition occupe la moitié d'un écran 2560 et le panneau
   (100svh) est aux trois quarts vide. Même logique que --hero-u pour le hero. */
@media (min-width: 1600px) {
  .hero-outro > .shell { max-width: min(1560px, 92vw); }
}

/* Le SIGLE (pas le logo complet), agrandi à deux fois la hauteur du panneau et
   collé au bord gauche dont il déborde. Filigrane ton sur ton, seul élément
   décoratif : il remplace un visuel, et il occupe le vide sous la colonne de
   gauche. Dérive de droite à gauche au scroll.
   Le logo complet a été essayé : ses lettres se lisent en toutes lettres et le
   panneau se met à répéter le nom déjà présent dans la nav et le footer. Le
   sigle est un signe, pas un mot — c'est ce qui en fait une texture. */
.bridge__mark {
  position: absolute;
  top: 50%;
  left: 0;
  /* 150 % : il déborde en haut et en bas (donc « zoomé », coupé par le cadre)
     mais le signe reste lisible (~2/3 visible). À 200 % on ne voyait plus que
     deux diagonales et le sigle ne se reconnaissait plus.
     C'est cette taille qui permet de le pousser vers la DROITE sans qu'il se
     décolle du bord gauche : plus large, il déborde encore à x=0 alors que sa
     masse a avancé d'une centaine de pixels. */
  height: 150%;
  width: auto;
  /* Repos = milieu de la course de `markDrift`, pour que l'état sans animation
     (mouvement réduit, navigateur ancien) soit celui du milieu et pas un bout. */
  translate: -23% -50%;
  color: var(--sage-deep);
  /* Volontairement bas : au-delà, le signe se lit comme un élément et
     concurrence le texte posé dessus. C'est une texture de fond. */
  opacity: 0.16;
  pointer-events: none;
}
/* ⚠️ Après la règle de base, pas dans le bloc `min-width: 1600px` du dessus :
   celui-ci est déclaré AVANT `.bridge__mark`, il se ferait écraser.
   Au-delà de 1600px la gouttière fait ~500px. Collé au bord de l'ÉCRAN, le sigle
   se retrouvait entièrement dans ce vide et ne touchait plus la composition. On
   le colle donc au bord du CONTENU (même expression que la largeur du shell, à
   garder synchronisée) : il déborde dans la gouttière au lieu d'y vivre. Un peu
   plus grand aussi, sinon il rétrécit à vue d'œil pendant que le panneau, lui,
   reste haut de ~645px. */
@media (min-width: 1600px) {
  .bridge__mark {
    left: calc((100% - min(1560px, 92vw)) / 2);
    height: 155%;
  }
}

/* Structure reprise de la référence client (anoracstudio.ch) :
     · colonne étroite à gauche (5/12) = le titre, en bloc plein ;
     · colonne LARGE à droite (7/12), DÉCALÉE VERS LE BAS = le texte, le filet
       et l'action.
   Les deux partent du haut, c'est le décalage de la colonne droite qui crée la
   diagonale de lecture.

   ⚠️ Deux erreurs de la version précédente, à ne pas refaire :
   1. les proportions étaient inversées (large à gauche, étroite à droite) ;
   2. le filet traversait tout le cadre. Dans la référence il est À L'INTÉRIEUR
      de la colonne de droite et ne sépare que ses deux paragraphes. En pleine
      largeur il coupe la composition en deux et se voit trop. */
/* DEUX rangées : les pictos seuls sur la première (colonne 1), puis le titre et
   le texte de droite sur la seconde. C'est la grille qui garantit que les deux
   colonnes démarrent au même y — pas une valeur de `padding-top` recalculée à la
   main, qui se dérègle dès que la taille du titre change.
   ⚠️ La colonne droite avait un `padding-top: clamp(0px, 7vw, 104px)` : sa
   première ligne tombait alors ~15 px SOUS celle du titre. Trop peu pour se lire
   comme un décalage voulu, assez pour se lire comme une erreur (retour client).
   Soit on aligne exactement, soit on décale franchement — le presque-aligné est
   toujours le mauvais choix. */
.bridge {
  display: grid;
  grid-template-columns: 5fr 7fr;
  grid-template-rows: auto 1fr auto;
  column-gap: clamp(28px, 5.5vw, 80px);
  align-items: stretch;
}
.bridge__sectors { grid-column: 1; grid-row: 1; }
/* `align-self: start` : sans lui le `stretch` de la grille étire la boîte du h2
   sur toute la rangée. Sans effet visible ici, mais une boîte qui ment sur son
   contenu finit toujours par surprendre. */
.bridge__title   { grid-column: 1; grid-row: 2; align-self: start; }
/* L'action au PIED de la colonne de gauche, et la colonne de droite qui enjambe
   les deux rangées : c'est ce qui fait finir les deux colonnes ensemble.
   ⚠️ L'action a vécu dans `.bridge__aside` : la colonne de droite finissait alors
   158 px plus bas que la gauche et le bas du panneau était un trou (retour
   client du 29/07/2026, capture à l'appui). */
.bridge__actions { grid-column: 1; grid-row: 3; }
/* Elle enjambe les deux rangées et RÉPARTIT son espace libre : sa dernière ligne
   descend donc exactement sur le bas du bouton, quelle que soit la façon dont le
   texte s'est enroulé. Les deux colonnes commencent ET finissent au même y.
   Sans ça la hauteur naturelle des deux colonnes ne coïncide à aucune largeur :
   à 1254 px le paragraphe tient sur une ligne, à 1440 sur deux. */
.bridge__aside {
  grid-column: 2;
  grid-row: 2 / span 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  max-width: 64ch;
}

/* ⚠️ Doit rester SOUS le `<h1>` du hero (`clamp(30px, 4.2vw, 60px)`). Il était
   à 4.3vw/84px, donc plus gros que le titre principal : hiérarchie inversée, et
   la bascule hero → pont paraissait brutale. Viser ~0,82 × le h1. */
.bridge__title {
  margin: 0;
  font-size: clamp(28px, min(3.4vw, calc(20.8px + 0.8125vw)), 52px);
  /* Interlignage serré comme la référence (`leading-[0.95]`) : c'est ce qui fait
     du titre un bloc plein plutôt qu'une suite de lignes. Pas de `max-width`,
     c'est la colonne qui découpe. */
  line-height: 1.02;
  letter-spacing: -0.022em;
  color: var(--bump-charcoal);
}
/* Emphase par la graisse et la couleur, pas par l'italique (préférence client :
   le moins d'italique possible sur le site). Graisse 400 et non 500 : le hero
   est intégralement en 300, un bond à 500 faisait tache à côté. */
.bridge__title em {
  font-style: normal;
  font-weight: 400;
  color: var(--bump-forest);
}

/* Accroche et paragraphe à la MÊME taille : seule la graisse les distingue,
   comme dans la référence (un `<strong>` en tête du même paragraphe). Deux
   tailles différentes fabriquaient une hiérarchie de plus pour rien. */
.bridge__lead,
.bridge__text,
.bridge__note {
  margin: 0;
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.6;
}
.bridge__lead {
  font-weight: var(--weight-medium);
  color: var(--bump-charcoal);
  margin-bottom: 12px;
}
.bridge__text { font-weight: 400; color: var(--text-body); }
.bridge__note { font-weight: 400; color: var(--text-muted); }
/* Filet interne à la colonne droite, il ne sépare que les deux paragraphes */
.bridge__rule {
  height: 1px;
  width: 100%;
  margin: clamp(22px, 2.4vw, 34px) 0;
  border: 0;
  background: var(--border-subtle);
}
/* Bouton puis email empilés : l'un sous l'autre, ils lisent comme un seul point
   d'action. Côte à côte, l'email concurrençait le bouton. */
/* Sous le texte, aligné à gauche de la colonne : c'est la fin du parcours de
   lecture, elle reste dans l'axe des paragraphes. */
.bridge__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px 28px;
  margin-top: clamp(26px, 3vw, 40px);
}
/* Le bouton n'est plus en `--lg` : à 267 × 70 px il pesait plus lourd que deux
   lignes de titre. Rond réduit à 28 px, le bouton tombe à ~54 px de haut. */
.bridge__actions .btn__circle { width: 28px; height: 28px; }
/* L'email reste un lien, pas un second bouton : un seul appel à l'action fort */
.bridge__mail {
  font-size: 0.8125rem;
  font-weight: var(--weight-medium);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--border-medium);
  transition: color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.bridge__mail:hover { color: var(--bump-forest); border-color: var(--bump-forest); }

/* Les 3 pictos secteurs, en pastille au-dessus du titre. Pas de sur-titre, pas
   de libellé : le texte de droite nomme déjà les trois secteurs, et l'`alt` de
   chaque image porte le nom pour les lecteurs d'écran.

   ⚠️ Deux corrections d'alignement optique, mesurées, à ne pas défaire :
   1. Le dessin n'occupe que ~47 % de la boîte carrée du SVG (711 × 711) : chaque
      picto traîne ~26 % de vide de chaque côté. Un `gap` positif s'ajoute à ce
      vide — 34 px de gap donnaient 56 px de blanc perçu, les icônes partaient en
      archipel. On récupère donc une partie de ce vide en marge négative.
      (`gap` ne peut pas être négatif, d'où la marge sur les frères.)
   2. Pour la même raison, le premier picto commence 26 % de sa boîte à droite du
      bord de colonne, alors que le B du titre commence à 0 : la rangée paraissait
      décalée vers l'intérieur. On la retire de la même quantité pour aligner
      l'ENCRE sur le texte, pas la boîte. Idem en HAUT : sans la marge négative,
      le dessin démarrait 13,5 px sous le bord du panneau et toute la colonne
      gauche paraissait commencer plus bas que la droite. */
.bridge__sectors {
  --st-box: clamp(42px, 3.6vw, 54px);
  display: flex;
  align-items: center;
  margin: calc(var(--st-box) * -0.26) 0 clamp(20px, 2.6vw, 32px) calc(var(--st-box) * -0.26);
}
.st-sector + .st-sector { margin-left: calc(var(--st-box, 54px) * -0.22); }

/* Remet toute la suite du site au-dessus du hero. Il est encore `sticky` (donc
   positionné) derrière la fin du recouvrement, et une section non positionnée
   se peint SOUS un élément positionné quel que soit l'ordre du HTML. Purement
   structurel : pas de rayon ni d'ombre ici, sinon on recrée l'empilement en
   cascade que le client a refusé. Le fond opaque est en revanche indispensable —
   `position: relative` seul met le TEXTE au-dessus, mais le vert du hero
   continue de transparaître entre les lignes. Les sections qui ont leur propre
   couleur (résultats, contact) la posent par-dessus. */
.after-hero {
  position: relative;
  background: var(--surface-page);
}

/* ---- Vie au scroll du pont ----------------------------------------------
   Animations pilotées par la position de scroll (`animation-timeline: view()`),
   pas par un écouteur JS : elles tournent hors du thread principal, donc elles
   ne peuvent pas saccader même pendant un chargement. Un navigateur qui ne les
   connaît pas affiche simplement l'état de repos, rien ne casse.

   Deux mouvements seulement, chacun avec une raison :
     · le sigle dérive vers la gauche et grandit un peu → profondeur, le fond
       ne colle pas au texte ;
     · le bloc de texte remonte doucement → ouvre de l'espace sous lui à mesure
       qu'on avance vers la suite du site.
   La courbe est symétrique (--ease-scroll) : le déplacement est presque nul aux
   extrémités et maximal au milieu de la traversée. C'est ce qui fait un
   mouvement courbe au lieu d'un glissement mécanique.
   ------------------------------------------------------------------------ */
/* La dérive au scroll ne touche QUE `translate`, le flottement QUE `rotate` et
   `scale` : ce sont des propriétés distinctes, les deux animations se composent
   au lieu de s'écraser. C'est pour ça que le sigle n'utilise pas `transform`. */
/* Course volontairement longue : 54 % de la largeur du sigle, soit ~380 px sur un
   écran 1440. Le sigle est bien plus étroit que le logo complet, donc à
   pourcentage égal il paraissait à peine bouger — c'est le reproche du client. */
@keyframes markDrift {
  from { translate: 0% -50%; }
  to   { translate: -46% -50%; }
}
/* Flottement continu, indépendant du scroll : c'est lui qui fait « flotter »
   plutôt que « glisser ». Amplitudes doublées par rapport à la version d'avant. */
@keyframes markFloat {
  0%   { rotate: -1.4deg; scale: 1.02; }
  100% { rotate: 1.4deg;  scale: 1.07; }
}
/* Le texte du pont MONTE À L'ARRIVÉE, il ne dérive plus.
   Il y avait ici `bridgeLift`, qui faisait descendre tout le bloc de −34 à
   +34 px sur toute la traversée. Remplacé le 29/07/2026 : le client veut que
   le texte « apparaisse dessus, vraiment juste au moment où on swipe, et
   qu'il vienne d'en bas ». Une dérive continue ne fait pas apparaître — elle
   accompagne. Ici chaque bloc monte de 72 px en se révélant, sur une fenêtre
   courte calée sur l'arrivée du panneau (voir `animation-range` plus bas).

   Le sens reste contrasté, comme demandé à l'origine : le sigle traverse à
   l'horizontale, le texte vient de la verticale.

   Ni flou ni filtre ici : `opacity` et `transform` sont les deux seules
   propriétés que le compositeur sait animer sans repeindre. Un `blur` piloté
   par le scroll re-rastérise à chaque image et c'est exactement le genre de
   chose qui fait saccader une page pendant un chargement. */
@keyframes bridgeRise {
  from { transform: translate3d(0, 72px, 0); opacity: 0; }
  to   { transform: translate3d(0, 0, 0);    opacity: 1; }
}
/* Le hero recule pendant que le pont monte : il se décale à peine (9 % de sa
   hauteur pour un écran entier de scroll) et s'estompe dans le fond forêt du
   pin, qui est de la même couleur. C'est ce décalage de vitesse qui donne la
   profondeur ; sans lui, hero et panneau défilent du même pas. */
@keyframes heroRecede {
  from { transform: translate3d(0, 0, 0); opacity: 1; }
  to   { transform: translate3d(0, -9%, 0); opacity: 0.45; }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .bridge__mark {
      animation-name: markDrift, markFloat;
      animation-duration: auto, 15s;
      animation-fill-mode: both, none;
      animation-timing-function: var(--ease-scroll), ease-in-out;
      animation-iteration-count: 1, infinite;
      animation-direction: normal, alternate;
      animation-timeline: view(), auto;
      /* ⚠️ 65 % et non 100 %. Le sujet du `view()` est le sigle lui-même, qui
         déborde largement du panneau : sur `cover 0% cover 100%`, il n'atteignait
         que ~0,61 de progression pendant que le panneau était encore à l'écran —
         soit 40 % de la course jamais vue, et une dérive qui paraissait molle.
         Mesuré au `getComputedTiming().progress`, pas déduit. */
      animation-range: cover 0% cover 65%, normal;
    }
    /* Là où l'animation au scroll existe, c'est ELLE qui possède l'entrée du
       texte : on neutralise le `.reveal` générique (flou + décalage + son
       observateur), sinon le flou resterait posé jusqu'au déclenchement de
       l'IntersectionObserver et les deux se marcheraient dessus. */
    .bridge > .reveal {
      filter: none;
      transition: none;
    }
    /* Chaque bloc monte en apparaissant, sur une fenêtre COURTE et décalée.
       `cover` et non `entry` : la fenêtre `entry` vaut la hauteur propre de
       l'élément — quelques dizaines de pixels pour la rangée de pictos, donc
       une apparition instantanée. `cover` couvre toute la traversée de
       l'écran, et on n'en prend que le premier tiers : c'est exactement le
       moment où le panneau recouvre le hero.
       ⚠️ Courbe `--ease-scroll` (symétrique) et non `--ease-soft`. Sur une
       animation pilotée par le scroll, une courbe de sortie concentre 80 % du
       déplacement dans le premier quart de la fenêtre : mesuré, le titre avait
       fini de monter au bout de ~66 px de scroll, ce qui se lit comme un
       surgissement, pas comme une montée. La courbe symétrique répartit le
       mouvement sur toute la fenêtre — il monte vraiment. */
    .bridge > * {
      animation-name: bridgeRise;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-scroll);
      animation-timeline: view();
      animation-range: cover 3% cover 30%;
    }
    .bridge__title   { animation-range: cover 6% cover 34%; }
    .bridge__aside   { animation-range: cover 10% cover 39%; }
    .bridge__actions { animation-range: cover 14% cover 44%; }
    /* Fenêtre absolue plutôt que `view()` : le hero est `sticky`, sa position
       de vue reste bloquée tant qu'il est épinglé, donc `view()` y serait figé.
       `scroll(root)` mesure le défilement de la page, et le pin démarre en haut
       du document : la fenêtre de recouvrement est donc calculable. */
    .hero {
      animation-name: heroRecede;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-scroll);
      animation-timeline: scroll(root block);
      animation-range: calc(var(--pin-travel) - var(--stack-band)) var(--pin-travel);
    }
  }
}

/* Entrée du contenu : plus longue et plus douce que le `.reveal` générique, et
   défloutée en arrivant — le flou masque la bascule et donne la sensation de
   mise au point plutôt que d'un bloc qui se pose. */
.bridge > .reveal {
  transform: translateY(32px);
  filter: blur(7px);
  transition: opacity var(--dur-enter) var(--ease-soft),
              transform var(--dur-enter) var(--ease-soft),
              filter var(--dur-enter) var(--ease-soft);
}
/* Cascade dans l'ordre de lecture : les secteurs, le titre, ce qui le justifie,
   puis l'action */
.bridge__title.reveal   { transition-delay: 55ms; }
.bridge__aside.reveal   { transition-delay: 110ms; }
.bridge__actions.reveal { transition-delay: 165ms; }
.bridge > .reveal.is-in {
  transform: none;
  filter: blur(0);
}

/* Bouton « button-in-button » : flèche dans un rond, glisse au survol */
.btn--ico { padding-right: 10px; }
.btn__circle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-left: 4px;
  border-radius: 50%;
  background: rgba(243, 244, 239, 0.16);
  transition: transform 340ms var(--ease-soft), background 340ms var(--ease-soft);
}
@media (hover: hover) and (pointer: fine) {
  .btn--ico:hover .btn__circle {
    transform: translate(5px, -1px);
    background: rgba(243, 244, 239, 0.3);
  }
}

/* La boîte est plus grande que l'encre voulue (~47 % de remplissage, cf. la note
   sur .bridge__sectors) : 54 px de boîte = ~26 px de dessin. */
.st-sector {
  width: var(--st-box, clamp(42px, 3.6vw, 54px));
  height: auto;
  flex: none;
  opacity: 0.9;
}

/* ---- Entrée orchestrée au chargement (accroche + sélecteur) ---- */
.hero__el {
  opacity: 0;
  animation: heroRise 0.9s var(--ease-out) forwards;
  animation-delay: var(--hd, 0s);
}
@keyframes heroRise {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}

/* ---- Épure architecturale (fond) : tracé lent, puis vie continue ----
   Lignes très fines sage ~10-16 %, respiration d'opacité déphasée (--bd),
   deux tracés (compas, empreinte au sol) se redessinent en boucle (--ld),
   dérive quasi imperceptible de l'ensemble. Jamais au premier plan. */
.hero__constructs {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transform: translateZ(0);
  animation: constructsDrift 70s ease-in-out infinite alternate;
}
@keyframes constructsDrift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(0, -1.2%, 0) scale(1.02); }
}
.hero .cline {
  fill: none;
  stroke: var(--bump-sage);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.13;
  stroke-dasharray: 360;
  stroke-dashoffset: 360;
  animation:
    clineDraw 2.6s var(--ease-out) var(--cd, 0s) forwards,
    clineBreathe 11s ease-in-out calc(2.2s + var(--bd, 0s)) infinite;
}
.hero .cline.tick {
  opacity: 0.24;
  animation: clineDraw 2.6s var(--ease-out) var(--cd, 0s) forwards;
}
.hero .cline--loop {
  animation:
    clineDraw 2.6s var(--ease-out) var(--cd, 0s) forwards,
    clineRedraw 44s ease-in-out calc(4s + var(--ld, 0s)) infinite;
}
@keyframes clineDraw { to { stroke-dashoffset: 0; } }
@keyframes clineBreathe {
  0%, 100% { opacity: 0.09; }
  50%      { opacity: 0.17; }
}
@keyframes clineRedraw {
  0%, 42% { stroke-dashoffset: 0; }
  70%     { stroke-dashoffset: 360; }
  100%    { stroke-dashoffset: 0; }
}

/* Tactile : les animations de tracé SVG repeignent à chaque frame — on garde
   uniquement le dessin initial et la dérive (compositeur) */
@media (pointer: coarse) {
  .hero .cline,
  .hero .cline--loop { animation: clineDraw 2.6s var(--ease-out) var(--cd, 0s) forwards; }
  .hero__bridges line { animation: none; }
}


/* ============================================================
   Prestations — rail horizontal de cartes verticales

   Deux modes, dans cet ordre :
     1. BASE (ce bloc) — défileur horizontal natif. C'est le repli complet :
        sans JavaScript, sans scroll-timeline, en mouvement réduit ou sur
        mobile, la section reste lisible et parcourable au doigt/à la molette.
     2. ÉPINGLÉ (bloc @supports plus bas) — la scène se fige sur un écran et
        le rail avance latéralement au fil du scroll vertical. Activé
        uniquement quand le script a pu mesurer la course (.is-pinned).
   ============================================================ */
.presta {
  padding-top: var(--space-10);
  padding-bottom: var(--space-10);
  /* Marge du rail = celle du contenu de .shell, pour que la 1re carte tombe
     pile sous le titre. `100%` (et non 100vw) : la barre de défilement ne
     doit pas décaler l'alignement. */
  --rail-pad: max(var(--gutter), calc((100% - var(--maxw-content)) / 2 + var(--gutter)));
}
/* ⚠️ Reprise en BLOC de la mise en deux colonnes de `.section-head` : ici la
   moitié droite de la rangée est déjà prise par le compteur `01 / 12`, porté
   par `.presta-head`. Le titre et son chapô restent donc empilés à gauche. */
.presta .section-head {
  margin-bottom: 0;
  display: block;
  max-width: 60ch;
}
.presta .section-head h2 { margin-bottom: var(--space-4); }
.presta .section-head .intro { text-align: left; max-width: 56ch; }
/* Le titre porte la MÊME marge que le défileur (--rail-pad), pas la classe
   `.shell` : celle-ci centre par `margin: 0 auto`, et des marges automatiques
   sur un élément de grille l'empêchent de s'étirer — le titre se retrouvait
   centré au milieu de l'écran au lieu d'être à l'aplomb de la 1re carte. */
.presta-head {
  padding-inline: var(--rail-pad);
  margin-bottom: var(--space-6);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-7);
}
/* ⚠️ `.presta-count` est AVANT `.section-head` dans le DOM (02/09/2026, voir
   index.html) pour que le flottant mobile fonctionne — mais `justify-content:
   space-between` place le PREMIER item flex à gauche : sans cet `order`, le
   compteur se retrouverait à gauche du titre sur desktop. `order` restaure
   la position visuelle d'origine sans dépendre du DOM. */
.presta-count { order: 1; }
.presta .section-head { order: 0; }

/* Compteur de position — il équilibre le bloc de titre à l'autre bout de la
   rangée et donne au rail un repère chiffré en plus de la barre.
   Aligné sur la première ligne du <h2> : même taille de corps, même graisse
   légère. Le total est en retrait, comme une mention. */
.presta-count {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 0.4em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.presta-count__now {
  font-size: clamp(30px, min(4vw, calc(18.4px + 0.71875vw)), 46px);
  font-weight: var(--weight-light);
  color: var(--text-body);
  /* Largeur figée sur deux chiffres : sans ça le « / 12 » se déplace
     latéralement à chaque changement de carte. */
  min-width: 2ch;
  text-align: right;
}
.presta-count__all {
  font-size: 0.8125rem;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}
@keyframes countIn {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
  to   { opacity: 1; transform: none; }
}
.presta-count__now.is-new { animation: countIn 300ms var(--ease-out); }

/* ⚠️ La marge latérale est portée par le DÉFILEUR, jamais par le rail.
   Le rail est en `width: max-content` : un padding en % y serait circulaire
   (sa largeur dépendrait de la piste de grille, qui dépend de sa largeur) et
   la marge gonflait jusqu'à 1369 px au lieu de 128 — constaté. Sur le
   défileur, le % se résout contre .presta-pin, qui a une largeur définie.
   Le débordement est découpé au bord du défileur et non à sa boîte de
   contenu : les cartes traversent bien la zone de marge. */
.presta-scroller {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
  /* Bas généreux : l'ombre portée d'une carte survolée descend de ~76 px */
  padding: 22px var(--rail-pad) 44px;
}
.presta-rail {
  display: flex;
  align-items: stretch;
  gap: 18px;
  width: max-content;
}

/* ---- Les deux flèches ---------------------------------------------------
   Le rail se parcourt au scroll ; les flèches sont une SECONDE entrée pour
   qui ne devine pas le geste, pas un remplacement (le scroll reste le
   mécanisme, cf. le module dans index.html).

   Elles vivent dans `.presta-view`, un simple repère de positionnement :
   dans le défileur elles seraient découpées avec le rail, et dans la grille
   de la scène épinglée elles créeraient une rangée. */
.presta-view { position: relative; }

.presta-arrow {
  --pa-box: 54px;
  /* Ce qui détache le rond du fond, c'est son OMBRE — pas de contour.
     Un anneau de papier a été essayé (réserve de 4px) : il détachait bien la
     flèche du bord d'une carte, mais il se lisait comme un contour dessiné et
     cassait la sobriété du rond. Retiré à la demande du client (06/08/2026).
     Deux couches : un contact court et
     serré qui pose le rond, puis une diffusion basse et très ouverte à
     étalement négatif — sans lui le flou déborde en halo autour du cercle.
     Discrète sur le papier, suffisante sur une carte forêt. */
  --pa-ombre: 0 1px 2px rgba(40, 40, 39, 0.16),
              0 8px 18px -8px rgba(40, 40, 39, 0.34);
  --pa-ombre-forte: 0 2px 4px rgba(40, 40, 39, 0.18),
                    0 14px 26px -10px rgba(40, 40, 39, 0.38);
  position: absolute;
  /* 🚨 Centré sur la BANDE DE CARTES, pas sur la boîte du défileur : celui-ci
     porte 22px de padding en haut et 44 en bas (l'ombre d'une carte survolée
     descend de ~76px). Un simple `top: 50%` posait donc les flèches 11px sous
     le milieu des cartes. Ces trois valeurs sont celles du `padding` de
     `.presta-scroller` — si l'une bouge, celle-ci suit. */
  top: calc(22px + (100% - 66px) / 2);
  /* Centrage vertical en `translate`, jamais en `transform` : `transform`
     appartient au survol et à l'appui, les deux doivent pouvoir jouer. */
  translate: 0 -50%;
  z-index: 3;
  width: var(--pa-box);
  height: var(--pa-box);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--bump-forest);
  box-shadow: var(--pa-ombre);
  cursor: pointer;
  transition: transform 240ms var(--ease-out),
              opacity 240ms var(--ease-out),
              box-shadow 240ms var(--ease-out);
}
/* Posées dans la marge du rail quand elle est assez large (128px à 1440 :
   la flèche y est centrée et ne recouvre aucune carte). Sur une fenêtre
   étroite la marge tombe à la gouttière et elles mordent légèrement sur le
   bord des cartes — c'est l'ombre, et elle seule, qui les en détache. */
.presta-arrow--prev { left: max(10px, calc((var(--rail-pad) - var(--pa-box)) / 2)); }
.presta-arrow--next { right: max(10px, calc((var(--rail-pad) - var(--pa-box)) / 2)); }

/* Même dessin que la flèche du curseur personnalisé : trait papyrus, bouts
   ronds, 2,4 d'épaisseur sur une boîte de 24. */
.presta-arrow svg {
  width: 38%;
  height: 38%;
  fill: none;
  stroke: var(--bump-sage);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Bout de course : la flèche s'éteint au lieu de disparaître — on garde le
   repère des deux sens. `disabled` est posé par le script. */
.presta-arrow:disabled {
  opacity: 0.26;
  cursor: default;
  /* Le contact court reste, la diffusion tombe : une flèche éteinte n'a plus
     de raison de flotter au-dessus du fond. */
  box-shadow: 0 1px 2px rgba(40, 40, 39, 0.16);
}

/* Le survol transforme : garde obligatoire, sinon un tap laisse la flèche
   collée dans son état survolé. */
@media (hover: hover) and (pointer: fine) {
  .presta-arrow:not(:disabled):hover {
    transform: scale(1.07);
    box-shadow: var(--pa-ombre-forte);
  }
}
.presta-arrow:not(:disabled):active { transform: scale(0.97); }

/* 🚨 LES FLÈCHES SONT RENDUES AU MOBILE (01/09/2026).
   Elles y étaient masquées, au motif que « le doigt fait déjà le geste, et
   deux ronds posés sur des cartes de 250 px les masqueraient à moitié ». Le
   premier argument est tombé (retour client : « ajouter de petites flèches
   pour montrer qu'on peut faire défiler vers la droite » — rien n'indiquait
   que la rangée continuait au-delà du bord). Le second ne tient plus non plus :
   le défileur a désormais une VRAIE marge latérale, celle qui permet à la
   dernière carte de se centrer (voir `--rail-bord` au palier ≤ 760 px), et la
   flèche se loge dedans sans mordre sur aucune carte au point de départ.
   ⚠️ Rien à ajouter côté script : `compteurDefileur()` appelle déjà `fleches()`
   sur le défileur natif, et le clic passe par `scrollBy` hors épinglage. Les
   flèches étaient fonctionnelles, seulement invisibles. */
@media (max-width: 760px) {
  .presta-arrow {
    /* Deux crans plus petites que sur desktop : elles indiquent un sens, elles
       ne sont pas le moyen principal de parcourir la rangée. */
    --pa-box: 36px;
  }
  .presta-arrow--prev { left: max(4px, calc((var(--rail-bord) - var(--pa-box)) / 2)); }
  .presta-arrow--next { right: max(4px, calc((var(--rail-bord) - var(--pa-box)) / 2)); }
  .presta-arrow svg { width: 42%; height: 42%; }
}

/* Repère de progression — n'a de sens qu'en mode épinglé */
.presta-prog { display: none; }

/* ---- La carte ---- */
.pcard {
  position: relative;
  flex: 0 0 auto;
  /* Hauteur bornée en svh : en mode épinglé la scène doit tenir dans un écran,
     titre + rail + repère de progression compris. Les cartes ont été
     agrandies le 29/07/2026 (elles laissaient trop de vide au-dessus et en
     dessous). Le plafond est contraint par la scène épinglée, qui doit tenir
     dans `100svh MOINS la hauteur du header` : bloc de titre ~150 + 22 +
     carte + 44 + repère 10. À 56svh/560px ça passe encore sur un portable de
     720 px de haut. Ne pas monter sans revérifier à cette hauteur-là. */
  width: clamp(250px, 23vw, 344px);
  height: min(56svh, 560px);
  min-height: 344px;
  /* Taille du picto. Le numéro et les marges de calage en découlent tous :
     un seul chiffre à bouger, la rangée du haut suit. */
  --pc-box: 60px;
  --pc-inset: 7px; /* respiration de la rangée du haut par rapport au bord */
  border-radius: var(--radius-card);
  /* Forêt et non papier : c'est la couleur de la carte une fois son visuel
     posé. Un fond clair par défaut ferait un éclair blanc le temps que
     l'image se charge, sur douze cartes à la suite. */
  background: var(--surface-forest);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  scroll-snap-align: center;
  isolation: isolate;
  transition: box-shadow 320ms var(--ease-soft);
}
.pcard.is-sage {
  background: var(--surface-sage);
  border-color: transparent;
  box-shadow: none;
}

/* ---- Le visuel au repos -------------------------------------------------
   La carte n'est jamais vide : elle porte sa 1re image en permanence, mais
   en MONOCHROME teinté forêt. Le survol ne fait donc pas apparaître une
   image, il lui rend ses couleurs — et c'est ce passage qui fait l'effet.
   Le fichier est le même que le 1er `.pcard__shot` : une seule requête.

   Le dégradé est plus dense en bas (0,94) qu'en haut (0,48) : le texte s'y
   pose, mais le haut de l'image reste lisible, sinon la carte redevient un
   aplat et on perd ce qu'on cherchait à montrer. */
.pcard__base {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}
.pcard__base img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(1) contrast(1.06) brightness(0.82);
  scale: 1.015;
  transition: filter 420ms var(--ease-soft), scale 900ms var(--ease-soft);
}
.pcard__base::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(22, 43, 28, 0.94) 0%,
    rgba(22, 43, 28, 0.78) 38%,
    rgba(22, 43, 28, 0.48) 100%
  );
}
/* Carte mise en avant : même dispositif, teinte plus claire et plus verte.
   C'est la ponctuation dans la rangée — le texte reste en papier, donc on ne
   touche à rien d'autre. */
.pcard.is-sage .pcard__base::after {
  background: linear-gradient(
    to top,
    rgba(22, 43, 28, 0.92) 0%,
    rgba(35, 74, 49, 0.66) 40%,
    rgba(90, 117, 99, 0.46) 100%
  );
}

/* ---- Repli : aucun visuel dans le CMS ----
   Le picto repris en grand tombe dans la bande vide entre la rangée du haut
   et le bloc de texte. Ce n'est plus le cas nominal, seulement le filet. */
.pcard__ghost {
  position: absolute;
  z-index: 0;
  left: 13%;
  top: 20%;
  width: 74%;
  height: auto;
  opacity: 0.07;
  pointer-events: none;
  /* `translate`/`rotate`/`scale` en propriétés séparées : `transform` reste
     libre pour la secousse de changement d'image, qui vit sur .pcard__media. */
  animation: ghostFloat 21s ease-in-out var(--gd, 0s) infinite alternate;
}
@keyframes ghostFloat {
  from { translate: -3% 1.5%;  rotate: -2.6deg; scale: 0.97; }
  to   { translate:  3% -2.5%; rotate:  2.6deg; scale: 1.05; }
}
/* Exemplaire clair, au-dessus du voile : uniquement sur les cartes sans
   visuel, sinon il serait de toute façon recouvert par l'image. */
.pcard__ghost--lit {
  z-index: 2;
  opacity: 0;
  filter: brightness(0) invert(1);
  transition: opacity 190ms var(--ease-out);
}
.pcard.is-hot .pcard__ghost--lit {
  opacity: 0.2;
  transition: opacity 300ms var(--ease-out) 120ms;
}

/* ---- Survol : rideau vert, puis image ----------------------------------
   Le voile part le premier, l'image le rattrape 95 ms plus tard et le
   recouvre : on voit le vert s'abattre, puis la photo se poser dessus. Le
   voile est donc SOUS l'image dans la pile — l'inverse laisserait le vert
   par-dessus la photo. À la sortie l'ordre s'inverse et tout va plus vite
   (165 ms contre 345).

   ⚠️ Les deux couches ne descendent PAS en volet droit. C'était un
   `inset(0 0 100% 0)`, remplacé le 29/07/2026 : le client voulait « pas
   vraiment tout parallèle », une forme ronde ou qui entre par le côté.
   Ce sont donc deux cercles qui s'ouvrent depuis des origines OPPOSÉES et
   hors de la carte — le vert arrive du coin haut-gauche, l'image du coin
   bas-droit. C'est ce croisement qui fait lire deux gestes distincts au lieu
   d'une couche qui double l'autre.

   Le rayon de 158 % est calculé, pas choisi : pour `circle(r% at …)` le rayon
   utilisé vaut r × √(l² + h²)/√2. Depuis un coin, atteindre le coin opposé
   demande ~144 % sur les trois tailles de carte du site (212×340, 302×470,
   372×540). 158 % laisse la marge. Ne pas descendre sous 150 %. */
.pcard__veil,
.pcard__media {
  position: absolute;
  inset: 0;
}
.pcard__veil {
  z-index: 1;
  background: var(--surface-forest);
  clip-path: circle(0% at 14% -6%);
  transition: clip-path 190ms var(--ease-wipe) 60ms;
}
.pcard__media {
  z-index: 2;
  clip-path: circle(0% at 92% 106%);
  transition: clip-path 165ms var(--ease-wipe);
}
.pcard.is-hot .pcard__veil {
  clip-path: circle(158% at 14% -6%);
  transition: clip-path 230ms var(--ease-wipe);
}
.pcard.is-hot .pcard__media {
  clip-path: circle(158% at 92% 106%);
  transition: clip-path 250ms var(--ease-wipe) 95ms;
}

/* ---- Passage d'un visuel au suivant -------------------------------------
   🚨 Ce n'est PAS un fondu croisé : la nouvelle image arrive PAR-DESSUS
   l'ancienne, qui reste opaque en dessous jusqu'à ce qu'elle soit entièrement
   recouverte. Un vrai fondu croisé fait forcément un creux — au milieu, deux
   images à 50 % laissent passer 25 % du fond — et sur deux photos proches ça
   se lit comme un clignotement.

   Mécanique : l'entrante passe au-dessus (`z-index`) et monte de 0 à 1 en
   240 ms ; la sortante ne s'estompe pas, elle bascule d'un coup à 0 APRÈS ces
   240 ms (`transition: opacity 0s linear 240ms`), donc à un instant où elle
   est déjà totalement cachée. Aucun creux, aucune surimpression. */
.pcard__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  z-index: 0;
  transition: opacity 0s linear 240ms, scale 400ms var(--ease-out);
}
.pcard__shot.is-on {
  opacity: 1;
  z-index: 1;
  /* ⚠️ Doit rester ÉGAL à la durée de `pswap` (le cadrage), sinon le cadre
     continue de bouger après la fin de l'image — c'est cette désynchronisation
     qui se lisait comme un saccadement, pas la vitesse. */
  transition: opacity 240ms var(--ease-out), scale 400ms var(--ease-out);
}

/* Le visuel respire lentement pendant le survol : c'est ce qui distingue une
   carte regardée d'une carte figée. Sur `scale` et non `transform` —
   `transform` appartient au cadrage de `pswap`, sur .pcard__media.

   🚨 L'animation porte sur TOUS les visuels de la carte, pas seulement sur
   `.is-on`. Ciblée sur le seul visuel actif, elle disparaissait de l'ancienne
   image au moment du changement : celle-ci revenait donc brutalement de son
   échelle courante à 1 pendant qu'elle était encore visible. C'était la vraie
   secousse parasite. En les animant toutes ensemble, l'entrante et la
   sortante sont exactement à la même échelle au moment de la bascule. */
@keyframes pcardZoom {
  from { scale: 1; }
  to   { scale: 1.07; }
}
.pcard.is-hot .pcard__shot { animation: pcardZoom 7s var(--ease-soft) forwards; }
/* Voile de lisibilité. Enfant de .pcard__media : il est découpé par le même
   clip-path, donc il descend avec l'image au lieu d'apparaître.
   Deux dégradés : le bas porte le titre et la description, le haut (plus
   léger) retient le picto et le numéro, qui sinon disparaissaient sur une
   image claire — constaté sur les visuels d'intérieur. */
.pcard__media::after {
  content: "";
  position: absolute;
  inset: 0;
  /* 🚨 `z-index: 2` — SANS LUI CE VOILE NE SE VOIT PAS DU TOUT.
     Découvert le 01/09/2026 en mesurant le contraste réel derrière le texte :
     rgb(211,201,188) au pied de la carte, soit 1,48:1 avec le papier, là où le
     dégradé aurait dû donner un vert quasi opaque. Le voile était bien calculé
     par le moteur (il apparaissait dans le style calculé du pseudo-élément),
     il était simplement PEINT DESSOUS.
     Pourquoi : `.pcard__media` porte un `clip-path`, donc c'est un CONTEXTE
     D'EMPILEMENT, et à l'intérieur `.pcard__shot.is-on` monte à `z-index: 1`
     (le passage d'un visuel au suivant fait arriver l'entrante par-dessus la
     sortante). Un `::after` sans `z-index` vaut `auto`, donc 0 : la photo
     passait au-dessus du voile de lisibilité.
     ⚠️ Le défaut ne datait pas du mobile : il valait AUSSI pour le survol
     desktop, où le voile n'a donc jamais rien voilé. C'est la cause commune du
     « texte blanc peu lisible sur les images claires », que le corps plus
     grand du desktop rendait seulement plus supportable.
     ⚠️ 2 et pas plus : on doit rester au-dessus des visuels (0 et 1) et sous
     `.pcard__in` (z-index 3), qui vit dans le contexte du PARENT. */
  z-index: 2;
  /* Nettement plus léger que le voile de repos : c'est tout l'intérêt du
     survol, l'image doit gagner en couleur ET en clarté. Assez dense en bas
     pour que le texte tienne, presque rien au-dessus de la moitié. */
  background:
    linear-gradient(
      to top,
      rgba(22, 43, 28, 0.9) 0%,
      rgba(22, 43, 28, 0.5) 40%,
      rgba(22, 43, 28, 0.02) 76%
    ),
    linear-gradient(
      to bottom,
      rgba(22, 43, 28, 0.34) 0%,
      rgba(22, 43, 28, 0) 22%
    );
}

/* Passage d'une image à la suivante.
   ⚠️ Remplace une version à 3 arrêts (0 % → 42 % → 100 %) qui faisait repartir
   le cadre en arrière à mi-course : une vraie inversion de sens, dans une
   fenêtre plus longue (460 ms) que le fondu des images (300 ms) — le fondu
   finissait, puis le cadre continuait seul seul à corriger sa trajectoire.
   C'est cette combinaison, pas la vitesse, qui se lisait comme un
   saccadement (retour client du 29/07/2026).
   Ici : deux arrêts seulement, un seul sens de course, et la MÊME durée que
   le fondu ci-dessus — les deux se posent exactement ensemble.
   Le décalage reste en POURCENTAGE, pas en pixels : il doit tenir sous le
   débord du scale, qui vaut (s − 1) / 2 de la LARGEUR de la carte et rétrécit
   donc sur mobile. En px, le bord blanc de la carte réapparaissait. */
@keyframes pswap {
  from { transform: translate3d(1.4%, 0, 0) scale(1.045); }
  to   { transform: translate3d(0, 0, 0) scale(1); }
}
.pcard__media.is-swap { animation: pswap 240ms var(--ease-out) both; }

/* ---- Contenu de la carte ---- */
.pcard__in {
  position: relative;
  z-index: 3;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: var(--space-5);
}
/* ---- La rangée du haut : picto + numéro -------------------------------
   🚨 Tout est calé sur l'ENCRE, jamais sur les boîtes.

   Le dessin d'un picto de la charte n'occupe que ~48 % de sa boîte carrée
   (711 × 711, encre centrée) : il traîne donc ~26 % de vide de chaque côté.
   Conséquence mesurée avant correction, avec une boîte de 60 px : l'encre
   tombait 17 px sous le bord de la zone de texte et 17 px à droite du titre,
   alors que les capitales du numéro n'étaient qu'à 8 px — 9 px d'écart entre
   les deux, et un picto visiblement rentré. Et le défaut GRANDIT avec la
   taille : c'est pour ça qu'agrandir le picto l'avait fait « bouger ».

   Les marges négatives annulent ce vide. L'encre se pose donc exactement sur
   le bord de la zone de texte, quelle que soit la taille du picto : on peut
   changer `--pc-box` sans que le dessin se déplace d'un pixel.

   Une seule variable commande la rangée : le numéro suit la taille du picto
   (0,25 × la boîte), donc les deux grandissent toujours ensemble. */
.pcard__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  /* Léger retrait supplémentaire de la rangée, appliqué APRÈS le calage sur
     l'encre : le picto et le numéro respirent un peu plus par rapport au bord
     de la carte, sans se désaligner l'un de l'autre (le padding pousse le
     picto vers la droite et le numéro vers la gauche d'autant). */
  padding: var(--pc-inset) var(--pc-inset) 0;
}
.pcard__icon {
  display: block;
  width: var(--pc-box);
  height: var(--pc-box);
  margin-top: calc(var(--pc-box) * -0.26);
  margin-left: calc(var(--pc-box) * -0.26);
}
.pcard__icon img {
  display: block;
  width: 100%;
  height: 100%;
  transition: filter 240ms var(--ease-out), opacity 240ms var(--ease-out);
}
.pcard__num {
  font-size: calc(var(--pc-box) * 0.25);
  font-weight: var(--weight-medium);
  /* `--tracking-wider` (0,16em) était calibré pour un numéro de 12px ; à 15px
     et en graisse medium, l'écart entre les deux chiffres devenait trop
     large. Le token en dessous (0,08em) tient le caractère de repère sans
     disloquer le nombre. */
  letter-spacing: var(--tracking-wide);
  color: rgba(243, 244, 239, 0.6);
  font-variant-numeric: tabular-nums;
  /* Même principe que le picto : on remonte le sommet des CAPITALES sur le
     bord de la zone de texte. `line-height: 1` retire l'interligne, la marge
     retire ce qui reste entre le haut de la boîte de ligne et la capitale
     (valeur relevée dans le navigateur avec Neue Haas Display). */
  line-height: 1;
  margin-top: -0.13em;
}
.pcard__body { margin-top: auto; }
/* 🚨 OMBRE PORTÉE SUR LE TEXTE DES CARTES (01/09/2026).
   Le texte est en papier EN PERMANENCE, mais l'image sous lui ne l'est pas :
   au survol (et EN PERMANENCE au doigt, où il n'y a pas de survol — voir le
   bloc `(hover: none)` plus bas) la carte montre le visuel en COULEUR, et sur
   un visuel clair (intérieurs, ciels, façades blanches) le papier sur papier
   ne se détache plus. Le défaut passait sur desktop, où le corps est plus
   grand, et devenait illisible sur mobile où la description tombe à 15 px
   (retour client).
   L'ombre est douce et très étalée : elle épaissit le contre-forme sans
   dessiner de liseré, donc elle ne se voit pas sur les visuels sombres où
   elle ne sert à rien. Elle vient EN PLUS du dégradé renforcé, pas à sa
   place — un dégradé seul devrait être si dense qu'il rendrait l'image grise,
   ce qui est exactement ce que le survol doit révéler. */
.pcard__title {
  font-size: 1.0625rem;
  letter-spacing: 0.06em;
  margin: 0 0 0.5em;
  color: var(--bump-paper);
  text-shadow: 0 1px 2px rgba(11, 22, 14, 0.55), 0 2px 14px rgba(11, 22, 14, 0.4);
}
.pcard__desc {
  font-weight: 400;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: rgba(243, 244, 239, 0.8);
  margin: 0;
  text-shadow: 0 1px 2px rgba(11, 22, 14, 0.5), 0 2px 12px rgba(11, 22, 14, 0.36);
}

/* Le picto de la charte est en #282827 : `brightness(0) invert(1)` le rend
   blanc pur, et l'opacité le ramène à la valeur du papier une fois composé
   sur le fond forêt.

   ⚠️ Le texte est en papier EN PERMANENCE depuis que la carte porte un visuel
   au repos. Il n'y a donc plus de bascule clair → sombre au survol, et plus
   de délais à caler sur la course du rideau : le numéro qui devenait blanc
   sur blanc quand la bascule précédait le vert est un problème supprimé, pas
   contourné. Ne pas réintroduire de transition de couleur ici. */
.pcard__icon img { filter: brightness(0) invert(1); opacity: 0.92; }
.pcard.is-hot { box-shadow: var(--shadow-lg); }

/* ---- Repli : carte sans aucun visuel ----
   Elle garde son fond clair, donc elle a besoin, elle, de la bascule de
   couleurs — et donc des délais calés sur le rideau. */
.pcard.is-bare { background: var(--surface-card); }
.pcard.is-bare .pcard__num,
.pcard.is-bare .pcard__title,
.pcard.is-bare .pcard__desc { transition: color 240ms var(--ease-out); }
.pcard.is-bare .pcard__icon img {
  filter: none;
  opacity: 1;
  transition: filter 240ms var(--ease-out), opacity 240ms var(--ease-out);
}
.pcard.is-bare .pcard__num { color: var(--text-muted); }
.pcard.is-bare .pcard__title { color: var(--text-body); }
.pcard.is-bare .pcard__desc { color: var(--text-muted); }
.pcard.is-bare.is-hot .pcard__num,
.pcard.is-bare.is-hot .pcard__title { color: var(--bump-paper); transition-delay: 120ms; }
.pcard.is-bare.is-hot .pcard__desc { color: rgba(243, 244, 239, 0.82); transition-delay: 150ms; }
.pcard.is-bare.is-hot .pcard__icon img {
  filter: brightness(0) invert(1);
  opacity: 0.92;
  transition-delay: 95ms;
}

/* Grands écrans : même respiration que le pont (min(1560px, 92vw)). Sans ça,
   à 2560 px les gouttières font 688 px de chaque côté et le rail paraît
   perdu au milieu de l'écran. Les cartes s'élargissent d'autant. */
@media (min-width: 1600px) {
  .presta {
    --rail-pad: max(var(--gutter), calc((100% - min(1560px, 92vw)) / 2 + var(--gutter)));
  }
  .pcard {
    width: clamp(320px, 17vw, 420px);
    height: min(60svh, 640px);
    --pc-box: 70px;
  }
  .presta-rail { gap: 22px; }
}

/* Sans pointeur fin, il n'y a pas de survol : la couleur est donnée d'emblée.
   C'est de toute façon la meilleure version de la carte sur un téléphone. */
@media (hover: none), (pointer: coarse) {
  .pcard:not(.is-bare) .pcard__media { clip-path: none; }

  /* 🚨 LE VOILE EST NETTEMENT PLUS DENSE AU DOIGT, et c'est la contrepartie
     directe de la règle ci-dessus (01/09/2026).
     Sur desktop ce dégradé est celui du SURVOL : il est volontairement léger,
     parce qu'il ne dure qu'un instant et que tout l'intérêt du geste est de
     rendre l'image à ses couleurs. Au doigt il n'y a pas de survol : ce même
     dégradé léger devient l'état PERMANENT de la carte, sous un texte deux
     crans plus petit que sur desktop. Sur les visuels clairs le papier ne se
     détachait plus (retour client).
     On garde donc la couleur — c'est le parti mobile — mais le vert remonte
     plus haut et plus fort SOUS LE TEXTE : opaque au pied, encore à 0,72 à
     mi-carte, et toujours presque rien au-dessus des deux tiers, là où il n'y
     a rien à lire. L'image reste lisible comme image dans sa moitié haute.
     ⚠️ C'est un dégradé VERS LE BAS de plus en plus dense, jamais un voile
     uniforme : un aplat sur toute la carte annulerait la couleur. */
  .pcard:not(.is-bare) .pcard__media::after {
    background:
      linear-gradient(
        to top,
        rgba(22, 43, 28, 0.97) 0%,
        rgba(22, 43, 28, 0.88) 22%,
        rgba(22, 43, 28, 0.72) 42%,
        rgba(22, 43, 28, 0.3) 64%,
        rgba(22, 43, 28, 0.04) 82%
      ),
      linear-gradient(
        to bottom,
        rgba(22, 43, 28, 0.42) 0%,
        rgba(22, 43, 28, 0) 24%
      );
  }
}

/* ---- Mode épinglé -------------------------------------------------------
   ⚠️ Le rail était déplacé par une animation liée au scroll (`view-timeline`).
   Elle a été retirée le 29/07/2026 : une timeline de scroll est par
   construction un mapping 1:1, elle ne peut pas produire d'inertie, et le
   passage du défilement vertical à l'horizontal était donc sec (retour
   client). Le rail est désormais interpolé image par image dans index.html —
   voir le commentaire du module pour le détail des points morts et du lerp.
   Ne pas « remettre » de scroll-timeline ici : on perdrait le retard.

   Ce qui reste en CSS, c'est la MISE EN PAGE de l'épinglage. La hauteur du
   pin vient de --presta-run (course du rail + les deux points morts), posée
   par le script : hauteur et course sortent du même calcul, elles ne peuvent
   pas se désynchroniser.

   🚨 Ce bloc était enfermé dans un `@supports (animation-timeline: view())`.
   Il a été retiré avec la timeline : plus rien ici n'en dépend, et le garder
   aurait été un piège — c'est le SCRIPT qui pose `is-pinned`, or il ne teste
   plus le support des scroll-timelines. Sur un navigateur sans cette
   fonctionnalité, la classe serait posée mais la mise en page épinglée
   ignorée : le rail se serait déplacé sans que la scène s'accroche. */
@media (prefers-reduced-motion: no-preference) {
  /* 🚨 Point d'atterrissage de l'ancre `#prestations`.
     La section démarre par `padding-top: var(--space-10)` — 128px qui ne sont,
     en mode épinglé, que du scroll AVANT l'accrochage. Un clic sur
     « Prestations » posait donc le visiteur 128px trop haut : la scène n'était
     pas encore épinglée, pas encore à sa taille, et le rail pas à son point de
     départ. La marge négative décale la cible de l'ancre du haut de la section
     vers le haut du PIN, c'est-à-dire pile sur le pixel où la scène s'accroche.
     ⚠️ La valeur doit rester l'opposé exact du `padding-top` de `.presta`.
     ⚠️ Uniquement en mode épinglé : hors épinglage ce padding est un vrai
     espacement, et l'ancre doit se poser avant lui. */
  .presta.is-pinned { scroll-margin-top: calc(-1 * var(--space-10)); }

  .presta.is-pinned .presta-pin {
    height: calc(100svh + var(--presta-run, 0px));
  }
  .presta.is-pinned .presta-stage {
    position: sticky;
    top: 0;
    height: 100svh;
    /* Le header est fixe et se peint par-dessus la scène : sans cette
       réserve, le titre et le compteur passent dessous (constaté). Le
       contenu est donc centré dans la hauteur RESTANTE, pas dans l'écran. */
    padding-top: var(--header-h);
    display: grid;
    /* ⚠️ `minmax(0, 1fr)` est indispensable. Une colonne `auto` se
       dimensionne sur le max-content de son contenu : le rail faisant
       3827 px, le défileur héritait de cette largeur et le `%` de sa marge
       se remettait à diverger (128 px attendus, 1202 px obtenus). */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    align-content: center;
  }
  /* Plus de défilement natif : le rail est déplacé par le script.
     `clip` et pas `hidden` — `hidden` ferait du défileur un conteneur de
     défilement et les cartes seraient de nouveau attrapables à la molette
     en même temps que la page. `clip visible` découpe latéralement sans
     rogner l'ombre portée des cartes en haut et en bas. */
  .presta.is-pinned .presta-scroller {
    overflow: clip visible;
    scroll-snap-type: none;
  }
  .presta.is-pinned .presta-rail { will-change: transform; }
  .presta.is-pinned .presta-prog {
    display: block;
    margin: var(--space-2) var(--rail-pad) 0;
    height: 2px;
    border-radius: 2px;
    background: var(--border-subtle);
    overflow: hidden;
  }
  /* La barre suit la valeur LISSÉE du rail (posée par le script), pas la
     position de scroll brute : sinon elle arriverait avant les cartes et
     trahirait le retard qui fait tout l'effet. */
  .presta.is-pinned .presta-prog__bar {
    display: block;
    height: 100%;
    background: var(--bump-forest);
    transform: scaleX(0.015);
    transform-origin: left center;
  }
}

/* ============================================================
   Résultats — bloc GRAPHITE : le palier neutre du site

   Cette section arrive juste après le rail des prestations, massivement vert
   forêt, et juste avant « À propos », qui repart sur du papier. Elle était
   elle-même en vert forêt : trois blocs verts d'affilée, la page n'avait plus
   de respiration.

   🚨 C'est un BLOC PLEIN, pas un dégradé. Une première version fondait le
   papier vers le gris et revenait au papier : les raccords étaient invisibles,
   donc la section ne se détachait pas du tout (refusé par le client — « ça
   sépare pas »). Ne pas y revenir : ce qui sépare, c'est la rupture franche
   entre le papier et le graphite, doublée d'un filet charbon de 1 px en haut
   et en bas qui rend l'arête nette.

   Palette, entièrement charte :
     · fond          Graphite  #6D6D6C  (--surface-graphite)
     · texte         Papyrus   #D0D3C1  (--bump-sage)
     · cellules      trois crans plus SOMBRES que le fond — c'est ce qui rend
                     le Papyrus lisible dessus (4,7 à 5,4:1 ; sur le graphite
                     nu il ne fait que 3,8:1, suffisant pour du grand texte
                     seulement). Ne pas éclaircir les cellules sans revérifier.
     · appuis        Charbon (ombres, gravures) et Sage profond (suffixes)
   ============================================================ */
.results {
  /* 🚨 DEUX COULEURS, et rien d'autre : Graphite (fond) et Papyrus (tout le
     reste). Demande client du 03/08/2026. Les cellules ont donc EXACTEMENT le
     fond de la section — le bento n'est plus dessiné par des aplats mais par
     ses seuls filets. Les tons intermédiaires (#4f4f4e et compagnie), les
     ombres au charbon et le papier du chapô ont tous sauté avec.

     ⚠️ Le prix à payer est connu et mesuré : Papyrus sur Graphite plafonne à
     **3,79:1**, quelle que soit la combinaison — c'est la distance entre les
     deux couleurs, on ne peut pas la contourner en restant à deux. Ça passe
     le seuil « grand texte » (3:1) pour les chiffres et le titre, pas le
     seuil « texte courant » (4,5:1) pour les descriptions et les sources.
     Le seul remède serait de redonner aux cellules un ton plus sombre que le
     fond (5,38:1) — c'est-à-dire une troisième couleur. */
  --gr-line: rgba(208, 211, 193, 0.22); /* Papyrus dilué : filets, bordures */

  position: relative;
  background: var(--surface-graphite);
  /* Pas de filet de séparation : la rupture papier → graphite est déjà une
     arête franche. Le liseré charbon qui la doublait était une 3e couleur. */
  color: var(--bump-sage);
}
/* ⚠️ Pas de `max-width` ici : le bloc de titre est en deux colonnes (titre à
   gauche, chapô à droite), c'est la colonne qui tient la mesure de lecture.
   Un `64ch` ramènerait les deux moitiés au milieu de la section. */
.results .section-head { max-width: none; }
.results .section-head h2 { color: var(--bump-sage); }
.results .section-head .intro { color: var(--bump-sage); }

/* Bento asymétrique : une grande cellule 2×2, deux carrées, une large 2×1,
   et la mention en pied sur toute la largeur. L'emprise vient de la classe
   posée par le gabarit (`taille` dans le CMS), pas de l'ordre du DOM. */
.results-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: minmax(196px, auto);
  gap: 14px;
}
.stat--lg   { grid-column: span 2; grid-row: span 2; }
.stat--wide { grid-column: span 2; }
.stat--md   { grid-column: span 1; }

.stat {
  /* Un seul chiffre commande le calage de la rangée du haut : la taille de la
     boîte du picto. Les marges négatives de `.stat__top` en découlent. */
  --sf-box: 46px;
  --sf-pad: clamp(22px, 2.4vw, 34px);

  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--sf-pad);
  /* 2 px et non 1 : le bento n'étant plus dessiné que par ses filets, ils
     doivent porter la composition à eux seuls. Même épaisseur pour le filet
     du pied, la mention et les traits du fond. */
  border: 2px solid var(--gr-line);
  border-radius: var(--radius-card);
  /* Le fond de la cellule EST celui de la section — mais opaque, pas
     `transparent` : c'est ce qui masque les traits du fond derrière le texte.
     Le bento se lit donc comme des fenêtres découpées dans le champ de
     lignes, cernées d'un filet Papyrus. */
  background: var(--surface-graphite);
  overflow: hidden;
  transition: border-color 520ms var(--ease-soft);
}
/* Le contenu repasse au-dessus de la lueur de survol (`.stat::before`), qui
   est un pseudo positionné et se peindrait sinon PAR-DESSUS le texte. */
.stat > * { position: relative; z-index: 1; }

/* Lueur de survol — le « fondu » demandé. Un halo de Papyrus très dilué,
   centré sur le curseur : il n'introduit aucune couleur, c'est la même teinte
   que le texte, simplement à 10 %.
   Rayons en POURCENTAGE : le halo s'adapte tout seul aux trois formats de
   cellule, d'une carrée de 286 px à la grande de 585. */
.stat::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  /* 🚨 `circle` avec un rayon EN PIXELS, jamais des rayons en pourcentage.
     En % le halo prend le rapport de la cellule : rond sur une carrée, ovale
     écrasé sur la cellule large (585 × 249). Une lueur doit garder la même
     forme partout — c'est la cellule qui la recadre, pas elle qui se déforme.
     240 px : assez large pour éclairer une carrée, assez court pour qu'on
     voie encore la chute sur la grande. */
  background: radial-gradient(circle 240px at var(--mx, 50%) var(--my, 50%),
    rgba(208, 211, 193, 0.14) 0%,
    rgba(208, 211, 193, 0.05) 48%,
    rgba(208, 211, 193, 0) 78%);
  /* Sortie plus rapide que l'entrée (règle transverse du site) : la durée
     courte est ici, la longue est dans la règle `:hover`. */
  transition: opacity 220ms var(--ease-out);
}
/* Seule différence entre les formats : la taille du picto de la grande
   cellule. Fond et filet sont identiques partout. */
.stat--lg { --sf-box: 82px; }

@media (hover: hover) and (pointer: fine) {
  /* 🚨 Survol SANS déplacement ni ombre — le premier soulevait la carte de
     4 px en 380 ms, c'est « l'hover sec » du retour client. Deux choses
     seulement, et toutes deux en fondu :
       · le filet se révèle (0,22 → 0,5) sur 520 ms ;
       · la lueur monte sous le curseur sur 460 ms et le suit.
     La seconde est ce qui empêche le survol d'être « basique » : elle n'est
     jamais deux fois au même endroit. */
  .stat:hover { border-color: rgba(208, 211, 193, 0.5); }
  .stat:hover::before { opacity: 1; transition: opacity 460ms var(--ease-out); }
  /* Le filet du pied s'éclaircit avec le cadre : la cellule répond d'un seul
     bloc au lieu de ne réagir que par son bord. */
  .stat:hover .stat__foot::before { background: rgba(208, 211, 193, 0.42); }
}

.stat__top {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* ⚠️ Calage sur l'ENCRE, pas sur la boîte. Le dessin d'un picto de la charte
     n'occupe que ~48 % de son carré : sans ces deux marges négatives, il
     démarre 0,26 × la boîte plus bas et plus à droite que le texte de la
     cellule, et la rangée paraît rentrée. Même correction que `.pcard__top`
     et `.bridge__sectors` — si l'une des trois évolue, penser aux autres. */
  margin-top: calc(var(--sf-box) * -0.26);
}
.stat__icon {
  position: relative;
  width: var(--sf-box);
  height: var(--sf-box);
  flex: none;
  margin-left: calc(var(--sf-box) * -0.26);
}
.stat__icon img { display: block; width: 100%; height: 100%; }
.stat__service {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--bump-sage);
}

/* Le chiffre est poussé en bas : toutes les cellules d'une rangée alignent
   donc leur valeur, quelle que soit la longueur du libellé du haut. */
.stat__body { margin-top: auto; }
.stat__figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  font-size: clamp(40px, min(3.6vw, calc(22.4px + 0.875vw)), 56px);
  font-weight: var(--weight-light);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--bump-sage);
}
.stat--lg .stat__figure   { font-size: clamp(56px, min(6.2vw, calc(36.8px + 1.4375vw)), 92px); }
.stat--wide .stat__figure { font-size: clamp(26px, min(3.1vw, calc(18.4px + 0.71875vw)), 46px); }
/* Chiffres à chasse fixe : le compteur monte sans faire danser ce qui suit. */
.stat__n { font-variant-numeric: tabular-nums; }
/* Séparateur et suffixe : même Papyrus que le chiffre, c'est la TAILLE seule
   qui les fait reculer (0,58 em). Ils étaient en sage profond — une troisième
   couleur, retirée avec les autres. */
/* Le séparateur de fourchette est le MOT « à », plus le tiret demi-cadratin
   (07/08/2026, passe « plus aucun tiret dans les textes »). Comme c'est un mot
   et non un signe, il n'est plus `aria-hidden` : « 3’400 à 11’200 CHF » se lit
   désormais correctement à voix haute. Il est ramené à la moitié du corps du
   chiffre, sinon il pèse autant que les deux nombres qu'il relie. */
.stat__sep {
  margin-inline: 0.26em;
  font-size: 0.5em;
  font-weight: var(--weight-light);
}
/* `pre` : le suffixe stocké peut commencer par une espace (« CHF ») et une
   espace de tête serait sinon avalée par le navigateur. */
.stat__suf {
  white-space: pre;
  font-size: 0.58em;
}
.stat__desc {
  margin-top: var(--space-4);
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: var(--leading-snug);
  color: var(--bump-sage);
  max-width: 34ch;
}
.stat--lg .stat__desc { font-size: 1.1875rem; max-width: 26ch; }

/* Source de l'étude, en pied de cellule. Le filet se trace à l'entrée dans le
   viewport (d'où le ::before plutôt qu'un `border-top`, qui ne s'anime pas). */
.stat__foot {
  position: relative;
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}
.stat__foot::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--gr-line);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 760ms var(--ease-soft) 220ms,
              background-color 520ms var(--ease-soft);
}
.stat.is-in .stat__foot::before { transform: scaleX(1); }

.stat__source {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--bump-sage);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 320ms var(--ease-out);
}
/* Le lien ne change pas de couleur au survol — il n'y en a pas d'autre à
   prendre. C'est son soulignement qui apparaît, en douceur. */
.stat__source:hover,
.stat__source:focus-visible { border-bottom-color: currentColor; }
.stat__arrow { width: 11px; height: 11px; flex: none; }

/* Mention de portée — cellule pleine largeur, en pied de bento. Même
   traitement que les autres : fond de la section, cerné d'un filet.
   Pas d'italique (règle de charte) : c'est le centrage et le corps réduit qui
   la mettent en retrait. */
.results__note {
  /* Hors grille (voir index.html) : la marge reprend l'écart du bento. */
  margin: 14px 0 0;
  padding: clamp(18px, 2vw, 26px) clamp(22px, 2.4vw, 34px);
  border: 2px solid var(--gr-line);
  border-radius: var(--radius-card);
  background: var(--surface-graphite);
  text-align: center;
  font-size: 0.875rem;
  font-weight: 400;
  line-height: var(--leading-normal);
  color: var(--bump-sage);
}

/* ---- Lignes de construction du FOND ---------------------------------------
   Traits parallèles fins qui traversent toute la section, derrière le bento.

   🚨 Historique, pour ne pas refaire le tour : la section a d'abord eu des
   décors ANIMÉS dans chaque cellule (ondes, orbite, flottement, balayage),
   puis des épures TEXTURÉES au fusain, toujours dans les cellules. Les deux
   ont été refusées — « trop d'éléments perturbateurs », puis « je veux que ce
   qu'il y a dans le bento soit minimaliste, et on oublie les vecteurs
   texturés ». **Les cellules ne portent plus aucun décor.** Le dessin est
   passé dans le fond gris, où il ne peut plus gêner la lecture.

   Traits fins et nus — même vocabulaire que l'épure du hero, aucun filtre,
   aucune texture. ⚠️ Ils font 2 px comme TOUS les filets de cette section
   (cadres des cellules, filet du pied, bordure de la mention) : le bento
   n'étant plus porté que par ses traits, ils tiennent la composition à eux
   seuls. Si l'un change, changer les autres — c'est une seule épaisseur, pas
   quatre réglages indépendants.

   Ils se tracent du BAS-GAUCHE vers le HAUT-DROITE à l'entrée dans le
   viewport, avec un léger décalage de l'un à l'autre. Mécanique : chaque
   trait est une barre inclinée, `transform-origin` sur son pied, et c'est
   `scale: 1 0 → 1 1` qui la fait pousser. Le pied étant à gauche, la ligne
   se déploie vers la droite. */
.results__rules {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
/* 🚨 Le placement dépend de la DÉRIVE, pas seulement de l'espacement voulu.
   Un trait incliné se décale horizontalement de `hauteur × tan(angle)` entre
   son pied et sa tête. En 1440 × 1040 ça fait ~77 % de la largeur : les
   traits qu'on voit en haut de la section sont donc ceux dont le pied est
   HORS CADRE à gauche. Sans eux le haut restait vide (constaté).

   Trois réglages liés — `--rl-depart`, `--rl-pas`, `rotate`, `height` : si
   l'un change, revérifier qu'il reste des traits en haut ET en bas. La
   section étant bien plus haute que large sur mobile, la dérive y explose
   (~215 % de la largeur) : le palier ≤ 760 px a donc ses propres valeurs. */
.results__rules {
  --rl-depart: -86%;
  --rl-pas: 26%; /* ≈ 375 px en 1440 : le fond respire, il n'est pas hachuré */
  --rl-angle: 32deg;
  --rl-haut: 170%;
}
.results__rules i {
  position: absolute;
  bottom: 0;
  left: calc(var(--rl-depart) + var(--i) * var(--rl-pas));
  width: 2px;
  height: var(--rl-haut);
  background: var(--bump-sage);
  opacity: 0.28;
  transform-origin: 50% 100%;
  rotate: var(--rl-angle);
}
/* La composition passe AU-DESSUS des lignes : elles courent derrière le bento
   comme des lignes de construction, elles ne le traversent jamais. */
.results > .section { position: relative; z-index: 1; }

@media (prefers-reduced-motion: no-preference) {
  /* `.results__rules` porte la classe `.reveal` uniquement pour récupérer le
     `.is-in` de l'observateur global — son effet visuel par défaut (opacité +
     décalage) est neutralisé ici, le tracé le remplace. */
  .results__rules.reveal { opacity: 1; transform: none; transition: none; }
  .results__rules i {
    scale: 1 0;
    transition: scale 1.15s var(--ease-soft);
    /* Le « mini décalage » demandé : les traits partent de la gauche, l'un
       après l'autre. */
    transition-delay: calc(var(--i) * 120ms);
  }
  .results__rules.is-in i { scale: 1 1; }

  /* Seul mouvement conservé, et il ne joue qu'une fois : le picto se découvre
     de gauche à droite quand la cellule entre. `clip-path` plutôt qu'un
     `scale` — le dessin ne se déforme pas. */
  .stat .stat__icon img {
    clip-path: inset(0 100% 0 0);
    opacity: 0;
    transition: clip-path 620ms var(--ease-soft) 140ms, opacity 280ms var(--ease-out) 140ms;
  }
  .stat.is-in .stat__icon img { clip-path: inset(0 0 0 0); opacity: 1; }
}

/* ============================================================
   À propos
   ============================================================ */
/* Le bloc graphite des Résultats s'arrête net juste au-dessus : la section
   prend un peu plus d'air en haut pour que la rupture respire. Le supplément
   s'ajoute à `--section-py`, il suit donc le palier mobile. */
#a-propos { padding-top: calc(var(--section-py) + var(--space-7)); }

.about-grid {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: var(--space-9);
  align-items: start;
}
.about-text .eyebrow { margin-bottom: var(--space-5); }
/* Le `<h2>` a quitté la colonne de gauche pour un vrai `.section-head`
   (10/08/2026) : le cadre `.trame-cadre` doit commencer sous le titre, et un
   titre imbriqué dans le contenu ne peut pas en être exclu.
   ⚠️ Seul le `max-width` survit du réglage d'origine : c'est lui qui casse le
   titre sur deux lignes (« Nés à Fribourg, / spécialisés par choix. »). La
   taille, elle, était déjà identique à celle de `.section-head h2`. */
#a-propos .section-head h2 { max-width: 18ch; }
.about-text p {
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: var(--leading-relaxed);
  color: var(--gray-700);
  margin-bottom: var(--space-4);
  max-width: 60ch;
}
.about-text p:last-child { margin-bottom: 0; }
.about-text p strong { font-weight: var(--weight-medium); color: var(--bump-charcoal); }

/* ---- Frise verticale "thermomètre" ----

   ⚠️ Le trait et les pastilles sont posés par des règles différentes : ils
   doivent partager le MÊME axe, sinon la ligne ne passe pas au centre des
   cercles. C'était le cas — trait à `left: 6px` (donc centre à 7px, il fait
   2px de large) contre pastilles centrées sur 6px : 1px de décalage, visible
   sur cinq pastilles d'affilée.

   Tout part maintenant de deux variables, et d'elles seules :
   `--tl-axis` (x du centre) et `--tl-dot-y` (y du centre de la pastille dans
   son étape = `top` de la pastille + son rayon). Si la taille des pastilles
   change, c'est `--tl-dot-y` qu'on met à jour, pas les règles une par une. */
.timeline {
  --p: 0;
  --tl-axis: 7px;   /* axe vertical commun trait ↔ pastilles */
  --tl-dot-y: 11px; /* = .tl-node::before top (4px) + rayon (7px) */
  /* Course du trait : du centre de la 1re pastille à celui de la dernière.
     La valeur exacte est posée en px par le script de la frise (la hauteur de
     la dernière étape dépend de son texte) ; ceci est le repli sans JS. */
  --tl-span: calc(100% - var(--tl-dot-y) - 40px);
  position: sticky;
  top: 120px;
}
.timeline__track {
  position: absolute;
  left: calc(var(--tl-axis) - 1px);
  top: var(--tl-dot-y);
  height: var(--tl-span);
  width: 2px;
  background: var(--border-subtle);
  border-radius: var(--radius-pill);
}
.timeline__fill {
  position: absolute;
  left: calc(var(--tl-axis) - 1px);
  top: var(--tl-dot-y);
  width: 2px;
  height: calc(var(--p) * var(--tl-span));
  background: var(--bump-forest);
  border-radius: var(--radius-pill);
  transition: height 120ms linear;
}
.timeline__nodes { display: flex; flex-direction: column; }
.tl-node {
  position: relative;
  padding: 0 0 var(--space-7) 34px;
}
.tl-node:last-child { padding-bottom: 0; }
.tl-node::before {
  content: "";
  position: absolute;
  top: 4px;
  left: var(--tl-axis);
  transform: translateX(-50%);
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--surface-page);
  border: 2px solid var(--border-medium);
  transition: background var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
.tl-node.is-active::before {
  background: var(--bump-forest);
  border-color: var(--bump-forest);
}
.tl-node.is-active.is-current::before {
  transform: translateX(-50%) scale(1.25);
  box-shadow: 0 0 0 5px rgba(22, 43, 28, 0.12);
}
.tl-node__year {
  font-size: 1.5rem;
  font-weight: var(--weight-light);
  letter-spacing: var(--tracking-tight);
  color: var(--gray-300);
  margin-bottom: 4px;
  transition: color var(--dur) var(--ease-out);
}
.tl-node.is-active .tl-node__year {
  color: var(--bump-forest);
  font-weight: var(--weight-medium);
}
.tl-node__label {
  font-size: 0.9375rem;
  font-weight: 400;
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 34ch;
  transition: color var(--dur) var(--ease-out);
}
.tl-node.is-active .tl-node__label { color: var(--gray-700); }
.tl-node--future .tl-node__year::after {
  content: " →";
  color: var(--sage-deep);
}

/* ============================================================
   Équipe
   ============================================================ */
.team-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-7);
}
.member__photo {
  /* Sortie du survol — voir « Survol d'une image » : le cadre recule pendant
     que la photo avance. Même geste que les briques de projets. */
  --hv: 460ms;
  aspect-ratio: 4 / 5;
  background: var(--surface-sage);
  border-radius: var(--radius-card);
  margin-bottom: var(--space-5);
  position: relative;
  overflow: hidden;
  transition: transform var(--hv) var(--ease-soft);
}
.member__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  /* assombrit légèrement la photo : le voile vert paraît plus profond */
  filter: brightness(0.82) saturate(1.05);
  transition: transform var(--hv) var(--ease-soft), filter 620ms var(--ease-soft);
}
/* Voile vert sapin : teinte la photo en vert (luminance préservée, photo visible) */
.member__photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--bump-forest);
  mix-blend-mode: color;
  opacity: 1;
  pointer-events: none;
  transition: opacity 620ms var(--ease-soft);
}
/* La mise à l'échelle est portée par la règle commune « Survol d'une image ».
   Ici, seule la couleur revient : le voile vert s'efface et la photo reprend
   sa luminosité. */
@media (hover: hover) and (pointer: fine) {
  .member:hover .member__photo img { filter: none; }
}
.member:hover .member__photo::after { opacity: 0; }
.member__name {
  font-size: 1.5rem;
  margin-bottom: 4px;
}
.member__role {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--bump-forest);
  margin-bottom: var(--space-4);
}
.member__bio {
  font-size: 1rem;
  font-weight: 400;
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
  max-width: 46ch;
  margin-bottom: var(--space-4);
}
.member__social {
  display: flex;
  gap: 18px;
}
.member__social a {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-body);
}
.member__social a:hover { color: var(--bump-forest); }

/* ============================================================
   Les planches de logos — clients et partenaires (07/08/2026)
   ------------------------------------------------------------
   🚨 `.lcloud` REMPLACE deux dispositifs d'un coup : les deux bandes
   défilantes des clients (03/08/2026, logos + cartes d'avis, convoyeurs
   pilotés image par image) et l'ancienne grille `.logo-grid` / `.logo-cell`
   des partenaires. Demande client : « j'aimerais déjà juste mettre les
   logos… on met tous les logos au même endroit », puis « applique ça à nos
   partenaires stratégiques, mais un peu différemment et en plus grand ».
   Les avis reviendront dans une section à part. Ne pas réintroduire de
   convoyeur ici, ni la moindre photo de personne.

   Référence client : une planche modulaire à filets, avec croix de repère
   aux intersections. Deux déclinaisons, un seul jeu de règles :

   | | `--clients` | `--partenaires` |
   |---|---|---|
   | colonnes | 4 (2 ≤ 900 px) | 3 (1 ≤ 760 px) |
   | fond | damier papier / papyrus dilué | papyrus PLEIN |
   | échelle | cases ~152 px, encre ≤ 62 | cases ~230 px, encre ≤ 92 |
   | croix | intersections intérieures | sur les deux filets pleine largeur |

   🚨 Le fond du palier partenaires est du PAPYRUS, jamais du forêt. La
   section Contact juste en dessous est en vert forêt : deux blocs sombres
   d'affilée, et la page perd la respiration qu'on lui a rendue en passant
   Résultats au graphite. Même raisonnement, même interdiction.
   ============================================================ */
.lcloud {
  --lc-h: clamp(104px, min(10.6vw, calc(60.8px + 2.375vw)), 152px);
  /* 🚨 Plafond de HAUTEUR d'encre, pas de largeur : c'est ce qui ramène au
     même registre un logo haut et un logo large. ⚠️ Les SVG de la charte
     n'ont pas de dimensions intrinsèques : la hauteur doit être IMPOSÉE et
     la largeur suivre, sinon le navigateur n'a rien pour déduire la
     dimension manquante du viewBox et l'image tombe à 0 × 0 (constaté). */
  --lc-ink: clamp(40px, min(4.4vw, calc(24.8px + 0.96875vw)), 62px);
  /* 🚨 Le plafond de largeur est borné EN PIXELS autant qu'en pourcentage :
     sur une case large (2 colonnes, ou une case partenaire), un pourcentage
     seul laissait une signature horizontale s'étaler sur 320 px pendant
     qu'un logo empilé en occupait 75 — la rangée n'avait plus de registre. */
  --lc-wmax: 208px;
  /* Bras de la croix de repère. Vient du jeton partagé avec `.trame-cadre` : la
     planche et l'habillage de section dessinent la même croix. Le palier
     partenaires le relève (case plus grande, croix plus grande). */
  --lc-croix: var(--trame-bras);
  position: relative;
  display: grid;
  /* 🚨 LA PLANCHE EST UN `.trame-cadre` ÉCRIT AUTREMENT : elle déborde du
     contenu de `--trame-air` exactement comme lui, et ses bordures tiennent
     lieu de verticales. C'est ce qui garde les deux dessins à l'aplomb l'un de
     l'autre (mesuré : 0,0 px de 390 à 2560) depuis que les lignes ont été
     écartées du texte, le 10/08/2026. Sa largeur n'est donc plus celle du
     `.shell` : c'est celle de la trame. Ne pas remettre à 0 d'un côté sans
     l'autre, et ne pas lui ajouter la classe `.trame-cadre` — elle doublerait
     rails, verticales et croix. */
  margin-inline: calc(-1 * var(--trame-air));
  /* Le cadre : filet en haut et à gauche ici, les cases portent la droite et
     le bas. Une seule épaisseur partout, aucun doublement. */
  border-top: 1px solid var(--trame-ligne);
  border-left: 1px solid var(--trame-ligne);
}

/* 🚨 Les deux filets qui TRAVERSENT L'ÉCRAN. C'est la signature de la
   référence : la planche reste dans la colonne de contenu, mais ses lignes
   horizontales vont d'un bord à l'autre de la fenêtre — la section respire
   au-delà du `.shell` sans que rien d'autre ne bouge.
   `overflow-x` du body est déjà masqué : le débord ne crée pas de barre.
   ⚠️ Les deux décalages ne sont pas symétriques, et c'est normal : le cadre
   d'un absolu est la boîte de PADDING, donc `top: 0` tomberait 1 px sous le
   filet du haut (que la planche porte en bordure), alors qu'en bas il n'y a
   pas de bordure et `bottom: 0` couvre exactement le filet de la dernière
   rangée de cases.

   ⚠️ Le débord DOIT rester exprimé en `left: 50%` + `width: 100vw`. Une
   version l'a écrit `left: -50vw; right: -50vw` pour rendre les positions
   mesurables depuis les bords de la planche : ça marchait, mais le calque
   faisait alors 100vw DE PLUS que la planche et le débordement horizontal
   du document passait de 1 px à 640 px (mesuré). Le `overflow-x` du body le
   masque, mais il fausse tous les bancs qui contrôlent « 0 débordement ». */
.lcloud::before,
.lcloud::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  border-top: 1px solid var(--trame-ligne);
  pointer-events: none;
}
.lcloud::before { top: -1px; }
.lcloud::after { bottom: 0; }

.lcloud__cell {
  position: relative;
  min-height: var(--lc-h);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5) var(--space-4);
  border-right: 1px solid var(--trame-ligne);
  border-bottom: 1px solid var(--trame-ligne);
  /* Micro-transition de couleur : sortie plus rapide que l'entrée (la durée
     d'entrée est relevée dans la règle de survol). */
  transition: background-color 160ms var(--ease-out),
              box-shadow 160ms var(--ease-out);
}

/* Croix de repère. Deux dégradés d'1 px croisés, centrés sur un COIN de la
   case — donc exactement sur un point où des filets se rejoignent. Pas
   d'icône importée : c'est deux lignes. `::after` = coin bas-droit (le seul
   qu'utilise le palier clients), `::before` = coin haut-droit (réservé aux
   partenaires, qui n'ont qu'une rangée). Chaque palier décide ensuite
   lesquelles se montrent. */
.lcloud__cell::before,
.lcloud__cell::after {
  content: "";
  position: absolute;
  z-index: 2;
  right: calc(-0.5 * var(--lc-croix));
  width: var(--lc-croix);
  height: var(--lc-croix);
  background:
    linear-gradient(var(--trame-croix) 0 0) center / 100% 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) center / 1px 100% no-repeat;
  pointer-events: none;
}
.lcloud__cell::before { top: calc(-0.5 * var(--lc-croix)); display: none; }
.lcloud__cell::after { bottom: calc(-0.5 * var(--lc-croix)); }

/* Au repos, le logo est en monochrome discret ; la couleur revient au survol.
   C'est ce qui unifie des identités très différentes sans les écraser.

   🚨 UNE TEINTE EN VERT FORÊT A ÉTÉ FAITE PUIS RETIRÉE (07/08/2026). Une
   chaîne de filtres calibrée reteignait toute l'encre en `--bump-forest`
   plein, à opacité 1 — refusé par le client, qui préfère le gris translucide
   (« reviens aux couleurs que t'as mises avant »). Elle avait par ailleurs
   deux vrais défauts, à retenir si l'idée revient :
     1. sur un logo DÉJÀ monochrome noir (Coliseum, La Cave, La Cène), le
        survol devenait invisible — forêt #162B1C → noir #000, on ne voyait
        plus rien se passer ;
     2. interpoler une chaîne de SIX fonctions à chaque image coûte cher, et
        ça se sentait sur les grandes cases des partenaires.
   Le gris n'a aucun de ces deux problèmes : il s'éclaircit vers la couleur
   quel que soit le logo, et `grayscale()` n'a qu'un paramètre à interpoler.

   ⚠️ `grayscale(0)` et non `none` au survol : `none` marche, mais écrire la
   même fonction des deux côtés garantit l'interpolation au lieu de la laisser
   dépendre de la règle « liste vide = identité » du moteur. */
.lcloud__cell img {
  height: var(--lc-ink);
  width: auto;
  max-width: min(76%, var(--lc-wmax));
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.58;
  transition: filter 160ms var(--ease-out), opacity 160ms var(--ease-out);
}

/* ------------------------------------------------------------
   Palier CLIENTS — 4 colonnes, damier papier / papyrus dilué

   🚨 Le nombre de cases est un MULTIPLE DE 4 : le gabarit complète la
   dernière rangée par des cases vides. Ce n'est pas un bouche-trou. La
   planche est un cadre rectangulaire, une rangée tronquée l'ouvrirait sur
   le côté ; la case vide prend sa couleur dans le damier et se lit comme
   une tuile de la composition. Et c'est cette divisibilité qui rend le
   damier ET la détection de « dernière rangée » exacts aux DEUX paliers de
   colonnes, 4 et 2 — les seuls autorisés, parce qu'ils divisent 4.
   ⚠️ Ne pas ajouter un palier à 3 colonnes : `:nth-last-child(-n+3)` serait
   faux. (Le palier partenaires, lui, est à 3 — mais il complète à 3.)
   ------------------------------------------------------------ */
.lcloud--clients { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.lcloud--clients .lcloud__cell { background: var(--surface-page); }

/* Damier à 4 colonnes : (rangée + colonne) pair, soit les cases 1, 3, 6, 8
   de chaque bloc de deux rangées. Un `:nth-child(odd)` ne marcherait pas —
   sur un nombre PAIR de colonnes il donne des bandes verticales. */
@media (min-width: 901px) {
  .lcloud--clients .lcloud__cell:nth-child(8n + 1),
  .lcloud--clients .lcloud__cell:nth-child(8n + 3),
  .lcloud--clients .lcloud__cell:nth-child(8n + 6),
  .lcloud--clients .lcloud__cell:nth-child(8n + 8) { background: var(--sage-soft); }
  /* Pas de croix sur le cadre extérieur : dernière colonne, dernière rangée. */
  .lcloud--clients .lcloud__cell:nth-child(4n)::after,
  .lcloud--clients .lcloud__cell:nth-last-child(-n + 4)::after { display: none; }
}

/* Palier 2 colonnes — le compte étant un multiple de 4, il l'est aussi de 2 :
   le damier redevient une case sur deux en quinconce. */
@media (max-width: 900px) {
  .lcloud--clients {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* La case devient deux fois plus large : sans reprise elle tombait à un
       rapport de 4:1, une meurtrière plutôt qu'une tuile. */
    --lc-h: clamp(108px, 14vw, 150px);
    --lc-ink: clamp(38px, 5.6vw, 54px);
  }
  .lcloud--clients .lcloud__cell:nth-child(4n + 1),
  .lcloud--clients .lcloud__cell:nth-child(4n + 4) { background: var(--sage-soft); }
  .lcloud--clients .lcloud__cell:nth-child(2n)::after,
  .lcloud--clients .lcloud__cell:nth-last-child(-n + 2)::after { display: none; }
}

/* ------------------------------------------------------------
   Palier PARTENAIRES — 3 colonnes, plus grand, papyrus plein

   Ils ne sont que trois et ça ne bougera pas : la planche tient sur UNE
   rangée, et c'est L'ÉCHELLE SEULE qui fait la différence avec celle des
   clients (case ~1,5 × plus haute, encre ~1,5 × plus grande).

   🚨 LES COULEURS SONT EXACTEMENT CELLES DES CLIENTS. Un aplat de papyrus
   plein sur toute la rangée a été fait puis retiré (07/08/2026) : demande
   client, « il faut que t'appliques les mêmes couleurs qu'en haut ». Ne pas
   y revenir, et ne pas non plus tenter le vert forêt — la section Contact
   juste en dessous est déjà en forêt, deux blocs sombres d'affilée et la
   page reperd sa respiration.

   Le damier tombe juste tout seul ici : sur un nombre IMPAIR de colonnes,
   `:nth-child(odd)` produit une vraie quinconce (1 et 3 teintées) et
   continue de s'inverser d'une rangée à l'autre si un quatrième partenaire
   arrive. C'est le cas inverse des clients, à 4 colonnes, où `odd` donnerait
   des bandes verticales.

   🚨 Les croix passent sur les DEUX FILETS PLEINE LARGEUR. Avec une seule
   rangée il n'existe aucune intersection intérieure : les seuls vrais
   croisements sont ceux des filets verticaux intérieurs avec le filet du
   haut et celui du bas. D'où `::before` (coin haut-droit) ET `::after`
   (coin bas-droit) sur les cases qui ne sont pas en dernière colonne.
   ------------------------------------------------------------ */
.lcloud--partenaires {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  --lc-h: clamp(150px, min(16vw, calc(92.8px + 3.625vw)), 232px);
  --lc-ink: clamp(52px, min(6.4vw, calc(36.8px + 1.4375vw)), 92px);
  --lc-wmax: 330px;
  --lc-croix: 15px;
}
.lcloud--partenaires .lcloud__cell { background: var(--surface-page); }
.lcloud--partenaires .lcloud__cell:nth-child(odd) { background: var(--sage-soft); }
.lcloud--partenaires .lcloud__cell:not(:nth-child(3n))::before { display: block; }
.lcloud--partenaires .lcloud__cell:nth-child(3n)::after { display: none; }

/* Une colonne : trois cases pleine largeur. Plus de filet vertical
   intérieur, donc plus aucun croisement à marquer — les croix disparaissent
   avec eux. */
@media (max-width: 760px) {
  .lcloud--partenaires {
    grid-template-columns: minmax(0, 1fr);
    --lc-h: clamp(132px, 34vw, 176px);
    --lc-ink: clamp(48px, 13vw, 72px);
  }
  /* ⚠️ Le sélecteur doit REPRENDRE le `:not(:nth-child(3n))` de la règle du
     dessus : une media query n'ajoute aucune spécificité, et `::before`
     restait donc affiché en une colonne (constaté). */
  .lcloud--partenaires .lcloud__cell:not(:nth-child(3n))::before,
  .lcloud--partenaires .lcloud__cell::after { display: none; }
}

/* ------------------------------------------------------------
   Les QUATRE COINS du cadre extérieur (07/08/2026)

   Ils étaient éteints — « pas de croix sur le cadre extérieur ». Depuis que
   `.trame-cadre` pose des verticales à l'aplomb des bords de la planche
   (mesuré : 0,0 px d'écart), ces quatre coins sont des intersections comme les
   autres et il n'y a plus de raison de les traiter à part.

   🚨 Aucune croix INTÉRIEURE n'est sacrifiée : on ne prend que des pseudos
   libres. `::before` est éteint partout chez les clients, et `::after` l'est
   sur la dernière colonne et la dernière rangée.
   ⚠️ Les pseudos sont ancrés à DROITE : les deux coins de gauche demandent
   donc de rebasculer l'ancrage (`right: auto` + `left`).
   ⚠️ Ces règles sont APRÈS les deux paliers : à spécificité égale (0,3,1)
   c'est l'ordre qui décide, et placées avant elles ne rallumeraient rien.
   ------------------------------------------------------------ */
/* Coin bas-droit : dernière case, le pseudo est déjà au bon endroit. */
.lcloud .lcloud__cell:last-child::after { display: block; }

/* Coin haut-gauche (clients). ⚠️ Scopé au palier clients : chez les
   partenaires, `::before` de la 1re case sert déjà à une intersection
   intérieure, elle est traitée en bande plus bas. */
.lcloud--clients .lcloud__cell:first-child::before {
  display: block;
  right: auto;
  left: calc(-0.5 * var(--lc-croix));
}

/* Coin haut-droit et coin bas-gauche : ils dépendent du nombre de colonnes,
   donc un jeu de règles par palier. Le coin bas-gauche est la 1re case de la
   dernière rangée, d'où le `:nth-last-child` — juste parce que le nombre de
   cases est un multiple du nombre de colonnes. */
@media (min-width: 901px) {
  .lcloud--clients .lcloud__cell:nth-child(4)::before { display: block; }
  .lcloud--clients .lcloud__cell:nth-last-child(4)::after {
    display: block;
    right: auto;
    left: calc(-0.5 * var(--lc-croix));
  }
}
@media (max-width: 900px) {
  .lcloud--clients .lcloud__cell:nth-child(2)::before { display: block; }
  .lcloud--clients .lcloud__cell:nth-last-child(2)::after {
    display: block;
    right: auto;
    left: calc(-0.5 * var(--lc-croix));
  }
}

/* Partenaires : trois cases, une rangée, donc 6 pseudos pour 8 croisements.
   Les deux coins de GAUCHE passent par un calque EN BANDE, large d'une case,
   qui porte une croix à chaque bout : le bout droit reprend exactement
   l'intersection intérieure que le point dessinait, le bout gauche ajoute le
   coin extérieur. Rien n'est perdu, rien n'est dessiné deux fois.
   ⚠️ Ne pas étendre la bande aux autres cases : deux bandes voisines
   dessineraient la même croix au même endroit, et deux couches d'une encre
   translucide font une croix plus foncée que ses voisines. */
.lcloud--partenaires .lcloud__cell:nth-child(3n)::before { display: block; }
.lcloud--partenaires .lcloud__cell:first-child::before,
.lcloud--partenaires .lcloud__cell:first-child::after {
  left: calc(-0.5 * var(--lc-croix));
  width: auto;
  background:
    linear-gradient(var(--trame-croix) 0 0) left 0 center / var(--lc-croix) 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) right 0 center / var(--lc-croix) 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) left calc((var(--lc-croix) - 1px) / 2) top 0 / 1px 100% no-repeat,
    linear-gradient(var(--trame-croix) 0 0) right calc((var(--lc-croix) - 1px) / 2) top 0 / 1px 100% no-repeat;
}
/* En une colonne il n'y a plus d'intersection du tout (aucun filet vertical
   intérieur, et les coins du cadre seraient les seuls marqués) : on garde la
   planche nue, comme avant. */
   ⚠️ Chaque sélecteur à rééteindre doit être REPRIS tel quel : une media
   query n'ajoute aucune spécificité, et `.lcloud--partenaires .lcloud__cell`
   (0,2,1) ne l'emporte pas sur les règles à `:first-child` / `:nth-child`
   (0,3,1) écrites au-dessus. Piège transverse déjà rencontré ici. */
@media (max-width: 760px) {
  .lcloud--partenaires .lcloud__cell:first-child::before,
  .lcloud--partenaires .lcloud__cell:nth-child(3n)::before,
  .lcloud--partenaires .lcloud__cell:first-child::after,
  .lcloud--partenaires .lcloud__cell:last-child::after { display: none; }
}

/* La case s'allume et le logo retrouve ses vraies couleurs. Aucun
   déplacement : une planche à filets ne se soulève pas, ce sont ses cases
   qui s'éclairent.

   🚨 DEUX PIÈGES DE CASCADE ICI, les deux constatés.

   1. Ce bloc doit rester APRÈS les deux paliers : `.lcloud__cell:hover` et
      `.lcloud--clients .lcloud__cell` ont la même spécificité (0,2,0), c'est
      donc l'ordre qui décide. Placé avant, le survol ne changeait plus aucun
      fond.
   2. 🚨 Le sélecteur doit être `.lcloud .lcloud__cell:hover` (0,3,0) et NON
      `.lcloud__cell:hover` (0,2,0). Les règles de damier portent un
      `:nth-child(…)`, donc (0,3,0) : à (0,2,0) le survol ne l'emportait que
      sur les cases NUES, et les cases TEINTÉES gardaient leur fond. Sur un
      logo déjà monochrome posé dans une case teintée (La Cave, Coliseum,
      MD Sherpa), il ne restait alors que le passage d'opacité et le survol
      paraissait absent — c'est exactement ce qu'a signalé le client le
      07/08/2026. Mesuré avant / après sur la case entière (écart moyen RGB) :
      La Cave 5,1 → 49,9 · Coliseum 9,4 → 44,2 · MD Sherpa 5,5 → 49,9.
      ⚠️ Si une règle de fond plus spécifique est ajoutée à un palier, il faut
      remonter celle-ci d'autant. Banc : `scratchpad/check-survol.js`. */
@media (hover: hover) and (pointer: fine) {
  .lcloud .lcloud__cell:hover {
    background: var(--bump-white);
    /* ⚠️ Le fond seul ne suffit pas à ÉGALISER le survol : une case teintée
       qui passe au blanc bouge beaucoup (écart mesuré ~46), une case nue
       très peu (~12), et la grille répondait donc à deux intensités. Le
       filet, lui, se referme de la même façon sur les deux — c'est le même
       geste que le cadre des cellules du bento Résultats. Posé en ombre
       INTÉRIEURE : les bordures sont partagées entre cases voisines, en
       assombrir deux côtés seulement donnait un survol bancal. */
    box-shadow: inset 0 0 0 1px var(--border-medium);
    transition-duration: 240ms;
  }
  .lcloud .lcloud__cell:hover img {
    filter: grayscale(0);
    opacity: 1;
    transition-duration: 240ms;
  }
}
/* 🚨 SANS POINTEUR FIN, LES LOGOS RESTENT GRIS — demande client du 14/08/2026
   (« dans la partie Ils nous font confiance, j'aimerais qu'ils soient toujours
   grisés sur mobile »). Ils y étaient jusque-là en COULEUR permanente
   (`grayscale(0)`, opacité 0,9) : faute de survol, on avait choisi de montrer
   l'état d'arrivée plutôt que l'état de repos. Le client préfère l'inverse —
   une planche de logos unifiée, qui ne part pas en arc-en-ciel sur un écran où
   rien ne peut la rendre à sa couleur.
   ⚠️ On reprend EXACTEMENT l'état de repos du desktop (`grayscale(1)`, 0,58),
   pas une valeur intermédiaire : c'est ce qui fait que la planche a la même
   allure aux deux bouts. Si elle paraît trop pâle au doigt, c'est l'opacité de
   repos qu'il faut remonter — des deux côtés à la fois, jamais ici seulement.
   ⚠️ Vaut pour les DEUX planches (clients et partenaires) : elles partagent
   `.lcloud__cell`, et le tronc commun ne se scinde pas. */
@media (hover: none), (pointer: coarse) {
  .lcloud__cell img { filter: grayscale(1); opacity: 0.58; }
}

/* ============================================================
   Projets — pièces communes
   ------------------------------------------------------------
   ⚠️ L'ancienne grille `.projects-grid` et les cartes `.project*` ont été
   SUPPRIMÉES le 05/08/2026 : les deux pages (home et projets.html) utilisent
   désormais les mêmes briques (`.bricks` / `.brick`, plus bas). Ne pas les
   recréer — une seule mise en page de liste de projets sur le site.
   ============================================================ */
.project__embed {
  margin-top: var(--space-4);
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--gray-100);
}
.project__embed iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
.projects-cta {
  margin-top: var(--space-7);
  display: flex;
  justify-content: center;
}
.projects-empty {
  grid-column: 1 / -1;
  font-size: 0.9375rem;
  color: var(--text-muted);
  border: 1px dashed var(--border-medium);
  border-radius: var(--radius-card);
  padding: var(--space-6);
  text-align: center;
}


/* ============================================================
   Projets — les BRIQUES
   Référence client : cuberto.com/projects (capture du 05/08/2026).
   Deux colonnes, l'image en grand puis une seule ligne de texte dessous ;
   la colonne de droite descend d'une DEMI-BRIQUE.

   Mise en page commune à la HOME et à `projets.html` : c'est la seule liste
   de projets du site. La seule différence est le lien porté par la brique —
   `projets.html#id` sur la home, `#id` sur la page projets, où il ouvre le
   panneau détail au lieu de naviguer.
   ============================================================ */
.bricks {
  /* Rapport de l'image. Le décalage en découle : une demi-brique vaut
     `0.5 / ratio` de la LARGEUR de colonne — d'où 37,5 % pour du 4/3.
     🚨 Si le rapport change, recalculer --brique-demi ET --brique-pied. */
  --brique-ratio: 4 / 3;
  --brique-demi: 37.5%;
  --brique-ecart: clamp(20px, 3.2vw, 44px);
  /* Rattrapage du débord de la dernière brique de droite. Exprimé sur la
     largeur du CONTENEUR (et non de la colonne) : 0.375 / 2 = 0.1875. */
  --brique-pied: calc((100% - var(--brique-ecart)) * 0.1875);

  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--brique-ecart);
  row-gap: clamp(44px, 6vw, 92px);
  /* Sans lui, la grille étire les deux briques d'une rangée à la même
     hauteur et le décalage ne se voit plus. */
  align-items: start;
  padding-bottom: var(--brique-pied);
}

/* Le décalage d'une demi-brique.
   🚨 `margin-top` positif ET `margin-bottom` négatif de la même valeur : la
   marge négative rend la hauteur de rangée identique à celle d'une brique
   normale. Sans elle, la rangée grandirait du décalage et l'écart vertical
   entre deux briques de la MÊME colonne s'ouvrirait d'autant.
   ⚠️ Le pourcentage d'une marge se résout sur la largeur de la COLONNE (la
   zone de grille), pas sur celle du conteneur — c'est ce qui rend la valeur
   proportionnelle à l'image, donc juste à toutes les tailles d'écran. */
.brick:nth-child(even) {
  margin-top: var(--brique-demi);
  margin-bottom: calc(-1 * var(--brique-demi));
}

.brick {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.brick:active { transform: scale(0.985); }

.brick__thumb {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-card);
  aspect-ratio: var(--brique-ratio);
  background: var(--surface-forest);
  margin-bottom: 18px;
}
.brick__thumb img,
.brick__thumb > div {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
/* Le survol des briques est le geste commun du site — voir « Survol d'une
   image » plus bas. Chaque cadre n'a qu'à déclarer `--hv` et sa transition. */

/* Badge vidéo — repris tel quel des cartes de projets.html */
.brick__play {
  position: absolute;
  bottom: 14px;
  right: 14px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 10px;
  border-radius: var(--radius-pill);
  background: rgba(243, 244, 239, 0.92);
  color: var(--bump-forest);
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  backdrop-filter: blur(4px);
}
.brick__play::before {
  content: "";
  width: 0; height: 0;
  border-style: solid;
  border-width: 5px 0 5px 8px;
  border-color: transparent transparent transparent var(--bump-forest);
}

/* Une seule ligne de texte, comme la référence : le nom du client en graisse,
   puis ce qu'on a fait pour lui. Coupée à 2 lignes — c'est la longueur de la
   référence, et une description de 300 signes déséquilibrerait la rangée. */
.brick__line {
  margin: 0;
  font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
  line-height: 1.45;
  color: var(--text-body);
  max-width: 46ch;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.brick__line strong { font-weight: var(--weight-medium); }
.brick__line span { color: var(--text-muted); }

/* Le pied de brique : la légende, et l'index de la série à l'aplomb du bord
   droit. Le numéro remplit le blanc laissé par une légende courte et donne
   l'échelle de la sélection — c'est le même vocabulaire que le compteur des
   prestations. */
.brick__foot {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
}
.brick__num {
  flex: none;
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  padding-top: 3px;
  font-variant-numeric: tabular-nums;
}

/* ---- Apparition au scroll -------------------------------------------------
   « Ils apparaissent petit à petit en commençant par le haut, ensuite celui de
   droite, celui de gauche, celui de droite. » C'est exactement l'ordre du DOM :
   chaque brique est observée séparément et se révèle en entrant dans le champ,
   avec un décalage `--d` posé par le script sur celles qui entrent ensemble.

   L'image se découvre par le bas (`clip-path`) pendant que son zoom se relâche.
   ⚠️ Le dézoom passe par la propriété `scale`, PAS par `transform` : `transform`
   appartient au survol, et les deux doivent pouvoir jouer en même temps. */
.brick__thumb {
  /* Durée du survol. Sur le cadre, donc héritée par l'image : les deux se
     déplacent forcément ensemble, et `:hover` n'a qu'un chiffre à changer. */
  --hv: 460ms;
  clip-path: inset(0 0 100% 0 round var(--radius-card));
  transition: clip-path 880ms var(--ease-soft) var(--d, 0ms),
              transform var(--hv) var(--ease-soft);
}
.brick.is-in .brick__thumb { clip-path: inset(0 0 0 0 round var(--radius-card)); }
.brick__thumb img {
  scale: 1.1;
  transition: transform var(--hv) var(--ease-soft),
              scale 1100ms var(--ease-soft) var(--d, 0ms);
}
.brick.is-in .brick__thumb img { scale: 1; }
.brick__line {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 620ms var(--ease-out) calc(var(--d, 0ms) + 150ms),
              transform 620ms var(--ease-soft) calc(var(--d, 0ms) + 150ms);
}
.brick.is-in .brick__line { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .brick__thumb { clip-path: none; transition: none; }
  .brick__thumb img { scale: 1; transition: none; }
  .brick__line { opacity: 1; transform: none; transition: none; }
  /* Le survol est un mouvement : sans transition il deviendrait un à-coup.
     On le retire plutôt que de le jouer d'un bloc. */
  .brick:hover .brick__thumb,
  .brick:hover .brick__thumb img { transform: none; }
}

@media (max-width: 900px) {
  /* Une colonne : plus de décalage à rattraper, ni en haut ni en pied. */
  .bricks { grid-template-columns: minmax(0, 1fr); padding-bottom: 0; row-gap: var(--space-7); }
  .brick:nth-child(even) { margin-top: 0; margin-bottom: 0; }
}


/* La section projets a porté un « palier papyrus » qui faisait basculer les
   couleurs de tout le site au scroll (05/08/2026). Rejeté par le client — le
   fondu ne rendait pas. La page garde donc ses couleurs d'un bout à l'autre.
   Ne pas le réintroduire. */


/* ============================================================
   Rideau de page — transition vers un projet
   ------------------------------------------------------------
   Même geste que le survol des cartes prestations, mais à l'échelle de
   l'écran : deux cercles ouverts depuis des coins OPPOSÉS, le vert d'abord,
   le papyrus qui le rattrape. On navigue une fois l'écran couvert ; la page
   d'arrivée rejoue le mouvement à l'envers.

   🚨 Rayon 170 % : pour `circle(R% at …)` le rayon utilisé vaut
   `R × √(l²+h²)/√2`. Couvrir l'écran depuis un coin demande ~145 % au format
   d'un mobile (390×844) et ~142 % en 16/9. Ne pas descendre sous 150 %.
   ============================================================ */
.pageveil {
  position: fixed;
  inset: 0;
  z-index: 900;
  pointer-events: none;
  visibility: hidden;
}
.pageveil > i {
  position: absolute;
  inset: 0;
  display: block;
}
.pageveil__a { background: var(--bump-forest); clip-path: circle(0% at 14% -6%); }
.pageveil__b { background: var(--bump-sage);   clip-path: circle(0% at 92% 106%); }

/* ---- Sortie de page : les deux volets montent, et c'est tout ----
   🚨 Le rideau ne joue QU'AU DÉPART. Il a un temps rejoué à l'envers sur la
   page d'arrivée (relais par `sessionStorage` + script en ligne dans le
   <head>) : le client l'a trouvé « double », et ça se passait mal — la page
   d'arrivée est déjà en train de se construire, on lui superposait une
   seconde animation. Un seul geste, au clic. Ne pas remettre l'arrivée. */
.pageveil.is-live { visibility: visible; }
.pageveil.is-cover .pageveil__a {
  clip-path: circle(170% at 14% -6%);
  transition: clip-path 340ms var(--ease-wipe);
}
.pageveil.is-cover .pageveil__b {
  clip-path: circle(170% at 92% 106%);
  transition: clip-path 360ms var(--ease-wipe) 110ms;
}

@media (prefers-reduced-motion: reduce) {
  .pageveil { display: none; }
}


/* ============================================================
   Curseur — le point qui suit, et devient une flèche sur un projet
   ------------------------------------------------------------
   Demande client (05/08/2026) : « un point noir qui se déplace, et quand
   j'arrive sur Projet il devient plus grand, devient une flèche, et les
   couleurs sont inversées en dessous ».

   🚨 L'inversion est faite par `mix-blend-mode: difference` sur un élément
   BLANC — c'est ce qui donne exactement la capture de référence : noir sur
   fond papier, vert clair sur fond forêt, et la flèche (noire dans l'élément)
   ressort dans la couleur du fond. Aucune couleur n'est choisie à la main :
   le curseur se contraste tout seul sur n'importe quel visuel.
   ⚠️ Un `mix-blend-mode` ne franchit pas un contexte d'empilement : le
   curseur doit rester enfant direct de `<body>`. Le déplacer dans un
   conteneur positionné le rendrait simplement blanc.

   Le curseur natif est CONSERVÉ (demande explicite) : ceci s'ajoute, ça ne
   remplace pas. Donc pas de `cursor: none` nulle part.
   ============================================================ */
/* 🚨 DEUX couches superposées, et c'est indispensable.
   `mix-blend-mode` s'applique à tout l'élément, pseudo-éléments compris : une
   flèche placée dedans prendrait elle aussi la couleur inversée, ce qui lui
   dessinait un contour au lieu d'un blanc franc (retour client). La flèche vit
   donc dans une SECONDE couche, non fusionnée, en blanc pur.
   ⚠️ Elles ne peuvent pas être imbriquées : un parent positionné avec
   `z-index` crée un contexte d'empilement, et le disque ne fusionnerait alors
   plus qu'avec l'intérieur de son parent, c'est-à-dire avec rien. Deux frères,
   enfants directs de <body>, déplacés par le même `transform`. */
.curseur,
.curseur-pt {
  position: fixed;
  top: 0; left: 0;
  width: var(--cur-taille, 78px);
  height: var(--cur-taille, 78px);
  margin: calc(var(--cur-taille, 78px) / -2) 0 0 calc(var(--cur-taille, 78px) / -2);
  pointer-events: none;
  will-change: transform;
}

/* Couche 1 — le disque qui inverse le fond. Il n'existe QUE sur un projet :
   au repos, le curseur est un simple point vert, jamais inversé (demande
   client du 05/08/2026). */
.curseur {
  z-index: 950;
  border-radius: var(--radius-pill);
  background: #fff;
  mix-blend-mode: difference;
  opacity: 0;
  transition: opacity 240ms var(--ease-out);
}
/* ⚠️ 0,66 et non 1 : l'opacité d'un élément fusionné dose exactement la part
   d'inversion (0,66 d'image inversée + 0,34 de l'image d'origine). C'est le
   seul réglage à toucher pour rendre l'effet plus ou moins franc — demande
   client du 06/08/2026, « réduire un tout petit peu l'intensité ». */
.curseur.is-hot { opacity: 0.66; }

/* Couche 2 — le point vert au repos, la flèche blanche sur un projet.
   Aucune fusion ici : le blanc reste du blanc sur n'importe quel fond. */
.curseur-pt {
  z-index: 951;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  /* Voile de vert forêt posé PAR-DESSUS le disque inversé : c'est lui qui
     fait « sentir le vert » sans toucher à l'inversion elle-même. Transparent
     au repos, où le curseur n'est qu'un point. */
  background: rgba(22, 43, 28, 0);
  opacity: 0;
  transition: opacity 260ms var(--ease-out), background 300ms var(--ease-out);
}
.curseur-pt.is-on { opacity: 1; }
.curseur-pt.is-hot { background: rgba(22, 43, 28, 0.34); }
.curseur-pt__dot {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  background: var(--bump-forest);
  transition: opacity 200ms var(--ease-out);
}
.curseur-pt.is-hot .curseur-pt__dot { opacity: 0; }
.curseur-pt svg {
  width: 34%;
  height: 34%;
  fill: none;
  /* Papyrus plutôt que du blanc pur : c'est la couleur claire de la charte,
     elle se lit comme la nôtre sur le voile vert. Passer à `--bump-paper`
     si on la veut encore plus claire — un seul jeton. */
  stroke: var(--bump-sage);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 200ms var(--ease-out), transform 320ms var(--ease-soft);
}
.curseur-pt.is-hot svg { opacity: 1; transform: none; }

/* Sans pointeur fin (tactile), et en mouvement réduit, rien du tout : un
   curseur décoratif n'a pas de raison d'être là. */
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
  .curseur, .curseur-pt { display: none; }
}

/* Filtres secteur (projets.html) */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: var(--space-7);
}
.filter-btn {
  font-family: var(--font-sans);
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  padding: 9px 18px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-medium);
  background: transparent;
  color: var(--text-body);
  cursor: pointer;
  transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out), transform 160ms var(--ease-out);
}
.filter-btn:hover { border-color: var(--bump-charcoal); }
.filter-btn:active { transform: scale(0.96); }
.filter-btn.is-active {
  background: var(--bump-forest);
  border-color: var(--bump-forest);
  color: var(--bump-paper);
}

/* ---- Bloc filtres : DEUX AXES, et ça doit se voir --------------------------
   🚨 Les deux rangées de pilules se ressemblaient trop : même forme, même
   couleur active, seule la taille changeait, et seule la seconde portait un
   libellé. Elles se lisaient donc comme UNE liste qui passe à la ligne —
   « je trouve pas très clair entre les deux lignes » (06/08/2026).

   Trois choses les séparent maintenant, et elles vont ensemble :
     • chaque axe est ANNONCÉ par son libellé, dans une colonne alignée ;
     • un filet sépare les deux rangées ;
     • les pilules de prestation restent plus discrètes — c'est l'axe
       secondaire, la hiérarchie doit rester lisible.
   Le libellé porte l'`aria-labelledby` du groupe : ce qui est visible et ce
   qui est annoncé sont le même texte. */
.filters-block {
  margin-bottom: var(--space-7);
}
.filters-row {
  display: grid;
  grid-template-columns: minmax(0, 104px) minmax(0, 1fr);
  align-items: start;
  gap: clamp(12px, 2vw, 26px);
  padding: 14px 0;
}
.filters-row + .filters-row { border-top: 1px solid var(--border-subtle); }
.filters-block .filters { margin-bottom: 0; }
.filters__label {
  /* Aligné sur la première ligne de pilules, pas sur le haut de la rangée :
     c'est le texte qui doit se caler sur le texte. */
  padding-top: 10px;
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
}
.filter-btn--sm {
  font-size: 0.6875rem;
  padding: 6px 13px;
  letter-spacing: 0.07em;
  border-color: var(--border-subtle);
  color: var(--text-muted);
}
.filter-btn--sm:hover { color: var(--text-body); border-color: var(--border-medium); }
.filter-btn--sm.is-active {
  background: var(--bump-forest);
  border-color: var(--bump-forest);
  color: var(--bump-paper);
}

/* ============================================================
   projets.html — la liste qui se replie au scroll
   ------------------------------------------------------------
   Référence client : webrepublic.com/en/projects (capture du 05/08/2026).
   « Au scroll, ils deviennent comme s'ils se sont fermés, ils se mettent plus
   petits et on garde que le titre. »

   🚨 Réservé à `projets.html`. La home garde ses BRIQUES (mise en page
   cuberto validée) : arbitrage explicite du client — une vitrine choisie
   d'un côté, l'index complet de l'autre. Ne pas porter l'un sur l'autre.

   🚨 Il faut UNE SEULE COLONNE : c'est ce qui permet à une ligne de se
   replier sur son titre sans que rien d'autre ne bouge latéralement. Replier
   des éléments dans la grille décalée des briques ferait sauter toute la
   composition — c'est la raison technique de la séparation.
   ============================================================ */
/* ---- En-tête de la page projets : grand titre centré ----
   Référence client du 06/08/2026. C'est la seule page du site avec un titre
   de cette échelle : elle n'a pas de hero, ce titre EST son hero. */
.phead {
  position: relative;
  text-align: center;
  /* 🚨 Beaucoup d'air EN HAUT et surtout EN BAS. Le bas n'est pas un
     espacement décoratif : c'est à la fois le TERRAIN DE JEU du sillage sous
     le titre et le tampon qui le tient à l'écart des filtres.
     ⚠️ Ne pas le réduire pour « resserrer » la page : on reprendrait au
     visiteur la zone où le serpent est le plus agréable à dessiner.
     ⚠️ Les deux paddings sont en VARIABLES parce que l'amorce traitillée les
     reprend telles quelles pour se raccorder au rail de la planche — voir
     `.ptitle`. Les écrire deux fois, c'est se garantir de les désynchroniser. */
  --ph-pt: clamp(38px, 8vw, 118px);
  --ph-pb: clamp(150px, 16vw, 250px);
  padding: var(--ph-pt) 0 var(--ph-pb);
}
.phead h1 {
  margin: 0;
  font-size: clamp(40px, min(8vw, calc(47.2px + 1.84375vw)), 118px);
  line-height: 0.98;
  letter-spacing: -0.02em;
}
.phead .intro {
  margin: clamp(12px, 1.6vw, 22px) auto 0;
  max-width: 46ch;
  font-size: clamp(1rem, 1.4vw, 1.25rem);
}

/* ---- La chaîne verticale de `projets.html` ----
   Amorce traitillée autour du grand titre, puis la planche, puis un dernier
   raccord jusqu'au rail haut du footer : les deux verticales ne s'interrompent
   jamais du haut de la page au bas, exactement comme sur la home.

   🚨 LES TROIS DÉBORDS SONT DES CONSTANTES DE GABARIT, PAS DES VALEURS
   CHOISIES. Chacun reprend un espacement existant ; si l'un d'eux bouge, le
   traitillé se décroche du rail qu'il prolonge :
     · haut de l'amorce, `--section-py + --ph-pt` : le padding haut de la
       section plus celui de l'en-tête, donc le bord haut du contenu de la page ;
     · bas de l'amorce, `--ph-pb` : le padding bas de l'en-tête, qui EST la
       distance du chapô au rail haut de la planche ;
     · sous la planche, `--section-py + --space-8` : le padding bas de la
       section plus le padding haut de `.site-footer__inner`, dont le bord haut
       porte le rail de `.fcloud`.

   ⚠️ L'amorce s'enveloppe autour du titre et du chapô, jamais autour de
   `.phead` : ce bloc est PLEINE LARGEUR (c'est la zone d'écoute du sillage) et
   les verticales tomberaient aux bords de l'écran au lieu du bord de contenu.
   ⚠️ `.psortie` n'a aucun contenu et c'est assumé : entre le dernier projet et
   le footer il n'y a rien à écrire, seulement une continuité à tenir. */
.ptitle {
  --tl-haut: calc(var(--section-py) + var(--ph-pt));
  --tl-bas: var(--ph-pb);
}
.psortie {
  --tl-haut: 0px;
  --tl-bas: calc(var(--section-py) + var(--space-8));
}

/* ---- Lignes de construction de l'en-tête ----
   Même vocabulaire que l'épure du fond de la section Résultats
   (`.results__rules`) : des barres inclinées, pas un SVG, pas un filtre.
   Deux différences, voulues : l'opacité est ici très faible (c'est un fond de
   page claire, pas un aplat graphite), et elles s'effacent vers le bas comme
   le sillage — rien ne doit toucher la ligne des filtres. */
/* ⚠️ 14 traits, et le départ va chercher DEUX traits de plus que le cadre
   (un à gauche, un à droite — demande du 06/08/2026) : la rangée traverse
   maintenant toute la largeur au lieu de s'arrêter avant les bords.
   Le bloc étant passé pleine largeur, ces pourcentages se résolvent contre la
   fenêtre entière et non plus contre la colonne de contenu : ne pas les
   reprendre d'une ancienne version, l'espacement ne serait plus le même. */
.phead__rules {
  --ph-depart: -78%;
  --ph-pas: 18%;
  --ph-angle: 30deg;
  --ph-haut: 210%;
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: clip;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0 54%, transparent 96%);
  mask-image: linear-gradient(to bottom, #000 0 54%, transparent 96%);
}
.phead__rules i {
  position: absolute;
  bottom: 0;
  left: calc(var(--ph-depart) + var(--i) * var(--ph-pas));
  width: 2px;
  height: var(--ph-haut);
  background: var(--bump-forest);
  opacity: 0.06;
  transform-origin: 50% 100%;
  rotate: var(--ph-angle);
}
@media (prefers-reduced-motion: no-preference) {
  /* Le tracé, comme dans Résultats : les traits poussent depuis leur pied,
     l'un après l'autre, quand le bloc entre dans le champ. */
  .phead__rules i {
    scale: 1 0;
    transition: scale 1.15s var(--ease-soft);
    transition-delay: calc(var(--i) * 90ms);
  }
  .phead.is-in .phead__rules i { scale: 1 1; }
}

/* ---- Le sillage d'images qui suit la souris dans l'en-tête ----
   Couche de dessin seulement : elle ne capte aucun événement, c'est la
   section qui écoute. `overflow: clip` et non `hidden` — `hidden` en ferait
   un conteneur de défilement (piège transverse du site). */
.ptrail {
  position: absolute;
  /* Débordement volontaire EN HAUT et sur les CÔTÉS : le découpage doit
     tomber hors du champ, sinon une vignette semée près du bord se coupe net
     au milieu de l'écran. `overflow-x` du body est déjà masqué, l'élargissement
     latéral ne crée donc aucune barre de défilement.
     🚨 En BAS, la couche s'arrête un peu avant le bord du bloc — assez pour
     que les vignettes ne touchent pas les filtres, pas plus.
     ⚠️ Elle s'arrêtait bien plus haut (jusqu'à 176px) : la zone de jeu était
     amputée d'un tiers et on ne pouvait plus dessiner sous le titre. Le
     client a corrigé le tir le 06/08/2026 — « ce n'est pas ça que je voulais
     dire, en les éloignant des filtres ». L'écart utile est donc réduit ici
     et compensé par le `padding-bottom` de `.phead`, qui, lui, est du vide
     où l'on peut jouer.

     ⚠️ Aucun masque. Le sillage s'effaçait en dégradé vers le bas : c'était un
     pansement, les images arrivaient trop près et on les faisait disparaître.
     Le dégradé n'appartient qu'aux lignes de construction. Le script cesse de
     semer avant le bas de cette couche, donc `overflow: clip` ne coupe jamais
     rien en pratique — il n'est qu'un garde-fou. */
  inset: -48px 0 clamp(40px, 4vw, 64px);
  overflow: clip;
  pointer-events: none;
  /* 🚨 Le sillage passe AU-DESSUS de la typographie (demande du 06/08/2026) :
     c'est ce qui en fait un serpent qui traverse le titre, et non une
     décoration posée derrière. */
  z-index: 2;
}
.phead > *:not(.ptrail):not(.phead__rules) { position: relative; z-index: 1; }
.ptrail img {
  position: absolute;
  width: clamp(104px, 10.5vw, 168px);
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  will-change: transform, opacity;
  /* Courbe rebondissante : la vignette dépasse sa taille puis se pose.
     C'est le « un peu rebondissant » demandé. Durée courte : la file doit
     défiler vite, pas s'accumuler à l'écran. */
  animation: ptrail 780ms cubic-bezier(0.34, 1.42, 0.64, 1) forwards;
}
@keyframes ptrail {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.42) rotate(var(--r, 0deg)); }
  22%  { opacity: 1; transform: translate(-50%, -50%) scale(1.05) rotate(var(--r, 0deg)); }
  40%  { transform: translate(-50%, -50%) scale(1) rotate(var(--r, 0deg)); }
  62%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -56%) scale(0.9) rotate(var(--r, 0deg)); }
}
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
  .ptrail { display: none; }
  /* Sans sillage, le grand vide sous le titre n'a plus de raison d'être :
     c'était son terrain de jeu. */
  .phead { padding-bottom: clamp(48px, 9vw, 96px); }
}

/* ---- Les lignes de construction prennent toute la place sur mobile (03/09/2026) ----
   Retour client : « ça prend les lignes un peu plus de place sur mobile,
   sans bordure blanche entre les lignes et la nav en haut et ce qu'il y a en
   bas. » Deux zones blanches, deux causes différentes :

   1. EN HAUT : `.phead__rules` est en `inset: 0`, donc calée sur la boîte de
      `.phead` — mais `.phead` est précédé du `padding-top: var(--section-py)`
      de la `<section>` qui l'enveloppe (96px à ce palier). Les lignes ne
      touchent donc jamais la nav, elles s'arrêtent 96px plus bas.
   2. EN BAS : le masque (`.phead__rules`, plus haut) efface les lignes dès
      54% de la hauteur de `.phead`. Sur desktop c'est un terrain de jeu de
      plusieurs centaines de pixels, la coupure ne se voit pas ; sur mobile
      `.phead` ne fait plus que ~160px de haut, et près de la moitié de ce
      qui reste après le titre est déjà invisible avant même d'atteindre les
      filtres, alors qu'il n'y a structurellement AUCUN vide entre `.phead`
      et `.pboard` (mesuré : 0,0px).

   🚨 On ne touche PAS à `--section-py` lui-même : `--tl-haut` de `.ptitle`
   (plus haut) s'appuie dessus pour caler l'amorce de la trame, et le
   désynchroniser romprait ce calcul. On étend seulement la BOÎTE DÉCORATIVE
   de `.phead__rules` vers le haut — un calque purement visuel, sans incidence
   sur la mise en page — et on repousse son fondu vers le bas. Le nav opaque
   (`.site-header--solide`) couvre de toute façon le débord du haut : aucun
   risque de voir les lignes déborder sur la barre. */
@media (max-width: 760px) {
  .phead__rules {
    top: calc(-1 * var(--section-py));
    -webkit-mask-image: linear-gradient(to bottom, #000 0 92%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0 92%, transparent 100%);
  }
}

/* ---- Arc de trois miniatures au-dessus du titre, sur mobile (03/09/2026) ----
   Remplace visuellement le sillage (`.ptrail`), qui ne joue jamais sur un
   pointeur tactile (voir @media hover:none plus haut) : sans lui, l'en-tête
   de la page projets reste entièrement vide sur mobile. Demande client :
   « des petits carrés comme dans le footer, en vert et en papyrus, formant
   un petit arc de cercle au-dessus de 'projet' », avec trois miniatures de
   projets choisies dans le CMS.

   🚨 Construit en JavaScript (`construitArc()` dans projets.html) : masqué
   ici en CSS pur sur desktop, où le sillage reprend la main. */
.parc { display: none; }
@media (max-width: 760px) {
  .parc {
    display: flex;
    justify-content: center;
    align-items: flex-end;
    gap: clamp(10px, 3vw, 16px);
    /* ⚠️ Marge basse remontée le 05/09/2026 (« les images un poil plus haut,
       donc un poil plus de marge avec le titre mais pas beaucoup ») : +~55 %,
       pas davantage. Au-delà, l'arc se décolle du titre et cesse de se lire
       comme SON ornement — c'est un bloc titre, pas deux éléments voisins. */
    margin: 0 0 clamp(26px, 6vw, 38px);
    /* 🚨 HAUTEUR RÉSERVÉE DÈS LE PREMIER RENDU. Le conteneur `.parc` est vide
       dans le HTML et rempli par `construitArc()` : sans cette réserve, son
       remplissage pousse le <h1> de 67 px et tout le document derrière lui —
       c'était 0,489 de CLS sur mobile, la pire valeur du site (mesuré le
       07/09/2026).
       ⚠️ La valeur DOIT rester identique à la largeur de `.parc__item` juste
       en dessous, qui est en `aspect-ratio: 1` : c'est donc sa hauteur. Les
       `transform` des trois items (l'arc lui-même) ne comptent pas dans la
       mise en page et n'ont pas à être ajoutés ici. Si l'une des deux bouge,
       l'autre suit. */
    min-height: clamp(52px, 15vw, 68px);
  }
  .parc[hidden] { display: none; }
  .parc__item {
    position: relative;
    display: block;
    width: clamp(52px, 15vw, 68px);
    aspect-ratio: 1;
    overflow: hidden;
    flex: none;
  }
  /* 🚨 L'ARC SE DESSINE PAR nth-child, PAS PAR UNE CLASSE POSÉE EN JS : il y a
     toujours trois cases (voir construitArc()), la du milieu remonte et les
     deux côtés redescendent légèrement inclinés — c'est ce décalage qui lit
     comme un arc plutôt qu'une rangée droite. */
  .parc__item:nth-child(1) { transform: translateY(6px) rotate(-4deg); }
  .parc__item:nth-child(2) { transform: translateY(-10px); }
  .parc__item:nth-child(3) { transform: translateY(6px) rotate(4deg); }
  .parc__item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    filter: grayscale(1);
  }
  /* Le même duo de teintes que la charte : forêt et papyrus, jamais une
     troisième couleur (contrairement aux tuiles du footer, qui cyclent sur
     trois fonds). `mix-blend-mode: color` teinte l'image en niveaux de gris
     sans effacer ses valeurs, comme un duotone. */
  .parc__item::after {
    content: "";
    position: absolute;
    inset: 0;
    mix-blend-mode: color;
  }
  .parc__item:nth-child(odd)::after { background: var(--bump-forest); }
  .parc__item:nth-child(even)::after { background: var(--bump-sage); }
}

/* Une colonne de libellé de 104px n'a pas sa place sur un écran étroit : le
   libellé repasse au-dessus de ses pilules. */
@media (max-width: 640px) {
  .filters-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
  }
  .filters__label { padding-top: 0; }
}

/* ---- Bulle de comptage sur les filtres ---- */
.filter-btn { position: relative; }
.filter-bulle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  height: 17px;
  margin-left: 8px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--border-subtle);
  font-size: 9.5px;
  font-weight: var(--weight-medium);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  vertical-align: 1px;
}
.filter-btn.is-active .filter-bulle { background: rgba(243, 244, 239, 0.24); }
/* Un filtre sans résultat reste cliquable (on doit pouvoir revenir dessus)
   mais s'efface : il annonce lui-même qu'il ne donnera rien. */
.filter-btn.is-vide { opacity: 0.42; }

/* 🚨 LA PLANCHE DE PROJETS (12/08/2026) — une planche de trame, comme
   `.lcloud` et le footer.

   Demande client : « rajouter cette espèce de layout qu'on réutilise dans le
   footer, avec ces croix et ces quadrillages, sur la page projet… les mettre
   en coin et tout ça, et j'aimerais qu'ils soient tous fermés, et c'est au
   hover que la page devient plus grande et fait apparaître l'image. »

   🚨 LA PILE COLLANTE A ÉTÉ SUPPRIMÉE. Les en-têtes étaient `position: sticky`
   à des crans échelonnés (`--t`), une profondeur `--d` posée image par image
   par `empile()` les faisant glisser d'un cran à chaque nouveau projet. Ça n'a
   plus d'objet : une ligne fermée fait la hauteur d'un cran, il n'y a plus de
   visuel à dépasser, donc plus rien à empiler. Sont partis avec : `--t`, `--d`,
   `--pile-h`, `z-index: calc(12 - …)`, l'opacité calculée, le `display:
   contents` de `.pitem` et les ~115 lignes de `empile()` dans `projets.html`.
   Ne pas les réintroduire : c'est le scroll qui pilotait, c'est le survol
   maintenant.

   🚨 `.pitem` REDEVIENT UNE VRAIE BOÎTE, et c'est indispensable : elle n'était
   en `display: contents` que pour faire de `.plist` le bloc conteneur des
   en-têtes collants. Or une cellule sans boîte n'a ni fond, ni bordure, ni
   pseudo-élément — donc aucune croix.

   La planche est `.lcloud` écrite pour UNE SEULE COLONNE : rails haut et bas en
   pleine fenêtre, les deux verticales en bordures, un séparateur par cellule.
   Elle consomme les mêmes jetons `--trame-ligne` / `--trame-croix` /
   `--trame-bras` : si l'encre change, elle change des trois côtés. NE PAS y
   remettre de valeur locale.

   🚨 `margin-inline: calc(-1 * var(--trame-air))`, comme `.lcloud` et
   `.fcloud` : c'est ce qui met ses verticales à l'aplomb de celles de toute la
   page, et de celles du footer juste en dessous. Si l'un des trois bouge, les
   autres suivent. */
.pboard {
  /* Hauteur d'une ligne FERMÉE. Bornée par la hauteur d'écran autant que par la
     largeur : sur un portable 720p, des lignes pleine taille ne laisseraient
     plus la place au visuel qui s'ouvre en dessous. */
  --pj-h: clamp(62px, min(6vw, 9svh), 96px);
  /* Demi-bras de croix, corrigé du pixel du filet : c'est lui qui centre la
     croix SUR la ligne et non à côté. Même calcul que `.trame-cadre`. */
  --pj-demi: calc((var(--trame-bras) - 1px) / 2);

  margin-inline: calc(-1 * var(--trame-air));
  position: relative;
  /* Contexte d'empilement : les deux rails passent en `z-index: -1` — donc
     derrière les cellules, qui ont un fond opaque — sans qu'on ait à toucher au
     z-index de la planche elle-même. */
  isolation: isolate;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border-top: 1px solid var(--trame-ligne);
  border-left: 1px solid var(--trame-ligne);
  border-right: 1px solid var(--trame-ligne);
}

/* Les deux rails, d'un bord à l'autre de la FENÊTRE.
   ⚠️ `left: 50%` + `width: 100vw` + `translateX(-50%)`, JAMAIS
   `left: -50vw; right: -50vw` : la seconde forme fait un calque de 100vw DE
   PLUS que la planche et porte le débordement horizontal du document à
   plusieurs centaines de pixels (mesuré sur `.lcloud`).
   Les séparateurs intérieurs, eux, ne traversent pas l'écran : ce sont les
   bordures des cellules, elles s'arrêtent au bord de la planche. */
.pboard::before,
.pboard::after {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  border-top: 1px solid var(--trame-ligne);
  pointer-events: none;
}
.pboard::before { top: -1px; }
.pboard::after { bottom: 0; }

/* `.plist` ne fait plus que recevoir les lignes injectées par le script : ce
   sont ELLES qui doivent être les cellules de la grille, pas leur conteneur. */
.plist { display: contents; }

.pcell {
  position: relative;
  background: var(--surface-page);
  border-bottom: 1px solid var(--trame-ligne);
  /* Ramène le contenu sur le bord de CONTENU. La planche déborde de
     `--trame-air` ; sans ce padding, le texte démarrerait à l'aplomb de la
     verticale et non à celui du grand titre au-dessus. */
  padding-inline: var(--trame-air);
}

/* ---- Les croix, en CALQUES EN BANDE ----
   🚨 Une cellule n'a que deux pseudo-éléments et il faut DEUX croix par
   frontière, une par verticale : chaque bande porte donc une croix à chacun de
   ses bouts. Même parti que la planche des partenaires, et pour la même raison.

   Répartition : la cellule des filtres porte la bande du HAUT (`::before`) et
   la sienne du bas (`::after`) ; chaque ligne de projet ne porte que sa bande
   du bas. Compte : n+2 bandes pour n+2 frontières, et aucune n'est dessinée
   deux fois — deux couches d'une encre translucide feraient une croix plus
   foncée que ses voisines.

   🚨 LA BANDE DÉBORDE DE LA CELLULE D'UN DEMI-BRAS SUR SES QUATRE CÔTÉS, et les
   lignes sont rentrées d'autant. Calée exactement sur la cellule, la moitié
   extérieure de chaque bras serait DÉCOUPÉE (`background-clip` vaut
   `border-box` : une couche qui dépasse n'est pas peinte) et on obtiendrait des
   équerres au lieu de croix — le défaut déjà rencontré sur `.trame-cadre`. */
.pcell::before,
.pcell::after {
  content: "";
  position: absolute;
  /* Au-dessus des cellules voisines : la bande déborde d'un demi-bras dans
     celle du dessous, dont le fond est opaque et la recouvrirait. */
  z-index: 2;
  left: calc(-1px - var(--pj-demi));
  right: calc(-1px - var(--pj-demi));
  height: var(--trame-bras);
  pointer-events: none;

  background-repeat: no-repeat;
  background-image:
    /* les 2 bras horizontaux */
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0),
    /* les 2 bras verticaux */
    linear-gradient(var(--trame-croix) 0 0),
    linear-gradient(var(--trame-croix) 0 0);
  background-size:
    var(--trame-bras) 1px, var(--trame-bras) 1px,
    1px var(--trame-bras), 1px var(--trame-bras);
  background-position:
    left 0 center, right 0 center,
    left var(--pj-demi) top 0, right var(--pj-demi) top 0;
}
/* Bande BASSE : sur la frontière que dessine la bordure de la cellule. */
.pcell::after { bottom: calc(-1px - var(--pj-demi)); }
/* Bande HAUTE : sur le rail du haut de la planche. Portée par la SEULE première
   cellule — toutes les autres frontières sont déjà couvertes par une bande
   basse. */
.pcell::before { top: calc(-1px - var(--pj-demi)); display: none; }
.pboard > .pcell:first-child::before { display: block; }

/* ---- La cellule des filtres : la tête de la planche ----
   🚨 Les filtres sont DANS la planche et non dans un `.trame-cadre` séparé :
   entre les deux il aurait fallu un raccord traitillé sans contenu, et un
   élément qui n'existe que pour porter une ligne est un passif. Ici le rail bas
   des filtres EST le rail haut du premier projet. */
.pfilters { padding-block: clamp(4px, 0.9vw, 12px); }
.pfilters .filters-block { margin-bottom: 0; }

/* ---- Le panneau de filtres du mobile (05/09/2026) ----
   Demande client : « pour que ça prenne moins de place et qu'on arrive
   directement sur les projets sur mobile, est-ce qu'on ferait par un bouton
   "filtre" et une popup s'ouvre avec des filtres à ajuster ? Le même filtre
   qu'il y a déjà et toujours avec ce style avec les lignes et croix. »

   Les deux rangées de pilules mesuraient ~340px de haut à 390px de large,
   soit près de la moitié de l'écran avant le premier projet.

   🚨 UNE SEULE SOURCE DE VÉRITÉ, JAMAIS UNE COPIE. Ce sont les MÊMES boutons
   aux deux tailles : sur desktop les trois boîtes du panneau sont en
   `display: contents`, donc elles n'existent pas et `.filters-block` reste
   l'enfant direct de la cellule, exactement comme avant. Sur mobile seulement,
   elles reprennent corps et deviennent le panneau. Les écouteurs délégués de
   `bindFilters()` vivent sur `#filtersSecteur`/`#filtersPrestation`, qui ne
   bougent jamais du DOM : rien à rebrancher, aucun risque de divergence. */
.pfilters__btn { display: none; }
.pfsheet,
.pfsheet__panel,
.pfsheet__corps { display: contents; }
.pfsheet__scrim,
.pfsheet__bar,
.pfsheet__pied { display: none; }

@media (max-width: 760px) {
  /* ---- Le bouton qui remplace les rangées ---- */
  .pfilters__btn {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 13px 4px;
    background: none;
    border: 0;
    cursor: pointer;
    text-align: left;
    color: var(--text-body);
    font-family: inherit;
    font-size: 0.8125rem;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
  }
  .pfilters__btn-ico { display: flex; flex: none; color: var(--bump-forest); }
  .pfilters__btn-ico svg { width: 18px; height: 18px; }
  .pfilters__btn-lbl { flex: none; }
  /* L'état courant, à droite : sans lui le bouton ne dit pas ce qu'il filtre,
     et un filtre actif deviendrait invisible une fois le panneau refermé.
     `--weight-medium` vaut 400 comme tout le reste (voir « Typographie ») :
     c'est la COULEUR qui marque l'état actif, jamais la graisse. */
  .pfilters__btn-etat {
    margin-left: auto;
    text-align: right;
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.8125rem;
    color: var(--text-muted);
  }
  .pfilters__btn.is-actif .pfilters__btn-etat { color: var(--bump-forest); }
  .pfilters__btn:active { transform: scale(0.97); }

  /* ---- Le panneau ---- */
  /* 🚨 IL FAUT ÉLEVER `.pboard` LUI-MÊME, PAS SEULEMENT LE PANNEAU.
     `.pboard` porte `isolation: isolate` (indispensable : ses deux rails
     passent en `z-index: -1` derrière les cellules) — c'est donc un contexte
     d'empilement, et le `z-index: 780` du panneau n'y vaut que DANS la
     planche. Or `.phead > .shell` porte `z-index: 1` au niveau racine (pour
     que le sillage passe au-dessus de la typographie) : le panneau se
     peignait donc SOUS le grand titre et sous l'arc de miniatures, tous deux
     visibles au travers (constaté à la capture, alors que la géométrie était
     déjà juste).
     La classe est posée par le script à l'ouverture et retirée seulement
     APRÈS la sortie, sinon le panneau repasserait derrière le titre pendant
     qu'il redescend.
     ⚠️ 790 : au-dessus du header (50) et du `.shell` de l'en-tête (1), mais
     SOUS `.pdetail` (800) et la visionneuse (850). Les trois ne sont jamais
     ouverts ensemble, mais si ça devait arriver, c'est le détail du projet
     qui doit passer devant, pas les filtres. */
  .pboard.is-filtres { z-index: 790; }
  .pfsheet {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 780;
    visibility: hidden;
  }
  .pfsheet.is-live { visibility: visible; }
  .pfsheet__scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(16, 26, 19, 0.55);
    opacity: 0;
    transition: opacity 340ms var(--ease-out);
  }
  .pfsheet.is-open .pfsheet__scrim { opacity: 1; }

  .pfsheet__panel {
    /* Jetons de la trame, LOCAUX au panneau : `--pfs-vx` place les deux
       verticales par rapport au bord du panneau, et le corps se cale dessus.
       🚨 Les deux doivent rester d'accord — les croix des rangées sont
       posées aux coins du corps, donc EXACTEMENT sur ces verticales. */
    --pfs-vx: 16px;
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 0; right: 0; bottom: 0;
    max-height: 86svh;
    background: var(--surface-page);
    border-radius: var(--radius-stack) var(--radius-stack) 0 0;
    box-shadow: 0 -16px 40px -28px rgba(0, 0, 0, 0.45);
    /* `clip` et jamais `hidden` : piège transverse du site, `hidden` en
       ferait un conteneur de défilement. */
    overflow: clip;
    transform: translateY(100%);
    /* Durée de SORTIE ; l'entrée la surcharge juste après — une transition
       lit sa durée dans le style d'ARRIVÉE. Même mécanique que `.pdetail`. */
    transition: transform 320ms var(--ease-drawer);
  }
  .pfsheet.is-open .pfsheet__panel {
    transform: translateY(0);
    transition-duration: 480ms;
  }

  .pfsheet__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex: none;
    padding: 16px var(--pfs-vx) 14px;
  }
  .pfsheet__titre {
    font-size: 0.75rem;
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .pfsheet__close {
    display: flex;
    align-items: center;
    justify-content: center;
    /* 40px : la cible tactile minimale déjà retenue partout ailleurs. */
    width: 40px; height: 40px;
    margin-right: -8px;
    background: none;
    border: 0;
    cursor: pointer;
    color: var(--text-body);
  }
  .pfsheet__close svg { width: 20px; height: 20px; }
  .pfsheet__close:active { transform: scale(0.97); }

  /* ---- Le corps : la trame, lignes ET croix ----
     🚨 Demande explicite du client (« toujours avec ce style avec les lignes
     et croix »). Mêmes jetons que `.lcloud`, `.trame-cadre`, le footer et la
     planche `.pboard` : si l'encre change, elle change des cinq côtés.
     Ne pas y remettre de valeur locale. */
  .pfsheet__corps {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Le padding met le bord du CONTENU exactement à l'aplomb des verticales,
       qui sont peintes ici en fond. Les rangées portent ensuite leur propre
       retrait pour ne pas coller aux lignes (règle transverse : « les
       verticales ne touchent PAS le contenu »). */
    padding-inline: var(--pfs-vx);
    background-image:
      linear-gradient(var(--trame-ligne) 0 0),
      linear-gradient(var(--trame-ligne) 0 0);
    background-size: 1px 100%, 1px 100%;
    background-position: var(--pfs-vx) 0, calc(100% - var(--pfs-vx)) 0;
    background-repeat: no-repeat;
  }
  /* 🚨 `.filters-block` porte `.reveal` : dans un panneau fixe et translaté
     hors écran, l'IntersectionObserver ne se déclenche JAMAIS, et le bloc
     resterait à `opacity: 0` une fois le panneau ouvert. On neutralise donc
     l'effet ici. Spécificité (0,2,0), la même que `.reveal.is-in` : celle-ci
     est déclarée plus bas dans le fichier et pose les mêmes valeurs, il n'y a
     pas de conflit. */
  .pfsheet .filters-block {
    opacity: 1;
    transform: none;
    margin-bottom: 0;
  }
  .pfsheet .filters-row {
    position: relative;
    /* Une colonne : le libellé passe AU-DESSUS de ses pilules, comme au
       palier 640px déjà en place plus bas dans ce fichier. */
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;
    padding: 16px;
    /* Le rail : il court d'une verticale à l'autre, puisque la boîte de la
       rangée est exactement la boîte de contenu du corps. */
    border-top: 1px solid var(--trame-ligne);
  }
  .pfsheet .filters-block { border-bottom: 1px solid var(--trame-ligne); }
  .pfsheet .filters__label { padding-top: 0; }

  /* Les croix, aux quatre coins où un rail rencontre une verticale. Même
     dessin que partout ailleurs : deux traits d'1px, aucune icône importée.
     Elles débordent d'un demi-bras de part et d'autre, donc `overflow` doit
     rester visible sur la rangée (il l'est). */
  .pfsheet .filters-row::before,
  .pfsheet .filters-row::after,
  .pfsheet .filters-block::before,
  .pfsheet .filters-block::after {
    content: "";
    position: absolute;
    width: var(--trame-bras);
    height: var(--trame-bras);
    background:
      linear-gradient(var(--trame-croix) 0 0) center / 100% 1px no-repeat,
      linear-gradient(var(--trame-croix) 0 0) center / 1px 100% no-repeat;
    pointer-events: none;
  }
  .pfsheet .filters-row::before,
  .pfsheet .filters-row::after { top: calc(-0.5 * var(--trame-bras)); }
  .pfsheet .filters-row::before { left: calc(-0.5 * var(--trame-bras)); }
  .pfsheet .filters-row::after { right: calc(-0.5 * var(--trame-bras)); }
  /* Les deux croix du rail du bas, portées par le bloc (les pseudos des
     rangées sont déjà pris par celles du haut). */
  .pfsheet .filters-block { position: relative; }
  .pfsheet .filters-block::before,
  .pfsheet .filters-block::after { bottom: calc(-0.5 * var(--trame-bras)); }
  .pfsheet .filters-block::before { left: calc(-0.5 * var(--trame-bras)); }
  .pfsheet .filters-block::after { right: calc(-0.5 * var(--trame-bras)); }

  .pfsheet__pied {
    display: block;
    flex: none;
    padding: 16px var(--pfs-vx) calc(16px + env(safe-area-inset-bottom));
  }
  .pfsheet__pied .btn { width: 100%; justify-content: center; }
}

/* ---- La ligne ----
   Hauteur libre : c'est le dépliage qui la fait varier. L'en-tête, lui, garde
   la sienne — c'est ce qui fait que le haut de la ligne survolée NE BOUGE
   JAMAIS (voir `deplie()` dans `projets.html`). */
.pitem__head {
  height: var(--pj-h);
  display: grid;
  grid-template-columns: auto minmax(0, 4fr) minmax(0, 7fr) auto;
  align-items: center;
  gap: clamp(16px, 2.6vw, 44px);
  color: inherit;
  text-decoration: none;
}
.pitem__num {
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  transition: opacity 420ms var(--ease-out);
}
/* Les deux blocs de texte tiennent sur une ligne, coupée au besoin : la hauteur
   de l'en-tête est la trame de la planche, elle ne doit pas varier avec la
   longueur d'un titre. */
.pitem__client,
.pitem__titre {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transform-origin: left center;
  /* 🚨 État piloté par `scale` (la propriété), jamais par `transform` :
     `transform` appartient au survol de la flèche. */
  transition: scale 520ms var(--ease-soft), opacity 420ms var(--ease-out),
              color 420ms var(--ease-out);
}
.pitem__client {
  font-size: clamp(21px, 2.5vw, 36px);
  font-weight: var(--weight-medium);
  line-height: 1.12;
  letter-spacing: var(--tracking-tight);
}
/* 🚨 DÉROGATION ASSUMÉE À LA RÈGLE « ne jamais poser de font-family ni de
   font-weight sur un sélecteur qui cible un h1-h6 » (voir CLAUDE.md,
   « Typographie »). Elle est ici DÉLIBÉRÉE et validée par le client le
   07/09/2026, pas un résidu de l'ancienne charte comme ceux nettoyés le
   14/08/2026.

   Pourquoi : ces titres étaient des <span>, donc `projets.html` n'avait pour
   Google qu'un <h1> et aucune structure. Ils sont passés en <h2> pour le
   référencement, mais l'arbitrage explicite était « apparence strictement
   inchangée » — or la règle globale h1-h6 les aurait passés en display Thin
   200 MAJUSCULES, ce qui n'est pas le rendu voulu pour une liste.

   ⚠️ Les sept déclarations ci-dessous neutralisent une à une CELLES de la règle
   globale (font-family, font-weight, text-transform, line-height,
   letter-spacing, margin, text-wrap). Si cette règle globale gagne une
   propriété, il faut la neutraliser ici aussi, sinon l'apparence de la liste se
   met à dériver.
   ⚠️ `text-wrap` compte vraiment, malgré le `white-space: nowrap` juste
   au-dessus : au palier mobile, `.pitem__titre` repasse en `white-space:
   normal` (voir plus bas), le titre s'enroule alors sur plusieurs lignes et le
   `balance` de la règle globale en changerait les coupures. */
.pitem__titre {
  font-family: var(--font-sans);
  font-weight: var(--weight-light);
  text-transform: none;
  letter-spacing: normal;
  text-wrap: wrap;
  margin: 0;
  font-size: clamp(18px, 2.1vw, 31px);
  line-height: 1.18;
  color: var(--gray-700);
}

/* 🚨 UNE LIGNE FERMÉE RESTE PLEINEMENT LISIBLE. Les anciennes valeurs
   (`opacity: .42` sur le titre, `scale: .84` sur le client) décrivaient un
   bandeau qui avait RECULÉ dans la pile, c'est-à-dire un état de passage.
   Fermé est maintenant l'état NORMAL de toutes les lignes au chargement : y
   laisser un texte à demi effacé rendrait la page illisible d'entrée.
   Seule la flèche est en retrait — c'est elle qui s'allume à l'ouverture, et
   elle seule dit « il y a quelque chose ici ». */
/* ⚠️ 0,6 et pas moins : c'est la SEULE affordance qui dit que la ligne mène
   quelque part. Plus bas (essayé à 0,5), le disque forêt vire au gris franc et
   se lit comme un bouton désactivé. */
.pitem:not(.is-open) .pitem__go { scale: 0.86; opacity: 0.6; }
.pitem.is-open .pitem__client { scale: 1.03; }
.pitem.is-open .pitem__titre { color: var(--text-body); }
.pitem__go {
  flex: none;
  width: 40px; height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--bump-forest);
  color: var(--bump-paper);
  transform-origin: right center;
  transition: transform 260ms var(--ease-soft), background 200ms var(--ease-out),
              scale 520ms var(--ease-soft), opacity 420ms var(--ease-out);
}
.pitem__go svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
@media (hover: hover) and (pointer: fine) {
  .pitem__head:hover .pitem__go { transform: translate(3px, -3px); }
}
.pitem__head:active .pitem__go { transform: scale(0.92); }

/* ---- Le dépliage ----
   🚨 `grid-template-rows: 0fr → 1fr`, et surtout PAS une hauteur mesurée en
   JavaScript : c'est animable en CSS pur, donc il n'y a rien à relire à chaque
   survol et rien qui se dérègle si le contenu change.
   ⚠️ `min-height: 0` sur l'intérieur est indispensable — sans lui l'élément de
   grille refuse de descendre sous sa taille de contenu et la ligne reste
   ouverte. Et `overflow: clip`, JAMAIS `hidden` : `hidden` en ferait un
   conteneur de défilement (piège transverse du site). */
.pitem__vis {
  display: grid;
  grid-template-rows: 0fr;
  /* Sortie plus rapide que l'entrée — règle transverse. La durée d'entrée est
     posée par la règle d'état ci-dessous. */
  transition: grid-template-rows 360ms var(--ease-drawer);
}
.pitem.is-open .pitem__vis {
  grid-template-rows: 1fr;
  transition-duration: 520ms;
}
.pitem__visin { min-height: 0; overflow: clip; }

/* Le cadre porte le rayon et le découpage ; l'image se relâche à l'intérieur.
   ⚠️ Les deux ne peuvent pas être le même élément : une image mise à l'échelle
   déborde de son cadre, et c'est le cadre qui doit garder les coins arrondis
   pendant tout le mouvement. */
.pitem__cadre {
  border-radius: var(--radius-card);
  overflow: clip;
  margin: clamp(4px, 0.8vw, 12px) 0 clamp(16px, 2.2vw, 28px);
}
.pitem__cadre img {
  width: 100%;
  /* 🚨 16/9 et non plus 2,6/1 (14/08/2026). Le bandeau très large recadrait
     beaucoup plus que la brique 4/3 de la home : la même photo n'y montrait pas
     la même chose, et l'écart se voyait en passant d'une page à l'autre. 16/9
     est l'entre-deux — assez large pour une ligne pleine largeur, assez haut
     pour que le cadrage reste reconnaissable.
     ⚠️ Ce qui reste hors du cadre se règle projet par projet dans le CMS
     (« Cadrage de la miniature ») : c'est `object-position`, voir plus bas. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  opacity: 0;
  /* 🚨 `scale` en PROPRIÉTÉ et non en `transform` : `transform` reste
     disponible pour un survol, comme partout ailleurs sur le site. */
  scale: 1.06;
  transition: opacity 380ms var(--ease-out), scale 760ms var(--ease-soft);
}
.pitem.is-open .pitem__cadre img { opacity: 1; scale: 1; }

/* Aucun projet ne correspond aux filtres : le message occupe une cellule
   entière, sinon la planche se refermerait sur un vide de 1 px. */
.pboard .projects-empty {
  padding-block: clamp(28px, 5vw, 56px);
  color: var(--text-muted);
}

@media (max-width: 760px) {
  /* Deux rangées : index + client, puis le titre. La hauteur d'un cran devient
     un PLANCHER — un titre qui s'enroule doit pouvoir pousser la ligne. */
  .pitem__head {
    height: auto;
    min-height: var(--pj-h);
    padding-block: clamp(14px, 3vw, 22px);
    grid-template-columns: auto minmax(0, 1fr) auto;
    row-gap: 4px;
    align-items: start;
  }
  .pitem__num { grid-row: 1; }
  .pitem__client { grid-row: 1; }
  .pitem__go { grid-row: 1 / span 2; align-self: center; }
  .pitem__titre { grid-column: 2; white-space: normal; }
  /* Même rapport qu'en desktop : c'est tout l'objet du réglage, la photo doit
     montrer la même chose d'un écran à l'autre. */
  .pitem__cadre img { aspect-ratio: 16 / 9; }
}


/* ============================================================
   Détail d'un projet — le panneau qui monte du bas
   ------------------------------------------------------------
   Remplace le dépliant en ligne (`.project-detail`, supprimé le 05/08/2026).
   Demande client : « que ça prenne presque le plein écran, que les éléments
   soient beaucoup mieux équilibrés comme une page projet d'habitude — pas
   toutes les images à droite et le texte à gauche », et « juste un fond forêt
   qui sort du bas de la page avec la page de détail dedans, en vert foncé ».

   🚨 Le dépliant précédent héritait de la largeur d'une rangée de la grille :
   c'est ce qui imposait deux colonnes texte | médias. Le panneau est
   maintenant DÉTACHÉ du flux, donc la mise en page peut être verticale et
   pleine largeur — titre, méta, couverture, texte, médias. Ne pas revenir à
   une insertion dans la grille.
   ============================================================ */
.pdetail {
  /* Largeur de la colonne de contenu — partagée par la barre de tête et le
     corps, c'est ce qui met le bouton Fermer à l'aplomb des médias. */
  --pd-maxw: 1400px;
  position: fixed;
  inset: 0;
  z-index: 800;
  visibility: hidden;
}
.pdetail.is-live { visibility: visible; }
.pdetail__scrim {
  position: absolute;
  inset: 0;
  background: rgba(16, 26, 19, 0.55);
  opacity: 0;
  transition: opacity 380ms var(--ease-out);
}
.pdetail.is-open .pdetail__scrim { opacity: 1; }

.pdetail__panel {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* « Presque » le plein écran : la bande laissée en haut montre le site
     dessous, c'est ce qui dit qu'on est dans un panneau et pas sur une page. */
  height: 94svh;
  display: flex;
  flex-direction: column;
  background: var(--surface-forest);
  color: var(--text-inverse);
  border-radius: var(--radius-stack) var(--radius-stack) 0 0;
  /* Ombre volontairement discrète : le panneau est déjà détaché par sa
     couleur et son rayon. Elle a été à `0 -30px 90px -46px / .8` — « un peu
     trop présente » (retour client du 05/08/2026). */
  box-shadow: 0 -16px 40px -28px rgba(0, 0, 0, 0.45);
  overflow: hidden;
  transform: translateY(100%);
  /* Durée de SORTIE. L'entrée la surcharge plus bas : une transition lit sa
     durée dans le style d'arrivée, donc 360ms en repli = sortie rapide. */
  transition: transform 360ms var(--ease-drawer);
}
.pdetail.is-open .pdetail__panel {
  transform: translateY(0);
  transition-duration: 560ms;
}
/* 🚨 `.pdetail__grip` supprimée le 02/09/2026 : une petite barre purement
   décorative (`pointer-events: none`, aucun tirer-pour-fermer implémenté)
   qui donnait l'impression d'être cliquable sans rien faire (retour client).
   Le bouton × et le clic sur le scrim ferment déjà le panneau — ne pas la
   recréer sans la rendre réellement fonctionnelle. */
/* Barre de tête : elle tient le bouton Fermer à l'aplomb du bord DROIT de la
   colonne de contenu.
   🚨 Le bouton était en `position: absolute` dans le coin du panneau : à 1440
   le rayon haut vaut 72px, donc il se posait en plein sur l'arrondi et
   paraissait « complètement en dehors » (retour client du 05/08/2026).
   Aligné sur le contenu, il est à ~190px du bord — largement hors de l'arc. */
.pdetail__bar {
  flex: none;
  width: 100%;
  max-width: var(--pd-maxw);
  margin: 0 auto;
  padding: 20px clamp(20px, 4vw, 56px) 0;
  display: flex;
  justify-content: flex-end;
}
.pdetail__close {
  width: 40px; height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(243, 244, 239, 0.08);
  border: 1px solid rgba(208, 211, 193, 0.34);
  border-radius: var(--radius-pill);
  font-family: var(--font-sans);
  font-size: 18px;
  line-height: 1;
  color: var(--bump-paper);
  cursor: pointer;
  transition: background 200ms var(--ease-out), border-color 200ms var(--ease-out),
              transform 160ms var(--ease-out);
}
.pdetail__close:hover { background: rgba(243, 244, 239, 0.18); border-color: var(--bump-sage); }
.pdetail__close:active { transform: scale(0.94); }

/* 🚨 Le corps du panneau ne défile PAS : c'est la colonne des médias qui a
   son propre ascenseur. Le rail d'information reste donc strictement
   immobile. Il a été `sticky` dans un conteneur unique : il bougeait au
   démarrage et se décalait en fin de course (retour client du 05/08/2026).
   Un élément collant se déplace toujours un peu — le seul moyen qu'il ne
   bouge jamais est qu'il ne soit pas dans le flux qui défile. */
.pdetail__body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

/* ---- Le contenu : rail d'information à gauche, flux de médias à droite ----
   Référence client : creativesupply.com/projects/mona (capture du 05/08/2026).
   Le rail est COLLANT : il reste lisible pendant qu'on parcourt les visuels,
   c'est ce qui fait la différence avec une simple colonne de texte.

   🚨 Ce n'est PAS le retour à l'ancien dépliant. Celui-ci coupait la largeur
   en deux (texte 45 % | médias 55 %) et le texte défilait avec les images.
   Ici le rail fait ~26 % et ne bouge pas : les visuels ont les trois quarts
   de la largeur, et l'information reste sous les yeux. */
.pd {
  --pd-rail: clamp(230px, 26%, 330px);
  height: 100%;
  max-width: var(--pd-maxw);
  margin: 0 auto;
  padding: 0 clamp(20px, 4vw, 56px);
  display: grid;
  grid-template-columns: var(--pd-rail) minmax(0, 1fr);
  gap: clamp(28px, 4vw, 64px);
}
.pd__rail {
  height: 100%;
  padding: clamp(10px, 1.6vw, 18px) 0 clamp(24px, 3vw, 40px);
  display: flex;
  flex-direction: column;
  gap: clamp(18px, 2vw, 26px);
  /* Filet de sécurité : si le rail est plus haut que le panneau (petit écran
     en paysage, texte très long), il a son propre ascenseur plutôt que d'être
     coupé. Dans le cas normal il ne défile pas du tout. */
  overflow-y: auto;
  overscroll-behavior: contain;
}
.pd__eyebrow {
  --pd-ico: 30px;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--sage-deep);
}
/* 🚨 Calé sur l'ENCRE du picto, pas sur sa boîte : le dessin n'occupe que
   ~48 % du carré du SVG, donc la rangée démarrait 8px à droite du titre.
   Même méthode que `.pcard__top` et `.bridge__sectors`.
   ⚠️ UNIQUEMENT quand il y a un picto. Le secteur « Autre » n'en a pas : la
   marge négative tirait alors le TEXTE hors du cadre et mangeait sa première
   lettre (« UTRE · VILLARS-SUR-GLÂNE » au lieu d'« AUTRE ») — constaté. */
.pd__eyebrow:has(img) { margin-left: calc(var(--pd-ico) * -0.26); }
.pd__eyebrow img { width: var(--pd-ico); height: var(--pd-ico); }
.pd__eyebrow span {
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}
.pd__title {
  margin: 0;
  font-size: clamp(28px, 2.9vw, 40px);
  line-height: 1.06;
  letter-spacing: var(--tracking-tight);
}

/* Les faits du projet, empilés dans le rail — comme la colonne « Industry /
   Location / Services » de la référence. */
.pd__meta {
  margin: 0;
  padding: clamp(16px, 1.8vw, 22px) 0;
  border-top: 1px solid rgba(208, 211, 193, 0.24);
  border-bottom: 1px solid rgba(208, 211, 193, 0.24);
  display: grid;
  gap: 14px;
}
.pd__meta > div > span {
  display: block;
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--sage-deep);
  margin-bottom: 5px;
}
.pd__meta > div > strong {
  font-size: 0.9375rem;
  font-weight: var(--weight-light);
  color: var(--bump-paper);
}
.pd__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}
.pd__chip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 8px 15px 8px 11px;
  border: 1px solid rgba(208, 211, 193, 0.34);
  border-radius: var(--radius-pill);
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bump-sage);
  opacity: 0;
  transform: translateY(6px);
  animation: pdChipIn 340ms var(--ease-out) forwards;
}
.pd__chip img { width: 22px; height: 22px; flex: none; }
@keyframes pdChipIn { to { opacity: 1; transform: none; } }

.pd__text {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: var(--weight-light);
  line-height: var(--leading-relaxed);
  color: rgba(243, 244, 239, 0.8);
}
.pd__challenge {
  font-size: 0.9375rem;
  font-weight: var(--weight-light);
  line-height: var(--leading-normal);
  color: rgba(243, 244, 239, 0.8);
}
.pd__challenge strong,
.pd__label {
  display: block;
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--sage-deep);
  margin-bottom: 8px;
}

/* Le flux de médias : c'est LUI qui défile, et lui seul. */
.pd__media {
  height: 100%;
  overflow-y: auto;
  /* Sans lui, arrivé en bas de la colonne, la molette continue sur la page. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: clamp(10px, 1.6vw, 18px) 0 clamp(50px, 7vw, 110px);
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 1.8vw, 22px);
}
/* 🚨 `flex: none` sur chaque bloc du flux — indispensable.
   La colonne a une hauteur définie (100 %) et son contenu la dépasse
   largement : sans ça, `flex-shrink: 1` écrase chaque bloc. Et le garde-fou
   habituel (`min-height: auto`) ne joue pas ici, parce qu'un élément qui a un
   `overflow` autre que `visible` — c'est le cas de tous nos cadres, qui
   découpent leurs coins arrondis — voit son minimum automatique ramené à 0.
   Résultat constaté : la couverture mesurait 513 × 0 px. */
.pd__media > * { flex: none; }
.pd__videos { display: flex; flex-direction: column; gap: clamp(14px, 1.8vw, 22px); }
.pd__video {
  border-radius: var(--radius-card);
  overflow: hidden;
  background: rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
}
.pd__video video { display: block; max-width: 100%; max-height: 74svh; margin: 0 auto; }

/* ---- Couverture : le format de l'image, jamais un cadre imposé ----
   🚨 Elle était en `16/10` + `object-fit: cover` : une photo verticale y
   perdait la moitié de sa hauteur (« sur le projet Riedo merchandising elles
   sont très croppées »). Le script mesure le rapport réel de chaque visuel et
   le pose en `--ar` : plus aucun recadrage nulle part dans le panneau.
   Le plafond de hauteur évite qu'un portrait 2:3 occupe deux écrans. */
.pd__cover {
  border-radius: var(--radius-card);
  overflow: hidden;
  background: rgba(0, 0, 0, 0.25);
  /* 🚨 Le plafond de hauteur se pose en LARGEUR, pas en `max-height`.
     Avec `max-height`, la boîte reste large, le rapport ne peut plus tenir et
     `object-fit: cover` recadre — c'est-à-dire exactement le défaut qu'on
     cherche à supprimer. Ramené à une largeur maximale (`hauteur × rapport`),
     le rapport tient et l'image reste entière.
     ⚠️ `align-self: center` et non `margin-inline: auto` : dans un conteneur
     flex, une marge automatique fait passer l'élément en largeur « auto », et
     comme son image est en `width: 100%` la boîte s'effondre à 0 (constaté). */
  align-self: center;
  width: 100%;
  max-width: calc(76svh * var(--ar, 1.6));
}
/* 🚨 C'est l'IMAGE qui porte le rapport, pas son cadre.
   Sur un élément flex dont la taille croisée est automatique (ici
   `align-self: center`), Chromium n'applique pas `aspect-ratio` : la boîte
   se retrouve à 513 × 0 px — constaté. Posé sur l'image, qui a une largeur
   définie, le rapport tient toujours, et le cadre prend simplement la
   hauteur de son contenu. Marche à l'identique en flex et en grille.
   `--ar` est une propriété personnalisée, donc héritée du cadre : rien à
   recopier sur l'image côté script. */
.pd__cover img,
.pd__shot img {
  width: 100%;
  height: auto;
  aspect-ratio: var(--ar, 16 / 10);
  object-fit: cover;
  display: block;
}

/* ---- Visite 360° : notre aperçu d'abord, l'iframe seulement au clic ----
   🚨 Le lecteur du prestataire (Kuula, Matterport…) impose son habillage —
   « vu que le panneau fait par le service externe est moche… je veux mettre
   une image en preview moi-même, cliquer sur un bouton, et c'est seulement
   maintenant que le panneau moche apparaît » (05/08/2026). L'iframe n'est
   donc injectée qu'au clic. Bénéfice secondaire et pas des moindres : plus
   aucun script tiers n'est chargé tant que le visiteur ne l'a pas demandé. */
.pd__tour {
  position: relative;
  border-radius: var(--radius-card);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: rgba(0, 0, 0, 0.3);
}
.pd__tour img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  filter: brightness(0.74);
  transition: transform 760ms var(--ease-soft), filter 460ms var(--ease-out);
}
.pd__tour-go {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--bump-paper);
}
.pd__tour-go i {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 62px; height: 62px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(243, 244, 239, 0.7);
  background: rgba(22, 43, 28, 0.34);
  font-style: normal;
  font-size: 16px;
  transition: background 240ms var(--ease-out), transform 240ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pd__tour:hover img { transform: scale(1.03); filter: brightness(0.64); }
  .pd__tour-go:hover i { background: rgba(22, 43, 28, 0.62); }
}
.pd__tour-go:active i { transform: scale(0.94); }
/* Une fois lancée, l'iframe prend toute la boîte. */
.pd__tour.is-live { background: var(--gray-100); }
.pd__tour iframe { width: 100%; height: 100%; border: 0; display: block; }

/* ---- Galerie : la grille suit l'ORIENTATION des images ----
   Le script mesure chaque visuel avant de l'insérer et lui pose son rapport
   (`--ar`) plus une classe d'orientation. La règle est alors très simple :

     paysage / carré  → toute la largeur de la colonne
     portrait         → une demi-colonne, donc deux verticales se retrouvent
                        côte à côte tout naturellement

   C'est ce que demandait le client : « la première en vertical en entier, et
   deux petites verticales après, juste en dessous, qui se combinent ».
   `grid-auto-flow: dense` referme les trous quand une verticale se retrouve
   seule avant une horizontale.

   🚨 Le rythme précédent (« une sur trois en pleine largeur ») était posé sur
   l'INDEX, sans rien savoir des images : il mettait un portrait en 16/9 et le
   massacrait. Ne pas revenir à une règle qui ignore le contenu. */
.pd__gal {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-flow: dense;
  align-items: start;
  gap: clamp(12px, 1.6vw, 20px);
}
.pd__shot {
  --hv: 460ms;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: rgba(0, 0, 0, 0.25);
  border-radius: var(--radius-card);
  overflow: hidden;
  cursor: zoom-in;
  line-height: 0; /* le bouton ne doit pas ajouter de blanc sous l'image */
  transition: transform var(--hv) var(--ease-soft);
}
/* Paysage et carré : toute la largeur. Portrait : une demi-colonne. */
.pd__shot--large { grid-column: 1 / -1; }
.pd__shot--haut { grid-column: span 1; }
/* Le premier visuel de la galerie a droit à la pleine largeur quoi qu'il
   arrive : c'est l'ouverture de la série. S'il est vertical, il est ramené à
   la largeur que permet le plafond de hauteur, et centré dans la colonne.
   ⚠️ Le plafond passe par `max-width` (= hauteur × rapport) et jamais par
   `max-height` : voir la note de `.pd__cover`. */
.pd__gal > .pd__shot:first-child {
  grid-column: 1 / -1;
  justify-self: center;
  width: 100%;
  max-width: calc(76svh * var(--ar, 1.6));
}
.pd__shot img { transition: transform var(--hv) var(--ease-soft); }


/* ============================================================
   Survol d'une image — le cadre RECULE, l'image AVANCE dedans
   ------------------------------------------------------------
   Geste commun à toutes les images cliquables du site (05/08/2026, demande
   client : « adapte ce style d'animation sur le reste du site »).

   Deux mouvements en sens contraire, tous les deux très faibles : le cadre
   perd 1,6 %, l'image en gagne 5, soit 3,3 % net de grossissement du sujet.
   C'est le CONTRASTE des deux sens qui se lit, pas leur amplitude — la
   référence pousse plus loin, et c'est déjà un poil trop (retour client).

   ⚠️ Ne pas monter ces valeurs pour « mieux voir l'effet ». Le recul du cadre
   est passé de 2,8 % à 1,6 % le 05/08/2026 (« un poil trop accentué, vraiment
   un tout petit peu moins ») : au-delà, il décolle visiblement la légende,
   qui elle ne bouge pas, et sur un portrait il se lit comme un défaut.
   ⚠️ La durée passe par une variable HÉRITÉE (`--hv`, posée sur le cadre et
   lue aussi par l'image) et non par `transition-duration` : la liste de
   transitions d'un cadre peut commencer par autre chose (le `clip-path` de la
   révélation des briques), et l'écraser ici déréglerait cette animation-là.
   Chaque cadre déclare donc `--hv: 460ms` (la SORTIE) et sa propre transition.
   ============================================================ */
@media (hover: hover) and (pointer: fine) {
  .brick:hover .brick__thumb,
  .member:hover .member__photo,
  .pd__shot:hover {
    transform: scale(0.984);
    --hv: 680ms; /* entrée plus ample que la sortie — règle transverse */
  }
  .brick:hover .brick__thumb img,
  .member:hover .member__photo img,
  .pd__shot:hover img { transform: scale(1.05); }
}
@media (prefers-reduced-motion: reduce) {
  /* Le survol est un mouvement : sans transition il deviendrait un à-coup.
     On le retire plutôt que de le jouer d'un bloc. */
  .brick:hover .brick__thumb, .brick:hover .brick__thumb img,
  .member:hover .member__photo, .member:hover .member__photo img,
  .pd__shot:hover, .pd__shot:hover img { transform: none; }
}

/* Prestations en chips avec pictos */
/* ---- Paliers du panneau détail ----
   Sous 900px la composition passe en une colonne : le bandeau d'identité,
   le couple texte/challenge et la galerie perdent tous leur seconde colonne. */
@media (max-width: 900px) {
  .pdetail__panel { height: 96svh; }
  /* Une colonne : le rail redevient un simple en-tête et c'est le CORPS qui
     défile — deux ascenseurs superposés n'auraient aucun sens au doigt. */
  .pdetail__body { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
  .pd { height: auto; grid-template-columns: minmax(0, 1fr); }
  .pd__rail { height: auto; overflow: visible; padding-bottom: 0; }
  .pd__media { height: auto; overflow: visible; padding-top: clamp(20px, 4vw, 30px); }
  /* Deux verticales côte à côte tiennent encore à 900px ; sous 620 non. */
  .pd__shot--large { grid-column: 1 / -1; }
}
@media (max-width: 620px) {
  .pd__gal { grid-template-columns: minmax(0, 1fr); }
  .pd__shot--haut {
    grid-column: 1 / -1;
    justify-self: center;
    width: 100%;
    max-width: calc(68svh * var(--ar, 1));
  }
}
@media (prefers-reduced-motion: reduce) {
  .pdetail__panel { transition: none; }
  .pdetail__scrim { transition: none; }
  .pd__chip { animation: none; opacity: 1; transform: none; }
}

/* ---- Slider comparaison avant / après ---- */
.ba {
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 16 / 10;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  cursor: ew-resize;
  background: var(--gray-100);
}
.ba img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}
.ba__top { clip-path: inset(0 50% 0 0); will-change: clip-path; }
.ba__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  background: var(--bump-paper);
  box-shadow: 0 0 0 1px rgba(40, 40, 39, 0.18);
  transform: translateX(-50%);
  pointer-events: none;
}
.ba__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 42px;
  height: 42px;
  border-radius: var(--radius-pill);
  background: var(--bump-paper);
  box-shadow: var(--shadow-md);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--bump-charcoal);
  font-size: 11px;
  transition: transform 160ms var(--ease-out);
}
.ba:active .ba__grip { transform: translate(-50%, -50%) scale(0.94); }
.ba__label {
  position: absolute;
  top: 10px;
  z-index: 1;
  font-size: 10px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  background: rgba(40, 40, 39, 0.55);
  color: var(--bump-paper);
  backdrop-filter: blur(4px);
  pointer-events: none;
}
.ba__label--avant { left: 10px; }
.ba__label--apres { right: 10px; }

/* ---- Lightbox galerie ---- */
.lightbox {
  position: fixed;
  inset: 0;
  /* ⚠️ Au-dessus du panneau détail (800), d'où elle est ouverte. */
  z-index: 850;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 56px 20px 20px;
  background: rgba(40, 40, 39, 0.94);
  opacity: 0;
  visibility: hidden;
  transition: opacity 150ms var(--ease-out), visibility 0s 150ms;
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 220ms var(--ease-out);
}
.lightbox__img {
  max-width: min(1100px, 92vw);
  max-height: 68vh;
  object-fit: contain;
  border-radius: var(--radius-sm);
  transform: scale(0.96);
  transition: transform 220ms var(--ease-out);
}
.lightbox.is-open .lightbox__img { transform: scale(1); }
.lightbox__caption {
  margin-top: 14px;
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: rgba(243, 244, 239, 0.65);
}
.lightbox__btn {
  position: absolute;
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid rgba(243, 244, 239, 0.35);
  border-radius: var(--radius-pill);
  color: var(--bump-paper);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: border-color 160ms var(--ease-out), transform 160ms var(--ease-out);
}
.lightbox__btn:hover { border-color: var(--bump-paper); }
.lightbox__btn:active { transform: scale(0.94); }
.lightbox__close { top: 18px; right: 18px; }
.lightbox__prev { left: 18px; top: 50%; margin-top: -21px; }
.lightbox__next { right: 18px; top: 50%; margin-top: -21px; }
.lightbox__thumbs {
  display: flex;
  gap: 8px;
  margin-top: 16px;
  max-width: 92vw;
  overflow-x: auto;
  padding: 4px;
}
.lightbox__thumbs img {
  width: 66px;
  height: 48px;
  object-fit: cover;
  border-radius: 6px;
  opacity: 0.45;
  cursor: pointer;
  border: 2px solid transparent;
  flex: none;
  transition: opacity 150ms var(--ease-out), border-color 150ms var(--ease-out);
}
.lightbox__thumbs img.is-active { opacity: 1; border-color: var(--bump-paper); }
@media (hover: hover) and (pointer: fine) {
  .lightbox__thumbs img:hover { opacity: 0.85; }
}
@media (max-width: 760px) {
  .lightbox__prev { left: 8px; }
  .lightbox__next { right: 8px; }
}

/* ============================================================
   Contact — dark section
   ============================================================ */
.contact {
  background: var(--surface-forest);
  color: var(--text-inverse);
}
/* La colonne de droite porte désormais soit l'agenda Cal.com, soit le
   formulaire : elle a besoin d'un peu plus de place que la colonne
   d'information (une vue « mois » sous ~560px retombe en affichage
   compact). D'où les proportions dissymétriques et la gouttière ramenée
   d'un cran — --space-10 (8rem) mangeait 128px à lui seul. */
.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
  gap: clamp(32px, 5vw, 88px);
  align-items: start;
}
.contact .eyebrow { color: var(--sage-deep); margin-bottom: var(--space-5); }
.contact h2 {
  font-size: clamp(34px, 4.4vw, 52px);
  line-height: 1.05;
  color: var(--bump-paper);
  margin: 0 0 var(--space-6);
}
.contact__intro {
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: var(--leading-relaxed);
  color: rgba(243, 244, 239, 0.66);
  max-width: 44ch;
  margin-bottom: var(--space-7);
}
.contact__coords {
  list-style: none;
  margin: 0 0 var(--space-7);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.contact__coords li {
  display: flex;
  gap: var(--space-4);
  font-size: 1rem;
  font-weight: 400;
  color: rgba(243, 244, 239, 0.82);
}
.contact__coords .k {
  flex: 0 0 96px;
  font-size: 0.6875rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--sage-deep);
  padding-top: 3px;
}
.contact__coords a { color: rgba(243, 244, 239, 0.82); }
.contact__coords a:hover { color: var(--bump-paper); }
/* La valeur d'une ligne peut être une pile (téléphones, réseaux) : sans
   `min-width: 0`, elle refuse de se réduire et pousse la colonne du libellé. */
.contact__coords li > :not(.k) { min-width: 0; }

/* Téléphones — un par fondateur. Le NUMÉRO d'abord (c'est ce qu'on compose),
   le prénom en retrait derrière : le libellé « Téléphones » de la colonne de
   gauche dit déjà de quoi il s'agit. */
.contact__tels {
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.contact__tel {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.contact__tel-qui {
  font-size: 0.8125rem;
  color: rgba(243, 244, 239, 0.5);
}

.contact__socials {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
}
.contact__socials a {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: rgba(243, 244, 239, 0.7);
}
.contact__socials a:hover { color: var(--bump-paper); }

/* Rangée d'icônes des réseaux — MOBILE UNIQUEMENT (01/09/2026).
   🚨 Même parti que le pied de page : en toutes lettres sur desktop (où la
   place ne manque pas et où le mot est plus clair qu'un pictogramme), en
   icônes sur mobile, où quatre libellés en capitales espacées passaient à la
   ligne et pesaient plus que les coordonnées au-dessus d'eux.
   🚨 Les deux formes sortent de la MÊME liste (gabarit `contact` de
   render-contenu.js) et ne sont JAMAIS visibles ensemble : `display: none` par
   défaut ici, et c'est le palier ≤ 760 px qui inverse les deux. */
.csocial { display: none; }
.contact__logo { margin-top: var(--space-8); }
.contact__logo img { height: 46px; width: auto; }

/* ---- Deux options : réserver un appel, ou écrire un message -----------------
   Sélecteur segmenté, puis un seul panneau visible à la fois. L'onglet actif
   prend le papyrus, comme les pilules de secteur du hero : sur du forêt, c'est
   la couleur claire qui marque l'état actif, jamais le forêt lui-même. */
.contact__choice {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
.choice {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  padding: 6px;
  border: 1px solid var(--border-inverse);
  border-radius: var(--radius-card);
  background: rgba(0, 0, 0, 0.14);
}

/* La pastille active : un SEUL élément qui glisse, au lieu d'un fond qui
   s'allume ici et s'éteint là. C'est le mouvement qui relie les deux états —
   sans lui, la bascule se lit comme deux images sans rapport.
   🚨 Elle est APRÈS les boutons dans le DOM : `~` ne regarde qu'en avant,
   c'est ce qui permet de la piloter sans classe posée par le script.
   ⚠️ Sa largeur reprend le calcul de la grille (`gap` 6 + `padding` 2×6) :
   si l'un des deux bouge, refaire le calcul ici. */
.choice__thumb {
  position: absolute;
  z-index: 0;
  top: 6px;
  bottom: 6px;
  left: 6px;
  width: calc((100% - 18px) / 2);
  border-radius: calc(var(--radius-card) - 7px);
  background: var(--bump-sage);
  transition: transform 460ms var(--ease-soft);
}
#tabMessage[aria-selected="true"] ~ .choice__thumb {
  transform: translateX(calc(100% + 6px));
}
.choice__btn {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-family: var(--font-sans);
  text-align: left;
  padding: 13px 16px;
  border: none;
  border-radius: calc(var(--radius-card) - 7px);
  background: transparent;
  color: rgba(243, 244, 239, 0.68);
  cursor: pointer;
  transition: background var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.choice__btn:hover { color: var(--bump-paper); }
.choice__btn:active { transform: scale(0.97); }
/* Plus de fond ici : c'est la pastille qui le porte et qui se déplace.
   Seule la couleur du texte bascule — et un peu plus vite que la pastille,
   pour qu'elle arrive sur un texte déjà lisible plutôt que l'inverse. */
.choice__btn[aria-selected="true"] {
  color: var(--bump-forest);
  transition-duration: 200ms;
}
.choice__t {
  font-size: 0.75rem;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.choice__s {
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: var(--leading-normal);
  opacity: 0.72;
}

/* 🚨 La boîte qui porte les deux panneaux a sa HAUTEUR animée par le script.
   L'agenda fait presque le double du formulaire (≈960 px contre ≈490 à
   1440) : sans ça, la bascule reflowait d'un coup et le pied de page
   remontait d'un demi-écran sous le lecteur.
   `overflow: clip` et jamais `hidden` (règle transverse) : il ne faut pas
   créer de conteneur de défilement, seulement découper pendant la course. */
.choice__panels {
  overflow: clip;
  transition: height 520ms var(--ease-drawer);
}
.choice__panel[hidden] { display: none; }
.choice__panel:focus-visible { outline: none; }
/* L'entrée d'un panneau : opacité + translation, rien d'autre — ce sont les
   deux seules propriétés que le compositeur anime sans repeindre, et le
   contenu qui arrive est lourd (un mois de calendrier, ou 5 champs).
   Elle est CALÉE sur la course de la hauteur : plus courte, le contenu était
   posé bien avant que la boîte ait fini de s'ouvrir. */
.choice__panel {
  animation: choiceIn 460ms var(--ease-out) 60ms both;
}
@keyframes choiceIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* L'agenda Cal.com — une carte de papier posée sur le forêt.
   Le fond clair est porté par NOTRE carte et pas seulement par l'iframe :
   sinon un rectangle vert vif reste visible le temps du chargement. */
.cal-card {
  position: relative;
  /* 🚨 CETTE RÉSERVE EST MESURÉE, PAS CHOISIE (07/09/2026). Elle valait 560px
     alors que l'iframe de Cal, une fois chargée, en fait 959 : la carte
     grandissait donc de ~400px à l'arrivée de l'agenda et poussait tout le bas
     de page. Sur mobile c'était 272px de croissance, soit 0,336 de CLS, la
     deuxième pire valeur du site.
     ⚠️ C'est bien un `min-height` et jamais une hauteur figée : CLAUDE.md
     documente que l'iframe se redimensionne toute seule bien après le
     chargement, et qu'une hauteur figée la couperait. Un minimum réserve la
     place sans jamais plafonner.
     ⚠️ Valeur à REMESURER si Cal change son gabarit d'intégration : banc
     `scratchpad/mesure-cal.js`, qui relève la hauteur de l'iframe chargée. */
  min-height: 960px;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--bump-paper);
  border: 1px solid var(--border-inverse);
}
.cal-card__load {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--text-muted);
}
.cal-embed {
  position: relative;
  width: 100%;
  /* Même réserve mesurée que `.cal-card` juste au-dessus — si l'une bouge,
     l'autre suit, sinon la carte réserve une place que son contenu ne remplit
     pas (ou l'inverse). */
  min-height: 960px;
  overflow: auto;
}
.cal-embed iframe { border-radius: inherit; }
.cal-note {
  margin: var(--space-4) 0 0;
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: var(--leading-relaxed);
  color: rgba(243, 244, 239, 0.6);
}
.cal-note a {
  color: rgba(243, 244, 239, 0.85);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cal-note a:hover { color: var(--bump-paper); }

/* Form */
.form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
}
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field label {
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--sage-deep);
}
.field input,
.field select,
.field textarea {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 16px;
  color: var(--bump-paper);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border-inverse);
  padding: 10px 2px;
  outline: none;
  transition: border-color var(--dur) var(--ease-out);
  /* Le clavier virtuel mobile fait remonter un champ au focus, natif au
     navigateur : sans cette marge, un champ peut se retrouver calé sous la
     barre de nav fixe (--header-h), donc masqué au moment même où on veut
     y taper. */
  scroll-margin-top: calc(var(--header-h) + var(--space-3));
}
.field textarea { resize: vertical; min-height: 80px; }
.field select { appearance: none; cursor: pointer; }
.field select option { background: var(--bump-charcoal); color: var(--bump-paper); }
.field input::placeholder,
.field textarea::placeholder { color: rgba(243, 244, 239, 0.4); }
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--sage); }
.form__submit { margin-top: var(--space-4); }

/* Piège à robots : retiré du flux et de l'arbre d'accessibilité */
.form__trap {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Retour d'envoi — réserve sa place pour éviter tout saut de mise en page */
.form__feedback {
  min-height: 1.4em;
  margin: var(--space-3) 0 0;
  font-size: 0.9rem;
  line-height: var(--leading-normal);
  color: var(--bump-sage);
}
.form__feedback.is-ok { color: var(--bump-sage); font-weight: var(--weight-medium); }
.form__feedback.is-err { color: #e8b4a8; }

/* ============================================================
   Footer — la planche de trame (11/08/2026)

   Références fournies par le client : la grille et la mise en place du footer
   kosbiotic (desktop et mobile), les informations affichées par flair.digital,
   et surtout les TUILES SEMÉES ET ANIMÉES d'appindex.ai.

   🚨 C'EST UNE PLANCHE DE TRAME, exactement comme `.lcloud`. Elle reprend les
   jetons `--trame-ligne` / `--trame-croix` / `--trame-bras`, et surtout le
   `margin-inline: calc(-1 * var(--trame-air))` : ses verticales extérieures
   tombent alors À L'APLOMB de celles de toute la page. C'est ça, « se fondre
   dans la masse ». Si l'un des deux bouge, l'autre suit.

   🚨 AUCUN TITRE DE COLONNE SUR DESKTOP (demande client : « je n'ai même pas
   envie que tu mettes les titres sur les panneaux »). Ce sont les filets
   verticaux qui séparent les colonnes, pas des libellés — même parti que la
   référence flair.digital. Le nom de chaque colonne survit dans son
   `<summary>`, masqué au-dessus de 760 px : il ne réapparaît que comme ligne
   d'accordéon sur mobile, là où il faut bien savoir sur quoi on appuie.

   🚨 LES TUILES SONT SEMÉES DANS DEUX GRILLES DE MODULES, PAS DANS UNE
   COLONNE (refonte du 12/08/2026, référence appindex.ai) : un bloc de 3 × 2
   au-dessus de la marque, et une bande de 12 cases sur toute la largeur sous
   les colonnes. La grille n'apparaît que là où il y a des tuiles ; le milieu
   reste ouvert pour le texte. C'est la composition de la référence.

   Le markup vient d'un gabarit UNIQUE (`footer` dans render-contenu.js), rendu
   par `pied.js` sur les trois pages secondaires et par la table `cibles`
   d'`index.html` sur la home.
   ============================================================ */
.site-footer__inner {
  /* Pas de `border-top` sur `.site-footer` : le rail haut de la planche s'en
     charge, et lui traverse l'écran là où une bordure s'arrêterait au `.shell`. */
  padding-top: var(--space-8);
  padding-bottom: var(--space-7);
}

.fcloud {
  position: relative;
  /* 🚨 Voir l'entête : c'est ce décalage qui met les verticales du footer à
     l'aplomb de celles de `.lcloud` et de `.trame-cadre`. */
  margin-inline: calc(-1 * var(--trame-air));
  /* Les deux verticales extérieures. Les rails horizontaux, eux, traversent
     l'écran et sont donc des calques, pas des bordures. */
  border-left: 1px solid var(--trame-ligne);
  border-right: 1px solid var(--trame-ligne);
}

/* ---- Les deux rails pleine fenêtre : haut et bas de la planche ----
   ⚠️ `left: 50%` + `width: 100vw` + `translateX(-50%)`, JAMAIS
   `left: -50vw; right: -50vw` : la seconde forme fait un calque de 100vw DE
   PLUS que la planche et porte le débordement horizontal du document à
   plusieurs centaines de pixels (mesuré sur `.lcloud`).
   Les autres traits (sous le bloc de tuiles, autour de la bande semée) ne
   traversent PAS l'écran : ils s'arrêtent au bord de leur colonne, comme dans
   la référence. Ce sont donc de simples bordures. */
.fcloud::before,
.fcloud::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  border-top: 1px solid var(--trame-ligne);
  pointer-events: none;
}
.fcloud::before { top: 0; }
.fcloud::after { bottom: 0; }

/* ---- Les deux colonnes, pleine hauteur ----
   🚨 LE CONTENU EST POUSSÉ AUX EXTRÉMITÉS et le grand vide du milieu est
   VOULU : c'est la composition de la référence, où le bloc de texte occupe le
   bas de la colonne de gauche. Demande du client le 12/08/2026 — « le logo à
   la place de la citation, tout en bas à gauche, et un bel espace au-dessus,
   donc faut que ça soit assez long ».
   🚨 C'est la hauteur minimale qui crée ce vide. Sans elle la planche se
   referme sur son contenu (~370 px) et la marque remonte contre les tuiles.
   ⚠️ Resserré une deuxième fois le 13/08/2026 (« réduire un tout petit peu cet
   espace vide, mais tout en gardant un [espace conséquent] ») : même ampleur
   que le premier rabot du 12/08, environ 8-9 % (480→440, 58→53svh, 720→660).
   Le vide reste voulu et « assez long », seule son ampleur continue d'être
   rabotée par petites touches. Si on retouche encore, garder ce même ordre de
   grandeur (quelques % à la fois), pas un coup de rabot large.

   🚨 Le TERME `svh` a été validé sur un écran bien plus haut que ce que la
   plupart des visiteurs ont réellement (15/08/2026). Toutes les captures et
   vérifications de ce fichier tournent sur des fenêtres d'au moins ~1250 px
   de HAUTEUR (2560×1600, 3840×2160…) : à cette hauteur, `53svh` dépasse déjà
   660 px et le plafond du clamp() fait tout le travail, le pourcentage lui-
   même n'ayant jamais été vu à l'œil en dessous de son plafond. Sur un écran
   1920×1080 réel, la fenêtre du navigateur (barre d'onglets/adresse
   déduites) tourne plutôt autour de 950-1040 px de haut : `53svh` n'y vaut
   plus que ~505-550 px, soit le tiers du vide en moins que sur l'écran de
   référence. Le vide ne disparaît pas (il reste bien au-dessus du plancher
   de 440 px), mais l'écart avec les 660 px habituels se voit, et la totalité
   du manque est absorbée par le vide (aucun autre élément de la colonne ne
   rétrécit) — d'où l'impression qu'« il n'est plus là ».
   Correctif : la pente passe de 53 à 66svh. Le plafond de 660 px est déjà
   atteint dès ~1000 px de hauteur de fenêtre (660 / 0.66) au lieu de
   ~1245 px : l'écran de référence, largement au-dessus des deux seuils,
   n'y voit RIEN changer (toujours plafonné à 660). Un 1080p réel (~950-
   1040 px de fenêtre) se retrouve en revanche pile au plafond ou tout
   contre, au lieu d'à un tiers en dessous. */
.fgrid {
  position: relative;
  display: flex;
  align-items: stretch;
  min-height: clamp(440px, 66svh, 660px);
}

/* 🚨 TOUT LE FOOTER TOMBE SUR UN SEUL MODULE DE 12 COLONNES. La colonne de
   gauche en occupe 4, chaque colonne de texte 2, la bande semée les 8 de
   droite : une case vaut EXACTEMENT un module des deux côtés, et les deux
   grilles de tuiles sont à l'aplomb. C'est ce qui fait lire la référence comme
   un seul système plutôt que comme deux grilles voisines.
   ⚠️ Si le nombre de colonnes de texte change, refaire le compte : 4 + 2n doit
   valoir 12. */
.fside {
  position: relative;
  flex: 4 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--trame-ligne);
  /* 🚨 LE BAS DE LA MARQUE PARTAGE LA MARGE DU TEXTE DE PIED, AU TOKEN PRÈS
     (13/08/2026 : « les écritures sont centrées sur la case, il faut qu'elles
     soient centrées en bas et le logo soit aligné avec »). `.fbase__txt` est
     désormais cadré en bas (`align-items: flex-end`) au lieu d'être centré
     dans sa rangée : son texte repose donc sur son propre padding-bottom, qui
     est `var(--space-4)`.

     🚨 LA RELATION EST INDÉPENDANTE DE LA HAUTEUR TOTALE, ET C'EST VOULU.
     `.fbase` est calée en bas de `.fmain` (le `margin-top: auto` de
     `.ftiles--bande` pousse tout ce qui suit, dont `.fbase`, jusqu'au bord bas
     commun) : son bord bas colle donc TOUJOURS au bord bas de `.fside`/
     `.fmain`, quelle que soit la taille du vide au-dessus (`svh`, palier…).
     Reprendre le MÊME token en `padding-bottom` ici, plutôt qu'une fraction de
     module recalculée, garantit que le bas de la marque et la ligne de base
     du texte tombent au même écart du bord commun à toutes les tailles — ce
     n'est plus une valeur mesurée et calée à l'œil, c'est structurellement la
     même marge des deux côtés.

     ⚠️ Ancienne approche (calc(100%/24), qui visait le CENTRE du copyright
     centré) abandonnée avec le passage du texte au calage bas — un centrage
     et un calage bas ne tombent pas au même endroit, le recalcul à l'ancienne
     formule aurait décalé la marque du texte.

     🚨 CORRECTIF DU 13/08/2026 : le même token égalise les BOÎTES, pas
     l'ENCRE. Mesuré par capture + scan de pixels (`scratchpad/pied-logo-ink.js`,
     méthode documentée plus bas dans ce fichier) : le logo, détouré au plus
     près de son dessin, a son encre à 0,5 px du bas de sa boîte ; le texte, lui,
     laisse ~4 px de son line-height sous la ligne de base des lettres. À marge
     de boîte égale, l'ENCRE du logo tombait donc 4 px plus BAS que celle du
     texte — visible à l'œil (« un à deux pixels, trois max », signalé par le
     client). D'où les `+ 4px`, appliqués UNIQUEMENT ici : `.fbase__txt` n'a
     pas bougé, c'est le logo qu'on relève pour rejoindre l'encre du texte, pas
     l'inverse. Revérifier au même banc si `--space-4` ou le détourage du logo
     changent. */
  padding-bottom: calc(var(--space-4) + 4px);
}

/* Colonne de droite : les colonnes de texte en haut, la bande semée et la
   ligne de pied poussées en bas (voir `margin-top: auto` sur la bande). */
.fmain {
  position: relative;
  flex: 8 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* 🚨 LE LOGO EST DE TAILLE MOYENNE, plus le bandeau géant du premier jet
   (retiré le 12/08/2026 : « j'aime pas mon logo en grand »). Il pesait 450 px
   de haut à 1440 et le footer n'était plus qu'un logo ; il en fait maintenant
   ~160. C'est l'entre-deux demandé : une signature, pas une affiche.
   🚨 Le dérivé web est un WebP LOSSLESS (215 Ko) et non l'original : le SVG
   texturé de la charte pèse 3,37 Mo, et `build.js` ne ré-encode que le raster.
   Voir CLAUDE.md pour le piège de l'alpha en WebP lossy. */
.fmark__logo {
  display: block;
  width: 100%;
  max-width: 420px;
  height: auto;
  margin: auto 0 0;
  /* 🚨 PAS DE PADDING-BOTTOM. Corrigé le 12/08/2026 (« le logo doit être
     aligné en bas »). `margin-top: auto` cale la BOÎTE de l'image sur le bas
     de `.fside` au pixel près (vérifié), mais le WebP est détouré au plus
     près de l'encre : le moindre padding sous l'image rouvre un vide entre le
     bas RÉEL du mot-symbole et les rangées du dessous, invisible dans les
     mesures de boîte (`getBoundingClientRect` inclut le padding) mais très
     visible à l'œil. Avec `padding-bottom: var(--space-5)` (24px), ce vide
     mesurait ~24 px en pixels d'écran — constaté par relevé direct des pixels
     de la capture, pas par la boîte CSS. Un site qui documente déjà « soit on
     aligne exactement, soit on décale franchement » n'a pas à laisser un
     presque-alignement ici. */
  padding: var(--space-6) var(--space-5) 0;
}

/* ---- Les colonnes de texte ----
   `flex` et non `grid` : si une colonne ne sort pas (un champ CMS vide, et
   c'est la règle transverse du site), les autres se répartissent la place au
   lieu de laisser un trou dans une piste de grille. */
.fcloud__cols {
  position: relative;
  display: flex;
  align-items: stretch;
}
.fcol {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  padding: var(--space-6) var(--space-5);
  border-right: 1px solid var(--trame-ligne);
}
.fcol:last-child { border-right: 0; }

/* ---- Icônes des réseaux — MOBILE UNIQUEMENT (15/08/2026) ----
   Voir `reseauxIcones` dans render-contenu.js pour le pourquoi. Masquée par
   défaut : desktop garde sa seule colonne texte « Réseaux », qui suffit et
   n'a pas besoin de doublon. Rendue dans le HTML des DEUX côtés (une seule
   liste de liens), c'est le CSS seul qui choisit laquelle des deux formes se
   montre à quelle taille d'écran. */
.fsocial { display: none; }

.fcol__body { display: flex; flex-direction: column; gap: var(--space-3); }
.fcol__body a,
.fcol__adr span {
  display: block;
  font-size: 0.9375rem;
  font-weight: 400;
  line-height: var(--leading-normal);
  color: var(--text-body);
}
.fcol__adr { font-style: normal; }         /* `<address>` est en italique par défaut,
                                              et le site n'a pas d'italique. */
.fcol__adr span { color: var(--text-muted); }
.fcol__adr span:first-child { color: var(--text-body); }
/* Le prénom derrière le numéro : c'est le numéro qu'on compose. */
.fcol__qui {
  display: block;
  font-style: normal;
  font-size: 0.8125rem;
  color: var(--text-muted);
}
.fcol__body a { transition: color var(--dur-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .fcol__body a:hover { color: var(--bump-forest); }
}

/* ============================================================
   Les tuiles semées
   ============================================================ */
.ftiles { display: grid; }
/* Le bloc de 4 × 2, en haut de la colonne de gauche : ses 4 modules. */
.ftiles--bloc {
  grid-template-columns: repeat(4, 1fr);
  border-bottom: 1px solid var(--trame-ligne);
}
/* La bande de 8 cases, en bas de la colonne de droite : ses 8 modules.
   🚨 `margin-top: auto` est ce qui la POUSSE EN BAS, avec la ligne de pied :
   c'est ce qui ouvre le grand vide au milieu du footer. */
.ftiles--bande {
  position: relative;
  grid-template-columns: repeat(8, 1fr);
  margin-top: auto;
  border-top: 1px solid var(--trame-ligne);
}

.ftile {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  /* Donne du relief au pivot des pictos : sans perspective, la rotation
     s'aplatit et l'entrée se lit comme un simple étirement horizontal. */
  perspective: 460px;
  border-right: 1px solid var(--trame-ligne);
  border-bottom: 1px solid var(--trame-ligne);
}
.ftiles--bloc .ftile:nth-child(4n) { border-right: 0; }
.ftiles--bande .ftile:nth-child(8n) { border-right: 0; }
.ftiles--bloc .ftile:nth-last-child(-n + 4) { border-bottom: 0; }
.ftiles--bande .ftile { border-bottom: 0; }

/* Croix de repère aux coins des modules — même dessin que `.lcloud` et
   `.trame-cadre` : deux traits d'1 px, aucune icône importée. */
.ftile::after {
  content: "";
  position: absolute;
  z-index: 2;
  right: calc(-0.5 * var(--trame-bras));
  bottom: calc(-0.5 * var(--trame-bras));
  width: var(--trame-bras);
  height: var(--trame-bras);
  background:
    linear-gradient(var(--trame-croix) 0 0) center / 100% 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) center / 1px 100% no-repeat;
  pointer-events: none;
}
/* On n'éteint que les croix sans intersection sous elles : la dernière colonne
   de chaque grille, dont le bord droit est une verticale déjà marquée. */
.ftiles--bloc .ftile:nth-child(4n)::after,
.ftiles--bande .ftile:nth-child(8n)::after { display: none; }

/* ---- L'apparition / disparition ----
   🚨 LE PICTO NE SE REMPLACE PAS SUR PLACE : IL PIVOTE, DISPARAÎT, ET UN AUTRE
   APPARAÎT DANS UNE AUTRE CASE. Demande client du 12/08/2026 (« pas que juste
   une image défile »).

   ⚠️ Ce n'est PAS ce que fait appindex.ai. Mesuré au banc sur 20 s : la
   référence a six cases FIXES, toujours six pictos présents, chacun basculant
   entre deux à quatre images sans jamais changer de case. Le client décrit
   autre chose que ce qu'il a vu, et c'est son choix — ne pas « corriger » vers
   la référence.

   🚨 LA MÉCANIQUE TIENT EN CSS PUR, SANS QU'AUCUN SCRIPT N'AIT À CHOISIR UNE
   CASE : il y a PLUS DE CASES PORTEUSES que de pictos visibles à un instant
   donné, et chacune a son propre cycle apparition → séjour → disparition. Les
   cycles étant désaccordés, on voit des pictos s'éteindre ici et se rallumer
   ailleurs. Sept cases à ~44 % de présence → 3 visibles en moyenne.

   🚨 LES PÉRIODES SONT TOUTES DIFFÉRENTES ET PREMIÈRES ENTRE ELLES (8,3 à
   13,7 s, posées par le gabarit dans `--per`). Égales ou harmoniques, elles
   feraient clignoter le footer d'un seul bloc. Le décalage `--d` est NÉGATIF :
   sans lui les sept cases s'allumeraient à la même frame au chargement. */
.ftile__in {
  display: grid;
  place-items: center;
  width: clamp(40px, 58%, 72px);
  aspect-ratio: 1;
  border-radius: var(--radius-xs);
  /* ⚠️ Deux animations, une par propriété — voir les keyframes plus bas. Elles
     partagent période et décalage, donc restent en phase. Aucune courbe ici :
     chaque keyframe pose la sienne. */
  animation:
    ftileTurn var(--per, 10s) var(--d, 0s) infinite,
    ftileFade var(--per, 10s) var(--d, 0s) infinite;
}
.ftile__in--forest { background: var(--surface-forest); }
.ftile__in--graphite { background: var(--surface-graphite); }
.ftile__in--sage { background: var(--bump-sage); }
/* ⚠️ LARGEUR ET HAUTEUR TOUTES DEUX IMPOSÉES : les SVG de la charte n'ont
   aucune dimension intrinsèque (`naturalWidth` = 0). Avec une seule des deux
   en `auto`, le navigateur n'a rien pour déduire l'autre du viewBox et l'image
   tombe à 0 × 0 — piège déjà rencontré sur les planches de logos. */
.ftile__in img { width: 76%; height: 76%; }
/* 🚨 Le picto reste la variante DARK de la charte, éclairci ici. Dériver le
   chemin de la variante Light serait fragile : le fichier 360 s'appelle
   « PICTO-360-vecto -light.svg », avec une espace, et un picto importé par
   l'utilisateur n'a pas forcément de variante claire. Même solution que sur
   les cartes prestations. */
.ftile__in--forest img,
.ftile__in--graphite img { filter: brightness(0) invert(1); opacity: 0.92; }

/* 🚨 DEUX ANIMATIONS SÉPARÉES, ET C'EST TOUT L'OBJET : la rotation d'un côté,
   l'opacité de l'autre. Corrigé le 12/08/2026 (« ça saccade »).

   🚨 LA CAUSE DE LA SACCADE : `animation-timing-function` s'applique À CHAQUE
   SEGMENT entre deux keyframes, PAS à l'animation entière. La version d'avant
   réunissait opacité et rotation dans un seul jeu de keyframes, ce qui forçait
   des arrêts à 2 %, 4 % et 16 % juste pour faire varier l'opacité — avec
   `ease-in-out`, la rotation décélérait puis ré-accélérait à chacun de ces
   arrêts. Deux ralentissements en plein quart de tour : ça se voyait.

   En séparant, chaque quart de tour redevient UN SEUL SEGMENT, avec une seule
   courbe : plus aucune rupture de vitesse. Ne pas les refusionner.

   ⚠️ Plus de `scale()` : un rétrécissement uniforme irait à l'encontre de
   l'effet recherché (un aplatissement sur un seul axe, pas un zoom arrière).
   La perspective posée sur `.ftile` fait tout le travail.

   ⚠️ Les courbes sont posées PAR KEYFRAME (`animation-timing-function` dans le
   bloc) et non sur la déclaration : c'est le seul moyen d'avoir une entrée
   freinée et une sortie à vitesse constante dans la même animation.
     · entrée  → `--ease-out`, la tuile arrive et se pose ;
     · sortie  → `linear`, vitesse constante, comme une pale. Pas d'`ease-in` :
       la charte l'interdit, et il n'apporterait rien ici puisque la tuile
       s'efface avant la fin de sa course. */
@keyframes ftileTurn {
  0% { transform: rotateY(-90deg); animation-timing-function: var(--ease-out); }
  8% { transform: rotateY(0deg); animation-timing-function: linear; }
  50% { transform: rotateY(0deg); animation-timing-function: linear; }
  58% { transform: rotateY(90deg); }
  100% { transform: rotateY(90deg); }
}

/* 🚨 LE FONDU DURE 0,5 % DU CYCLE, soit ~40 ms — il est là pour garantir la
   disparition, pas pour se voir. Quand il se déclenche la tuile est déjà à
   plus de 84° de rotation, donc large de moins de 10 % : on ne perçoit que la
   pale qui se met de profil.
   ⚠️ Sans lui, une tuile à 90° peut laisser un filet d'un pixel à l'écran
   (anticrénelage du fond et du rayon) : c'est une ceinture, pas l'effet. */
@keyframes ftileFade {
  0% { opacity: 0; }
  0.5%, 57.5% { opacity: 1; }
  58%, 100% { opacity: 0; }
}
/* 🚨 Les colonnes de texte S'ÉTIRENT jusqu'à la bande semée : leurs filets
   verticaux traversent donc le grand vide et se referment sur le bord haut de
   la bande, au lieu de s'arrêter en l'air sous le dernier lien. C'est ce que
   fait la référence avec la verticale de sa colonne de gauche. */
.fcloud__cols { flex: 1 1 auto; }

/* ---- Les croix des trois verticales ----
   Un calque en bande par verticale, deux croix chacun (haut et bas), faute de
   pseudo libre — même technique que la planche des partenaires. */
.fside::before,
.fside::after,
.fmain::after {
  content: "";
  position: absolute;
  z-index: 2;
  top: calc(-0.5 * var(--trame-bras));
  bottom: calc(-0.5 * var(--trame-bras));
  width: var(--trame-bras);
  pointer-events: none;
  background:
    linear-gradient(var(--trame-croix) 0 0) left 0 top calc(0.5 * var(--trame-bras)) / 100% 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) left 0 bottom calc(0.5 * var(--trame-bras)) / 100% 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) left calc(0.5 * var(--trame-bras)) top 0 / 1px var(--trame-bras) no-repeat,
    linear-gradient(var(--trame-croix) 0 0) left calc(0.5 * var(--trame-bras)) bottom 0 / 1px var(--trame-bras) no-repeat;
}
.fside::before { left: calc(-0.5 * var(--trame-bras)); }   /* verticale extérieure gauche */
.fside::after { right: calc(-0.5 * var(--trame-bras)); }   /* la séparation centrale */
.fmain::after { right: calc(-0.5 * var(--trame-bras)); }   /* verticale extérieure droite */

/* Les croix au pied de chaque colonne de texte, sur le bord haut de la bande. */
.fcol::after {
  content: "";
  position: absolute;
  z-index: 2;
  right: calc(-0.5 * var(--trame-bras));
  bottom: calc(-0.5 * var(--trame-bras));
  width: var(--trame-bras);
  height: var(--trame-bras);
  background:
    linear-gradient(var(--trame-croix) 0 0) center / 100% 1px no-repeat,
    linear-gradient(var(--trame-croix) 0 0) center / 1px 100% no-repeat;
  pointer-events: none;
}
/* La dernière colonne borde la verticale extérieure, déjà marquée par son
   calque en bande : sa croix ferait un double trait. */
.fcol:last-child::after { display: none; }

/* ---- L'accordéon : replié en `<details>`, déplié au-dessus de 760 px ----
   🚨 `<details>` / `<summary>` NATIFS, zéro JavaScript : ça fonctionne au
   clavier, c'est annoncé correctement, et le footer reste utilisable si le
   script est coupé. */
.fcol__sum {
  display: none;                            /* desktop : aucun titre de colonne */
  list-style: none;
  cursor: pointer;
}
.fcol__sum::-webkit-details-marker { display: none; }

/* 🚨 CE BLOC EST CE QUI REND LES COLONNES VISIBLES SUR DESKTOP. Le markup ne
   porte PAS d'attribut `open` — il ne peut pas, sinon l'accordéon mobile
   s'ouvrirait tout seul. Les `<details>` sont donc fermés, et ce sont ces deux
   règles qui les déplient au-dessus de 760 px :
     · `display` sur le corps  → suffit aux moteurs qui masquaient le contenu
       d'un `<details>` fermé par un simple `display: none` ;
     · `content-visibility`    → pour ceux qui passent par `::details-content`
       (Chrome 131+, Safari 18.4+, Firefox 139+).
   Il faudrait que les DEUX échouent pour qu'une colonne se retrouve vide.
   ⚠️ Le bloc est borné au desktop : hors de ce garde il rendrait le contenu
   d'un panneau fermé visible sur mobile, donc plus d'accordéon du tout. */
@media (min-width: 761px) {
  .fcol__body { display: flex; }
  .fcol::details-content { content-visibility: visible; }
}

/* ---- La ligne de pied : une RANGÉE DE MODULES ----
   🚨 5 modules pour le texte + 3 vraies cases, soit les 8 de la colonne de
   droite. La grille descend donc jusqu'en bas du footer, au lieu de s'arrêter
   au-dessus d'une bande de texte flottante.
   ⚠️ La hauteur de la rangée est donnée par les cases (`aspect-ratio: 1`), pas
   par le texte : elle vaut donc exactement un module, comme la bande du
   dessus. C'est ce qui permet de caler la marque de gauche dessus. */
.fbase {
  position: relative;
  display: grid;
  grid-template-columns: 5fr 1fr 1fr 1fr;
  border-top: 1px solid var(--trame-ligne);
}
.fbase__txt {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  border-right: 1px solid var(--trame-ligne);
  font-size: 12px;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.fbase__sect { display: flex; gap: 18px; flex-wrap: wrap; }
.fbase .ftile { border-bottom: 0; }
.fbase .ftile:last-child { border-right: 0; }
.fbase .ftile:last-child::after { display: none; }

/* ------------------------------------------------------------
   Footer — palier intermédiaire (761 → 1200 px)

   🚨 QUATRE COLONNES DE TEXTE NE TIENNENT PAS SOUS ~1200 px. Le module vaut
   1/12 de la planche, une colonne en fait 2, moins ses 48 px de padding : à
   768 il reste 75 px de texte utile et « contact@bumpstudio.ch » sortait de la
   case, les numéros se coupant en deux lignes (constaté). Le seuil est calculé
   sur le plus long libellé : à 1200 la colonne offre ~145 px, c'est la limite.
   Si un libellé plus long arrive un jour, remonter ce seuil.

   Les deux colonnes s'empilent : la colonne de gauche (tuiles + marque) passe
   AU-DESSUS, et les colonnes de texte se rangent en 2 × 2, où chacune dispose
   de 6 modules. Le semis reste sur la même trame : le bloc s'étale en une
   seule rangée de 8 cases, toujours un module par case.
   ⚠️ La hauteur minimale tombe : empilées, les deux colonnes font déjà la
   hauteur voulue, et la garder ouvrirait un vide de plusieurs centaines de
   pixels au milieu de la colonne de gauche.
   ------------------------------------------------------------ */
@media (max-width: 1200px) and (min-width: 761px) {
  .fgrid { display: block; min-height: 0; }
  .fside {
    border-right: 0;
    border-bottom: 1px solid var(--trame-ligne);
    /* ⚠️ Le calage sur les deux rangées basses n'a plus d'objet : empilée, la
       marque n'est plus à côté d'elles. Le garder ouvrirait un vide de deux
       modules sous le logo. */
    padding-bottom: 0;
  }
  .fmark__logo { max-width: 380px; }
  /* Les calques de croix des verticales n'ont plus d'objet : empilées, les deux
     colonnes n'ont plus de séparation verticale intérieure. */
  .fside::before,
  .fside::after,
  .fmain::after { display: none; }

  /* Le bloc devient une bande de 8 cases sur toute la largeur. */
  .ftiles--bloc { grid-template-columns: repeat(8, 1fr); }
  .ftiles--bloc .ftile { border-bottom: 0; }
  .ftiles--bloc .ftile:nth-child(4n) { border-right: 1px solid var(--trame-ligne); }
  .ftiles--bloc .ftile:nth-child(8n) { border-right: 0; }
  /* ⚠️ Reprendre les DEUX sélecteurs de croix : une media query n'ajoute
     aucune spécificité, la règle d'extinction du palier large s'appliquerait
     encore telle quelle (piège transverse du site). */
  .ftiles--bloc .ftile:nth-child(4n)::after { display: block; }
  .ftiles--bloc .ftile:nth-child(8n)::after { display: none; }

  .fcloud__cols { display: grid; grid-template-columns: repeat(2, 1fr); }
  .fcol:nth-child(2n) { border-right: 0; }
  .fcol:nth-child(-n + 2) { border-bottom: 1px solid var(--trame-ligne); }
  .fcol:last-child::after { display: none; }
}

/* ------------------------------------------------------------
   Footer — accordéon mobile (référence client : kosbiotic mobile)
   ------------------------------------------------------------ */
@media (max-width: 760px) {
  .fgrid { display: block; min-height: 0; }
  .fside { border-right: 0; padding-bottom: 0; }
  .fcloud__cols { display: block; }
  .fside::before,
  .fside::after,
  .fmain::after { display: none; }

  /* 🚨 La BANDE disparaît sous 760 px : à 3 cases par rangée elle ferait trois
     rangées de modules sous l'accordéon, soit plus de hauteur que le reste du
     footer. Le bloc de 4 × 2 suffit à porter le semis, et la référence mobile
     n'a d'ailleurs aucune bande. */
  .ftiles--bande { display: none; }

  /* 🚨 LE LOGO SE CENTRE SOUS 760 px, ET SOUS 760 px SEULEMENT (demande client
     du 14/08/2026). Ici la colonne de gauche fait toute la largeur et n'a plus
     rien à sa droite : calé à gauche, le mot-symbole se lisait comme un bloc
     oublié dans un coin. Sur desktop il garde son calage BAS-GAUCHE, qui n'est
     pas un défaut mais un alignement mesuré sur la ligne du copyright (voir
     `.fside`, travail des 12-13/08/2026) — ne pas propager `margin-inline:
     auto` au-delà de ce palier, ça défait exactement ce réglage-là.
     ⚠️ Le padding redevient SYMÉTRIQUE : centrer une image dont les marges
     latérales diffèrent déplace son encre du décalage entre les deux. */
  .fmark__logo {
    max-width: 320px;
    margin-inline: auto;
    /* ⚠️ PLUS DE PADDING-BOTTOM (retiré le 15/08/2026). Le `--space-6` (32px)
       du bas datait du 14/08, quand le logo était encore suivi directement de
       l'accordéon : c'était alors le seul propriétaire de cet écart. Depuis
       l'arrivée de `.fsocial` juste après (icônes des réseaux), les deux
       padding s'additionnaient (32 + 16 = 48 px), désaccordant l'espace
       logo→icônes du reste du rythme (24 px partout ailleurs dans ce bloc :
       tuiles→logo, icônes→Naviguer). C'est maintenant `.fsocial` seul qui
       porte cet écart, au même jeton que les deux autres. */
    padding: var(--space-5) var(--space-4) 0;
  }

  .fcol {
    border-right: 0;
    padding: 0;
  }
  /* ⚠️ Ces traitillés sont HORIZONTAUX, et c'est voulu : ce sont les
     séparateurs de l'accordéon, demandés par le client sur le modèle de sa
     référence. Ils n'ont rien à voir avec la règle « le traitillé de la trame
     est vertical, jamais horizontal », qui porte sur les frontières de section
     de la home. Ne pas les prendre pour une régression.
     ⚠️ `repeating-linear-gradient` et non `border-style: dashed` : la longueur
     des tirets d'une bordure est laissée au moteur et n'est pas réglable. */
  /* ⚠️ Sur TOUTES les lignes, pas seulement `.fcol + .fcol` : la première
     colonne suit désormais la marque et non une autre colonne, elle se
     retrouvait sans séparateur pendant que les trois autres en avaient un. */
  .fcol {
    background-image: repeating-linear-gradient(
      to right,
      var(--trame-ligne) 0 var(--trame-tiret),
      transparent var(--trame-tiret) calc(var(--trame-tiret) + var(--trame-vide))
    );
    background-size: 100% 1px;
    background-position: left top;
    background-repeat: no-repeat;
  }
  /* Les croix sont des intersections : sans filet vertical intérieur, il n'y a
     plus rien à marquer. */
  .fcol::after { display: none; }

  .fcol__sum {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-4);
    font-size: 1.0625rem;
    font-weight: 400;
    color: var(--text-body);
  }
  /* 🚨 Le `+` EST la croix de la trame : mêmes deux dégradés d'1 px, même
     `--trame-bras`. Il pivote en `×` à l'ouverture. */
  .fcol__plus {
    flex: none;
    width: var(--trame-bras);
    height: var(--trame-bras);
    background:
      linear-gradient(var(--trame-croix) 0 0) center / 100% 1px no-repeat,
      linear-gradient(var(--trame-croix) 0 0) center / 1px 100% no-repeat;
    transition: rotate 220ms var(--ease-out);
  }
  .fcol[open] .fcol__plus { rotate: 45deg; }

  .fcol__body {
    display: none;
    padding: 0 var(--space-4) var(--space-6);
  }
  .fcol[open] .fcol__body { display: flex; }

  /* ---- Ouverture / fermeture animées (15/08/2026) ----
     Retour client : « le footer... manque un peu d'animation fluide,
     j'aimerais que quand ça clique, ce soit un peu plus fluide », en
     référence au geste du sélecteur de Contact.
     🚨 CSS SEUL : la force de l'accordéon natif, c'est de fonctionner SANS
     JavaScript (voir plus haut) — l'animer ne doit pas remettre ça en cause.
     `display` et `height: auto` sont deux valeurs DISCRÈTES qu'une transition
     CSS classique ne sait pas interpoler ; `allow-discrete` + `@starting-style`
     couvrent la première, `interpolate-size` la seconde (c'est le même
     mécanisme, récent, déjà supposé disponible pour `::details-content` plus
     haut dans ce fichier). Le `<details>` continue de s'ouvrir sans script :
     un moteur qui ne comprend pas ces propriétés retrouve simplement la
     bascule instantanée d'avant, jamais cassé.
     ⚠️ La liste de transition vit sur `.fcol__body` (l'état FERMÉ, toujours
     présent), pas sur `.fcol[open] .fcol__body` : c'est de là qu'elle est
     héritée à l'ouverture ET à la fermeture, dans les deux sens. */
  @supports (transition-behavior: allow-discrete) {
    .fcol { interpolate-size: allow-keywords; }
    .fcol__body {
      height: 0;
      overflow: hidden;
      opacity: 0;
      transition: height 360ms var(--ease-drawer),
                  opacity 220ms var(--ease-out),
                  display 360ms allow-discrete;
    }
    .fcol[open] .fcol__body {
      height: auto;
      opacity: 1;
    }
    @starting-style {
      .fcol[open] .fcol__body { height: 0; opacity: 0; }
    }
  }

  .fbase__txt { justify-content: center; text-align: center; }

  /* ---- Icônes des réseaux, visibles sans clic (15/08/2026) ----
     Voir `reseauxIcones` dans render-contenu.js. La rangée est visible ici,
     et c'est la colonne texte « Réseaux » (même liste, mêmes liens) qui
     s'efface pour ne pas la répéter deux fois. */
  .fsocial {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    /* 🚨 Retour client (15/08/2026) : les icônes touchaient le traitillé de
       « Naviguer » juste en dessous, et n'étaient pas centrées comme le logo
       au-dessus. `padding-bottom` ouvre la respiration manquante avant la
       ligne, `justify-content: center` aligne la rangée sur l'axe du logo.
       🚨 `padding-top` porte maintenant SEUL l'écart logo → icônes (voir la
       note sur `.fmark__logo`) : les TROIS écarts du bloc — tuiles → logo,
       logo → icônes, icônes → Naviguer — sont au même jeton `--space-5`
       (24 px). Vérifié par échantillonnage de l'ENCRE du logo (le WebP est
       détouré au plus près, mais son padding CSS ne l'est pas) : 24,0 / 24,0 /
       24,0 px aux trois tailles mobiles. Ne pas les redésaccorder en ne
       retouchant qu'un seul des trois. */
    padding: var(--space-5) var(--space-4) var(--space-5);
  }
  .fsocial__ic {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--trame-ligne);
    color: var(--text-body);
    transition: color 200ms var(--ease-out),
                border-color 200ms var(--ease-out),
                transform 160ms var(--ease-out);
  }
  .fsocial__ic svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .fsocial__ic:active { transform: scale(0.93); }
  .fsocial__ic:focus-visible,
  .fsocial__ic:hover {
    color: var(--bump-forest);
    border-color: var(--bump-forest);
  }
  .fcol--reseaux { display: none; }

  /* ---- Section Contact : les réseaux passent en icônes ----
     Le pendant exact de `.fsocial` ci-dessus, sur le vert forêt de la section
     Contact — d'où l'encre papyrus et le liseré dilué plutôt que les jetons de
     trame, qui sont calibrés pour le papier du footer.
     ⚠️ Les deux formes s'excluent : on masque le texte au moment même où l'on
     montre les icônes, sinon les liens seraient présents deux fois. */
  .contact__socials { display: none; }
  .csocial {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
  }
  .csocial__ic {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid rgba(208, 211, 193, 0.3);
    color: var(--bump-sage);
    transition: color 200ms var(--ease-out),
                border-color 200ms var(--ease-out),
                transform 160ms var(--ease-out);
  }
  .csocial__ic svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .csocial__ic:active { transform: scale(0.93); }
  .csocial__ic:focus-visible,
  .csocial__ic:hover {
    color: var(--bump-paper);
    border-color: var(--bump-paper);
  }

  /* 🚨 LE SIGLE DISPARAÎT SUR MOBILE (01/09/2026). Retour client : « il y a le
     sigle tout à la fin après la partie réseaux sociaux, il faudrait
     l'enlever, enlever l'espace, et passer de réseaux sociaux directement au
     bouton pour choisir entre Réserver un appel et le formulaire ».
     `display: none` retire bien l'élément et sa propre marge (`margin-top`)
     du flux — mais ça ne suffisait pas : il RESTAIT un vide de 130px, deux
     espacements empilés au lieu d'un seul (retour client du 02/09/2026,
     « il faudrait le retirer complètement... avoir un espace naturel sans
     créer un espace supplémentaire »). Mesuré : `.contact__coords` porte son
     PROPRE `margin-bottom: var(--space-7)` (48px, pour séparer les
     coordonnées du reste QUAND le sigle est là) et `.contact__grid` porte un
     `gap: var(--space-8)` (64px) entre ses deux colonnes empilées — les deux
     s'additionnent (marge + gap ne fusionnent jamais), et ça ne dépend pas du
     sigle : ils étaient déjà tous les deux là avant sa suppression, c'est
     juste que le sigle comblait visuellement l'écart.
     🚨 Le vrai correctif est donc de retirer LA MARGE DES COORDONNÉES, pas
     une marge du sigle : une fois le sigle absent, `.contact__coords` n'a
     plus besoin de sa propre respiration — c'est le `gap` de la grille
     (64px) qui devient LE seul espacement, naturel, entre les réseaux et les
     boutons de choix. Mesuré après correction : 64px pile, contre 130px
     avant.
     ⚠️ MOBILE UNIQUEMENT (≤ 760 px) : sur desktop, où la colonne de gauche a
     de la place, le sigle reste et `.contact__coords` garde sa marge — ce
     n'est pas une suppression de charte, seulement un resserrement de la
     colonne empilée. */
  .contact__logo { display: none; }
  .contact__coords { margin-bottom: 0; }
}

/* 🚨 SOUS 1200 px LA RANGÉE DE PIED REDEVIENT UNE SIMPLE BANDE DE TEXTE.
   Ses trois cases de droite tomberaient à moins de 92 px, et surtout la case
   fusionnée n'a plus la place de tenir le copyright ET les secteurs sur une
   ligne : le texte passait à deux lignes, la rangée devenait plus haute qu'un
   module, et les cases carrées se déformaient (constaté à 768). */
@media (max-width: 1200px) {
  .fbase { display: block; }
  .fbase .ftile { display: none; }
  .fbase__txt { border-right: 0; }
}

/* 🚨 EN MOUVEMENT RÉDUIT, ON ARRÊTE LE RETOURNEMENT — il ne suffit PAS de
   compter sur le bloc global de fin de fichier. Celui-ci force
   `animation-duration: 0.01ms !important`, ce qui ne supprime pas l'animation
   mais la fait boucler à toute vitesse : les tuiles clignoteraient. Il faut
   couper le NOM de l'animation, que le `!important` global ne touche pas.
   Le pivot du `+` en `×`, lui, n'a besoin de rien : c'est une transition, et
   le bloc global la neutralise bien (mesuré à 1440 et 390 : 1e-05s). */
@media (prefers-reduced-motion: reduce) {
  .ftile__in { animation-name: none, none; }
}

/* ============================================================
   Scroll reveal
   ============================================================ */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.reveal.is-in { opacity: 1; transform: none; }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 1024px) {
  :root { --gutter: 32px; }
  /* L'air de la trame suit la gouttière : elle est prise dessus, et sous
     `--maxw-content` la section touche déjà les bords de la fenêtre. */
  :root { --trame-air: 16px; }
  :root { --pin-travel: 230vh; }
  .about-grid { grid-template-columns: 1fr; gap: var(--space-7); }
  /* 🚨 `position: static` cassait le CONTENEUR D'ANCRAGE de `.timeline__track`
     et `.timeline__fill` (tous deux en `position: absolute`, ancrés sur
     `.timeline`). Sans bloc englobant positionné, les deux remontaient
     s'ancrer sur `.about-grid.trame-cadre` (l'ancêtre positionné suivant) :
     mesuré, le trait se dessinait à `top: -560px`, soit ~700 px au-dessus de
     la frise, traversant les paragraphes du texte. C'est ce qui se lisait
     comme « le texte dépasse l'encadré à gauche » (15/08/2026) alors qu'aucun
     texte ne débordait : c'était le filet vert, mal ancré, qui recoupait le
     bloc de paragraphes. `position: relative` referme un bloc englobant sur
     place sans réintroduire le `sticky` desktop (déjà exclu par le `top: auto`
     qui suit). */
  .timeline { position: relative; top: auto; margin-top: var(--space-4); }
  /* 🚨 « BARRE FIXE » (demande client 15/08/2026 : « ça ne marche pas très
     bien, il faut faire une barre fixe »). Sur desktop le trait se remplit au
     fil du scroll (`height: calc(var(--p) * var(--tl-span))`) parce que la
     frise reste épinglée pendant toute la traversée de la section : l'anima-
     tion a le temps de se lire. Sur mobile elle défile avec la page, la
     fenêtre de remplissage est courte, et le trait qui grandit pendant qu'on
     lit donnait l'impression de « ne pas marcher ». Le trait est donc
     TOUJOURS PLEINEMENT TRACÉ ici, plus jamais animé : c'est un repère fixe,
     pas une jauge. Les pastilles restent scroll-réactives (`is-active` /
     `is-current`, posées par `update()`) — seul le fil qui les relie cesse de
     bouger. `--p` continue d'être écrit par le script (il pilote toujours les
     classes des pastilles), cette règle l'ignore simplement pour la hauteur.
     ⚠️ `height: var(--tl-span)`, PAS `100%` : `100%` se serait résolu sur la
     hauteur totale de `.timeline` (qui inclut le texte sous la DERNIÈRE
     pastille), alors que `--tl-span` est la distance entre le CENTRE de la
     première et celui de la dernière — exactement ce que mesure
     `.timeline__track`. Avec `100%` le trait débordait de 83 px sous le
     dernier point (mesuré), en `var(--tl-span)` il s'arrête pile dessus,
     comme le rail. */
  .timeline__fill { height: var(--tl-span); transition: none; }
  .pcard { width: clamp(240px, 33vw, 306px); height: min(62svh, 500px); }
  .contact__grid { grid-template-columns: 1fr; gap: var(--space-8); }

  /* Bento sur 2 colonnes : la grande cellule perd sa 2e rangée mais garde sa
     pleine largeur, elle reste donc la tête de la composition. */
  .results-grid { grid-template-columns: repeat(2, 1fr); }
  .stat--lg { grid-column: span 2; grid-row: span 1; }
}

@media (max-width: 760px) {
  :root { --gutter: 22px; }
  :root { --trame-air: 10px; }
  :root { --header-h: 77px; }
  :root { --section-py: var(--space-9); }
  .nav { display: none; }
  .nav-toggle { display: flex; }

  /* 🚨 LE BOUTON « NOUS CONTACTER » REVIENT SUR MOBILE, À CÔTÉ DE LA POIGNÉE
     (02/09/2026). Il disparaissait jusqu'ici, au motif que « la poignée de
     menu prend le relais » — retour client : *« j'aimerais aussi que sur
     mobile... je rajoute un bouton Nous contacter comme sur desktop juste à
     sa gauche, on peut directement cliquer dessus »*. Le clic direct vaut
     mieux que de forcer un détour par le tiroir pour l'action la plus
     probable du header.
     `margin-left: auto` pousse le bouton (et tout ce qui le suit dans le DOM,
     ici la poignée) vers la droite : avec `.nav` maintenant absente du flux
     (display:none), les 3 items flex restants sont [logo][cta][poignée], et
     `justify-content: space-between` du conteneur les aurait sinon répartis
     à parts égales sur toute la largeur — le bouton se serait retrouvé au
     milieu de la barre au lieu d'être collé à la poignée.
     ⚠️ Rien à ajouter entre le bouton et la poignée en PLUS du `gap` : un
     `margin-left` posé sur `.nav-toggle` s'ADDITIONNERAIT à ce gap (32 + 12 =
     44px, constaté) au lieu de le remplacer — piège déjà rencontré ailleurs
     sur ce site avec des marges qui s'empilent au lieu de fusionner.

     🚨 CE GAP DE 32px (`--space-6`, celui du conteneur flex, partagé avec
     l'écart logo → bouton) EST TROP LARGE ENTRE LE BOUTON ET LA POIGNÉE
     (05/09/2026). Retour client : « le bouton devrait être un poil plus à
     droite, plus cohérent avec le reste. » Mesuré : l'écart bouton → poignée
     (32px) était PLUS GRAND que la marge poignée → bord d'écran (22px, le
     gutter) — un écart intérieur plus large qu'une marge extérieure se lit
     comme un défaut d'intention, le bouton semblant flotter au milieu de la
     barre plutôt que collé à la poignée qui, elle, est bien à sa place.
     Corrigé en RÉDUISANT cet écart à `--space-4` (16px, un cran de l'échelle
     existante, pas une valeur inventée) via une marge NÉGATIVE calculée en
     fonction du gap qu'elle corrige (`--space-4 - --space-6`) : ce n'est PAS
     le piège ci-dessus, qui empilait une marge en plus d'un gap déjà correct
     — ici la marge RETRANCHE délibérément une partie d'un gap trop large pour
     CE couple précis, sans toucher à l'écart logo → bouton, qui reste à 32px
     (il a besoin de plus d'air, coincé contre le mot « STUDIO »). */
  .site-header .btn {
    margin-left: auto;
    margin-right: calc(var(--space-4) - var(--space-6));
  }

  /* Mobile nav drawer.
     ⚠️ Le tiroir part de `--header-h`, jamais d'une valeur en dur : il était
     à `64px` alors que le header fait 77px sous 760px, donc il remontait de
     13px et recouvrait l'arête de la barre. Le token est remesuré par le
     script des pages, le tiroir suit tout seul. */
  .nav.is-open {
    display: flex;
    position: fixed;
    inset: var(--header-h) 0 auto 0;
    flex-direction: column;
    /* ⚠️ `align-items: center` est hérité de `.nav` (rangée desktop) : sans
       cette reprise, les lignes se dimensionnent sur leur texte, le traitillé
       ne fait que la largeur du mot et la flèche vient se coller au libellé.
       Le tiroir n'a rien d'une rangée : ses lignes sont des bandes. */
    align-items: stretch;
    gap: 0;
    background: var(--bump-paper);
    border-bottom: 1px solid var(--border-subtle);
    padding: var(--space-4) var(--gutter) var(--space-6);
  }
  /* ---- Entrée / sortie fluides (15/08/2026) ----
     Le tiroir n'était qu'un `display: none → flex`, un à-coup. Retour client :
     « j'aimerais que quand ça clique, ce soit un peu plus fluide », en
     référence au geste du sélecteur de Contact (`.choice__panels`).
     🚨 CSS SEUL, aucun changement dans nav.js. Le mécanisme : `display` est un
     type de propriété DISCRÈTE (elle ne peut prendre que `none` ou `flex`,
     rien entre deux) ; `transition-behavior: allow-discrete` dit au moteur de
     garder l'ANCIENNE valeur affichée pendant toute la transition plutôt que
     de basculer d'un coup, et `@starting-style` fournit l'état de départ pour
     l'entrée. Résultat : `nav.js` continue de faire un simple
     `classList.toggle("is-open")`, l'entrée ET la sortie s'animent toutes les
     deux sans une ligne de script en plus.
     ⚠️ Repli sans risque : un moteur qui ne comprend pas `allow-discrete`
     ignore tout ce bloc et retrouve l'ancien comportement instantané, jamais
     cassé.
     🚨 LA LISTE `transition` VIT SUR `.nav` (l'état FERMÉ, toujours présent),
     PAS SUR `.nav.is-open`. Une transition CSS se déclenche d'après la liste
     `transition` de l'état D'ARRIVÉE : posée seulement sur `.is-open`, elle ne
     gouvernait donc que l'OUVERTURE (`.is-open` ajouté → état d'arrivée =
     `.is-open`, qui la porte). À la FERMETURE, l'état d'arrivée redevient la
     règle de base, qui n'avait aucune transition déclarée : `display`
     retombait à `none` en une frame, sans jamais animer. Constaté au banc :
     fermeture instantanée malgré une durée de 300 ms. En la posant sur `.nav`
     tout court, elle est présente des deux côtés du bascule. */
  @supports (transition-behavior: allow-discrete) {
    .nav {
      opacity: 0;
      transform: translateY(-12px);
      transition: opacity 300ms var(--ease-drawer),
                  transform 300ms var(--ease-drawer),
                  display 300ms allow-discrete;
    }
    .nav.is-open {
      opacity: 1;
      transform: translateY(0);
    }
    @starting-style {
      .nav.is-open { opacity: 0; transform: translateY(-12px); }
    }
  }
  /* 🚨 LE TIROIR REPREND L'ESTHÉTIQUE DE L'ACCORDÉON DU FOOTER (demande client
     du 14/08/2026 : « j'aimerais avoir le même sur le menu de navigation
     mobile, mais sans le logo et tout ça, il faut que ça reste ressembler à
     une navigation »). Donc : lignes pleine largeur, traitillés horizontaux
     tirés des MÊMES jetons que `.fcol` (`--trame-tiret` / `--trame-vide` /
     `--trame-ligne`), et le corps de texte du footer plutôt que le 12 px
     capitales de la barre desktop — c'est précisément ce petit corps espacé
     qui faisait la différence entre les deux.
     ⚠️ Ces traitillés sont HORIZONTAUX comme ceux du footer, et pour la même
     raison : ce sont des séparateurs de lignes empilées, pas les frontières de
     section de la home auxquelles s'applique la règle « le traitillé de la
     trame est vertical ». Ne pas les prendre pour une régression.
     ⚠️ `repeating-linear-gradient` et non `border-style: dashed` : la longueur
     des tirets d'une bordure n'est pas réglable. Posé en `background` sur
     TOUTES les lignes (et non `a + a`), pour que la première en ait un aussi. */
  .nav.is-open a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-5) 0;
    font-size: 1.0625rem;
    font-weight: var(--weight-text);
    letter-spacing: normal;
    text-transform: none;
    color: var(--text-body);
    background-image: repeating-linear-gradient(
      to right,
      var(--trame-ligne) 0 var(--trame-tiret),
      transparent var(--trame-tiret) calc(var(--trame-tiret) + var(--trame-vide))
    );
    background-size: 100% 1px;
    background-position: left top;
    background-repeat: no-repeat;
  }
  /* 🚨 PAS LE `+` DU FOOTER. Là-bas la croix annonce un dépliage ; ici la
     ligne NAVIGUE, et lui donner le même signe promettrait un panneau qui
     n'existe pas. Une flèche dit « ça mène ailleurs » — c'est ce qui garde au
     tiroir son allure de navigation malgré le vocabulaire du footer.
     ⚠️ On RÉEMPLOIE `::after`, qui porte le filet de survol de la barre
     desktop : il faut donc défaire son positionnement absolu, son `scaleX(0)`
     et son fond, sinon la flèche reste écrasée et invisible. */
  .nav.is-open a::after {
    content: "→";
    position: static;
    width: auto;
    height: auto;
    background: none;
    color: var(--sage-deep);
    font-size: 0.9375rem;
    transform: none;
    transition: transform 220ms var(--ease-out);
  }
  .nav.is-open a[aria-current]::after,
  .nav.is-open a.is-active::after { color: var(--bump-forest); }
  /* La pastille est centrée sous un libellé ; dans le tiroir les liens sont
     des lignes pleine largeur, elle n'y a plus de point d'accroche. C'est la
     flèche qui porte l'état courant, par sa couleur. */
  .nav.is-open a::before { display: none; }
  .nav.is-open a[aria-current],
  .nav.is-open a.is-active { color: var(--bump-forest); }

  /* ---- Hero mobile : DEUX ARCS STATIQUES DE TROIS CARTES (14/08/2026) ----
     🚨 La constellation flottante n'existe plus sous 760 px. Voir hero.js
     (`layoutArc`) pour le pourquoi et la géométrie ; ici il n'y a que
     l'échelle, dont `sizePx()` est le miroir exact.
     ⚠️ L'ancien cap positionnel `.hm-set .hm-card:nth-child(n+9)` a été RETIRÉ :
     la sélection vient désormais de la case « Afficher sur mobile » du CMS, et
     c'est `hm-card--off` (posé par le script) qui masque le reste. */
  :root { --pin-travel: 220vh; }

  /* 🚨 LE TITRE PERD DEUX CRANS ET GAGNE EN MESURE. Il était à
     `clamp(26px, 7.2vw, 34px)` sur `max-width: 16ch` : à 390 px ça fait une
     colonne de ~224 px pour un corps de 28 px, donc UN MOT PAR LIGNE sur
     « communication » ou « l'hôtellerie », et un bloc de titre qui mangeait
     la moitié de la hauteur du hero (retour client : « il prend beaucoup de
     hauteur, parce qu'il n'y a qu'un mot par ligne »). C'est la MESURE qui
     comptait autant que le corps : les deux ont bougé ensemble.
     ⚠️ Ne pas resserrer `max-width` sans rebaisser le corps d'autant — c'est
     leur rapport qui décide du nombre de mots par ligne, pas l'un des deux. */
  .hero h1 { font-size: clamp(21px, 5.6vw, 27px); max-width: 24ch; }

  /* 🚨 Les retours à la ligne saisis dans le CMS sont NEUTRALISÉS ici, et c'est
     tout l'intérêt du réglage ci-dessus : un saut placé à l'œil sur un écran
     large tomberait au milieu d'un mot de la colonne de 24ch et redonnerait
     exactement le « un mot par ligne » corrigé le 14/08/2026. Le titre se
     recoupe donc tout seul sur mobile.
     ⚠️ Le rendu pose une espace AVANT chaque <br> (index.html + build.js) :
     sans elle, masquer le <br> collerait les deux mots qu'il séparait. */
  .hero h1 br { display: none; }

  /* 🚨 LE BLOC DE TEXTE EST VRAIMENT CENTRÉ DANS LE HERO (01/09/2026).
     `.hero__center` porte `justify-content: center` mais un padding
     DISSYMÉTRIQUE (`--header-h + 16px` en haut, 0 en bas) : le centrage se
     faisait donc dans une boîte amputée par le haut, et poussait tout vers le
     bas. Mesuré à 390×844 : centre du bloc à 468 px pour un hero de 844, soit
     46 px sous le milieu. Les deux arcs se calant sur ce bloc (`bandesTexte()`
     dans hero.js), TOUTE la composition suivait — 207 px de vert au-dessus
     contre 114 px en dessous, soit près du double. D'où « trop d'espace en
     haut, pas assez en bas ».
     Le padding bas reprend donc la valeur du haut : la boîte de centrage
     redevient symétrique et le milieu du bloc tombe sur le milieu du hero.
     ⚠️ Le padding HAUT doit rester au moins `--header-h` : c'est lui qui
     empêche le sur-titre de passer sous la barre de navigation. On équilibre
     par le bas, jamais en rabotant le haut. */
  .hero__center {
    padding-bottom: calc(var(--header-h) + 16px);
  }

  /* Échelle d'arc — resserrée pour que TROIS cartes tiennent sur une rangée.
     Miroir de `ARC_VW` dans hero.js : si l'une bouge, l'autre suit.
     ⚠️ Baissée d'un cran le 01/09/2026 (« réduire légèrement la taille des
     éléments pour que tout tienne bien dans l'écran ») : ~9 % de moins, ce qui
     rend à la composition une vraie respiration haut et bas une fois centrée.
     Ne pas descendre plus bas sans revoir `--hm-r` : sous ~20vw les vignettes
     cessent de se lire comme des projets. */
  .hero--arc .hm-card--sm { width: 21vw; }
  .hero--arc .hm-card--md { width: 22.5vw; }
  .hero--arc .hm-card--lg { width: 24vw; }
  .hero--arc .hm-card--xl { width: 25.5vw; }

  .hero__switch-pills { gap: 6px; }
  .hs-btn__label { font-size: 0.75rem; }

  /* Sous 760 px la section n'est plus épinglée (voir index.html) : le rail
     redevient un défileur au doigt, avec accroche pour caler chaque carte. */
  /* ⚠️ `scroll-snap-type` retiré d'ici : `.presta-stage` NE DÉFILE PAS. Voir
     `.presta-scroller { scroll-snap-type }` plus bas, qui est le conteneur de
     défilement réel. */
  .presta-rail { gap: 14px; }

  /* 🚨 LE CHAPÔ PASSE SOUS LE COMPTEUR ET PREND TOUTE LA LARGEUR (02/09/2026).
     Retour client : « le chapô en dessous du titre ne va pas sur mobile
     autant à droite que sur le reste... il faudrait qu'il passe en dessous
     du compteur pour occuper la largeur complète ».
     Cause : `.presta-head` est un FLEX ROW sur toutes les tailles —
     `.section-head` (qui contient le `<h2>` ET le chapô ENSEMBLE) et
     `.presta-count` sont deux items côte à côte. Sur desktop c'est voulu
     (le compteur équilibre la rangée du titre) ; sur mobile, faute
     d'un `flex-wrap`, les DEUX (titre et chapô) restent compressés à côté du
     compteur — mesuré, le chapô n'atteint pas le bord droit du contenu comme
     ailleurs sur le site.
     🚨 Le flex ne peut pas résoudre ça seul : h2 et chapô sont UN SEUL item
     flex (`.section-head`), on ne peut pas faire flotter « seulement le
     chapô » sous le compteur avec `flex-wrap`, qui ferait passer TOUT
     `.section-head` à la ligne, pas juste le paragraphe.
     Solution : `.presta-head` repasse en flux normal (les flottants
     n'existent pas dans un conteneur flex) et `.presta-count` devient un
     FLOTTANT à droite. Le `<h2>` (première ligne de `.section-head`)
     s'enroule autour de lui comme avant ; le chapô, avec `clear: both`,
     descend sous le flottant et retrouve la largeur complète — exactement le
     comportement demandé, sans toucher au balisage ni à l'animation
     `.reveal` (qui reste posée sur `.section-head`, un vrai bloc, jamais
     `display: contents`). */
  .presta-head { display: block; }
  .presta-count {
    float: right;
    margin-left: var(--space-4);
  }
  .presta .section-head { max-width: none; }
  .presta .section-head .intro {
    max-width: none;
    clear: both;
  }

  /* 🚨 RÉSULTATS MASQUÉE SUR MOBILE : PLUS DE DOUBLE ESPACEMENT (02/09/2026).
     Retour client : masquer une section sur mobile (l'exemple donné :
     Résultats) « garde les deux gros espacements... il faut qu'il y ait un
     espace égal, comme s'il n'y avait pas cette partie-là ».
     Mesuré : Prestations a son PROPRE padding-bottom fixe (`--space-10`,
     128px) et À propos son PROPRE padding-top (`--section-py + --space-7`,
     144px — les 48px de `--space-7` sont réservés à l'amorce de la trame,
     nécessaires quel que soit ce qui précède À propos, résultats visible ou
     non). Résultats DISPARU (`display:none`) retire bien SON PROPRE padding,
     mais laisse les deux voisins face à face avec CES DEUX valeurs, conçues
     chacune pour leur propre section, jamais pensées comme UNE SEULE
     transition : 128 + 144 = 272px, contre 192px pour une transition
     normale ailleurs sur la page (deux `--section-py` de 96px chacun).
     🚨 Seul le côté PRESTATIONS est ajusté, jamais À propos : ses 48px
     supplémentaires servent l'amorce de la trame, une contrainte géométrique
     indépendante de résultats — les retirer décrocherait l'amorce du bord du
     graphite qu'elle doit prolonger. Ramener SEULEMENT le padding-bottom de
     Prestations au `--section-py` commun (au lieu de son `--space-10` propre,
     plus généreux) ramène le total à 96 + 144 = 240px — la même chose que
     n'importe quelle AUTRE section qui se retrouverait juste avant À propos.
     🚨 `:has(+ …)` cible PRÉCISÉMENT ce cas et aucun autre : Prestations ne
     change que si son voisin suivant immédiat est Résultats ET porte
     `.masque-mobile` (masquée sur mobile seulement) ou `[hidden]` (masquée
     partout) — un sibling combinator lit la position dans le DOM, pas le
     rendu, donc ça fonctionne même si Résultats est `display:none`. Si
     Résultats reste affichée, ou si un autre voisin se retrouve là après un
     réordonnancement (impossible ici : Résultats n'est pas une des 4
     sections réordonnables), cette règle ne s'applique jamais. */
  .presta:has(+ [data-section-key="resultats"].masque-mobile),
  .presta:has(+ [data-section-key="resultats"][hidden]) {
    padding-bottom: var(--section-py);
  }

  /* 🚨 LA MARGE DU DÉFILEUR DOIT VALOIR LA DEMI-DIFFÉRENCE ÉCRAN / CARTE
     (01/09/2026), sinon la DERNIÈRE carte ne peut pas se centrer.
     Les cartes portent `scroll-snap-align: center` : chacune veut se poser au
     milieu du défileur. Celle du bout n'y arrivait pas — il n'y a plus rien à
     faire défiler derrière elle, la course s'arrêtait donc avec la carte collée
     au bord droit, à la gouttière près (22 px). Elle était la seule de la
     rangée à ne jamais atteindre sa position d'accroche (retour client).
     En ouvrant de chaque côté exactement la moitié du vide qui reste autour
     d'une carte, la première et la dernière atteignent le centre comme les
     dix autres. Mesuré à 390 px : 43 px, contre 22 auparavant.
     ⚠️ `--pcard-w` existe pour que la largeur de carte ne soit écrite QU'UNE
     FOIS : le calcul de la marge en dépend directement. Si l'une bouge, elles
     bougent ensemble — c'est tout l'objet du jeton.
     ⚠️ On n'écrase surtout PAS `--rail-pad` : il aligne aussi le bloc de titre
     et le repère de progression, qui doivent rester à la gouttière du reste de
     la page. La marge du défileur est un réglage distinct. */
  .presta {
    --pcard-w: clamp(230px, 78vw, 310px);
    /* 🚨 `100vw` ET NON `100%` : ce jeton sert AUSSI sur une carte, c'est-à-dire
       sur un enfant de `.presta-rail`, qui est en `width: max-content`. Un
       pourcentage s'y résout contre la largeur du RAIL (~3 800 px), pas contre
       l'écran — mesuré, la marge partait à 1 761 px au lieu de 43.
       C'est le piège déjà consigné pour `--rail-pad` (« un padding en % y est
       circulaire »), et il vaut pour toute valeur en % posée dans le rail.
       En `vw`, le jeton ne dépend plus d'aucun bloc conteneur : le padding du
       défileur et la marge de la dernière carte donnent alors la même valeur.
       ⚠️ Sur une fenêtre de bureau étroite (< 760 px) avec une barre de
       défilement classique, `100vw` la compte : le vide de fin y est plus large
       de quelques pixels. Sans conséquence, et sans objet sur un téléphone, où
       les barres sont en superposition. */
    --rail-bord: max(var(--gutter), calc((100vw - var(--pcard-w)) / 2));
  }

  /* 🚨 LE VIDE DE DROITE EST UNE MARGE SUR LA DERNIÈRE CARTE, PAS UN
     `padding-right` SUR LE DÉFILEUR (01/09/2026, second correctif).

     Premier jet : `padding-inline: var(--rail-bord)` sur `.presta-scroller`.
     Juste sur Chrome de bureau (et donc dans l'émulation mobile des DevTools,
     qui utilise le MÊME moteur) ; faux sur un vrai téléphone. WebKit —
     c'est-à-dire Safari iOS ET Chrome iOS, qui est Safari sous le capot —
     n'inclut PAS le padding de FIN d'un conteneur de défilement dans son
     débordement défilable. Le padding de DÉBUT, lui, est honoré partout : d'où
     l'asymétrie du correctif, qui n'est pas une négligence.
     Conséquence exacte du retour client : « ça marche sur Chrome, pas sur
     mobile ; l'espace à droite reste collé ». Il n'y avait pas d'espace du
     tout, la dernière carte butait sur le bord et ne pouvait pas se centrer.

     Une MARGE sur le dernier enfant est comptée dans le débordement défilable
     par tous les moteurs : c'est le contournement standard, et il ne demande
     aucune arithmétique de `gap` (le `gap` ne s'applique qu'ENTRE les cartes,
     donc rien ne s'ajoute après la dernière).
     ⚠️ Ne pas « simplifier » en remettant `padding-inline` : le banc headless
     (Chrome) validerait la régression sans rien voir. */
  .presta-scroller { padding-left: var(--rail-bord); padding-right: 0; }
  .presta-rail > :last-child { margin-right: var(--rail-bord); }

  /* 🚨 L'ACCROCHE VIT SUR LE CONTENEUR QUI DÉFILE, ET C'EST `.presta-scroller`.
     Elle était posée sur `.presta-stage`, qui ne défile pas (c'est
     `.presta-scroller` qui porte `overflow-x: auto`) : la déclaration
     n'avait donc AUCUN effet et le mobile retombait silencieusement sur le
     `x proximity` de la règle de base. En `proximity`, le navigateur est libre
     de ne pas caler la carte — c'est le « je n'arrive pas à centrer la carte »
     du retour client. Le palier mobile veut bien une accroche ferme, comme
     documenté depuis le 29/07/2026. */
  .presta-scroller { scroll-snap-type: x mandatory; }

  .pcard {
    width: var(--pcard-w);
    height: min(64svh, 480px);
    min-height: 360px;
    --pc-box: 52px;
  }
  .pcard__in { padding: var(--space-4); }

  /* Section bien plus haute que large : la dérive des traits passe de ~77 % à
     ~215 % de la largeur. Il faut donc partir beaucoup plus à gauche, et
     l'angle est redressé pour limiter la casse. Le pas reste proportionnel-
     lement plus large qu'en desktop (32 % contre 26 %) : sur un écran étroit,
     trois traits suffisent à faire un fond. */
  .results__rules {
    --rl-depart: -215%;
    --rl-pas: 32%;
    --rl-angle: 24deg;
    --rl-haut: 130%;
  }

  /* Une colonne : les emprises explicites du bento DOIVENT être remises à
     `span 1`, sinon `span 2` déborde de la grille à une seule piste. */
  .results-grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .stat--lg, .stat--wide, .stat--md { grid-column: span 1; grid-row: span 1; }
  .stat--lg { --sf-box: 54px; }

  /* 🚨 LES UNITÉS REMONTENT SUR MOBILE (01/09/2026).
     `.stat__suf` (« % », « CHF ») et `.stat__sep` (le mot « à ») sont exprimés
     en `em` du chiffre : le rapport qui les fait reculer joliment sur un
     chiffre de 92 px les rend minuscules sur un chiffre de 26 px. Mesuré à
     390 px : la cellule large tombe à 26 px de chiffre, donc 15 px de suffixe
     et 13 px de séparateur — plus petit que le corps de texte courant, pour
     une information qui fait partie du chiffre (retour client : « sur desktop
     le rendu est bon, mais sur mobile c'est beaucoup trop petit »).
     ⚠️ Le rapport monte, il ne disparaît pas : l'unité doit rester en retrait
     du nombre, sinon « 403 % » se lit comme deux nombres. 0,74 conserve la
     hiérarchie tout en restant au-dessus du corps courant.
     ⚠️ Le chiffre de la cellule large monte aussi son plancher (26 → 34 px) :
     c'est lui qui rendait le suffixe si petit, et une fourchette
     « 3’400 à 11’200 CHF » tient encore sur la largeur à 390 px. */
  .stat--wide .stat__figure { font-size: clamp(34px, 8.6vw, 46px); }
  .stat__suf { font-size: 0.74em; }
  .stat__sep { font-size: 0.66em; margin-inline: 0.2em; }
  .team-grid { grid-template-columns: 1fr; gap: var(--space-8); }
  .form__row { grid-template-columns: 1fr; }

  /* Le segmenté passe en deux lignes : côte à côte sur un écran étroit, les
     deux sous-titres s'enroulent sur trois lignes chacun et la barre devient
     plus haute que le contenu qu'elle commande. */
  .choice { grid-template-columns: 1fr; }
  /* Une seule colonne : la pastille prend toute la largeur et glisse vers le
     BAS. Sans reprise, elle garderait sa demi-largeur et sa course latérale. */
  .choice__thumb {
    width: calc(100% - 12px);
    height: calc((100% - 18px) / 2);
    bottom: auto;
  }
  #tabMessage[aria-selected="true"] ~ .choice__thumb {
    transform: translateY(calc(100% + 6px));
  }
  /* Réserve mobile, mesurée elle aussi (iframe Cal = 752px à 390px de large,
     contre 480px réservés auparavant). Voir la note de `.cal-card`. */
  .cal-card, .cal-embed { min-height: 754px; }

  /* Pont : colonnes empilées. Le panneau remonte toujours par-dessus le hero. */
  .hero-outro { padding-top: clamp(72px, 11vh, 104px); }
  /* Une colonne : pictos + titre, puis le texte et l'action. Les rangées et les
     colonnes explicites du desktop DOIVENT être remises à `auto`, sinon le titre
     et le texte se superposent tous les deux sur la rangée 2. */
  .bridge {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    row-gap: clamp(22px, 5vw, 32px);
  }
  .bridge__sectors,
  .bridge__title,
  .bridge__aside,
  .bridge__actions { grid-column: 1; grid-row: auto; }
  .bridge__sectors { --st-box: clamp(38px, 11vw, 48px); }
  /* Le sigle est plus haut que large : on le cale sur la HAUTEUR, y compris ici.
     (Le logo complet, lui, demandait l'inverse — d'où l'ancienne règle en
     `width`, qui rendait le sigle démesuré.) */
  .bridge__mark { height: 145%; opacity: 0.18; }
  /* Bouton pleine largeur, mais pas l'email : étiré, son soulignement traverse
     l'écran et se lit comme un séparateur. */
  .bridge__actions { flex-direction: column; align-items: stretch; }
  .bridge__actions .btn { justify-content: center; }
  .bridge__mail { align-self: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  /* Plus spécifiques que `.reveal` : sans reprise explicite, leur flou et leur
     décalage survivraient à la règle ci-dessus. */
  .bridge > .reveal { transform: none; filter: none; }

  /* Plus de recouvrement : la bande passe à zéro, le pont redevient une section
     qui suit le hero. Les coins arrondis, eux, restent (c'est du dessin, pas du
     mouvement). hero.js suit automatiquement via #heroBand. */
  :root { --stack-band: 0px; }

  /* Hero : tout est statique — épure dessinée, contenu visible, aucune boucle
     (hero.js coupe de son côté flottement, drag et bascules animées) */
  .hero__el { animation: none !important; opacity: 1; transform: none; }
  .hero__constructs { animation: none !important; }
  .hm-card__in { animation: none !important; }
  .hero__bridges line { animation: none !important; }
  .hero .cline,
  .hero .cline--loop {
    animation: none !important;
    stroke-dashoffset: 0;
  }
}

/* ---------------- Page Mentions légales ----------------
   Colonne de lecture étroite : le confort de lecture prime sur la largeur. */
.legal h1 { margin-bottom: var(--space-4); }
.legal h2 {
  font-size: clamp(1.1rem, 2vw, 1.35rem);
  margin: var(--space-7) 0 var(--space-3);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-subtle);
}
.legal p {
  max-width: 68ch;
  color: var(--text-body);
  line-height: var(--leading-relaxed);
  margin: 0 0 var(--space-4);
}
.legal a { color: var(--text-accent); }
.legal__back { margin-top: var(--space-8); }

/* ---------------- Page 404 ---------------- */
.notfound { min-height: 58vh; display: flex; flex-direction: column; justify-content: center; }
.notfound h1 { max-width: 16ch; }
.notfound .intro { max-width: 52ch; }
.notfound__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }

/* Laptop: reduce the spacing/type scale without changing browser zoom.
   Mobile keeps its own composition; large desktop keeps its existing tokens. */
@media (min-width: 1001px) and (max-width: 1599px) {
  :root { font-size: clamp(14px, 1.05vw, 16px); --section-py: clamp(64px, 5.5vw, 88px); }
  .presta-head { margin-bottom: 1.5rem; }
  .presta .section-head h2 { margin-bottom: .75rem; }
  .presta-scroller { padding-top: 16px; padding-bottom: 28px; }
  .pcard { --pc-box: 54px; }
  .pcard__desc { font-size: max(14px, .9375rem); }
  .contact h2 { font-size: clamp(34px, 3.2vw, 46px); }
  .phead { --ph-pt: clamp(38px, 8svh, 90px); --ph-pb: clamp(120px, 18svh, 200px); }
  .phead h1 { font-size: clamp(40px, 5.1vw, 72px); }
}
@media (min-width: 761px) {
  .presta .pcard {
    width: var(--presta-card-width, clamp(250px, 23vw, 344px));
    height: var(--presta-card-height, auto);
    min-height: 0;
    aspect-ratio: 5 / 8;
  }
  .presta.is-pinned .presta-stage {
    padding-top: calc(var(--header-h) + 12px);
    padding-bottom: 12px;
  }
}
@media (min-width: 1600px) {
  .presta .pcard {
    width: var(--presta-card-width, clamp(320px, 17vw, 420px));
    height: var(--presta-card-height, min(60svh, 640px));
    aspect-ratio: auto;
  }
}
