/* ==========================================================================
   COMPOSANTS
   Aucune valeur en dur — tout vient de tokens.css (CADRAGE §7.1 bis A)
   ========================================================================== */

:root {
  --page-pad:   clamp(16px, 2vw, 32px);
  --logo-inset: clamp(16px, 2vw, 32px);

  /* L'unité de grille — un carré sur 12 colonnes.                          */
  --u: calc((100vw - 2 * var(--page-pad) - 11 * var(--grid-gap)) / 12);

  /* Le logo occupe 2 carrés (CADRAGE §4.1).                                */
  --logo-size: calc(2 * var(--u) + var(--grid-gap));

  /* Le logo en mode fiche — voir son bloc plus bas. Déclarés ici, sur la
     racine, parce que `.peek-zone` s'en sert aussi : la poignée fait la
     largeur du logo réduit, et les deux doivent partager leur source.     */
  --logo-shrink: 0.5;                            /* 2 carrés → 1 carré */

  /* Le déplacement vers le bord, en mode fiche : le logo réduit vient s'y
     caler exactement.

     ⚠️ IL SE DÉDUIT DE `--logo-size`, JAMAIS DE `--u`. Il valait
     `50vw - --u / 2` — juste tant que le logo faisait 2 carrés et que sa
     moitié réduite en valait donc un demi. **Ce lien était implicite** :
     doubler la taille du logo sans y toucher l'aurait envoyé 57px hors de
     l'écran, et rien dans la déclaration ne l'annonçait.

     Écrit ainsi, il reste juste quelle que soit la taille : on décale du
     centre jusqu'au bord, moins la demi-largeur VISIBLE (donc après
     `scale`). C'est le §7.1 bis A — une valeur qui dépend d'une autre doit
     le dire, pas le supposer.                                             */
  --logo-shift: calc(50vw - var(--logo-size) * var(--logo-shrink) / 2);

  /* Le retrait gauche du texte de la fiche — titre, description, crédits.
     Une seule source : les trois doivent rester alignés sur le même axe,
     sinon un réglage en désaligne un sans qu'on le voie (§7.1 bis A).     */
  --text-indent: calc(2 * var(--u));

  /* Le retrait de la GALERIE seule — distinct de `--text-indent`, qui reste
     l'axe du texte ET du pas à pas (`.project__nav` n'y touche pas). Sur la
     MÊME trame horizontale en carrés (`--u`), pas un `vw` déconnecté : 2
     carrés de chaque côté sur 12, la colonne la plus large qui reste un
     nombre entier de carrés sous la barre des 80vw visée.                 */
  --gallery-indent: calc(2 * var(--u));

  /* Où s'arrête le bloc titre, mesuré depuis le bord GAUCHE DE L'ÉCRAN. Le
     titre casse donc avant le bord droit au lieu de courir jusqu'au bout.

     ⚠️ En `vw` et non en `%`. Un pourcentage se résout contre le parent :
     il valait l'écran tant que le titre était enfant du hero, il vaut le
     corps depuis qu'il est descendu dedans — et le même token aurait
     silencieusement changé de sens en changeant de place. `vw` dit ce
     qu'on veut réellement dire, où qu'on l'emploie.                       */
  --title-end: 70vw;

  /* L'écart minimal entre les deux boutons du pas à pas, quand deux titres
     longs se rejoignent au centre. En carrés parce qu'il se mesure sur la
     trame horizontale, comme le retrait du texte — pas sur le rythme
     vertical. Voir `.project__nav`.                                       */
  --nav-split: calc(2 * var(--u));

  /* La largeur du volet about — la moitié de l'écran (§9 q.3). */
  --about-w: 50vw;

  /* La pastille du hero (§4.7). L'ancienne version faisait `5vw` ; bornée
     ici, sinon elle deviendrait une soucoupe sur un grand écran et un
     bouton sur un petit.

     ⚠️ Réduite après essai — elle valait `clamp(96px, 8vw, 132px)`. Le
     contenu ne mesure que ~60px de large (« for more » en `--fs-label`) :
     le disque flottait autour de son texte, et resserrer le `gap` ou
     l'interlignage n'y changeait rien. **Le vide était dans le cercle, pas
     entre ses lignes.**

     ⚠️ Une version agrandie (112-144px, texte en `--fs-value`) a été essayée
     le 08/10/2026 puis annulée : le vert fluo suffit à la rendre visible.  */
  --hint-size: clamp(76px, 6vw, 96px);
  --hint-icon: clamp(16px, 1.2vw, 20px);

  /* ⚠️ LA DISTANCE DU PUSH EST UNE VALEUR À PART, même si elle vaut la
     largeur du volet ici.

     Les deux ont été confondues d'abord — une seule valeur, « les deux
     mouvements ne peuvent pas se désaccorder ». C'est vrai tant que le
     volet pousse d'exactement ce qu'il occupe. Mais sur mobile il prend
     TOUT l'écran, et pousser le site d'autant le ferait sortir entièrement
     du champ : plus rien à voir derrière, et un mouvement de 100vw pour un
     résultat que 50 suffisent à obtenir.

     Ce que le volet OCCUPE et ce dont le site RECULE sont deux décisions,
     pas une.                                                             */
  --push-d: var(--about-w);

  /* L'air PROPRE à la galerie, en plus du `--sp-section` qu'elle partage
     avec les autres temps de la fiche. Elle est le seul bloc qui sorte du
     texte — bord à bord, images en bande, compteur — et se détache donc
     un cran de plus que les autres.

     ⚠️ Réglé ici et pas dans l'échelle : ce n'est pas un palier de rythme
     mais un supplément qui ne vaut que pour ce bloc. L'ajouter à
     `--sp-section` déplacerait aussi titre → texte et récit → crédits.   */
  --gallery-gap: 40px;

  /* L'écart ENTRE deux médias empilés de la galerie. Distinct de
     `--gallery-gap`, qui sépare la galerie de ce qui l'entoure : celui-ci
     vit à l'intérieur. Chaque média a son propre ratio ; deux bords qui se
     toucheraient se liraient comme une seule image plutôt que deux pièces
     distinctes — l'écart doit rester franc.                                */
  --gallery-stack: 8rem;   /* 128px */

  /* L'écart VERTICAL entre deux lignes de la galerie : le double de
     l'écart horizontal (`--gallery-stack`, entre deux `half`).            */
  --gallery-row: calc(2 * var(--gallery-stack));   /* 256px */
}

/* ==========================================================================
   LOGO — l'ancre. Centre sur la landing, bord sur une fiche.
   Sa position seule dit à quel niveau on se trouve (CADRAGE §4.2)
   ========================================================================== */

/* ⚠️ LE LOGO N'ANIME QUE `transform`. Rien d'autre.

   Il animait `top`, `left`, `width` et `height` : quatre propriétés qui
   forcent le navigateur à RECALCULER LA MISE EN PAGE à chaque frame. Tant
   que rien d'autre ne chargeait la frame, ça passait. Mais à l'aller, le
   logo se déplace pendant que `renderProject()` construit toute la fiche
   (innerHTML, découpage du titre, mesure de chaque média) — et le logo
   sautillait. Au retour, où rien n'est construit, il restait fluide.
   C'est ce que le §7.1 bis D interdit : `transform` seul est composé par
   le GPU, sans recalcul.

   La géométrie s'y prête : **le logo est TOUJOURS à mi-hauteur** et n'a que
   **trois positions horizontales** — centre, bord gauche, bord droit.
   Un seul axe bouge, trois arrêts connus d'avance.

   La boîte garde donc sa taille de landing (2 carrés) en permanence ; le
   passage à 1 carré se fait par `scale(0.5)`, et le déplacement par
   `translateX`. Aucune de ces deux propriétés ne touche la mise en page.  */

/* ==========================================================================
   ENTRÉE DU SITE — l'état AVANT la première peinture (§9)

   `is-entering` est posé sur le `<body>` dans le document lui-même, pas par
   le JS : le masque doit exister dès la première frame. Ajouté après coup,
   on verrait la landing complète le temps que le module se charge — le
   flash que la séquence d'entrée existe précisément pour supprimer
   (§7.1 bis D : rien ne surgit).

   `motion.js` retire la classe une fois le logo posé ; les items portent
   ensuite leur propre opacité, gérée par la cascade.

   ⚠️ On masque le CONTENU de la grille, pas `.stage` : celle-ci porte le
   fond gris (`--paper`), qui doit être là dès le premier instant. C'est sur
   lui que le logo apparaît.                                                */

.is-entering #work,
.is-entering .corners,
.is-entering #logo { opacity: 0; }

/* Mouvement réduit : aucune séquence, tout est en place d'emblée. Le JS
   retire la classe immédiatement, mais la règle évite même le clignotement
   d'une frame si le script tarde.                                          */
@media (prefers-reduced-motion: reduce) {
  .is-entering #work,
  .is-entering .corners,
  .is-entering #logo { opacity: 1; }
}

.logo {
  position: fixed;
  z-index: var(--z-logo);
  top: 50%;
  left: 50%;
  width: var(--logo-size);
  height: var(--logo-size);

  /* Le repos : centré, pleine taille.
     `--push` chasse le logo avec le reste du site quand l'about s'ouvre —
     composé DANS le translate, jamais dans `left` (voir le bloc about).  */
  transform: translate(calc(-50% + var(--push, 0px)), -50%);

  overflow: hidden;
  transition: transform var(--d-slow) var(--e-inout);
  will-change: transform;
}

.logo__mark { width: 100%; height: 100%; object-fit: cover; }

/* Le pourcentage du chargement d'entrée (motion.js, `loadAssets`) : sous le
   logo, sur l'axe central. Posé par le JS le temps de l'attente, puis retiré. */
