/* =========================================================
   Rivmont — sections de la page d'accueil
   Ordre : hero, manifesto, positioning, expertises, method,
   work, results, territories, clients, final CTA, inquiry.
   ========================================================= */

/* =========================================================
   01 — HERO
   Deux états sur une piste de scroll. Toute l'animation dérive de
   variables écrites par ui/hero.js — une par couche, chacune avec sa
   propre courbe et sa propre fenêtre :

     --p         progression brute, 0 → 1
     --stage-p   sculpture et anneau
     --ring-p    anneau seul, plus rapide
     --intro-p   titre et note
     --detail-p  fiche éditoriale

   Aucune n'est animée par le temps : le scroll est le seul moteur.
   Au repos, toutes valent 0 et rien n'est composé.
   ========================================================= */
.hero{
  position:relative;
  min-height:100svh;
  background:var(--bg);
}

/* Piste : un écran de course par transition — quatre paliers, donc
   trois transitions — et le contenu reste collé pendant tout ce temps.
   `overflow` reste sur l'enfant collant — posé sur la piste, il
   désactiverait `position:sticky` sans rien signaler.

   S'y ajoute la **sortie** : un écran de plus, pendant lequel la
   sculpture reste collée et le panneau suivant lui glisse par-dessus au
   lieu de la pousser hors de l'écran. C'est le mécanisme que le
   manifesto utilisait, et ce pour quoi `.seq` existe.

   Deux choses en découlent, et elles sont solidaires :
   — la séquence ne s'y allonge pas. `ui/sequence.js` retire cette sortie
     de sa course, en lisant cette marge négative ; la progression vaut
     donc déjà 1 quand le panneau entre par le bas.
   — la marge négative fait remonter ce qui suit d'exactement un écran.
     Elle traverse `.seq` par fusion des marges — ce conteneur n'a ni
     bordure ni remplissage, et c'est ce qui le permet. */
.hero.is-sequenced{
  height:calc(var(--hero-track,430svh) + var(--hero-exit,100svh));
  margin-bottom:calc(-1 * var(--hero-exit,100svh));
}

.hero__frame{
  position:relative;
  min-height:100svh;
  overflow:hidden;
  isolation:isolate;
}
.hero.is-sequenced .hero__frame{
  position:sticky;top:0;
  height:100svh;
  min-height:0;
  /* Solidaire du panneau clair. Ce que le cadre découvre en se
     fermant n'est pas le noir du site mais la surface sur laquelle le
     hero se retire — la même que celle du panneau qui monte, sans quoi
     la bordure et le panneau seraient deux blancs différents.

     Rien à piloter : la scène est absolue et couvre toute la boîte, donc
     ce fond n'existe à l'écran qu'à mesure que la découpe l'ouvre. Il
     apparaît exactement au rythme du retrait, sans une valeur animée.

     Si le panneau redevient noir, cette ligne repasse à `--bg`. */
  background:var(--marble);
}

/* ---------- fond ----------
   Presque noir, avec une seule respiration au point d'où vient la
   lumière. Il ne doit jamais virer au gris.

   Le dégradé a été ouvert d'environ deux tiers pour se rapprocher de la
   référence client, où le sujet se détache d'un sol gradué au lieu de
   flotter dans du vide. Les valeurs sont dans `tokens.css` — c'est de là
   qu'on le dose — et désormais de là seulement : le vignettage qui le
   rattrapait dans les angles est déposé, voir plus bas. */
/* La sortie du hero : la scène se retire dans un cadre qui se ferme,
   pendant que le panneau suivant monte. Voir le bloc « le retrait »
   plus bas — c'est là que le mouvement est décrit et réglé.

   `--hero-out` est publiée sur la racine par `ui/hero.js` : elle vaut 0
   sur la séquence des quatre paliers et monte de 0 à 1 sur la montée du
   panneau. C'est la source unique de toute la sortie — le retrait de la
   scène ici, le bord d'attaque du panneau dans `layout.css`, et
   l'arrivée des mains plus bas.

   Un flou piloté au scroll a été essayé ici, avec un voile de dégradé
   bleu par-dessus, puis retiré : voir `docs/DECISIONS.md`. */
.hero__stage{
  position:absolute;
  inset:0;
  z-index:var(--z-canvas);
  background:
    radial-gradient(circle at 58% 45%,
      var(--hero-lift) 0%,
      var(--hero-mid) 35%,
      var(--hero-floor) 75%),
    var(--hero-floor);
}

/* ---------- le retrait ----------
   LE PRINCIPE : LE CADRE SE FERME, L'IMAGE NE SE REDUIT PAS. C'est toute
   la différence entre un retrait et un dézoom. La sculpture garde
   exactement sa taille, son cadrage et sa position dans la scène ; ce
   qui bouge, c'est la fenêtre par laquelle on la regarde, qui se referme
   vers son centre. Rien n'est touché côté 3D — ni la caméra, ni
   l'échelle du modèle, ni le rendu : c'est une découpe CSS posée sur le
   conteneur.

   LE CADRE SUIT LE PANNEAU, ET C'EST CE QUI LE REND HOMOTHÉTIQUE. Le
   panneau de `#positioning` monte sur la scène en flux, donc à la
   vitesse du scroll : il fait toujours le bord bas de ce qu'on voit du
   hero (aucune découpe adoucie ne le devance — mesuré : la courbe de
   `--hero-out` part trop lentement). Une première version fermait les
   côtés de 15 % sur les 62 premiers pour cent de la course, et le bas
   d'abord : le client a vu un format qui change, puis, une fois les
   côtés arrêtés, un cadre qui « monte » — le bord bas seul, poussé par
   le panneau. D'où cette version : LES CÔTÉS SE FERMENT À LA VITESSE DU
   PANNEAU, LINÉAIREMENT (`--hero-exit-lin`, la progression brute que
   `ui/hero.js` publie à côté de `--hero-out`), d'un seul mouvement du
   début à la fin. ET ILS NE SE FERMENT QUE DE 15 % CHACUN : c'est la
   fin de la référence (verostudio.com, voir `docs/DECISIONS.md`), que
   le client a demandée — le cadre finit en bande large et basse, à
   70 % de la largeur, qui s'amincit jusqu'à disparaître sous le haut de
   l'écran. Un essai à 50 % (les côtés se rejoignant au centre, le
   rectangle réduit vers un point) était homothétique mais finissait
   petit ; le client a préféré la bande, et l'a voulue un peu plus
   étroite que les 12 % d'un premier réglage. Le bas de la
   découpe suit la
   loi du panneau (100 % × t) : il coïncide avec son bord, et rien ne
   dépend de qui des deux est devant. Le haut ne se ferme pas — voir la
   découpe. Plus de dérive : une translation en plus d'une réduction se
   lirait comme un second mouvement.

   Les valeurs sont nommées ci-dessous et ne se touchent qu'ensemble :
   c'est leur rapport qui fait le mouvement, pas leur valeur absolue. */
.hero.is-sequenced .hero__stage{
  /* Fermeture au bout de la course, en part du bord : 15 % de chaque
     côté (la bande finale fait 70 % de la largeur), 100 % en bas (le
     bord du panneau). Les deux axes sont séparés parce qu'ils ne se
     règlent pas ensemble sur petit écran : voir la requête média plus
     bas. */
  --retract-close-x:15%;
  --retract-close-y:100%;
  /* Dérive vers le haut, en hauteur d'écran. Zéro : voir ci-dessus. */
  --retract-drift:0svh;

  /* LINÉAIRE, comme le panneau. Repli sur `--hero-out` sans le module
     (qui les publie ensemble). */
  --retract:var(--hero-exit-lin, var(--hero-out, 0));

  /* LE HAUT NE SE FERME PAS, et ce n'est pas une simplification : c'est
     la composition de la référence — image à fond perdu en haut, marges
     sur les côtés et en bas — et c'est aussi ce qui garde le header sur
     la scène jusqu'au bout.

     Sans cela la bordure claire atteignait la barre bien avant que le
     panneau ne la couvre : 15 % de 860 px font 129 px, la barre en fait
     98, donc dès 0,47 de course le « RIV » crème se retrouvait sur du
     marbre. Le décaler aurait demandé un second seuil de thème à tenir
     accordé avec une géométrie ; ne pas fermer le haut supprime le
     problème au lieu de le rattraper. (La bande latérale, elle, passe
     sous le logotype pendant la course : `--riv` dans `layout.css` est
     calé dessus.) */
  clip-path:inset(0
                  calc(var(--retract-close-x) * var(--retract))
                  calc(var(--retract-close-y) * var(--retract))
                  calc(var(--retract-close-x) * var(--retract)));
  transform:translate3d(0, calc(var(--retract) * -1 * var(--retract-drift)), 0);
  will-change:clip-path, transform;
}

/* La typographie n'est pas dans le cadre : elle est posée par-dessus, et
   elle s'efface avant lui. C'est l'ordre qui compte — le texte part, le
   cadre se referme sur ce qui reste, le panneau arrive. S'ils partaient
   ensemble, la sortie n'aurait qu'un temps au lieu de trois.

   Pas de transition : la valeur est tenue par le scroll, comme partout
   ailleurs sur cette page. */
.hero.is-sequenced .hero__ui{
  /* La course de ce fondu n'est pas un goût, c'est une géométrie : le
     repère latéral est à 7 % du bord, et la bordure claire l'atteint
     quand la fermeture horizontale dépasse cette valeur — à 15 % par
     côté, linéaire, c'est 7/15 = 0,47 de la course de sortie. Le texte
     doit donc être parti avant, sinon du crème se retrouve sur du
     marbre. Il l'est à 0,40, sur la même progression brute que le
     cadre — le texte part, le cadre se ferme sur ce qui reste. */
  opacity:calc(1 - clamp(0, calc((var(--hero-exit-lin, var(--hero-out, 0)) - .05) / .35), 1));
}

@media (max-width:760px){
  /* EN PORTRAIT, LES CÔTÉS NE SE FERMENT PLUS DU TOUT, et c'est une
     mesure, pas un goût. La bande latérale y ferait 35 px pour un
     logotype qui en occupe 70 : elle n'en couvrirait jamais que la
     première lettre, donc aucune couleur d'encre ne serait juste — crème
     laisse un « R » invisible sur le marbre, noir rend tout le reste du
     mot invisible sur la scène. Ne pas ouvrir de bande supprime le
     dilemme, et 35 px de marge sur un téléphone n'encadraient rien de
     toute façon.

     Le retrait garde donc son bas — le bord du panneau —, qui suffit à
     le lire. */
  .hero.is-sequenced .hero__stage{
    --retract-close-x:0%;
  }
}

/* ---------- trame ----------
   Six colonnes. Repère architectural de toute la scène : elle ne bouge
   jamais, et c'est sur ses lignes que le contenu vient se caler. */
.hero__grid{
  position:absolute;inset:0;
  z-index:0;
  pointer-events:none;
  background-image:linear-gradient(90deg,var(--grid-line) 0 1px,transparent 1px);
  background-size:calc(100% / 6) 100%;
}

/* L'anneau n'est plus ici : il est redevenu un objet de la scène,
   enfant du pivot du modèle (`CONFIG.halo` dans hero.js). Un calque CSS
   ne pouvait pas suivre la sculpture quand elle s'oriente sous le
   pointeur — il serait resté plat et immobile devant elle. */

.hero__canvas{position:relative;z-index:2;display:block;width:100%;height:100%}

/* Vignettage — EN RÉSERVE. Il fermait les angles sans éclaircir le
   centre, et il a tenu tant que la scène était à bord perdu.

   Le retrait l'a rendu contradictoire : le cadre se ferme sur une arête
   franche, et le voile posait juste avant elle une extinction
   progressive — l'image s'éteignait vers ses bordures au lieu d'être
   coupée net. Deux gestes opposés au même endroit ; c'est la coupe qui
   gagne, elle est le sujet de la sortie.

   Ce qu'il faut savoir si on le remet : il travaillait **contre**
   `--hero-lift`, qu'il rattrapait dans les angles jusqu'à 62 % de noir.
   Sans lui, le fond du hero se dose donc d'un seul endroit, les trois
   jetons de `tokens.css`, et l'ancien équilibre entre les deux n'a plus
   à être tenu. Son élément est commenté dans `index.html`.

.hero__veil{
  position:absolute;inset:0;z-index:3;pointer-events:none;
  background:radial-gradient(112% 88% at 54% 48%,
    rgba(0,0,0,0) 52%,rgba(0,0,0,.28) 78%,rgba(0,0,0,.62) 100%);
}
*/

.hero__loader{
  position:absolute;left:50%;bottom:14%;z-index:4;
  transform:translateX(-50%);
  width:150px;height:1px;margin:0;
  background:var(--line-soft);
  transition:opacity .6s var(--ease);
}
.hero__loader span{
  display:block;height:100%;width:0%;
  background:var(--accent);
  transition:width .35s var(--ease);
}
.hero__loader.is-done{opacity:0}

/* Pastille tournante, masquée depuis le cadrage rapproché : elle
   tombait sur la sculpture. Conservée pour un usage ultérieur. */
.badge{
  position:absolute;left:9%;bottom:20%;z-index:4;
  width:112px;height:112px;
  place-items:center;
  color:var(--muted);
  opacity:0;
  transition:opacity .9s var(--ease) .5s;
  display:none;
}
.badge.is-in{opacity:1}
.badge__ring{
  position:absolute;inset:0;width:100%;height:100%;
  fill:currentColor;
  animation:spin 22s linear infinite;
}
.badge__ring text{
  font-family:var(--sans);font-size:8.1px;font-weight:400;letter-spacing:2.35px;
}
.badge__core{display:grid;place-items:center;width:34px;height:34px;color:var(--accent-text)}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------- grain ----------
   Bruit désaturé en SVG inline : aucune requête, aucune image à
   décoder. Opacité à la limite du perceptible. */
.hero__grain{
  position:absolute;inset:0;
  z-index:4;
  pointer-events:none;
  opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:160px 160px;
}

/* ---------- interface ---------- */
.hero__ui{
  position:relative;
  z-index:var(--z-content);
  min-height:100svh;
  /* Ce wrapper couvre toute la section : sans cela il intercepte le
     pointeur au-dessus de la statue, et le survol du marbre ne reçoit
     plus rien. Les enfants qui doivent répondre le redemandent. */
  pointer-events:none;
}
.hero.is-sequenced .hero__ui{height:100%;min-height:0}

/* Repère latéral : un trait pour l'étape courante, un point pour les
   autres. Il ne bouge pas de toute la séquence. */
.hero__rail{
  position:absolute;
  left:7%;top:50%;
  transform:translateY(-50%);
  display:grid;gap:14px;
  pointer-events:auto;
}
/* Seule la couleur du trait au repos garde une transition de temps :
   c'est un retour de survol, pas un mouvement de la séquence. */
.hero__rail button{
  display:flex;align-items:center;height:12px;
  color:var(--faint);
  transition:color var(--dur) var(--ease);
}
/* `--on` est écrit palier par palier par ui/hero.js, sur la progression
   du scroll : le trait s'allonge en approchant du sien et se rétracte en
   le quittant. Pas de transition ici — elle ajouterait un retard de
   temps à un mouvement dont le scroll est le seul moteur. */
.hero__rail button i{
  position:relative;
  display:block;height:1px;
  width:calc(5px + var(--on,0) * 39px);
  background:currentColor;
}
/* Le bleu se superpose au trait au lieu de le remplacer : deux couches
   qui se croisent en opacité, là où interpoler une couleur demanderait
   `color-mix`. */
.hero__rail button i::after{
  content:'';
  position:absolute;inset:0;
  background:var(--accent);
  opacity:var(--on,0);
}
.hero__rail button:hover{color:var(--ink-soft)}
/* Sans JavaScript — ou sous `prefers-reduced-motion` — personne n'écrit
   `--on` : c'est `aria-current`, posé dans le HTML, qui marque l'étape. */
.hero:not(.is-sequenced) .hero__rail button[aria-current="true"] i{width:44px}
.hero:not(.is-sequenced) .hero__rail button[aria-current="true"] i::after{opacity:1}

/* Hors séquence — pas de JavaScript, ou `prefers-reduced-motion` — les
   trois couches restent dans le flux et se lisent l'une après l'autre.
   C'est la règle du projet : le contenu ne dépend jamais du script pour
   être lisible. Le positionnement absolu et la superposition sont donc
   entièrement derrière `is-sequenced`. */
.intro,.intro__note,.detail,.credo,.outro{pointer-events:auto}
.intro__note,.detail,.credo,.outro{margin-top:var(--s-xl)}
/* ENTRE LE PREMIER RENDU ET `is-sequenced` : les couches des paliers
   02 à 04 ne prennent pas de place. Sans cette règle, la fiche
   s'affichait dans le flux sous le titre pendant que les modules
   se chargeaient, puis disparaissait quand la séquence s'installait —
   un décalage de mise en page visible (CLS 0,37 mesuré). `js-motion`
   est posée par le script inline du `<head>`, avant le premier rendu,
   et jamais sous `prefers-reduced-motion` : le repli en flux reste
   entier sans script et sous mouvement réduit. */
html.js-motion .hero:not(.is-sequenced) :is(.detail,.credo,.outro,.intro__note){
  position:absolute;
  visibility:hidden;
}
/* Et le logotype est déjà à sa place : la composition de `is-sequenced`,
   à l'identique, pour que le premier rendu soit le rendu final. */
html.js-motion .hero:not(.is-sequenced) .intro{
  position:absolute;
  left:var(--gutter);right:var(--gutter);
  top:50%;
  margin:0;
  text-align:center;
  transform:translateY(-50%);
  pointer-events:none;
}
.hero:not(.is-sequenced) .hero__ui{
  padding:calc(var(--header-h) + var(--s-lg)) var(--gutter) var(--s-2xl);
}

/* ---------- état 1 · le logotype et l'accroche ----------
   LA COMPOSITION DE LA RÉFÉRENCE (Vero) : le mot-marque, grand, au
   milieu de l'écran sur l'image, et l'accroche juste dessous, petite,
   sur une ligne, qui mêle italique et capitales — chez eux « Custom
   SCULPTURE of your WEDDING DRESS. », ici « Nous façonnons L'EXCEPTION
   DIGITALE. », les deux états de Cormorant que `#positioning` emploie
   déjà (italique bas-de-casse contre capitales romaines à chasse). Le
   bloc entier est centré à mi-hauteur ; il ne dérive que vers le haut
   en s'effaçant — un glissement latéral d'une composition symétrique
   se lirait comme un défaut. Une composition d'avant mettait le titre
   à gauche sous l'anneau avec une note à droite (en réserve). */
.hero.is-sequenced .intro{
  position:absolute;
  left:var(--gutter);right:var(--gutter);
  top:50%;
  margin:0;
  text-align:center;
  transform:translateY(-50%);
  pointer-events:none;
}
/* LA SORTIE EST CELLE DES AUTRES TEXTES DU HERO : chaque ligne se
   retire derrière son masque en s'effaçant, sur `--intro-fade`, au
   lieu d'un fondu du bloc qui dérivait de 20 px (déposé). Même masque
   que `.mask` : il dépasse de la boîte pour ne rogner ni la cédille ni
   les jambages, compensé par la marge. */
.intro__mark,
.intro__title{
  overflow:hidden;
  padding:.14em 0 .1em;
}
.intro__mark i,
.intro__title i{
  display:block;
  font-style:normal;
  transform:translateY(calc(var(--intro-fade,0) * 110%));
  opacity:calc(1 - var(--intro-fade,0));
}
/* LE LOGOTYPE : le registre du footer et de la signature — Cormorant,
   capitales, chasse .06 em, « mont » en bleu — mais EN GRAS, 500, le
   seul des trois à l'être (demandé) ; 6,5 vw (12, puis 9, « réduit le
   titre » deux fois) : le mot couvre la poitrine, pas le torse. La
   marge droite retire l'interlettrage final pour que le mot soit
   centré sur son encre. */
.intro__mark{
  margin:-.14em 0 -.1em;
  font-family:var(--serif);
  font-weight:500;
  font-size:clamp(48px, 6.5vw, 110px);
  line-height:.8;
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--ink);
}
.intro__mark .wordmark{margin-right:-.06em}
.intro__mark .wordmark span{color:var(--accent)}
/* L'ACCROCHE, juste sous le mot : petite, une ligne, dans le registre
   de « Ce que vous bâtissez » (`.showcase__line`) — Cormorant 300,
   capitales romaines contre bas-de-casse italique. Les mêmes deux
   compensations qu'à la scène : les capitales prennent une chasse
   (plus large qu'à 100 px, une capitale a besoin d'autant plus d'air
   qu'elle est petite), l'italique n'en prend aucune et gagne six pour
   cent de corps. La version en Manrope (300 puis capitales 500) est
   déposée. */
.intro__title{
  margin:calc(clamp(10px,1.8vh,22px) - .14em) 0 -.1em;
  font-family:var(--serif);
  font-weight:300;
  font-size:clamp(18px,1.9vw,30px);
  line-height:1.1;
  letter-spacing:-.005em;
  color:var(--ink);
}
.intro__title .caps{
  text-transform:uppercase;
  letter-spacing:.06em;
}
.intro__title em{
  font-style:italic;
  letter-spacing:0;
  font-size:1.06em;
}

/* Bloc éditorial de droite. Presque un élément graphique : il ne doit
   pas prendre le pas sur la sculpture. */
.hero.is-sequenced .intro__note{
  position:absolute;
  left:66vw;bottom:9vh;
  width:200px;
  margin:0;
  font-size:10px;
  line-height:1.45;
  color:rgba(255,255,255,.45);
  transform:
    translateX(calc(var(--intro-p,0) * 11vw))
    translateY(calc(var(--intro-fade,0) * -16px));
  opacity:calc(1 - var(--intro-fade,0));
  pointer-events:auto;
}

/* ---------- état 2 · la fiche ----------
   Calée sur la trame : son bord gauche tombe sur la première ligne de
   colonnes, au sixième de l'écran — aux deux sixièmes elle passait sur
   la sculpture. Le repère latéral, à 7 %, reste à sa gauche. */
.hero.is-sequenced .detail{
  position:absolute;
  left:calc(100% / 6);
  top:50%;
  /* Comme pour `.credo` : `top` se mesure jusqu'au bord de marge, et la
     marge du flux (`--s-xl`, le repli sans JavaScript) décalait le bloc
     d'autant sous le centre. */
  margin:0;
  transform:translateY(-50%);
  width:min(38vw,570px);
}
.detail__block + .detail__block{margin-top:clamp(26px,4vh,52px)}

/* Chaque ligne remonte depuis son propre masque. */
.detail__title,
.detail__metric,
.detail__text,
.detail__rule{display:block;overflow:hidden}
.detail__title span,
.detail__metric span,
.detail__text span{
  display:block;
  transform:translateY(calc((1 - var(--o,1)) * 110%));
  opacity:var(--o,1);
}

.detail__rule{height:1px;margin-bottom:clamp(12px,1.6vh,20px)}
.detail__rule i{
  display:block;height:1px;
  background:var(--line-soft);
  transform:scaleX(var(--o,1));
  transform-origin:left;
}

/* Les trois titres au même corps que les déclarations des paliers 03
   et 04 : une seule échelle de titre dans le hero (3,6 vw, puis 3,2,
   puis 2,8, 2,5, 2,2 : « réduis encore un peu », trois fois). EN CAPITALES ROMAINES, un seul
   état, avec la chasse des capitales de « Ce que vous bâtissez » — le
   mélange capitales / italique de la déclaration a été essayé ici et
   retiré (« ne passe pas »). « SITE WEB / SUR MESURE » sur deux lignes
   coupées à la main, les deux autres sur une seule. Le masque dépasse de la boîte pour les jambages de
   l'italique, compensé par la marge — comme `.mask`. */
.detail__title,
.detail__metric{
  margin:-.14em 0 calc(clamp(8px,1.2vh,14px) - .1em);
  padding:.14em 0 .1em;
  font-family:var(--serif);
  /* En gras — 500, la graisse la plus forte du site (demandé). */
  font-weight:500;
  font-size:clamp(26px,2.2vw,40px);
  line-height:.9;
  text-transform:uppercase;
  letter-spacing:.012em;
  color:var(--ink);
}
/* Seul « SITE WEB » (`.br`) coupe la ligne. */
.detail__title .br{display:block;font-style:normal}
.detail__text{
  margin:0;
  /* Lisible : 10 px en blanc à 45 % ne l'était pas. `--ink-soft`
     (4,6:1 sur le noir) et un corps de texte courant ; les lignes
     s'équilibrent pour que le paragraphe fasse un bloc, pas une
     dernière ligne orpheline. */
  font-size:clamp(13px,.95vw,15px);
  line-height:1.55;
  color:var(--ink-soft);
  /* Trois lignes, pas deux longues : la colonne (38 vw pour le titre
     en capitales) est plus large que ce qu'un paragraphe doit faire. */
  /* La première phrase est seule sur sa ligne (`<br>`), le reste
     s'enchaîne sur la largeur de la colonne : à 34 em, il laissait un
     mot orphelin sur une quatrième ligne. Pas d'équilibrage, qui
     recouperait les segments. */
}

/* ---------- palier 03 · la conviction ----------
   Plan rapproché sur le fils de droite : la sculpture tient la moitié
   gauche de l'écran, le texte s'installe dans celle de droite et se
   cale à mi-hauteur, à hauteur de buste. Même mécanique de masque que
   la fiche — chaque ligne remonte depuis son propre bord.

   LA DÉCLARATION EST SUR QUATRE LIGNES POSÉES À LA MAIN — « Le digital
   / est devenu ordinaire, / nous refusons / de l'être. », alignées à
   gauche (un triangle décalé a été essayé, le client l'a trouvé laid)
   — et le bloc part de 55 % de l'écran (la tête du fils est à la
   moitié) jusqu'à la gouttière — 57 %, « un tout petit peu à droite »
   de 55 : la plus longue ligne (21 signes) tient sans se rompre à
   3,6 vw. Les colonnes dessous sont en réserve : la
   phrase seule. */
.hero.is-sequenced .credo{
  position:absolute;
  left:57%;right:var(--gutter);
  top:50%;
  /* `top` se mesure jusqu'au bord de marge : sans cette remise à zéro,
     la marge du flux (héritée du repli sans JavaScript) décalerait le
     bloc d'autant vers le bas. */
  margin:0;
  transform:translateY(-50%);
}
/* LE FILET DE LA FICHE, le même : 1 px, tracé de la gauche avec le
   texte (`scaleX` sur la progression de la ligne), même marge. */
.credo__rule{display:block;overflow:hidden;height:1px;margin-bottom:clamp(12px,1.6vh,20px)}
.credo__rule i{
  display:block;height:1px;
  background:var(--line-soft);
  transform:scaleX(var(--credo-p,1));
  transform-origin:left;
}
.credo__claim{
  margin:0 0 clamp(20px,3.2vh,44px);
  font-family:var(--serif);
  font-weight:300;
  font-size:clamp(26px,2.2vw,40px);
  /* Serré comme « Ce que vous bâtissez » : c'est l'interlignage qui
     fait tenir les quatre lignes comme un bloc. */
  line-height:.9;
  letter-spacing:-.015em;
  color:var(--ink);
}
/* Le masque dépasse de la boîte de texte, sinon il rogne les jambages
   et les accents à cet interlignage ; compensé par une marge négative —
   même valeur que `.mask` dans `base.css` et `.showcase__line`. */
.credo__claim span{
  display:block;overflow:hidden;white-space:nowrap;
  padding:.14em 0 .1em;
  margin:-.14em 0 -.1em;
}
/* LES DEUX ÉTATS DE CORMORANT, en alternance — le registre de « Ce que
   vous bâtissez » (`.showcase__line`), mêmes compensations : chasse sur
   les capitales, six pour cent de corps sur l'italique. */
.credo__claim .caps{
  text-transform:uppercase;
  letter-spacing:.012em;
}
.credo__claim em{
  font-style:italic;
  letter-spacing:0;
  font-size:1.06em;
}
/* LE REFUS EN GRAS : « NOUS REFUSONS / de l'être. », les deux
   dernières lignes, en 500 — le gras du site — contre le constat en
   300 (demandé). */
.credo__strong i{font-weight:500}
/* EN RÉSERVE — la version à trois familles (le sujet en Manrope
   capitales, le constat en italique, le refus en Cormorant 500),
   déposée : « c'est moche ».
.credo__kicker{
  margin-bottom:.35em;
  font-family:var(--sans);
  font-weight:500;
  font-size:clamp(11px,.85vw,14px);
  letter-spacing:.16em;
  text-transform:uppercase;
  line-height:1.4;
  color:var(--ink-soft);
}
.credo__soft i{font-style:italic;font-weight:300}
.credo__strong i{font-weight:500} */
.credo__claim i{
  display:block;
  font-style:normal;
  transform:translateY(calc((1 - var(--credo-p,1)) * 110%));
  opacity:var(--credo-p,1);
}

/* LA NOTE : le corps et l'encre des sous-textes de la fiche, sous son
   masque, sur `--credo-note` (la fenêtre après la phrase, qui porte
   déjà la sortie du palier). */
.credo__note{
  margin:0;
  overflow:hidden;
  font-size:clamp(13px,.95vw,15px);
  line-height:1.55;
  color:var(--ink-soft);
}
.credo__note span{
  display:block;
  transform:translateY(calc((1 - var(--credo-note,1)) * 110%));
  opacity:var(--credo-note,1);
}

.credo__columns{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(20px,2.2vw,40px);
  max-width:34em;
  padding-left:.6em;
}
.credo__columns p{margin:0;overflow:hidden}
.credo__columns span{
  display:block;
  font-size:10px;
  line-height:1.5;
  color:rgba(255,255,255,.45);
  transform:translateY(calc((1 - var(--credo-note,1)) * 110%));
  opacity:var(--credo-note,1);
}

