/*
  La police n'est plus appelée d'ici. Un `@import` en tête de feuille n'est
  découvert qu'**après** l'analyse de celle-ci : mesuré, la requête partait à
  13 ms quand le CSS s'achevait à 12 — deux allers-retours au lieu d'un, et les
  deux `preconnect` du <head> ne servaient à rien, la requête arrivant trop
  tard pour en profiter. Le `<link>` est désormais dans le <head>, avant la
  feuille, et les deux partent ensemble.
*/

/* ==========================================================================
   ALEXANDRE VUILLAUME — Feuille de style
   Palette « Champagne Chic » en clair, Jost en graisses fines.

   Les scènes de l'accueil sont pilotées par une seule variable par section :
   --p, entre 0 et 1, calculée par le script à partir de la seule position à
   l'écran. Toutes les sous-plages en découlent en CSS pur :

       clamp(0, calc((var(--p) - DÉBUT) / DURÉE), 1)

   Pour régler une animation, il suffit de changer DÉBUT et DURÉE ici. Aucun
   JavaScript à toucher.

   Deux variables locales reviennent partout, toujours dans le même ordre :

       --t  la progression sur la plage voulue, ramenée entre 0 et 1
       --e  la même progression, adoucie aux deux bouts : t²(3 - 2t)

   Cette courbe démarre doucement, accélère au milieu, puis se pose. Une
   sortie cubique a été essayée avant elle : elle faisait 87 % du trajet dans
   la première moitié de la course, et les images semblaient bondir. Ici le
   trajet suit le geste — à mi-course l'image est à mi-chemin.

   Ce n'est pas de l'inertie : --p reste une fonction pure de la position de
   défilement. On s'arrête, tout se fige.
   ========================================================================== */

/* --------------------------------------------------------------- 1. Jetons */

:root {
  --paper:   #FAF7F2;
  --beige:   #E1D4C1;
  --cream:   #F4E1D2;

  /* Ratios vérifiés sur --paper : 15.4, 7.3, 5.7 */
  --ink:     #2B1C10;
  --earth:   #6E493A;
  --muted:   #6B6055;

  --on-photo: #FFFFFF;

  --line:      rgba(43, 28, 16, 0.14);
  --line-soft: rgba(43, 28, 16, 0.07);

  --sans: 'Jost', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --fs-title:   clamp(1.6rem, 5.2vw, 4.25rem);
  --fs-h1:      clamp(1.75rem, 4.2vw, 3.25rem);
  --fs-h2:      clamp(1.4rem, 3vw, 2.25rem);
  --fs-over:    clamp(0.95rem, 1.5vw, 1.35rem);
  --fs-lead:    clamp(1rem, 1.35vw, 1.2rem);
  --fs-body:    1rem;
  --fs-small:   0.875rem;
  --fs-micro:   0.7rem;

  --s1: 0.5rem;  --s2: 1rem;    --s3: 1.5rem;  --s4: 2.5rem;
  --s5: 4rem;    --s6: 6rem;    --s7: 9rem;    --s8: 12.5rem;

  --gutter: clamp(1.25rem, 5vw, 5rem);
  --measure: 34rem;

  /* Rayon des images qui montent, à leur arrivée. Il est franc : l'image se
     lit longuement comme une carte posée, et ne se carre que sur le dernier
     quart de sa montée. Un seul nombre pour tout l'accueil. */
  --rayon: 120px;

  /* Bandes floues fixes sur les bords de la fenêtre (accueil seulement).
     --cote et --bas donnent leur épaisseur, --voile la lumière qu'elles
     laissent entrer depuis le bord. Trois nombres, rien d'autre. */
  /* Les bandes latérales se voyaient comme deux traînées claires sur
     l'ouverture : elles se resserrent et le lait de lumière se réduit de plus
     de moitié. */
  --cote:  3.5vw;
  /* La bande du bas est passée de 13 à 11 vh : la ligne d'ouverture, qu'on a
     fait descendre, se retrouvait dessous et s'en trouvait floutée. */
  --bas:   11vh;
  /* Le lait de lumière que les bandes laissent entrer depuis le bord. */
  --voile: 0.05;

  --ease-out:  cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-expo: cubic-bezier(0.16, 1, 0.3, 1);
  /* L'équivalent en Bézier de `t²(3 − 2t)`, la courbe des scènes : à
     mi-durée, mi-distance. Les trois autres sont fortement sorties — 85 % du
     mouvement dans le premier tiers —, ce qui convient à un survol et donne
     à une montée l'impression de partir trop vite. */
  --ease-milieu: cubic-bezier(0.33, 0, 0.67, 1);

  /* Visibilité de la barre du haut. L'accueil la pilote au défilement ;
     partout ailleurs elle vaut 1 dès le départ. */
  --head: 1;
}

/* ------------------------------------------------------------------ 2. Base */

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

html {
  -webkit-text-size-adjust: 100%;
  /* Le débordement horizontal se coupe ici, jamais sur <body> : poser
     overflow sur le body en ferait le conteneur de défilement et figerait
     window.scrollY, dont dépend toute l'animation. */
  overflow-x: hidden;
}
@supports (overflow: clip) { html { overflow-x: clip; } }

/* Pas de rebond élastique en bout de course.

   **Le tirer-pour-rafraîchir est rendu**, le 8 septembre 2026, à la demande
   du client. Il avait été supprimé avec le rebond lui-même — `overscroll-
   behavior-y: none` —, ce qui traitait les deux d'un coup : le geste de
   rafraîchissement est en haut, le défaut était en bas.

   Le défaut : l'écran d'ouverture est collant et **voyage jusqu'au bas de la
   page** — mesuré, il reste épinglé en haut de la fenêtre après 6 041 px de
   défilement. Au rebond, le pied de page remonte avec le contenu, l'ouverture
   est retenue par sa contrainte, et c'est sa photographie qu'on découvre dans
   la bande ainsi ouverte. Le client l'avait signalé par capture d'écran.

   La cause est traitée là où elle se voit : `.foot` porte désormais une ombre
   pleine qui prolonge son encre d'une hauteur d'écran vers le bas. Le pied
   pèse `z-index: 20`, l'ouverture 0 : l'ombre passe devant elle. Elle ne
   change rien à la mise en page — une ombre n'occupe aucune place — et reste
   invisible tant qu'on ne dépasse pas le bas du document, le pied étant déjà
   de la même encre sur les six pages. */

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 300;
  /* Resserré depuis 1,7 : les paragraphes tenaient trop d'air et se lisaient
     comme des listes de lignes plutôt que comme des blocs. */
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.is-loading { overflow: hidden; }

img, picture, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

button {
  font: inherit; color: inherit; background: none;
  border: 0; cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--earth);
  outline-offset: 4px;
  border-radius: 2px;
}

::selection { background: var(--beige); color: var(--ink); }

.skip-link {
  position: absolute;
  top: -100px; left: var(--gutter);
  z-index: 300;
  padding: var(--s2) var(--s3);
  background: var(--ink); color: var(--paper);
  font-size: var(--fs-small);
  letter-spacing: 0.1em; text-transform: uppercase;
  transition: top 0.25s var(--ease-out);
}
.skip-link:focus { top: var(--s2); }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* ---------------------------------------------------------- 3. Typographie */

h1, h2, h3 {
  font-family: var(--sans);
  font-weight: 300;
  line-height: 1.2;
  margin: 0;
  text-wrap: balance;
}
h2 { font-size: var(--fs-h2); }

p { margin: 0 0 var(--s3); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

.lead { font-size: var(--fs-lead); line-height: 1.42; color: var(--earth); }

/* Le surtitre de page — « Services », « Contact » — tombait sous le menu
   déplié, qui occupe les 294 premiers pixels à gauche. Il passe à droite,
   où rien ne le recouvre, plutôt que de repousser toute la page vers le bas. */
.eyebrow.rise { text-align: right; }

.eyebrow {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 var(--s4);
}

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

/*
  L'accent plat, dessiné.

  Le glyphe Ē de Jost est inutilisable en grand, et c'est la mesure qui l'a
  montré : à 300 px, sa barre fait **106 px de large pour un E de 108** et
  flotte **0,117 em au-dessus des capitales**. L'accent grave, lui, fait la
  moitié de la lettre et se tient à 0,070 em. À 68 px sur l'ouverture, le
  macron devenait donc un filet de 24 px suspendu 8 px plus haut : un trait
  détaché, pas un accent. Sur téléphone, où le titre tombe à 25,6 px, l'écart
  n'était plus que de 3 px et le défaut ne se voyait pas.

  On reprend donc les proportions de l'accent grave, qui se lisait juste : la
  moitié de la largeur du E, posé à 0,05 em des capitales. La lettre redevient
  un E ordinaire ; la barre est un pseudo-élément, donc jamais lue à voix
  haute, et `aria-label="Esthète"` porte déjà l'orthographe réelle.

  Tout est en em : le trait suit le corps du texte, de la barre du haut à
  l'ouverture, sans une seule valeur à reprendre.
*/
.ma {
  /*
    `inline-block`, et c'est la seule forme dont la hauteur soit **définie par
    la spécification**.

    Le trait est posé en absolu : son repère vertical est la boîte de cette
    balise. Or la hauteur d'une boîte **en ligne** ne suit pas `line-height` —
    elle suit les métriques de la police, soit 1,445 em pour Jost (ascendante
    1,07 + descendante 0,375), et la ligne de base y tombe à 0,371 em du bas.
    Mesuré : en ligne, le trait passait 0,17 em **sous** le haut des capitales,
    c'est-à-dire au travers de la lettre.

    Pour un `inline-block`, la boîte vaut `line-height` — ici 1 em — et la
    ligne de base est celle de sa dernière ligne : demi-interligne
    (1 − 1,445)/2 = −0,2225, plus la descendante 0,375, soit **0,1525 em du
    bas**. Cette valeur-là ne dépend d'aucune interprétation.

    Contrepartie assumée : une frontière de bloc au milieu du mot. `innerText`
    rend « ESTH⏎E⏎TE » et le copier-coller suit. L'affichage, lui, est intact —
    la balise reste dans le flux de la ligne — et `aria-label="Esthète"` porte
    déjà l'orthographe pour les lecteurs d'écran.
  */
  display: inline-block;
  position: relative;
  line-height: 1;
  /*
    **Le retrait doit être annulé.** `.wordmark` porte `text-indent: 0.34em`,
    qui compense le blanc que l'interlettrage ajoute après la dernière lettre
    et garde le mot optiquement centré. Or un `inline-block` établit un
    conteneur de bloc : le retrait, hérité, s'applique **à l'intérieur** de
    cette balise et y pousse le E vers la droite — pendant que le trait, lui,
    reste calé sur le bord de la boîte. La lettre partait d'un côté, son accent
    de l'autre.

    Le mot dans son ensemble garde son retrait : il est posé sur `.wordmark`,
    et seule cette balise-ci s'en dispense.
  */
  text-indent: 0;
}
.ma::after {
  content: "";
  position: absolute;
  /* Le trait **couvre la lettre** : il reprend le bras du haut du E, dont
     l'encre commence à 0,0867 em du bord de la chasse — mesuré. Il s'y aligne
     donc bord à bord, et se trouve centré par la même occasion, le bras du
     haut occupant toute la largeur d'encre de la lettre. */
  left: 0.0867em;
  /* 0,1525 (la ligne de base depuis le bas de la boîte) + 0,70 (la hauteur
     des capitales) + 0,08 (le dégagement). Chaque terme est mesuré, et le
     résultat est vérifié par une sonde indépendante — un inline-block de
     hauteur nulle aligné sur la ligne de base — et non par la formule qui a
     servi à l'écrire. Le dégagement est passé de 0,05 à 0,08, 0,11, puis
     0,14 em : plus le trait s'est aminci et élargi, plus il lui a fallu d'air
     pour ne pas se lire comme une seconde barre du E. */
  bottom: 0.9925em;
  /* La largeur du bras du haut, mesurée au pixel : 0,3583 em — soit toute
     l'encre de la lettre, le bras du haut et celui du bas ayant la même
     étendue. *(Il a valu 0,18 em, la moitié, sur le modèle de l'accent
     grave : il ne couvrait alors que la moitié gauche du E.)* */
  width: 0.3583em;
  /* L'épaisseur des **bras horizontaux** du E, mesurée au pixel en graisse
     200 : 0,030 em, les trois bras au même chiffre. Un accent est un trait de
     la même plume que la lettre ; à 0,045 il pesait une fois et demie trop
     lourd et se voyait comme un ajout. Le fût vertical fait 0,0333 — c'est le
     bras qu'il faut suivre, le macron étant horizontal. */
  height: 0.03em;
  background: currentColor;
  pointer-events: none;
}

/* Le titre du site : capitales fines, très espacées, avec l'accent grave
   du français. L'attribut aria-label rend « Esthète » en minuscules aux
   lecteurs d'écran, qui épellent mal les capitales espacées. */
.wordmark {
  font-weight: 200;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  /* L'espacement ajoute du blanc après la dernière lettre : on le compense
     pour que le mot reste optiquement centré. */
  text-indent: 0.34em;
  margin: 0;
}

/* ============ Grand écran : l'échelle entière reprend sa croissance ====== */

/*
  Au-delà de 1600 px, **tous** les jetons étaient à leur borne haute : les
  corps depuis ~1300 px, la coquille à 96 rem, la gouttière à 5 rem. Une page
  de 2560 px recevait donc exactement la composition d'un écran de 1300, posée
  au milieu d'un cadre deux fois plus grand. C'est ce que le client a signalé
  sur la page services, et le défaut valait pour les six.

  On relève l'échelle **ici plutôt que page par page** : les six pages en
  vivent, y compris les écrans de l'accueil et le bandeau de clôture, et il n'y
  a qu'un endroit à reprendre si l'on veut la retoucher.

  Deux principes, les mêmes que sur services :

  — **Le seuil est à 100 rem**, non à 90 : 90 rem valent exactement 1440 px, la
    largeur sur laquelle tout a été réglé, et elle ne doit pas bouger.
  — **Chaque plancher vaut l'ancienne valeur plafonnée**, si bien que la
    formule coïncide avec la précédente à 1600 px. Aucun saut à la frontière :
    on peut redimensionner au travers sans rien voir bouger.

  Le texte d'affichage grandit d'environ un tiers, le texte courant d'un
  huitième seulement : un paragraphe qui grandit autant qu'un titre cesse de se
  lire comme du texte. La mesure suit le corps de près, si bien que le nombre
  de signes par ligne reste à peu près constant.
*/
@media (min-width: 100rem) {
  :root {
    --fs-title:   clamp(4.25rem, 1.958rem + 2.292vw, 5.625rem);   /*  68 →  90 */
    --fs-h1:      clamp(3.25rem, 1.479rem + 1.771vw, 4.3125rem);  /*  52 →  69 */
    --fs-h2:      clamp(2.25rem, 1.104rem + 1.146vw, 2.9375rem);  /*  36 →  47 */
    --fs-over:    clamp(1.35rem, 0.892rem + 0.458vw, 1.625rem);   /* 21,6 →  26 */
    --fs-lead:    clamp(1.2rem, 0.804rem + 0.396vw, 1.4375rem);   /* 19,2 →  23 */
    --fs-body:    clamp(1rem, 0.792rem + 0.208vw, 1.125rem);      /*  16 →  18 */
    --fs-small:   clamp(0.875rem, 0.719rem + 0.156vw, 0.969rem);  /*  14 → 15,5 */
    --fs-micro:   clamp(0.7rem, 0.565rem + 0.135vw, 0.781rem);    /* 11,2 → 12,5 */

    /* La gouttière et la mesure suivent, sans quoi le texte grandirait dans
       une colonne restée étroite. */
    --gutter:  clamp(5rem, 6.25vw - 1.25rem, 8.75rem);            /*  80 → 140 */
    --measure: clamp(34rem, 24rem + 10vw, 40rem);                 /* 544 → 640 */
  }
}

/* --------------------------------------------------------- 4. Mise en page */

.shell {
  width: 100%;
  max-width: 96rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* La coquille s'élargit avec l'écran : 1536 px de contenu dans une fenêtre de
   2560 laissaient 512 px de vide de chaque côté. Écrite **ici et non avec les
   jetons** : à poids égal c'est l'ordre qui tranche, et la déclaration
   ci-dessus est postérieure au bloc `:root`. */
@media (min-width: 100rem) {
  .shell { max-width: clamp(96rem, 42.67rem + 53.33vw, 128rem); } /* 1536 → 2048 */
}

section { position: relative; }

.section { padding-block: clamp(5rem, 12vw, var(--s8)); }
.section--tight { padding-block: clamp(3.5rem, 7vw, var(--s6)); }

/* La page à propos ne porte plus qu'un nom et un paragraphe : la colonne de
   texte est bien plus courte que le portrait, et c'est donc le portrait qui
   décide de la hauteur. Sous lui, le creux de la section se lisait comme une
   bande de papier posée là. Il est réduit de moitié — la page se referme sur
   la photographie, pas sur du vide. */
.section--apropos { padding-bottom: clamp(2.5rem, 5vw, 4rem); }

/* ------------------------------------------------------------ 5. Barre haute */

.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 90;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s3);
  padding: clamp(1rem, 2.4vw, 1.75rem) var(--gutter);
  opacity: var(--head);
  /* Invisible tant qu'elle n'est pas révélée. L'opacité seule ne suffit
     pas : un élément transparent reste atteignable au clavier. On coupe
     donc aussi la visibilité, remise par le script au moment de la
     révélation. */
  visibility: hidden;
  pointer-events: none;
}
.topbar--dark,
.head-on .topbar { visibility: visible; }
.topbar > * { pointer-events: auto; position: relative; }