.loader {
  position: fixed;
  z-index: var(--z-logo);
  left: 50%;
  top: calc(50% + var(--logo-size) / 2 + var(--sp-close));
  transform: translateX(-50%);
  font-size: var(--fs-value);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Le logo calé à un bord — UN SEUL état, atteint d'un seul trait.

   ⚠️ Il y avait ici deux classes, `is-shrinking` (pendant le glissement du
   décor) puis `is-side` (au repos), échangées en cours de trajet. Elles ont
   fini par déclarer le même transform, et l'échange ne faisait plus que
   redémarrer la transition à mi-parcours — d'où un micro-saut. Une seule
   classe désormais : le logo part une fois et arrive une fois.            */
.logo.is-side.is-left {
  transform: translate(calc(-50% - var(--logo-shift) + var(--push, 0px)), -50%)
             scale(var(--logo-shrink));
}

.logo.is-side.is-right {
  transform: translate(calc(-50% + var(--logo-shift) + var(--push, 0px)), -50%)
             scale(var(--logo-shrink));
}

/* ==========================================================================
   TEXTURE — une couche unique par-dessus tout le site, en color-burn.
   Opacité réglable seule via `--texture-opacity`.
   ========================================================================== */

.texture {
  position: fixed;
  inset: 0;
  z-index: var(--z-texture);
  mix-blend-mode: color-burn;
  opacity: var(--texture-opacity);
  pointer-events: none;
}

/* ==========================================================================
   QUATRE COINS — mentions fixes. Contenu à définir.
   ========================================================================== */

/* Au-dessus de tout, en permanence.

   ⚠️ LES COINS SUIVENT LA FICHE pendant l'entrebâillement — voir plus bas.
   Ils étaient fixes par principe (« ils ne bougent jamais », §4.3), ce qui
   se tenait tant qu'ils ne portaient que des mentions. Depuis que le coin
   bas gauche porte les FILTRES — seule navigation du site — rester en place
   les ferait chevaucher la colonne révélée : une navigation doit rester
   lisible, donc accompagner ce qui bouge.                                 */
.corners {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-corners);

  /* ⚠️ DEUX TRANSFORMS SUR LE MÊME ÉLÉMENT, et ils ne peuvent pas cohabiter
     en deux déclarations : la seconde écraserait la première.

     Les coins suivent déjà la fiche pendant l'entrebâillement
     (`is-peeking--corners`, §4.3). Le décalage de l'about s'y AJOUTE, dans
     un seul `translateX` — d'où `--peek-x`, que la règle d'entrebâillement
     pose au lieu d'écrire son propre `transform`.                        */
  transform: translateX(calc(var(--peek-x, 0px) + var(--push, 0px)));

  /* ⚠️ LA DURÉE DÉPEND DE LA CAUSE, PAS DE L'ÉLÉMENT.

     Ces coins se déplacent pour deux raisons qui n'ont pas le même tempo :
     l'entrebâillement est un geste VIF (`--d-base`, §4.3), l'ouverture de
     l'about est un mouvement AMPLE que tout le site accompagne
     (`--d-slow`).

     Une durée fixe forçait donc l'un des deux : à `--d-base`, les coins
     arrivaient à 400 ms quand la scène et le volet mettaient 800 — **on
     voyait la colonne pendant que le reste finissait de partir.** Le geste
     se jouait en deux temps.

     `--move-*` porte donc le tempo courant : la règle `.is-about` le passe
     à `--d-slow`, et l'entrebâillement garde le défaut vif.              */
  transition: transform var(--move-d, var(--d-base)) var(--move-e, var(--e-out));
}

.corner {
  position: absolute;
  color: var(--ink);
  transition: --ink var(--d-base) var(--e-out);
}

/* ⚠️ `.act` pose sa propre `transition` (opacité du survol) et, venant plus
   tard, écraserait celle de `.corner` sur les coins qui sont des commandes.
   Les deux sont donc redites ici, ensemble.                                */
.corner.act {
  transition: opacity var(--d-fast) var(--e-out),
              --ink   var(--d-base) var(--e-out);
}

/* Un coin posé sur le hero passe en blanc : la trame l'assombrit, le noir n'y
   lit plus. C'est `--ink` qui bascule et non `color` — les enfants (filtres,
   chevron, casque) lisent tous `--ink`, ils suivent sans règle propre.
   `project.js` (`bindCornerInk`) pose la classe, coin par coin.            */
.corner.is-over { --ink: var(--ink-invert); }

.corner--tl { top: var(--page-pad); left: var(--page-pad); }
.corner--tr { top: var(--page-pad); right: var(--page-pad); }
.corner--bl { bottom: var(--page-pad); left: var(--page-pad); text-align: left; }
.corner--br { bottom: var(--page-pad); right: var(--page-pad); text-align: right; }

/* Panne d'API — posé au-dessus de la mention en bas à gauche, pour ne pas
   la recouvrir. N'apparaît que si le contenu n'a pas pu être chargé.       */
.corner--failure { bottom: calc(var(--page-pad) + 2em); }

/* ⚠️ LE SEUL COIN QUI PORTE DEUX COMMANDES — le nom et le casque.

   `.corner` ne déclare que `position` et `color` : elle place, elle
   n'aligne pas. Le jour où un coin contient plus d'un élément, il lui faut
   donc un mode de disposition — c'est tout ce que fait cette classe, et
   elle ne sert qu'ici pour l'instant.

   ⚠️ `align-items: center` et non `baseline` : le casque est une icône, pas
   du texte. Sur une baseline commune, un SVG carré se cale par son bas et
   flotte visuellement au-dessus du mot.                                   */
.corner--pair {
  display: flex;
  align-items: center;
  gap: var(--sp-tight);
}

/* ==========================================================================
   LE CASQUE — l'interrupteur de tout l'audio (nappe + sons de l'interface)

   ⚠️ IL N'EST PAS UN PLAYER. Pas de titre, pas de progression, pas de
   piste : le site n'affiche aucun widget, et un lecteur complet dans un
   coin en ferait le seul élément d'interface à se présenter comme tel.
   Une icône, deux états — l'austérité est ce qui rend le son acceptable
   sur un site de ce registre.
   ========================================================================== */

.sound {
  /* Un bouton reste un bouton : sans ça, le navigateur pose son fond, sa
     bordure et son padding, et l'icône se retrouve dans une boîte grise. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;

  /* La zone de clic dépasse le dessin — une icône de 14px est atteignable
     à la souris, beaucoup moins au doigt. Le `margin` négatif reprend ce
     que le `padding` ajoute, pour que le casque ne pousse pas le nom.    */
  padding: 0.4em;
  margin: -0.4em;
}

/* ⚠️ L'ICÔNE SE MESURE SUR LE TEXTE, PAS EN PIXELS.

   `1em` la cale sur `.t-label` : elle suit donc la typo du coin, y compris
   ses replis responsive, sans qu'aucune valeur n'ait à être tenue d'accord.
   C'est la même logique que `--peek-zone`, calculée depuis les tokens du
   logo plutôt que mesurée en JS (§4.3).                                   */
.sound__icon {
  width: 1.15em;
  height: 1.15em;
  display: block;
}

/* La barre du muet — c'est ELLE qui porte les deux états, pas deux icônes.

   ⚠️ Deux dessins échangés auraient produit un saut : le navigateur ne
   peut pas fondre entre deux `path` différents, et l'échange se voit d'une
   frame. Ici un seul trait se rétracte sur lui-même — un mouvement, pas un
   remplacement (§7.1 bis D : rien ne surgit, rien ne s'efface).

   `stroke-dasharray` vaut la longueur du trait (25,45 ≈ √(18² + 18²)) :
   l'offset le fait sortir de son propre tracé.                           */
.sound__bar {
  stroke-dasharray: 26;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--d-base) var(--e-out);
}

.is-sound .sound__bar { stroke-dashoffset: 26; }

/* Le son actif — l'icône passe en plein. Sans ça, rien ne distingue « le
   son est allumé » de « le son est allumé mais rien ne joue en ce moment »,
   et la nappe seule est trop discrète pour lever le doute.               */
.is-sound .sound__icon { opacity: 1; }
.sound__icon { opacity: 0.75; transition: opacity var(--d-fast) var(--e-out); }

/* ==========================================================================
   COMMANDE TEXTE — un mot qui se clique, sans décor.

   ⚠️ Le site en a plusieurs et elles disaient toutes la même chose : les
   deux coins qui ouvrent l'about, son bouton de fermeture, les natures
   d'une fiche. Chacune redéclarait couleur, curseur, transition et le même
   `:hover` à 0.6 — quatre fois la même règle sous quatre noms (§7.1 bis A).

   `.act` porte le comportement, la classe d'origine ne garde que la
   position. C'est la séparation structure/typo appliquée à l'interaction.
   ========================================================================== */
.act {
  color: var(--ink);
  cursor: pointer;
  transition: opacity var(--d-fast) var(--e-out);
}
.act:hover { opacity: 0.6; }

/* ==========================================================================
   SURLIGNAGE — le survol REMPLIT le fond (31/07/2026)

   Trois règles faisaient la même chose sous trois noms : le lien externe
   d'une fiche, le pas à pas, le contact de l'about. Toutes passaient le
   TEXTE en `--accent`.

   ⚠️ Ça ne fonctionne plus depuis que l'accent est le vert lemon : en texte
   il donne 1,23:1 sur blanc — invisible. En fond avec du noir dessus, 15,3:1.

   Le survol remplit donc l'aplat au lieu de teinter la lettre. C'est plus
   franc, ça vaut sur les deux fonds du site, et le contraste ne dépend plus
   de la couleur choisie : c'est toujours du noir sur de l'accent.

   ⚠️ IL SURLIGNE LIGNE PAR LIGNE, PAS LE BLOC ENTIER.

   Un fond posé sur un conteneur habille son rectangle : sur un titre qui
   casse en deux lignes, la zone verte englobe aussi le blanc au bout de la
   première. `box-decoration-break: clone` découpe le fond ligne à ligne,
   comme un surligneur passé sur du texte — mais il n'agit QUE sur un
   élément inline.

   D'où le porteur : `.mark` s'applique au conteneur cliquable (curseur,
   transition), et c'est **son texte** qui reçoit le fond. Sur un `.step`,
   qui est un flex empilant libellé et titre, poser le fond sur le
   conteneur aurait couvert les deux d'un seul rectangle.

   ⚠️ Le padding déborde légèrement (`-0.1em` de marge) pour que le
   surlignage ne colle pas aux lettres, sans décaler le texte dans le flux.
   ========================================================================== */
.mark {
  color: var(--ink);
  cursor: pointer;
}

/* ⚠️ LE SURLIGNAGE VA SUR `.mark__in`, JAMAIS SUR `.mark`.

   Cibler `.mark` ET ses enfants poserait le fond deux fois — sur le
   conteneur puis sur le texte — et le rectangle du conteneur annulerait la
   découpe ligne par ligne qu'on cherche.

   Le markup nomme donc explicitement ce qui se surligne. Un `.mark` sans
   `.mark__in` ne s'allume pas : c'est visible tout de suite, contrairement
   à un sélecteur trop large qui produit un rendu presque juste.          */
.mark__in {
  transition: background var(--d-fast) var(--e-out),
              color      var(--d-fast) var(--e-out);
}

.mark:hover .mark__in {
  /* ⚠️ APLAT NOIR — l'accent vert a été abandonné (31/07/2026), ici comme
     sur la pastille et l'infobox. Il fonctionnait, mais il posait du vert
     lemon sur les deux fonds du site à chaque survol : trop présent pour un
     geste qui se répète à chaque lien.

     Le noir dit la même chose sans occuper l'écran, et se lit aussi bien
     sur le gris de la landing que sur le blanc de la fiche.             */
  background: var(--block);
  color: var(--ink-invert);

  /* La découpe ligne par ligne. `-webkit-` pour Safari, qui ne suit pas
     encore la propriété non préfixée.                                    */
  -webkit-box-decoration-break: clone;
          box-decoration-break: clone;

  padding: 0.1em 0.3em;
  margin: -0.1em -0.3em;
}

/* Les coins actifs reprennent en plus les événements : leur conteneur
   `.corners` les laisse passer (comme les filtres en face).               */
.corner--act { pointer-events: auto; }