/* ---------- palier 04 · le revers ----------
   Miroir du précédent : le groupe se retourne et passe à droite, le
   texte prend la gauche. Le bord droit du bloc s'arrête avant la
   silhouette — c'est ce qui tient la composition, pas une marge. Le
   bord gauche est à 24 % (22 d'abord, « un tout petit peu à droite » ;
   à 1/12 le texte passait sous le repère latéral et le client le
   trouvait trop à gauche) ; les trois mots sont courts, le bloc n'a
   pas besoin de largeur — jusqu'à ce que les trois passent sur une
   ligne, en capitales : le bloc va de 16 % (« trop près du modèle »
   à 24) à 52 % de l'écran, pour que le masque ne coupe pas la ligne
   et qu'elle s'arrête avant la silhouette, qui commence à ~52 % à
   cette hauteur. Les colonnes dessous sont en réserve. */
.hero.is-sequenced .outro{
  position:absolute;
  left:16%;right:48%;
  top:50%;
  margin:0;
  transform:translateY(-50%);
}
/* Le même filet ; il se retire avec les lignes sur la sortie. */
.outro__rule{display:block;overflow:hidden;height:1px;margin-bottom:clamp(12px,1.6vh,20px)}
.outro__rule i{
  display:block;height:1px;
  background:var(--line-soft);
  transform:scaleX(calc(var(--outro-p,1) * (1 - var(--outro-out,0))));
  transform-origin:left;
}
.outro__claim{
  margin:0 0 clamp(20px,3.2vh,44px);
  font-family:var(--serif);
  /* Le gras du site sur les trois verbes : 500, la graisse la plus
     forte chargée (`docs/DESIGN.md`). */
  font-weight:500;
  /* Le corps du palier 03 (`.credo__claim`), demandé : les deux
     déclarations à la même échelle. */
  font-size:clamp(26px,2.2vw,40px);
  line-height:.9;
  letter-spacing:-.015em;
  color:var(--ink);
}
.outro__claim > span{
  display:block;overflow:hidden;
  /* Une seule ligne, coupée nulle part ; le masque a la largeur du
     bloc, qui est dimensionné pour elle. */
  white-space:nowrap;
  padding:.14em 0 .1em;
  margin:-.14em 0 -.1em;
}
/* Une famille, une ligne, un seul état : les trois verbes en capitales
   romaines à l'encre. « Convertir. » a été en bas-de-casse italique
   (le registre de « bâtissez ») et en bleu (`--accent-text`,
   l'exception nommée de `docs/DESIGN.md`) : le client a retiré l'un
   puis l'autre. Les règles sont en réserve, avec la version à deux
   familles (« CRÉER. ATTIRER. » en Manrope capitales, petit) et celle
   en trois lignes. */
.outro__claim .caps{
  text-transform:uppercase;
  letter-spacing:.012em;
}
/* EN RÉSERVE — « Convertir. » en italique, puis en bleu.
.outro__claim em{
  font-style:italic;
  letter-spacing:0;
  font-size:1.06em;
}
.outro__claim em{color:var(--accent-text)} */
/* EN RÉSERVE — le premier vers en Manrope capitales à la chasse du
   logotype.
.outro__kicker{
  margin-bottom:.3em;
  font-family:var(--sans);
  font-weight:500;
  font-size:clamp(12px,1vw,16px);
  letter-spacing:.16em;
  text-transform:uppercase;
  line-height:1.4;
  color:var(--ink);
} */
.outro__claim i{
  display:block;
  font-style:normal;
  /* La sortie est celle des autres textes : le dernier palier n'a pas
     de suite dans la séquence, la ligne se retire donc derrière son
     masque sur la sortie du hero (`--outro-out`, publiée par
     `ui/hero.js` sur `exit`), avant que `.hero__ui` ne s'efface. */
  --outro-on:calc(var(--outro-p,1) * (1 - var(--outro-out,0)));
  transform:translateY(calc((1 - var(--outro-on)) * 110%));
  opacity:var(--outro-on);
}

/* LA NOTE : le corps et l'encre des sous-textes de la fiche, sous son
   masque, sur `--outro-note` (la fenêtre après les verbes), et la même
   sortie que les lignes. */
.outro__note{
  margin:0;
  overflow:hidden;
  font-size:clamp(13px,.95vw,15px);
  line-height:1.55;
  color:var(--ink-soft);
}
.outro__note span{
  display:block;
  --outro-note-on:calc(var(--outro-note,1) * (1 - var(--outro-out,0)));
  transform:translateY(calc((1 - var(--outro-note-on)) * 110%));
  opacity:var(--outro-note-on);
}

.outro__columns{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(18px,2vw,34px);
}
.outro__columns p{margin:0;overflow:hidden}
.outro__columns span{
  display:block;
  font-size:10px;
  line-height:1.5;
  color:rgba(255,255,255,.45);
  transform:translateY(calc((1 - var(--outro-note,1)) * 110%));
  opacity:var(--outro-note,1);
}

/* ---------- responsive ---------- */
@media (max-width:1240px){
  .hero.is-sequenced .intro__note{left:64vw;width:170px}
  .hero.is-sequenced .detail{width:min(38vw,420px)}
  .hero.is-sequenced .credo{right:calc(100% / 16)}
}

@media (max-width:1000px){

  .hero.is-sequenced .intro__note{display:none}
  .hero.is-sequenced .detail{left:calc(100% / 6);width:min(50vw,460px)}
  /* À 57 % la plus longue ligne (396 px à 5 vw) touchait la gouttière
     en 1000 : le bloc revient à 55 %. */
  .hero.is-sequenced .credo{left:55%;right:var(--gutter)}
  .hero.is-sequenced .outro{left:12%;right:36%}
  /* Un cran sous le credo : la ligne en capitales faisait 635 px
     pour un masque de 640 en 1000. */
  .outro__claim{font-size:clamp(26px,3.4vw,34px)}
  .outro__columns{grid-template-columns:1fr;gap:var(--s-sm)}
  .credo__claim{font-size:clamp(26px,3.6vw,36px)}
  .detail__title,.detail__metric{font-size:clamp(26px,3.6vw,36px)}
  .credo__columns{grid-template-columns:1fr;gap:var(--s-sm)}
}

@media (max-width:760px){
  /* La mise en scène tient, mais elle se réorganise : la sculpture
     garde le haut de l'écran, le texte se pose dessous. */
  .hero{--hero-track:355svh}
  .hero__grid{background-size:calc(100% / 4) 100%}

  .hero__rail{display:none}

  .hero.is-sequenced .intro{top:auto;bottom:10vh;transform:translateY(calc(var(--intro-fade,0) * -16px))}
  /* Le premier rendu, à la même place. */
  html.js-motion .hero:not(.is-sequenced) .intro{top:auto;bottom:10vh;transform:none}
  .intro__mark{font-size:clamp(56px,16vw,84px)}
  .intro__title{font-size:clamp(17px,5vw,22px)}

  .hero.is-sequenced .detail{
    left:var(--gutter);right:var(--gutter);
    top:auto;bottom:10vh;
    transform:none;
    width:auto;
  }
  .detail__block + .detail__block{margin-top:var(--s-sm)}
  .detail__title,.detail__metric{font-size:clamp(26px,7.4vw,38px)}
  .detail__text{font-size:13px}

  .hero.is-sequenced .credo{
    left:var(--gutter);right:var(--gutter);
    top:auto;bottom:10vh;
    transform:none;
  }
  .credo__claim{font-size:clamp(26px,7.4vw,38px)}
  .credo__columns{padding-left:0}
  .credo__columns{grid-template-columns:1fr;max-width:none;gap:var(--s-sm)}
  .credo__columns span{font-size:11px}

  .hero.is-sequenced .outro{
    left:var(--gutter);right:var(--gutter);
    top:auto;bottom:10vh;
    transform:none;
  }
  .outro__claim{font-size:clamp(24px,7vw,36px)}
  /* Sur téléphone la ligne ne tient pas (395 px pour 335) : elle se
     coupe, « CONVERTIR. » passe dessous. */
  .outro__claim > span{white-space:normal}
  /* Et la note reprend son flux : ses coupes à la main, faites pour
     la colonne du desktop, laissaient un mot orphelin par ligne. */
  .outro__note br,
  .credo__note br{display:none}
  .outro__columns{grid-template-columns:1fr;gap:var(--s-sm)}
  .outro__columns span{font-size:11px}
}

/* =========================================================
   02 — MANIFESTO
   Une carte ivoire vient se poser sur le hero resté collé. Le
   contraste est le sujet : après un premier écran noir et sculpté,
   une page claire où il n'y a plus que des mots.

   La section elle-même reste transparente — c'est le hero, éteint
   derrière, qui fournit le noir des marges.

   Deux états. Sans `is-sequenced` — pas de JavaScript, ou
   `prefers-reduced-motion` — les actes s'empilent dans le flux et se
   lisent normalement. Avec, ils se superposent dans un écran collant
   et le scroll les enchaîne. Le contenu ne dépend jamais du script
   pour être lisible.
   ========================================================= */
.manifesto{position:relative;z-index:1}

/* La marge fait respirer la séquence : pendant cette course, le hero
   est seul à l'écran et son interface s'efface. Une marge ne peint
   rien — la sculpture reste visible derrière. */
.manifesto.is-sequenced{
  height:var(--manifesto-track,280svh);
  margin-top:var(--manifesto-hold,80svh);
}

.manifesto__screen{
  position:relative;
  padding:var(--card-gap,clamp(12px,2.6vw,44px));
}
.manifesto.is-sequenced .manifesto__screen{
  position:sticky;top:0;
  height:100svh;
  display:grid;
}

/* ---------- la carte ----------
   Les jetons de couleur sont redéfinis ici plutôt que dans les
   règles : tout le sous-arbre bascule en clair sans qu'une seule
   classe ait à connaître le fond sur lequel elle est posée. */
.manifesto__card{
  --ink:#0B0B0A;
  --ink-soft:#55534D;
  --muted:#82807A;
  --faint:#B4B2AB;
  --line:rgba(11,11,10,.18);
  --line-soft:rgba(11,11,10,.10);

  position:relative;
  background:#F2F1EB;
  color:var(--ink);
  border-radius:clamp(16px,2vw,34px);
  overflow:hidden;
}

.manifesto__stage{
  padding:clamp(28px,5vw,96px);
}
.manifesto.is-sequenced .manifesto__card{height:100%}
.manifesto.is-sequenced .manifesto__stage{
  height:100%;
  display:grid;
  align-content:center;
}

/* ---------- un acte ---------- */
.act{position:relative}
.act + .act{margin-top:var(--s-2xl)}

.manifesto.is-sequenced .act{
  /* Les deux actes occupent la même case de la grille : ils se
     superposent sans position absolue, donc sans perdre le
     dimensionnement automatique. */
  grid-area:1 / 1;
  align-self:center;
  margin-top:0;
  /* `--p` : progression d'entrée · `--o` : reste de vie avant sortie.
     Toutes deux écrites par ui/sequence.js.
     L'acte devient présent sur le premier sixième de son entrée, puis
     ce sont ses mots qui se densifient : le texte est là dès le début,
     en gris pâle, et seule sa densité progresse. */
  opacity:calc(clamp(0,calc(var(--p,1) * 6),1) * var(--o,1));
  transform:translate3d(0,calc((1 - var(--o,1)) * -3vh),0);
}
/* Un acte éteint ne doit ni capter le pointeur ni gêner la sélection. */
.manifesto.is-sequenced .act[data-hidden]{visibility:hidden}

/* ---------- révélation mot à mot ----------
   Le texte est entier dès le premier pixel : seule sa densité
   progresse. `--words` est posé sur l'acte, `--i` sur chaque mot ;
   le 4 est le nombre de mots en transition simultanée — plus il est
   bas, plus le front est net. */
.w{display:inline-block}
.manifesto.is-sequenced .w{
  --w:clamp(0,calc((var(--p,1) * (var(--words) + 4) - var(--i)) / 4),1);
  opacity:calc(.14 + .86 * var(--w));
}

/* ---------- label éditorial ---------- */
.act__label{
  display:flex;align-items:center;gap:var(--s-xs);
  margin:0 0 var(--s-md);
  font-size:var(--fs-label);
  letter-spacing:.28em;
  text-transform:uppercase;
  color:var(--muted);
}
.act__label-rule{
  width:38px;height:1px;flex:none;
  background:var(--accent-deep);
}

/* ---------- acte 01 · le constat et sa réponse ----------
   Les deux phrases vivent dans la même composition. Le constat tient
   la colonne de gauche, la réponse vient s'inscrire dans le vide
   qu'il laisse à sa droite. Le contraste se fait par le placement,
   pas par un effet. */
.act__phrase{
  margin:0;
  font-family:var(--serif);
  font-weight:400;
  font-size:clamp(38px,6.4vw,106px);
  line-height:1;
  letter-spacing:-.008em;
}
.act__phrase .ln{display:block}
.act__phrase--statement .ln:nth-child(2){padding-left:.22em}
/* Le mot pivot en italique — le même écart que « l'exception » dans
   le hero, et le seul toléré dans un titre. */
.act__phrase em{font-style:italic;font-weight:300}

.act__phrase--answer{
  margin-top:.34em;
  padding-left:36%;
}

/* ---------- acte 02 · la vision ----------
   Rupture d'échelle assumée : après la composition monumentale, un
   bloc éditorial, plus petit et décalé. C'est le contraste qui porte. */
.act__aside{
  max-width:min(46ch,66%);
  margin-left:auto;
}
.act__vision-text{
  margin:0;
  font-family:var(--serif);
  font-size:clamp(24px,2.9vw,46px);
  line-height:1.26;
}

/* ---------- responsive ---------- */
@media (max-width:1000px){
  .manifesto{--manifesto-track:260svh;--manifesto-hold:70svh}
  /* Le plafond vient de la largeur : « Nous refusons » est la plus
     longue des cinq lignes et fixe la taille maximale utilisable. */
  .act__phrase{font-size:clamp(38px,8.4vw,84px)}
  .act__phrase--answer{padding-left:17%}
  .act__aside{max-width:min(44ch,84%)}
  .act__vision-text{font-size:clamp(23px,3.4vw,38px)}
}

@media (max-width:760px){
  /* La séquence reste, mais raccourcie : deux actes tiennent sans
     imposer trois écrans de scroll sur un téléphone. */
  .manifesto{--manifesto-track:230svh;--manifesto-hold:55svh}
  .manifesto__stage{padding:clamp(24px,7vw,40px)}
  .act__phrase{font-size:clamp(32px,12.4vw,52px);line-height:1.04}
  /* Le décalage est illisible à cette largeur : les lignes sont déjà
     presque pleines. */
  .act__phrase--statement .ln:nth-child(2){padding-left:0}
  .act__phrase--answer{padding-left:0;margin-top:.55em}
  .act__aside{max-width:none}
  .act__vision-text{font-size:clamp(20px,5.4vw,27px);line-height:1.38}
  .act__label{margin-bottom:var(--s-sm)}
}

/* =========================================================
   02 — MANIFESTO
   Le silence après le hero. C'est cette section qui porte
   désormais `section--panel` : c'est elle qui glisse par-dessus
   la sculpture restée collée (voir « Le hero ne sort pas, il est
   recouvert » dans docs/DECISIONS.md).

   Le bloc `02 — MANIFESTO` plus haut dans ce fichier est l'ancienne
   version — carte ivoire et scène des deux mains, en réserve, dont
   le HTML est commenté dans index.html. Il est laissé intact et sans
   effet : rien ne porte plus ses classes. D'où le préfixe `stance`
   ici, pour ne pas les écraser.

   La piste fait un écran collé plus 80 svh de course. C'est court
   et voulu : le hero a déjà une longue séquence, et le visiteur doit
   arriver vite au positionnement.
   ========================================================= */
/* ---------- le voile — EN RÉSERVE, SANS EFFET ----------
   Plus rien ne porte ces classes : la déclaration est remontée dans le
   hero et son bloc HTML est commenté. Le CSS reste en place, comme celui
   de l'ancien manifesto plus haut, pour que décommenter suffise.

   Ce qui suit décrit donc un état qui n'est plus à l'écran.

   Ce qui remplaçait le panneau noir. Un dégradé de bleus de nuit qui monte
   en opacité pendant que la sculpture se floute — il n'entre pas par un
   bord, il **apparaît sur place**.

   `position:fixed` est ce qui le permet. La section, elle, glisse bel et
   bien dans le flux : sa piste remonte d'un écran par la marge négative
   du hero. Un fond posé sur elle serait donc monté avec elle, et on
   aurait revu le panneau glisser. Fixé au viewport, le voile couvre
   l'écran dès le premier pixel de la sortie et ne bouge plus.

   Ce que ça oblige à gérer : un élément fixe ne sort jamais de l'écran
   tout seul. Sans le second facteur ci-dessous, le voile resterait posé
   sur tout le reste du site. `--p` est la progression de la piste,
   publiée par `ui/stage.js` ; le voile s'éteint sur ses vingt derniers
   pour cent, avant que la section ne quitte l'écran.

   Pas de `filter: blur()`. Un flou plein écran recalculé à chaque image
   coûte cher, et un dégradé radial est déjà une tache floue : le
   demander deux fois ne change rien à l'image et double la facture. */
.stance__wash{
  position:fixed;
  inset:0;
  z-index:0;
  pointer-events:none;
  background:var(--wash-deep);
  opacity:calc(
    var(--hero-out, 0)
    * clamp(0, (1 - var(--p, 0)) / .20, 1)
  );
}

/* Les trois taches. Deux couches qui dérivent l'une sur l'autre à des
   périodes incommensurables — 31 et 43 secondes —, comme l'arc de la
   lumière du hero : le cycle ne se laisse pas compter, donc la surface
   ne se lit jamais comme une boucle.

   C'est la troisième et dernière exception du site à « le scroll est le
   seul moteur », après la lumière et la poussière du hero. Elle est
   bornée de la même façon : le mouvement ne déplace rien, il ne fait que
   vivre, et il est gelé sous `prefers-reduced-motion`.

   Les gradients sont volontairement plus larges que l'écran et centrés
   hors cadre : ce qu'on voit est le flanc d'une tache, pas son centre,
   et c'est ce qui empêche la lecture en « ronds ». */
.stance__wash::before,
.stance__wash::after{
  content:'';
  position:absolute;
  inset:-30%;
  will-change:transform;
}
.stance__wash::before{
  background:
    radial-gradient(58% 46% at 22% 28%, var(--wash-1) 0%, transparent 68%),
    radial-gradient(52% 60% at 82% 78%, var(--wash-3) 0%, transparent 72%);
  animation:washDriftA 31s ease-in-out infinite alternate;
}
.stance__wash::after{
  background:
    radial-gradient(64% 52% at 78% 22%, var(--wash-2) 0%, transparent 70%);
  animation:washDriftB 43s ease-in-out infinite alternate;
}
@keyframes washDriftA{
  from{transform:translate3d(-4%, -3%, 0) scale(1.00)}
  to  {transform:translate3d( 5%,  4%, 0) scale(1.12)}
}
@keyframes washDriftB{
  from{transform:translate3d( 4%,  3%, 0) scale(1.08)}
  to  {transform:translate3d(-5%, -4%, 0) scale(1.00)}
}
@media (prefers-reduced-motion:reduce){
  .stance__wash::before,
  .stance__wash::after{animation:none}
}

/* La section elle-même n'a plus de fond : c'est le voile qui le porte.
   Elle ne porte plus non plus `section--panel` — le fond opaque et le
   filet de ce rôle rendaient justement visible le glissement qu'on
   cherche à cacher. */
.stance{position:relative}

.stance__frame{
  position:relative;
  z-index:1;                          /* au-dessus du voile */
  min-height:100svh;
  display:grid;
  align-content:center;
  padding:calc(var(--header-h) + var(--s-md)) var(--gutter) var(--s-lg);
}
.stance__inner{
  width:100%;
  max-width:var(--container);
  margin-inline:auto;
}

/* Piste et superposition entièrement derrière `is-staged` : sans
   script, ou sous `prefers-reduced-motion` où `ui/stage.js` ne
   s'initialise pas, les actes restent dans le flux et se lisent
   l'un après l'autre. */
.stance.is-staged{height:var(--stance-track,180svh)}
.stance.is-staged .stance__frame{
  position:sticky;top:0;
  height:100svh;min-height:0;
}
.stance.is-staged .stance__inner{
  display:grid;
  align-content:center;
  min-height:0;
}
.stance.is-staged .stance__act{grid-area:1/1;align-self:center}

/* `.stance__label` n'est plus employé : le manifesto n'a plus d'index ni
   d'intertitre. Les règles restent, sans effet — rien ne porte plus la
   classe. */
.stance__label{margin-bottom:var(--s-xl)}

/* ---------- la déclaration ----------
   Centrée, et à une taille mesurée. Elle a été essayée en très grand
   bord à bord, en bas-de-casse puis en capitales : à cette échelle, un
   grotesque géant est le tic d'agence que `docs/DESIGN.md` range dans
   « à éviter », et le corps ne faisait que crier plus fort.

   Ce qui la rend présente ici n'est pas sa taille mais son isolement :
   une phrase seule, centrée, au milieu d'un écran de dégradé qui bouge
   à peine. Le Cormorant reprend donc le premier rôle, comme partout
   ailleurs sur le site — le Jost 700 n'avait été chargé que pour la
   version géante et ne sert plus.

   `--fs-stance` est propre à cette phrase : entre `--fs-h3` et
   `--fs-h2`, assez pour tenir l'écran, trop peu pour hurler. */
.stance__act{
  margin:0;
  font-family:var(--serif);
  font-weight:300;
  /* La taille est bornée par la ligne la plus longue, « Le digital est
     devenu ordinaire, », qui vaut 11,78 fois sa taille de police. Le
     minimum du `clamp` est calculé pour qu'elle tienne encore sur un
     téléphone : 26 px donnent 306 px de large, à l'aise dans les
     335 px utiles d'un écran de 375 moins les gouttières. Au-dessus,
     3,8vw la maintient autour de 45 % de la largeur — assez pour tenir
     l'écran, assez peu pour qu'elle ne crie pas. */
  font-size:clamp(26px, 3.8vw, 64px);
  line-height:1.22;
  letter-spacing:-.008em;
  text-align:center;
  color:var(--ink);
  margin-inline:auto;
  /* Deux extinctions, et elles ne font pas le même travail. `--out` est
     la sortie d'acte de `ui/stage.js` : avec un acte unique elle reste à
     zéro, la phrase se pose et reste. Le second facteur est celui du
     voile, au mot près — sans lui, le fond bleu s'effacerait sous une
     phrase restée pleine, et le texte crème finirait seul sur du noir
     avant de quitter l'écran. Les deux partent ensemble. */
  opacity:calc(
    (1 - var(--out,0))
    * clamp(0, (1 - var(--p, 0)) / .20, 1)
  );
  transform:translateY(calc(var(--out,0) * -2.2vh));
}
.stance__act + .stance__act{margin-top:var(--s-xl)}
.stance.is-staged .stance__act + .stance__act{margin-top:0}

/* Chaque ligne part avec un cran de retard sur la précédente.
   `--d` est posé par la position, pas par un style en ligne.

   `nowrap` : la césure de la phrase est portée par sa virgule, pas par
   la largeur du bloc. Une ligne qui s'enroule casserait le retard entre
   les deux — le masque n'ouvrirait plus une ligne mais un paragraphe —
   et la composition centrée perdrait son axe. La taille ci-dessus est
   calculée pour que ça ne puisse pas arriver, jusqu'à 375 px. */
.stance__line{
  --k:clamp(0, calc((var(--in,1) - var(--d,0) * .20) / .46), 1);
  white-space:nowrap;
}
.stance__line:nth-child(1){--d:0}
.stance__line:nth-child(2){--d:1}
.stance__line:nth-child(3){--d:2}

@media (max-width:1000px){
  .stance.is-staged{--stance-track:170svh}
}
@media (max-width:760px){
  /* Une piste collante d'un écran sur un téléphone laisse de grandes
     zones vides et allonge la visite pour rien : la déclaration
     redevient un bloc dans le flux. C'est déjà le repli sans script.

     Le voile suit : sans piste, `--p` reste à 0 et son extinction ne se
     déclenche jamais. Il repasse donc en absolu dans la section, où il
     entre et sort avec elle comme n'importe quel fond. */
  .stance.is-staged{height:auto}
  .stance.is-staged .stance__frame{position:relative;height:auto;min-height:78svh}
  .stance.is-staged .stance__inner{display:block}
  .stance.is-staged .stance__act{grid-area:auto}
  .stance.is-staged .stance__act + .stance__act{margin-top:var(--s-xl)}
  .stance__act{opacity:1;transform:none}
  .stance__line{--k:1}
  .stance__wash{position:absolute;opacity:1}
  .stance__frame{min-height:78svh;padding-block:calc(var(--header-h) + var(--s-md)) var(--s-lg)}
}

/* =========================================================
   03 — POSITIONNEMENT
   Le modèle, expliqué. Après le silence du manifesto, la structure
   revient : label, titre monumental, deux territoires reliés, et
   une signature qui les additionne.

   Section en flux normal, sans piste collante. C'est délibéré : le
   manifesto vient d'en tenir une, et deux séquences collées à la
   suite se liraient comme un procédé. Le mouvement est ici
   typographique et structurel, pas cinématique.
   ========================================================= */
/* Le titre est monté dans la scène, au centre, entre les deux mains — il
   vivait dessous, aligné à gauche et décalé en escalier. Le décalage n'a
   plus lieu d'être : une composition centrée dans un couloir se tient par
   son axe, et l'escalier y lirait comme un défaut d'alignement.

   Il se superpose au canvas **par la grille** et non par
   `position:absolute` : la même déclaration les empile, et sans elle les
   deux restent dans le flux, l'un sous l'autre. C'est la règle du projet
   — le contenu ne dépend jamais du script ni d'un positionnement.

   Rien ici ne porte de transition : ce sont les `--on` de `ui/flow.js`
   qui allument les mots, et le scroll doit les tenir sans retard. */
/* Le remplissage de tête est calé sur le header, pas sur le rythme des
   autres sections. Deux raisons, et elles vont ensemble : cette section
   est le panneau qui recouvre le hero, donc elle passe toute son entrée
   collée à `top: 0` — ce qu'il faut dégager est la barre, rien d'autre ;
   et elle n'a plus de `.section-label`, or `--section-y` est calibré pour
   un index suivi d'un titre, pas pour une scène pleine largeur.

   Mesuré : le titre passait de 46 % de la hauteur d'écran à 35 %, sans
   qu'aucune proportion interne ne bouge — la scène et le titre
   descendent d'un bloc, et le titre reste centré dans le couloir que les
   mains lui laissent. */
/* ---------- l'œuvre qui se fait — EN RÉSERVE ----------
   LE BLOC HTML EST COMMENTÉ dans `index.html` : plus aucune de ces
   règles ne s'applique, faute d'un `.craft` dans le document. Elles
   sont conservées telles quelles — c'est la mise en réserve qui tient
   lieu de retour arrière sur ce projet — et redeviennent actives à la
   seconde où le bloc est décommenté, ici ou dans une autre section.

   Une vidéo dont le scroll est le seul moteur : la progression sur la
   piste devient le temps de lecture, et remonter la rembobine. Rien n'est
   lu automatiquement, il n'y a aucun contrôle.

   La piste est haute, l'image est collée : c'est le même mécanisme que
   le hero, et pour la même raison — ce qui avance n'est pas la
   composition, c'est le temps. */
.craft{
  position:relative;
  /* La hauteur de piste décide de la **vitesse de l'animation**, pas de
     sa durée : c'est le rapport entre les deux qui compte. Le montage
     complet fait 10,21 s contre 5,17 s pour la première version ; à
     piste égale, l'œuvre se serait peinte deux fois plus vite par pixel
     de scroll.

     Conserver exactement le rythme d'origine demanderait
     320 × (10,21 / 5,17) = 633svh, soit plus de six écrans pour cette
     seule section. 520svh est le compromis retenu : l'animation avance
     1,2 fois plus vite qu'avant, ce qui reste posé, et la section garde
     une longueur tenable. C'est la seule valeur à toucher pour ralentir
     ou accélérer l'ensemble. */
  height:var(--craft-track, 520svh);
  /* Le fond, avant que la vidéo n'ait décodé sa première image. Un
     `<video>` vide se peint en blanc dans plusieurs navigateurs, et un
     flash blanc entre le hero noir et une première image noire serait
     le seul défaut qu'on remarquerait de toute la section. */
  background:var(--bg);
}
.craft__frame{
  position:sticky;top:0;
  height:100svh;
  overflow:hidden;
  isolation:isolate;
  background:var(--bg);
}

/* 16:9 dans une fenêtre qui ne l'est jamais : `cover` conserve le
   rapport et laisse le cadrage déborder, plutôt que d'ajouter des bandes.
   `transform: translateZ(0)` place la vidéo sur sa propre couche — sans
   lui, chaque déplacement du temps repeint tout le panneau. */
.craft__video{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  object-position:center;
  background:var(--bg);
  transform:translateZ(0);
  will-change:transform;
  pointer-events:none;
}

/* Vignettage. Centre presque transparent, bords assombris : le texte
   gagne son fond sans que l'œuvre perde le sien. Les valeurs sont
   celles du hero, à peine resserrées — c'est la même lumière. */
.craft__veil{
  position:absolute;inset:0;z-index:1;
  pointer-events:none;
  background:
    radial-gradient(78% 62% at 50% 50%,
      rgba(0,0,0,0) 38%,
      rgba(0,0,0,.42) 74%,
      rgba(0,0,0,.72) 100%);
}

/* Le grain du hero, repris à l'identique — même image, même opacité.
   C'est ce qui raccorde la vidéo au reste du site : sans lui, elle se
   lit comme une pièce rapportée. */
.craft__grain{
  position:absolute;inset:0;z-index:2;
  pointer-events:none;
  opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:160px 160px;
}

/* ---------- les trois mots ----------
   Ils se relaient sur la course, un par tiers, et se superposent par la
   grille : la même déclaration les empile ici et les laisserait s'empiler
   dans le flux sans script.

   `--craft-p` est la progression, posée par `ui/craft.js` sur la piste —
   **une seule écriture par image**, et le CSS en tire les trois états.
   Chaque mot porte ses deux bornes en style en ligne : `--a` quand il
   entre, `--b` quand il sort. Le dernier a `--b` hors d'atteinte, donc
   il reste.

   Repli à 1 : sans script, le troisième mot est en place et les deux
   autres sont sortis — la phrase reste lisible et le titre garde son
   sens. */