/*
  Aucun bandeau sous la barre. Rien que le menu et le mot ESTHÈTE, dans la
  couleur du reste des écritures — claire sur les photographies, sombre sur
  le papier.

  Ce qui tient la lisibilité n'est plus une surface mais une ombre portée sur
  les lettres elles-mêmes : très diffuse, sans direction, elle est invisible
  à l'œil et empêche seulement le blanc de se perdre dans une haute lumière.
  C'est déjà ce qui tient la ligne du bas de l'ouverture.

  Deux dégradés l'ont précédée et ont été retirés : un voile sombre, qui
  posait une barre grise en haut de chaque photographie, et un voile papier,
  qui brouillait les vignettes défilant dessous. */
/* Trois ombres superposées, du plus serré au plus large. Elles se composent :
   au ras des lettres l'opacité cumulée atteint 0,98, si bien que le fond
   immédiat devient presque l'encre du site et que le blanc y ressort à près
   de 18:1. Une seule ombre n'aurait pas suffi — mesuré sous la barre, les
   photographies des écrans 2 à 5 descendent jusqu'à 1,42:1.

   Le halo reste invisible parce qu'il est sans direction et qu'il s'éteint
   en quelques pixels : on ne voit pas une ombre, on voit un texte qui tient. */
.topbar__title,
.topbar__lang {
  text-shadow:
    0 0  7px rgba(20, 12, 6, 0.85),
    0 1px 16px rgba(20, 12, 6, 0.75),
    0 0  32px rgba(20, 12, 6, 0.55);
}

/* Les points ne portent plus d'ombre : retirée le 8 septembre 2026, à la
   demande du client. Ce qui les tient désormais, c'est le moment où ils
   paraissent — jamais avant qu'une photographie ne soit sous eux. Le mot
   ESTHÈTE et le sélecteur de langue gardent la leur : ce sont des lettres
   fines, un point plein de 5 px tolère ce qu'un jambage de 1 px ne tolère
   pas. */

/* Sur fond clair, l'encre n'a besoin de rien : l'ombre disparaît, sans quoi
   elle salirait le papier. */
.topbar--dark .topbar__title,
.topbar--dark .topbar__lang,
.barre-claire .topbar__title,
.barre-claire .topbar__lang { text-shadow: none; }





/* Le mot du site s'efface dès qu'une photographie passe sous la barre : posé
   sur une vignette, il la salissait sans rien apporter — le menu suffit à
   dire où l'on est. Il revient partout où le fond est uni. */
.titre-cache .topbar__title {
  opacity: 0; visibility: hidden;
  transition: opacity 0.35s var(--ease-soft), visibility 0s linear 0.35s;
}

.topbar__title {
  grid-column: 2;
  transition: opacity 0.35s var(--ease-soft), visibility 0s linear 0.35s;
  justify-self: center;
  font-size: clamp(0.75rem, 1.15vw, 0.95rem);
  color: var(--on-photo);
  white-space: nowrap;
  /* C'est un lien vers l'accueil : sa zone tactile doit valoir celle des
     autres. Le texte ne fait que 18 px de haut ; la hauteur s'ajoute autour de
     lui sans rien déplacer, la barre étant déjà réglée sur le bouton à points,
     plus haut.

     **Pas de flex ici.** Un conteneur flex *blockifie* ses enfants : la balise
     qui porte l'accent y devenait un bloc, « ESTH », le E et « TE » trois
     éléments distincts, et l'interlettrage du mot-marque disparaissait entre
     eux. L'interligne fait le même travail — la ligne vaut 44 px et le texte
     s'y centre tout seul — sans rompre le contexte en ligne. */
  display: inline-block;
  line-height: 44px;
  min-height: 44px;
}

.topbar__lang {
  grid-column: 3;
  justify-self: end;
  display: flex; align-items: center; gap: 0.35rem;
  font-size: var(--fs-micro);
  letter-spacing: 0.2em;
  color: var(--on-photo);
}
.topbar__lang button {
  min-width: 44px; min-height: 44px;
  /* 0,72 et non 0,55 : à 11,2 px le seuil est de 4,5:1, et 0,55 ne donnait que
     **3,68**. C'est un bouton, pas un ornement — il doit se lire. La distinction
     avec la langue active, à 1, reste nette. */
  color: inherit; opacity: 0.72;
  transition: opacity 0.3s var(--ease-out);
}
.topbar__lang button:hover { opacity: 1; }
.topbar__lang button.is-active { opacity: 1; }
.topbar__lang span { opacity: 0.35; }

/* Sur les pages posées sur fond clair, l'encre remplace le blanc */
.topbar--dark .topbar__title,
.topbar--dark .topbar__lang,
.barre-claire .topbar__title,
.barre-claire .topbar__lang { color: var(--ink); }
.topbar--dark .dots__btn i,
.barre-claire .dots__btn i { background: var(--ink); }
.topbar--dark .dots__menu a,
.barre-claire .dots__menu a { color: var(--ink); }

/* -------------------------------------------------------- 6. Menu à points */

.dots { grid-column: 1; justify-self: start; position: relative; }

.dots__btn {
  display: flex; align-items: center;
  gap: 7px; height: 44px;
  margin-left: -10px; padding: 0 10px;
}

.dots__btn i {
  display: block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--on-photo);
  transition: transform 0.5s var(--ease-expo), opacity 0.4s var(--ease-soft);
}
.dots.is-open .dots__btn i { opacity: 0.3; }
.dots.is-open .dots__btn i:nth-child(1) { transform: translateX(-3px); }
.dots.is-open .dots__btn i:nth-child(3) { transform: translateX(3px); }

.dots__menu {
  position: absolute;
  top: 40px; left: 0;
  min-width: 13rem;
  display: flex; flex-direction: column;
  gap: 0.1rem;
  pointer-events: none;
}
.dots.is-open .dots__menu { pointer-events: auto; }

/* `will-change` réserve un calque de composition, et le garde tant que la
   déclaration s'applique. Posé à demeure sur les cinq entrées, il en tenait
   cinq ouverts en permanence, sur toutes les pages, pour une inclinaison qui
   ne joue qu'au survol du menu déplié. Il n'est promis qu'à ce moment-là. */
.dots.is-open .dots__menu a { will-change: transform; }

/* --- Le menu au doigt : un panneau plein écran --- */

/*
  Déplié, le menu n'avait aucun fond et ses libellés prenaient la couleur de la
  page — de l'encre sombre sur les pages claires. Les deux premières entrées
  tombaient sur le papier et se lisaient ; les trois suivantes tombaient sur la
  grille de photographies et disparaissaient. Le sélecteur de langue avec elles.

  La borne est `hover: none`, et non une largeur. C'est le bon critère : le
  défaut n'est pas une affaire de place mais d'ouverture au doigt, là où le
  menu reste déplié pendant qu'on regarde la page derrière. Une borne en
  largeur aurait manqué la tablette en paysage, qui fait 1024 px et n'a pas
  plus de survol qu'un téléphone. L'ordinateur, lui, n'est pas touché.

  Ce panneau n'est pas la surface refusée deux fois sous la barre du haut :
  celle-là était permanente et se posait sur chaque photographie. Celui-ci
  n'existe que le temps du menu ouvert.
*/
/* La borne ne regarde plus seulement le survol.

   `hover: none` manquait la tablette en « mode ordinateur » : Safari s'y fait
   passer pour un ordinateur et déclare un survol qu'aucun doigt ne possède.
   `any-pointer: coarse` est vrai dès qu'un moyen de pointage grossier existe
   sur la machine — un écran tactile ne se cache pas, même quand le navigateur
   ment sur le reste.

   Contrepartie assumée par le client : un ordinateur portable à écran tactile
   recevra le panneau lui aussi. */
@media (hover: none), (pointer: coarse), (any-pointer: coarse) {
  .dots__menu {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100lvh;
    min-width: 0;
    z-index: 1;
    justify-content: center;
    gap: 0.35rem;
    padding: 0 var(--gutter);
    background: var(--paper);
    opacity: 0;
    /* Motif déjà éprouvé ici : la visibilité revient sans délai à l'ouverture
       et n'attend la fin du fondu qu'à la fermeture. Dans une liste de
       transitions, une valeur calculée restée `hidden` à l'instant zéro rend
       `focus()` sans effet. */
    visibility: hidden;
    transition: opacity 0.4s var(--ease-out), visibility 0s linear 0.4s;
  }
  .dots.is-open .dots__menu {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.4s var(--ease-out), visibility 0s linear 0s;
  }

  /* Le bouton reste au-dessus du panneau : c'est lui qui le referme. */
  .dots__btn { position: relative; z-index: 2; }

  /* Et il passe à l'encre pendant ce temps : clair sur une photographie, il
     serait invisible sur le papier du panneau. */
  .dots.is-open .dots__btn i { background: var(--ink); box-shadow: none; }

  /*
    L'encre, et il faut trois classes pour l'obtenir.

    Deux règles écrites ailleurs pèsent plus lourd qu'un simple
    `.dots__menu a` : celle de la page contact
    (`body:has(.contact-screen--encre) .dots__menu a`, deux classes et deux
    éléments) qui met les entrées en papier, et celle de la barre sombre. La
    règle générale du menu, elle, est écrite **plus bas** dans la feuille et
    l'emportait aussi par l'ordre.

    Résultat mesuré avant correction : entrées `250,247,242` sur un panneau
    `250,247,242` sur la page contact — rigoureusement invisibles — et
    `255,255,255` sur papier à l'accueil. Deux pages sur six, dont une que le
    client a signalée et une qu'il n'avait pas vue.

    `.topbar .dots .dots__menu a` en pèse trois : il passe devant les deux, et
    devant l'ordre d'écriture. Ne pas l'alléger.
  */
  .topbar .dots .dots__menu a {
    color: var(--ink);
    /* Plus petit et plus discret qu'à l'origine : le client a trouvé le menu
       trop imposant en plein écran. La zone tactile, elle, ne descend pas —
       3 rem valent 48 px, au-dessus du seuil de 44. */
    font-size: clamp(0.82rem, 3.4vw, 1rem);
    letter-spacing: 0.24em;
    min-height: 3rem;
    padding: 0.2rem 0;
  }

  /* Le sélecteur du panneau disparaît : la barre porte le sien, visible en
     permanence, et deux jeux FR / EN à l'écran se disputeraient la même
     fonction. Trois classes, parce que la règle de base est écrite plus bas
     dans la feuille et l'emporterait par l'ordre — le piège a déjà coûté un
     diagnostic sur ce même élément. */
  .topbar .dots .dots__lang { display: none; }

  /* Le sélecteur de la barre passe **au-dessus** du panneau, comme le bouton :
     le menu ouvert est un aplat plein écran, et sans cela la langue devenait
     injoignable au moment précis où l'on navigue. Il prend l'encre du panneau
     pour la même raison que le bouton. */
  .topbar:has(.dots.is-open) .topbar__lang {
    position: relative;
    z-index: 2;
    color: var(--ink);
  }

  /* Le disque magnétique suit un curseur : il n'a rien à faire ici. */
  .dots__menu a::before { display: none; }

  /*
    La barre du haut se rétracte au défilement par une transformation — et
    toute transformation sur un ancêtre en fait le bloc conteneur de ses
    descendants `fixed`. Le panneau se serait ancré sur la barre au lieu de
    l'écran. Tant que le menu est ouvert, la barre ne se transforme donc pas ;
    et elle reste opaque, sans quoi le panneau hériterait de son opacité.
  */
  .topbar:has(.dots.is-open) {
    transform: none !important;
    opacity: 1 !important;
    visibility: visible;
  }
}

.dots__menu a {
  position: relative;
  display: flex; align-items: center;
  min-height: 44px;
  padding: 0.4rem 0;
  font-size: var(--fs-small);
  font-weight: 300;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--on-photo);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(-8px);
  transition:
    opacity 0.45s var(--ease-out) calc(var(--i) * 45ms),
    transform 0.55s var(--ease-expo) calc(var(--i) * 45ms);
  /* --mx et --my sont posés par le script : l'entrée s'incline très
     légèrement vers le curseur. Voir Magnetic dans main.js. */
}
.dots.is-open .dots__menu a { opacity: 1; transform: none; }

/* L'attraction magnétique : l'entrée survolée glisse de quelques pixels vers
   le curseur et son crénage s'ouvre. Aucune barre, aucun soulignement — le
   mouvement seul signale la cible. */
.dots.is-open .dots__menu a.is-magnetic {
  transform: translate(calc(var(--mx, 0) * 1px), calc(var(--my, 0) * 1px));
  transition: transform 0.35s var(--ease-out), letter-spacing 0.5s var(--ease-out);
  letter-spacing: 0.3em;
}

/* Le disque de lumière suit le curseur derrière le mot */
.dots__menu a::before {
  content: '';
  position: absolute;
  left: calc(var(--mx, 0) * 1px + 50%);
  top: 50%;
  width: 3.5rem; height: 3.5rem;
  margin: -1.75rem 0 0 -1.75rem;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  transform: scale(0.2);
  transition: opacity 0.4s var(--ease-out), transform 0.5s var(--ease-expo);
  pointer-events: none;
  z-index: -1;
}
.dots__menu a.is-magnetic::before { opacity: 0.1; transform: scale(1); }

/* La page courante se signale par la seule graisse, sans trait */
.dots__menu a[aria-current='page'] { font-weight: 400; }

/* Doublon du sélecteur de langue, réservé aux petites largeurs */
.dots__lang {
  display: none;
  align-items: center;
  gap: 0.5rem;
  margin-top: var(--s2);
  padding-top: var(--s2);
  border-top: 1px solid currentColor;
  border-image: linear-gradient(90deg, currentColor, transparent) 1;
  font-size: var(--fs-micro);
  letter-spacing: 0.2em;
  opacity: 0;
  transition: opacity 0.45s var(--ease-out) 0.24s;
}
.dots.is-open .dots__lang { opacity: 0.75; }
.dots__lang button { min-width: 44px; min-height: 44px; color: inherit; }
.dots__lang button.is-active { font-weight: 400; }

/* ------------------------------------------------------- 7. Préchargeur */

.preloader {
  position: fixed; inset: 0;
  z-index: 300;
  background: var(--paper);
  display: grid; place-items: center;
}

.preloader__line {
  width: clamp(9rem, 22vw, 16rem);
  height: 1px;
  background: var(--line-soft);
  overflow: hidden;
}
.preloader__line i {
  display: block; height: 100%; width: 0;
  background: var(--ink);
  transition: width 0.3s linear;
}

.preloader.is-done {
  opacity: 0; visibility: hidden;
  transition: opacity 0.7s var(--ease-soft), visibility 0s linear 0.7s;
}

/* ============================ 8. Scènes de l'accueil ====================== */

/* Chaque scène se colle en haut et y reste définitivement : elle ne se
   décroche jamais. Les scènes suivantes viennent s'empiler par-dessus, par
   z-index croissant.

   C'est indispensable : avec un épinglage borné à sa propre section, une
   scène se décrochait dès sa section franchie et laissait réapparaître
   l'écran d'ouverture derrière la scène suivante, dont l'image n'a pas
   encore fini de monter.

   La course d'animation est fournie par un bloc d'espacement placé juste
   après la scène — c'est lui qui défile pendant que la scène reste fixe.

   Les hauteurs sont en `lvh` et non en `svh`. `svh` vaut la hauteur d'écran
   **barre du navigateur déployée** : dès que Safari rétracte la sienne,
   l'écran devient plus grand que la scène et une bande de papier apparaît
   sous l'image. `lvh` prend la hauteur barre rétractée : l'image couvre
   toujours, dans les deux états. En contrepartie, barre déployée, les
   quatre-vingt-dix derniers pixels passent derrière elle — c'est pourquoi
   la ligne du bas de l'ouverture se tient loin du bord. */
.scene {
  position: sticky;
  top: 0;
  height: 100lvh;
  overflow: hidden;
  z-index: var(--z, 1);
}

.scene__pin { position: relative; height: 100%; }

/* Bloc d'espacement : sa seule fonction est de donner du défilement à la
   scène qui le précède. --len s'exprime en hauteurs d'écran. */
/*
  Bloc d'espacement : sa seule fonction est de donner du défilement à la scène
  qui le précède.

  Sa marge basse négative est ce qui supprime le défilement mort. Chaque scène
  occupe une hauteur d'écran de flux **avant** que sa propre course ne
  commence : une fois la scène précédente achevée, il fallait donc parcourir
  900 px sans que rien ne bouge, quatre fois de suite — 43 % de l'accueil.

  La marge est posée ici, sur l'espacement, et non sur la scène : posée sur la
  scène, elle remonterait aussi l'espacement, et la course commencerait déjà
  entamée. Ici, seule la scène suivante remonte ; les deux courses restent
  justes.

  Il en reste un dixième de hauteur d'écran : l'écran achevé n'est pas emporté
  à l'instant même où il se pose.
*/
.scene-gap { height: calc(100lvh * var(--len, 1)); }

/*
  La marge n'est retirée qu'entre **deux scènes**. Là, le vide est réellement
  mort : la scène suivante monte dans le flux mais son contenu est décalé hors
  champ, donc rien ne bouge à l'écran.

  Le dernier espacement, celui qui précède la galerie, doit être laissé
  intact : ce n'est pas du vide, c'est la remontée de `.after` par-dessus la
  dernière image. En le supprimant, la galerie entrait par le bas de l'écran
  pendant que la dernière image montait encore, et l'on voyait trois bandes
  empilées — deux photographies et le fond clair.

  `:has()` dit exactement cela : « un espacement suivi d'une scène ».
*/
.scene-gap:has(+ .scene) { margin-bottom: -90lvh; }



/* --- Écran 1 : ouverture --- */

/* L'ouverture est collante par elle-même : elle reste en place tout le
   temps que les scènes suivantes défilent par-dessus. C'est ce qui permet au
   titre de ne pas bouger pendant que les deux colonnes le recouvrent. */