/* ==========================================================================
   ABOUT — un volet qui POUSSE depuis la gauche (CADRAGE §9 q.3)

   ⚠️ Il ne recouvre pas, il pousse : le site entier se décale vers la droite
   pendant que le volet prend sa place. C'est la grammaire de l'axe
   horizontal déjà posée par l'entrebâillement (§4.3) et le fiche→fiche
   (§4.4) — les choses s'écartent, se poussent, glissent de côté. Un
   recouvrement aurait introduit un geste de plus.

   Les coins gauches en sont les poignées, comme le logo est celle de
   l'entrebâillement.
   ========================================================================== */

/* Le voile — le site poussé recule dans l'ombre.

   ⚠️ Il couvre TOUT l'écran, pas seulement la partie droite. Le volet est
   posé par-dessus (`--z-about` > `--z-veil`) et l'occulte sur sa moitié :
   le découper à `left: var(--about-w)` aurait produit exactement le même
   rendu, en ajoutant une valeur à tenir d'accord avec la largeur du volet.

   ⚠️ `pointer-events` suit l'opacité. Sans ça, un voile invisible mais
   présent intercepterait tous les clics du site — le corps couvre l'écran
   entier, il faut donc qu'il cesse d'exister pour le pointeur.           */
.veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-veil);
  background: var(--block);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-slow) var(--e-inout);
}

.is-about .veil {
  opacity: 0.3;
  pointer-events: auto;
}

.about {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--about-w);
  height: 100%;
  z-index: var(--z-about);

  background: var(--paper-light);
  overflow-y: auto;

  /* Au repos, entièrement hors champ à gauche. */
  transform: translateX(-100%);
  transition: transform var(--d-slow) var(--e-inout);
}

.is-about .about { transform: translateX(0); }

/* ⚠️ LE SITE EST POUSSÉ PAR `transform`, COMME TOUT LE RESTE.

   Une première version décalait `left`. Deux raisons de ne pas le faire, et
   la seconde est la vraie :

   1. **`left` force un recalcul de mise en page à chaque frame** — c'est le
      défaut qui faisait sautiller le logo au §4.2, corrigé en passant tout
      en `transform`. Le réintroduire ici l'aurait ramené.

   2. **Sur `.stage` et `.corners`, ça ne poussait rien du tout.** Les deux
      ont `inset: 0`, qui pose `left` ET `right` : déplacer le seul `left`
      laissait le bord droit collé à l'écran, si bien qu'ils **se
      comprimaient** au lieu de se déplacer. D'où l'impression d'un overlay
      alors qu'on demandait un push.

   ⚠️ TROIS DES QUATRE ÉLÉMENTS PORTENT DÉJÀ UN `transform` — et deux
   déclarations ne s'additionnent pas, la seconde écrase la première. Le
   décalage est donc COMPOSÉ dans le translate existant de chacun :

   | Élément    | Ce qu'il portait déjà                        |
   |------------|----------------------------------------------|
   | `.stage`   | rien — `--push` seul                         |
   | `.logo`    | son centrage, plus deux états de bord + scale |
   | `.corners` | le décalage d'entrebâillement (`--peek-x`)   |
   | `.project` | idem                                          |

   ⚠️ Corollaire : les règles d'entrebâillement posent désormais une
   VARIABLE (`--peek-x`) au lieu d'écrire leur propre `transform`. Sans ça,
   ouvrir l'about depuis une fiche entrouverte annulerait l'un des deux
   décalages — silencieusement.

   ⚠️ ET TOUS PARTAGENT LA DURÉE ET LA COURBE DU VOLET — sinon le geste se
   joue en deux temps. Les coins et la fiche sont réglés sur `--d-base` pour
   l'entrebâillement, qui doit être vif : ils arrivaient donc à 400 ms quand
   la scène et le volet mettaient 800, **et on voyait la colonne pendant que
   le reste finissait de partir.**

   Le tempo est donc porté par la CAUSE, pas par l'élément : `--move-*` est
   passé à `--d-slow` ici, et retombe sur le défaut vif partout ailleurs.

   ⚠️ DEUX CLASSES, ET C'EST LA FERMETURE QUI L'EXIGE. `is-about` porte le
   décalage, `is-about-move` porte le tempo — et cette seconde survit à la
   fermeture, le temps du retour.

   Réunies, retirer la classe remettrait `--push` à zéro ET le tempo à
   `--d-base` dans la même frame : les coins reviendraient en 400 ms pendant
   que le volet mettrait 800. **Le défaut qu'on vient de corriger, réapparu
   à l'envers** — et invisible à l'aller, donc facile à laisser passer.
   La règle B (§7.1 bis) veut un aller-retour symétrique : le tempo doit
   valoir dans les deux sens.                                              */
.is-about { --push: var(--push-d); }

.is-about-move {
  --move-d: var(--d-slow);
  --move-e: var(--e-inout);
}

/* — Le contenu du volet ————————————————————————————————————————————————— */

.about__body {
  padding: var(--v-body) var(--page-pad) var(--sp-end);

  /* ⚠️ UN DEMI-CARRÉ DE RETRAIT — et le chemin pour y arriver dit pourquoi.

     Il est parti de `--text-indent` (2 carrés, l'axe du texte dans une
     fiche), puis un carré, puis la moitié. Chaque réduction dit la même
     chose : **la trame se prend sur l'ÉCRAN, le volet n'en fait qu'une
     moitié.** Un carré y vaut donc le double de ce qu'il vaut dans une
     fiche, et l'axe qui compose une page pleine écrase un panneau.

     Le carré reste l'unité — c'est la trame du site (§4.1). Ce qui se
     rejuge, c'est la QUANTITÉ, sur le contenant réel.                    */
  margin-left: calc(var(--u) / 2);

  /* ⚠️ Le texte s'arrête à 90 % — une marge blanche à droite.

     Sans elle, la ligne court jusqu'au bord du volet et s'y cogne : ce
     bord est une arête franche, le site poussé commence juste derrière —
     pas la respiration d'un bord d'écran.

     ⚠️ LE RETRAIT SE DÉDUIT — `90%` seul débordait. Les pourcentages se
     résolvent contre le volet entier, mais la boîte commence déjà à un
     demi-carré de son bord gauche : `90%` seul dépassait d'autant, et la
     marge demandée devenait un débordement. Le retrait se déduit donc, et
     les deux valeurs restent liées — changer l'un oblige à changer
     l'autre.                                                            */
  width: calc(90% - var(--u) / 2);
}

/* ⚠️ NI `.about__title` NI `.about__text` — les classes `.t-*` les portent.

   Les deux ne déclaraient que de la typo : taille, couleur, interlignage.
   C'est exactement ce que `type.css` fabrique, et le §7.1 bis A l'écrit —
   « la classe `.t-*` porte la TYPO, la classe d'origine ne garde que la
   STRUCTURE ». Elles recopiaient `.t-display` et `.t-value`, à un
   interlignage près.

   Le markup pose donc `.t-display` sur le titre et `.t-value` sur les
   paragraphes. Ne reste ici que ce qui est structurel : l'écart sous le
   titre, l'écart entre paragraphes.

   ⚠️ Le choix de `--fs-value` pour le texte mérite d'être noté : `--fs-title`
   semblerait le cran naturel sous 30px, mais son `clamp` plafonne à 32px —
   au-delà de 1333px de large il serait PLUS GRAND que ce qu'il remplace.
   Un palier qui grandit en descendant, c'est le trou d'échelle du §6.    */
/* ⚠️ `--sp-part` (64px) et NON `--sp-section` (128px).

   Ce dernier a été réglé pour la FICHE : il y sépare des blocs à
   `--fs-hero` (jusqu'à 110px) dans un écran entier. Le volet fait la moitié
   de cette largeur et son titre un cran de moins — le même écart y pèse
   donc le double, et éloignait au lieu de séparer.

   C'est le défaut du `--v-title` réemployé plus tôt dans le projet : un
   token juste ailleurs, appliqué sans rejuger le contenant.             */
.about__body > .t-display { margin-bottom: var(--sp-part); }
.about__text p            { margin-bottom: var(--sp-block); }

/* Le groupe de contact se détache du récit — même écart qu'entre deux temps
   d'une fiche. Les trois liens suivent en ligne, séparés d'un point médian
   — même motif que les natures de la fiche (§3, « atlas · interface ·
   image ») : ce sont trois portes cumulables vers le même sujet, pas une
   liste à empiler.                                                        */
.about__links {
  display: flex;
  flex-wrap: wrap;
  margin-top: var(--sp-part);
}

.about__contact + .about__contact::before {
  content: '·';
  margin: 0 0.5em;
  opacity: 0.5;
}

/* Le contact — chaque mot cliquable, dans la voix du pas à pas.

   ⚠️ Il porte `.step__title` (la taille) et `.mark` (le survol), mais PAS
   `.step` : celle-ci empile un libellé au-dessus d'une valeur, or il n'y a
   plus de libellé. Ne reste ici que sa place dans le flux.                */
.about__contact {
  display: inline-block;
}

/* La fermeture — seule la POSITION est propre à l'about, le comportement
   vient de `.act`. À gauche, sur l'axe du texte (`.about__body`, un demi-carré)
   plutôt qu'au bord du volet : bouton et récit partagent la même arête.   */
.about__close {
  position: absolute;
  top: var(--page-pad);
  left: calc(var(--u) / 2);
}

/* ==========================================================================
   CONSENTEMENT — le bandeau cookies (CADRAGE §D2)

   Un bandeau bas, pas un volet : il ne pousse rien, il flotte au-dessus de
   tout (`--z-consent`, le sommet de la pile) jusqu'à ce qu'un choix soit
   fait. Les deux boutons partagent la même classe — RGPD veut que refuser
   soit aussi simple qu'accepter, aucun des deux n'est mis en avant.        */
.consent {
  position: fixed;
  left: var(--page-pad);
  right: var(--page-pad);
  bottom: var(--page-pad);
  z-index: var(--z-consent);

  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-block);

  max-width: 640px;
  margin: 0 auto;
  padding: var(--sp-block);
  background: var(--paper-light);
  color: var(--ink);
}

/* ⚠️ `display: flex` ci-dessus ÉCRASE l'attribut `hidden` : une règle d'auteur
   l'emporte sur la feuille du navigateur. Sans cette ligne, le bandeau était
   affiché en permanence — y compris après un choix, que le JS marque pourtant
   `hidden`.                                                                 */
.consent[hidden] { display: none; }

.consent__acts {
  display: flex;
  gap: var(--sp-block);
  flex: none;
}

/* Même classe `.act` que le reste des commandes du site — pas de bouton
   plein, pas de couleur d'accent : un bandeau cookies n'a pas à crier plus
   fort que le logo ou les filtres.                                        */
.consent__act {
  white-space: nowrap;
}

/* — Les filtres — la seule navigation du site (§4.3) ————————————————————
   Une colonne, dans le coin haut droit. Chaque nature filtre la landing ;
   « All » remet tout — sans lui, le filtrage était sans issue.            */
.filters {
  display: flex;
  flex-direction: column;
  align-items: flex-end;   /* calés au bord droit, comme leur coin */

  /* Les mentions ne se cliquent pas ; les filtres, si. Le conteneur
     `.corners` laisse passer les événements, celui-ci les reprend.        */
  pointer-events: auto;
}

/* — Le déclencheur — replié, c'est la seule chose visible —————————————— */