.craft__words{
  position:absolute;inset:0;z-index:3;
  margin:0;
  display:grid;
  place-items:center;
  padding-inline:var(--gutter);
  pointer-events:none;
  text-align:center;
}
.craft__word{
  grid-area:1/1;
  font-family:var(--serif);
  font-weight:300;
  font-size:clamp(52px, 7vw, 110px);
  line-height:1;
  letter-spacing:-.012em;
  color:var(--ink);

  --in:clamp(0, (var(--craft-p, 1) - var(--a)) / .09, 1);
  --out:clamp(0, (var(--craft-p, 1) - var(--b)) / .09, 1);
  --on:calc(var(--in) * (1 - var(--out)));

  opacity:var(--on);
  /* Le mot monte en entrant et continue de monter en sortant : il ne
     redescend jamais, sinon la sortie se lirait comme un retour en
     arrière. Douze pixels — au-delà, ça devient un mouvement. */
  transform:translateY(calc((1 - var(--in)) * 12px + var(--out) * -12px));
  /* Un flou très court, à l'entrée comme à la sortie. Il fait la
     différence entre un texte qui s'allume et un texte qui se forme. */
  filter:blur(calc((1 - var(--in)) * 5px + var(--out) * 5px));
}

/* Le point, et rien d'autre, porte le bleu de marque. C'est le seul
   objet coloré de toute la scène — un signe, pas du texte, donc les
   seuils de contraste d'un texte ne s'y appliquent pas. */
.craft__word em{
  font-style:normal;
  color:var(--accent);
}

@media (max-width:760px){
  /* Piste raccourcie sur téléphone, dans la même proportion que sur
     desktop. Le décodage y coûte plus cher et une course trop longue
     donnerait un parcours haché sur la moitié des appareils. */
  .craft{--craft-track:400svh}
  .craft__word{font-size:clamp(38px, 11vw, 64px)}
}

@media (prefers-reduced-motion:reduce){
  /* Pas de course : l'œuvre est montrée achevée et les trois mots se
     lisent d'un coup, empilés par la grille. */
  .craft{height:auto}
  .craft__frame{position:relative;height:100svh}
  .craft__word{
    opacity:1;transform:none;filter:none;
    position:relative;
  }
  .craft__words{position:absolute;display:grid;gap:var(--s-2xs);place-items:center}
}

/* La section n'a plus de remplissage propre : elle s'ouvre sur la vidéo,
   qui occupe l'écran entier et ne souffre aucune marge. C'est le
   conteneur du texte qui suit qui reprend le rythme, une fois la piste
   de l'œuvre traversée. */
/* ---------- le panneau en clair ----------
   Le recouvrement se fait sur une surface de marbre au lieu du noir.
   C'est la seconde surface du site, et le seul endroit où elle vit.

   LA BASCULE PASSE PAR LES JETONS, PAS PAR LES RÈGLES. Tout le
   sous-arbre change de fond sans qu'une seule classe ait à savoir sur
   quoi elle est posée — c'est le patron de l'ancienne carte ivoire du
   manifesto, et c'est ce qui permet d'essayer sans toucher aux vingt
   règles de la section. `--bg` bascule avec les autres : quelques
   éléments s'en servent pour se découper sur le fond, et ils doivent
   suivre.

   L'ACCENT CHANGE DE MARCHE. `--accent-text` (#5C8CFF) est calé pour le
   noir et tombe à 2,8:1 ici. `--accent-deep` existe précisément pour ce
   cas — 6,7:1 sur le marbre.

   TROIS CONSÉQUENCES, TOUTES ASSUMÉES :
   — le raccord n'est plus une affaire de valeur. Le panneau arrivait sur
     exactement le noir qu'il recouvrait ; il arrive en contraste
     maximal, et c'est le geste le plus franc de la page. Le fondu de
     140 px, documenté dans `layout.css`, reste le recours si l'arête
     devait s'adoucir un jour.
   — les mains sont en bronze noirci, donc en silhouette sur cette
     surface au lieu d'être révélées par leur clé. C'est un autre
     tableau, pas le même vu autrement.
   — le header bascule avec la section, et il le fait par l'encre avant
     de le faire par le fond : voir `.header.is-hero-exit` et
     `.header.is-light` dans `layout.css`. */
/* Le marbre bascule par ses jetons. Une seule section le porte
   aujourd'hui, le positionnement ; `#pillars` — les deux piliers, en
   réserve dans `index.html` — reste dans le sélecteur pour que sa remise
   en service ne demande rien ici. */
#positioning,
#pillars,
#method{
  --bg:var(--marble);
  --ink:var(--marble-ink);
  --ink-soft:var(--marble-ink-soft);
  --muted:var(--marble-muted);
  --faint:var(--marble-faint);
  --line:var(--marble-line);
  --line-soft:var(--marble-line-soft);
  --accent-text:var(--accent-deep);

  background:var(--marble);
  /* Indispensable, et facile à oublier : redéfinir un jeton ne suffit
     pas pour ce qui n'a pas de `color` à soi. L'héritage transmet une
     valeur **calculée**, pas la référence — sans cette ligne, tout ce
     qui hérite sa couleur du body reste crème, donc illisible sur le
     marbre. C'est le titre de la section qui l'a montré. */
  color:var(--ink);
}

/* ---------- la pièce, et les trois mots autour d'elle ----------
   TROIS CALQUES, ET C'EST TOUT LE SUJET : le texte derrière, la pièce,
   le texte devant. Le même mot existe deux fois, et la copie de devant
   n'est découpée que sur sa bande haute — la pièce passe donc **à
   travers** les lettres au lieu d'être posée devant ou derrière elles.
   C'est ce qui distingue une composition d'un objet 3D collé sur du
   texte, et c'est ce qu'on cherchait.

   La découpe est statique et vaut une propriété par mot : elle ne coûte
   rien, là où un masque calculé sur la silhouette réelle demanderait une
   passe de rendu par image. La pièce étant allongée et centrée, elle
   occupe la bande médiane du cadre — c'est exactement la zone que la
   copie de devant laisse voir.

   Une seule progression pilote l'ensemble, `--sp`, publiée sur la piste
   par `ui/showcase.js` et lue aussi par la scène 3D. Aucune transition :
   le scroll est le seul moteur, comme partout ailleurs sur cette page.

   Sans script, la classe `is-live` n'est pas posée : la piste retombe à
   sa hauteur de contenu, la scène cesse d'être collée et les trois mots
   se lisent empilés. C'est la règle du site — le contenu ne dépend
   jamais du JavaScript. */
.showcase{position:relative}
.showcase.is-live{
  /* Trois écrans et demi : un par mot, plus la marge d'entrée et de
     sortie. En dessous les mots se relaient trop vite pour être lus, au
     delà la rotation devient une attente. */
  height:var(--showcase-track, 350svh);
}
.showcase.is-live .showcase__stage{
  position:sticky;top:0;
  height:100svh;
  /* Les mots dépassent volontiers du cadre : c'est ce qui leur donne
     leur échelle. Le débord est coupé ici, jamais sur la piste — un
     `overflow` sur la piste désactiverait le collant sans rien dire. */
  overflow:hidden;
  isolation:isolate;
}
.showcase__stage{
  position:relative;
  display:grid;
  place-items:center;
}

/* La pièce. Entre les deux calques de texte, et c'est le seul endroit du
   site où l'ordre d'empilement porte du sens plutôt que de l'hygiène. */
.showcase__canvas{
  grid-area:1/1;
  z-index:2;
  display:block;
  width:100%;height:100%;
  pointer-events:none;
  /* UN SEUL FONDU, ET IL EST À L'ENTRÉE. La pièce se découvre sur les
     premiers pour-cent : ce n'est pas une apparition mais la conséquence
     de l'amorce — la scène ne rend rien avant ce point, c'est là que le
     hero coupe la sienne, et sans ce dégradé elle surgirait d'un coup à
     pleine opacité.

     Elle ne s'efface jamais à la sortie. Un fondu sur le modèle entier a
     été essayé et retiré : il faisait disparaître une matière qu'on
     regarde encore. C'est son **pied** qui se dissout, dans le dégradé de
     marbre posé sur le bas du cadre — voir `.showcase__fade`. */
  opacity:clamp(0, calc(var(--sp, 1) / .06), 1);
}

/* ---------- les trois temps ----------
   Tous posés en grille sur la même cellule que le canvas : ils se
   superposent sans `position:absolute`, donc la même déclaration les
   empile sous `is-live` et les laisse s'empiler dans le flux sans elle.

   Chacun porte son créneau en style en ligne — `--a` quand il entre,
   `--b` quand il sort — et la course est la même pour les trois. Le
   dernier a sa borne de sortie hors d'atteinte : il reste. */
.showcase__line,
.showcase__final{
  grid-area:1/1;
  margin:0;
  width:100%;
  pointer-events:none;
  text-align:center;
  padding-inline:var(--gutter);

  --in:clamp(0, calc((var(--sp, 1) - var(--a)) / .10), 1);
  --out:clamp(0, calc((var(--sp, 1) - var(--b)) / .10), 1);
  --on:calc(var(--in) * (1 - var(--out)));
}

/* ---------- l'apparition, celle du hero ----------
   Le même mécanisme que les déclarations des paliers : chaque ligne est
   une boîte à `overflow:hidden` dont le contenu remonte de 110 % en
   même temps qu'il s'opacifie. Rien n'entre en glissant sur le côté ni
   en changeant d'échelle — c'est le vocabulaire de la page, et une
   seconde grammaire d'entrée dans la même colonne se serait vue.

   Une seule valeur pilote les deux sens, `--on` : la ligne se retire
   derrière son masque comme elle en est sortie. C'est ce que fait
   `--credo-p` dans le hero, et ça ne se lit pas comme un retour en
   arrière parce que le masque, lui, ne bouge pas. */
.showcase__line > span,
.showcase__final-head > span{
  display:block;
  overflow:hidden;
  /* Le masque doit dépasser de la boîte de texte, sinon il rogne les
     jambages et les accents. Compensé par une marge négative pour ne pas
     décaler la composition — même valeur que `.mask` dans `base.css`. */
  padding:.14em 0 .1em;
  margin:-.14em 0 -.1em;
}
.showcase__line i,
.showcase__final-head i{
  display:block;
  font-style:normal;
  transform:translateY(calc((1 - var(--on)) * 110%));
  opacity:var(--on);
}

/* LA PHRASE DERRIÈRE. Au centre du cadre, comme dans la référence, et
   **sans z-index** au-dessus du canvas : c'est la montée de la pièce qui
   la recouvre, à l'instant où les épaules arrivent à sa hauteur. Elle
   dérive légèrement vers le haut pendant ce temps, ce qui rapproche les
   deux et rend le passage plus franc. */
.showcase__line--back{
  z-index:1;
  align-self:center;
  transform:translateY(calc(var(--sp, 0) * -9svh));
}
/* LA PHRASE DEVANT. Posée bas, par-dessus tout — canvas et fondu : elle
   se lit sur la matière, ce qui est tout l'intérêt de la faire passer
   devant, et le fondu ne doit pas l'éteindre. */
.showcase__line--front{
  z-index:4;
  align-self:end;
  /* 16 svh (11 d'abord, « un petit peu plus haut ») : la phrase se
     pose sur le fondu du pied sans toucher le bord bas de l'écran. */
  margin-bottom:16svh;
}

/* ---------- le fondu du pied ----------
   Le bas du cadre reçoit un dégradé de marbre, au-dessus du canvas mais
   sous les textes. La pièce s'y **dissout** au lieu d'être coupée net par
   le bord de la scène, et c'est dans cette zone que le dernier bloc vient
   s'écrire — exactement comme la référence pose sa copy dans la
   dissolution de la robe.

   C'est un dégradé, pas une opacité sur le modèle : effacer la pièce
   entière aurait fait disparaître une matière qu'on regarde encore. Ici
   seul son pied s'en va, et il s'en va dans le fond. */
.showcase__fade{
  grid-area:1/1;
  z-index:3;
  align-self:end;
  width:100%;
  /* IL N'EST PAS LÀ TOUT DU LONG, IL MONTE AVEC LE TEXTE. C'est ce que
     montre la référence : à mi-course la pièce descend nette jusqu'au
     bord du cadre, et ce n'est qu'à l'arrivée de la copy que son bas se
     dissout. Un dégradé constant aurait mangé le corps pendant toute la
     descente — ce qui est exactement le défaut de la version d'avant.

     Il est aussi beaucoup plus long qu'un simple ourlet : il commence
     haut, atteint le marbre plein vers la moitié de sa propre hauteur, et
     laisse donc tout le bas du cadre en aplat. C'est cet aplat qui rend
     la copy lisible, pas un fond posé derrière elle. */
  height:78svh;
  pointer-events:none;
  opacity:clamp(0, calc((var(--sp, 1) - .60) / .24), 1);
  background:linear-gradient(to bottom,
    color-mix(in srgb, var(--marble) 0%, transparent) 0%,
    color-mix(in srgb, var(--marble) 38%, transparent) 26%,
    var(--marble) 54%,
    var(--marble) 100%);
}
.showcase__line > span{
  font-family:var(--serif);
  font-weight:300;
  /* Deux lignes désormais, donc un cran plus bas qu'en une seule, et un
     interlignage très serré : c'est lui qui fait tenir les deux lignes
     comme un bloc plutôt que comme deux phrases. */
  font-size:clamp(36px, 5.7vw, 104px);
  line-height:.86;
  letter-spacing:-.015em;
  color:var(--ink);
}

/* ---------- les deux traitements ----------
   La référence ne mélange pas deux familles, elle mélange deux **états**
   de la même : capitales romaines et bas-de-casse italique. C'est aussi
   ce que le site fait déjà — `docs/DESIGN.md` réserve l'italique de
   Cormorant au mot pivot d'un grand titre. Y faire entrer le grotesque
   posé un troisième registre typographique sur la page, ce que les deux
   familles du site interdisent.

   Les capitales prennent une chasse : sans elle, un mot en capitales se
   referme sur lui-même à cette échelle. L'italique n'en prend aucune —
   elle est déjà cursive — et gagne six pour cent de corps, parce qu'un
   bas-de-casse posé à côté d'une capitale se lit toujours plus petit
   qu'il ne l'est. Les deux réglages compensent la même illusion. */
.showcase__line .caps{
  text-transform:uppercase;
  letter-spacing:.012em;
}
.showcase__line em{
  font-style:italic;
  text-transform:none;
  letter-spacing:0;
  font-size:1.06em;
}

/* LE BLOC DE FIN. Au-dessus de tout : la pièce s'efface derrière lui.
   Il reprend la composition centrée de la référence — une affirmation en
   deux lignes, un chapô, un appel à l'action — et rend ses évènements au
   pointeur, sinon le bouton ne serait pas cliquable. */
.showcase__final{
  z-index:5;
  align-self:end;
  margin-bottom:5svh;
  display:grid;
  justify-items:center;
  gap:var(--s-md);
  /* En pixels, pas en `ch` : l'unité `ch` se résout sur la police de
     **cet** élément — 16 px hérités — et non sur celle du titre qu'il
     contient. Posée à 58ch, la colonne faisait 464 px et l'affirmation
     partait sur cinq lignes au lieu de trois. Le piège vaut pour tout
     conteneur dont les enfants changent de corps. */
  max-width:min(780px, 86vw);
  margin-inline:auto;
}

/* ---------- le bloc de fin entre comme le reste ----------
   Il entrait en fondu, ce qui en faisait le seul élément de la section à
   ne pas parler la même langue. Il prend donc le masque de ligne du
   hero : chaque bloc est une boîte à `overflow:hidden` dont le contenu
   remonte de 110 %. Le chapô et le bouton sont masqués **en entier** —
   c'est ce que fait le hero pour ses colonnes, où le texte se replie sur
   plusieurs lignes et ne peut donc pas être découpé ligne à ligne.

   `--step` échelonne les quatre blocs de trois centièmes de course
   chacun : les deux lignes de l'affirmation, le chapô, puis le bouton.
   C'est l'équivalent du `--stagger` du hero, calculé en CSS plutôt que
   publié, puisqu'une seule progression alimente toute la scène.

   L'ÉCHELONNEMENT EST PRIS EN ÉTAU, et c'est ce qui fixe ses valeurs.
   D'un côté le bloc ne peut pas s'ouvrir avant que le second temps ne
   soit **entièrement sorti**, sinon deux grands textes se superposent —
   c'est arrivé, en l'avançant à 0,78 alors que « Mérite / d'être vu. »
   ne finissait de partir qu'à 0,90. De l'autre le dernier échelon doit
   tomber avant la fin de la course, sinon le bouton n'arrive jamais
   tout à fait.

   Les deux contraintes se résolvent ensemble : le second temps sort plus
   tôt (0,72, donc parti à 0,82), le bloc s'ouvre à 0,84, et les pas
   passent de trois à deux centièmes avec une rampe de sept — soit
   treize centièmes entre la première ligne et le bouton posé, qui
   tiennent dans les seize restants. */
.showcase__final-body,
.showcase__final-act{
  margin:0;
  overflow:hidden;
  padding:.14em 0 .1em;
  margin-block:-.14em -.1em;
}
/* LA SORTIE DU BLOC DE FIN : SON ENTRÉE À L'ENVERS. Le bloc ne sort
   pas sur la course (`--b: 9`) — c'est sous son bouton que naît le
   cadre du procédé —, mais une fois la scène décollée il défile avec
   la page et passait sous la barre, transparente : les deux textes se
   croisaient. Le client a tranché : la barre reste, le bloc part avant
   de la toucher, en rejouant son entrée à l'envers — chaque ligne
   redescend dans son masque et s'éteint (c'est la seconde exception
   assumée à « une sortie ne rejoue pas l'entrée », après les mains).
   Sur `--sq`, l'écran qui suit le décollage (`ui/showcase.js`).

   CHACUN PART JUSTE AVANT SON CONTACT, DE HAUT EN BAS — pas au
   décalage uniforme de l'entrée : le bouton, tout en bas, a presque
   deux fois plus de course que la première ligne, et le client l'a
   vu partir trop tôt (comme le chapô). `--leave` est le départ de
   chacun, sa sortie dure .07 ; mesuré au décollage en 768 de haut,
   la barre finissant à 87 px, le contact de chaque haut de boîte :

     ligne 1   415 px   .43    part à .32,  éteinte à .39
     ligne 2   448 px   .47            .365            .435
     ligne 3   481 px   .51            .41             .48
     chapô     542 px   .59            .49             .56
     bouton    670 px   .76            .65             .72

   Les proportions tiennent aux autres hauteurs (le bloc est ancré à
   5 svh du bas, les corps en `vw`) ; la marge est de .035 à .04 écran
   — réglée en trois retours : .03–.04, puis .015 (« trop »), puis
   ceci. */
.showcase__final-head > span:nth-child(1){--leave:.32}
.showcase__final-head > span:nth-child(2){--leave:.365}
.showcase__final-head > span:nth-child(3){--leave:.41}
.showcase__final-body{--leave:.49}
.showcase__final-act{--leave:.65}
.showcase__final-head i,
.showcase__final-body > span,
.showcase__final-act > span{
  --on-step:clamp(0, calc((var(--sp, 1) - var(--a) - var(--step, 0) * .02) / .07), 1);
  --off-step:clamp(0, calc((var(--sq, 0) - var(--leave, 1)) / .07), 1);
  --on-off:calc(var(--on-step) * (1 - var(--off-step)));
  transform:translateY(calc((1 - var(--on-off)) * 110%));
  opacity:var(--on-off);
}
.showcase__final-body > span{display:block}
.showcase__final-act > span{display:inline-block}
/* ---------- les rôles typographiques sont inversés ici, et c'est voulu
   ----------
   Ailleurs sur le site, le Cormorant porte les titres et le Manrope le
   texte courant. Ce bloc fait l'inverse : **la définition est en Manrope**,
   le paragraphe de faits en Cormorant.

   Ce n'est pas une fantaisie, c'est ce qui distingue ce moment des deux
   qui le précèdent. « Ce que vous bâtissez » et « Mérite d'être vu. »
   sont des déclarations, en Cormorant, à l'échelle du monument. Ici on
   explique — et l'explication ne peut pas parler la même langue que la
   déclaration, sinon elle sonne comme une troisième formule. Le
   grotesque dit « voici ce que nous sommes », le sérif dit « voici ce
   que nous fabriquons ». La référence fait exactement cette bascule au
   même endroit de sa page.

   Elle reste contenue à ce bloc. Le Manrope n'a pas vocation à revenir en
   titre ailleurs, et le Cormorant n'a pas vocation à porter du texte
   long : le paragraphe fait quatre lignes centrées, il se lit comme une
   légende, pas comme un corps de texte. */
/* Le logotype dans la phrase : capitales, « RIV » à l'encre et « MONT »
   à l'accent — le même partage que la barre du header. Il fait de la
   définition une signature plutôt qu'une phrase qui commence par un nom.

   Sa chasse n'est pas celle du header. Là-bas le logotype fait 15 à
   18 px et prend 0,2 em, ce qui est la dose d'un mot isolé à petit
   corps ; ici il fait 36 px au milieu d'une ligne, et la même valeur le
   ferait éclater. Une capitale a besoin d'autant moins d'air qu'elle est
   grande : 0,05 em suffit à l'ouvrir sans la détacher de la phrase.

   L'accent garde `--accent`, comme le logotype du header : `DESIGN.md`
   en fait l'exception nommée — un logotype échappe aux seuils de
   contraste, et c'est là que la couleur de marque doit être exacte. Sur
   le marbre il donne d'ailleurs 5,0:1, mieux que sur le noir. */
.showcase__final-head .wordmark{
  text-transform:uppercase;
  letter-spacing:.05em;
}
.showcase__final-head .wordmark span{color:var(--accent)}

.showcase__final-head{
  font-family:var(--sans);
  font-weight:500;
  /* Serré : trois lignes qui doivent se tenir comme un bloc. */
  font-size:clamp(22px, 2.5vw, 38px);
  line-height:1.18;
  letter-spacing:-.006em;
  color:var(--ink);
  margin:0;
}
.showcase__final-body{
  /* Quatre lignes, pas cinq : la référence en fait quatre et c'est ce
     qui donne au paragraphe sa forme de bloc plutôt que de colonne. Le
     `ch` est ici légitime — il se résout sur la police de ce paragraphe,
     qui est bien celle du texte qu'il contient. */
  max-width:56ch;
  font-family:var(--serif);
  font-size:clamp(15px, 1.15vw, 19px);
  line-height:1.62;
  letter-spacing:.005em;
  color:var(--ink-soft);
}
.showcase__final-cta{pointer-events:auto}

@media (max-width:1000px){
  .showcase.is-live{--showcase-track:300svh}
  .showcase__line > span{font-size:clamp(34px, 7vw, 68px)}
  .showcase__line--back{margin-top:18svh}
  .showcase__line--front{margin-bottom:17svh}
}
@media (max-width:760px){
  .showcase.is-live{--showcase-track:280svh}
  .showcase__line > span{font-size:clamp(28px, 8vw, 44px)}
  .showcase__final{gap:var(--s-sm)}
}

/* ---------- le repli, et il couvre deux cas d'un coup ----------
   `is-live` est posée par `ui/showcase.js`, qui ne s'initialise ni sans
   JavaScript ni sous `prefers-reduced-motion`. Son absence est donc le
   seul test à faire, et il vaut pour les deux situations.

   Les trois temps redeviennent trois blocs empilés, opaques, dans
   l'ordre du DOM : la section se lit entièrement sans une ligne de
   script — ce que la règle du site impose. */
.showcase:not(.is-live) .showcase__stage{
  min-height:100svh;
  display:grid;
  gap:var(--s-xl);
  align-content:center;
  padding-block:var(--s-2xl);
}
/* Le masque et son contenu doivent être remis à plat **eux aussi** :
   sans cette règle les lignes restaient derrière leur masque, opacité
   nulle, et la section était vide sans JavaScript. Le repli ne se teste
   pas sur les seuls blocs parents. */
.showcase:not(.is-live) .showcase__line i,
.showcase:not(.is-live) .showcase__final-head i,
.showcase:not(.is-live) .showcase__final-body > span,
.showcase:not(.is-live) .showcase__final-act > span{
  transform:none;
  opacity:1;
}
.showcase:not(.is-live) .showcase__line > span,
.showcase:not(.is-live) .showcase__final-head > span,
.showcase:not(.is-live) .showcase__final-body,
.showcase:not(.is-live) .showcase__final-act{
  overflow:visible;
  padding:0;
  margin:0;
}
.showcase:not(.is-live) .showcase__line,
.showcase:not(.is-live) .showcase__final{
  grid-area:auto;
  opacity:1;
  transform:none;
  align-self:auto;
  margin-top:0;
  margin-bottom:0;
  pointer-events:auto;
}
.showcase:not(.is-live) .showcase__canvas,
.showcase:not(.is-live) .showcase__fade{display:none}

/* Les piliers — EN RÉSERVE, voir `index.html`. Leur copy vit dans les
   légendes du procédé. */
#pillars > .container{padding-block:var(--section-y)}

/* Le titre est redevenu autonome : la scène des mains est descendue dans
   la signature, il n'a donc plus de couloir à respecter ni de canvas
   sous lui. Il reste centré — c'est la composition à laquelle il a été
   réglé, et trois mots courts centrés tiennent l'écran sans image. */
.positioning__claim{
  display:grid;gap:var(--s-2xs);
  justify-items:center;
  text-align:center;
  margin-block:var(--s-lg) var(--s-xl);
}
.positioning__claim span{
  font-family:var(--serif);
  font-size:var(--fs-h2);
  line-height:var(--lh-heading);
  color:var(--ink);
}
/* Un seul mot coloré dans le titre. Le brief l'autorise explicitement
   (« un mot important »), ce que `docs/DESIGN.md` interdisait pour
   l'italique du hero — les deux écarts ne sont pas le même. */
.positioning__claim em{font-style:normal;color:var(--accent-text)}
.positioning__claim .mask{--k:var(--on,1)}

.positioning__lead{max-width:56ch;color:var(--ink-soft)}

/* ---------- les deux mains ----------
   Seule image de toute la séquence qui suit le hero, et la seule 3D en
   dehors de lui. Elle n'est **pas** épinglée : trois sections collées
   d'affilée se liraient comme un procédé, et le rythme de cette partie
   se joue sur la retenue.

   La boîte sort du container et occupe toute la largeur de la fenêtre.
   Bornée à 980 px comme elle l'était, elle coupait les avant-bras en
   plein milieu de l'écran, bouchons plats compris : le rognage se lisait
   comme un défaut de cadrage. Pleine largeur, la coupe se fait au bord
   de l'écran et les bras se prolongent hors champ.

   C'est toujours la largeur qui commande : `hands.js` cadre sur une
   largeur visible (`fitWide`) plus étroite que le modèle, et le ratio
   ci-dessous ne fait que donner de l'air au-dessus et au-dessous. Le
   plafond de hauteur évite qu'un écran très large ne pousse la scène
   au-delà de l'écran. */
/* ---------- la piste de la scène ----------
   Plus haute que la scène elle-même, et c'est cette différence qui fait
   la **pause**. La scène s'y colle au centre de l'écran et n'en bouge
   plus tant que la piste défile : le texte achève de s'écrire, les mains
   achèvent de se rejoindre, puis plus rien ne se passe pendant environ
   les deux tiers d'un écran.

   Le scroll n'est jamais bloqué — la molette répond, la page avance, le
   visiteur garde la main. C'est la composition qui s'arrête, pas lui.
   Bloquer réellement le défilement contredirait la règle qui tient tout
   le site, et se lit comme une page cassée.

   Marges portées ici : la scène collée ne doit rien avoir qui la
   décale. */
.hands-track{
  --hands-h:min(calc(100vw / 2.44), 78svh);
  /* La durée de la pause. Elle s'ajoute à la hauteur de la scène, et
     c'est le seul réglage à toucher pour l'allonger ou la raccourcir —
     il n'entre plus dans aucune course depuis que `data-flow` est posé
     sur la scène et non sur la piste.

     Une fois retirés ce que la fin du titre et la fermeture des mains
     consomment, il reste environ un demi-écran où plus rien ne bouge.
     À 70svh il n'en restait que 0,29, ce qui se lit comme un ralenti,
     pas comme un arrêt. */
  --hands-hold:95svh;
  height:calc(var(--hands-h) + var(--hands-hold));
  margin-block:var(--s-sm) var(--s-xl);
}

.hands{
  position:sticky;
  /* Centrée dans l'écran. Le collage commence donc quand la scène atteint
     ce centre — à quelques pixels près, l'instant où le panneau achève sa
     montée. Tout ce qui suit se joue à l'arrêt. */
  top:calc((100svh - var(--hands-h)) / 2);
  display:grid;                       /* la scène et le titre s'y empilent */
  margin-inline:calc(50% - 50vw);     /* déborde du container, jusqu'aux bords */
  /* Une hauteur, et non `aspect-ratio` avec un `max-height` : ce couple
     dimensionne la piste de grille **avant** d'appliquer le plafond, si
     bien que le canvas en `height:100%` débordait de la boîte de 85 px
     sur un écran court. Le `min()` fait le même arbitrage en une valeur,
     que la grille voit d'emblée. */
  height:var(--hands-h);
}
/* `min-height:0` n'est pas décoratif. Un élément de grille a
   `min-height:auto`, donc il ne peut pas descendre sous sa taille
   intrinsèque — et celle d'un canvas est le rapport de ses attributs
   `width`/`height`, que `renderer.setSize` vient d'y écrire. Sans cette
   ligne, la boucle se referme : le canvas garde la hauteur du dernier
   rendu, `hands.js` la remesure, la réécrit, et le canvas déborde de sa
   boîte sans jamais revenir. */
.hands__canvas{
  grid-area:1/1;
  display:block;
  width:100%;height:100%;
  min-width:0;min-height:0;
  /* Rien sur l'opacité : les mains n'apparaissent pas en fondu, elles
     **entrent par les bords**. Elles démarrent hors du cadre et glissent
     jusqu'à leur place — voir `gap` dans `hands.js`. Le canvas est donc
     toujours là, simplement vide tant qu'elles sont dehors, et il est
     transparent : c'est le panneau qu'on voit au travers.

     Le titre n'est pas concerné : il est frère de ce canvas, pas enfant.
     Il s'écrit pendant la montée du panneau, avant qu'elles n'entrent. */
}