.opening {
  position: sticky;
  top: 0;
  height: 100lvh;
  overflow: hidden;
  z-index: 0;
  /* L'ouverture ne s'anime pas : la première hauteur d'écran de défilement
     ne produisait donc rien du tout, et c'est ce vide que le client a
     ressenti comme un départ trop lent. La marge négative rend au flux la
     moitié de la place que l'ouverture occupait : la scène suivante se colle
     après une demi-hauteur d'écran au lieu d'une. L'ouverture, elle, reste
     collée en haut et continue de remplir l'écran — seule sa place dans le
     flux a changé. */
  margin-bottom: -48lvh;
}

.opening__media { position: absolute; inset: 0; }

.opening__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Aucune transformation : ni zoom, ni déplacement. L'image d'ouverture est
     montrée aussi grande que la fenêtre le permet, et le seul recadrage
     qu'elle subit est celui qu'impose le format de l'écran.
     Le surdimensionnement de 1,10 qui existait ici n'avait qu'une raison :
     laisser de la marge à une parallaxe qui déplaçait l'image. La lueur
     l'ayant remplacée ne déplace rien, il n'a plus lieu d'être. */
  transition: filter 0.9s var(--ease-soft);
}

/* La lueur qui suit le curseur. Elle se pose entre la photographie et les
   textes : elle éclaire l'image sans jamais toucher au titre, qui vit dans
   un autre bloc. Le script ne publie que --mx et --my, la position du
   curseur en pourcentage, déjà amortie — la tache glisse, elle ne saute pas.
   Rien ne bouge géométriquement : c'est une lumière, pas un mouvement. */
.opening__lueur {
  position: absolute; inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(circle 19vmax at var(--mx, 50%) var(--my, 45%),
    rgba(255, 242, 220, 0.26) 0%,
    rgba(255, 242, 220, 0.12) 34%,
    transparent 66%);
  opacity: 0;
  transition: opacity 0.9s var(--ease-soft);
}
.opening.is-lit .opening__lueur { opacity: 1; }

/* L'anneau du pointeur. Il se cale sur les mêmes --mx / --my que la lueur,
   déjà amortis : la lumière et l'anneau glissent donc exactement ensemble,
   et l'anneau désigne le centre de la tache lumineuse au lieu de courir
   après elle. Le curseur du système s'efface sous lui — c'est l'anneau qui
   en tient lieu. */
.opening__pointeur {
  position: absolute;
  left: var(--mx, 50%);
  top: var(--my, 45%);
  width: 44px; height: 44px;
  margin: -22px 0 0 -22px;
  border: 1px solid rgba(255, 248, 236, 0.5);
  border-radius: 50%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.72);
  transition: opacity 0.5s var(--ease-soft), transform 0.6s var(--ease-out);
}
.opening__pointeur::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 3px; height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: rgba(255, 248, 236, 0.8);
}
.opening.is-lit .opening__pointeur { opacity: 1; transform: scale(1); }
.opening:hover .opening__media img { filter: brightness(1.06) saturate(1.04); }

.opening__media::after {
  content: '';
  position: absolute; inset: 0;
  /* Deux voiles superposés, calibrés sur la photo réelle. Le dégradé
     vertical protège le nom en haut et la ligne du bas ; l'ellipse centrale
     ne fonce que la zone du titre, ce qui laisse les angles de l'image
     lumineux. Sur cette photographie claire, un voile plat assez dense pour
     le titre aurait éteint toute l'image. */
  background: linear-gradient(180deg,
    rgba(30, 19, 11, 0.80) 0%,
    rgba(30, 19, 11, 0.52) 12%,
    rgba(30, 19, 11, 0.30) 24%,
    rgba(30, 19, 11, 0.60) 40%,
    rgba(30, 19, 11, 0.60) 60%,
    rgba(30, 19, 11, 0.32) 76%,
    rgba(30, 19, 11, 0.54) 88%,
    rgba(30, 19, 11, 0.80) 100%);
}

.opening__inner {
  position: relative;
  z-index: 2;
  height: 100%;
  display: grid;
  /* Trois rangs : le nom, ESTHÈTE au centre, la ligne d'accroche. Un quatrième
     a existé, pour le filet qui s'intercalait entre les deux derniers ; il est
     parti avec lui. */
  grid-template-rows: auto 1fr auto;
  /* La marge basse tient la ligne d'ouverture au-dessus de deux obstacles :
     la bande floue du bas (13 vh) et, sur iPhone, la barre de Safari quand
     elle est déployée (~90 px). 14,5 vh passe au-dessus des deux — de
     justesse, et c'est voulu : la ligne devait descendre. */
  padding: clamp(1rem, 2.4vw, 1.75rem) var(--gutter) clamp(5.8rem, 14vh, 7.25rem);
  text-align: center;
  color: var(--on-photo);
}

/* Le nom, tout en haut, très discret */
.opening__name {
  /* C'est un lien vers la page « à propos » : il lui faut la zone tactile
     des autres, sans que le texte ne bouge pour autant. */
  display: inline-flex; align-items: center; justify-self: center;
  min-height: 44px;
  font-size: var(--fs-micro);
  font-weight: 300;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  opacity: 0;
  margin: 0;
}

.opening__title {
  align-self: center;
  font-size: var(--fs-title);
  opacity: 0;
}

/*
  Le point qui sépare les deux membres de l'accroche.

  Un point typographique se pose sur la ligne de base, tout en bas des
  capitales : entre deux mots en capitales espacées, il se lit comme une fin
  de phrase et non comme une articulation. Il doit se trouver à mi-hauteur de
  capitale — au niveau de la barre du E.

  On le **dessine** plutôt que de relever le caractère : la position d'un
  glyphe dépend de la police et se déplacerait avec elle, celle d'un disque ne
  dépend que de sa propre déclaration. Il vit dans `data-fr`, et non à côté :
  le sélecteur de langue réécrit l'`innerHTML`, et un frère écrit hors de
  l'attribut disparaîtrait au premier changement de langue.
*/
.opening__sub .pt {
  display: inline-block;
  /* Le caractère lui-même, et non un disque dessiné : c'est la seule façon
     d'obtenir exactement la taille d'un point de texte. Le point de Jost fait
     **0,05 em** — au corps de l'accroche, 0,6 px. Une boîte CSS de cette
     taille se rend mal ou pas du tout ; le rastériseur d'une police, lui, sait
     le faire. On ne garde donc de la version dessinée que la **position**.

     La hauteur de capitale de Jost vaut 0,70 em, mesurée : le centre du point
     doit se poser à 0,35 em de la ligne de base. Son encre étant centrée à
     0,025 em au-dessus d'elle, on l'élève de la différence. */
  vertical-align: 0.325em;
  /* Le blanc se lit le même des deux côtés : l'interlettrage ajoute déjà
     0,26 em après le mot précédent **et** après le disque lui-même, ces deux
     parts s'annulent, et les marges restent donc symétriques. */
  margin-inline: 0.62em;
}

.opening__sub {
  /* Le plancher a été relevé de 0,60 à 0,66 rem : avec l'article en plus, la
     ligne descendait à 9,6 px sur un écran étroit — trop peu pour des
     capitales espacées. */
  font-size: clamp(0.66rem, 0.82vw, 0.72rem);
  font-weight: 300;
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  text-transform: uppercase;
  opacity: 0;
  /* Le filet occupait 41 px juste au-dessus. Sa disparition rendait cette
     place au rang central, et ESTHÈTE — centré dedans — descendait de 21 px :
     on retirait un trait, et c'est le titre qui bougeait. La marge reprend
     cette place, et la composition ne change nulle part ailleurs. */
  margin: var(--s4) 0 0;
}

/* Le seul raccourci du premier écran. Il faut sinon traverser neuf hauteurs
   d'écran, ou ouvrir le menu, pour trouver comment vous joindre — un
   directeur d'hôtel pressé n'ira pas jusque-là.

   Il se tient en haut à droite, où la barre du haut n'est pas encore levée :
   il ne dispute sa place à rien, à aucune largeur. Il arrive en dernier,
   après la ligne du bas, pour ne pas troubler l'ouverture. */
.opening__contact {
  position: absolute;
  /* Le nom occupe la première ligne de la grille, à la hauteur de la marge
     haute. Le raccourci porte une zone tactile de 44 px, bien plus haute que
     sa ligne de texte : on le remonte de la moitié de l'écart pour que les
     deux lettrages tombent exactement au même niveau. */
  top: calc(clamp(1rem, 2.4vw, 1.75rem) + 22px - 22px);
  right: var(--gutter);
  z-index: 3;
  display: inline-flex; align-items: center;
  min-height: 44px;
  font-size: var(--fs-micro);
  font-weight: 300;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--on-photo);
  text-shadow:
    0 0  7px rgba(20, 12, 6, 0.85),
    0 1px 16px rgba(20, 12, 6, 0.75),
    0 0  32px rgba(20, 12, 6, 0.55);
  opacity: 0;
}
/* Sur les petits écrans, « Prendre contact » et le nom se disputaient la
   ligne : le libellé se raccourcit plutôt que de descendre d'un cran. */
.opening__contact .court { display: none; }

.opening__contact::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: calc(50% - 0.85em);
  height: 1px;
  background: currentColor;
  opacity: 0.45;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.5s var(--ease-expo);
}
.opening__contact:hover::after,
.opening__contact:focus-visible::after { transform: scaleX(1); transform-origin: left; }

.is-ready .opening__contact { animation: rise-in 1.3s var(--ease-out) 1.5s both; }

/* Arrivée au chargement : le titre d'abord, la ligne du bas ensuite.
   Sans fill-mode — si l'animation ne se joue pas, le texte reste lisible. */
.is-ready .opening__name  { animation: rise-in 1.2s var(--ease-out) 0.15s both; }
.is-ready .opening__title { animation: title-in 1.7s var(--ease-out) 0.35s both; }
.is-ready .opening__sub   { animation: rise-in 1.3s var(--ease-out) 1.15s both; }

@keyframes title-in {
  from { opacity: 0; letter-spacing: 0.62em; }
  to   { opacity: 1; letter-spacing: 0.34em; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(0.9rem); }
  to   { opacity: 1; transform: none; }
}

/* --- Écran 2 : deux colonnes qui montent en décalé --- */

.duo { position: absolute; inset: 0; display: grid; grid-template-columns: 1fr 1fr; }

.duo__col { position: relative; overflow: hidden; margin: 0; }

.duo__col img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: filter 0.9s var(--ease-soft);
}
.duo__col:hover img { filter: brightness(1.06); }

/* La colonne gauche monte sur la première moitié de la course ; la droite
   ne s'élance qu'une fois la gauche aux trois quarts.

   Chaque colonne arrondit ses quatre angles pendant sa montée. L'ouverture,
   restée en place derrière, apparaît par les encoches ; elles se referment au
   fil du défilement et l'écran est plein et carré quand les colonnes arrivent
   en haut. Le rayon suit --e : il atteint zéro exactement au moment où la
   colonne se pose.

   Le bas compte autant que le haut. Sur téléphone les deux images sont
   empilées, et c'est leur jonction horizontale qui se voit. */