.filters__toggle {
  display: flex;
  align-items: center;
  gap: 0.5em;
  color: var(--ink);
}

/* Quand un filtre est actif, le déclencheur porte son nom — c'est le NOM
   qui dit l'état, pas une couleur. Tout reste en `--ink`.                 */

/* Chevron dessiné en CSS — pas d'image pour deux traits. Il pivote à
   l'ouverture, ce qui suffit à dire l'état.                               */
.filters__chevron {
  width: 0.5em;
  height: 0.5em;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform var(--d-fast) var(--e-out);
}

.filters.is-open .filters__chevron {
  transform: rotate(-135deg) translate(-0.15em, -0.15em);
}

/* — La liste — dépliée au clic ————————————————————————————————————————
   `visibility` en plus de l'opacité : sans elle, les boutons resteraient
   cliquables une fois repliés, et atteignables au clavier.                */
.filters__list {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.55em;

  margin-top: 0.8em;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity    var(--d-fast) var(--e-out),
              transform  var(--d-fast) var(--e-out),
              visibility 0s linear var(--d-fast);
}

.filters.is-open .filters__list {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;   /* à l'ouverture, rien n'attend */
}

/* Tout le bloc est en `--ink`. L'actif et le survolé se distinguent par
   l'appui, pas par la couleur : les inactifs reculent légèrement.         */
.filter {
  color: var(--ink);
  text-align: right;
  opacity: 0.45;
  transition: opacity var(--d-fast) var(--e-out);
}

.filter:hover,
.filter.is-on { opacity: 1; }

/* ==========================================================================
   LANDING — la grille composée
   ========================================================================== */

/* La landing ne scrolle pas la page : les colonnes défilent en `transform`,
   pilotées à la molette. Le scroll du document est réservé à la fiche.

   `.is-back` (la landing s'efface derrière la fiche) a été retiré : le
   décor ne s'efface pas, il glisse et reste dessous — c'est lui qu'on
   entrevoit par l'entrebâillement (§4.3).                                 */
.stage {
  position: fixed;
  inset: 0;
  padding: 0 var(--page-pad);
  overflow: hidden;
  background: var(--paper);

  /* `--push` chasse le site vers la droite quand l'about s'ouvre.
     ⚠️ EN `transform`, jamais en `left` — voir le bloc de l'about.       */
  transform: translateX(var(--push, 0px));
  transition: transform var(--d-slow) var(--e-inout);
}



/* ==========================================================================
   GLISSEMENT — après le clic, le décor s'écarte (CADRAGE §4.2)

   La colonne cliquée vient occuper l'écran : l'axe central se cale sur x=0
   (clic à droite) ou sur x=100% (clic à gauche). Tout rétrécit de moitié
   pendant le trajet. La vignette élue, elle, est déjà détachée et immobile.
   ========================================================================== */

.work {
  transform-origin: center center;
  transition: transform var(--d-slow) var(--e-inout);
}

/* L'AXE DE JONCTION des deux colonnes vient se caler sur le bord de l'écran :
   une colonne sort entièrement du cadre, l'autre occupe tout l'espace.
   L'axe est au milieu → il faut le déplacer d'une demi-largeur d'écran.    */
.work.is-sliding--left  { transform: translateX(-50vw) scale(0.5); }
.work.is-sliding--right { transform: translateX(50vw)  scale(0.5); }

/* La vignette élue reste en place dans sa colonne — le clone est un calque
   séparé qui voyage par-dessus. Pas de trou pendant le glissement, pas de
   réapparition brutale au retour.                                          */

/* — Un item ——————————————————————————————————————————————————————————— */

.item {
  position: relative;
  display: block;
  cursor: pointer;
  /* Pas d'overflow:hidden — le flou doit pouvoir déborder du cadre,
     sinon le bord de la div reste net et la boîte se voit.               */
}

.item__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Au repos : désaturé — unifie un corpus hétérogène (CADRAGE §4.1). */
  filter: grayscale(1);
  transition: filter    var(--d-base) var(--e-out),
              transform var(--d-slow) var(--e-out);
}

/* RÈGLE FONDATRICE : la couleur marque le premier plan (CADRAGE §4.1).
   L'item survolé reprend sa couleur et avance.                             */
.item.is-hot {
  z-index: 3;   /* devant les voisins dont le flou déborde */
}

.item.is-hot .item__media {
  filter: grayscale(0);
  transform: scale(1.03);
}

/* Les autres reculent : ils s'effacent et se floutent, BORDS COMPRIS. */
.work.is-hovering .item:not(.is-hot) .item__media {
  filter: grayscale(1) blur(10px) opacity(0.3);
}

/* ⚠️ LE SURVOL SE COUPE NET QUAND UNE TRANSITION COMMENCE.

   Le survol est animé par le CSS (ci-dessus), les transitions du site par
   `motion.js` : deux mécanismes qui s'ignorent. Au clic, retirer l'état de
   survol lançait donc un `blur(10px → 0)` sur CHAQUE vignette visible —
   l'effet le plus coûteux qui soit — pile sur la frame où le clone commence
   à grandir. D'où un à-coup au clic, alors que « Next » (rien de survolé,
   donc rien à défaire) restait parfaitement fluide.

   Ici la transition est neutralisée : l'état retombe instantanément. Rien
   ne se voit — le décor est déjà en train de glisser et de rétrécir de
   moitié, l'attention est sur le clone qui grandit.                        */
.work.is-leaving .item__media {
  transition: none;
}

/* ==========================================================================
   INFOBOX — flotte et suit le curseur.
   Un seul élément pour toute la page, hors des items : son mouvement est
   continu d'un item à l'autre, avec un léger retard — organique, pas collé.
   ========================================================================== */

.info {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-info);
  padding: 1em 1.2em 1.1em;

  /* ⚠️ APLAT NOIR — l'essai en accent a été annulé (31/07/2026).

     L'idée était d'accorder l'infobox à la pastille du hero : deux repères
     qui suivent le curseur, donc une seule couleur. À l'écran, non — la
     landing est une grille d'images désaturées sur fond gris, et une boîte
     vert lemon qui la parcourt en permanence prend le pas sur le corpus.

     La pastille du hero peut se le permettre : elle apparaît sur UNE image,
     brièvement, et disparaît au premier scroll. L'infobox, elle, accompagne
     toute la lecture de la liste.

     **Le même geste ne demande pas la même couleur quand il ne dure pas le
     même temps.**                                                        */
  background: var(--block);
  color: var(--ink-invert);
  pointer-events: none;
  opacity: 0;
  max-width: 22em;
  transition: opacity var(--d-fast) var(--e-out);
  will-change: transform;
}
.info.is-on { opacity: 1; }

/* ⚠️ AU DOIGT, L'INFOBOX NE SUIT PLUS RIEN — elle s'ancre en bas d'écran.

   Elle est placée par `translate3d` depuis `followCursor` ; sans curseur,
   cette fonction n'est jamais appelée et la boîte resterait figée dans
   l'angle haut-gauche, sur son `top/left: 0` de départ.

   Elle porte le nom de l'item qualifié au centre (§4.5) : elle doit donc
   être lisible sans recouvrir ce qu'elle nomme. D'où le bas de l'écran, et
   la pleine largeur — sur 375px, une boîte flottante de 22em déborderait.

   ⚠️ `transform: none` est indispensable : il annule tout résidu laissé par
   un éventuel passage au curseur (écran tactile hybride, redimensionnement
   depuis le desktop). Sans lui, la boîte serait décalée d'une position
   qu'aucun geste ne remettrait à zéro.

   ⚠️ TROISIÈME occurrence des 600px — avec `grid.css` et `grid.js` (`MOBILE`).
   Le seuil décide de la composition, du DOM construit et du placement de
   cette boîte : les trois doivent tomber ensemble ou la landing devient
   incohérente sans qu'aucune erreur ne le signale.                         */
@media (max-width: 600px) {
  .info {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    max-width: none;
    transform: none;
    will-change: auto;
  }
}

/* ⚠️ UNE SEULE COULEUR, AUCUNE OPACITÉ.

   Les trois lignes portaient `--ink-invert` avec 1, 0.6 et 0.85 d'opacité —
   trois nuances pour hiérarchiser dans une boîte qu'on survole une seconde.
   Sur l'accent, ces demi-teintes délavent le noir sans rien clarifier : la
   hiérarchie se lit déjà à la taille et à l'ordre.

   `color: inherit` — le noir vient du conteneur, un seul endroit le règle.
   ⚠️ Indispensable : `.t-value` pose `--ink` et `.t-meta` aussi, mais rien
   ne garantit qu'ils suivront la couleur du fond le jour où elle change. */
.info__title,
.info__natures { color: inherit; }

/* ⚠️ Les DEUX lignes sont en `.t-meta`, donc en Outfit — le titre était en
   `.t-value` (Roboto), hérité de la fiche où les natures sont en display
   parce qu'elles voisinent un titre en display. Ici le titre est dans la
   boîte : deux familles pour deux lignes qui se lisent d'un coup, ça se
   voyait.

   `.t-meta` ne portant pas d'échelle (ses emplois n'ont pas la même), la
   taille reste à la charge de chacun.                                    */
.info__title {
  font-size: var(--fs-value);
  margin-bottom: 0.15em;
}

/* Même casse que sur la fiche : capitale initiale, pas de capitales
   pleines. Les natures arrivent en minuscules de la base (§4.3).        */
.info__natures {
  margin-top: 0.5em;
  text-transform: capitalize;

  /* ⚠️ La taille est posée ici : `.t-meta` porte la voix mais pas
     l'échelle, ses deux emplois n'ayant pas la même (voir `type.css`).

     `--fs-body` (14px) — un cran SOUS les natures d'une fiche. La boîte se
     lit en une seconde au survol, pas comme un bloc de page : ce qui pèse
     juste sur une fiche encombre ici.

     Le titre descend du même cran (`.t-value`, 18px) : c'est l'ÉCART entre
     les deux qui porte la hiérarchie, là où trois opacités s'en chargeaient
     avant. Les déplacer ensemble le conserve.                            */
  font-size: var(--fs-body);
}

/* ==========================================================================
   FICHE PROJET — hero fixe, contenu qui scrolle par-dessus (CADRAGE §4.3)
   ========================================================================== */

.project {
  position: relative;
  z-index: var(--z-project);
  min-height: 100vh;
  /* Deux décalages possibles, composés dans UN seul `translateX` :
     l'entrebâillement (`--peek-x`, §4.3) et l'about qui pousse le site
     (`--push`, §9 q.3). Deux déclarations de `transform` ne s'additionnent
     pas — la seconde écrase la première.                                   */
  transform: translateX(calc(var(--peek-x, 0px) + var(--push, 0px)));
  /* Même tempo variable que les coins : vif pour l'entrebâillement, ample
     pour l'about — voir `.corners`.                                       */
  transition: transform var(--move-d, var(--d-base)) var(--move-e, var(--e-out));
  /* ⚠️ PAS d'overflow ici : même `clip` sur un seul axe force l'autre en
     `auto` et crée un conteneur de défilement — le `sticky` de la galerie
     s'y accrocherait au lieu de l'écran et cesserait de coller.
     Le clipping se fait sur le cadre épinglé lui-même.                     */
}
.project[hidden] { display: none; }