/* ---------- les deux territoires ----------
   Trois colonnes : bloc, passerelle, bloc. Pas de cartes, pas de
   boîtes — des filets et de la typographie. La colonne du milieu
   n'est pas un ornement : c'est elle qui dit que les deux ne sont
   pas deux services côte à côte. */
.pillars{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  gap:var(--s-md);
  margin-top:var(--s-xl);
}
.pillar__label{
  display:flex;align-items:center;gap:var(--s-2xs);
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  text-transform:uppercase;
  color:var(--muted);
}
.pillar__index{color:var(--accent-text);font-variant-numeric:tabular-nums}
.pillar__title{
  margin:var(--s-sm) 0 0;
  font-family:var(--serif);
  font-weight:300;
  font-size:var(--fs-h3);
  line-height:var(--lh-heading);
  color:var(--ink);
}
.pillar__title span{display:block}
.pillar__text{
  margin-top:var(--s-sm);
  max-width:44ch;
  color:var(--ink-soft);
}

/* La liste de services est un détail éditorial, pas une grille
   d'icônes : des filets, des capitales étroites, rien d'autre. */
.pillar__services{
  margin-top:var(--s-lg);
  display:grid;gap:0;
  border-top:1px solid var(--line-soft);
}
.pillar__services li{
  padding-block:var(--s-2xs);
  border-bottom:1px solid var(--line-soft);
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  text-transform:uppercase;
  color:var(--muted);
}

/* ---------- la passerelle ----------
   Un filet qui se trace au scroll, et le signe de la multiplication
   posé dessus. C'est le seul endroit de la section où l'accent
   apparaît en dehors des index. */
.bridge{
  align-self:stretch;
  position:relative;
  width:clamp(44px,7vw,120px);
  display:grid;place-items:center;
}
.bridge__line{
  position:absolute;top:0;bottom:0;
  width:1px;
  background:var(--line-soft);
  transform:scaleY(var(--on,1));
  transform-origin:top center;
}
.bridge__mark{
  position:relative;
  padding-block:.5em;
  background:var(--bg);
  font-family:var(--serif);
  font-size:var(--fs-h3);
  line-height:1;
  color:var(--accent-text);
  opacity:var(--on,1);
}

/* ---------- la signature ----------
   Le seul bloc centré de la page. Après deux colonnes qui se
   répondent, la convergence se dit par l'axe, pas par une flèche. */
/* La signature, posée dans la scène des mains, et pilotée par elle.

   `--hands-e` est l'avancement du rapprochement, publié par `hands.js` —
   la courbe d'approche elle-même, pas la progression brute du scroll.
   Les deux termes glissent donc **au rythme des mains**, comme poussés
   par elles, puis se rejoignent pour reformer l'équation d'un seul
   tenant. « Croissance. » s'écrit ensuite dessous.

   Repli à 1 : sans script, ou avant le premier rendu, tout est en place
   et lisible. Aucune transition de temps — le scroll tient ces valeurs,
   comme partout ailleurs sur le site. */
.signature{
  grid-area:1/1;
  place-self:center;
  display:grid;
  justify-items:center;
  gap:var(--s-sm);
  text-align:center;
  padding-inline:var(--gutter);
}

/* Les deux termes. Même famille et même corps que « Croissance. » — ils
   ne sont plus des étiquettes mais les deux moitiés d'une phrase.

   La course est de 48vw de part et d'autre, et cette valeur se calcule.
   Le bout du doigt parcourt 0,74 unité de scène, soit 871 px sur un
   écran de 1512 ; à 34vw le mot n'en faisait que 514 et se trouvait
   **déjà visible au bord quand la main était encore hors champ** — rien
   ne le poussait, il attendait. À 48vw il part de 149 px hors de
   l'écran et n'entre qu'à 20 % de la course, juste après la main, qui
   franchit le bord à 17 %. L'ordre est alors le bon : la main d'abord,
   le mot derrière.

   En vw pour suivre la largeur de l'écran, comme les mains elles-mêmes,
   dont le cadrage est fixé en largeur visible. */
.signature__term{
  display:inline-block;
  transform:translateX(calc((1 - var(--hands-e, 1)) * var(--push, 0px)));
  /* Le fondu, sur ces deux mots seulement. Il court d'un bout à l'autre
     du glissement et atteint sa pleine valeur au moment même où
     « Croissance. » achève de s'écrire : les trois lignes de la
     signature se posent ensemble.

     LA COURBE COMPTE PLUS QUE LA PLAGE. Étalé linéairement, il occupait
     déjà 82 % de la course et paraissait pourtant court : à mi-parcours
     un fondu linéaire est à moitié fait, donc les mots sont lisibles
     bien avant d'arriver. Ce qu'il fallait allonger n'était pas la
     plage mais la montée.

     Deux réglages y concourent, et ils ne font pas la même chose : le
     **départ** de la rampe dit à partir de quand le mot existe, la
     **puissance** dit à quelle vitesse il se referme. Les monter
     ensemble est ce qui repousse vraiment l'apparition.

     Au fil des passes, à 72 % de la course :

       linéaire, départ 0,18        0,66
       carré,    départ 0,10        0,47
       cube,     départ 0,10        0,33
       puissance 4, départ 0,25     0,15
       puissance 5, départ 0,35     0,06   ← réglage actuel

     Le mot ne vaut plus rien avant 45 % et 0,01 à 60 % : il traverse les
     trois premiers quarts du glissement en pure ombre et ne se referme
     que sur le dernier, avec « Croissance. ».

     C'est le bout de la course utile, et l'arbitrage est assumé : à ce
     réglage, l'idée des mains qui poussent les mots ne se lit plus que
     sur la fin du trajet — avant, il n'y a rien à pousser. Descendre
     d'un cran (puissance 4, départ 0,25) rend le début du glissement
     visible ; monter encore reviendrait à supprimer le glissement au
     profit d'un simple fondu au centre.

     Écrit en multipliant la rampe par elle-même : `pow()` n'est pas
     assez répandu pour un projet sans étape de build, et `calc()` sait
     multiplier des nombres. Vérifié sur l'opacité **résolue** et non sur
     la variable — une propriété personnalisée non enregistrée ne calcule
     pas, et se lirait comme un jeton.

     Le reste de la signature garde son propre calendrier — voir
     `--join`, qui n'ouvre que sur les 28 derniers pour cent. */
  --rise:clamp(0, (var(--hands-e, 1) - .35) / .65, 1);
  opacity:calc(var(--rise) * var(--rise) * var(--rise) * var(--rise) * var(--rise));
}
.signature__term--left{--push:-48vw}
.signature__term--right{--push:48vw}

/* Le signe et ce qui suit n'apparaissent qu'une fois les termes
   rejoints : avant, le `×` flotterait entre deux mots encore lointains,
   et la conclusion serait lisible avant sa prémisse. */
.signature__equation em,
.signature__result,
.signature__note{
  --join:clamp(0, (var(--hands-e, 1) - .72) / .28, 1);
}
.signature__equation em{opacity:var(--join)}
.signature__result .mask{--k:var(--join)}
.signature__note{opacity:var(--join)}

.signature__equation,
.signature__result{
  margin:0;
  line-height:var(--lh-heading);
}
/* L'équation se centre sur sa **masse**, pas sur son signe. C'est un
   arbitrage, et il a été tranché dans les deux sens avant d'être fixé.

   « Acquisition » mesure 173 px contre 131 pour « Création » : les deux
   ne peuvent pas être vrais à la fois.

     — masse centrée : les trois lignes de la signature partagent le même
       axe, mais le `×` tombe 21 px à gauche du centre ;
     — signe centré : le `×` est sur l'axe, mais la masse penche de 21 px
       à droite et « Croissance. » paraît décalée sous elle.

   Les concilier demanderait 42 px d'espace en plus à gauche du signe
   qu'à droite — le calcul tombe juste, mais le signe se retrouverait
   collé à « Acquisition » et perdu loin de « Création ».

   C'est la masse qui gagne : un bloc de trois lignes dont l'une ne
   s'aligne pas se voit tout de suite, un signe à 21 px de l'axe sur une
   ligne de 358 ne se voit pas. */
.signature__equation{
  /* LA COMPOSITION DE LA SIGNATURE : l'autre famille, celle du logotype.
     Manrope en capitales espacées, à la chasse du logo (.2em) — le
     client l'a demandée après une version en deux états de Cormorant,
     italique contre capitales, qui restait « typographie de titre » là
     où il voulait la marque. L'équation est posée petite, au corps du
     logotype ; le mot dessous est le même dessin, quatre fois plus
     grand. Le signe reste bleu. */
  font-family:var(--sans);
  font-size:clamp(13px, 1.1vw, 17px);
  font-weight:500;
  font-style:normal;
  text-transform:uppercase;
  letter-spacing:.2em;
  color:var(--ink-soft);
  /* Une seule ligne, toujours : les deux termes glissent l'un vers
     l'autre, ils ne doivent jamais se replier. */
  white-space:nowrap;
}
.signature__equation em{font-style:normal;color:var(--accent-text);padding-inline:.3em}
.signature__result{
  /* Cormorant en capitales — le mot qu'on grave, sous une équation en
     Manrope qui parle la langue du logotype : les deux familles du site,
     une par ligne. Plus petit que `--fs-h2` parce qu'en capitales : à
     85 px le mot faisait 630 px pour un couloir de 468 entre les doigts
     et mordait dessus ; à 4,2 vw il en fait 436, et des capitales de
     63 px ont la masse d'un bas-de-casse de 85. Une chasse, parce qu'à
     cette échelle des capitales serrées se referment sur elles-mêmes.
     Bleu : c'est le mot de la marque, l'exception du logotype s'étend à
     lui (5,0:1 sur le marbre) — le point, lui, reste à l'encre. */
  font-family:var(--serif);
  font-size:clamp(30px, 4.2vw, 66px);
  font-weight:300;
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--accent);
}
/* Le point après « Croissance » est retiré (client) ; la règle reste
   pour le jour où il reviendrait. */
.signature__dot{color:var(--ink)}
/* Le second mot garde l'encre : c'est « MÉTHODE » qui s'écrit, et le
   logotype y porte seul le bleu, sur « mont ». */
.signature__result--method{color:var(--ink)}
/* « CROISSANCE » en gras — 500, la graisse la plus forte du site —, seul :
   « MÉTHODE RIVMONT » qui prend sa place reste en 300 (demandé). */
.signature__result:not(.signature__result--method){font-weight:500}
/* « Méthode Rivmont. » : le logotype, avec son « mont » en bleu — le
   même geste que l'affirmation du positionnement, et la même exception
   nommée : un logotype garde `--accent`. Et EN GRAS, 500, comme le
   logotype du hero : « MÉTHODE » reste en 300 (demandé). */
.signature__result .wordmark{font-weight:500}
.signature__result .wordmark span{color:var(--accent)}
.signature__note{
  margin:0;
  /* Un cran au-dessus du micro : 11 px, le corps des labels (« agrandis
     un petit peu »). */
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  text-transform:uppercase;
  color:var(--muted);
}

@media (max-width:1000px){
  /* Les deux territoires ne tiennent plus côte à côte : ils passent
     l'un sous l'autre et la passerelle bascule à l'horizontale.
     C'est le même objet, tourné d'un quart de tour. */
  .pillars{grid-template-columns:1fr;gap:var(--s-lg)}
  .bridge{
    align-self:center;
    width:auto;height:clamp(64px,13vw,120px);
    justify-self:stretch;
  }
  .bridge__line{
    top:50%;bottom:auto;left:0;right:0;
    width:auto;height:1px;
    transform:scaleX(var(--on,1));
    transform-origin:left center;
  }
}
@media (max-width:760px){
  /* Le titre redescend sous la scène. `--fs-h2` a touché la borne basse
     de son `clamp` (38 px) et ne rétrécit plus, tandis que le couloir
     entre les mains, lui, continue de suivre la largeur : sous cette
     largeur « Convertir. » finirait par mordre sur les doigts. Le titre
     retrouve donc sa place d'origine, sous l'image.

     La scène bascule au même endroit en gros plan sur les mains, dès que
     la boîte est plus **haute que large** — c'est `camera.aspect < 1` qui
     choisit `fitNarrow` dans `hands.js`. D'où un rapport en portrait et
     non en paysage : c'est contre-intuitif et ça a été posé à l'envers
     une première fois.

     Le ratio quitte la boîte pour le canvas : la boîte doit maintenant
     tenir la scène **et** le titre l'un sous l'autre, donc sa hauteur
     n'est plus celle de l'image. `hands.js` mesure le canvas, pas la
     boîte — c'est ce qui garde le cadrage juste dans les deux cas. */
  /* Ni piste ni collage sur un téléphone : la pause y laisserait un
     grand vide pour rien, et la scène passe en gros plan avec le titre
     dessous. La piste retombe sur la hauteur de son contenu. */
  .hands-track{height:auto}
  .hands{position:static;height:auto}
  /* Deux annulations, et chacune a d'abord fait disparaître la scène.
     `height:auto` parce que le `height:100%` de la règle de base ne se
     résout plus contre rien une fois la boîte en hauteur automatique.
     `min-height:auto` parce que le `min-height:0` qui casse le cercle
     vicieux du cadrage plein écran laisse ici la ligne de grille se
     replier sur zéro : le canvas gardait sa hauteur et débordait d'une
     piste haute de 0 px. Ici c'est le CSS qui impose le ratio, pas le
     rendu — la contribution automatique est donc la bonne. */
  .hands__canvas{
    grid-area:auto;
    /* Un peu moins haut que le 4/5 d'origine. Les mains sont larges et
       fines — 0,494 unité sur 1,10 de largeur visible, soit 168 px de
       haut sur un téléphone : dans un cadre au 4/5 il restait 300 px de
       vide et le titre se retrouvait loin de l'image. Le rapport reste
       sous 1, ce qui est la condition du gros plan (`camera.aspect < 1`
       choisit `fitNarrow`). */
    aspect-ratio:1 / 1.1;
    height:auto;
    min-height:auto;
  }
  /* Alignement à gauche sur téléphone : trois mots centrés sur une
     colonne étroite flottent, là où le drapeau à gauche tient contre le
     texte qui suit. Les propriétés de grille et la gouttière que cette
     règle portait ont été retirées avec la scène des mains : le titre
     est dans `.container`, qui applique déjà la sienne — les cumuler
     donnait un double retrait. */
  .positioning__claim{
    justify-items:start;
    text-align:left;
    max-width:none;
    margin-top:var(--s-lg);
  }
  .positioning__claim .mask{--k:1}
  /* La signature quitte la scène et redevient un bloc dans le flux : la
     grille à trois colonnes n'a pas de place sur un téléphone, et les
     mains y sont en gros plan, pas de part et d'autre d'un couloir. Les
     quatre blocs se lisent l'un sous l'autre, dans l'ordre du DOM —
     création, acquisition, puis ce qu'ils produisent. */
  /* La signature quitte la scène et redevient un bloc dans le flux : sur
     un téléphone les mains sont en gros plan, pas de part et d'autre
     d'un couloir, et il n'y a rien à pousser. Le glissement est donc
     neutralisé et les trois lignes se lisent l'une sous l'autre. */
  .signature{
    grid-area:auto;
    place-self:auto;
    justify-items:start;
    text-align:left;
    padding-inline:0;
    margin-top:var(--s-lg);
  }
  /* Dans le cadre du procédé, la boîte est pleine largeur et n'a plus de
     container pour porter la gouttière : la signature la reprend. */
  .process__hands .signature{padding-inline:var(--gutter)}

  .signature__term{transform:none;opacity:1}
  .signature__equation{white-space:normal}
  .signature__equation em,
  .signature__result,
  .signature__note{--join:1}
  .pillar__text{max-width:none}
}

/* =========================================================
   04 / 05 — EXPERTISES
   Deux colonnes : texte à gauche, zone visuelle réservée à droite.
   La zone visuelle est un emplacement vide et nommé — elle accueillera
   plus tard une scène 3D, une vidéo ou une visualisation.
   ========================================================= */
.expertise__grid{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:var(--s-xl);
  align-items:start;
}
.expertise__aside{position:relative}
.expertise__sticky{position:sticky;top:calc(var(--header-h) + var(--s-md))}

/* Liste typographique plutôt qu'une grille de cartes. */
.service-list{border-top:1px solid var(--line-soft)}
.service-list li{
  display:grid;
  grid-template-columns:auto 1fr;
  align-items:baseline;gap:var(--s-sm);
  padding-block:var(--s-xs);
  border-bottom:1px solid var(--line-soft);
  transition:background var(--dur) var(--ease);
}
.service-list li:hover{background:var(--panel)}
.service-list__index{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  color:var(--faint);font-variant-numeric:tabular-nums;
}
.service-list__name{font-size:var(--fs-lead);color:var(--ink)}

/* Emplacement média réservé — remplacé plus tard par une scène ou une vidéo. */
.media-slot{
  position:relative;
  aspect-ratio:4/5;
  border:1px solid var(--line-soft);
  background:
    linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,0));
  display:grid;place-items:center;
}
.media-slot--wide{aspect-ratio:16/10}
.media-slot__note{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--faint);
}

/* La chaîne d'acquisition : plus technique, plus dense. */
.pipeline{
  display:grid;
  grid-template-columns:repeat(6,1fr);
  border:1px solid var(--line-soft);
  margin-top:var(--s-lg);
}
.pipeline__step{
  padding:var(--s-sm) var(--s-xs);
  border-right:1px solid var(--line-soft);
  display:grid;gap:8px;align-content:start;
}
.pipeline__step:last-child{border-right:0}
.pipeline__step b{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--accent-text);font-weight:400;
}
.pipeline__step span{font-size:14px;color:var(--ink-soft)}

.platforms{display:flex;flex-wrap:wrap;gap:var(--s-xs);margin-top:var(--s-md)}
.platforms li{
  padding:9px 16px;
  border:1px solid var(--line-soft);
  font-size:var(--fs-label);letter-spacing:var(--tr-wide);
  text-transform:uppercase;color:var(--ink-soft);
}

@media (max-width:1000px){
  .expertise__grid{grid-template-columns:1fr;gap:var(--s-lg)}
  .expertise__sticky{position:static}
  .pipeline{grid-template-columns:repeat(3,1fr)}
  .pipeline__step:nth-child(3n){border-right:0}
  .pipeline__step{border-bottom:1px solid var(--line-soft)}
}
@media (max-width:760px){
  .pipeline{grid-template-columns:1fr 1fr}
  .pipeline__step:nth-child(3n){border-right:1px solid var(--line-soft)}
  .pipeline__step:nth-child(2n){border-right:0}
  .media-slot{aspect-ratio:1/1}
}

/* =========================================================
   06 — MÉTHODE
   Structure prête pour une future séquence collante : la colonne
   gauche restera fixe pendant que les étapes défilent à droite.
   ========================================================= */
.method__grid{
  display:grid;
  grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);
  gap:var(--s-xl);
  align-items:start;
}
.method__aside{position:sticky;top:calc(var(--header-h) + var(--s-md))}
.method__steps{border-top:1px solid var(--line-soft)}
.method-step{
  display:grid;
  grid-template-columns:auto 1fr;
  gap:var(--s-md);
  padding-block:var(--s-lg);
  border-bottom:1px solid var(--line-soft);
}
.method-step__index{
  font-family:var(--serif);
  font-size:clamp(30px,3.4vw,54px);
  line-height:1;
  color:var(--faint);
  font-variant-numeric:tabular-nums;
}
.method-step.is-active .method-step__index{color:var(--accent-text)}
.method-step__name{
  font-size:var(--fs-h3);
  font-family:var(--serif);
  color:var(--ink);
  margin-bottom:var(--s-2xs);
}

@media (max-width:1000px){
  .method__grid{grid-template-columns:1fr;gap:var(--s-lg)}
  .method__aside{position:static}
}

/* =========================================================
   07 — SELECTED WORK
   Chaque projet occupe une large part du viewport. Le composant est
   autonome pour permettre plus tard des transitions entre projets.
   ========================================================= */
.work__list{display:grid;gap:var(--s-3xl)}

.project{
  display:grid;
  grid-template-columns:minmax(0,.9fr) minmax(0,1.4fr);
  gap:var(--s-lg) var(--s-xl);
  align-items:start;
}
.project__meta{display:grid;gap:var(--s-md);align-content:start}
.project__count{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  color:var(--muted);font-variant-numeric:tabular-nums;
}
.project__client{font-family:var(--serif);font-size:var(--fs-h2);line-height:1;color:var(--ink)}
.project__facts{display:grid;gap:10px}
.project__fact{
  display:grid;grid-template-columns:auto 1fr;gap:var(--s-sm);
  align-items:baseline;
  padding-block:8px;
  border-top:1px solid var(--line-soft);
}
.project__fact dt{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--muted);
}
.project__fact dd{margin:0;color:var(--ink-soft);font-size:14px}
.project__media{position:relative}

@media (max-width:1000px){
  .work__list{gap:var(--s-2xl)}
  .project{grid-template-columns:1fr}
}

/* =========================================================
   08 — RÉSULTATS
   Toutes les valeurs sont des gabarits (XX+, XXX%). La section
   entière se masque en posant `hidden` sur l'élément.
   ========================================================= */
.metrics{
  display:grid;
  grid-template-columns:repeat(4,1fr);
  border-top:1px solid var(--line-soft);
}
.metric{
  padding:var(--s-lg) var(--s-sm) var(--s-md) 0;
  border-bottom:1px solid var(--line-soft);
  display:grid;gap:var(--s-2xs);align-content:start;
}
.metric__value{
  font-family:var(--serif);
  font-size:clamp(44px,5vw,86px);
  line-height:1;
  color:var(--ink);
}
.metric__value[data-placeholder]{color:var(--faint)}
.metric__label{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--muted);
}

@media (max-width:1000px){ .metrics{grid-template-columns:1fr 1fr} }
@media (max-width:560px){ .metrics{grid-template-columns:1fr} }

/* =========================================================
   09 — TERRITOIRES
   ========================================================= */
.territories__list{display:grid;border-top:1px solid var(--line-soft)}
.territory{
  display:grid;
  grid-template-columns:auto 1fr auto;
  align-items:center;gap:var(--s-sm);
  padding-block:var(--s-md);
  border-bottom:1px solid var(--line-soft);
}
.territory__city{
  font-family:var(--serif);
  font-size:clamp(38px,5.6vw,92px);
  line-height:1;
  color:var(--ink);
  transition:color var(--dur) var(--ease);
}
.territory:hover .territory__city{color:var(--accent-text)}
.territory__country,.territory__role{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--muted);
}
.territory__role{text-align:right}

@media (max-width:760px){
  .territory{grid-template-columns:1fr auto}
  .territory__country{grid-column:1;order:2}
  .territory__role{grid-column:2;order:3}
  .territory__city{grid-column:1/-1;order:1}
}

/* =========================================================
   10 — PROFIL CLIENT
   ========================================================= */
.clients__grid{
  display:grid;
  grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);
  gap:var(--s-xl);
  align-items:end;
}
.clients__tags{display:flex;flex-wrap:wrap;gap:var(--s-2xs)}
.clients__tags li{
  padding:10px 18px;
  border:1px solid var(--line-soft);border-radius:999px;
  font-size:var(--fs-label);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-soft);
}
@media (max-width:1000px){ .clients__grid{grid-template-columns:1fr;gap:var(--s-lg)} }

/* =========================================================
   11 — FINAL CTA
   Section haute, réservée au retour de la sculpture (boucle narrative).
   ========================================================= */
.final{
  position:relative;
  min-height:100svh;
  display:grid;place-items:center;
  text-align:center;
  padding-block:var(--s-3xl);
  overflow:hidden;
}
.final__stage{
  position:absolute;inset:0;
  z-index:var(--z-canvas);
  pointer-events:none;
}
.final__inner{position:relative;z-index:var(--z-content);display:grid;gap:var(--s-md);justify-items:center}
/* LE TITRE, DANS LE REGISTRE DE LA DÉCLARATION DU HERO (`.credo__claim`) :
   Cormorant 300, capitales romaines contre bas-de-casse italique en
   alternance, même corps, même interlignage serré, mêmes compensations
   (chasse sur les capitales, 1,06 em sur l'italique) ; la question — les
   deux dernières lignes — en 500, comme « NOUS REFUSONS / de l'être. ». */
.final__title{
  margin:0;
  font-family:var(--serif);
  font-weight:300;
  font-size:clamp(26px,2.2vw,40px);
  line-height:.9;
  letter-spacing:-.015em;
  color:var(--ink);
}
.final__title span{display:block}
.final__title .caps{text-transform:uppercase;letter-spacing:.012em}
.final__title em{font-style:italic;letter-spacing:0;font-size:1.06em}
.final__title .final__strong{font-weight:500}
/* Une respiration entre l'affirmation et la question : la première
   ligne en 500 prend une marge d'un demi-corps. */
.final__title .caps.final__strong{margin-top:.5em}
/* EN RÉSERVE — la question en bleu, quand le titre était en `.display`.
.final__title em{font-style:normal;color:var(--accent-text)} */
/* La ligne de qualification — la sélectivité de `#clients`, en positif :
   qui l'on cherche, pas qui l'on refuse. En retrait, courte. */
.final__lead{max-width:44ch;color:var(--muted)}
/* Le lien WhatsApp, serré sous le bouton : la grille espace de `--s-md`,
   la marge négative ramène l'écart à `--s-xs` — c'est la suite du
   bouton, pas un autre choix. En `--muted`, le filet du survol en accent. */
.final__ask{margin:calc(var(--s-xs) - var(--s-md)) 0 0;color:var(--muted)}
.final__ask .text-link:hover{color:var(--ink)}
/* LE CTA VIT DANS `#method`, QUI EST EN MARBRE PAR SES JETONS : il
   rebascule au noir par les siens — les valeurs de la racine, écrites
   ici parce qu'un jeton hérité ne se « remet » pas, il se redéfinit —,
   et pose sa couleur, comme le marbre pose la sienne. Même bascule,
   dans l'autre sens. Les jetons sont sur la scène du musée entière :
   ses poussières lisent `--ink` aussi. */
.panels__museum{
  --bg:var(--night);
  --ink:var(--night-ink);
  --ink-soft:var(--night-ink-soft);
  --muted:var(--night-muted);
  --faint:var(--night-faint);
  --line:var(--night-line);
  --line-soft:var(--night-line-soft);
  --accent-text:var(--night-accent-text);
}
.panels__final{
  background:var(--bg);
  color:var(--ink);
}

/* =========================================================
   12 — PROJECT INQUIRY
   Formulaire multi-étapes. Seul l'état est géré pour l'instant :
   aucune soumission, aucun backend.
   ========================================================= */
.inquiry__shell{
  border:1px solid var(--line-soft);
  background:var(--bg-raise);
  padding:clamp(24px,3.4vw,60px);
}
.inquiry__progress{
  display:flex;align-items:center;gap:var(--s-xs);
  margin-bottom:var(--s-lg);
}
.inquiry__dot{
  flex:1;height:2px;background:var(--line-soft);
  transition:background var(--dur) var(--ease);
}
.inquiry__dot.is-done{background:var(--accent)}
.inquiry__step[hidden]{display:none}
.inquiry__question{margin-bottom:var(--s-md)}

.choice-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--s-2xs)}
.choice{
  display:block;width:100%;text-align:left;
  padding:var(--s-sm);
  border:1px solid var(--line-soft);
  color:var(--ink-soft);
  transition:border-color var(--dur) var(--ease),background var(--dur) var(--ease),color var(--dur) var(--ease);
}
.choice:hover{border-color:var(--line);background:var(--panel);color:var(--ink)}
.choice[aria-pressed="true"]{border-color:var(--accent);color:var(--ink);background:rgba(0,72,255,.12)}

.field{display:grid;gap:8px;margin-bottom:var(--s-sm)}
.field label{
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--muted);
}
.field input,.field textarea{
  width:100%;
  padding:14px 16px;
  background:transparent;
  border:1px solid var(--line-soft);
  color:var(--ink);
  font:inherit;
  transition:border-color var(--dur) var(--ease);
}
/* `outline:none` a été retiré : la bordure d'un pixel était le seul
   indicateur de focus des champs, et elle ne suffit pas. L'anneau global
   s'ajoute donc par-dessus, décalé de 3 px. */
.field input:focus,.field textarea:focus{border-color:var(--accent-text)}
.field textarea{min-height:120px;resize:vertical}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-sm)}

.inquiry__actions{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--s-sm);margin-top:var(--s-lg);
}

@media (max-width:760px){
  .choice-grid{grid-template-columns:1fr}
  .field-row{grid-template-columns:1fr}
}

/* =========================================================
   LE PROCÉDÉ — trois états révélés par balayage
   ---------------------------------------------------------
   LES IMAGES NE BOUGENT PAS. Elles sont empilées, exactement au même
   endroit et à la même taille, et c'est une **découpe** qui se déplace :
   chaque étape se révèle depuis le bord droit, sa limite gauche filant
   vers la gauche jusqu'à couvrir l'écran. Aucune translation, aucune
   échelle, aucun fondu entre les images — un `wipe` net, et rien d'autre.

   C'est ce que la continuité des rendus impose. Les trois partagent la
   même caméra : même sol, mêmes colonnes, même mur. Superposés, ils se
   recouvrent au pixel, et le balayage donne la seule chose qu'on veut
   voir — la sculpture qui se dégage de sa gangue sans que le cadre
   bouge. Les faire défiler côte à côte a été essayé et retiré : ça
   racontait trois photographies au lieu d'une transformation.

   UNE SEULE VALEUR PILOTE TOUT : `--pp`, publiée sur la section par
   `ui/process.js`, de 0 à 1 sur la course. Chaque découpe, chaque texte
   et la barre en sont tirés.

   POURQUOI PAS GSAP + ScrollTrigger, qui étaient nommés dans le brief.
   `scrub` avec `ease:"none"` produit exactement une progression linéaire
   réversible — c'est déjà ce que `--pp` est. La différence tient au
   coût : il n'y a ni bundler ni CDN autorisé ici, donc il faudrait
   vendoriser une quarantaine de kilo-octets et les inscrire dans l'import
   map, pour piloter des `clip-path` que le CSS calcule seul, sans une
   ligne de JavaScript par image. Le `pin` est un `position:sticky`, le
   `end:"+=300%"` est la hauteur de la piste, et le nettoyage au démontage
   n'a pas lieu d'être : il n'y a rien à démonter.
   ========================================================= */