.duo__col--a {
  --t: clamp(0, calc(var(--p, 0) / 0.42), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  /* L'arrondi ne bouge pas tant que la colonne n'a pas couvert les trois
     quarts de l'écran ; il se referme sur le dernier quart et vaut zéro à
     l'arrivée. */
  --r: calc(var(--rayon) * (1 - clamp(0, calc((var(--e) - 0.75) / 0.25), 1)));
  transform: translate3d(0, calc((1 - var(--e)) * 100%), 0);
  border-radius: var(--r);
}
.duo__col--b {
  --t: clamp(0, calc((var(--p, 0) - 0.28) / 0.42), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  /* L'arrondi ne bouge pas tant que la colonne n'a pas couvert les trois
     quarts de l'écran ; il se referme sur le dernier quart et vaut zéro à
     l'arrivée. */
  --r: calc(var(--rayon) * (1 - clamp(0, calc((var(--e) - 0.75) / 0.25), 1)));
  transform: translate3d(0, calc((1 - var(--e)) * 100%), 0);
  border-radius: var(--r);
}

/* --- Écrans 3 à 5 : une image pleine qui recouvre la précédente --- */

.rise-media { position: absolute; inset: 0; overflow: hidden; }

.rise-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: filter 0.9s var(--ease-soft);
}
.rise-media:hover img { filter: brightness(1.05); }

/* --- Les deux écrans qui mènent à la page services --- */

/*
  Le lien couvre l'image entière : sur un plein écran, une cible de la taille
  d'un titre se cherche. Il est vide de texte et porte son libellé dans
  `aria-label`, sinon le lecteur d'écran annoncerait le titre, le paragraphe
  et la chute comme un seul intitulé à rallonge.

  Il vit dans `.rise-media`, l'élément que le défilement translate — et c'est
  la raison d'être de ce placement. Tant que l'image n'est pas montée, le lien
  attend hors de l'écran avec elle. Posé sur `.scene__pin`, il aurait couvert
  la fenêtre en permanence : chaque scène est collée en haut dès le début, et
  celle d'un z-index supérieur aurait avalé les clics destinés à la scène que
  l'on regarde encore.
*/
.scene__lien { position: absolute; inset: 0; z-index: 2; }

/* Le lien suit l'image au pixel près — mais cela ne suffisait pas.

   Chaque scène se colle en haut **avant** que sa course ne commence : il
   reste, entre deux écrans, un dixième de hauteur d'écran pendant lequel la
   scène suivante est déjà épinglée, son image encore hors champ. Son
   `.scene__pin` est transparent, mais un div transparent reçoit les clics
   comme un autre — et son z-index est supérieur. Pendant ce court moment, le
   lien de l'écran que l'on regarde était donc inatteignable.

   La cible cliquable est ramenée à l'image seule : le cadre qui la porte ne
   reçoit plus rien. `.over` était déjà transparent aux clics pour la même
   raison. */
.scene--rise { pointer-events: none; }
.scene--rise .rise-media { pointer-events: auto; }

/* Une cible plein écran mérite un cadre plein écran, mais posé à l'intérieur :
   le décalage de 4 px du réglage général le pousserait hors du cadre, que la
   scène rogne. */
.scene__lien:focus-visible { outline-color: var(--on-photo); outline-offset: -1rem; }

/* Deux gestes minuscules disent le lien : l'image s'éclaire — c'est déjà le
   cas de toutes celles qui montent — et le filet posé entre le titre et le
   texte s'élargit. Rien qui ressemble à un bouton : la page n'en a aucun.

   Ce filet-là existe désormais sur les cinq écrans (voir plus bas) ; il a
   remplacé un trait qui n'apparaissait qu'au survol. Deux lignes au même
   endroit, l'une permanente et l'autre non, se lisaient comme un défaut. */
@media (hover: hover) and (pointer: fine) {
  .scene__pin:has(.scene__lien:hover) .over__filet { width: 4.75rem; opacity: 1; }
}
.scene__pin:has(.scene__lien:focus-visible) .over__filet { width: 4.75rem; opacity: 1; }

.scene--rise .rise-media {
  /* RÈGLE DE FOND — celle qui a demandé quatre passes pour être trouvée :
     **aucun texte ne paraît avant que son image n'ait recouvert l'écran.**

     Sans elle, le texte d'un écran se posait sur celui du précédent, encore
     visible dans la partie non recouverte : deux titres et deux paragraphes
     l'un sur l'autre. Faire apparaître le texte à 75 %, à 90 %, ou « pendant
     la montée » revient toujours au même défaut, en moins visible.

     L'image occupe donc la première moitié de la course, et tout le texte la
     seconde. Le prix à payer est un bref instant où l'image est seule ; la
     course a été raccourcie d'autant pour qu'il ne coûte rien. */
  --t: clamp(0, calc(var(--p, 0) / 0.45), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  transform: translate3d(0, calc((1 - var(--e)) * 100%), 0);

  /* L'image monte longtemps avec ses quatre coins arrondis : la transition
     vers l'angle vif ne commence qu'une fois les trois quarts de l'écran
     couverts. Les coins hauts se carrent les premiers — ce sont eux qui
     atteignent le bord — puis les coins bas les rejoignent à l'instant où
     l'image se pose, pleine et carrée. */
  --r-haut: calc(var(--rayon) * (1 - clamp(0, calc((var(--e) - 0.75) / 0.20), 1)));
  --r-bas:  calc(var(--rayon) * (1 - clamp(0, calc((var(--e) - 0.80) / 0.20), 1)));
  border-top-left-radius:     var(--r-haut);
  border-top-right-radius:    var(--r-haut);
  border-bottom-left-radius:  var(--r-bas);
  border-bottom-right-radius: var(--r-bas);
}

/* --- Le texte en surimpression, commun aux écrans 2 à 5 --- */

.over {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-content: center;
  padding: var(--gutter);
  text-align: center;
  color: var(--on-photo);
  pointer-events: none;
}

/* Voile concentré au centre : les bords de l'image restent lumineux, seul
   le cœur s'assombrit — là où se pose le texte. Un voile uniforme assez
   dense pour être lisible éteindrait toute la photographie. */
.over::before {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 72% 62% at 50% 50%,
    rgba(30, 19, 11, 0.80) 0%,
    rgba(30, 19, 11, 0.64) 44%,
    rgba(30, 19, 11, 0.34) 74%,
    rgba(30, 19, 11, 0.16) 100%);
}

.over__inner { position: relative; }

/*
  La mesure des écrans, **partagée** par le paragraphe et par sa chute.

  Elle a valu 44 rem fixes, ce qui était juste tant que `--fs-over` ne bougeait
  pas — mais il grandit au-delà de 100 rem, et une mesure figée sous un corps
  qui croît **réduit** le nombre de signes par ligne : 53 à 1440, 44 seulement
  à 2560. Le rapport est rétabli, 44 → 53 rem quand le corps passe de 21,6 à
  26 px, si bien que la ligne garde partout la même longueur en signes.
  Continue à 1600 px : le plancher vaut l'ancienne valeur.

  **C'est un jeton et non deux déclarations** : la feuille prévenait déjà que
  les valeurs de la chute sont celles du paragraphe et qu'en changer une sans
  l'autre romprait leur accord. Le piège a été pris une fois ; il ne peut plus
  l'être.
*/
:root { --mesure-ecran: clamp(44rem, 29rem + 15vw, 53rem); }

.over__text {
  max-width: var(--mesure-ecran);
  margin-inline: auto;
  font-size: var(--fs-over);
  font-weight: 200;
  line-height: 1.45;
  letter-spacing: 0.01em;
}

.over__word {
  font-size: clamp(1.5rem, 4vw, 2.75rem);
  font-weight: 200;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  margin: 0 0 var(--s3);
}

/* La chute : la dernière phrase de l'écran, détachée du paragraphe.

   Elle ne se distingue plus par rien de typographique. Trois accents ont été
   essayés puis retirés l'un après l'autre : un gras, qui pesait trop lourd
   dans une page qui ne dépasse jamais le 400 ; puis un interlettrage plus
   large et une colonne plus étroite, que le client a demandé de reprendre —
   il la veut « dans le même style que le reste ».

   Ne restent que les deux choses qu'il a voulu garder : la ligne blanche qui
   la sépare du paragraphe, et son arrivée, qui suit celle du paragraphe au
   lieu de la partager. C'est le retard, seul, qui la souligne.

   Toutes les valeurs ci-dessous sont donc celles de `.over__text`, à la
   marge près. Les changer ici sans les changer là romprait ce qui vient
   d'être demandé. */
.over__chute {
  margin: var(--s4) auto 0;
  max-width: var(--mesure-ecran);
  font-weight: 200;
  font-size: var(--fs-over);
  letter-spacing: 0.01em;
  line-height: 1.45;
}

/* --- Le filet, entre le titre et son texte --- */

/*
  Une ligne de 2,75 rem qui s'ouvre depuis son milieu, sur les cinq écrans de
  l'accueil. Elle arrive **entre** les deux : le titre s'est posé, le texte
  n'a pas encore paru. C'est la respiration que le client demandait, et le
  seul ornement du site.

  Comme tout le reste elle est indexée sur le défilement — sauf sur
  l'ouverture, qui ne défile pas et dont la séquence est jouée au chargement.
*/
.over__filet {
  display: block;
  width: 2.75rem;
  height: 1px;
  margin: 0 auto var(--s4);
  background: currentColor;
  opacity: 0.75;
  --t: 0;
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  transform: scaleX(var(--e));
  transition: width 0.55s var(--ease-out), opacity 0.55s var(--ease-out);
}

/* Le titre se rapproche du filet : c'est lui, désormais, qui tient l'écart
   avec le paragraphe. */
.over__title:has(+ .over__filet),
.over__phon:has(+ .over__filet) { margin-bottom: var(--s3); }

/* Il part une fois le titre posé et finit quand le paragraphe s'ouvre. Les
   deux plages se chevauchent volontairement d'un cheveu : sans cela on verrait
   la ligne s'arrêter avant que le texte ne bouge, et l'enchaînement se
   briserait. */
.scene--rise .over__filet { --t: clamp(0, calc((var(--p, 0) - 0.55) / 0.12), 1); }
.scene--duo  .over__filet { --t: clamp(0, calc((var(--p, 0) - 0.62) / 0.12), 1); }

/* L'ouverture n'a plus de trait : le client l'a fait retirer de la première
   image seule. Les quatre autres écrans gardent le leur — c'est là que le
   trait sépare un titre de son texte, et l'ouverture n'a pas de texte à
   séparer, seulement une ligne d'accroche. `.opening__filet` et l'animation
   `filet-in` ont donc disparu avec lui. */

/* La signature, sous la définition d'« esthète ».

   Même traitement que le nom de l'ouverture — capitales très espacées,
   graisse 300, corps micro — et même destination : la page « à propos ». Elle
   signe le texte au lieu de l'annoncer, d'où le corps discret plutôt qu'un
   nom en vedette.

   `pointer-events: auto` n'est pas une précaution mais une nécessité : `.over`
   est à `none` d'un bout à l'autre, pour que les textes en surimpression
   n'interceptent jamais les clics destinés à l'image qui les porte. Sans ce
   rappel, le seul lien de la scène serait parfaitement inerte — visible,
   soulignable au clavier, et sans effet à la souris. */
.over__signature {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: var(--s4);
  font-size: var(--fs-micro);
  font-weight: 300;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  pointer-events: auto;
}

/* Elle suit le paragraphe sans le chevaucher, sur la plage même de la chute
   des écrans 4 et 5 : le texte finit à 0,74, elle part à 0,76. La règle de
   fond vaut ici comme ailleurs — rien ne paraît avant que l'image n'ait
   recouvert l'écran. */
.scene--rise .over__signature {
  --t: clamp(0, calc((var(--p, 0) - 0.76) / 0.15), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  opacity: var(--e);
  transform: translate3d(0, calc((1 - var(--e)) * 1.6rem), 0);
}

.over__phon {
  display: block;
  font-size: var(--fs-micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  opacity: 0.85;
  margin-bottom: var(--s4);
}

/* Titre d'écran. Même traitement que le mot ESTHÈTE de l'écran 3 : les cinq
   écrans parlent ainsi la même langue typographique. */
.over__title {
  font-size: clamp(1.25rem, 3.4vw, 2.4rem);
  font-weight: 200;
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  text-transform: uppercase;
  line-height: 1.3;
  margin: 0 0 var(--s4);
}

/* L'esperluette. Elle a longtemps été le seul signe du site à changer de
   corps au milieu d'un titre — un demi-cadratin de plus, pour la faire lire
   comme une ligature. Le client l'a ramenée à la taille du texte : plus
   aucune exception typographique nulle part.

   Le `line-height: 0` et le décalage vertical qui la recalaient n'ont plus
   d'objet — ils corrigeaient un corps qui n'existe plus. Ne reste que
   l'espacement remis à zéro : les capitales du titre en portent 0,16 em, un
   signe seul n'en veut pas. */
.over__title .eperl {
  font-style: normal;
  font-size: 1em;
  letter-spacing: 0;
}

/* Le titre monte d'un bloc, derrière une ligne invisible qui le dévoile.

   Chaque mot garde sa propre fenêtre de rognage — c'est elle qui fait la
   ligne — mais tous partent au même instant : le décalage mot à mot a existé
   et a été retiré, le client voulant que toutes les lettres arrivent
   ensemble. Le script n'écrit que le nombre --d, identique pour tous.

   Comme le reste du site, c'est indexé sur le défilement — on s'arrête, le
   mot se fige à mi-course ; on remonte, il redescend.

   Les marges négatives compensent le rognage : la fenêtre doit laisser
   passer les accents des capitales (É, Ô, È) sans les couper. */
.over__title .mot {
  display: inline-block;
  overflow: hidden;
  padding: 0.16em 0 0.08em;
  margin: -0.16em 0 -0.08em;
  vertical-align: bottom;
}
.over__title .mot > i {
  display: inline-block;
  font-style: normal;
  --t: clamp(0, calc((var(--p, 0) - var(--d, 0)) / 0.14), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  transform: translate3d(0, calc((1 - var(--e)) * 118%), 0);
}

/* Marqueur des textes que tu dois encore écrire */
.draft {
  display: inline-block;
  margin-top: var(--s3);
  padding: 0.25rem 0.7rem;
  border: 1px dashed rgba(255, 255, 255, 0.5);
  font-size: var(--fs-micro);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0.8;
}

/* Les plages d'apparition, scène par scène. Un seul nombre à changer pour
   décaler un effet.

   Elles se terminent volontairement tard dans la course — le texte finit
   d'apparaître aux alentours de 85 % — parce que les courses elles-mêmes ont
   été raccourcies. Ce qui restait après l'apparition du texte était du temps
   mort : on avait fini de lire et il fallait encore défiler. Il reste
   aujourd'hui un dixième de hauteur d'écran, juste de quoi finir sa phrase.

   Le titre arrive toujours avant son texte : le visiteur sait ce qu'il
   regarde avant de lire pourquoi. */
.scene--rise .over { opacity: clamp(0, calc((var(--p, 0) - 0.46) / 0.08), 1); }

/* Le paragraphe ne se contente pas de paraître : il monte de deux
   caractères en se révélant, et arrive à l'instant où l'image se pose. Sans
   ce déplacement, il serait la seule chose immobile d'une composition qui
   bouge — et c'est précisément ce qui se remarque. */
.scene--duo .over__text,
.scene--rise .over__text {
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  opacity: var(--e);
  transform: translate3d(0, calc((1 - var(--e)) * 2.2rem), 0);
}
.scene--duo  .over__text { --t: clamp(0, calc((var(--p, 0) - 0.70) / 0.20), 1); }
.scene--rise .over__text { --t: clamp(0, calc((var(--p, 0) - 0.58) / 0.16), 1); }

/* La chute suit le paragraphe sans le chevaucher : il finit à 0,74, elle part
   à 0,76. Le défilement ne s'interrompt pas, il continue de la porter. */
.scene--rise .over__chute {
  --t: clamp(0, calc((var(--p, 0) - 0.76) / 0.15), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  opacity: var(--e);
  transform: translate3d(0, calc((1 - var(--e)) * 1.6rem), 0);
}

/* --- Écran 2 : le mot monte avec la colonne droite --- */

/*
  PHOTOGRAPHIE ne se dévoile pas sur place comme les titres des écrans
  suivants : il monte depuis le bas de l'écran, et **se pose exactement quand
  la colonne droite atteint le haut** — c'est le seul point qui doive rester
  synchronisé, à `0,85`.

  Son départ, lui, est plus tardif que celui de la colonne, et c'est
  volontaire. Partagée avec elle, sa plage le faisait entrer dans l'écran
  alors qu'elle n'avait couvert que 28 % de la hauteur : le mot apparaissait
  bien avant d'avoir recouvert ESTHÈTE. Avec `[0,58 ; 0,85]`, il franchit le
  bas de l'écran quand la colonne est à 70 % — donc après avoir passé le
  centre, là où ESTHÈTE se tient.

  Si la plage de la colonne change, seule sa **fin** doit être reportée ici.
*/
.scene--duo .over { opacity: 1; }
.scene--duo .over::before { opacity: clamp(0, calc((var(--p, 0) - 0.68) / 0.12), 1); }

.scene--duo .over__title {
  --t: clamp(0, calc((var(--p, 0) - 0.50) / 0.20), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));
  transform: translate3d(0, calc((1 - var(--e)) * 62vh), 0);

  /* Au repos, sa position basse le laissait dépasser de quarante pixels sous
     le bord de l'écran : on le voyait attendre en bas, avant même que le duo
     ne s'élance. L'opacité le retient jusqu'à l'instant précis où il part. */
  opacity: clamp(0, calc((var(--p, 0) - 0.48) / 0.03), 1);
}

/* Le mot voyage d'un bloc : la fenêtre de rognage mot à mot, qui sert aux
   écrans suivants, n'a plus lieu d'être ici et le couperait en route. */
.scene--duo .over__title .mot { overflow: visible; padding: 0; margin: 0; }
.scene--duo .over__title .mot > i { transform: none; }

/* Tout ce qui suit les scènes repasse sur fond clair et doit recouvrir
   l'ouverture, restée collante en arrière-plan. */
.after {
  position: relative;
  z-index: 10;
  background: var(--paper);
}

/* ================================================== 9. Galerie ============ */

/*
  Trente-six photographies, trente-six provenances. Le format a été unifié à
  la fabrication (recadrage 4:5 cuit dans les fichiers) ; le ton et le grain
  le sont ici, à l'affichage — deux déclarations à retoucher plutôt que
  trente-six fichiers à refabriquer.

  Le ton : `sepia` ramène tout à une seule teinte chaude, `saturate` rend
  ensuite la couleur, désormais tirée du même côté. C'est ce couple qui fait
  la famille — y compris pour la photographie en noir et blanc, que le sépia
  ramène dans le rang au lieu de la laisser étrangère.

  Le grain : un bruit fractal en SVG, tuilé par le navigateur. Aucun fichier,
  aucune requête. `overlay` le fait mordre dans les demi-teintes sans toucher
  aux noirs ni aux blancs, comme un grain d'argentique.
*/
:root {
  --ton: sepia(0.24) saturate(1.28) hue-rotate(-6deg) contrast(1.02) brightness(1.01);
  --grain: 0.075;
}

.tile img,
.lightbox__stage img { filter: var(--ton); }

.tile::after,
.lightbox__stage::after {
  content: '';
  position: absolute;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 190px 190px;
  opacity: var(--grain);
  mix-blend-mode: overlay;
}

.tile::after { inset: 0; }

/* Dans la visionneuse, l'image est centrée et sa taille dépend à la fois de
   la fenêtre et de ses propres contraintes. Le grain doit épouser sa boîte
   exactement, faute de quoi il déborde en un liseré clair sur le fond.

   Deux tentatives ont échoué avant celle-ci. Reproduire les contraintes de
   l'image sur un pseudo-élément voisin laissait dix pixels d'écart. Et
   l'envelopper d'un cadre cassait son dimensionnement : dans un conteneur
   flex, une image à max-height ne trouve plus de hauteur de référence et
   s'étire.

   La seule mesure juste est celle du navigateur : le script relève la boîte
   de l'image après chargement et après chaque redimensionnement, et la
   publie ici. Aucun calcul n'est refait de ce côté. */
.lightbox__stage::after {
  inset: 0;
  margin: auto;
  width: var(--imgw, 0);
  height: var(--imgh, 0);
}

.gallery {
  display: grid;
  gap: clamp(0.4rem, 1vw, 1rem);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 48rem) { .gallery { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 75rem) { .gallery { grid-template-columns: repeat(4, 1fr); } }

.tile {
  position: relative;
  display: block; width: 100%;
  padding: 0; overflow: hidden;
  background: var(--beige);
  aspect-ratio: 4 / 5;
  cursor: pointer;
}

.tile img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.9s var(--ease-expo), filter 0.6s var(--ease-soft);
}
/* `filter` remplace la déclaration entière : écrire `brightness()` seul ici
   effaçait le ton chaud pendant tout le survol, et l'image changeait de
   couleur sous le curseur. Le ton est donc rappelé devant. */
/* Chaque vignette est une boîte close : rien de ce qui s'y passe n'affecte la
   mise en page du reste. Le dire au navigateur lui évite de reconsidérer la
   grille entière — trente-six vignettes — à chaque recalcul. Aucun effet
   visible ; seulement du travail en moins. */
.tile { contain: layout paint; }

.tile:hover img, .tile:focus-visible img {
  transform: scale(1.04);
  filter: var(--ton) brightness(1.05);
}

/* Une fois la galerie dépliée, le bouton n'a plus grand-chose à dire et
   occupait une pleine hauteur de bouton juste avant le bandeau des maisons.
   Il s'efface — on replie en remontant, comme on est venu. */
.deplie .gallery__pied { display: none; }

/* Le reste de la galerie, déplié sur place au clic */
.gallery__more {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.9s var(--ease-expo);
}
.gallery__more.is-open { grid-template-rows: 1fr; }
.gallery__more > div { overflow: hidden; }
.gallery__more .gallery { padding-top: clamp(0.4rem, 1vw, 1rem); }

/* ================================================== 10. Maisons =========== */

/*
  Un seul trait, en haut. Celui du bas a été retiré **à toutes les largeurs** :
  le bandeau de clôture qui suit est un aplat d'encre pleine hauteur qui arrive
  en dôme, et une ligne claire juste avant lui se lit comme une bordure
  oubliée. Celui du haut reste — il sépare encore deux blocs de papier.
*/
.houses {
  border-top: 1px solid var(--line-soft);
  padding-block: clamp(3rem, 7vw, var(--s6));
}

/*
  Le bandeau défile en continu, de la droite vers la gauche. La file de logos
  est écrite deux fois et la piste se déplace de la moitié de sa largeur :
  au moment où elle revient à zéro, la seconde file occupe exactement la
  place de la première, et la boucle ne se voit pas. C'est ce qui donne
  l'impression d'un défilé sans fin.

  Les bords s'estompent : sans cela, les logos apparaîtraient et
  disparaîtraient d'un coup sur une arête franche.
*/
.houses__band {
  margin-top: var(--s5);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.houses__piste {
  display: flex;
  width: max-content;
  animation: defile 52s linear infinite;
}

.houses__file {
  display: flex;
  align-items: center;
  gap: clamp(2.5rem, 6vw, 5.5rem);
  padding-right: clamp(2.5rem, 6vw, 5.5rem);
}

/*
  La piste se déplace d'exactement **une file**, soit 100 / nombre-de-files
  pour cent. Le pas est donc lié au nombre de copies écrites par `pages.py`
  (`FILES_DEFILE`) : les deux se règlent ensemble, jamais l'un sans l'autre.

  À deux copies et −50 %, la boucle ne tenait que jusqu'à 1708 px de large —
  la largeur d'une file. Au-delà, du papier apparaissait après le dernier
  logo. Il en faut quatre : la condition est
  `(copies − 1) × 1708 ≥ largeur d'écran`, soit 5124 px de couverture.
*/
@keyframes defile {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-25%, 0, 0); }
}

/* Le survol suspend la course : on doit pouvoir lire un nom qui passe. */
.houses__band:hover .houses__piste { animation-play-state: paused; }

/* Le filtre aplatit noir, or et blanc en une seule silhouette sombre :
   c'est ce qui rend le bandeau homogène malgré huit chartes différentes. */
.houses__band img {
  width: auto; object-fit: contain;
  flex: none;
  filter: brightness(0);
  opacity: 0.42;
  transition: opacity 0.55s var(--ease-out);
}
.houses__band img:hover { opacity: 0.85; }

/* ================================================== 11. Appel ============= */

/*
  Le bloc de clôture s'inverse à mesure qu'il entre à l'écran. Mais il ne
  s'agit pas d'un fondu de couleurs : l'encre **monte** depuis le bas comme un
  rideau, et le texte se révèle derrière elle.

  Le fondu avait été essayé et il est impossible. Deux couleurs qui s'échangent
  entre les deux mêmes bornes se croisent nécessairement : à mi-parcours, fond
  et titre valaient exactement (0,574 0,539 0,506) l'un et l'autre — un
  contraste de 1:1, le titre purement invisible. Ici le texte n'est jamais que
  sur du papier ou sur de l'encre, jamais entre les deux.

  Le script publie --q, la progression de l'entrée dans la fenêtre : une
  fonction pure de la position de défilement, comme partout ailleurs.
*/
.cta {
  text-align: center;
  position: relative;
  padding-inline: var(--gutter);
  --t: clamp(0, calc(var(--q, 0) / 0.55), 1);
  --e: calc(var(--t) * var(--t) * (3 - 2 * var(--t)));

  /* Le rideau est le **fond** du bloc, pas un calque posé dessus : un dégradé
     d'encre dont la hauteur croît depuis le bas. Un pseudo-élément à
     `z-index: -1` avait été essayé et ne se peignait pas de façon fiable —
     couche négative dans un contexte d'empilement imbriqué. Ici il n'y a ni
     couche, ni contexte, ni ordre à arbitrer : c'est une image de fond. */
  background-image: linear-gradient(var(--ink), var(--ink));
  background-repeat: no-repeat;
  background-position: bottom;
  background-size: 100% calc(var(--e, 0) * 100%);
}

/* Le texte est papier de bout en bout ; seule son opacité le retient tant que
   l'encre n'est pas passée derrière lui. */
.cta__title,
.cta .btn {
  color: var(--paper);
  /* Le rideau d'encre est complet à q = 0,55 : le texte ne commence pas
     avant. À 0,42 il paraissait encore sur les deux derniers pour cent de
     papier restant en haut du bloc. */
  opacity: clamp(0, calc((var(--q, 0) - 0.55) / 0.20), 1);
}

.cta .btn { border-color: rgba(250, 247, 242, 0.42); }
.cta .btn::before { background: var(--paper); }
.cta .btn:hover, .cta .btn:focus-visible {
  color: var(--ink);
  border-color: var(--paper);
}

.cta__title {
  /* Ramené d'un cran : à 52 px il pesait plus lourd que les titres des
     écrans de l'accueil, qui sont pourtant la pièce maîtresse. */
  font-size: var(--fs-h2);
  font-weight: 200;
  margin-bottom: var(--s4);
}

.btn {
  position: relative;
  display: inline-flex; align-items: center;
  min-height: 48px;
  padding: 0.85rem 2.4rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: var(--fs-micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink);
  overflow: hidden;
  z-index: 0;
  transition: color 0.5s var(--ease-out), border-color 0.5s var(--ease-out);
}

/* Le fond monte du bas, et le libellé se relaie : celui du haut sort, son
   double entre par en dessous. Deux mouvements, une seule impression. */
.btn__label { display: block; position: relative; overflow: hidden; }
.btn__label span {
  display: block;
  transition: transform 0.55s var(--ease-expo);
}
.btn__label span::after {
  content: attr(data-label);
  position: absolute;
  left: 0; top: 100%;
  width: 100%;
}
.btn:hover .btn__label span,
.btn:focus-visible .btn__label span { transform: translateY(-100%); }

.btn::before {
  content: '';
  position: absolute; inset: 0;
  background: var(--ink);
  transform: translateY(101%);
  transition: transform 0.55s var(--ease-expo);
  z-index: -1;
}
.btn:hover, .btn:focus-visible { color: var(--paper); border-color: var(--ink); }
.btn:hover::before, .btn:focus-visible::before { transform: translateY(0); }

.btn--solid { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.btn--solid::before { background: var(--earth); }

/* ================================================== 12. Formulaire ======== */

.form { display: grid; gap: var(--s4); max-width: 42rem; }
.field { display: grid; gap: 0.5rem; }

.field label {
  font-size: var(--fs-micro);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

.field input, .field textarea {
  width: 100%; min-height: 46px;
  padding: 0.7rem 0;
  background: transparent;
  border: 0; border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-family: var(--sans); font-size: 1rem; font-weight: 300;
  border-radius: 0;
  transition: border-color 0.4s var(--ease-out);
}
.field textarea { resize: vertical; min-height: 5.5rem; line-height: 1.6; }
.field input:focus, .field textarea:focus { outline: none; border-bottom-color: var(--earth); }

.field__hint { font-size: var(--fs-small); color: var(--muted); }
.field__error { font-size: var(--fs-small); color: #A33C1E; min-height: 1.2em; }
.field.has-error input, .field.has-error textarea { border-bottom-color: #A33C1E; }

.form__status { font-size: var(--fs-small); color: var(--earth); min-height: 1.4em; }

/* La page contact tient sur un écran : plus de défilement, une seule vue.
   Tout y est resserré — marges, interlignes des champs, hauteur du pied —
   pour que le bouton Envoyer reste au-dessus de la ligne de flottaison. */
.contact-screen {
  /* Plus de réserve pour le pied : la page contact n'en a plus depuis le
     9 septembre 2026, la mention y ayant été retirée. */
  min-height: 100lvh;
  display: grid;
  align-items: center;
  padding: 5.25rem 0 1.5rem;
}
/* Le titre se centre sur la hauteur du formulaire au lieu de flotter en haut
   d'une moitié vide. Le formulaire, lui, ne bouge pas : il remplit déjà la
   hauteur d'écran, et le descendre pousserait le bouton Envoyer sous le bord.
   `.split` aligne ses colonnes en haut ; seule celle-ci s'en écarte. */
.contact-screen .contact-intro { align-self: center; }

/* L'adresse et l'icône, sous le titre. La page ne portait que le formulaire —
   parti-pris longtemps tenu, levé par le client le 1er septembre 2026. */
.contact-mail { margin-top: var(--s4); }
.contact-mail a {
  font-size: var(--fs-small);
  letter-spacing: 0.05em;
  padding-bottom: 0.15em;
  border-bottom: 1px solid rgba(250, 247, 242, 0.28);
  transition: border-color 0.4s var(--ease-out);
}
.contact-mail a:hover,
.contact-mail a:focus-visible { border-bottom-color: var(--paper); }

.contact-social { margin-top: var(--s4); }
.contact-screen--encre .contact-mail a { color: var(--paper); }
.contact-screen--encre .social {
  color: rgba(250, 247, 242, 0.78);
  border-color: rgba(250, 247, 242, 0.28);
}
.contact-screen--encre .social:hover,
.contact-screen--encre .social:focus-visible {
  color: var(--ink); background: var(--paper); border-color: var(--paper);
}

/*
  Le mot caché derrière l'icône.

  Il est **hors du flux** : `.social` est une grille de 44 px qui centre son
  glyphe, et un second enfant en flux y ajouterait une rangée, décentrant
  l'icône. En absolu il ne crée aucun élément de grille.

  Le dévoilement se fait au `clip-path` et non à l'opacité seule : le mot sort
  de derrière l'icône lettre après lettre, comme tiré d'une fente. C'est cela,
  l'effet de surprise demandé — une opacité aurait fait paraître un texte qui
  était déjà là.
*/
.social--reveal { position: relative; }
.social__mot {
  position: absolute;
  left: calc(100% + 0.9rem);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  pointer-events: none;
  font-size: var(--fs-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper);
  opacity: 0;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.75s var(--ease-expo), opacity 0.45s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .social--reveal:hover .social__mot { opacity: 1; clip-path: inset(0 0 0 0); }
}
.social--reveal:focus-visible .social__mot { opacity: 1; clip-path: inset(0 0 0 0); }

/* Sans souris, il n'y a pas de survol : le mot se dévoile alors au défilement,
   sur --v publié par le module Vue. Même geste, même durée — c'est le doigt
   qui remplace le curseur. */
@media (hover: none), (any-pointer: coarse) {
  .contact-social { --r: clamp(0, calc((var(--v, 0) - 0.30) / 0.22), 1); }
  .social__mot {
    opacity: var(--r, 0);
    clip-path: inset(0 calc((1 - var(--r, 0)) * 100%) 0 0);
    transition: none;
  }
}

/* Sous 768 px le mot est passé sous l'icône : il se dévoile donc par le haut,
   et non plus par la gauche. La règle vient après celle du dessus, qu'elle
   corrige pour cette largeur seulement. */
@media (any-pointer: coarse) and (max-width: 47.99rem) {
  .social__mot { clip-path: inset(0 0 calc((1 - var(--r, 0)) * 100%) 0); }
}

.contact-screen .field { gap: 0.3rem; }
.contact-screen .form { gap: 1.1rem; }
/* 48 px et non 42 : le seuil tactile est à 44, et un champ de saisie doit
   l'atteindre comme un bouton. Le corps reste à 16 px — en deçà, iOS zoome à
   la mise au point et l'on sort de la page pour y revenir de travers. */
.contact-screen .field input,
.contact-screen .field textarea { min-height: 48px; padding: 0.5rem 0; }
.contact-screen .field textarea { min-height: 4.5rem; }
.contact-screen .field__error { min-height: 0; }
.contact-screen .field__hint { font-size: var(--fs-micro); }
.contact-screen .form__status { min-height: 0; }
.contact-screen ~ .shell .footline,
body:has(.contact-screen) .footline { padding: 1.1rem 0; }


/*
  ------------------------------------------- La page contact sur téléphone

  Elle devait tenir sur un écran ; elle débordait de **101 px** sur un iPhone
  de 812, et le bouton Envoyer s'arrêtait à 807 — cinq pixels sous la ligne de
  flottaison, donc invisible sans défiler. Signalé le 8 septembre 2026.

  Le client a demandé de resserrer **sans rien retirer** : les quatre champs
  restent, la ligne d'aide reste, l'icône Instagram reste. Ce qui cède, ce
  sont les intervalles — et seulement eux. Les 48 px de hauteur des champs ne
  bougent pas : c'est le seuil tactile, et un champ de saisie doit l'atteindre
  comme un bouton.

  Le bouton se centre, à sa demande. Il est seul sur sa ligne, en fin de
  formulaire : `:has(.btn)` désigne son paragraphe sans qu'on ait à compter
  les frères.

  Honnêteté sur la portée : cela suffit sur un grand téléphone. Dans Safari,
  barres déployées, il reste une centaine de pixels de moins ; sur un petit
  appareil, moins encore. Un léger défilement y subsistera, et le supprimer
  demanderait de retirer quelque chose — ce que le client a écarté.
*/
@media (max-width: 47.99rem) {
  .contact-screen { padding: 5rem 0 0.25rem; }
  .contact-screen h1 { line-height: 1.14; }
  .contact-screen .contact-social { margin-top: var(--s2); }
  .contact-screen .form { gap: 0.6rem; }
  .contact-screen .field { gap: 0.2rem; }
  .contact-screen .field textarea { min-height: 3.25rem; }
  .contact-screen .field__hint { line-height: 1.35; }
  body:has(.contact-screen) .footline { padding: 0.5rem 0; }

  /* Le bouton au centre de l'écran, et non plus contre le bord gauche. */
  .contact-screen .form > p:has(.btn) { justify-self: center; }
}

/* La page contact répond au bandeau qui y mène : même encre, même clarté.
   La barre du haut doit suivre, sans quoi son encre disparaîtrait sur
   l'encre. */
.contact-screen--encre { background: var(--ink); color: var(--paper); }
.contact-screen--encre h1 { color: var(--paper); }
.contact-screen--encre .field label { color: rgba(250, 247, 242, 0.7); }
.contact-screen--encre .field input,
.contact-screen--encre .field textarea {
  color: var(--paper);
  border-bottom-color: rgba(250, 247, 242, 0.26);
}
.contact-screen--encre .field input:focus,
.contact-screen--encre .field textarea:focus { border-bottom-color: var(--paper); }
.contact-screen--encre .field__hint,
.contact-screen--encre .form__status { color: rgba(250, 247, 242, 0.62); }
.contact-screen--encre .field__error { color: #E9A088; }
.contact-screen--encre .todo {
  color: rgba(250, 247, 242, 0.8);
  border-bottom-color: rgba(250, 247, 242, 0.4);
}
.contact-screen--encre .btn { color: var(--paper); border-color: rgba(250, 247, 242, 0.4); }
.contact-screen--encre .btn::before { background: var(--paper); }
.contact-screen--encre .btn:hover,
.contact-screen--encre .btn:focus-visible { color: var(--ink); border-color: var(--paper); }

body:has(.contact-screen--encre) .topbar__title,
body:has(.contact-screen--encre) .topbar__lang { color: var(--paper); }
body:has(.contact-screen--encre) .dots__btn i { background: var(--paper); }
body:has(.contact-screen--encre) .dots__menu a { color: var(--paper); }

/* ================================================== 13. Pied ============== */

/* Réduit à une ligne centrée */
/* Le pied de page vit hors de <main>, et l'ouverture — collée, et rendue plus
   courte dans le flux par sa marge négative — débordait sous lui : sa
   photographie réapparaissait dans les cent derniers pixels de l'accueil. Le
   pied doit donc porter son propre fond et son propre plan. */
/* L'ouverture ne se peint plus une fois la galerie largement dépassée : voir
   le commentaire du script, à `ouverture-close`. Conditionné à `.js`, comme
   tout ce qui masque du contenu — sans script, la classe n'arrive jamais. */
.js.ouverture-close .opening { visibility: hidden; }

.foot {
  position: relative;
  z-index: 20;
  background: var(--paper);
  /* L'encre prolongée d'une hauteur d'écran vers le bas, pour la seule zone
     de rebond. Décalage et étalement valent tous deux 50 vh : le rectangle
     part donc exactement du haut du pied et descend d'une hauteur pleine,
     sans laisser l'interstice qu'un simple décalage aurait ouvert. */
  box-shadow: 0 50vh 0 50vh var(--ink);
}

.footline {
  border-top: 1px solid var(--line-soft);
  padding: var(--s4) 0;
  text-align: center;
  font-size: var(--fs-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}

.todo {
  color: var(--earth);
  border-bottom: 1px dashed rgba(110, 73, 58, 0.4);
  font-style: normal;
}

/* ========================================= 13 bis. Bandes floues ========= */

/*
  Trois calques par bord, chacun plus flou que le précédent et masqué plus
  près de l'arête. C'est ce dégradé de rayons — et non un flou unique posé en
  fondu — qui donne l'estompage progressif : au ras du bord tout est brouillé,
  vers l'intérieur l'image redevient nette sans qu'on puisse dire où.

  Rien n'est caché : aucun calque n'est opaque, on voit les éléments passer
  au travers pendant le défilement.

  Chaque bande est bornée à sa propre zone plutôt qu'étendue à toute la
  fenêtre. C'est ce qui rend l'effet tenable : le compositeur ne recopie que
  quelques milliers de pixels par image, pas trois pleins écrans.
*/
.edgeblur {
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
}

.edgeblur__bande { position: absolute; overflow: hidden; }
.edgeblur__bande--g { top: 0; bottom: 0; left: 0;  width: var(--cote); }
.edgeblur__bande--d { top: 0; bottom: 0; right: 0; width: var(--cote); }
.edgeblur__bande--b { left: 0; right: 0; bottom: 0; height: var(--bas); }

.edgeblur__bande i { position: absolute; inset: 0; display: block; }

/* Du plus large et du plus léger au plus étroit et au plus flou. */
/* Du flou, et rien d'autre.

   Deux tentatives d'« effet verre » ont échoué ici, et pour deux raisons
   distinctes. `brightness()` portait le papier de (250,247,242) à
   (254,254,253) — onze niveaux d'écart, un rectangle plus clair visible en
   travers de la page. `saturate()`, lui, ne se voyait pas sur le papier mais
   virait franchement à l'orange sur les photographies chaudes du site, qui
   sont presque toutes chaudes.

   Un bord flouté n'a besoin ni de l'un ni de l'autre : le dégradé de rayons
   suffit à faire la matière. Ne pas les remettre. */
.edgeblur__bande i:nth-child(1) { --f: 100%; backdrop-filter: blur(2px);  -webkit-backdrop-filter: blur(2px); }
.edgeblur__bande i:nth-child(2) { --f: 62%;  backdrop-filter: blur(7px);  -webkit-backdrop-filter: blur(7px); }
.edgeblur__bande i:nth-child(3) { --f: 30%;  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); }

/*
  Au doigt, une couche de flou en moins.

  C'est le poste le plus cher du site pendant le défilement : un
  `backdrop-filter` oblige le compositeur à recopier puis à flouter le fond
  **à chaque image**, et le coût suit l'aire multipliée par le rayon. Trois
  couches par bande, trois bandes, font neuf recopies par image sur l'écran
  d'accueil — celui-là même où l'on défile le plus.

  La couche retirée est la plus floue et la plus étroite (30 % de la bande, un
  rayon de 18) : c'est aussi la plus coûteuse, le rayon entrant directement
  dans le calcul. La deuxième s'élargit un peu et son rayon monte à 10 pour
  reprendre la profondeur perdue. Le dégradé de matière reste, à un tiers de
  couches en moins.

  Borné à `hover: none` : sur ordinateur, où le coût ne se sent pas, les trois
  couches restent.
*/
@media (hover: none) {
  .edgeblur__bande i:nth-child(2) { --f: 52%; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
  .edgeblur__bande i:nth-child(3) { display: none; }

  /*
    **Les deux bandes latérales s'éteignent.** À 3,5 vw elles mesurent 13 px
    sur un téléphone de 375 : on ne les voit pas, mais chacune porte deux
    `backdrop-filter` pleine hauteur que le compositeur refait à chaque image.
    Quatre couches recomposées en continu pour un effet invisible.

    Celle du bas reste : elle fait 375 × 81 px, elle se lit, et c'est elle que
    `PHOTOGRAPHIE` traverse en montant sur l'écran 2.
  */
  .edgeblur__bande--g,
  .edgeblur__bande--d { display: none; }
}

.edgeblur__bande--g i {
  -webkit-mask-image: linear-gradient(to right, #000 0, transparent var(--f));
          mask-image: linear-gradient(to right, #000 0, transparent var(--f));
}
.edgeblur__bande--d i {
  -webkit-mask-image: linear-gradient(to left, #000 0, transparent var(--f));
          mask-image: linear-gradient(to left, #000 0, transparent var(--f));
}
.edgeblur__bande--b i {
  -webkit-mask-image: linear-gradient(to top, #000 0, transparent var(--f));
          mask-image: linear-gradient(to top, #000 0, transparent var(--f));
}

/* Le lait de lumière du bord : c'est lui qui fait « passer à la
   transparence » plutôt que buter sur une arête nette. */
.edgeblur__bande--g { background: linear-gradient(to right, rgba(250, 247, 242, var(--voile)), transparent); }
.edgeblur__bande--d { background: linear-gradient(to left,  rgba(250, 247, 242, var(--voile)), transparent); }
.edgeblur__bande--b { background: linear-gradient(to top,   rgba(250, 247, 242, var(--voile)), transparent); }

/* Seule la bande du bas s'éteint, et bien avant que la galerie n'arrive :
   c'est elle qui brouillait le bas des vignettes et le pied de page. Les
   côtés, eux, restent allumés d'un bout à l'autre de l'accueil — ce sont eux
   qui tiennent le cadre.

   Elle se **rétracte** — sa hauteur tombe à zéro — au lieu de s'effacer. Une
   opacité animée sur l'ancêtre d'un `backdrop-filter` change la racine du
   fond capturé pendant la transition : la bande virait au blanc pendant une
   seconde avant de disparaître. La hauteur, elle, ne touche à rien. */
.edgeblur__bande--b { transition: height 0.5s var(--ease-soft); }
.bas-eteint .edgeblur__bande--b { height: 0; }

/* Sans backdrop-filter, un calque masqué ne floute rien : il ne resterait
   qu'un halo clair sans raison. On préfère ne rien afficher. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .edgeblur { display: none; }
}

/* ================================================== 14. Visionneuse ======= */

.lightbox {
  position: fixed; inset: 0;
  z-index: 200;
  background: rgba(250, 247, 242, 0.96);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  display: grid;
  grid-template-rows: auto 1fr auto;
  opacity: 0; visibility: hidden;
  /* La visibilité ne s'interpole pas : mise en transition, sa valeur calculée
     vaut encore `hidden` à l'instant zéro et focus() serait sans effet. */
  transition: opacity 0.45s var(--ease-soft), visibility 0s linear 0.45s;
}
.lightbox.is-open {
  opacity: 1; visibility: visible;
  transition: opacity 0.45s var(--ease-soft), visibility 0s linear 0s;
}

.lightbox__bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s3) var(--gutter);
  font-size: var(--fs-micro);
  letter-spacing: 0.22em;
  color: var(--muted);
}

/* Flex et non grid : dans une grille, la ligne implicite créée par
   place-items est auto-dimensionnée, donc le max-height:100% de l'image ne
   se résout pas et la photo déborde de l'écran. */
.lightbox__stage {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  padding: 0 var(--gutter);
  min-height: 0; overflow: hidden;
}

.lightbox__stage img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain;
  opacity: 0; transform: scale(0.985);
  transition: opacity 0.5s var(--ease-out), transform 0.6s var(--ease-expo);
}
.lightbox__stage img.is-shown { opacity: 1; transform: none; }

.lightbox__foot {
  display: flex; align-items: center; justify-content: center;
  gap: var(--s4);
  padding: var(--s3) var(--gutter) var(--s5);
}

.lb-btn {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  transition: color 0.3s, border-color 0.3s, background 0.3s;
  flex: none;
}
.lb-btn:hover { color: var(--paper); background: var(--ink); border-color: var(--ink); }
.lb-btn svg { width: 16px; height: 16px; }

/* ================================================== 15. Curseur =========== */

.cursor { display: none; }

@media (hover: hover) and (pointer: fine) {
  /* Sur l'ouverture, l'anneau remplace le curseur du système. Nulle part
     ailleurs : partout où l'on clique, la flèche doit rester. */
  .opening { cursor: none; }

  .cursor {
    display: grid; place-items: center;
    position: fixed; top: 0; left: 0;
    z-index: 210;
    width: 72px; height: 72px;
    margin: -36px 0 0 -36px;
    border-radius: 999px;
    background: var(--ink); color: var(--paper);
    font-size: 0.6rem; letter-spacing: 0.2em; text-transform: uppercase;
    pointer-events: none;
    opacity: 0; transform: scale(0.2);
    transition: opacity 0.35s var(--ease-out), transform 0.45s var(--ease-expo);
    will-change: transform;
  }
  .cursor.is-on { opacity: 1; transform: scale(1); }
}

/* ============================================ 16. Blocs de contenu ======== */

/* L'état masqué est conditionné à la classe `js`, posée par un script en
   ligne dans le <head>. Sans script, le texte s'affiche normalement : du
   contenu ne doit jamais dépendre d'une animation pour exister. */
.js .rise {
  opacity: 0;
  transform: translateY(1.1rem);
  /* Raccourci depuis 0,9 s / 1 s : sur les pages intérieures, l'apparition
     traînait derrière le geste. Le bloc doit être arrivé quand l'œil s'y
     pose, pas une demi-seconde plus tard. */
  transition: opacity 0.55s var(--ease-out), transform 0.7s var(--ease-expo);
}
.rise.is-in { opacity: 1; transform: none; }
.rise-1 { transition-delay: 0.06s; }
.rise-2 { transition-delay: 0.12s; }

/* Instagram : le bouton seul, sans texte. Il reprend la pastille des
   commandes de la visionneuse, la seule forme circulaire du site — et sa
   zone tactile de 44 px, sans laquelle un glyphe de 18 px serait
   inatteignable au doigt. Le pseudo n'est pas écrit, il est dit : aria-label. */
.social {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  transition: color 0.3s var(--ease-out), background 0.3s var(--ease-out),
              border-color 0.3s var(--ease-out);
}
.social svg { width: 18px; height: 18px; }
.social:hover, .social:focus-visible {
  color: var(--paper); background: var(--ink); border-color: var(--ink);
}

.split { display: grid; gap: clamp(2.5rem, 6vw, var(--s6)); align-items: start; }
@media (min-width: 60rem) { .split { grid-template-columns: 1fr 1fr; } }

.portrait img { width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }

/* L'accroche de la page « à propos ». En capitales espacées, elle cesse
   d'être une phrase parmi d'autres pour devenir une déclaration — et elle
   arrive lettre par lettre, seule animation d'apparition du site qui ne
   dépende pas du défilement : la page n'en a pas besoin, on y arrive. */
.accroche {
  font-size: clamp(0.95rem, 1.5vw, 1.2rem);
  font-weight: 300;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  line-height: 1.7;
  color: var(--ink);
}
.accroche .lettre {
  display: inline-block;
  white-space: pre;
}
.is-ready .accroche .lettre {
  animation: lettre-in 0.9s var(--ease-out) both;
  animation-delay: calc(0.35s + var(--i, 0) * 0.022s);
}
@keyframes lettre-in {
  from { opacity: 0; transform: translateY(0.5em); }
  to   { opacity: 1; transform: none; }
}

/* Le nom tient lieu de titre de page : plus de surtitre au-dessus, il ne
   disait rien que la page ne dise déjà. En capitales espacées, comme le
   reste du site. */
.nom {
  font-size: var(--fs-h1);
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  /* Pas d'indentation ici : elle ne décale que la **première** ligne, et le
     nom en tient deux — « ALEXANDRE » se retrouvait 8 px à droite de
     « VUILLAUME ». La compensation d'espacement ne vaut que pour un mot seul. */
  line-height: 1.15;
}

/* Plus de filet au-dessus des services : retiré le 2 septembre 2026. Sur
   grand écran il n'aurait plus de sens — un seul volet s'affiche à la fois —
   et sur petit écran l'espace suffit à séparer les trois. */
.service {
  position: relative;
  display: grid; gap: var(--s3);
  padding-top: var(--s4);
}

/* Au survol, le disque enfle et se teinte, le titre avance de quelques
   pixels, le filet du haut se fonce. Trois gestes minuscules qui, ensemble,
   font répondre le bloc entier plutôt qu'un lien isolé. */
@media (hover: hover) and (pointer: fine) {
  .service:hover .puce i { transform: scale(1.16); opacity: 0.8; }
  .service:hover h3 { transform: translateX(0.35rem); }
  .service:hover .service__body { color: var(--ink); }
}

.service h3 { transition: transform 0.6s var(--ease-expo); }
.service__body { transition: color 0.5s var(--ease-out); }
.services { display: grid; gap: var(--s5); }

/* --- La page services --- */

/*
  Sur ordinateur, une scène épinglée. Le titre, l'accroche et les trois noms de
  service ne bougent pas ; seuls le paragraphe et la photographie changent, et
  **une seule de chaque est visible à la fois** — on ne voit pas les suivants
  tant qu'on n'a pas défilé.

  Chaque bloc connaît son centre de plage, `--c` : 0 pour le premier service,
  0,5 pour le deuxième, 1 pour le troisième. Sa visibilité ne dépend que de
  l'écart entre --p et ce centre. Les plages ne se recouvrent pas : le bloc
  sortant a fini de s'effacer avant que l'entrant ne commence à paraître.

  Sous 55 rem, tout autre chose : un service par écran, que l'on fait glisser
  du doigt. Voir plus bas.
*/

.services { display: grid; gap: var(--s5); }

/* Le titre et son accroche se collent à gauche, alignés sur les noms de
   service et sur la description : la page tient désormais sur un seul axe.
   Sur téléphone aussi — un titre centré au-dessus d'un texte aligné à gauche
   se lisait comme deux compositions superposées. */
.services__h1,
.services__lead { text-align: left; margin-inline: 0; }

.services__h1 {
  font-size: clamp(1.6rem, 4.6vw, 3.4rem);
  font-weight: 200;
  line-height: 1.15;
  margin: 0;
  /* Un **retrait intérieur**, et non une marge. La barre du haut est fixe et
     flotte au-dessus du contenu : sans dégagement, le titre lui passait dessous
     sur téléphone. Mais une marge haute posée ici **fusionne** avec celles de
     ses parents — `.shell` et le bloc épinglé n'ayant ni bordure ni retrait —
     et ressort de la scène, qu'elle poussait alors de 88 px vers le bas : le
     bas des photographies et les trois noms de service tombaient sous le bord
     de l'écran. Un padding ne fusionne pas. */
  padding-top: clamp(4rem, 11vh, 6rem);
}
.services__lead { max-width: 64ch; }

/* Le titre monte d'un bloc derrière une ligne invisible, toutes les lettres
   ensemble — le geste des cinq écrans de l'accueil, déclenché ici à l'arrivée
   sur la page : la scène est épinglée, elle n'a rien à attendre. Les marges
   négatives laissent passer les accents des capitales. */
.services__h1 .mot {
  display: inline-block;
  overflow: hidden;
  padding: 0.16em 0 0.08em;
  margin: -0.16em 0 -0.08em;
  vertical-align: bottom;
}
.services__h1 .mot > i {
  display: inline-block;
  font-style: normal;
}
/* L'état masqué est conditionné à `.js`, posée par un script en ligne du
   <head>. Sans cette condition, un visiteur dont le script ne s'exécute pas
   garderait le titre **caché pour toujours** derrière sa fenêtre de rognage :
   c'est `.is-ready` qui le libère, et elle n'arriverait jamais. Le piège est
   déjà consigné pour le reste du site ; il a été réintroduit ici. */
.js .services__h1 .mot > i { transform: translate3d(0, 118%, 0); }
.is-ready .services__h1 .mot > i {
  animation: titre-monte 1.1s var(--ease-expo) 0.25s both;
}
@keyframes titre-monte {
  from { transform: translate3d(0, 118%, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* --- Les noms de service --- */

.services__titre {
  position: relative;
  display: block;
  width: fit-content;
  margin: 0;
  padding: 0.45rem 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink);
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
  /* |2p − i| : nul pour le service affiché, 1 pour son voisin. */
  --ecart: max(calc(var(--p, 0) * 2 - var(--i)), calc(var(--i) - var(--p, 0) * 2));
  --proche: clamp(0, calc(1 - var(--ecart)), 1);
  transition: opacity 0.35s var(--ease-out);
}

/*
  Le disque. Deux choses le déplacent, à ne pas confondre : une **position de
  repos propre à chaque service** — sans elle les trois se superposeraient au
  même endroit — et une **aimantation au survol**, --mx et --my publiés par la
  fonction `aimante()`, partagée avec le menu.
*/
.services__titre .puce {
  right: auto;
  left: calc(var(--repos) * 100% + var(--mx, 0) * 0.5px);
  top: calc(50% + var(--my, 0) * 0.5px);
  transform: translate(-50%, -50%);
  transition: left 0.5s var(--ease-out), top 0.5s var(--ease-out);
}
/* Le troisième disque rejoint le premier ; seul celui du milieu s'en écarte.
   *(Une progression 14 – 50 – 86 l'a précédée : le client a demandé que le
   troisième revienne sur le premier.)* */
.services__titre:nth-of-type(1) { --repos: 0.14; }
.services__titre:nth-of-type(2) { --repos: 0.50; }
.services__titre:nth-of-type(3) { --repos: 0.14; }

.services__titre .puce i {
  opacity: calc(var(--proche) * 0.5);
  transform: scale(calc(0.7 + var(--proche) * 0.3));
}

@media (hover: hover) and (pointer: fine) {
  /*
    Au survol d'un nom, ce sont les **deux autres** qui reculent : celui qu'on
    désigne reste seul net. Le geste est inhabituel, et c'est ce qui le rend
    lisible — le survol précédent glissait de 6 px sur un texte de 29, et
    personne ne le voyait.

    `:has()` sur le conteneur dit exactement cela : « quand un nom est survolé,
    tous ceux qui ne le sont pas ». Aucune classe, aucun script.

    À savoir, et le client l'a accepté : pendant le survol, les deux noms en
    retrait passent sous le seuil de contraste. C'est un état transitoire, qui
    n'existe que sous la souris et jamais au repos — mais c'en est un.
  */
  .services__index:has(.services__titre:hover) .services__titre:not(:hover) {
    opacity: 0.2;
  }
  /* Le disque, lui, ne s'efface pas. Il marque le service que l'on est en train
     de lire : le faire disparaître pendant qu'on survole un autre nom ôterait
     le seul repère de position de la page. */

  /*
    Le nom survolé avance franchement — 20 px au lieu de 6 — et **son disque le
    suit** au lieu de rester en arrière. Le geste précédent glissait de 6 px sur
    un texte de 29 : le client ne le voyait pas, et l'a redemandé deux fois.

    Le disque garde son décalage magnétique : `--mx` et `--my` s'ajoutent au
    déplacement, ils ne le remplacent pas.
  */
  .services__titre:hover { opacity: 1 !important; }
  .services__titre:hover > span:first-child { transform: translateX(1.25rem); }
  .services__titre:hover .puce {
    transform: translate(calc(-50% + 1.25rem), -50%);
  }
  .services__titre:hover .puce i { opacity: 0.62; transform: scale(1.35); }
}
.services__titre > span:first-child {
  display: inline-block;
  transition: transform 0.55s var(--ease-expo);
}

/* ============ Grand écran : la scène s'épingle ============ */

/*
  L'écran se partage en deux moitiés. À droite, la photographie, du bas de la
  barre d'en-tête au bas de la fenêtre — la barre est sa limite haute, et elle
  monte donc **à côté** du titre, non en dessous. À gauche, tout le reste :
  l'en-tête en haut, puis les noms de service et la description, centrés
  ensemble dans la hauteur qui reste.

  Six jetons décrivent la composition, et il n'y a rien d'autre à toucher pour
  la régler :

    --barre    la hauteur de la barre du haut, recalculée à l'identique
    --media    la part d'écran donnée à la photographie
    --entete   la bande du titre et de son accroche
    --marge    le bord gauche de la coquille, reconstitué
    --noms     la colonne des trois noms de service
    --ecart    le blanc entre les colonnes
*/
/*
  La page services bascule sur **la nature du pointeur**, non sur la largeur.

  Elle le faisait à 55 rem seulement : un iPad en paysage (1024 px) recevait
  donc la version bureau — scène épinglée, noms dont l'état actif se révèle au
  survol, disque magnétique qui suit un curseur inexistant — et le même iPad
  tourné en portrait (820 px) recevait la visionneuse à glisser. Deux pages
  entièrement différentes selon l'orientation.

  `(hover: hover) and (pointer: fine)` décrit le pointeur **principal** : un
  portable à écran tactile mais piloté au trackpad garde la version bureau,
  une tablette au doigt reçoit la visionneuse dans les deux sens. C'est la
  discrimination juste — et plus fine qu'`any-pointer: coarse`, qui aurait
  basculé les portables tactiles.
*/
@media (min-width: 55rem) and (hover: hover) and (pointer: fine) {
  .js .services-scene { position: sticky; top: 0; height: 100lvh; overflow: hidden; }

  /*
    Deux rangs, et le rythme devient une **conséquence** au lieu d'un calcul :
    l'en-tête prend la hauteur qu'il lui faut, le corps prend le reste, et le
    même `--rythme` sépare la barre du titre et le titre des noms de service.
    Aucune hauteur d'en-tête à deviner — c'est ce qu'une version précédente
    faisait, avec 59 px d'un côté et 183 de l'autre.

    Le bloc reste positionné : la photographie s'y accroche pour aller de la
    barre au bas de la fenêtre, en traversant les deux rangs.
  */
  .js .services-scene__pin {
    position: relative;
    height: 100%;
    display: grid;
    grid-template-rows: auto 1fr;

    /* La barre du haut tire sa hauteur de la **largeur** de la fenêtre : deux
       fois son retrait, plus les 44 px de sa zone tactile. */
    --barre: calc(2 * clamp(1rem, 2.4vw, 1.75rem) + 2.75rem);
    --rythme: clamp(3rem, 13vh, 8rem);
    /* Le bas des trois points du menu, reconstitué : le retrait de la barre,
       plus la moitié de sa zone tactile. C'est **de là** que se mesure le
       rythme du haut — mesuré du bas de la barre, l'écart valait 46 px de trop
       et les deux respirations n'étaient plus égales. */
    --points-bas: calc(clamp(1rem, 2.4vw, 1.75rem) + 1.625rem);
    --media: clamp(11rem, 31vw, 34rem);
    /* Sous 96 rem la coquille vaut la gouttière, au-delà elle s'en écarte de la
       moitié du débord. */
    --marge: max(var(--gutter), calc((100vw - 96rem) / 2 + var(--gutter)));
    --noms: clamp(11rem, 20vw, 17rem);
    --ecart: clamp(1.5rem, 3vw, 2.5rem);
  }

  /* --- L'en-tête, calé à gauche --- */

  .js .services__entete {
    display: grid;
    /* `justify-content` **et** `justify-items` : sans le premier, la colonne
       implicite s'étire et l'accroche, dont `min-width: 100%` vise cette
       colonne, déborderait le titre. */
    justify-content: start;
    justify-items: start;
    padding: calc(var(--points-bas) + var(--rythme)) var(--gutter) 0 var(--marge);
  }
  .js .services__h1 {
    white-space: nowrap;
    font-size: min(4.2vw, 3.4rem);
    padding-top: 0;
  }
  .js .services__lead {
    width: 0;
    min-width: 100%;
    max-width: none;
    font-size: clamp(0.85rem, 1.05vw, 0.95rem);
    line-height: 1.5;
    margin-top: clamp(0.6rem, 1.4vh, 1rem);
  }

  /* --- Le corps : noms | description | image --- */

  .js .services__corps {
    display: grid;
    grid-template-columns: var(--noms) minmax(0, 1fr);
    column-gap: var(--ecart);
    padding: 0 0 0 var(--marge);
    /* Le corps occupe le second rang, donc **du bas du titre au bas de
       l'écran** : `align-content: center` l'y centre, ce qui est exactement la
       demande. `align-items: start` reste, lui : les noms et la description
       partagent le même bord haut, et c'est ce qui les tient alignés.

       *(Un rythme égal — même écart au-dessus du titre et au-dessous — a
       précédé ce centrage, du 3 au 4 septembre.)* */
    align-content: center;
    align-items: start;
    min-height: 0;
  }

  .js .services__index {
    display: grid;
    gap: clamp(0.6rem, 1.8vh, 1.1rem);
    justify-items: start;
  }
  .js .services__titre {
    font-size: clamp(1.2rem, 2vw, 1.85rem);
    font-weight: 200;
    letter-spacing: 0.09em;
    line-height: 1.25;
    /* Plancher à 0,50 : c'est un seuil, non un goût. À 0,26 l'encre composée
       donnait 1,72:1, sous les 3,00 exigés pour ce corps. Ne pas redescendre. */
    opacity: calc(0.5 + var(--proche) * 0.5);
  }

  /*
    Les trois planches se superposent dans **une seule case de grille**, et non
    par positionnement absolu. C'est ce qui permet à la photographie, elle
    absolue, de s'accrocher au bloc épinglé et de traverser les deux rangs :
    une planche positionnée serait devenue son bloc conteneur, et l'image
    n'aurait plus pu remonter jusqu'à la barre.
  */
  .js .services__plan { display: grid; }
  .js .services__plan > .service {
    grid-area: 1 / 1;
    /* `position: static`, et c'est décisif. La règle de base `.service` est en
       `relative` : elle captait la photographie, qui s'y ancrait au lieu de
       s'ancrer au bloc épinglé — son cadre tombait à 117 px de haut au milieu
       de l'écran, au lieu d'aller de la barre au bas de la fenêtre. */
    position: static;
    margin: 0;
    padding: 0;
    display: block;
    /* Invisible, une planche ne doit pas rester cliquable par-dessus la
       suivante. */
    pointer-events: none;
  }

  /*
    Les trois planches occupent **la même case de grille** : la case fait donc la
    hauteur du plus long des trois textes. En donnant à chaque colonne toute
    cette hauteur et en calant sa dernière phrase en bas, celle-ci se pose au
    même endroit pour les trois services.

    C'est la place du plus long qui est réservée : le texte du milieu fait une
    ligne de moins, et sa dernière phrase remontait de 29 px en arrivant. On
    voyait le saut à chaque changement de service.
  */
  .js .services__plan > .service { height: 100%; }
  .js .services__plan .service__col { max-width: min(58ch, 100%); }
  .js .services__plan .service__body {
    max-width: none;
    font-size: clamp(1.05rem, 1.32vw, 1.2rem);
    line-height: 1.55;
  }

  /* La photographie : de la barre d'en-tête au bas de la fenêtre, collée au
     bord droit. Elle s'accroche au bloc épinglé, seul élément positionné du
     lot, et traverse donc les deux rangs de la grille. */
  .js .services__plan .service__vue {
    position: absolute;
    top: var(--barre); right: 0; bottom: 0;
    width: var(--media);
    height: auto;
    margin: 0;
    overflow: hidden;
    /* Le même arrondi que les demi-colonnes du duo de l'accueil, sur le seul
       coin haut-gauche : la photographie paraît entrer par la droite de
       l'écran. Fixe ici — les images ne parcourent que 24 px, et un arrondi
       qui se refermerait sur cette course ne se verrait pas. */
    border-radius: var(--rayon) 0 0 0;
    transform: none;
  }

  /*
    LE CADRE NE BOUGE PAS — C'EST L'IMAGE QUI GLISSE DEDANS.

    La translation était portée par le cadre : en arrivant, chaque
    photographie était décalée jusqu'à 24 px vers le haut, et laissait cette
    bande de papier **sous elle** le temps de la transition. Le client l'a
    signalée deux fois.

    L'image déborde donc son cadre de 3 rem en haut comme en bas, et c'est elle
    qui se déplace : le cadre, lui, reste plaqué du haut au bas de l'écran et
    rogne ce qui dépasse. Aucun bord ne peut plus entrer dans le champ — la
    course maximale d'une image visible est de 0,25 × 6 rem, soit 24 px, contre
    48 px de débord de chaque côté.
  */
  .js .services__plan .service__vue img {
    position: absolute;
    left: 0; right: 0;
    top: -3rem;
    width: 100%;
    height: calc(100% + 6rem);
    object-fit: cover;
    transform: translate3d(0, calc((var(--p, 0) - var(--c)) * 6rem), 0);
  }

  /*
    L'apparition, planche par planche. `--c` est le centre de plage du service ;
    l'écart à --p décide seul de sa visibilité. Demi-domaine 0,25, fondu 0,05 :
    la sortante a **fini** de s'effacer quand l'entrante commence à paraître.
  */
  .js .services__plan > .service {
    --dp: max(calc(var(--p, 0) - var(--c)), calc(var(--c) - var(--p, 0)));
    --vis: clamp(0, calc((0.25 - var(--dp)) / 0.05), 1);
    opacity: var(--vis);
  }

  /* Le texte monte, la photographie descend : le sens contraire d'un bord à
     l'autre de l'écran. */
  .js .services__plan .service__col {
    transform: translate3d(0, calc((var(--c) - var(--p, 0)) * 6rem), 0);
  }

  /* Le titre de l'article passe en lecture invisible : c'est l'index qui le
     montre. Le conteneur pèse une classe de plus qu'une règle `.service__head`
     écrite plus bas dans la feuille. */
  .js .services__plan .service__head {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0);
    white-space: nowrap; border: 0;
  }

  .services__points { display: none; }

}

/* ================= Le bandeau de clôture et le pied ================= */

/*
  Le pied de page passe **sur l'encre** du bandeau, sur les cinq pages qui en
  portent un : marge négative égale à sa propre hauteur, fond transparent,
  mention en papier. Sans cela il posait une bande claire sous le bandeau.

  Les deux valeurs sortent du même jeton `--pied` — jamais deux nombres à tenir
  d'accord — et `height` plutôt que `min-height` : la mention dépassait de 2 px
  la valeur déclarée, et ces 2 px de papier se voyaient au tout dernier pixel de
  la page.

  Borné à `body:has(.cta)` : la page contact n'a pas de bandeau, et un pied
  transparent y écrirait du papier sur du papier.

  Le fond est **de l'encre, non du transparent**, et c'est délibéré. D'abord
  parce que `.foot` doit rester opaque : sur l'accueil, la boîte de bordure de
  l'écran d'ouverture déborde sous `<main>`, et la photographie y réapparaissait
  — c'est un piège déjà consigné. Ensuite parce qu'un liseré d'un pixel
  subsistait sous le bandeau, né des arrondis de sous-pixel : une encre opaque le
  couvre au lieu de le pourchasser.
*/
body:has(.cta) .foot {
  --pied: 6rem;
  height: var(--pied);
  margin-top: calc(-1 * var(--pied));
  background: var(--ink);
  display: grid;
  align-content: center;
}
body:has(.cta) .footline { color: rgba(250, 247, 242, 0.62); }

/*
  La page contact n'a pas de bandeau de clôture — il y ferait doublon — et son
  pied restait donc en papier : une bande claire de cent pixels fermait une
  page entièrement à l'encre. Il prend la même encre que le reste, et la
  mention la même clarté que sur les cinq autres pages. Le filet, lui, doit
  être redéclaré : `--line-soft` est réglé pour le papier et disparaît ici.
*/
/*
  **Le corps de page prend l'encre, pas seulement ses blocs.** La page contact
  est sombre du haut jusqu'en bas ; son `<body>` restait pourtant en papier, et
  le moindre reliquat le montrait. Mesuré : `.contact-screen` réserve
  `3.75rem` (60 px) pour le pied, qui n'en fait que 54 — six pixels de papier
  au bas de l'écran, que le client a signalés sur grand écran.

  La réserve n'est pas recalée : elle dépendrait de la hauteur réelle du pied,
  laquelle suit le corps du texte — et celui-ci grandit désormais au-delà de
  100 rem, si bien que l'écart changerait avec la largeur de la fenêtre. Un
  fond d'encre rend le problème sans objet, quelle que soit l'arrondi.
*/
body:has(.contact-screen) { background: var(--ink); }
body:has(.contact-screen) .foot { background: var(--ink); }
body:has(.contact-screen) .footline {
  color: rgba(250, 247, 242, 0.62);
  /* Aucun filet : la page est un aplat d'encre du haut jusqu'en bas, et une
     ligne au-dessus de la mention y découpait un pied qui n'existe pas. */
  border-top: 0;
}

/*
  Le bandeau lui-même. Sur les quatre pages hors services il se resserre de 477
  à 360 px — le client l'a trouvé trop haut —, et son contenu s'y centre. Le
  rideau d'encre qui monte y reste : c'est leur geste de clôture.
*/

.cta {
  min-height: 22.5rem;
  display: grid;
  align-content: center;
  padding-block: clamp(2.5rem, 6vw, 4rem);
}

/*
  Sur services, le bandeau prend une hauteur d'écran — sur téléphone comme sur
  ordinateur — et arrive **en dôme** : ses deux arrondis hauts valent chacun la
  moitié de la largeur, donc ils se rejoignent au sommet, puis s'ouvrent jusqu'au
  carré. `--qq` et non `--q` : la première ne vaut 1 qu'une fois le bandeau
  arrivé **en haut de l'écran**, quand la seconde est déjà pleine à 62 % de
  recouvrement — le bandeau se carrait alors à mi-chemin.

  L'encre est là dès le premier pixel : le rideau qui monte laissait sa partie
  haute en papier sous les photographies.
*/
/*
  Deux pages reçoivent ce traitement : services et l'accueil. `.opening` ne vit
  que sur l'accueil, `.services-scene` que sur services — deux sélecteurs, deux
  pages, et la galerie, à propos et 404 gardent leur rideau à 360 px.

  La hauteur d'écran n'est pas un ornement : c'est elle qui rend le dôme
  possible. À 360 px, chaque arrondi serait borné à la moitié de la hauteur et
  il resterait 720 px de plat entre les deux — on lirait deux angles adoucis,
  pas un dôme.
*/
/*
  **Une hauteur d'écran sur les cinq pages**, depuis le 9 septembre 2026.

  Le bandeau valait 360 px sur la galerie, à propos et la 404 — le client
  l'avait trouvé trop haut en septembre — et une hauteur d'écran sur l'accueil
  et les services. Mais c'est cette hauteur qui **fait** l'animation : un
  rideau d'encre qui monte ne peut pas remplir l'écran s'il ne fait que
  360 px, et sa course s'achevait donc avant d'y arriver. Le client a demandé
  la même animation partout, « comme sur l'accueil ou services » : elle exige
  la même hauteur.

  Le sélecteur ne nomme plus les pages : toutes les portent.
*/
.cta {
  min-height: 100lvh;
  background-image: none;
  background-color: var(--ink);
  border-radius: calc((1 - var(--qq, 0)) * 50vw) calc((1 - var(--qq, 0)) * 50vw) 0 0;
}
body:has(.services-scene) .cta__title,
body:has(.services-scene) .cta .btn,
body:has(.opening) .cta__title,
body:has(.opening) .cta .btn {
  /* Plus de rideau à attendre : le texte paraît dès l'entrée du bandeau, et non
     à partir de 0,55 comme là où l'encre doit d'abord monter. */
  opacity: clamp(0, calc(var(--q, 0) / 0.4), 1);
}

/*
  La course de la scène épinglée n'a aucun sens sous 55 rem, où rien n'est
  épinglé : elle y laissait **2 111 px de papier** entre le dernier service et
  le bandeau. Le client l'a prise pour un défaut, et c'en était un.
*/
@media (max-width: 54.99rem), (hover: none), (pointer: coarse) {
  .services-gap { display: none; }
}

/*
  La bande de verre : **le bord droit seulement**, sur toute la hauteur — c'est
  là que passent les photographies, et lui seul a quelque chose à flouter.

  Elle s'éteint à l'approche du bandeau de clôture. Elle se **rétracte** au lieu
  de s'effacer : animer l'opacité d'un ancêtre de `backdrop-filter` change la
  racine du fond capturé, et la bande vire au blanc une seconde avant de
  disparaître. Sa largeur tombe à zéro, et il ne se passe rien d'autre.
*/
/* Le bord gauche seul reste éteint : rien n'y passe que du papier. Le bord
   droit et le bas gardent leur verre — c'est là que défilent les
   photographies. */
body:has(.services-scene) .edgeblur__bande--g { display: none; }

body:has(.services-scene) .edgeblur__bande--d {
  transition: width 0.45s var(--ease-out);
}
/* La classe `cta-proche` est posée sur <html> par Scrub, comme `bas-eteint` sur
   l'accueil — donc **sur toutes les pages qui portent un bandeau**. L'extinction
   doit donc être bornée à la page services : écrite sans cette borne, elle
   éteignait aussi les bandes de l'accueil dès qu'on approchait du bandeau de
   clôture, et le client a signalé la disparition du flou à droite.

   Chaque bande se rétracte sur sa propre dimension, jamais par l'opacité :
   animer l'opacité au-dessus d'un flou de fond le fait virer au blanc avant de
   disparaître. */
body:has(.services-scene) .edgeblur__bande--b { transition: height 0.45s var(--ease-out); }
.cta-proche body:has(.services-scene) .edgeblur__bande--d,
body.cta-proche .edgeblur__bande--d { width: 0; }
.cta-proche body:has(.services-scene) .edgeblur__bande--b,
body.cta-proche .edgeblur__bande--b { height: 0; }

@media (max-width: 54.99rem), (hover: none), (pointer: coarse) {
  body:has(.services-scene) .edgeblur { display: none; }
}

/* ============ Grand écran : la composition reprend sa croissance ========= */

/*
  Au-delà de 1600 px, tout était plafonné — la coquille à 96 rem, les corps à
  leur borne haute, la photographie à 34 rem. À 2560 px, cela donnait 592 px de
  vide à gauche, 574 px entre le texte et l'image, et une photographie réduite à
  **21 % de l'écran** : la page ne grandissait plus, elle flottait.

  Le seuil est à **100 rem et non 90** : 90 rem valent exactement 1440 px, la
  largeur de référence sur laquelle tout a été réglé, et elle ne doit pas
  bouger d'un pixel.

  Chaque formule est construite pour **coïncider avec l'ancienne à 1600 px** —
  le plancher de chaque `clamp` vaut la valeur plafonnée d'avant. Il n'y a donc
  aucun saut à la frontière : on peut redimensionner la fenêtre au travers sans
  rien voir bouger.

  Le texte grandit peu, l'image beaucoup : c'est un portfolio, le sujet est la
  photographie. Titre 54 → 72 px, noms 30 → 38, description 19 → 22, image
  21 % → 38 % de l'écran.
*/
@media (min-width: 100rem) {
  .js .services-scene__pin {
    /* 496 px à 1600, 973 à 2560 — 38 % de l'écran. Le plafond à 64 rem n'est
       pas décoratif : les fichiers sources ne font que 736 px de large, et
       au-delà de ~1024 px d'affichage le flou deviendrait visible. */
    --media: clamp(31rem, 49.69vw - 18.7rem, 64rem);
    /* La coquille s'élargit avec l'écran au lieu de rester à 96 rem : 112 px
       de marge à 1600, 336 à 2560, au lieu de 592. */
    --marge: clamp(7rem, 23.33vw - 16.33rem, 24rem);
    /* La colonne des noms suit leur corps, sinon « Architecture & design » se
       replierait sur trois lignes. */
    --noms: clamp(17rem, 8.125vw + 8.875rem, 22rem);
  }

  /* Deux classes, et pour la raison qui revient sans cesse dans cette
     feuille : le bloc 55 rem porte `.js .services__h1 { font-size: min(4.2vw,
     3.4rem) }`, qui pèse plus lourd qu'un `.services__h1` seul et l'emporte
     quel que soit l'ordre d'écriture. */
  .js .services__h1 { font-size: clamp(3.4rem, 1.567rem + 1.833vw, 4.5rem); }
  .js .services__titre { font-size: clamp(1.85rem, 0.975rem + 0.875vw, 2.375rem); }
  .js .services__plan .service__body { font-size: clamp(1.2rem, 0.908rem + 0.292vw, 1.375rem); }

  /*
    Le corps reste **centré** entre le bas du titre et le bas de l'écran — le
    réglage du 4 septembre —, mais on lui retire de la place par le bas, ce qui
    le remonte de la moitié de ce qu'on retire. Sans cela, un écran de 1440 px
    de haut ouvrait 448 px de vide sous le titre.

    La formule ne mord **que sur les écrans hauts** : à 900 px de haut elle est
    négative, donc nulle, et rien ne bouge. À 1440 elle vaut 416 px et remonte
    le bloc de 208.
  */
  .js .services__corps { padding-bottom: max(0rem, calc(80vh - 46rem)); }
}

/* ============ Téléphone : un service par écran ============ */

@media (max-width: 54.99rem), (hover: none), (pointer: coarse) {
  .services__entete { padding-inline: var(--gutter); }
  .services__index { display: none; }

  /*
    La planche est **plus étroite que le conteneur**, et c'est délibéré : une
    lisière de la suivante reste visible au bord droit. C'est le seul signal
    qui se comprend sans légende — on ne déduit pas d'un jeu de points qu'il
    faut glisser, on le déduit d'une image coupée.

    Le débord vaut la gouttière **plus** 1,5 rem : la gouttière est reprise par
    le retrait gauche de la planche suivante, et ce sont les 24 px restants qui
    montrent quelque chose. Réglé à la seule gouttière, on ne verrait dépasser
    que du papier.
  */
  .services__plan {
    display: flex;
    gap: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .services__plan::-webkit-scrollbar { display: none; }

  /* Le corps sert d'ancrage aux points, qui le survolent. */
  .services__corps { position: relative; }

  .services__plan > .service {
    flex: 0 0 calc(100% - var(--gutter) - 1.5rem);
    min-width: 0;
    scroll-snap-align: start;
    /*
      **La photographie est ancrée au bas de la planche**, non poussée par le
      texte. Les trois textes n'ont pas la même longueur — celui de « Direction
      artistique » tient une ligne de plus —, si bien que l'image partait 8 px
      plus bas dans la troisième planche : à taille identique, elle n'était pas
      au même niveau, et cela se voit en glissant de l'une à l'autre.

      La rangée du milieu absorbe la différence : `1fr` sur le texte, `auto`
      sur l'en-tête et sur l'image. Les planches ayant toutes la même hauteur,
      les trois images tombent au même endroit. C'est l'image qui commande, et
      non la longueur du propos.
    */
    display: grid;
    grid-template-rows: auto 1fr auto;
    /* Le bas dégage la photographie du bandeau de clôture, qui arrive juste
       après et s'y collait. */
    padding: var(--s4) var(--gutter) var(--s5);
  }

  /*
    **La dernière planche a besoin d'un dégagement à sa droite**, sans quoi
    elle ne peut pas atteindre son accroche.

    Chaque planche est plus étroite que la fenêtre du débord — 331 px contre
    375. La piste fait donc 3 × 331 = 993, et le défilement s'arrête à
    993 − 375 = **618**, alors que la troisième planche réclame 2 × 331 = 662.
    Il manquait exactement la largeur du débord : la planche s'immobilisait
    44 px trop à gauche, et son nom comme son texte se décalaient d'autant par
    rapport au titre de la page. Les deux premières, elles, tombaient juste.

    La marge de fin rallonge la piste de ces 44 px, et la troisième s'aligne
    comme les autres.
  */
  .services__plan > .service:last-child {
    margin-inline-end: calc(var(--gutter) + 1.5rem);
  }

  /* Sur téléphone, le titre se réduit au premier mot, en capitales. « L'unique »
     reste dans le document — le titre de niveau 1 doit rester entier pour les
     lecteurs d'écran et les moteurs de recherche —, mais quitte l'écran par la
     mise en retrait d'usage. Les deux mots sont **déjà** dans deux `.mot`
     distincts : c'est le découpage de l'animation qui sert ici, sans une balise
     de plus. */
  .services__h1 .mot:first-of-type i { text-transform: uppercase; }
  /* `:not(:first-of-type)` et non `:last-of-type` : le titre anglais
     — « Photographing the singular » — compte **trois** mots, et la règle
     précédente n'en masquait qu'un. Il restait « PHOTOGRAPHING the » à
     l'écran, signalé par le client le 8 septembre 2026. Écrite ainsi, elle
     ne dépend plus du nombre de mots, en français comme en anglais. */
  .services__h1 .mot:not(:first-of-type) {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0);
    white-space: nowrap; border: 0;
  }

  /*
    **Le titre et les noms de service partagent une seule taille**, demandée le
    5 septembre 2026. C'est la largeur qui l'a fixée, et non le goût : dans les
    291 px utiles d'une planche à 375 px, « ARCHITECTURE & DESIGN » — le plus
    long des trois — mesure 378 px à la taille qu'avait le titre. Il fallait
    donc renoncer à quelque chose. Ce n'est pas l'interlettrage : des capitales
    fines non espacées se referment, et c'est un parti-pris du site depuis le
    premier jour. Les deux se rejoignent donc plus bas, à 20 px, où le nom
    mesure 262 px et tient sur une ligne.

    Le même `clamp` porte les deux déclarations : une seule valeur à reprendre
    si l'échelle doit bouger, et aucun risque de les voir diverger.
  */
  .services__h1,
  .js .services__plan .service__head h3 { font-size: clamp(1.25rem, 5.3vw, 1.75rem); }

  .js .services__plan .service__head h3 {
    text-transform: uppercase;
    font-weight: 200;
    letter-spacing: 0.1em;
    line-height: 1.3;
  }

  /* La bande réservée aux points : deux dégagements de 1,5 rem de part et
     d'autre d'un jeu de 7 px. Elle a valu deux fois 2,5 rem — on voyait trop
     peu de la photographie. */
  .service__vue {
    display: block;
    margin: 3.5rem 0 0;
    border-radius: 2px;
    overflow: hidden;
    aspect-ratio: 3 / 4;
  }

  /*
    Les points ne vivent plus dans une planche : ils la survolent, et restent
    donc **immobiles** pendant qu'on glisse. Leur hauteur est le seul nombre
    que la feuille de style ne peut pas connaître — elle dépend de la longueur
    du texte, donc de la largeur de l'écran —, et c'est le script qui la
    publie, comme il publie déjà `--p` et `--v`.

    `50%` n'est qu'un repli : si le script ne tourne pas, les points se posent
    au milieu du corps plutôt que de disparaître.
  */
  .services__points {
    position: absolute;
    top: var(--pastille, 50%);
    left: 0;
    right: 0;
    transform: translateY(-50%);
    display: flex;
    justify-content: center;
    gap: 0.55rem;
    margin: 0;
    pointer-events: none;
  }
  .services__points i {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--ink);
    opacity: 0.22;
    transition: opacity 0.35s var(--ease-out);
  }
  .services__points i.is-la { opacity: 0.7; }
}

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

@media (min-width: 55rem) and (hover: hover) and (pointer: fine) {
  .service { grid-template-columns: 1fr 1.5fr; gap: var(--s5); align-items: start; }

}

.service__head { display: grid; gap: var(--s2); }
.service__head .eyebrow { margin: 0; }

/* Le point qui a remplacé le numéro est devenu un disque : assez grand pour
   passer derrière le titre, assez transparent pour ne pas le disputer. Il
   n'est plus une ponctuation posée devant le texte mais la matière sur
   laquelle le texte se pose. Purement graphique, donc masqué aux lecteurs
   d'écran.

   Il se tenait dans la gouttière de gauche, à 20 px en dehors de la colonne.
   C'est exactement la bande qu'occupent les trois points du menu, fixes en
   haut à gauche : en défilant, chaque disque passait sous eux et les deux
   formes rondes se confondaient. Le disque ferme donc maintenant le titre au
   lieu de l'ouvrir — l'en-tête se réduit à la largeur de son texte pour lui
   servir d'ancrage, et le disque s'accroche à son bord droit. */
.service__head { position: relative; width: fit-content; max-width: 100%; }

.puce {
  position: absolute;
  right: -1.15em; top: -0.72em;
  z-index: 0;
  font-size: var(--fs-h2);
  width: 2.9em; height: 2.9em;
  margin: 0;
  pointer-events: none;
}
.puce i {
  display: block;
  width: 100%; height: 100%;
  border-radius: 50%;
  background: var(--beige);
  opacity: 0.55;
  transition: transform 0.7s var(--ease-expo), opacity 0.5s var(--ease-out);
}

/* Le titre passe devant le disque. */
.service__head h3 { position: relative; z-index: 1; }
.service h3 { font-size: var(--fs-h2); font-weight: 300; }
/* Le paragraphe. Trois demandes du 1er septembre 2026 tiennent dans ce bloc.

   **Plus visible** : le gris chaud passe au brun d'encre. Le contraste sur le
   papier monte de 5,7:1 à 7,3:1 — au-dessus du seuil renforcé, sans sortir de
   la palette ni assombrir la page.

   **Justifié** : les deux bords sont nets. La césure automatique est
   indispensable ici — sans elle, une colonne de 46 signes justifiée creuse des
   lézardes blanches entre les mots. Elle est indexée sur la langue de la page,
   que le sélecteur FR/EN met à jour sur `<html>`.

   La largeur est passée de 46 à 52 signes : un texte justifié a besoin de plus
   de mots par ligne pour que la césure ait de quoi travailler. */
/* Ni justification ni césure : le client les a fait retirer le 2 septembre
   2026. Le bord droit redevient libre, et les mots gardent leur espacement
   naturel — c'était le prix d'un bord aligné sur une colonne étroite. */
.service__body {
  color: var(--earth);
  max-width: 56ch;
}

.notfound {
  min-height: 100lvh;
  display: grid; place-items: center;
  text-align: center; padding: var(--gutter);
}

/* ============================================ 17. Accessibilité =========== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* Sans mouvement, tout doit être lisible à l'arrêt : on neutralise les
     formules indexées sur le défilement et on affiche l'état final. */
  .opening__name, .opening__title, .opening__sub { opacity: 1 !important; animation: none !important; }
  .duo__col--a, .duo__col--b { transform: none !important; border-radius: 0 !important; }
  .scene--rise .rise-media { transform: none !important; border-radius: 0 !important; }
  .over, .over__text, .over__chute,
  .over__signature { opacity: 1 !important; transform: none !important; }
  .over__filet { transform: none !important; animation: none !important; }
  /* Le mot ne se dévoile plus : il est là. */
  .social__mot { opacity: 1 !important; clip-path: none !important; }
  .over__title .mot > i { transform: none !important; }
  .rise { opacity: 1; transform: none; }
  /* Un défilé perpétuel est exactement ce que ce réglage demande d'éviter. */
  .houses__piste { animation: none !important; }
  .topbar { opacity: 1 !important; }
  .cursor { display: none !important; }
}

/* ============================================ 18. Réactif ================= */

@media (max-width: 47.99rem) {
  /* Deux moitiés côte à côte deviennent illisibles sous 768 px : le duo
     passe en une colonne, chaque image occupant la moitié de la hauteur. */
  .duo { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }

  /* Sur téléphone, le clavier virtuel couvre la moitié de l'écran : bloquer
     le défilement rendrait les derniers champs inatteignables. */
  .contact-screen { min-height: 0; display: block; padding-top: 6rem; }

  /* Le titre et l'Instagram se partagent la largeur : le titre à gauche,
     l'icône calée à droite sur sa première ligne. Empilés, ils laissaient une
     bande vide entre le titre et le formulaire. */
  .contact-intro {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--s4);
    /* L'icône se centre sur la hauteur du titre, et non sur sa première
       ligne : le titre en tient trois, et l'icône posée en haut paraissait
       accrochée au coin plutôt que mise en regard du texte. */
    align-items: center;
  }
  .contact-intro h1 { grid-column: 1; grid-row: 1; }
  .contact-social {
    grid-column: 2; grid-row: 1;
    margin-top: 0;
    justify-self: end;
  }

  /* Le mot passe **sous** l'icône au lieu de sortir à sa droite : à droite, il
     n'a plus la place — l'icône touche déjà la gouttière.

     Il se cale sur le bord droit, et non centré sous l'icône : centré, ses
     110 px débordaient de 33 px hors de l'écran, l'icône n'en faisant que 44.
     Et le dévoilement se fait par le haut, puisque c'est de là qu'il vient
     désormais. */
  .social__mot {
    left: auto; right: 0;
    top: calc(100% + 0.55rem);
    transform: none;
    text-align: right;
    clip-path: inset(0 0 100% 0);
  }

  /* Le surtitre « Contact » passait sous la barre. Il n'apportait rien que
     le titre ne dise déjà ; il reste sur ordinateur, où la place existe. */
  .contact-screen .eyebrow { display: none; }

  /* Le raccourci contact quitte le téléphone.

     Deux réglages l'y avaient maintenu : le libellé réduit à « Contact » et le
     nom resserré. Les deux tenaient alors sur la même ligne — mais à 20 px
     l'un de l'autre, et l'œil ne lisait plus qu'une seule phrase, « Alexandre
     Vuillaume Contact ». L'alignement à la même hauteur avait été demandé
     quand les deux tenaient au large ; il ne tient plus à cette largeur.

     La page contact reste à deux gestes : le menu la porte, et le bandeau de
     clôture y mène en bas de l'accueil. Le haut de l'écran ne porte plus que
     le nom.

     Le nom garde son espacement resserré : seul et centré, il tient ainsi
     jusqu'à 320 px sans se rompre. */
  .opening__contact { display: none; }
  .opening__name { letter-spacing: 0.26em; text-indent: 0.26em; }

  /* Sur un petit écran, la barre mange une bande de la photographie à chaque
     instant. Elle s'efface donc dès qu'on descend et revient au premier geste
     vers le haut — sans attendre le sommet de la page, sinon le menu serait
     injoignable au milieu d'une galerie de trente-six images. */
  .topbar { transition: transform 0.42s var(--ease-out), opacity 0.4s var(--ease-soft); }
  .barre-cachee .topbar { transform: translateY(-120%); }

  /* Le sélecteur de langue **reste dans la barre**, à droite, à la hauteur des
     trois points — la barre est une grille à trois colonnes centrée sur son
     axe vertical, il s'y aligne donc sans qu'on ait rien à calculer. Il y est
     visible en permanence, sans ouvrir le menu.

     *(Il a passé une journée au bas du panneau, où il ne se voyait qu'une fois
     le menu déplié.)* */
  .topbar__lang { display: flex; }
  .dots__lang { display: none; }
  .dots__menu { min-width: 11rem; }
  .lightbox__foot { gap: var(--s3); }

  /* Sur un écran étroit, des bandes à 5 vw mangeraient la photographie sans
     qu'on y gagne : elles se resserrent. */
  :root { --cote: 3.5vw; --bas: 10vh; --rayon: 64px; }

}

@media print {
  .topbar, .preloader, .cursor, .lightbox { display: none !important; }
  body { background: #fff; color: #000; }
}

/* ============================================ 15. L'accroche et le flou du bas

  L'accroche de l'ouverture se pose **juste au-dessus du flou visible**, et non
  plus à une distance fixe ni au bord de la bande.

  Deux corrections successives. La première a lié son écart à `--bas` plutôt
  qu'à `clamp(5.8rem, 14vh, 7.25rem)`, plafonné à 116 px : la bande, elle,
  vaut 11 % de la hauteur et grandit sans plafond, si bien qu'au-delà de
  ~830 px l'accroche passait **sous** elle et qu'en deçà elle flottait.

  La seconde tient au **dégradé**. La bande n'est pas un bloc de flou : ses
  trois couches sont masquées en `linear-gradient(to top, #000 0, transparent
  var(--f))`, avec `--f` à 100 %, 62 % et 30 %. Le flou perceptible ne commence
  donc qu'à ~62 % de la bande, pas à son bord haut. Posée à 3 px de ce bord,
  l'accroche restait à **35 px du flou qu'on voit** — le vide signalé par le
  client, à 1470 × 738.

  L'écart se mesure désormais depuis ce seuil-là. Vérifié : l'accroche descend
  de 30 px et se pose juste au-dessus de la matière, à toutes les hauteurs.

  Écrit en fin de feuille pour passer après la règle qu'il corrige.
*/
/* Mesuré à 1470 × 738 : le bord bas de l'accroche tombe **exactement** sur la
   valeur du retrait, sans le décalage de boîte que j'avais supposé. Le flou
   perceptible commence à 50 px du bas ; le retrait vaut donc ce seuil plus une
   demi-ligne, et l'accroche se pose 8 px au-dessus de la matière. */
.opening__inner { padding-bottom: calc(var(--bas) * 0.62 + 0.5rem); }

/*
  Sur ordinateur, le rebond de macOS étire la page vers le haut et découvre du
  vide au-dessus de l'écran d'ouverture — que le client veut **fixe**. On
  supprime donc le débordement vertical, mais **sur ordinateur seulement** :
  le même réglage sur téléphone reprendrait le tirer-pour-rafraîchir, rétabli
  à sa demande le 8 septembre. Les deux ne peuvent pas coexister, c'est la
  même propriété — les séparer par le type de pointeur est la seule sortie.
*/
@media (hover: hover) and (pointer: fine) {
  html { overscroll-behavior-y: none; }
}