/* La zone sensible — une bande sur le bord, large comme la colonne révélée.
   Elle déclenche ET maintient l'ouverture : sans elle, sortir du logo
   refermerait tout et les items seraient inatteignables.                   */
/* Large comme le logo réduit — la poignée fait la taille de ce qu'on saisit.
   Calculée depuis les MÊMES tokens que lui : les deux ne peuvent pas se
   désaccorder, et le JS n'a plus à mesurer quoi que ce soit pendant la
   transition.                                                              */
/* AU REPOS : la zone sensible se limite AU LOGO — même taille, même
   position, à mi-hauteur. C'est lui la poignée (§4.3), pas une bande
   invisible courant sur toute la hauteur de l'écran.

   ⚠️ Elle occupait toute la hauteur : n'importe quel élément posé dans ce
   couloir devenait inatteignable, puisque l'entrebâillement s'ouvrait au
   passage du curseur et dérobait la fiche. C'est ce qui rendait les filtres
   inaccessibles — le symptôme d'une zone trop large, pas d'un empilement.  */
.peek-zone {
  position: fixed;
  top: 50%;
  height: calc(var(--logo-size) * var(--logo-shrink));
  width: calc(var(--logo-size) * var(--logo-shrink));
  transform: translateY(-50%);
  z-index: var(--z-project);
}
.peek-zone--left  { left: 0; }
.peek-zone--right { right: 0; }

/* Ouverte, la bande s'élargit pour couvrir la colonne dégagée.
   Elle laisse passer les événements pour que les items révélés restent
   scrollables et cliquables — le survol continue d'être capté par les
   éléments qu'elle contient (elle englobe la colonne).                     */
.is-peeking .peek-zone {
  /* OUVERTE, elle reprend toute la hauteur : c'est ce qui maintient
     l'entrebâillement pendant qu'on parcourt la colonne révélée, du haut
     vers le bas. Fermée, seul le logo est sensible ; ouverte, toute la
     colonne l'est.                                                        */
  top: 0;
  height: 100%;
  transform: none;

  /* Ouverte, la bande couvre exactement la colonne dégagée — pas plus.

     ⚠️ Elle ADDITIONNAIT la poignée et l'amplitude, alors que le logo est
     posé PAR-DESSUS la colonne révélée, pas à côté : les deux zones se
     superposent. La bande dépassait donc la colonne de toute la largeur du
     logo, et restait sensible au-delà de ce qu'elle révèle.

     `max()` et non une somme : la colonne l'emporte dès qu'elle est plus
     large que la poignée, et la poignée sert de plancher si la colonne est
     étroite (un module d'1 carré).                                        */
  width: max(
    calc(var(--logo-size) * var(--logo-shrink)),
    var(--peek-abs, 0px)
  );
  pointer-events: none;
}

/* L'amplitude vaut la largeur du plus grand item de la colonne révélée —
   calculée au survol et posée en `--peek`. Le décor ne bouge pas : on
   n'entrevoit qu'une colonne, celle du côté du logo.
   UN SEUL point de transformation : le hero est dans le flux, il suit.
   La fiche s'abaisse pour que la grille redevienne cliquable.              */
/* ⚠️ Pose la VARIABLE, pas le `transform` — voir la règle de base. */
.is-peeking .project {
  z-index: 0;
  --peek-x: var(--peek, 0px);
}

/* Les quatre coins suivent la fiche, du même décalage et d'un bloc.
   Ils forment un cadre : le dissocier se verrait plus que le mouvement
   lui-même. Sans ça, les filtres du coin bas gauche se retrouveraient
   par-dessus la colonne révélée, illisibles.

   ⚠️ CLASSE DISTINCTE de `is-peeking`, et c'est délibéré. Au retour, la
   fiche doit GARDER son décalage jusqu'au bout : le clone part de cette
   position (§4.2). Les coins, eux, n'ont aucune raison d'attendre — ils
   revenaient 1800 ms après le clic, tout le reste étant déjà rentré.
   Deux classes = deux tempos, sans toucher à la mécanique du clone.      */
/* ⚠️ Pose la VARIABLE, pas le `transform`. Les coins peuvent être décalés
   par deux causes à la fois — l'entrebâillement et l'about (§9 q.3) — et
   deux déclarations de `transform` ne s'additionnent pas : la seconde
   écrase la première. Leur règle de base les compose dans un seul
   `translateX`. */
.is-peeking--corners .corners {
  --peek-x: var(--peek, 0px);
}

/* LE HERO — cadre qui s'ouvre depuis la vignette jusqu'aux 4 coins (§4.2).

   Il RESTE EN COULEUR et SCROLLE avec le reste : décision révisée (§4.3).
   Il devait se désaturer à 18% pour devenir fond — abandonné, l'image est
   le sujet, pas la toile. Plus de `sticky`, plus de `margin-bottom: -100vh`.

   ⚠️ Il reste DANS LE FLUX de la fiche. Ne pas le passer en
   `position: fixed` : il faudrait alors répliquer à deux endroits chaque
   transformation du parent (l'entrebâillement notamment). Dans le flux,
   il suit tout seul.                                                       */
.project__hero {
  position: relative;
  height: 100vh;
  overflow: hidden;
  background: var(--paper-light);
  z-index: 0;
}

/* L'image remplit le hero. Elle est en absolu et non en flux : c'est ce qui
   lui donne les `100vh` du parent sans qu'elle ait à les redéclarer.

   ⚠️ Le hero ne porte plus que l'image. Titre et natures sont descendus dans
   le corps : l'image occupe l'écran seule, sans texte par-dessus.          */
.project__hero-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform-origin: center;
}

/* La trame du hero : un PNG répété (`pattern_print-invert.png`, 10×10) et une
   vignette sur les bords, PAR-DESSUS l'image. Réglage de la vignette :
   `--crt-vignette` dans `tokens.css` ; la force de la trame est dans le PNG.

   ⚠️ À L'ESSAI. Lignes de balayage en dégradé CSS et masque RVB ont été
   essayés puis retirés : un motif régulier fin se croise avec la grille de
   pixels de l'écran (échelle Windows à 125/150 %) et produit un moiré.

   ⚠️ Opacité simple, JAMAIS de `mix-blend-mode` : la fiche porte des
   `transform` (entrebâillement, about) qui éteindraient le blend (§4.1,
   piège n°1). Un aplat translucide, lui, survit à tout.

   ⚠️ Elle fond APRÈS le zoom : le délai (`--d-slow`) laisse le clone
   atterrir. Le clone n'en porte pas ; apparue avec le hero au relais, on la
   verrait surgir sur une image qui, une frame plus tôt, n'en avait pas. Sans
   zoom (lien profond), elle monte doucement — rien ne surgit non plus.

   Et elle fond À LA SORTIE : `is-leaving`, posée par `closeProject` et par la
   copie chassée de `swapProject`, joue le fondu inverse (§7.1 bis B — le même
   geste dans les deux sens). Rien ne s'efface brutalement.                  */
.project__hero::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, transparent 55%,
                    rgba(0, 0, 0, var(--crt-vignette)) 100%),
    url('/assets/pattern_print-invert.png') 0 0 / 10px 10px repeat;
  pointer-events: none;
  animation: trame-in var(--d-base) var(--e-out) var(--d-slow) both;
}

.project__hero.is-leaving::after {
  animation: trame-out var(--d-base) var(--e-out) both;
}

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

/* ==========================================================================
   LA PASTILLE DU HERO — « scroll for more », qui suit le curseur (§4.7)

   Le hero est une image nue depuis que le titre est descendu dans le corps :
   rien n'annonçait qu'il y a du contenu en dessous. La pastille le dit : elle
   est là dès que le curseur survole le hero, et revient chaque fois.

   ⚠️ Elle est CENTRÉE sur le curseur, contrairement à l'infobox de la
   landing qui se pose en bas à droite. D'où le `translate(-50%, -50%)`
   ci-dessous, et l'absence de clamp aux bords : la contraindre la
   décollerait du pointeur. Le `overflow: hidden` du hero la coupe
   proprement là où elle dépasse.
   ========================================================================== */

.hint {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;   /* au-dessus de l'image, dans le hero */

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  /* ⚠️ Le cercle a une taille FIXE : « réduire le padding » revient donc à
     resserrer ce qu'il contient, pas à rogner un bord. Deux réductions
     successives — `--sp-tight` (8px), puis 0.35em, puis 0.25em : le picto
     se rapproche du texte, et l'air se reporte sur le pourtour.          */
  gap: 0.25em;


  width: var(--hint-size);
  height: var(--hint-size);
  border-radius: 50%;

  /* ⚠️ VERT FLUO (08/10/2026) — `--hint-bg`. Un premier essai en accent vert
     avait été annulé (31/07/2026) : une pastille verte se posait sur des
     photographies dont on ne maîtrise pas la teinte, bien sur certaines,
     mal sur d'autres. Reprise ici sciemment : le fluo se détache d'une
     vidéo sombre sans anneau, et c'est l'unique repère de couleur de la
     fiche. À rejuger sur un hero vert ou très clair.

     Texte en `--ink` : le blanc ne se lit pas sur un vert aussi clair.   */
  background: var(--hint-bg);
  color: var(--ink);
  text-align: center;

  /* Elle suit le curseur, elle ne l'intercepte pas. */
  pointer-events: none;

  opacity: 0;
  transition: opacity var(--d-fast) var(--e-out);

  /* ⚠️ Le JS n'écrit QUE la translation du suivi. Le recentrage vit ici,
     dans une variable qu'il ne touche pas : deux `transform` sur le même
     élément s'écrasent, et le suivi partirait du coin haut-gauche.
     Même partage qu'entre `--peek-x` et `--push` (§4.6).                 */
  transform: translate(var(--hint-x, 0px), var(--hint-y, 0px))
             translate(-50%, -50%);
  will-change: transform;
}

.hint.is-on { opacity: 1; }

/* Le texte de la pastille — `.t-meta is-light` porte la voix (Outfit 300),
   comme le titre de l'infobox de la landing. Les deux repères qui suivent
   le curseur parlent la même langue.

   ⚠️ `color: inherit` : `.t-meta` pose `--ink`, invisible sur un aplat
   sombre. On reprend le blanc du parent.

   ⚠️ La taille et l'interlignage se règlent ICI. `.t-meta` ne porte pas
   d'échelle — ses emplois n'ont pas la même — et l'interlignage par défaut
   creuse un blanc entre deux lignes dans un disque étroit.              */
.hint__text {
  color: inherit;
  font-size: var(--fs-body);
  line-height: 1.15;
}

.hint__icon {
  width: var(--hint-icon);
  height: var(--hint-icon);
  /* `currentColor` sur le tracé : le picto suit le texte, une seule
     couleur à régler.                                                    */
  color: inherit;
}

/* Le hero RESTE EN COULEUR après l'ouverture — pas de recul en fond gris.
   L'image est le sujet, pas la toile.                                      */

/* ==========================================================================
   LAYOUT DE LA FICHE — tout est posé sur la grille de 12 carrés.
   Les positions sont exprimées en unités `--u` : le même socle géométrique
   que la landing (CADRAGE §4.1).
   ========================================================================== */