.process{
  position:relative;background:var(--bg);

  /* ---------- l'ouverture ----------
     LA SECTION NAÎT D'UN PETIT RECTANGLE posé sous l'appel à l'action de
     la section précédente. Il monte avec la page, se colle au centre de
     l'écran, puis s'ouvre — en largeur, en hauteur, et ses angles se
     redressent — jusqu'à occuper exactement la fenêtre. À ce moment il
     **est** la première image du procédé, et le balayage peut commencer.

     CE N'EST PAS UN `scale()`. La boîte change de taille, l'image la
     couvre (`cover`) au même point de cadrage (`--fit`) du début à la
     fin : ce qu'on voit grandir est la fenêtre, et le cadrage final est
     celui de la première image, au pixel — il n'y a rien à raccorder.

     Les dimensions de départ, l'écart sous le bouton et l'angle sont
     ici ; la course d'ouverture aussi, et elle a un pendant en
     JavaScript (voir `--process-open`). */
  --frame-w:clamp(240px, 22vw, 360px);
  /* CARRÉ, comme la référence : un rectangle en 16/10 se lisait comme
     une vignette de galerie ; le carré se lit comme un objet posé. */
  --frame-h:var(--frame-w);
  --frame-gap:clamp(60px, 7vh, 100px);
  --frame-r:4px;

  /* LA COURSE D'OUVERTURE, EN HAUTEURS D'ÉCRAN — et elle est écrite deux
     fois. `ui/process.js` porte la même valeur (`OPEN = 1`) : il publie
     `--pa` de 0 à 1 sur ce premier écran de piste, puis `--pp` sur le
     reste. La piste en tient compte (`--process-track`), et le repère
     `.process__light` aussi. Les deux valeurs se changent ensemble ou
     pas du tout — même règle que `CUT + PRE` entre le hero et la pièce. */
  --process-open:100svh;
  /* LA COURSE DE FERMETURE, même règle : `CLOSE = 1` dans `ui/process.js`.
     Le dernier écran de la piste, où les deux portes s'écartent. */
  --process-close:160svh;
  /* LE RELAIS, même règle : `HAND = 1.3` dans `ui/process.js`. Assez
     long pour que le mot se transforme (0,30 → 0,55), que la boîte
     remonte (0,35 → 0,75) et que `#method` n'entre qu'ensuite. */
  --process-hand:130svh;
  /* LA QUEUE : voir `.process.is-live`. Zéro par défaut ; la course des
     panneaux au-dessus de 1000 px (jeton partagé, `tokens.css`). */
  --process-tail:0svh;

  /* LE CHEVAUCHEMENT, ET IL SE CALCULE. Le cadre est au centre de sa
     scène de 100 svh ; pour qu'il paraisse à `--frame-gap` sous le
     bouton — lui-même à 5 svh du bas de la section précédente —, la
     piste remonte sous cette section d'exactement ce qu'il faut :
     55 svh, moins une demi-hauteur de cadre, moins l'écart. Tout ce qui
     est au-dessus du cadre reste transparent, et c'est la section
     précédente qu'on voit à travers, bouton compris. */
  --process-overlap:calc(55svh - var(--frame-h) / 2 - var(--frame-gap));

  /* LE CADRAGE PASSE PAR UN JETON, ET C'EST UNE CORRECTION DE BOGUE.
     Écrit en clair sur `.process__img`, il perdait contre
     `.process.is-live .process__img` — trois classes contre une — et le
     téléphone recadrait au centre : des colonnes plein écran, la
     sculpture hors champ. Une valeur héritée n'a pas de spécificité,
     donc le point de cadrage se règle où il se pense, par palier, sans
     jamais courir après le sélecteur qui l'emporte.

     12 % ET NON LE CENTRE, À CAUSE DU CARRÉ. Dans le petit cadre carré,
     `cover` ne montre qu'une bande de 56 % de la largeur de l'image ;
     centrée, elle coupait la sculpture, qui occupe les 16 à 41 % de
     gauche. Le point de cadrage `p` ouvre la fenêtre en
     `[p·(1−f), p·(1−f)+f]` : à 12 % elle commence à 5 % et la pièce se
     tient entre 19 et 64 % du carré, anneau compris — presque au
     centre. Plein écran, la même valeur ne déplace rien de visible : en
     1512 × 860 la fenêtre couvre 99 % de l'image (8 px d'écart avec le
     centre), en 1440 × 900 elle en couvre 90 % (61 px). */
  --fit:12% center;
}
.process.is-live{
  /* Cinq écrans et quelques : un écran d'ouverture, puis — inchangé —
     deux balayages d'une bonne image et demie chacun et les trois temps
     d'arrêt qui les encadrent. Le rythme est la moitié du sujet — un
     balayage rapide ressemble à une transition, un balayage tenu
     ressemble à un geste. */
  /* Plus LA QUEUE : au-delà du relais, la scène reste collée — avec
     son titre — pendant toute la séquence des panneaux de `#method`,
     qui monte par-dessus, transparente. `ui/process.js` la soustrait
     de sa course (sonde `.process__tail`). Nulle sous 1000 px, où les
     panneaux sont dans le flux. */
  height:calc(var(--process-track, 810svh) + var(--process-tail, 0svh));
  margin-top:calc(-1 * var(--process-overlap));
  /* Au même niveau que le panneau qu'elle chevauche (`.section--panel`
     est à `--z-sticky`), et après lui dans le DOM : la piste passe donc
     **au-dessus** de la section précédente, ce qui est la condition pour
     que le voile l'estompe. Sans cette ligne, le panneau restait devant
     et le voile ne voilait rien. */
  z-index:var(--z-sticky);
  /* Transparente sur le chevauchement, marbre en dessous : c'est le
     pseudo-élément qui porte le fond, et il commence là où la section
     précédente s'arrête. Un fond sur la piste entière aurait recouvert
     le bouton qu'on veut voir au-dessus du cadre. */
  background:transparent;
  /* ET ELLE NE PREND PAS LE POINTEUR : la scène le laissait déjà
     (`pointer-events:none`, plus bas), mais la piste elle-même, au
     niveau `--z-sticky` et après le positionnement dans le DOM,
     couvrait le bouton « Démarrer un projet » sur tout le chevauchement
     — mesuré : `elementFromPoint` sur le bouton renvoyait la section,
     le clic n'arrivait jamais. Le cadre reprend la main sur lui-même ;
     rien d'autre dans la piste n'a besoin du pointeur (les mains
     écoutent `pointermove` sur `window`). */
  pointer-events:none;
}
.process.is-live::before{
  content:"";
  position:absolute;
  /* Quelques pixels plus haut que la couture, et c'est mesuré : deux
     bords de marbre qui se touchent au pixel près laissent passer un
     cheveu de noir dès que l'écran est mis à l'échelle — les deux
     arêtes sont lissées chacune contre le fond, et le fond est noir. À
     1 px le cheveu restait ; à 8 px la marge couvre tout facteur
     d'échelle, et le bouton, à 5 svh du bord, n'est pas atteint. */
  top:calc(var(--process-overlap) - 8px);left:0;right:0;bottom:0;
  background:var(--marble);
}
.process.is-live .process__stage{
  position:sticky;top:0;
  height:100svh;
  /* Sur la scène collée, jamais sur la piste : posé sur la piste, un
     `overflow` désactiverait le collant sans rien signaler. */
  overflow:hidden;
  isolation:isolate;
  /* La scène chevauche le bouton de la section précédente tant que le
     cadre est petit : elle doit le laisser cliquer. Le cadre reprend la
     main sur lui-même. */
  pointer-events:none;
}
/* Le voile. La section précédente s'efface derrière le cadre pendant
   qu'il s'ouvre — en douceur, sur le premier tiers, le temps que son
   bouton finisse de sortir par le haut. Marbre sur marbre : seul le
   texte s'estompe. */
.process.is-live .process__stage::before{
  content:"";
  position:absolute;inset:0;
  background:var(--marble);
  opacity:clamp(0, calc((var(--pa, 1) - .06) / .34), 1);
}

/* ---------- le cadre ----------
   Centré par une translation, jamais par ses marges : le centre est ce
   qui ne doit pas bouger, et une boîte qui grandit depuis une marge
   glisserait. Largeur, hauteur et angle sont trois interpolations de la
   même valeur adoucie `--e` — une Hermite (3t² − 2t³), l'équivalent d'un
   `power1.inOut` calculé en CSS : départ doux, accélération, arrivée
   posée. Le scroll reste le seul moteur ; la courbe ne fait que
   redistribuer sa course, elle n'ajoute aucun temps.

   L'ouverture occupe 82 % du premier écran ; les 18 % restants sont le
   temps d'arrêt en plein cadre, avant même que `--pp` ne démarre. */
.process.is-live .process__frame{
  --t:clamp(0, calc(var(--pa, 1) / .82), 1);
  --e:calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  position:absolute;
  left:50%;top:50%;
  width:calc(var(--frame-w) + (100% - var(--frame-w)) * var(--e));
  height:calc(var(--frame-h) + (100% - var(--frame-h)) * var(--e));
  transform:translate(-50%, -50%);
  border-radius:calc(var(--frame-r) * (1 - var(--e)));
  overflow:hidden;
  isolation:isolate;
  pointer-events:auto;
  /* Le marbre que les portes découvrent. Invisible sous les images tant
     qu'elles sont là. */
  background:var(--marble);
}

/* ---------- les deux portes ----------
   La sortie de la section. À la fermeture (`--pc`), tout ce qui habille
   l'image s'éteint — texte, trame, poussières, repère — puis l'image de
   la troisième étape se fend par le milieu : deux moitiés qui s'écartent
   vers leurs bords et découvrent le marbre de la section suivante, où
   les mains arrivent. Le symétrique de l'entrée, où l'image s'ouvrait
   depuis son centre.

   LES PORTES SONT DES COPIES, ET C'EST CE QUI REND LE RELAIS INVISIBLE.
   Chacune porte la même image que la scène, au même cadrage, avec la
   même dérive de caméra — même animation, même départ, donc même
   position à tout instant. Elles n'existent qu'à partir de la fermeture
   et couvrent alors la scène pixel pour pixel ; les trois plaques
   s'éteignent sous elles au même moment, et rien ne se voit.

   `clip-path` se calcule avant `transform` : la moitié découpée voyage
   avec la porte — c'est l'inverse du balayage, où la découpe devait
   rester fixe et où la dérive a été posée sur l'image. Ici les deux
   sont au même endroit, et c'est voulu. Translation de 50 % de leur
   largeur : la moitié visible sort exactement par son bord. `--door`
   est publié sur la racine par `ui/process.js`, adouci — les portes
   bougent de 0,15 à 0,90 de la fermeture, le reste est le temps
   d'éteindre l'habillage avant, et de tenir le marbre après. */
/* ---------- les mains, derrière ----------
   La scène des deux mains vit dans le cadre, sous les portes, sur le
   marbre qu'elles découvrent. Centrée dans l'écran ; la boîte `.hands`
   garde sa hauteur mais perd ce qui la faisait coller et déborder d'un
   container — ici elle est déjà pleine largeur et déjà collée, par le
   cadre. Sa progression ne vient plus d'une piste : voir `hands.js`,
   `follow`. Invisible tant que les portes n'ont pas commencé à s'ouvrir
   — le canvas est vide, mais la signature est là, à sa position de
   départ hors champ ; l'opacité évite qu'un repli l'affiche au centre
   avant l'heure. Les jetons du marbre s'appliquent à cette boîte seule :
   le reste du cadre reste sur le noir de l'image. */
.process.is-live .process__hands{
  position:absolute;inset:0;
  z-index:3;
  /* Une piste de grille DÉFINIE, et c'est le cercle vicieux du canvas
     qui l'impose : sur une ligne `auto`, la boîte en `height:100%`
     retombe sur la hauteur intrinsèque du canvas — celle que
     `renderer.setSize` vient d'y écrire, 1 080 px sur un écran de 860 —,
     `hands.js` la remesure, la réécrit, et le canvas déborde sans jamais
     revenir. Mesuré. Avec une ligne à 100 %, la boîte a une hauteur
     définie et le canvas s'y tient. */
  display:grid;place-items:center;
  grid-template-columns:100%;
  grid-template-rows:100%;
  opacity:clamp(0, calc(var(--pc, 0) * 1000), 1);
  --ink:var(--marble-ink);
  --ink-soft:var(--marble-ink-soft);
  --muted:var(--marble-muted);
  --faint:var(--marble-faint);
  --line:var(--marble-line);
  --line-soft:var(--marble-line-soft);
  --accent-text:var(--accent-deep);
  color:var(--ink);
}
.process.is-live .process__hands .hands{
  position:relative;top:auto;
  width:100%;
  /* LE RELAIS : une fois le mot transformé, la boîte remonte vers le
     haut du cadre, et `#method` vient se glisser dessous — la section
     chevauche la fin de la piste (`--method-overlap`), ses tableaux
     arrivent sous le titre pendant que la scène est encore collée.
     DE COMBIEN : ce qu'il faut pour que le titre, réduit, soit au
     milieu de l'air entre la barre et la rangée — `--hands-rise`,
     mesuré et publié en pixels par `ui/panels.js` (`measureTitle`) ;
     34 svh, l'ancienne valeur posée à l'œil, en repli. */
  --up:clamp(0, calc((var(--pd, 0) - .35) / .40), 1);
  --up-e:calc(var(--up) * var(--up) * (3 - 2 * var(--up)));
  transform:translateY(calc(var(--up-e) * -1 * var(--hands-rise, 34svh)));
  /* PLEIN CADRE, ET C'EST POUR LA POUSSIÈRE. La boîte faisait 620 px,
     centrée : le canvas s'arrêtait à 120 px du haut de l'écran, et les
     éclats qui montaient au survol disparaissaient à son bord — sous le
     header, à l'œil, alors que la barre est transparente. Le cadrage ne
     change pas : `hands.js` cadre sur la largeur visible, la hauteur ne
     donne que de l'air. Sur téléphone la boîte reprend sa hauteur de
     contenu (bloc mobile du procédé) : pas de survol au doigt. */
  height:100%;
  margin-inline:0;
}
/* Les mains ne montent pas : elles repartent par où elles sont venues,
   chacune vers son bord — c'est `hands.js` qui rejoue sa course à
   l'envers sur le relais (`hand`), et les deux termes partent avec
   elles, poussés dehors comme ils ont été poussés dedans. Ce qui reste
   au centre lit `--hands-in`, l'entrée seule : le mot, et la note. */
.process.is-live .process__hands .signature__equation em,
.process.is-live .process__hands .signature__result,
.process.is-live .process__hands .signature__note{
  --join:clamp(0, (var(--hands-in, 1) - .72) / .28, 1);
}
/* Le titre NE SORT PLUS à la fin du relais : la scène des panneaux
   (`#method`) le reprend à l'identique — mêmes classes, même place —
   au pixel exact où elle se colle et où celle-ci se décolle, et c'est
   lui qui reste au-dessus des quatre panneaux. Il sortait par le haut
   de son masque (`--gone` de .86 à 1 du relais) quand la méthode était
   une grille qui arrivait dessous ; le client a voulu garder ce titre
   avec les cartes, pas en créer un second. La note, qui n'a pas de
   masque, s'efface comme avant. La position finale est mesurée dans
   `sections.css` (« la méthode — quatre panneaux »). */
.process.is-live .process__hands .signature__result--method{
  --gone:0;
  /* ET IL SE RÉDUIT quand les cartes sont montées : `--method-mp` est
     la progression des panneaux, publiée sur la racine par
     `ui/panels.js` (la barre et cette scène ne peuvent pas lire les
     jetons de `#method`, comme `--frame-top` dans l'autre sens). Sur la
     phase des portes (0 → .07), de 1 à .52, par le haut — le titre
     garde sa ligne, la rangée commence dessous. Posé à .76 d'abord, le
     client l'a voulu plus petit. Une échelle sur le bloc, pas sur le
     `i` du masque qui porte déjà sa translation. */
  --shrink:clamp(0, calc(var(--method-mp, 0) / .07), 1);
  /* .52 = le corps des noms des panneaux (2,2 vw) sur celui du titre
     (4,2 vw), demandé : réduit, le titre est à leur taille. C'était
     .64 (« plus petit », après .76), puis .74 quand les noms étaient à
     3,1 vw. `SHRINK` dans `ui/panels.js`, par paires. */
  transform:scale(calc(1 - .48 * var(--shrink)));
  transform-origin:50% 0;
  /* EN RÉSERVE — il s'effaçait quand le musée commençait (`--method-mq`,
     même publieur, les premiers .15 de cette course). Retiré à la
     demande du client : il reste, et le musée le recouvre.
  opacity:calc(1 - clamp(0, calc(var(--method-mq, 0) / .15), 1)); */
}
/* LA NOTE DE LA MÉTHODE — « Quatre temps, jamais séparés. » — ENTRE ET
   SORT COMME LES DÉCLARATIONS DU HERO : par le bas de son masque, avec
   son opacité (`.mask > i`, `base.css`, la même mécanique que
   `--credo-p`). Elle n'était qu'un fondu ; le client a voulu, pour sa
   disparition, l'animation des textes du hero — l'entrée suit, pour
   que le geste soit le même dans les deux sens. Entrée de .40 à .60
   du relais, sortie de .86 à .98 : elle est partie avant que le titre
   ne soit seul au-dessus des cartes. */
.process.is-live .process__hands .signature__note--method{
  opacity:1;
}
.process.is-live .process__hands .signature__note--method .mask{
  --k:calc(clamp(0, calc((var(--pd, 0) - .40) / .20), 1)
         * (1 - clamp(0, calc((var(--pd, 0) - .86) / .12), 1)));
}
.process.is-live .process__hands .signature__note--method .mask > i{
  opacity:var(--k, 1);
}
/* La transformation du texte. L'équation s'efface la première ;
   « Croissance. » sort par le haut de son masque pendant que « Méthode
   Rivmont. » entre par le bas du sien, à la même place — les deux
   résultats sont empilés sur la même cellule. La note fait de même. Le
   masque de `base.css` ne connaît que `--k`, l'entrée : la sortie
   s'ajoute ici, en `--gone`. */
.signature__swap{display:grid;justify-items:center}
.signature__swap > *{grid-area:1/1}
/* Le signe seul s'efface : les deux termes partent avec les mains. */
.process.is-live .process__hands .signature__equation em{
  opacity:calc(var(--join) * (1 - clamp(0, calc(var(--pd, 0) / .20), 1)));
}
.process.is-live .process__hands .signature__result .mask > i{
  transform:translateY(calc((1 - var(--k, 1)) * 112% - var(--gone, 0) * 112%));
}
.process.is-live .process__hands .signature__result:not(.signature__result--method){
  --gone:clamp(0, calc((var(--pd, 0) - .10) / .25), 1);
}
.process.is-live .process__hands .signature__result--method .mask{
  --k:clamp(0, calc((var(--pd, 0) - .30) / .25), 1);
}
.process.is-live .process__hands .signature__note:not(.signature__note--method){
  opacity:calc(var(--join) * (1 - clamp(0, calc((var(--pd, 0) - .10) / .20), 1)));
}


/* Sans script — ou sous mouvement réduit — la signature reste un
   contenu : le bloc redevient une bande de marbre dans le flux, sans
   canvas, ses lignes l'une sous l'autre. */
.process:not(.is-live) .process__hands{
  display:block;
  padding:var(--section-y) var(--gutter);
  background:var(--marble);
  --ink:var(--marble-ink);--ink-soft:var(--marble-ink-soft);
  --muted:var(--marble-muted);--faint:var(--marble-faint);
  --line:var(--marble-line);--line-soft:var(--marble-line-soft);
  --accent-text:var(--accent-deep);
  color:var(--ink);
}
.process:not(.is-live) .process__hands .hands{position:static;height:auto;margin:0;width:auto}
.process:not(.is-live) .process__hands .hands__canvas{display:none}
.process:not(.is-live) .process__hands .signature{grid-area:auto;place-self:auto;padding:0}
.process:not(.is-live) .process__hands .signature__swap > *{grid-area:auto}

.process.is-live .process__door{
  position:absolute;inset:0;
  z-index:4;
  opacity:clamp(0, calc(var(--pc, 0) * 1000), 1);
  will-change:transform;
}
.process.is-live .process__door--left{
  clip-path:inset(0 50% 0 0);
  transform:translate3d(calc(var(--door, 0) * -50%), 0, 0);
}
.process.is-live .process__door--right{
  clip-path:inset(0 0 0 50%);
  transform:translate3d(calc(var(--door, 0) * 50%), 0, 0);
}
/* Les trois plaques de la scène s'éteignent dès que les portes sont
   là — elles montrent la même chose, il n'y a rien à fondre. */
.process.is-live .process__step .process__plate{
  opacity:calc(1 - clamp(0, var(--pc, 0) * 1000, 1));
}

/* Le repère du header. `ui/header.js` lit `[data-header="light"]` et
   prend la barre en marbre tant qu'elle est entièrement dessus : ce
   trait invisible la tient sur le marbre pendant l'ouverture, puis
   `data-header="clear"` — sur la section — reprend et la barre
   s'efface sur l'image.

   LA BARRE EST TRANSPARENTE PARTOUT SUR LE MARBRE ; ce repère ne règle
   que son encre. Jusqu'à 0,44 de l'ouverture — le bord haut du cadre
   est alors à plus de cent pixels sous elle —, c'est l'encre sombre de
   `is-light` ; ensuite `is-clear` seul, dont le fondu par élément part
   de cette même encre sombre et passe au crème quand l'image arrive
   sous chaque élément, piloté par `--frame-top` — voir `layout.css`,
   « l'encre de la barre pendant l'ouverture ». Le relais à 0,44 ne se
   voit pas : même encre des deux côtés. */
.process__clear{
  position:absolute;top:0;left:0;
  width:0;height:calc(100% - var(--method-dark-on, 0svh));
  visibility:hidden;pointer-events:none;
}
.process.is-live .process__light{
  position:absolute;top:0;left:0;
  width:1px;
  height:calc(var(--process-open) * .44 + var(--header-h));
  visibility:hidden;pointer-events:none;
}
/* Le repère de sortie. Il commence quand les portes sont ouvertes (0,45
   de la fermeture) — l'encre y est déjà sombre par le fondu des portes,
   le relais ne se voit pas — et dépasse le bas de la piste d'une hauteur
   de barre : une zone claire ne compte que la barre entièrement dessus,
   donc elle s'arrête une barre avant son bas, et `#method` ne commence
   la sienne qu'à son haut. Sans ce débord, 98 px de barre noire sur le
   marbre entre les deux — le même éclair que celui documenté entre le
   marbre et le procédé, dans l'autre sens. */
.process.is-live .process__light--end{
  top:calc(100% - var(--process-tail, 0svh) - 100svh - var(--process-hand) - var(--process-close) * .45);
  /* Deux hauteurs de barre de débord, pas une : `--header-h` vaut 72 px
     là où la barre en fait 98 (padding + bouton), et à une seule il
     restait 26 px de trou — mesuré, la barre repassait crème juste
     avant `#method`. Le débord tombe dans `#method`, clair aussi : deux
     zones claires qui se recouvrent ne se voient pas. */
  height:calc(100svh + var(--process-hand) + var(--process-close) * .45 + var(--process-tail, 0svh) - var(--method-dark-on, 0svh) + 2 * var(--header-h));
}
.process__tail{
  position:absolute;top:0;left:0;
  width:0;height:var(--process-tail, 0svh);
  visibility:hidden;pointer-events:none;
}
.process__probe{
  position:absolute;top:0;left:0;
  width:0;height:var(--frame-h);
  visibility:hidden;pointer-events:none;
}

/* ---------- l'étape ----------
   Une étape porte son image ET son texte : c'est ce qui la garde lisible
   sans script, où les trois se lisent l'une sous l'autre.

   Sous `is-live`, la boîte disparaît — `display:contents` — et ses deux
   enfants remontent dans le contexte d'empilement de la scène. C'est ce
   qui permet aux **trois textes de passer au-dessus des trois images**
   alors qu'ils appartiennent chacun à une étape différente : avec une
   boîte positionnée par étape, le texte de la première serait
   prisonnier de son niveau et passerait sous l'image de la troisième. */
.process.is-live .process__step{display:contents}

/* ---------- les images ----------
   Superposées au pixel. `cover`, `center`, aucune transformation : la
   moindre différence d'échelle ou de cadrage entre deux d'entre elles
   ferait sauter la sculpture au moment du balayage, et c'est
   exactement ce que la superposition sert à éviter. */
.process.is-live .process__plate{
  position:absolute;inset:0;
  overflow:hidden;
  z-index:var(--z);
}
/* LES TROIS RENDUS AUX MÊMES NIVEAUX. Livrés, ils ne les ont pas : le
   deuxième est un peu plus sombre que le premier, le troisième
   nettement (médiane 12 contre 16, 90e percentile 36 contre 49 sur
   255) — et le balayage, qui les superpose au pixel, le montrait. Le
   client a voulu la même luminosité et le même contraste sur les
   trois. Chacun est donc ramené sur le premier par un couple
   luminosité / contraste AJUSTÉ PAR LA MESURE (percentiles 10 → 99 de
   la luminance, pondérés vers les demi-tons, recherche sur une
   grille) :

     image    10   25   50   75   90   95   99   (sur 255)
     01        9   12   16   28   49   69  122   la référence
     02       11   13   17   27   47   69  127   brightness 1.05, contrast .98
     03       12   15   18   27   45   66  134   brightness 1.19, contrast .97

   LE FILTRE EST SUR LA PLAQUE, PAS SUR L'IMAGE : l'image porte la
   dérive de caméra, une animation du compositeur, et un filtre sur un
   calque qui bouge tressaute (voir le musée). La plaque, elle, est
   immobile — sa découpe suit le scroll, sa surface a la taille de
   l'écran une fois pour toutes. Les portes, copies de la 03, prennent
   ses niveaux par leur propre plaque. Sans la séquence, les plaques
   n'existent plus (`display:contents`) : le filtre passe sur l'image,
   qui n'y bouge pas. */
.process{
  --process-levels-02:brightness(1.05) contrast(.98);
  --process-levels-03:brightness(1.19) contrast(.97);
}
.process.is-live .process__step--02 .process__plate{filter:var(--process-levels-02)}
.process.is-live .process__step--03 .process__plate,
.process.is-live .process__door .process__plate{filter:var(--process-levels-03)}
.process.is-live .process__img{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  object-position:var(--fit);
  /* LA DÉRIVE DE CAMÉRA. Les trois images respirent ensemble — même
     animation, même départ, donc même position à tout instant : la
     superposition au pixel tient. Une lente respiration d'échelle et une
     dérive de quelques millièmes, en aller-retour, comme la sculpture du
     hero qui flotte sur place. L'échelle minimale garde les bords hors
     champ pendant la dérive. C'est une animation par le temps, du même
     ordre que la poussière : elle ne fait qu'exister. Elle est sur
     l'image et non sur la plaque, qui porte la découpe — le bord du
     balayage reste immobile pendant que la matière bouge dessous. */
  transform-origin:50% 60%;
  will-change:transform;
  animation:process-float 26s ease-in-out infinite alternate;
}
@keyframes process-float{
  from{transform:scale(1.045) translate3d(.4%, .5%, 0)}
  to{transform:scale(1.075) translate3d(-.6%, -.5%, 0)}
}

/* La découpe, et le sens compte. `inset()` prend ses quatre côtés dans
   l'ordre haut / droite / bas / gauche : c'est donc le **gauche** qu'on
   rétracte de 100 % à 0 pour que la zone visible naisse contre le bord
   droit et grandisse vers la gauche. Rétracter la droite ferait
   l'inverse — un balayage de la gauche vers la droite.

   La première étape n'a pas de découpe : elle est le fond sur lequel les
   deux autres se posent. */
.process.is-live .process__step--02 .process__plate,
.process.is-live .process__step--03 .process__plate{
  clip-path:inset(0 0 0 calc((1 - var(--r, 1)) * 100%));
  /* Seulement sur les deux qui en changent : la première n'a pas de
     découpe, et l'annoncer au compositeur lui coûterait une couche pour
     rien. */
  will-change:clip-path;
}
/* Les deux fenêtres de balayage, et les trois temps d'arrêt qu'elles
   laissent : 0 → 10 % la première seule, 10 → 45 % le premier balayage,
   45 → 55 % la deuxième seule, 55 → 90 % le second, 90 → 100 % la
   troisième seule. Les valeurs sont écrites en clair plutôt que passées
   en jetons : deux fenêtres ne valent pas une indirection.

   Les étapes se désignent par une classe et non par leur rang : un
   `:nth-child` se décalerait au premier élément inséré dans la scène, et
   le symptôme — un balayage qui part au mauvais moment — ne désignerait
   pas la cause. */
.process.is-live .process__step--02 .process__plate{
  --r:clamp(0, calc((var(--pp, 1) - .10) / .35), 1);
}
.process.is-live .process__step--03 .process__plate{
  --r:clamp(0, calc((var(--pp, 1) - .55) / .35), 1);
}

/* ---------- le fil du balayage ----------
   Un cheveu clair posé sur la limite, et seulement pendant qu'elle
   bouge. Il ne décore pas : il donne une arête à une frontière qui,
   sans lui, est une simple discontinuité de valeurs entre deux images
   presque noires. Il s'éteint aux deux bouts de la course — une ligne
   qui resterait à l'arrêt sur un bord serait un défaut.

   `translateX` et non `left` : la limite se déplace à chaque image, et
   une position calculée la ferait recalculer la mise en page. */
.process.is-live .process__edge{
  position:absolute;top:0;bottom:0;left:0;
  width:1px;
  z-index:var(--z);
  pointer-events:none;
  background:linear-gradient(to bottom,
    transparent, rgba(255,255,255,.26) 22%,
    rgba(255,255,255,.26) 78%, transparent);
  transform:translateX(calc((1 - var(--r, 1)) * 100vw));
  opacity:calc(min(var(--r, 0) * 12, (1 - var(--r, 0)) * 12, 1) * .7);
}
.process.is-live .process__step--02 .process__edge{
  --r:clamp(0, calc((var(--pp, 0) - .10) / .35), 1);
}
.process.is-live .process__step--03 .process__edge{
  --r:clamp(0, calc((var(--pp, 0) - .55) / .35), 1);
}

/* ---------- les textes ----------
   Dans la moitié droite, où les sculptures laissent le champ libre, et
   **au-dessus de toutes les images** : ils ne sont pas balayés avec
   elles. Ils se relaient en fondu court, décalé après le début du
   balayage — l'image change d'abord, le texte suit. L'inverse ferait
   annoncer le changement au lieu de l'accompagner.

   Deux courses par texte, jamais une seule : `--in` le fait venir,
   `--out` l'emporte. Les fenêtres de sortie et d'entrée se chevauchent
   de deux centièmes — juste assez pour qu'il y ait toujours quelque
   chose à l'écran. Mesuré sans ce chevauchement : à 0,22 les deux textes
   valent zéro, et la moitié droite du cadre reste vide le temps d'un
   battement. Au-delà, les deux blocs se liraient l'un sur l'autre. */
.process.is-live .process__caption{
  position:absolute;
  /* Décollée du bord droit : à 7 vw, « LA PERFORMANCE » finissait
     contre l'arête de l'écran. */
  right:clamp(48px, 10vw, 180px);
  /* LE HAUT, PAS LE MILIEU. Centrées sur leur propre hauteur, les trois
     légendes partaient de trois lignes différentes — un titre sur deux
     lignes, un chapô sur trois — et l'index sautait à chaque relais. Le
     haut est fixé, le bas varie : c'est ce qu'on ne voit pas. La valeur
     place la plus haute des trois à peu près au centre de l'écran. */
  top:27svh;
  z-index:5;
  width:min(38vw, 600px);
  opacity:calc(var(--in, 1) * (1 - var(--out, 0)));
  transform:translateY(calc((1 - var(--in, 1)) * 20px));
  text-shadow:0 1px 24px rgba(0,0,0,.55);
}
.process.is-live .process__step--01 .process__caption{
  /* Le premier texte n'arrive qu'une fois le cadre presque ouvert : dans
     un petit rectangle il n'aurait ni sa place ni son sens. */
  --in:clamp(0, calc((var(--pa, 1) - .84) / .12), 1);
  --out:clamp(0, calc((var(--pp, 0) - .14) / .10), 1);
}
.process.is-live .process__step--02 .process__caption{
  --in:clamp(0, calc((var(--pp, 1) - .20) / .10), 1);
  --out:clamp(0, calc((var(--pp, 0) - .58) / .10), 1);
}
.process.is-live .process__step--03 .process__caption{
  --in:clamp(0, calc((var(--pp, 1) - .64) / .10), 1);
  /* Le dernier texte part au début de la fermeture, avant les portes. */
  --out:clamp(0, calc(var(--pc, 0) / .10), 1);
}

.process__index{
  margin:0 0 var(--s-sm);
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  text-transform:uppercase;
  color:var(--accent-text);
  font-variant-numeric:tabular-nums;
}
.process__title{
  margin:0 0 var(--s-sm);
  font-family:var(--serif);
  /* En gras — 500, la graisse la plus forte du site (demandé sur
     « IDÉE », les trois titres suivent : un seul registre). */
  font-weight:500;
  /* Le corps des titres du hero (fiche, déclaration, triptyque),
     demandé : une seule échelle de titre du hero au procédé. C'était
     4,4 vw (84 px max). */
  font-size:clamp(26px, 2.2vw, 40px);
  line-height:1.02;
  letter-spacing:.012em;
  color:var(--ink);
  text-transform:uppercase;
}
.process__sub{
  margin:0;
  max-width:34ch;
  /* Les familles du sous-titre et du corps sont INVERSÉES (demandé) :
     le sous-titre en Cormorant, un cran au-dessus du corps, le corps
     en Manrope. */
  font-family:var(--serif);
  font-size:clamp(18px, 1.35vw, 23px);
  line-height:1.3;
  color:var(--ink-soft);
}
/* Le chapô et les services — le copy des anciens piliers, un état par
   métier. Le chapô a été en serif, la voix du positionnement ; il est
   en Manrope depuis l'inversion avec le sous-titre (le sous-titre porte
   le serif). Les services sont une ligne et non une colonne : sur une image, une liste à filets devenait un tableau ; une
   ligne de capitales espacées reste une légende. */
.process__body{
  margin:var(--s-sm) 0 0;
  /* Pas de largeur en `ch` : les coupes sont posées à la main dans le
     balisage, la légende borne la ligne. `pretty` reprend la main là où
     les `<br>` s'effacent, et évite l'orphelin. */
  text-wrap:pretty;
  font-family:var(--sans);
  font-size:clamp(14px, 1.05vw, 17px);
  line-height:1.55;
  letter-spacing:.005em;
  color:var(--ink-soft);
}
.process__services{
  list-style:none;
  margin:var(--s-md) 0 0;padding:0;
  /* Une seule ligne, jamais coupée (demandé). La plus longue des trois
     (669 px en 1512) dépasse la légende (575) : elle déborde à droite
     dans la marge de 10 vw et finit à 57 px du bord, la gouttière de la
     page. Sous 1240 px, là où les coupes du chapô s'effacent aussi, la
     liste reprend le droit de se replier. */
  display:flex;flex-wrap:nowrap;
  white-space:nowrap;
  width:max-content;
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  text-transform:uppercase;
  color:var(--muted);
}
.process__services li{display:flex;align-items:center}
/* Un point d'accent entre les termes, jamais après le dernier. */
.process__services li + li::before{
  content:"";
  width:3px;height:3px;
  margin-inline:var(--s-xs);
  border-radius:50%;
  background:var(--accent-text);
}

/* ---------- la trame ----------
   La même que celle du hero : six colonnes, le repère architectural du
   site, posée sur les trois images. Même jeton, même pas — c'est ce qui
   dit que ces rendus sont dans la même salle que la sculpture. Sous les
   poussières et les textes, au-dessus des images ; aucune découpe ne
   l'atteint, donc elle ne bouge pas pendant les balayages. */
.process__grid{
  position:absolute;inset:0;
  z-index:4;
  pointer-events:none;
  background-image:linear-gradient(90deg,var(--grid-line) 0 1px,transparent 1px);
  background-size:calc(100% / 6) 100%;
}

/* ---------- la poussière qui vole ----------
   Le rappel du nuage du hero, sans son contexte WebGL — la limite de
   deux tient. Trois nappes de points tirés en dégradés radiaux, chacune
   sur son propre pas de répétition et à sa propre vitesse, qui dérivent
   lentement vers le haut et la droite. Trois périodes différentes et
   impaires, pour que la répétition ne se lise pas ; trois vitesses, pour
   la profondeur — la nappe aux points les plus serrés est la plus
   rapide, comme le premier plan.

   LA BOUCLE EST EXACTE : chaque nappe est translatée d'exactement une
   période, donc la dernière image est la première, sans raccord. La
   nappe déborde d'une période en bas et à gauche — d'où elle vient —
   et pas ailleurs : c'est la plus petite surface qui reste couverte
   pendant toute la course. `transform` et non `background-position` :
   la première se compose, la seconde repeint l'écran à chaque image.

   Comme les neuf poussières, c'est une animation par le temps, du même
   ordre que les exceptions déjà admises : elle ne déplace rien, elle ne
   fait qu'exister. Elle n'apparaît qu'une fois le cadre ouvert. */
.process__dust{
  position:absolute;inset:0;
  z-index:4;
  overflow:hidden;
  pointer-events:none;
}
/* Les nappes sont PARTAGÉES avec les tableaux de la méthode
   (`.method-dust`, le tableau qui s'effrite au survol) : même pas,
   mêmes points, même dérive — c'est la même poussière, dans la même
   salle. Un réglage de nappe se fait ici et se voit aux deux. */
.process__dust i,
.method-dust i,
.panels__dust i{
  position:absolute;
  left:calc(-1 * var(--tw));top:0;
  width:calc(100% + var(--tw));
  height:calc(100% + var(--th));
  background-size:var(--tw) var(--th);
  will-change:transform;
  animation:process-dust var(--d) linear infinite;
}
@keyframes process-dust{
  to{transform:translate3d(var(--tw), calc(-1 * var(--th)), 0)}
}
/* Les trois nappes : la première serrée, vive et fine ; la troisième
   large, lente et un peu plus grosse — le point le plus près bouge le
   plus, et c'est l'inverse d'une profondeur de champ, à dessein : ce
   qu'on veut est le grain qui frémit devant, pas un ciel étoilé. */
.process__dust i:nth-child(1),
.method-dust i:nth-child(1),
.panels__dust i:nth-child(1){
  --tw:263px;--th:241px;--d:44s;
  background-image:
    radial-gradient(1px 1px at 31px 57px, rgba(255,255,255,.55) 50%, transparent 51%),
    radial-gradient(1px 1px at 142px 19px, rgba(255,255,255,.35) 50%, transparent 51%),
    radial-gradient(1px 1px at 201px 173px, rgba(255,255,255,.45) 50%, transparent 51%),
    radial-gradient(1px 1px at 88px 214px, rgba(255,255,255,.3) 50%, transparent 51%);
}
.process__dust i:nth-child(2),
.method-dust i:nth-child(2),
.panels__dust i:nth-child(2){
  --tw:353px;--th:307px;--d:76s;
  background-image:
    radial-gradient(1.5px 1.5px at 47px 121px, rgba(255,255,255,.4) 50%, transparent 51%),
    radial-gradient(1px 1px at 233px 63px, rgba(255,255,255,.5) 50%, transparent 51%),
    radial-gradient(1px 1px at 311px 251px, rgba(255,255,255,.3) 50%, transparent 51%);
}
.process__dust i:nth-child(3),
.method-dust i:nth-child(3),
.panels__dust i:nth-child(3){
  --tw:431px;--th:389px;--d:118s;
  background-image:
    radial-gradient(2px 2px at 109px 341px, rgba(255,255,255,.28) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 377px 97px, rgba(255,255,255,.34) 50%, transparent 51%),
    radial-gradient(1px 1px at 259px 203px, rgba(255,255,255,.42) 50%, transparent 51%);
}

/* ---------- les particules ----------
   Neuf points, et c'est un chiffre, pas une approximation : au-delà on
   quitte l'atmosphère pour la météo. Elles vivent dans la scène et non
   dans les étapes — au-dessus des trois images, sous les trois textes,
   et aucune découpe ne les atteint. Elles ne bougent pas pendant les
   balayages : c'est ce qui donne la profondeur.

   C'est la seule animation par le temps de cette section, et elle est du
   même ordre que les deux exceptions déjà admises sur le site : elle ne
   déplace rien, elle ne fait qu'exister. */
.process__particles{
  position:absolute;inset:0;
  z-index:4;
  pointer-events:none;
}
/* Trame, poussières et repère n'existent pas dans le petit carré : ils
   s'allument sur la fin de l'ouverture, quand l'image est presque
   plein cadre — et s'éteignent au début de la fermeture, avant que les
   portes ne bougent. */
.process.is-live .process__grid,
.process.is-live .process__dust,
.process.is-live .process__particles,
.process.is-live .process__meter{
  opacity:calc(clamp(0, calc((var(--pa, 1) - .86) / .14), 1)
             * (1 - clamp(0, calc(var(--pc, 0) / .12), 1)));
}
.process__particles span,
.panels__particles span{
  position:absolute;
  left:var(--x);top:var(--y);
  width:var(--s);height:var(--s);
  border-radius:50%;
  background:var(--ink);
  opacity:var(--o);
  animation:process-drift var(--d) ease-in-out infinite alternate;
}
@keyframes process-drift{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(1.6vw,-3.2svh,0)}
}

/* ---------- le repère de progression ----------
   Une ligne, deux bornes. Le remplissage est un `scaleX` sur la même
   progression que les découpes : il suit donc le scroll dans les deux
   sens sans qu'aucune valeur ne soit calculée deux fois. (Passé en
   bleu `--accent` un instant à la demande du client, puis remis.) */
.process__meter{
  position:absolute;
  left:clamp(20px, 4vw, 64px);
  right:clamp(20px, 4vw, 64px);
  bottom:clamp(28px, 4svh, 48px);
  z-index:6;
  display:grid;
  grid-template-columns:auto 1fr auto;
  align-items:center;
  gap:var(--s-sm);
  pointer-events:none;
  font-size:var(--fs-label);
  letter-spacing:var(--tr-label);
  color:var(--muted);
  font-variant-numeric:tabular-nums;
}
.process__meter-rail{
  position:relative;
  height:1px;
  background:var(--line-soft);
  overflow:hidden;
}
.process__meter-rail i{
  position:absolute;inset:0;
  background:var(--ink-soft);
  transform:scaleX(var(--pp, 0));
  transform-origin:left center;
}

/* ---------- le repli ----------
   Pas de script, et mouvement réduit. `ui/process.js` ne pose `is-live`
   ni sans JavaScript ni sous `prefers-reduced-motion` : les trois étapes
   redeviennent trois blocs dans le flux, image puis texte, sans une
   superposition ni une découpe. Rien à annuler — tout ce qui précède est
   posé **sous** `is-live`, jamais sur la classe nue. */
.process:not(.is-live) .process__step{
  position:relative;
  min-height:100svh;
  display:grid;
  align-content:center;
}
.process:not(.is-live) .process__img{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  object-position:var(--fit);
}
.process:not(.is-live) .process__caption{
  position:relative;
  justify-self:end;
  width:min(46ch, 86vw);
  padding-inline:var(--gutter);
  text-shadow:0 1px 24px rgba(0,0,0,.55);
}
.process:not(.is-live) .process__edge,
.process:not(.is-live) .process__meter,
.process:not(.is-live) .process__grid,
.process:not(.is-live) .process__dust,
.process:not(.is-live) .process__particles,
.process:not(.is-live) .process__light{display:none}
/* Le cadre n'a de sens qu'ouvert au scroll : sans script il s'efface et
   ses enfants remontent dans la scène, exactement comme avant lui. */
.process:not(.is-live) .process__frame,
.process:not(.is-live) .process__plate{display:contents}
.process:not(.is-live) .process__door{display:none}
.process:not(.is-live) .process__step--02 .process__img{filter:var(--process-levels-02)}
.process:not(.is-live) .process__step--03 .process__img{filter:var(--process-levels-03)}

@media (max-width:1240px){
  /* Sous cette largeur la légende n'a plus la place des lignes posées à
     la main : le moteur coupe, `text-wrap:pretty` équilibre. */
  .process__body br{display:none}
  .process__services{flex-wrap:wrap;white-space:normal;width:auto;gap:var(--s-2xs) 0}
}
@media (max-width:1000px){
  .process__title{font-size:clamp(26px, 3.6vw, 36px)}
  .process.is-live .process__caption{width:min(44vw, 420px)}
  /* LE CADRAGE SE DÉPLACE, PARCE QUE LE SUJET N'EST PAS AU MILIEU. Les
     trois rendus sont en 16:9 et la pièce occupe leur tiers gauche — de
     16 à 41 % de la largeur, anneau compris ; la droite est le vide qui
     porte le texte. Sur un écran plus étroit que l'image, `cover` rogne
     les côtés à parts égales et emporte la sculpture avant le vide.

     La valeur se calcule, elle ne se cherche pas : la fraction visible
     vaut `(L/H) / 1,778`, et le point de cadrage `p` place la fenêtre en
     `[p(1-f), p(1-f)+f]`. Il suffit de la faire commencer à 16 %.

     ELLE EST LA MÊME POUR LES TROIS, et ce n'est pas un détail : deux
     cadrages différents feraient sauter la sculpture au balayage. */
  .process{
    --fit:30% center;
    /* Un rectangle un peu plus large sur tablette : à 28 vw il ferait
       moins de 300 px, et l'image y perdrait sa sculpture. */
    --frame-w:clamp(280px, 36vw, 420px);
  }
}
@media (max-width:760px){
  /* Le balayage reste — il n'a aucun coût sur un écran étroit, et c'est
     le même geste. Ce qui change est la place du texte, en partie basse
     pour que la sculpture reste entière au-dessus, et la course, plus
     courte : trois écrans d'attente sur un téléphone est long. */
  .process.is-live{height:calc(var(--process-track, 710svh) + var(--process-tail, 0svh))}
  /* Le rectangle prend toute la largeur moins les gouttières, et un
     bon tiers de l'écran en hauteur : assez pour que la sculpture y
     soit lisible, assez peu pour qu'il reste un aperçu. */
  .process{
    --frame-w:calc(100vw - 2 * var(--gutter));
    --frame-gap:clamp(48px, 6vh, 72px);
  }
  .process.is-live .process__caption{
    right:auto;left:0;
    /* Même règle que sur grand écran, le haut fixé : la plus haute des
       trois s'arrête au-dessus du repère de progression. */
    top:56svh;
    width:auto;
    padding-inline:var(--gutter);
    transform:translateY(calc((1 - var(--in, 1)) * 20px));
  }
  .process__title{font-size:clamp(26px, 7.4vw, 38px)}
  /* Sur téléphone la boîte des mains reprend sa hauteur de contenu :
     le canvas est au-dessus de la signature, pas derrière. Posé ici,
     après les règles de base, et non dans le bloc mobile des mains, qui
     les précède dans la feuille et perdait la cascade. */
  .process.is-live .process__hands .hands{
    height:auto;
    transform:translateY(calc(var(--up-e) * -42svh));
  }
  /* Sur téléphone les deux termes ne glissent pas avec les mains (le
     bloc mobile des mains neutralise le glissement) : ils s'effacent
     donc au relais, sinon ils restaient au-dessus du titre transformé. */
  .process.is-live .process__hands .signature__equation{
    opacity:calc(1 - clamp(0, calc(var(--pd, 0) / .2), 1));
  }
  /* Le chapô est retiré sur téléphone : posé en partie basse, il
     montait jusqu'au socle de la sculpture. Les services restent — ils
     sont l'information, le chapô est la voix. */
  .process__body{display:none}
  .process__services{margin-top:var(--s-xs)}
  /* 375 × 812 : la fenêtre tombe à 26 % de la largeur de l'image. Pour
     qu'elle commence à 16 % — le bras levé —, il faut 20 %. Un cadrage
     centré ne montre même plus la sculpture : rien que les colonnes du
     fond. */
  .process{--fit:20% center}
}

/* =========================================================
   LA MÉTHODE — quatre tableaux qui prennent vie
   Quatre vidéos portrait sur le marbre, chacune avec son cartouche
   dessous. Pas une card : un tableau accroché, et sa légende. Rien
   autour — ni fond, ni angle, ni ombre —, un filet fin entre l'image et
   le texte, et l'image occupe toute la colonne.

   Un réglage est un jeton, sur la section, pour être changé sans
   chercher : le filtre noir et blanc. Le balayage bleu, lui, n'a pas
   de réglage : il suit `--on`, la progression de `ui/flow.js`.
   ========================================================= */
.method{
  /* Le noir et blanc, uniforme sur les quatre : les vidéos sont
     livrées presque monochromes, mais pas exactement — c'est le filtre
     qui les accorde. Réglable ici, jamais cuit dans le fichier. */
  --method-filter:grayscale(100%) contrast(1.12) brightness(.95);

  /* Le balayage bleu au scroll est en réserve (plus bas, avec son
     raisonnement dans `docs/DECISIONS.md`).

     Le balayage a d'abord été un survol — au curseur, `--mx` publié
     par `ui/method.js` — puis un passage au bleu au scroll, déposé à
     son tour quand la section est passée à la séquence en pile ; le
     survol est la poussière (`.method-dust`, plus bas), et le module
     garde le publieur en réserve. */

  /* EN RÉSERVE — le tableau se réchauffait au survol (sépia, par le
     filtre). Déposé pour la zone bleue : deux révélations de couleur
     sur le même tableau, c'est une de trop. Pour le remettre : le
     `saturate(1)` inerte se rajoute au bout de `--method-filter`, même
     liste de fonctions dans le même ordre pour que le navigateur
     interpole terme à terme ; `saturate(2.2)` parce que les rushes sont
     sépia, presque gris (saturation mesurée 0,07 à 0,17 sur 1) — à 3
     ça vire à l'ocre, à 1,6 ça ne se lit pas sur les deux premiers.
     --method-filter-live:grayscale(0%) contrast(1.04) brightness(1) saturate(2.2);
     --method-hover-dur:var(--dur-slow);
     .method-media{transition:filter var(--method-hover-dur) var(--ease)}
     @media (hover:hover){
       .method-card:hover .method-media{filter:var(--method-filter-live)}
     } */

  /* LA SECTION SE GLISSE SOUS LE TITRE. Le relais du procédé laisse
     « MÉTHODE RIVMONT. » en haut de son cadre collé ; la section remonte
     sous la fin de la piste de cette valeur, passe au-dessus du panneau
     (`z-index`), et ses tableaux arrivent sous le titre pendant que la
     scène tient encore. Sans le chevauchement il restait plus d'un
     demi-écran de marbre entre le titre et les images. Calculé : le
     titre remonte de 34 svh et finit vers 16 svh du haut ; à un écran
     entier de chevauchement, la section entre à 0,23 du relais — au bas
     de l'écran, loin du mot qui se transforme —, les tableaux sont à
     31 svh quand le titre commence à sortir (0,86) et à 13 svh quand il
     est sorti (1) : c'est leur place collée. Le procédé se décolle et la
     méthode se colle au même pixel de scroll — les deux scènes se
     relaient, comme le hero et la pièce. */
  --method-overlap:100svh;
  margin-top:calc(-1 * var(--method-overlap));
  z-index:calc(var(--z-sticky) + 1);

  /* LA SÉQUENCE. La scène se colle et le scroll fait tout : `ui/method.js`
     publie `--mp`, de 0 à 1, sur une course de `--method-seq` — trois
     écrans, quatre avec la scène. Le scroll n'est jamais bloqué, c'est
     la composition qui tient. Sans script, `--mp` vaut 1 : la grille,
     l'état final. */
  --method-seq:300svh;

  /* LA PILE : la place de la grande carte, à gauche. Bornée par la
     hauteur et non la largeur — une carte 3:4 de 50 vw ne tient pas
     dans 100 vh (786 × 1048 px en 1512 × 860) —, et gardée en 3:4 pour
     que ce soit **la même image, au même cadrage** qui grandit et
     rapetisse : c'est ce qui fait un zoom arrière et non un recadrage.
     Centrée dans la moitié gauche du conteneur. */
  --stack-h:74svh;
  --stack-w:calc(var(--stack-h) * 3 / 4);
}
/* La piste : la scène, plus la course. La scène est collée tant que la
   course dure — un `sticky` colle exactement sur la hauteur de sa piste
   moins la sienne, et c'est ce que le module mesure. LA COURSE EST UN
   ESPACEUR EN FLUX, PAS UN `padding` : un `sticky` est borné par la
   boîte de contenu de son conteneur, marges intérieures exclues — avec
   un `padding-bottom` la piste avait bien sa hauteur mais aucune place
   pour coller, et la scène défilait avec la page. Le `::after` s'ajoute
   au contenu, quelle que soit la hauteur de la scène — 100 svh ici,
   celle de la liste sous 1000 px. */
.method__track::after{content:"";display:block;height:var(--method-seq)}
.method__stage{
  position:sticky;top:0;
  height:100svh;
  display:grid;align-content:center;
}
/* Le repère de tout ce qui se mesure : la pile, le panneau, les cartes
   se lisent en `offset*` depuis ce cadre. */
.method__frame{position:relative}
.method__stack{
  position:absolute;
  visibility:hidden;pointer-events:none;
  width:var(--stack-w);height:var(--stack-h);
  left:calc(25% - var(--stack-w) / 2 + var(--gutter) / 2);
  top:calc(50% - var(--stack-h) / 2);
}
.method-cards{
  display:grid;
  grid-template-columns:repeat(4, minmax(0, 1fr));
  gap:var(--s-lg) var(--s-sm);
}
.method-card{
  display:grid;
  gap:var(--s-2xs);
  align-content:start;
}
/* Le cadre réserve exactement le 3:4 avant que quoi que ce soit n'ait
   chargé : l'affiche est la première image de la vidéo, à la même
   taille — aucun changement de dimension, aucun flash, aucun gris. */
.method-card__media{
  position:relative;
  margin:0 0 var(--s-xs);
  aspect-ratio:3 / 4;
  overflow:hidden;
  background:transparent;
  /* Le voile se mélange à la vidéo, et à rien d'autre : sans
     l'isolation, `mix-blend-mode` irait chercher le marbre derrière. */
  isolation:isolate;
}
.method-media{
  display:block;
  width:100%;height:100%;
  object-fit:cover;
  filter:var(--method-filter);
  /* Pas de survol sur la vidéo elle-même. Un zoom de 2 % a été posé
     puis retiré — rééchantillonnée à chaque image, elle scintillait —,
     et rien de géométrique ne doit y revenir : un tableau ne bouge pas
     quand on s'approche. Le survol est le voile ci-dessous. */
}
/* ---------- EN RÉSERVE — le balayage bleu ----------
   Le voile d'accent qui passait les tableaux au bleu avec `--on`,
   le fil sur l'arête. Déposé quand la section est passée à la séquence
   en pile ; tout le raisonnement est dans `docs/DECISIONS.md`.

   Le voile d'accent par-dessus la vidéo, masqué par une arête verticale
   à `--on` × 100 % — les deux arrêts du dégradé au même endroit, c'est
   ce qui fait l'arête franche. Le cadre est isolé pour que le blend ne
   touche que la vidéo, jamais le marbre derrière. * /
.method-card__media::after{
  content:"";
  position:absolute;inset:0;
  background:var(--accent);
  mix-blend-mode:color;
  pointer-events:none;
  -webkit-mask-image:linear-gradient(to right,
    #000 calc(var(--on, 1) * 100%), transparent 0);
  mask-image:linear-gradient(to right,
    #000 calc(var(--on, 1) * 100%), transparent 0);
}
/ * Le fil sur la limite, comme celui du balayage du procédé : 1 px,
   éteint aux deux bouts de sa hauteur, et éteint aux deux bouts de sa
   course — une ligne qui resterait posée sur un bord serait un défaut.
   En accent, sans blend : il est posé, pas mélangé. `translateX` et non
   `left`, comme `.process__edge` : l'arête bouge à chaque image, et une
   position calculée ferait recalculer la mise en page. L'élément fait
   toute la largeur, son fil est sa première colonne, et le débord est
   coupé par le cadre — à `--on` = 1 il est entièrement sorti. * /
.method-card__media::before{
  content:"";
  position:absolute;inset:0;
  z-index:1;
  pointer-events:none;
  background:linear-gradient(to right, var(--accent) 1px, transparent 1px);
  -webkit-mask-image:linear-gradient(to bottom,
    transparent, #000 22%, #000 78%, transparent);
  mask-image:linear-gradient(to bottom,
    transparent, #000 22%, #000 78%, transparent);
  transform:translateX(calc(var(--on, 1) * 100%));
  opacity:min(var(--on, 1) * 12, (1 - var(--on, 1)) * 12, 1);
}

   ---------- fin de la réserve ---------- */
/* ---------- le tableau s'effrite ----------
   Le seul survol des tableaux : la poussière du procédé — ses trois
   nappes, au même jeton, voir `.process__dust` — se lève devant le
   tableau sous la main, et retombe quand elle part. C'est la
   signature du site sans son WebGL : la sculpture du hero et les mains
   s'effritent au curseur, les tableaux aussi. Rien de géométrique sur
   la vidéo — un zoom scintille —, une opacité qui monte, c'est tout.
   Par-dessus le voile bleu : la poussière est devant la pierre, quelle
   que soit sa couleur.

   Trois nappes par tableau, douze en tout, et elles ne coûtent rien
   hors survol : `visibility:hidden` les sort de la composition — à la
   fin du fondu, transition discrète retardée de la durée —, et
   l'animation est en pause tant que la main n'est pas là. Sous
   `prefers-reduced-motion`, `base.css` fige l'animation : la poussière
   monte, immobile. Au pointeur seulement — au tactile, il n'y a pas de
   survol. Sur la carte entière, cartouche compris : passer de l'image
   à sa légende ne fait pas retomber la poussière. */