/* ⚠️ PAS de `display: grid` ici — testé et confirmé : une grille contraint
   la galerie dans une cellule dont elle calcule la hauteur, et le `sticky`
   interne ne peut plus coller. Flux normal obligatoire (CADRAGE §4.3 bis).

   La grille avait été posée pour rendre les crédits sticky — besoin
   abandonné depuis : ils sont dans le flux. Ne pas la réintroduire.        */
.project__body {
  position: relative;
  z-index: 1;   /* au-dessus du hero, qui est le fond */
  /* Le padding haut est ICI, pas sur le texte : le fond blanc démarre donc
     avant lui, au lieu d'être poussé avec.

     Trois mesures, trois rôles : `--v-body` compose l'écran (l'air sous le
     hero suit la hauteur disponible), `--page-pad` tient l'axe horizontal,
     et `--sp-end` ferme la page — il valait `20vh` en dur.                */
  padding: var(--v-body) var(--page-pad) var(--sp-end);
  background: var(--paper-light);
}


/* — Titre et natures — en tête du corps, sur l'axe du texte.

   ⚠️ Ils ont quitté le hero. Ils y étaient posés en blanc par-dessus
   l'image ; ils ouvrent désormais le bloc blanc, avec l'intro et la
   description. L'image occupe l'écran seule, le texte commence après.

   Deux conséquences qui se lisent dans les règles ci-dessous : la couleur
   repasse en `--ink` (on est sur blanc), et le retrait n'est plus à
   composer avec `--page-pad` — le corps le porte déjà.                     */
.project__head {
  /* Même axe que l'intro, la description et les crédits : `--text-indent`
     seul. Le `--page-pad` du corps s'y ajoute par héritage du padding, il
     ne se redéclare pas ici — sinon le titre décrocherait du texte qu'il
     ouvre, du double du retrait (§7.1 bis A : une valeur, un endroit).    */
  margin-left: var(--text-indent);

  /* Le titre déborde la colonne de texte et s'arrête à `--title-end` : le
     contraste d'échelle entre le nom du projet et le récit est ce qui fait
     l'ouverture du bloc.

     ⚠️ `--title-end` marque une POSITION sur l'écran, pas une largeur : la
     largeur du bloc est donc ce qui reste une fois déduit tout ce qui le
     sépare déjà du bord gauche — le padding du corps et le retrait.       */
  width: calc(var(--title-end) - var(--page-pad) - var(--text-indent));

  /* ⚠️ PLUS D'ÉCART SOUS LE BLOC TITRE — retiré le 31/07/2026.

     Il valait `--sp-section` (128px), et avant cela `--v-title` (45vh). Il
     détachait le titre du RÉCIT, qui le suivait directement.

     Depuis que le client et le rôle sont remontés entre les deux, ce bloc
     n'est plus le voisin du récit : les 128px tombaient entre les natures
     et le client, à l'intérieur de ce qui devait se lire d'un seul tenant.
     C'est `.project__meta` qui porte désormais l'air avant le récit.

     ⚠️ Le cas est le même que celui du `--v-title` : un écart juste tant
     que l'ordre des blocs ne change pas. **Déplacer un bloc oblige à
     relire les marges de ses anciens voisins.**                          */
}

/* ⚠️ `.project__title` N'A PLUS DE RÈGLE — `.t-hero` porte tout.

   Elle redéclarait `font-size: calc(var(--fs-hero))` et `color: var(--ink)`,
   soit exactement ce que `.t-hero` déclare déjà et que le markup pose à côté
   d'elle. Le `calc()` autour d'une variable seule n'ajoutait rien non plus.

   La classe reste dans le markup : le JS la sélectionne pour le volet du
   titre (`revealOnScroll`). Elle sert donc de crochet, pas de style — c'est
   la séparation que le §7.1 bis A décrit.                                 */

/* Volet typographique — chaque ligne est masquée par son cache et remonte
   depuis dessous (`splitLines` / `revealLines` dans motion.js).            */
.line {
  display: block;
  overflow: hidden;
}
.line__in {
  display: block;
  will-change: transform;
}

/* — Description — 4 carrés depuis la gauche et 4 de marge à droite,
     donc 4 carrés de large. 2 carrés depuis le haut.                        */

/* Aligné à gauche SOUS LE TITRE — même retrait d'un carré.
   L'écart au-dessus est porté par le padding du corps, pour que le fond
   blanc démarre avant le texte.                                            */
.project__content {
  margin-left: var(--text-indent);
  width: calc(6 * var(--u));
}

.project__intro,
.project__text p {
  font-size: var(--fs-read);
  line-height: 1.35;
}
/* L'intro se détache du récit — c'est l'accroche, pas le premier
   paragraphe. Les paragraphes entre eux gardent l'écart de lecture.       */
.project__intro   { margin-bottom: var(--sp-block); }
.project__text p  { margin-bottom: var(--sp-close); }

/* Le dernier paragraphe ne pousse pas les crédits : l'écart entre les deux
   blocs est porté par les crédits eux-mêmes, en un seul endroit.          */
.project__text p:last-child { margin-bottom: 0; }

/* — Crédits — 1 carré depuis la gauche, dans le flux (plus sticky).
     Deux styles : le label en capitales, la valeur en casse normale.        */
/* Client, rôle et lien externe — sous les natures, avant le récit.

   ⚠️ Ils étaient SOUS le récit, sous les libellés « Client » et « Role » en
   petits caractères. Le §4.3 faisait déjà ce reproche au rôle : « traité
   comme une mention légale ». Ils qualifient le projet au même titre que
   les natures — leur place est en tête, avec elles.

   Les libellés ont disparu : « Air Brussels » et « Design system, UI » se
   lisent pour ce qu'ils sont, sans qu'on ait à les nommer.               */
.project__meta {
  margin-left: var(--text-indent);

  /* ⚠️ AUCUN écart au-dessus. Les natures, le client et le rôle forment un
     seul bloc de qualification : les séparer les faisait lire comme deux
     temps distincts, alors qu'ils répondent à la même question — ce que ce
     projet EST et pour qui.

     L'air qui compte est celui qui les détache du titre (`--sp-part`, porté
     par les natures) et celui qui ouvre le récit (ci-dessous).           */
  margin-bottom: var(--sp-section);
}
/* Le label colle à sa valeur, la paire se détache de la suivante.

   ⚠️ Ces deux règles ont porté un temps le nom `.meta`, partagé avec la
   liste de définitions de l'about. Celle-ci a été retirée — le contact est
   devenu une commande, le reste du texte est passé dans le récit. La
   classe n'ayant plus qu'un emploi, elle revient à son composant : une
   classe partagée par un seul usager n'est plus une factorisation, c'est
   un nom de plus (§7.1 bis A).                                          */
/* ⚠️ Les règles `dt`/`dd` ont disparu avec la <dl> : sans libellés, il n'y
   avait plus de paires terme/définition à décrire. Client et rôle sont des
   `<p class="project__intro">` — ils prennent donc la taille de la
   description longue, sans une règle de plus.

   Seul l'écart se resserre : `--sp-block` (32px) sépare deux blocs, or ces
   deux lignes n'en font qu'un — le client et le rôle se lisent ensemble.  */
/* Le client et le rôle — un cran SOUS la description longue.

   ⚠️ Classe propre plutôt que `.project__intro` dont on écraserait la
   taille : celle-ci ne porte que `font-size` et `line-height`, il n'y avait
   rien à hériter. La partager pour en annuler la moitié aurait fait mentir
   son nom (§7.1 bis A).

   `--fs-value` (18px) contre les 30px du récit : ils qualifient, ils ne
   racontent pas. À égalité, ils pèseraient autant que ce qu'ils précèdent. */
.project__cred {
  font-size: var(--fs-value);
  line-height: 1.35;
  margin-bottom: var(--sp-tight);
}

/* Les natures — sous le titre, en tête du corps.

   ⚠️ ELLES SONT TRAITÉES COMME LE PAS À PAS, et plus comme un satellite du
   titre. Même voix (`.t-meta`), même taille (`--fs-value`), même casse,
   même surlignage au survol.

   Ce sont deux repères de parcours de même nature : une nature ramène à la
   landing filtrée, un pas mène au projet voisin (§4.3). Rien ne justifiait
   qu'ils se traitent différemment — l'écart venait de l'époque où le titre
   était en `--fs-hero` et où il fallait s'en distinguer.                  */
.project__natures {
  display: flex;
  flex-wrap: wrap;
  /* Gouttière horizontale nulle : c'est le point médian (`::before`
     ci-dessous) qui porte l'écart, sinon les deux se cumulent.            */
  gap: var(--sp-tight) 0;

  /* ⚠️ Un écart de LECTURE sous les natures, pas de section.

     Elles étaient collées au client : les deux blocs se lisent ensemble
     (qui est ce projet, pour qui), mais les coller les faisait se toucher.
     `--sp-close` (16px) est l'écart entre deux lignes d'un même propos —
     le cran au-dessus (`--sp-block`, 32px) rouvrirait la séparation qu'on
     vient de fermer.                                                     */
  margin-bottom: var(--sp-close);

  /* ⚠️ 64px sous le titre — elles ne lui sont plus collées.

     Elles l'étaient à `--sp-tight` (8px) au motif qu'elles le qualifient.
     Le titre ayant perdu un palier de taille (`--fs-display`), le
     rapprochement les faisait lire comme une seconde ligne du titre plutôt
     que comme une information distincte.                                 */
  margin-top: var(--sp-part);

  /* La voix vient de `.t-meta`. Restent la taille, propre à cet emploi, et
     LES CAPITALES — celles-ci sont une décision du §4.3 : les natures les
     portent, le titre au-dessus est en casse normale. Deux blocs en
     capitales l'un sur l'autre s'annulent.

     ⚠️ Même taille ET même casse que le pas à pas : les deux sont des
     repères de parcours dans la même voix `.t-meta`, ils n'ont aucune
     raison de se traiter différemment. Les natures étaient à `--fs-title`
     et en capitales, hérité de l'époque où le titre était en `--fs-hero` :
     il fallait alors les en distinguer. Le titre ayant baissé d'un palier,
     l'écart n'a plus lieu d'être.
     ⚠️ Si l'un des deux bouge, l'autre doit suivre.                      */
  font-size: var(--fs-value);

  /* ⚠️ La capitale initiale est POSÉE ICI, pas saisie dans l'admin.

     Les natures sont stockées en minuscules (`catalogue`, `interface`) et
     servent aussi de slug d'URL : les capitaliser en base obligerait à
     normaliser à chaque lecture, et le filtre `?nature=` cesserait de
     correspondre. La casse est un fait d'affichage, elle reste au CSS.   */
  text-transform: capitalize;
}

.project__nature {
  /* ⚠️ PAS `font: inherit` : la propriété raccourcie `font` REMET À ZÉRO
     tout ce qu'elle ne mentionne pas — dont `text-transform`. Les capitales
     du conteneur étaient donc annulées ici même. On hérite propriété par
     propriété.                                                            */
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  text-transform: inherit;

  /* Couleur, curseur et survol viennent de `.mark` — le markup pose les
     deux classes, et le texte va dans un `.mark__in` pour que le
     surlignage se découpe ligne par ligne.                               */
}