.method-dust{
  position:absolute;inset:0;
  z-index:2;
  overflow:hidden;
  pointer-events:none;
  opacity:0;
  visibility:hidden;
  /* La poussière s'évanouit en montant : un fondu sur le tiers haut du
     cadre, pour que les points quittent la pierre et disparaissent au
     lieu de sortir par le bord — c'est ce qui fait un effritement et
     non un ciel qui défile. */
  -webkit-mask-image:linear-gradient(to bottom, transparent, #000 34%);
  mask-image:linear-gradient(to bottom, transparent, #000 34%);
  transition:opacity var(--dur-slow) var(--ease),
             visibility 0s var(--dur-slow);
}
.method-dust i{animation-play-state:paused}
/* Les nappes du procédé sont calibrées pour un plein écran et une
   atmosphère : des points d'un pixel à 44 s par période — 6 px/s —,
   invisibles sur un cadre de 330 px où la vidéo bouge. Vu en 1:1 :
   trois ou quatre points, rien ne s'effrite. Ici c'est un effritement,
   pas une atmosphère, et tout est réglé pour se lire à trente
   centimètres : même pas de répétition et même boucle exacte, mais
   cinq fois plus vite (la poussière monte, on la voit monter), quatre
   fois plus de points, un à deux crans plus gros, et deux populations
   comme le nuage du hero — les claires se détachent du fond sombre,
   les sombres creusent les zones claires. La montée est verticale —
   ses propres images-clés, une période de tuile vers le haut, boucle
   exacte — : la poussière quitte la pierre, elle ne traverse pas. Les
   tuiles (`--tw`, `--th`) restent celles du procédé, elles portent les
   positions. Positions posées à la main, sans alignement, pour
   qu'aucune rangée ne se lise. */
.method-dust i{
  left:0;width:100%;
  animation-name:method-dust;
}
@keyframes method-dust{
  to{transform:translate3d(0, calc(-1 * var(--th)), 0)}
}
.method-dust i:nth-child(1){
  --d:9s;
  background-image:
    radial-gradient(2px 2px at 31px 57px, rgba(255,255,255,.85) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 142px 19px, rgba(255,255,255,.6) 50%, transparent 51%),
    radial-gradient(2px 2px at 201px 173px, rgba(255,255,255,.75) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 88px 214px, rgba(10,10,12,.7) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 17px 131px, rgba(255,255,255,.7) 50%, transparent 51%),
    radial-gradient(2px 2px at 113px 96px, rgba(10,10,12,.75) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 176px 41px, rgba(255,255,255,.8) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 236px 118px, rgba(10,10,12,.6) 50%, transparent 51%),
    radial-gradient(2px 2px at 59px 9px, rgba(255,255,255,.5) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 151px 227px, rgba(255,255,255,.75) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 247px 199px, rgba(10,10,12,.7) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 72px 163px, rgba(255,255,255,.65) 50%, transparent 51%),
    radial-gradient(2px 2px at 218px 79px, rgba(10,10,12,.65) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 37px 231px, rgba(255,255,255,.8) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 189px 139px, rgba(10,10,12,.6) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 127px 66px, rgba(255,255,255,.85) 50%, transparent 51%);
}
.method-dust i:nth-child(2){
  --d:14s;
  background-image:
    radial-gradient(3px 3px at 47px 121px, rgba(255,255,255,.7) 50%, transparent 51%),
    radial-gradient(2px 2px at 233px 63px, rgba(255,255,255,.8) 50%, transparent 51%),
    radial-gradient(2px 2px at 311px 251px, rgba(10,10,12,.7) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 96px 281px, rgba(255,255,255,.6) 50%, transparent 51%),
    radial-gradient(2.5px 2.5px at 171px 174px, rgba(10,10,12,.75) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 289px 137px, rgba(255,255,255,.55) 50%, transparent 51%),
    radial-gradient(3px 3px at 23px 29px, rgba(10,10,12,.6) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 134px 41px, rgba(255,255,255,.7) 50%, transparent 51%),
    radial-gradient(2px 2px at 337px 19px, rgba(255,255,255,.65) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 203px 293px, rgba(10,10,12,.7) 50%, transparent 51%),
    radial-gradient(2px 2px at 68px 208px, rgba(255,255,255,.6) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 259px 197px, rgba(10,10,12,.65) 50%, transparent 51%);
}
.method-dust i:nth-child(3){
  --d:20s;
  background-image:
    radial-gradient(3.5px 3.5px at 109px 341px, rgba(255,255,255,.5) 50%, transparent 51%),
    radial-gradient(3px 3px at 377px 97px, rgba(10,10,12,.6) 50%, transparent 51%),
    radial-gradient(2px 2px at 259px 203px, rgba(255,255,255,.7) 50%, transparent 51%),
    radial-gradient(3px 3px at 41px 61px, rgba(255,255,255,.55) 50%, transparent 51%),
    radial-gradient(2px 2px at 193px 27px, rgba(10,10,12,.65) 50%, transparent 51%),
    radial-gradient(3.5px 3.5px at 331px 289px, rgba(10,10,12,.5) 50%, transparent 51%),
    radial-gradient(2px 2px at 157px 143px, rgba(255,255,255,.65) 50%, transparent 51%),
    radial-gradient(3px 3px at 407px 221px, rgba(255,255,255,.5) 50%, transparent 51%),
    radial-gradient(2px 2px at 73px 247px, rgba(10,10,12,.6) 50%, transparent 51%),
    radial-gradient(3px 3px at 283px 371px, rgba(255,255,255,.55) 50%, transparent 51%);
}
@media (hover:hover){
  .method-card:hover .method-dust{
    opacity:1;
    visibility:visible;
    transition:opacity var(--dur-slow) var(--ease), visibility 0s;
  }
  .method-card:hover .method-dust i{animation-play-state:running}
}
/* ---------- le filet qui se trace ----------
   Chaque cartouche porte son filet, et le filet se trace au scroll —
   `--on` par tableau, dérivé de `--mp` à la fin de la séquence (voir
   « les cartouches » plus haut ; c'était `ui/flow.js` avant la
   séquence), l'un après l'autre. Rien par le temps : le scroll tient
   tout, et sans script les valeurs de repli valent 1, filets tracés,
   noms à l'encre. */
.method-card__index{
  position:relative;
  margin:0;
  padding-top:var(--s-xs);
  border-top:1px solid var(--line-soft);
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--accent-text);
  font-variant-numeric:tabular-nums;
}
.method-card__index::before{
  content:"";
  position:absolute;left:0;top:-1px;
  width:100%;height:1px;
  background:var(--accent-text);
  transform:scaleX(var(--on, 1));
  transform-origin:left center;
}
.method-card__name{
  margin:0;
  font-family:var(--serif);font-weight:300;
  font-size:var(--fs-h3);line-height:var(--lh-heading);
  /* Capitales espacées, comme les titres des trois états du procédé :
     c'est la même échelle qui continue. */
  text-transform:uppercase;letter-spacing:.02em;
  /* À l'encre, et ils y restent : seul le filet se trace en bleu. Une
     version passait le nom au bleu avec lui ; le client a préféré le
     nom noir. */
  color:var(--ink);
}
.method-card__text{
  margin:0;
  max-width:28ch;
  font-size:var(--fs-body);line-height:var(--lh-body);
  color:var(--ink-soft);
}

/* =========================================================
   LA SÉQUENCE — quatre tableaux découverts un par un
   Une progression, `--mp`, publiée par `ui/method.js` sur la section
   pendant que la scène est collée. Le CSS en tire tout : les fenêtres
   des quatre arrivées, la pile, la sortie du texte, la recomposition.
   Cinq courbes, toutes en `3t² − 2t³`, rien par le temps — aucune
   transition ici, le scroll tient chaque image.

     0    → .15   COMPRENDRE arrive (carte 1, texte 1)
     .15  → .35   IMAGINER se dépose devant (carte 1 recule)
     .35  → .55   CONSTRUIRE
     .55  → .75   ACCÉLÉRER, un peu plus présente
     .75  → .85   le texte part, ligne par ligne
     .80  → .93   la pile se défait, les quatre rejoignent leur place
                  dans la grille — le texte est parti avant
     .90  → 1     les cartouches s'allument, l'un après l'autre
     .93  → 1     pause, puis la page repart

   LA GRILLE EST L'ÉTAT FINAL ET L'ÉTAT DE REPOS. Les cartes ne sont
   jamais déplacées dans le flux : elles restent à leur place de grille
   et c'est un `transform` qui les emmène dans la pile — translation et
   échelle calculées par `ui/method.js` (`--tx`, `--ty`, `--s`), de la
   place de chacune vers la boîte `.method__stack`. La recomposition
   n'est donc qu'un retour à l'identité : `X × (1 − out)`. Rien n'est
   dupliqué, ce sont les mêmes vidéos qui voyagent, au même cadrage.

   Sans script ou sous `prefers-reduced-motion`, `.is-method` n'est pas
   posée : la grille est là, telle quelle, et le panneau se lit avant
   elle, en quatre temps. Les valeurs de repli valent 1.
   ========================================================= */
.method.is-method{
  --mp:1;
  /* Les fenêtres : `--eN` la part brute, `--iN` adoucie. */
  --e1:clamp(0, calc(var(--mp) / .15), 1);
  --e2:clamp(0, calc((var(--mp) - .15) / .20), 1);
  --e3:clamp(0, calc((var(--mp) - .35) / .20), 1);
  --e4:clamp(0, calc((var(--mp) - .55) / .20), 1);
  --e5:clamp(0, calc((var(--mp) - .80) / .13), 1);
  --i1:calc(var(--e1) * var(--e1) * (3 - 2 * var(--e1)));
  --i2:calc(var(--e2) * var(--e2) * (3 - 2 * var(--e2)));
  --i3:calc(var(--e3) * var(--e3) * (3 - 2 * var(--e3)));
  --i4:calc(var(--e4) * var(--e4) * (3 - 2 * var(--e4)));
  --i5:calc(var(--e5) * var(--e5) * (3 - 2 * var(--e5)));
  /* Ce qui reste de la pile : 1 pendant la séquence, 0 à la fin. */
  --u:calc(1 - var(--i5));
}

/* ---------- les cartes ----------
   Une seule règle de transformation, composée de trois états :
   l'ARRIVÉE (la carte vient d'un peu plus bas et d'un peu à droite,
   de 0,94 à 1, d'invisible à pleine, un degré au plus), la PILE (à
   chaque carte qui se dépose devant, celle-ci recule d'un cran :
   −8 % d'échelle, un peu vers le haut et la gauche, −30 % d'opacité,
   un demi-pixel de flou), et le RETOUR (tout tend vers l'identité avec
   `--u`). `--depth` est le nombre de cartes arrivées après celle-ci —
   une somme de courbes, continue.

   L'échelle se fait depuis le coin haut gauche (`transform-origin`,
   c'est ce qui rend `--tx`/`--ty` triviaux) ; le recentrage
   `sw × (1 − pq) / 2` la fait paraître autour du centre de la pile. */
.method.is-method .method-card{
  position:relative;
  z-index:var(--z);
  transform-origin:0 0;
  will-change:transform;
  --p:calc(1 - .08 * var(--depth));
  --q:calc(var(--boost) - .06 * (1 - var(--in)));
  --pq:calc(var(--p) * var(--q));
  --S:calc(var(--s, 1) * var(--pq));
  --X:calc(var(--tx, 0px) + var(--sw, 0px) * (1 - var(--pq)) / 2
          - 1.6vw * var(--depth) + var(--ax) * (1 - var(--in)));
  --Y:calc(var(--ty, 0px) + var(--sh, 0px) * (1 - var(--pq)) / 2
          - 2.4vh * var(--depth) + var(--ay) * (1 - var(--in)));
  --O:calc(var(--in) * (1 - .3 * var(--depth)));
  transform:translate(calc(var(--X) * var(--u)), calc(var(--Y) * var(--u)))
            scale(calc(1 + (var(--S) - 1) * var(--u)))
            rotate(calc(var(--rot) * (1 - var(--in)) * var(--u)));
  opacity:calc(1 + (var(--O) - 1) * var(--u));
  filter:blur(calc(var(--depth) * .5px * var(--u)));
}
.method.is-method .method-card:nth-child(1){
  --z:1;--in:var(--i1);--depth:calc(var(--i2) + var(--i3) + var(--i4));
  --ax:0vw;--ay:5vh;--rot:0deg;--boost:1;
}
.method.is-method .method-card:nth-child(2){
  --z:2;--in:var(--i2);--depth:calc(var(--i3) + var(--i4));
  --ax:3vw;--ay:8vh;--rot:1deg;--boost:1;
}
.method.is-method .method-card:nth-child(3){
  --z:3;--in:var(--i3);--depth:var(--i4);
  --ax:3vw;--ay:8vh;--rot:-.8deg;--boost:1;
}
/* La quatrième est l'aboutissement : un rien plus grande que la pile. */
.method.is-method .method-card:nth-child(4){
  --z:4;--in:var(--i4);--depth:0;
  --ax:3vw;--ay:8vh;--rot:.9deg;--boost:1.04;
}

/* ---------- les cartouches ----------
   Cachés pendant la séquence, ils s'allument une fois les cartes en
   place — « légèrement après », l'un après l'autre : `--on` est dérivé
   de `--mp`, et c'est le même `--on` qui trace le filet. Sans script,
   1 : tout est là. */
.method.is-method .method-card:nth-child(1){--on:clamp(0, calc((var(--mp) - .90) / .05), 1)}
.method.is-method .method-card:nth-child(2){--on:clamp(0, calc((var(--mp) - .915) / .05), 1)}
.method.is-method .method-card:nth-child(3){--on:clamp(0, calc((var(--mp) - .93) / .05), 1)}
.method.is-method .method-card:nth-child(4){--on:clamp(0, calc((var(--mp) - .945) / .05), 1)}
.method.is-method .method-card__index,
.method.is-method .method-card__name,
.method.is-method .method-card__text{opacity:var(--on, 1)}

/* ---------- le panneau ----------
   Le texte du temps en cours, dans la moitié droite, à hauteur de la
   pile. Quatre temps empilés sur la même case ; chaque ligne est un
   masque vertical : elle entre par le bas (`--k`) un peu après sa
   carte, ligne après ligne, et sort par le haut (`--gone`) quand la
   carte suivante commence à se déposer — une sortie ne rejoue pas
   l'entrée. Le dernier sort au début de la recomposition : c'est
   toute la colonne qui s'efface.

   Sans `.is-method`, le panneau est un bloc dans le flux, avant la
   grille : les quatre temps se lisent à la suite. */
.method__panel{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--s-xl) var(--s-lg);
  margin-bottom:var(--s-2xl);
}
/* Sans la séquence — sans script, ou sous `prefers-reduced-motion` —
   la scène n'est plus une fenêtre de 100 svh centrée : le panneau et
   la grille y tiendraient à l'étroit. Elle prend sa hauteur de contenu,
   sous la barre, et ne colle plus à rien. */
.method:not(.is-method) .method__stage{
  position:static;height:auto;
  display:block;
  padding-block:calc(var(--header-h) + var(--s-lg)) var(--section-y);
}
.method:not(.is-method) .method__track::after{display:none}
.method.is-method .method__panel{
  position:absolute;
  z-index:0;
  left:calc(50% + var(--s-lg));right:var(--gutter);
  top:50%;transform:translateY(-50%);
  margin:0;gap:0;
}
.method.is-method .method__step{grid-area:1 / 1}
.method__count{
  margin:0 0 var(--s-sm);
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--accent-text);
  font-variant-numeric:tabular-nums;
}
.method__name{
  margin:0 0 var(--s-md);
  font-family:var(--serif);font-weight:300;
  font-size:var(--fs-h2);line-height:var(--lh-heading);
  text-transform:uppercase;letter-spacing:.02em;
  color:var(--ink);
}
.method__lead{
  margin:0 0 var(--s-md);
  max-width:24ch;
  font-family:var(--serif);font-weight:300;
  font-size:var(--fs-h3);line-height:1.2;
  color:var(--ink);
}
.method__services{
  margin:0;
  font-size:var(--fs-label);letter-spacing:var(--tr-wide);
  text-transform:uppercase;color:var(--ink-soft);
}
/* Les fenêtres de chaque temps : `--a` le début de sa carte, `--b` le
   début de la suivante. */
.method.is-method .method__step:nth-child(1){--a:0;--b:.15}
.method.is-method .method__step:nth-child(2){--a:.15;--b:.35}
.method.is-method .method__step:nth-child(3){--a:.35;--b:.55}
.method.is-method .method__step:nth-child(4){--a:.55;--b:.75}
/* Ligne par ligne : chaque masque a son propre décalage `--d`. */
.method.is-method .method__count .mask{--d:0}
.method.is-method .method__name .mask{--d:.015}
.method.is-method .method__lead .mask{--d:.03}
.method.is-method .method__services .mask{--d:.045}
.method.is-method .method__step .mask{
  --kr:clamp(0, calc((var(--mp) - var(--a) - .03 - var(--d)) / .10), 1);
  --k:calc(var(--kr) * var(--kr) * (3 - 2 * var(--kr)));
  --gr:clamp(0, calc((var(--mp) - var(--b) - var(--d)) / .06), 1);
  --gone:calc(var(--gr) * var(--gr) * (3 - 2 * var(--gr)));
}
.method.is-method .method__step .mask > i{
  transform:translateY(calc((1 - var(--k)) * 112% - var(--gone) * 112%));
}
@media (max-width:1000px){
  /* Deux rangées — ou quatre — ne tiennent pas dans un écran : la
     scène prend sa hauteur de contenu et reste collée le temps de la
     séquence — un `sticky` plus haut que l'écran colle par son haut,
     et c'est ce qu'il faut : la pile est en haut, le texte dessous, le
     reste de la liste sous le pli, jusqu'à ce que la course finisse et
     que la page reparte. La pile est plus petite (44 svh) pour laisser
     la place au texte. */
  .method__stage{height:auto;min-height:100svh;display:block;padding-block:var(--s-md) var(--section-y)}
  .method{--stack-h:42svh;--stack-top:calc(var(--header-h) + var(--s-xs))}
  /* Sous la barre, pas dessous : la pile commence à sa hauteur. */
  .method__stack{left:calc(50% - var(--stack-w) / 2);top:var(--stack-top)}
  .method.is-method .method__panel{
    left:var(--gutter);right:var(--gutter);
    top:calc(var(--stack-top) + var(--stack-h) + var(--s-lg));
    transform:none;
  }
  .method-cards{grid-template-columns:1fr 1fr;gap:var(--s-xl) var(--s-md)}
  .method:not(.is-method) .method__stage{padding-top:calc(var(--header-h) + var(--s-md))}
}
@media (max-width:760px){
  .method__panel{grid-template-columns:1fr}
  /* Une colonne, un scroll vertical : pas de carrousel. La vidéo reste
     en 3:4 et prend la largeur de la gouttière à la gouttière. Le
     chevauchement suit la boîte des mains, qui remonte de 42 svh. */
  .method{--method-overlap:76svh}
  .method-cards{grid-template-columns:1fr;gap:var(--s-xl)}
  .method-card__text{max-width:none}
}


/* =========================================================
   LA MÉTHODE — quatre panneaux verticaux
   Une composition collée le temps de la séquence, sous le titre des
   mains qui reste. Quatre panneaux qui se partagent la largeur :
   quatre traits au départ, d'où les bandes se déplient, puis chaque
   temps s'ouvre en POUSSANT les autres — la frontière glisse, les
   panneaux restent attachés, aucun ne grandit par-dessus les autres.
   Un seul objet graphique, pas quatre composants. Puis l'objet entier
   part vers la gauche et la page repart.

   Une progression, `--mp`, publiée par `ui/panels.js` (0 → 1 sur la
   piste). HOMOGÈNE : quatre temps identiques, de même longueur, de
   même largeur ouverte, de même structure (la largeur bouge sur 72 %
   de la phase, puis tient 28 % le temps de lire), et rien par le temps
   — les largeurs suivent le scroll en ligne droite, seuls les masques
   ont une courbe :

     0    → .07   LES PORTES S'OUVRENT : chaque bande se déplie depuis
                  sa frontière vers la droite, l'une après l'autre —
                  et rien n'est visible avant : la rangée n'apparaît
                  que par ce balayage, à sa place
     .07  → .25   COMPRENDRE s'ouvre (.70), les trois autres à .10
     .25  → .43   01 se ferme, IMAGINER s'ouvre
     .43  → .61   02 se ferme, CONSTRUIRE s'ouvre
     .61  → .79   03 se ferme, ACCÉLÉRER s'ouvre

   Ces seuils sont ceux de l'ancienne course de 350 svh ; la course
   vaut aujourd'hui .79 de celle-là (`--method-seq`, `tokens.css`) et
   `--mpx` = `--mp` × .79 les garde lisibles. Le dernier temps — la
   carte 04 qui grandissait jusqu'à l'écran, puis le CTA en rideau — a
   sa propre course maintenant, `--mq` sur `--museum-seq` : LE MUSÉE,
   plus bas.

   LA LARGEUR EST UN `flex-grow`. Chaque panneau porte ses quatre
   cibles (`--t1` … `--t4`, en fractions) et une chaîne de mélanges les
   relie phase après phase : les phases sont successives, donc chaque
   mélange part de la valeur finale du précédent, exactement. C'est un
   layout par image, et il est petit : quatre boîtes.

   L'IMAGE A UNE TAILLE FIXE (`--panels-img`) : la hauteur de la rangée,
   3:4 exact — le panneau n'est qu'une fenêtre qui la recadre sur les
   côtés, jamais un zoom. Un panneau OUVERT est crème avec sa colonne de
   texte à gauche et l'image collée à droite de la colonne ; fermé, il
   n'est que son image. Le texte a une largeur fixe, entre quand la
   colonne le tient et sort avant qu'elle ne se resserre — jamais
   compressé.

   Sans `.is-panels` — sans script, `prefers-reduced-motion`, ou sous
   1000 px — : les quatre temps en plein écran l'un sous l'autre, image
   puis texte, la section en marbre dans le flux. Repli à 1.
   ========================================================= */
.panels{
  /* Marbre, comme `#method` (les jetons sont sur l'id). Le chevauchement
     avec la fin du procédé : le procédé se décolle et la scène se colle
     au même pixel. */
  --method-overlap:100svh;
  z-index:calc(var(--z-sticky) + 1);
  /* Le noir et blanc des vidéos, comme en pile. */
  --method-filter:grayscale(100%) contrast(1.12) brightness(.95);
  /* LES NIVEAUX DU MUSÉE SONT RAMENÉS À CEUX DE LA VIDÉO, avant ce
     noir et blanc commun. Le rendu du musée est plus sombre ET plus
     contrasté que la vidéo 04 sur le tableau qu'ils partagent — noirs
     bouchés, blancs hauts, là où la vidéo est plate —, et le raccord
     se voyait aux tons. Une luminosité seule (1,28 puis 1,36) égalait
     les moyennes, pas les ombres. Le couple est AJUSTÉ PAR LA MESURE :
     sur la même zone du tableau en unités de figures (ancrages et
     référence de la calibration, `ui/panels.js`), les percentiles
     5/10/25/50/75/90/95 de la luminance après toute la chaîne — ceux
     de l'image fixe valent 56/68/90/121/150/167/178 ; avec
     `brightness(1.26) contrast(.66)` le musée donne
     59/70/88/118/152/169/179, ±3 partout (recherche sur une grille,
     simulée sur un canvas). Posé en premier dans la chaîne, avant le
     `contrast` commun. LE PRIX : le contraste sous 1 grise la salle —
     à .66, sa médiane passait de 16 à 50 sur 255, et le client a
     trouvé l'image « sans niveaux ». LE JUSTE MILIEU, demandé : à
     `contrast(.83)` et `brightness(1.28)`, le tableau donne
     44/57/81/118/162/185/196 — la médiane de la vidéo, des ombres et
     des hautes lumières à mi-chemin entre elle et le rendu d'origine —
     et la salle une médiane de 32. C'est sur ce gris sombre que le CTA
     s'écrit et que le pied de page, noir, arrive. */
  --museum-levels:brightness(1.28) contrast(.83);

  /* La course, hors scène — les panneaux puis le musée : le jeton
     partagé avec la queue de la piste du procédé (`tokens.css`). */
  --panels-seq:var(--method-track);

  /* LE TITRE EST CELUI DES MAINS, ET IL NE BOUGE PAS. La scène des
     mains reste collée sous celle-ci pendant toute la séquence, et
     celle-ci est transparente : on voit le titre à travers, à sa place
     — mesurée au pixel du relais, en 1512 × 860 : boîte du masque à
     105 px du haut (12,2 svh), 65 px de haut. La rangée commence
     dessous. */
  --panels-title-y:12.2svh;
  --panels-title-h:7.6svh;

  /* La place de la rangée : sous le titre, avec de l'air — une
     composition qui prenait tout le cadre pesait trop. */
  --panels-top:calc(var(--panels-title-y) + var(--panels-title-h) + var(--s-md));
  --panels-bottom:var(--s-2xl);
  --panels-x:calc(var(--gutter) * 2.5);

  /* L'image : hauteur de la rangée, largeur 3:4, taille fixe. */
  --panels-h:calc(100svh - var(--panels-top) - var(--panels-bottom));
  --panels-img:calc(var(--panels-h) * .75);
  /* L'air entre la colonne de texte et l'image d'un panneau ouvert. */
  --panels-gap:var(--s-md);

  /* La colonne de texte d'un panneau ouvert, et le corps du nom — entre
     `--fs-h3` et `--fs-h2`, pour que « COMPRENDRE » tienne sur une
     ligne dans la colonne. */
  --panels-col:28vw;
  /* Le corps des titres du hero et du procédé (demandé) ; c'était
     3,1 vw. Le titre des mains, réduit, suit : `SHRINK` = 2,2 / 4,2. */
  --panels-name:clamp(26px, 2.2vw, 40px);
  --mp:1;
  --mq:1;
}
.panels__track::after{content:"";display:block;height:var(--panels-seq)}
.panels__stage{
  position:sticky;top:0;
  height:100svh;
  overflow:hidden;
}
/* La sonde de la course du musée : `ui/panels.js` lit sa hauteur pour
   séparer `--mp` (les panneaux) de `--mq` (le musée) — le jeton est en
   svh, seule une boîte le donne en pixels. */
.panels__museum-probe,
.panels__hold-probe{
  position:absolute;top:0;left:0;
  width:0;height:var(--museum-seq);
  visibility:hidden;pointer-events:none;
}
.panels__hold-probe{height:var(--museum-hold)}
/* Le repère clair du header : toute la piste, moins la fin où le cadre
   noir a recouvert la barre. */
.panels__light{
  position:absolute;top:0;left:0;
  width:0;height:calc(100% - var(--method-dark-on, 0svh));
  visibility:hidden;pointer-events:none;
}
/* AU-DESSUS DE 1000 PX, LA SECTION EST TRANSPARENTE ET RECOUVRE TOUTE
   LA QUEUE DU PROCÉDÉ : sa piste (scène + course) remonte exactement
   de sa hauteur — net, elle n'ajoute rien à la page —, la scène des
   mains reste collée dessous avec son titre, et les cartes montent
   sous lui. `#method` porte le marbre par son id : il faut l'id pour
   l'enlever.

   LA QUEUE DU PROCÉDÉ EST LA PISTE MOINS L'AVANCE (`--panels-early`,
   `tokens.css`) : la piste du procédé raccourcit d'autant, et la
   section des panneaux — qui remonte toujours de sa propre hauteur —
   se colle donc cette avance avant la fin du relais, dans son creux.
   Le relais, lui, ne bouge pas : `ui/process.js` soustrait la queue
   (sonde `.process__tail`) de sa course, et sa fin tombe au même
   pixel. Les deux scènes se décollent toujours ensemble. */
/* LE CHEVAUCHEMENT N'EXISTE QUE QUAND LE PROCÉDÉ EST VIVANT — `is-live`,
   posée par `ui/process.js`. Avant que les modules ne s'exécutent (le
   premier rendu, quelques centaines de millisecondes ; plusieurs
   secondes sur un réseau lent), la piste du procédé n'a pas sa queue,
   et une marge négative de 100 svh + la course des panneaux remontait
   la méthode de ~5 500 px : le marbre et l'affiche du panneau 01
   recouvraient le hero au premier rendu — mesuré, CLS 0,90 et un
   flash de marbre à 1,5 s. Sans script, la méthode suit le procédé
   dans le flux, sans chevauchement. */
.process.is-live + .panels{margin-top:calc(-1 * var(--method-overlap))}
@media (min-width:1001px){
  .process,.panels{--method-dark-on:var(--method-dark)}
  .process{--process-tail:calc(var(--method-track) - var(--panels-early))}
  .process.is-live + .panels{margin-top:calc(-1 * (100svh + var(--panels-seq)))}
  #method.panels.is-panels{background:transparent}
}
/* La rangée : toute la largeur utile, sous le titre. Ses bords sont des
   jetons (`--rt`, `--rb`, `--rx`) : le cadre du CTA part d'eux. */
.panels{
  --rt:var(--panels-top);
  --rb:var(--panels-bottom);
  --rx:var(--panels-x);
}
.panels__row{
  position:absolute;
  left:var(--rx);right:var(--rx);
  top:var(--rt);bottom:var(--rb);
  margin:0;padding:0;list-style:none;
  display:flex;
}
.panel{
  position:relative;
  /* × 10 : quand la somme des `flex-grow` est sous 1, les items ne
     prennent que cette fraction de l'espace libre — et 04 hors du flux,
     les trois autres ne sommaient plus qu'à .30. Les proportions sont
     les mêmes à toute échelle. */
  flex-grow:calc(var(--w, 1) * 10);flex-shrink:1;flex-basis:0;
  min-width:0;
  overflow:hidden;
  background:var(--bg);
  /* La frontière : un cheveu, jamais un angle — sur les quatre. */
  border-left:1px solid var(--line);
}
.panel__media{
  position:absolute;
  top:0;bottom:0;
  /* La fenêtre : tout le panneau tant qu'il est plus étroit que
     l'image ; au-delà, elle a la largeur de l'image et se place juste
     après la colonne de texte — et n'en bouge plus quand le panneau
     grandit encore (la carte 04, à la fin, jusqu'à l'écran). Des
     pourcentages du panneau lui-même, donc exacts. */
  --ml:max(0px, min(calc(100% - var(--panels-img) - var(--panels-gap)),
                    calc(var(--panels-col) + var(--panels-gap))));
  left:var(--ml);
  width:min(calc(100% - var(--ml)), var(--panels-img));
  margin:0;
  overflow:hidden;
  z-index:1;
}
.panel__media .method-media{
  /* Taille fixe, centrée dans la fenêtre : recadrée, jamais
     rééchelonnée. `max-width:none` contre le reset de `base.css`, qui
     la rétrécissait dans les bandes. */
  position:absolute;
  top:0;left:50%;
  height:100%;width:var(--panels-img);
  max-width:none;
  transform:translateX(-50%);
  object-fit:cover;
  filter:var(--method-filter);
}
.panel__num{
  position:absolute;z-index:2;
  left:var(--s-sm);top:var(--s-sm);
  margin:0;
  font-size:var(--fs-micro);letter-spacing:var(--tr-label);
  text-transform:uppercase;
  /* Bleu, à la demande du client : c'est un texte sur le marbre, donc
     `--accent-text`, que `#method` cale sur `--accent-deep` (6,7:1) —
     jamais `--accent` en texte. */
  color:var(--accent-text);
  font-variant-numeric:tabular-nums;
  opacity:.3;
}
.panel__text{
  position:absolute;z-index:2;
  left:var(--s-sm);
  top:50%;transform:translateY(-50%);
  /* Largeur FIXE : un texte qui se recompose pendant que la colonne se
     resserre, c'est un texte compressé. Il entre quand la colonne est
     assez large pour lui et sort avant qu'elle ne le soit plus. */
  width:calc(var(--panels-col) - var(--s-sm) * 2);
  margin:0;
}
.panel__name{
  margin:0 0 var(--s-sm);
  /* En gras — 500, la graisse la plus forte du site —, comme les
     titres du procédé (demandé). */
  font-family:var(--serif);font-weight:500;
  font-size:var(--panels-name);line-height:var(--lh-heading);
  text-transform:uppercase;letter-spacing:.02em;
  color:var(--ink);
}
.panel__lead{
  margin:0 0 var(--s-sm);
  font-family:var(--serif);font-weight:300;
  /* Un cran sous `--fs-h3` : « Votre marché, vos clients, » doit tenir
     sur une ligne dans la colonne (415 px à 38 px pour 369 de large ;
     coupe posée à la main). */
  font-size:clamp(22px,2.2vw,34px);line-height:1.2;
  color:var(--ink);
}
/* Le corps, entre le chapô et la liste : le corps et l'encre des
   sous-textes de la fiche du hero. */
.panel__body{
  margin:0 0 var(--s-sm);
  font-size:clamp(13px,.95vw,15px);
  line-height:1.55;
  color:var(--ink-soft);
}
.panel__services{
  margin:0;
  font-size:var(--fs-label);letter-spacing:var(--tr-wide);
  text-transform:uppercase;color:var(--ink-soft);
}
/* EN RÉSERVE — le mot géant de CONSTRUIRE, derrière l'image et devant
   le crème, plus large que le panneau. Retiré à la demande du client ;
   le balisage est en commentaire dans `index.html`. */
.panel__giant{
  position:absolute;z-index:0;
  left:-.06em;bottom:-.12em;
  margin:0;
  font-family:var(--serif);font-weight:300;
  font-size:clamp(120px, 21vw, 340px);line-height:1;
  text-transform:uppercase;letter-spacing:.04em;
  white-space:nowrap;
  color:var(--ink);
  opacity:0;
  pointer-events:none;
}

/* ---------- la séquence ---------- */
.panels.is-panels{
  --mpx:calc(var(--mp) * .79);
  --e0:clamp(0, calc(var(--mpx) / .07), 1);
  --e1:clamp(0, calc((var(--mpx) - .07) / .18), 1);
  --e2:clamp(0, calc((var(--mpx) - .25) / .18), 1);
  --e3:clamp(0, calc((var(--mpx) - .43) / .18), 1);
  --e4:clamp(0, calc((var(--mpx) - .61) / .18), 1);
  --p0:calc(var(--e0) * var(--e0) * (3 - 2 * var(--e0)));
  /* Chaque temps a la même structure : la largeur bouge sur les 72 %
     premiers de sa phase, puis tient 28 % — le temps de lire, le texte
     entré. Identique pour les quatre. */
  --m1:clamp(0, calc(var(--e1) / .72), 1);
  --m2:clamp(0, calc(var(--e2) / .72), 1);
  --m3:clamp(0, calc(var(--e3) / .72), 1);
  --m4:clamp(0, calc(var(--e4) / .72), 1);
  /* EN RÉSERVE — LA FIN D'AVANT, EN DEUX TEMPS SUR `--e5` : LA CARTE
     04 — son cadre, son image, son texte — GRANDISSAIT PAR-DESSUS TOUT
     jusqu'aux bords de l'écran (`--g`, 0 → .6), puis le CTA montait
     dedans comme un rideau (`--r`, .6 → 1). Remplacée par le musée ;
     `--g` reste lu par la géométrie de la carte 04, à zéro.
  --e5:clamp(0, calc((var(--mp) - .79) / .21), 1);
  --g:clamp(0, calc(var(--e5) / .6), 1);
  --r:clamp(0, calc((var(--e5) - .6) / .4), 1); */
  --g:0;
  /* La part de 04, recalculée ici parce que la rangée en a besoin :
     04 est hors du flux (absolu), la rangée lui réserve sa place. */
  --w4a:calc(.25 + (.10 - .25) * var(--m1));
  --w4b:calc(var(--w4a) + (.10 - var(--w4a)) * var(--m2));
  --w4c:calc(var(--w4b) + (.10 - var(--w4b)) * var(--m3));
  --w4:calc(var(--w4c) + (.70 - var(--w4c)) * var(--m4));
}
/* LE PANNEAU 04 EST ABSOLU, À GÉOMÉTRIE IDENTIQUE : la rangée lui
   réserve sa part à droite (`padding-right`), les trois autres se
   partagent le reste en flex comme avant — et lui, hors du flux,
   pouvait s'étendre à la fin sans que rien d'autre ne bouge : ses
   quatre côtés allaient de ceux de sa case aux bords de l'écran
   (`--g`), les bords de la rangée en négatif. `--g` vaut zéro depuis
   le musée — la carte ne grandit plus, c'est l'image du musée qui
   s'ouvre par-dessus ; la géométrie est gardée telle quelle. */
/* En largeur de rangée, pas en pourcentage : un `padding` en % se
   résout sur le bloc conteneur — la scène —, pas sur la rangée. */
.panels.is-panels .panels__row{padding-right:calc(var(--w4) * (100vw - 2 * var(--rx)))}
.panels.is-panels .panel:nth-child(4){
  position:absolute;
  top:calc(-1 * var(--rt) * var(--g));
  bottom:calc(-1 * var(--rb) * var(--g));
  right:calc(-1 * var(--rx) * var(--g));
  left:calc((1 - var(--w4)) * 100% * (1 - var(--g)) - var(--rx) * var(--g));
  z-index:2;
}
/* LES PORTES S'OUVRENT. Chaque bande se déplie depuis sa frontière
   vers la droite : un masque sur son image, pas une échelle, la 01
   d'abord et les autres décalées d'un souffle ; le numéro s'allume
   derrière. C'est le symétrique de la fin, où l'objet entier s'en va.
   Une entrée, donc adoucie — les largeurs, elles, restent linéaires.

   ET RIEN NE MONTE. La scène entre par le bas pendant le dernier écran
   du relais (le chevauchement, plus haut), et la rangée y était déjà
   dessinée — quatre traits, les frontières, qu'on voyait monter avant
   qu'elles ne se déplient. Retiré au retour du client : chaque
   panneau, son cheveu compris, n'existe qu'à partir du premier pixel
   de son dépliage (`--u0` > 0). Les bandes apparaissent par le
   balayage, à leur place, et par rien d'autre. */
.panels.is-panels .panel:nth-child(1){--d0:0}
.panels.is-panels .panel:nth-child(2){--d0:.15}
.panels.is-panels .panel:nth-child(3){--d0:.30}
.panels.is-panels .panel:nth-child(4){--d0:.45}
.panels.is-panels .panel{
  --u0r:clamp(0, calc((var(--e0) - var(--d0)) / .55), 1);
  --u0:calc(var(--u0r) * var(--u0r) * (3 - 2 * var(--u0r)));
  opacity:clamp(0, calc(var(--u0) * 1000), 1);
}
.panels.is-panels .panel__media{
  clip-path:inset(0 calc((1 - var(--u0)) * 100%) 0 0);
}
/* La chaîne des largeurs, LINÉAIRE phase par phase : adoucies, elles
   s'arrêtaient à chaque frontière de phase et repartaient — au scroll
   continu, un rebond. Quatre temps identiques. */
.panels.is-panels .panel{
  --a:calc(.25 + (var(--t1) - .25) * var(--m1));
  --b:calc(var(--a) + (var(--t2) - var(--a)) * var(--m2));
  --c:calc(var(--b) + (var(--t3) - var(--b)) * var(--m3));
  --w:calc(var(--c) + (var(--t4) - var(--c)) * var(--m4));
  /* L'ouverture, déduite de la largeur : le numéro s'allume avec. */
  --o:clamp(0, calc((var(--w) - .32) / .22), 1);
}
.panels.is-panels .panel:nth-child(1){--t1:.70;--t2:.10;--t3:.10;--t4:.10;--open:var(--e1);--close:var(--e2)}
.panels.is-panels .panel:nth-child(2){--t1:.10;--t2:.70;--t3:.10;--t4:.10;--open:var(--e2);--close:var(--e3)}
.panels.is-panels .panel:nth-child(3){--t1:.10;--t2:.10;--t3:.70;--t4:.10;--open:var(--e3);--close:var(--e4)}
.panels.is-panels .panel:nth-child(4){--t1:.10;--t2:.10;--t3:.10;--t4:.70;--open:var(--e4);--close:0}

/* Le numéro : discret, plein quand le panneau est ouvert — et rien
   avant que sa porte ne soit ouverte. */
.panels.is-panels .panel__num{opacity:calc((.3 + .7 * var(--o)) * var(--u0))}

/* Le texte : entre par le bas de son masque une fois le panneau bien
   ouvert, ligne après ligne ; sort par le haut dès que le panneau
   commence à se fermer — avant qu'il ne soit étroit, jamais compressé.
   `--open` et `--close` sont les phases LINÉAIRES, comme les largeurs ;
   les masques, eux, gardent leur courbe. La largeur est à .61 à .58 de
   la phase — la colonne tient le texte (372 px pour 363) — : le texte
   entre de .58 à .70, ligne après ligne, et se lit sur la tenue ; à
   .12 de la fermeture le panneau est encore à .60. */
.panels.is-panels .panel__name .mask{--dl:0}
.panels.is-panels .panel__lead .mask{--dl:.02}
.panels.is-panels .panel__body .mask{--dl:.04}
.panels.is-panels .panel__services .mask{--dl:.06}
.panels.is-panels .panel__text .mask{
  --kr:clamp(0, calc((var(--open) - .58 - var(--dl)) / .12), 1);
  --k:calc(var(--kr) * var(--kr) * (3 - 2 * var(--kr)));
  --gr:clamp(0, calc((var(--close) - var(--dl) * .5) / .12), 1);
  --gone:calc(var(--gr) * var(--gr) * (3 - 2 * var(--gr)));
}
.panels.is-panels .panel__text .mask > i{
  transform:translateY(calc((1 - var(--k)) * 112% - var(--gone) * 112%));
}

/* EN RÉSERVE — LA FIN D'AVANT : LE CTA DANS LA CARTE 04. `.panels__final`
   était une couche noire qui remplissait la carte — la dernière
   section, centrée —, cachée sous son bord bas tant que la carte
   grandissait, puis qui montait dedans comme un rideau (`--r`) une
   fois la carte plein écran. Une première version faisait monter puis
   grandir un rectangle noir découpé à la place de la carte : « un
   panneau noir qui apparaît, pas la carte qui grandit » — le client
   avait raison, c'est la carte qui doit grandir. Remplacée par le
   musée, ci-dessous ; le balisage du CTA a déménagé dans
   `.panels__museum`.
.panels.is-panels .panels__final{
  position:absolute;inset:0;
  z-index:3;
  min-height:0;padding:0;
  clip-path:inset(calc((1 - var(--r)) * 100%) 0 0 0);
} */

/* =========================================================
   LA MÉTHODE — LE MUSÉE
   La carte 04 était un gros plan : le tableau qu'elle montre est
   exposé dans un immense musée sombre, et la caméra recule pour le
   découvrir. Un seul mouvement, tenu par le scroll — jamais une carte
   qui s'étire, jamais un fondu entre deux images différentes.

   Une progression, `--mq`, publiée par `ui/panels.js` sur la section
   (0 → 1 sur `--museum-seq`, à 0 tant que `--mp` n'est pas à 1) et sur
   la racine (`--method-mq` : le titre des mains et la barre la lisent).
   Et une géométrie, mesurée hors défilement et publiée en pixels sur
   la section (voir le module) : la fenêtre de l'image de la carte 04
   (`--mu-t/r/b/l`, ses quatre bords depuis ceux de l'écran), le point
   d'ancrage et la course de l'image fixe (`--st-*`) et du musée
   (`--mu-*`). Les phases :

     0    → .18   RIEN NE BOUGE : le titre des mains, les trois autres
                  panneaux, le texte de la carte 04 restent tels quels —
                  c'est le musée qui, en s'ouvrant, recouvre toute la
                  scène. (Un fondu de l'interface sur 0 → .15 a été
                  essayé et retiré à la demande du client : en réserve.)
     .06  → .18   LA VIDÉO SE FIGE : son image fixe (la dernière frame)
                  se fond dessus, la boucle s'arrête sans à-coup ; le
                  module met la vidéo en pause à .18
     .18  → .40   LA CAMÉRA POUSSE sur le tableau : l'image fixe grandit
                  autour de son point d'ancrage — les figures — jusqu'à
                  ce que le tableau remplisse la fenêtre, et le point
                  d'ancrage rejoint le centre de la fenêtre
     .38  → .42   LE RACCORD : le musée, découpé à la fenêtre et cadré
                  sur les mêmes figures à la même taille, prend la
                  place de l'image fixe — 4 % de la course, les deux
                  images au repos et alignées
     .42  → 1     LA CAMÉRA RECULE : l'image du musée revient à sa
                  couverture (échelle en log, un travelling, pas un zoom
                  CSS), ses figures glissent de la fenêtre à leur place
                  finale, pendant que la fenêtre s'ouvre — à partir de
                  .51, jusqu'aux bords de l'écran à .855 — : les bords
                  du tableau, le cadre, le mur, l'architecture, les
                  statues, les visiteurs, le sol
     .88  → 1     LE CTA s'écrit dans le noir de droite ; la trame du
                  hero, ses nappes et ses poussières s'allument
     puis          LA PAUSE (`--museum-hold`) : `--mq` reste à 1, la
                  scène reste collée le temps de lire, et se décolle
                  sur le pied de page

   Le travelling avant n'était pas dans le brief : la vidéo 04 n'est pas
   un gros plan, c'est un plan large — la foule devant l'œuvre —, et
   deux images ne se raccordent invisiblement que là où elles montrent
   la même chose : le tableau. D'où la poussée d'abord. Une vidéo qui
   finirait déjà sur le tableau demanderait `PUSH` à 1 dans le module,
   rien d'autre.

   Les seuils de `--mq` sont écrits ici ET dans `ui/panels.js` (la
   pause de la vidéo, à .18 ; le texte cliquable, à .88) : se changent
   par paires. Les variables de cette séquence sont préfixées `--m-` :
   la feuille est plate, et `--a`, `--b`, `--c` sont déjà la chaîne des
   largeurs des panneaux — une collision mesurée, l'interface ne
   s'effaçait pas.
   ========================================================= */
.panels.is-panels{
  /* EN RÉSERVE — l'interface qui s'effaçait (0 → .15) :
  --m-ua:clamp(0, calc(var(--mq) / .15), 1);
  --m-a:calc(var(--m-ua) * var(--m-ua) * (3 - 2 * var(--m-ua))); */
  /* La vidéo se fige. */
  --m-fz:clamp(0, calc((var(--mq) - .06) / .12), 1);
  /* La poussée : une entrée et une sortie douces — la caméra part du
     repos et s'y arrête, le temps du raccord. */
  --m-pu:clamp(0, calc((var(--mq) - .18) / .22), 1);
  --m-push:calc(var(--m-pu) * var(--m-pu) * (3 - 2 * var(--m-pu)));
  /* Le raccord. */
  --m-sw:clamp(0, calc((var(--mq) - .38) / .04), 1);
  /* Le recul ; et la fenêtre, qui ne s'ouvre qu'à .15 du recul — d'abord
     le tableau respire dans sa fenêtre et ses bords apparaissent, puis
     la fenêtre s'ouvre, jusqu'aux bords de l'écran aux trois quarts.
     Mesuré : le champ visible (largeur de la fenêtre sur l'échelle de
     l'image) va de 440 à 3800 px de l'image, et cette courbe le fait
     croître en log à peu près régulièrement — une fenêtre ouverte dès
     le départ montrait le cadre, l'anneau et les statues avant le
     tiers du recul. */
  --m-dv:clamp(0, calc((var(--mq) - .42) / .58), 1);
  --m-dolly:calc(var(--m-dv) * var(--m-dv) * (3 - 2 * var(--m-dv)));
  --m-wv:clamp(0, calc((var(--m-dv) - .15) / .60), 1);
  --m-win:calc(var(--m-wv) * var(--m-wv) * (3 - 2 * var(--m-wv)));
  /* Le texte. */
  --m-tx:clamp(0, calc((var(--mq) - .88) / .12), 1);
}
/* EN RÉSERVE — L'INTERFACE S'EFFAÇAIT, et remontait d'un souffle : les
   trois autres panneaux, le numéro et le texte de la carte 04 (et le
   titre des mains, dans sa scène). Retiré à la demande du client :
   rien ne bouge, le musée recouvre tout en s'ouvrant.
.panels.is-panels .panel:not(:nth-child(4)){
  opacity:calc(1 - var(--m-a));
  transform:translateY(calc(var(--m-a) * -1.5svh));
}
.panels.is-panels .panel:nth-child(4) .panel__num{
  opacity:calc((.3 + .7 * var(--o)) * var(--u0) * (1 - var(--m-a)));
}
.panels.is-panels .panel:nth-child(4) .panel__text{
  opacity:calc(1 - var(--m-a));
} */
/* L'IMAGE FIXE : la même boîte que la vidéo, sans transformation de
   centrage — la sienne est la poussée. Elle apparaît sur la vidéo en
   douceur (`--m-fz`), puis grandit autour de son ancrage (`--st-ox/oy`,
   les figures) jusqu'à `--st-s0`, pendant que l'ancrage rejoint le
   centre de la fenêtre (`--st-dx/dy`). Échelle en log : `pow()`, la
   vitesse apparente est constante. Même filtre que la vidéo — le
   raccord compare deux images passées par le même filtre. */
.panel__still{display:none}
.panels.is-panels .panel:nth-child(4) .panel__still{
  display:block;
  position:absolute;
  top:0;left:calc(50% - var(--panels-img) / 2);
  height:100%;width:var(--panels-img);
  max-width:none;
  object-fit:cover;
  filter:var(--method-filter);
  opacity:var(--m-fz);
  transform-origin:var(--st-ox, 50%) var(--st-oy, 50%);
  transform:translate3d(calc(var(--st-dx, 0px) * var(--m-push)),
                        calc(var(--st-dy, 0px) * var(--m-push)), 0)
            scale(pow(var(--st-s0, 1), var(--m-push)));
  will-change:transform;
  z-index:1;
}
/* LE MUSÉE : une couche plein cadre au-dessus de tout, découpée à la
   fenêtre de l'image de la carte 04 (`--mu-t/r/b/l`, mesurés) et qui
   s'ouvre jusqu'aux bords (`--m-win`). Dedans, l'image en couverture :
   au raccord, transformée pour que ses figures soient à la taille et à
   la place de celles de l'image fixe (`--mu-s0` autour de `--mu-ox/oy`,
   décalées de `--mu-dx/dy`) ; à la fin, telle quelle. Entre les deux,
   le recul : l'échelle en log, le décalage en ligne droite. Repli
   sans mesure : l'état final. */
.panels.is-panels .panels__museum{
  display:block;
  position:absolute;inset:0;
  z-index:4;
  overflow:hidden;
  clip-path:inset(calc(var(--mu-t, 0px) * (1 - var(--m-win)))
                  calc(var(--mu-r, 0px) * (1 - var(--m-win)))
                  calc(var(--mu-b, 0px) * (1 - var(--m-win)))
                  calc(var(--mu-l, 0px) * (1 - var(--m-win))));
  opacity:var(--m-sw);
  pointer-events:none;
}
/* LE FLOTTEMENT : la dérive de caméra des trois images du procédé —
   même geste, même durée (`process-float`) —, sur un conteneur à part,
   parce que l'image porte déjà le recul au scroll et qu'une animation
   écraserait sa transformation.

   UNE ANIMATION DE `transform` PAR IMAGES-CLÉS, JOUÉE PAR LE
   COMPOSITEUR, ET RIEN D'AUTRE. Une première version pondérait
   l'amplitude par `--m-tx` en animant des propriétés enregistrées
   (`@property`) lues dans un `calc` : c'est un `transform` recalculé
   sur le thread principal à chaque image, que Chrome ne reconnaît pas
   comme une animation — il choisit une échelle de rastérisation à
   chaque image et re-rastérise le calque, et les bords de l'image
   clignotaient une fois le musée en place. Le compositeur, lui,
   rastérise une fois et déplace la texture.

   D'où : l'animation ne commence qu'à `is-afloat` — posée par le
   module à .88, quand le musée est en place, retirée sous .40, quand
   il est invisible sous le raccord — et PART DE L'IDENTITÉ : rien ne
   saute quand elle s'ajoute, et au raccord (à la descente) l'image ne
   flotte pas encore, les deux images s'y superposent au pixel. Les
   bords restent hors champ tout du long : la dérive est proportionnelle
   à l'échelle (à 1,03 et −.6 %, le bord gauche est à −31 px, le bas à
   866 pour 860).

   LE FILTRE N'EST NI SUR L'IMAGE NI SUR LE CALQUE QUI S'ANIME : sur
   `.panels__museum-film`, un parent immobile qui ne contient que la
   caméra. Un filtre crée une surface de rendu, et une surface qui
   bouge — sur l'image sous la caméra, comme au début, ou sur la caméra
   elle-même, comme ensuite — voit sa taille recalculée à chaque image
   d'après sa transformation à l'écran, arrondie au pixel : pendant une
   dérive lente, elle tressaute d'un pixel d'une image à l'autre — le
   flottement « en saccade » vu par le client. Sur un parent immobile,
   la surface a la taille de l'écran une fois pour toutes ; la caméra
   y est composée à chaque image, en sous-pixel, et la surface est
   filtrée ensuite — une matrice de couleur, un passage. */
.panels.is-panels .panels__museum-film{
  position:absolute;inset:0;
  filter:var(--museum-levels) var(--method-filter);
}
.panels.is-panels .panels__museum-cam{
  position:absolute;inset:0;
  transform-origin:50% 60%;
  will-change:transform;
}
.panels.is-panels.is-afloat .panels__museum-cam{
  animation:museum-float 26s ease-in-out infinite alternate;
}
@keyframes museum-float{
  from{transform:scale(1) translate3d(0, 0, 0)}
  to{transform:scale(1.03) translate3d(-.6%, -.5%, 0)}
}
/* LE RECUL, UNE SEULE FOIS : la transformation de l'image est une
   variable du conteneur, lue par l'image et par la vignette — les deux
   couches bougent ensemble, à la lettre. `--mu-*` sont publiés sur
   `.panels__museum` par `ui/panels.js`. */
.panels.is-panels .panels__museum{
  --mu-transform:translate3d(calc(var(--mu-dx, 0px) * (1 - var(--m-dolly))),
                             calc(var(--mu-dy, 0px) * (1 - var(--m-dolly))), 0)
                 scale(pow(var(--mu-s0, 1), calc(1 - var(--m-dolly))));
}
.panels.is-panels .panels__museum-img{
  position:absolute;inset:0;
  width:100%;height:100%;
  max-width:none;
  object-fit:cover;
  transform-origin:var(--mu-ox, 50%) var(--mu-oy, 50%);
  transform:var(--mu-transform);
  /* Le recul est tenu par le scroll, sur le thread principal : sans
     `will-change`, Chrome re-rastérise l'image à chaque changement
     d'échelle, et les tuiles à la nouvelle échelle n'arrivent pas
     toujours à temps — l'image saute d'une netteté à l'autre. Avec,
     l'échelle de raster est fixée à la première image visible, celle
     du raccord (`--mu-s0`, la plus grande) : les suivantes n'en sont
     qu'une réduction. Comme `.panel__still`. */
  will-change:transform;
}
/* LA VIGNETTE : les bords de la salle s'assombrissent autour du
   tableau — demande du client. Centrée sur l'ancrage du tableau
   (`--mu-ox/oy`, mesuré), SOLIDAIRE DE L'IMAGE : même origine, même
   transformation, elle suit le recul comme si elle était peinte dans
   l'image. TRANSPARENTE SUR LE TABLEAU — le cœur du dégradé (18 % de
   ses rayons, soit ~17 % de la largeur autour de l'ancrage, le tableau
   en fait ~12 %) ne touche pas aux niveaux qu'on vient de caler sur la
   vidéo, ni au raccord, qui ne montre que lui. Les rayons sont en part
   de la boîte (95 % × 110 %) : le coin le plus loin de l'ancrage, en
   bas à droite, arrive au bout du dégradé ; celui d'en haut à gauche,
   tout près, reste presque clair — c'est le tableau qui est le centre,
   pas l'écran. Une première force (.3 aux deux tiers, .55 au bout) ne
   se voyait pas sur une salle déjà sombre ; .55 / .9 non plus assez :
   « renforce encore, tu as de la marge ». Donc .75 à mi-rayon, noir
   plein au bout, et des rayons ramenés à 80 % × 95 % de la boîte pour
   que le noir arrive avant le bord. Le cœur reste à 18 % du rayon —
   ~14 % de la largeur autour de l'ancrage, le tableau en fait ~12 %.
   Sans la séquence : rien, la vignette n'existe pas. */
.panels__museum-vignette{display:none}
.panels.is-panels .panels__museum-vignette{
  display:block;
  position:absolute;inset:0;
  pointer-events:none;
  --vignette:rgba(0,0,0,1);
  background:radial-gradient(ellipse 80% 95% at var(--mu-ox, 50%) var(--mu-oy, 50%),
                             transparent 18%, rgba(0,0,0,.75) 55%, var(--vignette) 100%);
  transform-origin:var(--mu-ox, 50%) var(--mu-oy, 50%);
  transform:var(--mu-transform);
  will-change:transform;
}
/* Une fois le texte écrit (`is-final`, .88), la fenêtre est aux bords
   depuis .855 : la découpe ne découpe plus rien, et son masque, ré-évalué
   à chaque image du flottement, clignotait sur le bord de l'écran.
   Retirée ; `overflow:hidden` suffit. */
.panels.is-panels.is-final .panels__museum{clip-path:none}
/* LA TRAME, LES NAPPES ET LES POUSSIÈRES DU HERO — les mêmes règles que
   sur le procédé (`.process__grid`, `.process__dust`,
   `.process__particles` : sélecteurs partagés), au-dessus de l'image,
   sous le texte. Elles s'allument sur le dernier dixième, une fois la
   fenêtre aux bords de l'écran : posées pendant le recul, des lignes
   immobiles sur une image qui bouge auraient trahi le calque. Rien
   sans la séquence. */
.panels__grid,.panels__dust,.panels__particles{display:none}
.panels.is-panels .panels__grid,
.panels.is-panels .panels__dust,
.panels.is-panels .panels__particles{
  display:block;
  position:absolute;inset:0;
  z-index:1;
  pointer-events:none;
  opacity:var(--m-tx);
}
.panels.is-panels .panels__grid{
  background-image:linear-gradient(90deg,var(--grid-line) 0 1px,transparent 1px);
  background-size:calc(100% / 6) 100%;
}
.panels.is-panels .panels__dust{overflow:hidden}

/* LE CTA, À DROITE. L'image laisse sa moitié droite dans le noir :
   le texte s'y écrit, aligné à gauche de sa colonne, sur le dernier
   dixième de la course — 30 px de montée et une opacité, rien de plus,
   le moment fort est le travelling. La section ne porte plus de fond :
   le noir est celui de l'image. */
.panels.is-panels .panels__final{
  position:absolute;inset:0;
  z-index:2;
  min-height:0;padding:0;
  display:block;
  background:transparent;
  overflow:visible;
}
.panels.is-panels .panels__final .final__inner{
  position:absolute;
  left:58%;right:var(--gutter);
  top:50%;
  width:auto;max-width:none;padding:0;
  justify-items:start;text-align:left;
  opacity:var(--m-tx);
  transform:translateY(calc(-50% + 30px * (1 - var(--m-tx))));
}
/* Le corps du titre est celui de la déclaration du hero, posé sur
   `.final__title` : la colonne fait 36 vw, « par une ambition. » y
   tient sur sa ligne. (Il était en `--fs-h2` quand le titre était en
   `.display`.) */
.panels.is-panels .panels__final .final__lead{max-width:38ch}
/* Les liens ne sont cliquables qu'une fois le texte écrit (`is-final`,
   posée par le module à .88) : avant, la couche est transparente
   au-dessus des cartes, et rien ne doit s'y attraper. */
.panels.is-panels .panels__final .final__inner{pointer-events:none}
.panels.is-panels.is-final .panels__final .final__inner{pointer-events:auto}

/* ---------- sans la séquence ----------
   Sans script, sous `prefers-reduced-motion`, ou sous 1000 px : les
   quatre temps en plein écran, l'un sous l'autre — l'image en haut,
   le texte dessous. Pas de bandes étroites sur un téléphone. */
.panels:not(.is-panels) .panels__track::after{display:none}
.panels:not(.is-panels) .panels__stage{position:static;height:auto;overflow:visible}
.panels:not(.is-panels) .panels__row{
  position:static;
  display:block;
  padding:calc(var(--header-h) + var(--s-lg)) var(--gutter) var(--section-y);
}
.panels:not(.is-panels) .panel{
  min-height:100svh;
  overflow:visible;
  display:grid;
  grid-template-rows:62svh auto;
  gap:var(--s-md);
  padding-block:var(--s-lg);
  border-left:0;
}
.panels:not(.is-panels) .panel + .panel{border-top:1px solid var(--line)}
.panels:not(.is-panels) .panel__media{position:relative;inset:auto;width:auto;height:62svh}
.panels:not(.is-panels) .panel__media .method-media{
  position:static;width:100%;max-width:100%;transform:none;
}
.panels:not(.is-panels) .panel__num{position:static;opacity:1;margin-bottom:var(--s-xs)}
.panels:not(.is-panels) .panel__text{
  position:static;transform:none;width:auto;max-width:34ch;
}
.panels:not(.is-panels) .panel__giant{display:none}
/* Le musée sans la séquence : l'image en bloc, 16:9 sur toute la
   largeur, puis le CTA en bloc noir, centré comme avant. */
.panels:not(.is-panels) .panels__museum-img{
  display:block;width:100%;height:auto;
  filter:var(--museum-levels) var(--method-filter);
}
.panels:not(.is-panels) .panels__museum-cam{animation:none}
.panels:not(.is-panels) .panels__final{position:relative;clip-path:none}
.panels:not(.is-panels) .panels__final .eyebrow{justify-content:center}
@media (max-width:1000px){
  .panels{--method-overlap:76svh}
}