/* Séparateur — un point médian entre les natures, jamais après la dernière.
   Le §3 les écrit « atlas · interface · image ».                          */
.project__nature + .project__nature::before {
  content: '·';
  margin: 0 0.5em;
  opacity: 0.5;
}

/* Le lien externe — `.mark` porte le survol, ici seule la place.
   ⚠️ Le soulignement est retiré : il signalait le lien par la couleur
   d'accent, or celle-ci ne s'applique plus au texte. Le surlignage au
   survol suffit, et le libellé (« View the project ↗ ») dit le reste.  */
.project__link {
  display: inline-block;
  margin-top: var(--sp-block);

  /* ⚠️ MÊME TAILLE ET MÊME CASSE QUE LES NATURES. `.t-meta` porte déjà la
     voix (famille, graisse, interlignage) mais ni l'une ni l'autre : elles
     restent à la charge de l'emploi, les deux usages de `.t-meta` n'ayant
     pas la même échelle (§ règle de `.t-meta` dans `type.css`).

     Les natures, le pas à pas et ce lien sont trois commandes de même
     nature — elles se ressemblent jusqu'au bout.
     ⚠️ Si `.project__natures` change de taille, celui-ci doit suivre.

     ⚠️ PAS de `capitalize` ici, contrairement aux natures. Celles-ci
     viennent de la base en minuscules et sont des mots seuls : la règle
     leur pose l'unique majuscule qui manque. Ce libellé est une PHRASE
     écrite en dur — `capitalize` en ferait « View The Project ». La casse
     d'un texte rédigé se décide en l'écrivant, pas en CSS.               */
  font-size: var(--fs-value);
}

/* ==========================================================================
   GALERIE — médias empilés verticalement (CADRAGE §4.3, révisé)

   Le ruban horizontal épingle-scroll a été retiré : au-delà de 7-8 médias
   la course devenait trop longue pour le geste de scroll qui la pilote.
   Ce qui suit reprend la mécanique déjà en place au doigt depuis 600px
   (§4.5) — un seul comportement, toutes largeurs.
   ========================================================================== */

.project__gallery {
  background: var(--paper-light);
  margin-top: calc(var(--sp-section) + var(--gallery-gap));
  margin-bottom: calc(var(--sp-section) + var(--gallery-gap));
  /* Bord à bord : elle s'affranchit du padding du corps. */
  margin-left: calc(-1 * var(--page-pad));
  margin-right: calc(-1 * var(--page-pad));

  /* La galerie a ANNULÉ le `--page-pad` du corps par marge négative pour
     aller bord à bord (ci-dessus) — si son padding n'ajoutait que
     `--gallery-indent`, elle repartirait de zéro, pas de `--page-pad`.
     Doit donc RECOMPOSER les deux pour retrouver un vrai retrait depuis le
     bord de l'écran, pas juste depuis où `--page-pad` l'a laissée.       */
  padding: 0 calc(var(--page-pad) + var(--gallery-indent));

  display: flex;
  flex-flow: row wrap;
  justify-content: center;
  align-items: flex-start;
  row-gap: var(--gallery-row);
  column-gap: var(--gallery-stack);
}

/* ⚠️ LE `<figure>` PORTE `aspect-ratio` EN LIGNE (`project.js`) — sa hauteur
   est donc `largeur / ratio`, MÉCANIQUEMENT. Une image très verticale à
   pleine largeur de colonne peut ainsi dépasser l'écran : le plafond doit
   donc s'appliquer ICI, sur le conteneur dont la hauteur est réellement
   calculée — pas sur l'`<img>` à l'intérieur.
   La première tentative posait `max-height` sur l'`<img>`, en laissant le
   `<figure>` grandir selon son `aspect-ratio` sans plafond : l'image se
   limitait bien, mais le CADRE restait immense et un vide se creusait sous
   elle. Le plafond ne sert à rien s'il n'est pas posé sur ce qui pousse la
   mise en page.                                                            */
.project__shot {
  position: relative;
  /* `flex-basis: 100%` force toujours un retour à la ligne — la LARGEUR
     RÉELLE (`width`) est plus étroite que le conteneur dès que son padding
     dépasse 0, et flex ne la regarde pas pour décider si un item tient sur
     la ligne : sans la base à 100%, un 'full' se retrouvait collé au média
     suivant dès qu'il restait assez de place à côté de lui sur un grand
     écran — deux 'full' pris pour une paire de 'half'.                    */
  flex: 0 0 100%;
  width: 100%;
  max-height: 90vh;
  margin: 0;
}

/* Demi-largeur — compose deux OU TROIS médias côte à côte (CADRAGE §4.3
   bis). Pas une largeur fixe à 50% : `flex-grow` se partage la ligne avec
   ses voisins 'half', quel que soit leur nombre.

   - Isolé (rien derrière, ou un 'full' qui suit) → `flex-grow` n'a
     personne avec qui se partager la ligne : il s'étire seul, pleine
     largeur. Plus de vide à combler, contrairement à l'ancien site.
   - Deux à la suite → chacun grandit à parts égales, ~50% chacun.
   - Trois à la suite → `flex-grow` les étire tous les trois à parts égales
     sur la ligne.

     ⚠️ LA BASE DOIT TENIR LE PIRE CAS (trois), PAS LE CAS COURANT (deux).
     `flex-wrap` décide du retour à la ligne AVANT que `flex-grow` ne
     s'applique : une base calculée pour une moitié (50%) ne laisse jamais
     de place à un troisième, qui retombe seul sur la ligne suivante à
     pleine largeur — `flex-grow` n'a alors plus que lui à étirer. La base
     part donc d'un TIERS : large assez pour que deux `half` la dépassent
     tous les deux et remplissent la ligne (`grow` les pousse à 50%), mais
     assez étroite pour qu'un troisième y tienne encore.                   */
.project__shot--half {
  flex: 1 1 calc((100% - 2 * var(--gallery-stack)) / 3);
  width: auto;
  /* Plafond propre, plus bas que le 'full' (90vh) : à largeur moitié, une
     image verticale en 'half' touchait encore ce plafond et sortait aussi
     haute qu'une pleine largeur — gabarit disproportionné pour la moitié
     de la place.                                                          */
  max-height: 45vh;
}

.project__shot img,
.project__shot video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* — Pas à pas — précédent et suivant ————————————————————————————————————
   Deux boutons aux extrémités, calés sur le même axe que le texte de la
   fiche : `--text-indent` à gauche, son symétrique à droite.

   La barre qui les surmontait a été retirée : le blanc de la fiche et
   l'écart suffisent à les détacher. Une ligne de séparation, sur un site
   sans aucune décoration (§6), était le seul trait purement graphique.   */

.project__nav {
  display: flex;
  justify-content: space-between;
  padding: 0 var(--text-indent);

  /* ⚠️ PAS de `margin-top` ici — c'est la GALERIE qui porte l'écart, par son
     `margin-bottom`. Les deux étant adjacentes dans un flux normal, leurs
     marges verticales FUSIONNENT : déclarer les deux ne donnerait pas leur
     somme mais la plus grande, et la seconde valeur ne réglerait rien tout
     en donnant l'illusion du contraire.

     Un seul bloc gouverne un seuil — ici la galerie, qui tient ses deux
     bords (voir son commentaire).                                         */

  /* ⚠️ CET ÉCART N'EST PAS UN PALIER DE L'ÉCHELLE, et c'est pour ça qu'il a
     son propre token.

     `space-between` colle déjà chaque bouton à son bord : au repos, le gap
     ne fait rien. Il n'agit QUE lorsque deux titres longs se rejoignent au
     centre — « Waterloo Asset Management » face à « Blow Architects » — et
     là il est le seul rempart contre deux blocs qui se touchent.

     Ce n'est donc pas un rythme de lecture mais une DISTANCE MINIMALE DE
     SÉPARATION, horizontale, qui dépend de la longueur des titres et non
     de la composition verticale. Le régler depuis `--sp-*` reviendrait à
     modifier l'espacement du texte pour corriger un chevauchement de
     boutons.                                                              */
  gap: var(--nav-split);
}

/* Le bouton empile son libellé et le titre visé. */
/* ⚠️ `.step` ET NON `.project__step` — l'about porte la même commande.

   Elle n'a jamais rien eu de propre à la fiche : un libellé au-dessus d'une
   valeur, empilés, qui se colore au survol. Le contact de l'about est
   exactement ça (§9 q.3), et le nommer d'après la fiche aurait obligé à
   recopier la règle sous un autre nom.

   ⚠️ Elle porte son propre survol et ne se combine PAS à `.act` : celle-ci
   fond en opacité, `.step` passe à l'accent (voir plus bas). Deux natures
   de commande, deux traitements.                                         */
/* Seul l'empilement libellé/titre — couleur, curseur et survol viennent de
   `.mark`, que le markup pose à côté.                                     */
.step {
  display: flex;
  flex-direction: column;
  gap: var(--sp-tight);
}

/* ⚠️ Le survol du pas à pas vit dans `.mark` — le markup pose les deux
   classes. `.step` ne garde que l'empilement libellé/titre.

   Deux traitements pour deux natures : `.act` fond en opacité (un mot dans
   un texte : une nature, une fermeture), `.mark` remplit le fond (une
   destination — elle s'allume). Un élément ne porte jamais les deux.     */

/* Le libellé — `.t-label` porte la typo, ici seule la couleur suit le
   survol du bouton plutôt que de rester fixe.                             */
.step__label { color: inherit; opacity: 0.5; }

/* Le titre visé — même famille que les natures, un palier en dessous :
   ce sont des repères de parcours, ils ne concurrencent pas le nom du
   projet.                                                                 */
/* La voix vient de `.t-meta`, seule la taille est propre à cet emploi.
   ⚠️ Plus de capitales : le nom d'un projet s'écrit comme il s'écrit — le
   titre de fiche est déjà en casse normale (§4.3).                       */
.step__title { font-size: var(--fs-value); }

/* Chacun tiré vers son bord — le sens de lecture porte la direction. */
.step--back { align-items: flex-start; text-align: left; }
.step--next { align-items: flex-end; text-align: right; margin-left: auto; }

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */

@media (max-width: 900px) {
  .stage { padding-top: 34vh; }

  /* ⚠️ On ajuste l'ÉCHELLE, pas `--logo-size` : changer la taille de la
     boîte relancerait une animation de mise en page et ferait resauter le
     logo (voir son bloc plus haut). La boîte reste fixe, le facteur seul
     varie. `--u` valant ici 1/8ᵉ d'écran, 0.42 redonne à peu près la
     taille visée à l'origine (~44-56 px).                                 */
  :root { --logo-shrink: 0.42; }

  /* ========================================================================
     LA FICHE ENTRE 600 ET 900px — la plage qu'on ne regarde jamais.

     Ce bloc ne portait RIEN pour la fiche : entre les deux seuils, elle
     appliquait encore la mise en page desktop. C'est plus insidieux que le
     mobile, où le défaut sautait aux yeux — ici tout « fonctionne », c'est
     simplement mal composé, et personne n'ouvre cette largeur.

     ⚠️ La colonne de texte faisait 6 carrés, soit **exactement la moitié de
     la largeur utile** à toutes ces tailles. À 620px, la description tenait
     **20 caractères par ligne** — moins bien qu'un téléphone. Le retrait de
     2 carrés (98 à 144px) mangeait le reste.

     ⚠️ ET LA FICHE LIT LE `--u` DE `:root`, PAS CELUI DE `.work`.
     `grid.css` redéfinit `--u` sur 8 colonnes ici, mais localement à la
     landing. La fiche continue donc de compter en douzièmes — ce qui est
     cohérent (son axe est celui du texte, pas de la grille des vignettes)
     mais se vérifie, sinon les carrés ne valent pas la même chose des deux
     côtés.
     ======================================================================== */

  /* Le retrait RESTE à 2 carrés — l'axe du texte est une décision de
     composition (§4.3), pas une variable d'ajustement. C'est la colonne qui
     s'élargit pour occuper ce qui reste.                                   */
  .project__content { width: calc(10 * var(--u)); }

  /* Le titre suit : borné à 70vw, il s'arrêtait avant la colonne de texte
     qui vient de s'élargir — le bloc le plus large aurait été le corps, pas
     le nom du projet.                                                      */
  .project__head { width: auto; }

}

@media (max-width: 600px) {
  .stage { padding-top: 28vh; }

  /* ⚠️ LE LOGO DOUBLE — 4 carrés au lieu de 2, ICI SEULEMENT.

     Sur un écran de 375px, `--u` vaut ~28px : le logo à 2 carrés faisait
     56px de côté, trop peu pour tenir l'écran comme il le fait au desktop.
     C'est l'ancre du site (§4.1) — sa présence ne peut pas se réduire avec
     la fenêtre.

     ⚠️ Exception à la règle du bloc 900px ci-dessus (« on ajuste l'échelle,
     jamais la boîte »). Elle vise les REDIMENSIONNEMENTS en cours de
     session, où changer la boîte relance une animation de mise en page et
     fait resauter le logo. Ici on franchit un seuil qui reconstruit déjà
     toute la grille (`MOBILE` dans `grid.js`) : il n'y a aucune transition
     en vol à perturber.

     `--logo-shift` suit tout seul — il se déduit de `--logo-size`, il n'a
     pas à être répété.                                                    */
  :root {
    --logo-size:   calc(4 * var(--u) + 3 * var(--grid-gap));
    --logo-shrink: 0.5;
  }

  /* ⚠️ LA DÉSATURATION VAUT AUSSI AU DOIGT — la règle qui l'annulait ici a
     été RETIRÉE.

     Elle posait `grayscale(0)` sur toutes les vignettes, au motif que sans
     survol la couleur n'aurait rien marqué. C'était vrai tant que le tactile
     n'existait pas ; **depuis que l'item central se qualifie de lui-même
     (§4.5), elle annulait précisément ce qui le distingue** — tout en
     couleur, donc rien au premier plan.

     La règle fondatrice du §4.1 vaut donc partout : le corpus est gris, la
     couleur marque le premier plan. Ce qui change entre desktop et mobile
     n'est pas l'effet, c'est ce qui désigne l'item — le curseur ou le centre
     de l'écran.

     Le flou des voisins, lui, ne suit PAS : il vient de
     `.work.is-hovering .item:not(.is-hot)`, et `is-hovering` est bien posé
     par `setHot` au doigt comme au curseur. Voir ci-dessous.               */

  /* ⚠️ Pas de flou au doigt. Il coûte cher au GPU (§7.1 bis D) et serait
     recalculé à CHAQUE frame de défilement, l'item central changeant sans
     cesse — alors qu'au curseur il ne bouge qu'au survol. Sur mobile, la
     seule opacité suffit à faire reculer les voisins.                      */
  .work.is-hovering .item:not(.is-hot) .item__media {
    filter: grayscale(1) opacity(0.45);
  }

  /* ========================================================================
     LA FICHE — L'ÉCRAN DEVIENT LA COLONNE.

     Rien de ce bloc n'existait : tous les replis visaient la landing. Sur
     375px, les valeurs du desktop donnaient une fiche inutilisable — colonne
     de texte à 172px sur 286px disponibles, boutons du pas à pas à 86px.

     ⚠️ LE PRINCIPE : au desktop, le texte occupe une colonne étroite DANS un
     écran large — c'est ce qui compose la page, et `--text-indent` place cette
     colonne sur son axe. Sur mobile, **l'écran EST la colonne** : il n'y a
     plus de page autour, donc plus d'axe à tenir. Le retrait et les largeurs
     bornées n'ont plus d'objet, ils ne font que rétrécir.

     Ce n'est donc pas « la même mise en page en plus petit » : c'est la même
     décision — donner au texte sa largeur de lecture — appliquée à un
     contenant qui a changé de nature.
     ======================================================================== */

  /* Le retrait tombe à zéro : le `--page-pad` du corps suffit à décoller le
     texte du bord. Deux tokens à annuler — `--text-indent` (titre, natures,
     description, crédits, pas à pas) et `--gallery-indent` (galerie
     seule) — tous deux inutiles ici : plus de colonne large à tenir
     distincte de la colonne de lecture, l'écran entier remplace les deux
     (§4.3, §4.5).                                                        */

     ⚠️ LES GRANDS ÉCARTS SE RESSERRENT — et c'est la limite du choix de les
     poser en `rem`.

     Ce choix reste juste : un rapport typographique ne doit pas dépendre de
     la fenêtre (STRUCTURE §5). Mais il vaut pour les écarts DANS le texte —
     entre paragraphes, entre un titre et son intro. Les deux plus grands ne
     rythment plus un texte, ils **séparent des sections**, et une section se
     mesure à l'écran qui la contient.

     Sur un iPhone SE (667px de haut), `--sp-section` occupait 19 % de la
     hauteur, et la galerie — qui le porte deux fois, plus `--gallery-gap` —
     réservait **la moitié d'un écran au blanc**.

     ⚠️ Ils restent en `rem`, pas en `vh` : on change la VALEUR, pas l'unité.
     Passer en `vh` réintroduirait exactement le défaut que le §5 écarte —
     une respiration qui varie avec la fenêtre au lieu d'être composée.
     `--sp-tight` à `--sp-block` ne bougent pas : ceux-là rythment bien du
     texte, et leur rapport à la lecture est le même partout.              */
  :root {
    --text-indent: 0px;
    --gallery-indent: 0px;
    --sp-section: 3.5rem;   /* 56px — valait 128px */
    /* ⚠️ Ce qui FERME la page — remonté à 80px après essai à l'écran.
       À 40px, les boutons du pas à pas touchaient presque le bas : c'est le
       dernier bloc de la fiche, il lui faut de l'air derrière lui pour que
       le scroll s'arrête franchement au lieu de buter.                    */
    --sp-end:       5rem;   /* 80px — valait 96px au desktop */
    --gallery-gap: 16px;    /*        valait 40px  */

    /* La séparation minimale entre les deux pas — voir `.project__nav`.
       Valait 2 carrés, soit 57px pris sur les 343px utiles.               */
    --nav-split: var(--sp-block);

    /* ⚠️ L'about prend TOUTE la largeur au doigt — à 50vw il ne resterait
       que ~127px de lecture, soit 14 caractères par ligne.

       ⚠️ MAIS LE PUSH RESTE À 50vw. C'est ici que les deux valeurs se
       séparent : pousser le site de 100vw le sortirait entièrement du
       champ, pour un résultat que la moitié suffit à obtenir — le volet
       couvre de toute façon ce qui reste. Un mouvement deux fois plus long
       sans rien de plus à voir.                                          */
    --about-w: 100vw;
    --push-d:  50vw;
  }

  /* Le titre n'est plus borné : `--title-end` (70vw) le coupait à 189px de
     large pour rien. Le contraste d'échelle titre/texte qu'il produisait
     n'existe plus ici, puisque les deux occupent la même largeur.          */
  .project__head { width: auto; }

  /* La colonne de texte valait `6 * --u`, soit la moitié de l'écran.       */
  .project__content { width: auto; }

  /* Le pas à pas RESTE CÔTE À CÔTE — un pas à gauche, un pas à droite.

     C'est le sens de lecture qui porte la direction : empilés, « Previous »
     au-dessus de « Next » ne dit plus rien de l'axe qu'on parcourt, et il
     faut lire les libellés pour savoir où l'on va. Deux boutons opposés le
     disent sans un mot.

     ⚠️ Ce qui rendait la ligne intenable, c'était la TAILLE, pas la
     disposition : à `--fs-value` (18px) deux titres se rejoignaient au
     centre. Le palier en dessous — `--fs-body`, 14px — leur redonne la
     place. On corrige donc la cause, pas la mise en page.

     `--text-indent` valant 0 ici, chaque bouton part de son bord ; le
     `--nav-split` entre eux garde son rôle de séparation minimale.        */
  .step__title { font-size: var(--fs-body); }

  /* ⚠️ LA DESCRIPTION DESCEND À `--fs-value` (18px) — `--fs-read` (30px)
     est inutilisable ici.

     Sur 343px de large, 30px ne tient que **23 caractères par ligne** : le
     texte le plus long du site casserait tous les trois ou quatre mots. À
     18px il en tient ~38, ce qui est la fourchette de lecture mobile.

     Deux paliers pour un même bloc, chacun juste dans son registre —
     `--fs-read` pour la lecture longue en plein écran, `--fs-value` pour la
     même lecture resserrée. Pas un repli approximatif du premier.        */
  .project__intro,
  .project__text p { font-size: var(--fs-value); }

  /* ⚠️ La pastille du hero disparaît au doigt (§4.7). Elle suit un curseur
     qui n'existe pas, et le picto qu'elle porte dessine une SOURIS — un
     objet que le visiteur n'a pas sous la main. Le scroll, lui, est le
     geste le plus évident du tactile : il n'a besoin d'aucune invite.   */
  .hint { display: none; }

  /* La galerie reste empilée au doigt, toutes tailles confondues — décision
     déjà tranchée (CADRAGE §4.5) : le corpus est tout en paysage et deux
     ou trois médias côte à côte n'y tiendraient pas lisiblement sur 375px.
     'half' ne vaut que sur grand écran, où l'axe horizontal a de la place.

     ⚠️ `flex-basis` DOIT être annulé, pas seulement `width` — le raccourci
     `flex: 1 1 calc(...)` du desktop fixe une base explicite qui prime sur
     `width` dans la résolution flex. Sans le forcer à `100%` ici, le
     'half' continuait de se partager la ligne avec son voisin au lieu de
     revenir pleine largeur comme le 'full'.                              */
  .project__shot--half {
    flex-basis: 100%;
    width: 100%;
    max-height: 90vh;
  }

  /* Le bandeau cookies empile son texte et ses actions : côte à côte, le
     texte se serrait à moins de 20 caractères par ligne avant les boutons. */
  .consent {
    flex-direction: column;
    align-items: stretch;
  }
}
