/* ── Here — Home : intro « portail » + panneaux scrollytelling (extrait du prototype validé) ── */
  /* ── Intro / hero « portail » ── */
  /* ── LA RELÈVE ────────────────────────────────────────────────────────────
     La piste porte toute la longueur : un écran par univers. Les trois écrans,
     eux, sont épinglés au même endroit (`sticky` + `margin-bottom:-100svh`, qui
     annule leur hauteur dans le flux) et se relèvent par rognage.
     Règle du mouvement : LE FILM GLISSE, LE TITRE NON. */
  .releve{position:relative;height:calc(var(--n,3) * 100svh);
    /* hauteur du titre dans l'écran, et corps des trois titres — les deux
       valeurs qui font tenir l'échange. Le corps est calibré sur le plus long
       (« Lofts and Art Residences ») : mesuré, il occupe 15,2 fois le corps à
       cet interlettrage. Plafond 4,4rem, plancher 1,02rem pour les très petits
       écrans. Un univers au nom plus long demanderait de revoir ceci. */
    --ligne:42%; --fs-univers:clamp(1.02rem, calc(5.9vw - 6px), 4.4rem)}
  /* ⚠️ UNE SEULE boîte collante, et les trois écrans empilés DEDANS.
     Première tentative : rendre chaque écran `sticky` avec `margin-bottom:-100svh`
     pour qu'ils se superposent. Ça superpose bien… mais la marge négative allonge
     la plage de collage, et les écrans ne se décollaient JAMAIS : arrivé en bas,
     le pied de page défilait par-dessous sans jamais apparaître. D'où une seule
     boîte collante, qui se décolle proprement à la fin de la piste, et les écrans
     posés en absolu dedans. */
  .releve__vue{position:sticky;top:0;height:100svh;overflow:clip}
  /* `isolation` est indispensable : sans elle, les z-index internes des trois
     écrans (titre en 3, décor en 0) se comparent dans UN SEUL empilement, et le
     titre de l'écran du dessous passait par-dessus le décor de l'écran du dessus.
     Chaque écran forme donc son propre empilement, et les écrans se superposent
     dans l'ordre du HTML. */
  .stage, .panel{position:absolute;inset:0;isolation:isolate}
  /* le panneau entrant est rogné par le haut, de 100 % à 0 */
  .panel{clip-path:inset(var(--rev,0%) 0 0 0)}
  /* seul le décor glisse — le titre, lui, ne reçoit aucune transformation */
  .panel__glisse{position:absolute;inset:0;transform:translateY(var(--rev,0%))}

  /* Le corps du titre d'arrivée, en un seul endroit. Déclaré sur :root pour que
     `home.js` puisse le lire (`getComputedStyle`) : la vignette du film et
     l'écart des lettres s'en servent comme référence. */
  :root{ --hw-k: 0.55;
    /* LA LIGNE DE PIED du masque des lettres. `line-height:1` fait descendre la
       boîte du texte jusqu'au jambage, alors que les capitales n'ont rien sous
       la ligne de base : sans cette coupe, la lettre qui monte se voyait SOUS
       son propre pied avant d'arriver. 14 % = mesuré sur Quablo SemiBold. */
    --coupe: 14% }

  .stage{overflow:hidden;background:var(--color-bg)}
  .portal{position:absolute;inset:0;z-index:1}
  /* Cadre centré qui grandit (vignette → plein écran). La vidéo en cover dedans → on voit toute la scène. */
  /* ⚠️ `visibility` ET NON `opacity`, et `will-change` SANS width/height.
     Une <video> dans un élément dont l'opacité passe de 0 à 1 fait créer puis
     détruire une couche de composition : deux ressauts, l'un au début du fondu,
     l'autre à son terme — exactement au moment de la petite vignette. Et
     déclarer `will-change:width,height` ne prépare rien (ce ne sont pas des
     propriétés composées) : ça ne fait qu'entretenir la promotion de couche. */
  .portal__frame{position:absolute;left:50%;top:50%;translate:-50% -50%;
    width:var(--fw,360px);height:var(--fh,300px);overflow:hidden;visibility:hidden;
    will-change:transform;
    -webkit-mask-image:radial-gradient(130% 130% at 50% 50%, #000 var(--mfeather,48%), transparent 100%);
    mask-image:radial-gradient(130% 130% at 50% 50%, #000 var(--mfeather,48%), transparent 100%)}
  /* ⚠️ ET ON LE RETIRE DÈS QUE LE FILM EST PLEIN ÉCRAN — 30/08/2026.
     À ce stade `--mfeather` vaut 150 % : le masque ne masque plus rien, mais il
     coûte au compositeur une couche masquée pour une vidéo plein écran, à chaque
     image. Mesuré en 3840 px : 21,2 ms d'intervalle moyen entre images avec,
     17,3 ms sans. Le rendu est identique ; seule la fluidité change.
     La classe est posée par `releaseScroll()` et retirée par `construire()`. */
  .portal__frame.plein{-webkit-mask-image:none;mask-image:none}
  /* Le film d'accueil dérive lui aussi, mais dans son cadre : c'est le CADRE que
     l'intro anime (largeur, hauteur, échelle), on n'y touche donc pas. L'échelle
     1,14 posée ici sert uniquement de réserve de débord pour la dérive de 3,5 %.
     Pendant l'intro la dérive vaut 0 : la séquence validée n'est pas modifiée. */
  /* L'image d'attente occupe exactement la place du film, dessous. Elle reste
     visible tant que la première image n'est pas décodée — et pour toujours
     si le film ne démarre jamais. Même cadrage que la vidéo, sans quoi le
     passage de l'une à l'autre ferait un saut. */
  .portal__poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
    object-position:center 34%;display:block}
  /* `translateZ(0)` + `backface-visibility` : le film garde UNE seule couche de
     composition, du premier pixel au plein écran, au lieu d'être re-rastérisé
     quand sa boîte franchit les seuils du compositeur. */
  .portal__video{width:100%;height:100%;object-fit:cover;object-position:center 34%;display:block;
    transform:translateY(var(--derive,0%)) scale(var(--reserve-derive)) translateZ(0);
    backface-visibility:hidden;will-change:transform}
  /* ⚠️ LA DÉSATURATION A ÉTÉ RETIRÉE — 30/08/2026, décision de Guillaume.
     C'était `filter:saturate(0.6)`, « douce et permanente ». Un filtre sur une
     vidéo plein écran est recalculé à CHAQUE image, et le coût grandit avec
     l'écran. Mesuré en 3840 px sur l'arrivée à l'accueil : 18,2 ms d'intervalle
     moyen entre images avec le filtre, 12,1 ms sans — soit environ 55 im/s
     contre 83, et la pire image qui passe de 110 à 83 ms. C'est la seconde
     moitié de la saccade que Guillaume voyait en cliquant sur le mot-symbole ;
     la première était le masque, retiré juste au-dessus.
     Le film ressort donc plus chaud et plus contrasté. C'est un vrai changement
     d'image, vu et validé sur deux captures du même photogramme.

     ⚠️ ET C'EST UNE RÈGLE, PAS UNE EXCEPTION. Guillaume l'a formulée ainsi :
     « la saturation doit être présente lorsqu'on a une marie-louise autour de
     la page ». Là où la page est encadrée de papier — les rubriques — l'image
     reste en retrait ; là où elle occupe tout l'écran — l'accueil — elle est
     rendue pleine. Les panneaux de la relève ont donc perdu leur `saturate`
     eux aussi (ils gardent leur `brightness`, voir plus bas), et les rubriques
     conservent le leur (`pages.css`, `body.has-ml .page-hero__media`).
     Retour en arrière : remettre `filter:saturate(0.6)` sur cette règle. */
  /* Voile blanc : fond la vidéo dans le blanc quand elle est en petit carré, disparaît au plein écran */
  .portal__veil{position:absolute;inset:0;background:var(--color-bg);opacity:.68}
  /* ⚠️ MÊME VOILE QUE LES PANNEAUX — 31/08/2026. Le premier écran portait
     .35/.05/.50 quand les écrans 2 et 3 portaient .596/.41/.69 : Private Events
     paraissait donc bien plus clair et plus coloré que Lofts et Conciergerie.
     L'écart est ancien — il vient du `brightness(.62)` que les panneaux ont
     toujours eu et que le premier écran n'a jamais eu ; ce n'était pas une
     saturation, aucun des trois n'en porte sur l'accueil. Les trois écrans
     sont désormais traités à l'identique. */
  .portal__scrim{position:absolute;inset:0;opacity:0;background:linear-gradient(180deg,rgba(0,0,0,.596),rgba(0,0,0,.41) 30% 60%,rgba(0,0,0,.69))}

  /* Flex + gap animé → le R (lettre du milieu) reste centré quel que soit l'écart */
  .hero-word{position:absolute;inset:0;z-index:3;display:flex;justify-content:center;align-items:center;
    gap:var(--hgap,4.1em);font-family:var(--font-display);font-weight:var(--fw-display);
    /* ⚠️ `--hw-k` N'EST PLUS UN RÉGLAGE PROVISOIRE, c'est le rapport entre le
       corps AFFICHÉ du titre et le corps de RÉFÉRENCE du reste de la scène.
       `home.js` s'en sert pour recalculer la vignette du film et l'écart de
       départ des lettres à la taille de référence : c'est ce qui permet de
       changer le corps du titre SANS toucher au petit carré de vidéo ni à
       l'amplitude du rapprochement. Le fondre dans le clamp casserait les deux.
       0,55 = les 53 px retenus par Guillaume le 22/08/2026 (96 px × 0,55).
       Échelle qui en découle : 53 px au-delà de 1334 px de large, 34 px en
       768 px, 22 px en 390 px. */
    font-size:calc(clamp(2.2rem, 1rem + 6vw, 6rem) * var(--hw-k, .55));text-transform:uppercase;line-height:1;
    color:#FCFCFC;mix-blend-mode:difference;white-space:nowrap}
  /* LE MASQUE (fixe) et LE GLYPHE (qui coulisse). `clip-path` plutôt que
     `overflow:hidden` : il faut couper au ras de la ligne de pied, et laisser le
     haut respirer (−40 %) pour que rien ne rogne les capitales une fois posées.
     L'état de départ est en CSS, comme l'ancien `opacity:0` : sans lui les cinq
     lettres se verraient en clair le temps que GSAP prenne la main. */
  /* Les côtés sont ÉLARGIS (−8 %) et non coupés : un masque au ras de la
     chasse rogne les débords latéraux des glyphes (le R de Quablo en a un).
     Seul le pied doit couper — c'est un rideau, il ne monte que verticalement.

     ⚠️ L'ÉTAT DE DÉPART EST SUR LE MASQUE, PAS SUR LE GLYPHE. Première version :
     le glyphe partait à `transform:translateY(118%)` en CSS, comme l'ancien
     `opacity:0`. Erreur payée le 22/08/2026 — les lettres ne revenaient JAMAIS.
     GSAP lit le transform CALCULÉ au moment du `fromTo` et le garde comme
     translation de base, puis empile son `yPercent` par-dessus : à la fin du
     tween, `yPercent:0` laissait `translate(0px, 118px)`, soit les cinq lettres
     immobiles sous leur propre ligne de pied. Mesuré : `matrix(1,0,0,1,0,118)`.
     Donc ici le masque est FERMÉ (100 % par le bas) — ce qui évite le
     clignotement d'avant-JS — et c'est `home.js` qui l'ouvre après avoir posé
     la lettre sous la ligne. Aucun transform en CSS sur ce qui est animé. */
  .hero-word span{display:inline-block;clip-path:inset(-40% -8% 100% -8%)}
  .hero-word span i{display:inline-block;font-style:normal;will-change:transform}

  /* LE POINT DE RELÈVE — la hauteur du titre, IDENTIQUE dans les trois écrans.
     C'est la condition de l'échange net : les titres doivent tomber sur la même
     ligne. On ancre donc par le HAUT (et non en centrant le bloc, sinon un titre
     sur deux lignes remonte plus qu'un titre sur une). */
  .hero-title-wrap, .panel__title{position:absolute;left:0;right:0;top:var(--ligne,42%);
    z-index:3;text-align:center;padding-inline:var(--gutter)}
  /* LE TITRE S'EFFACE AVANT L'APLAT (22/08/2026, demandé par Guillaume).
     `menu.js` pose `is-open` sur le corps en tout premier, avant même de monter
     la séquence du vert : la règle ci-dessous s'applique donc à l'image du clic,
     avant que la première bande ait bougé.

     ⚠️ ON VISE L'ENVELOPPE, PAS LE TITRE. `home.js` écrit l'opacité de
     `.hero-title` et de `.hero-dot` en style EN LIGNE à la fin de l'intro — une
     classe ne pourrait pas la contredire. L'enveloppe, elle, n'est touchée par
     personne : la masquer emporte le titre ET son carré, sans rien disputer à
     la séquence d'arrivée. Même chose pour `.panel__title`, qui couvre les
     univers 02 et 03 quand le menu s'ouvre depuis le bas de la page.

     `transition:none` n'est pas décoratif : le jour où l'on posera une
     transition sur ces enveloppes, « instantanément » cesserait d'être vrai
     sans que personne ne s'en aperçoive.

     Au retour, `is-open` est retiré tout aussi tôt : le titre est donc déjà
     revenu, derrière le vert, au moment où celui-ci le redécouvre. */
  body.is-open .hero-title-wrap,
  body.is-open .panel__title{opacity:0;transition:none}
  .hero-title{font-family:var(--font-display);font-weight:var(--fw-display);
    font-size:var(--fs-univers);text-transform:uppercase;letter-spacing:.04em;line-height:var(--lh-tight);
    white-space:nowrap;margin:0;
    color:var(--color-text-invert);opacity:0}
  /* repère carré de la charte (comme le header et le menu) */
  .hero-dot{display:block;width:.5625rem;height:.5625rem;background:var(--color-text-invert);margin:clamp(.9rem,2vw,1.6rem) auto 0;opacity:0}

  .intro-labels{opacity:0}
  .panel__label{position:absolute;bottom:var(--gutter);left:var(--gutter);z-index:3;font-size:var(--fs-label);text-transform:uppercase;letter-spacing:var(--ls-label);color:rgba(255,255,255,.7)}
  .panel__count{position:absolute;bottom:var(--gutter);right:var(--gutter);z-index:3;font-size:var(--fs-label);letter-spacing:var(--ls-label);color:rgba(255,255,255,.6);font-variant-numeric:tabular-nums}

  /* Le bouton de mise au point, calé sur le compteur : même ligne de base,
     même petite capitale, même encre. Il se tient à sa gauche, à distance
     d'une gouttière, et s'allume au survol pour qu'on le distingue du
     compteur — qui, lui, n'est pas cliquable. Éteint pendant l'intro (il la
     relancerait alors qu'elle joue) et sous le calque d'une rubrique : il
     n'appartient qu'à l'accueil. */
  /* ⚠️ RELEVÉ DE .25em — les deux ne s'alignent pas tout seuls. Le compteur est
     du TEXTE : sa boîte descend sous les chiffres, de la place réservée aux
     jambages. L'icône, elle, occupe exactement sa boîte. Calés tous deux sur
     `bottom`, leurs centres optiques étaient décalés. Mesuré à 390, 1440 et
     3840 px : 3,1 · 3,6 · 6,3 px, soit .25em à chaque fois — l'écart suit le
     corps, donc une valeur en em le corrige à toutes les largeurs. */
  .rejouer-intro{position:fixed;bottom:calc(var(--gutter) + .25em);
    right:calc(var(--gutter) + 5.5em);z-index:79;
    font-size:var(--fs-label);color:rgba(255,255,255,.6);
    display:block;line-height:0;
    transition:color var(--dur-fast) var(--ease-out),opacity var(--dur-fast) var(--ease-out)}
  /* Le tracé fait 1em de côté, donc exactement la hauteur d'œil du compteur.
     `line-height:0` sur le lien empêche la boîte de dépasser sous le dessin :
     sans cela l'icône se posait plus haut que le compteur, alignée sur une
     ligne de texte qui n'existe pas. */
  .rejouer-intro svg{display:block;width:1em;height:1em}
  .rejouer-intro:hover,
  .rejouer-intro:focus-visible{color:var(--color-text-invert)}
  .rejouer-intro:focus-visible{outline:1px solid var(--color-text-invert);outline-offset:4px}
  html.is-intro .rejouer-intro,
  html.en-rubrique .rejouer-intro{opacity:0;pointer-events:none}
  .scroll-cue{position:absolute;bottom:calc(var(--gutter) + 1.6rem);left:50%;transform:translateX(-50%);z-index:3;display:flex;flex-direction:column;align-items:center;gap:.5rem;color:rgba(255,255,255,.7)}
  .scroll-cue span{font-size:var(--fs-label);text-transform:uppercase;letter-spacing:var(--ls-label)}
  .scroll-cue i{display:block;width:1px;height:40px;background:linear-gradient(currentColor,transparent)}

  /* ── Panneaux suivants ── */
  .panel{overflow:clip}
  /* DÉRIVE DU FOND — la deuxième vitesse de la relève.
     La relève déplace déjà le décor entier (`.panel__glisse` translate de --rev).
     Le fond, lui, dérive d'un peu moins : il traîne à l'arrivée, puis continue de
     monter doucement pendant que le panneau attend son tour. C'est cet écart
     entre deux vitesses qui fait la profondeur — l'amplitude reste minime (3,5 %).
     ⚠️ Le débord vient de l'ÉCHELLE, pas d'un `inset` négatif. Agrandir la boîte
     changerait le calcul de `background-size:cover`, donc le cadrage de la photo ;
     l'agrandir elle laisse le recadrage centré et proportionnel. 1,12 donne 6 % de
     débord de chaque côté pour 3,5 % × 1,12 = 3,9 % de course : 2 % de marge,
     mesurée à 19 px sur un écran de 900. On ne touche à l'un de ces deux nombres
     qu'en touchant à l'autre. */
  .panel__bg{position:absolute;inset:0;background-size:cover;background-position:center;
    transform:translateY(var(--derive,0%)) scale(1.12);will-change:transform}
  .bg--green{background:radial-gradient(120% 100% at 50% 45%, rgba(42,62,47,.9), rgba(20,30,24,.6) 55%, #0b0f0c 85%),linear-gradient(180deg,#20302a,#0c110e)}
  .bg--dark{background:radial-gradient(120% 100% at 50% 40%, rgba(60,60,54,.5), rgba(12,12,12,.4) 50%, #070707 85%),linear-gradient(180deg,#141414,#070707)}
  .panel__scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.35),rgba(0,0,0,.05) 30% 60%,rgba(0,0,0,.5))}

  /* LE VOILE D'UN PANNEAU QUI PORTE UNE IMAGE — PORTÉ ICI, PAS PAR UN FILTRE
     Le titre blanc doit tenir sur la photo : il faut assombrir. C'était un
     `filter:brightness(.62)` sur `.panel__bg`. Or `.panel__bg` est justement
     l'élément que le passage nomme `film-univers` : le morphing partait donc
     d'une image à 62 % de luminosité vers une arrivée à 100 %, et faisait un
     saut que le premier écran — dont le `.portal__frame` n'a aucun filtre —
     n'avait pas. Mesuré le 31/08 : c'est ce qui rendait le geste différent
     sur Lofts et Conciergerie.
     Le voile passe donc sur le SCRIM, qui est un frère de `.panel__bg` et
     n'est pas nommé : il se fond avec la page au lieu de se déformer.
     Les alphas recomposent EXACTEMENT l'ancien rendu, sans approximation —
     assombrir puis voiler, ou voiler une fois, revient au même produit :
         0,62 × (1 − a_avant) = 1 − a_apres
         .35 -> .597    .05 -> .411    .5 -> .69
     Le combinateur de frère, plutôt que `:has()`, parce qu'il n'a pas de
     seuil de version : un navigateur sans `:has()` afficherait un panneau
     non voilé, donc un titre illisible. */
  .panel__bg--photo ~ .panel__scrim,
  .panel__bg--film  ~ .panel__scrim{
    background:linear-gradient(180deg,rgba(0,0,0,.597),rgba(0,0,0,.411) 30% 60%,rgba(0,0,0,.69))}
  .panel__title h2{font-family:var(--font-display);font-weight:var(--fw-display);
    font-size:var(--fs-univers);text-transform:uppercase;letter-spacing:.04em;line-height:var(--lh-tight);
    white-space:nowrap;margin:0;color:var(--color-text-invert)}
  .panel__title .dot{display:block;width:.5625rem;height:.5625rem;background:var(--color-text-invert);margin:clamp(.9rem,2vw,1.6rem) auto 0}


  /* Responsive mobile */

/* Lien plein cadre sur les panneaux 02/03 → la page de la rubrique */
.panel__link{position:absolute;inset:0;z-index:4}
/* ⚠️ INERTE PENDANT L'INTRO. Le premier écran porte maintenant un lien lui
   aussi ; sans cette ligne, un clic pendant la séquence d'arrivée naviguerait
   au lieu de l'accélérer — et le visiteur perdrait l'entrée du site pour avoir
   voulu la presser. */
html.is-intro .panel__link{pointer-events:none}

/* ══ LE PASSAGE — la rubrique affichée dans la page d'accueil ══════════════
   ⚠️ LES APPARITIONS SONT NEUTRALISÉES ICI, et il faut savoir pourquoi.
   Le contenu d'une rubrique porte des `[data-reveal]`, qui partent à
   `opacity:.001` sous un `clip-path` fermé : c'est `site.js` qui les ouvre au
   défilement. Or `site.js` n'est PAS chargé sur l'accueil — l'invariant « jamais
   `home.js` et `site.js` ensemble » est vérifié par le parcours de test, et on
   ne le casse pas pour si peu. Sans cette règle, la rubrique s'afficherait
   parfaitement… et serait invisible.
   Les apparitions au défilement seront rendues par le passage lui-même, plus
   tard ; d'ici là le contenu est simplement posé. */
/* ⚠️ LE REPLI, ET SEULEMENT LE REPLI. Les blocs d'une rubrique démarrent
   invisibles (`opacity:.001`) et c'est leur révélation qui les allume. Tant que
   `passage.js` n'a pas branché cette révélation sur le défilement du calque, il
   faut donc les rendre visibles — sinon une page sans GSAP, ou en mouvement
   réduit, resterait blanche. Dès que la révélation est en place, le calque
   porte la classe `anime` et cette règle s'efface. */
.passage:not(.anime) [data-reveal]{clip-path:none; transform:none; opacity:1}
/* La rubrique posée mais pas encore montrée : le temps que son film charge et
   se cale sur celui de l'accueil. Rendue — donc la vidéo se charge — mais
   invisible et hors du flux, donc rien ne bouge à l'écran. */
/* ── LE CALQUE ────────────────────────────────────────────────────────────
   Jusqu'ici la rubrique REMPLAÇAIT l'accueil : `hote.hidden = true`, et une
   copie de la page prenait sa place. On ne naviguait pas — même document,
   aucun rechargement — mais tout était échangé d'un coup, la hauteur de page
   passait de trois écrans à un seul, un `scrollTo(0,0)` suivait, et la barre
   de défilement sautait. Guillaume l'a lu pour ce que c'était : « on a
   l'impression que tu charges une autre page ». Il avait raison.
   La rubrique est maintenant un CALQUE posé dessus. L'accueil reste en place,
   intact, et c'est SON film que l'on continue de voir — le même élément, qui
   n'a jamais cessé de jouer. Le calque a son propre défilement, donc la
   position de l'accueil n'est ni déplacée ni restaurée : elle n'a pas bougé.
   Maquette validée le 31/08. */
.passage{position:fixed; inset:0; z-index:70; overflow-y:auto;
  overscroll-behavior:contain; -webkit-overflow-scrolling:touch}
.passage.attente{visibility:hidden; pointer-events:none}

/* Une page sans héros — « What's up », « Contact », les mentions — est du
   papier plein : rien à voir au travers, donc un fond opaque. Sans lui, le film
   de l'accueil se lirait sous le texte. */
.passage--papier{background:var(--color-bg); color:var(--color-text)}

/* Le héros du calque ne porte pas d'image : il est transparent, et le film de
   l'accueil se voit au travers. C'est tout le principe — il n'y a plus deux
   films à accorder, il n'y en a qu'un. */
.passage .page-hero{background:transparent}
.passage .page-hero__media{display:none}

/* Le voile de lisibilité ne garde que sa couche HORIZONTALE, celle qui porte
   le titre à gauche. La verticale est déjà posée par l'écran d'accueil, en
   dessous : les superposer assombrirait deux fois. */
.passage .page-hero__scrim{
  background:linear-gradient(90deg, rgba(12,12,12,.72) 0%,
                                    rgba(12,12,12,.42) 42%,
                                    rgba(12,12,12,.06) 72%)}

/* Sous le calque, l'accueil se tait : son titre a été remplacé par celui de la
   rubrique (qui vient s'y poser), et ses repères n'ont plus lieu d'être. */
/* ⚠️ SURTOUT PAS `overflow:hidden` POUR VERROUILLER L'ACCUEIL — 31/08/2026.
   Le banc tient par un `position:sticky` sur `.releve__vue`, et `overflow`
   autre que `visible` sur l'élément défilant PRIVE le collant de son
   conteneur. Mesuré sur l'écran 2 : la vue passait de y = 0 (épinglée) à
   y = −900 (sortie du champ). L'accueil disparaissait donc derrière le calque
   et l'on voyait le papier — tout le principe du calque tombait, et seulement
   sur les écrans 2 et 3, les seuls où l'on est défilé.
   Le verrou passe donc par le calque lui-même : `overscroll-behavior:contain`
   empêche le défilement de se propager à la fenêtre quand il arrive en bout,
   `data-lenis-prevent` rend la molette au navigateur, et Lenis est arrêté.
   Trois mesures qui ne touchent pas au collant. */

html.en-rubrique .stage .hero-title,
html.en-rubrique .panel__title,
html.en-rubrique .intro-labels,
html.en-rubrique .panel__label{opacity:0}

/* Et il ne défile plus : c'est le calque qui défile. Sans cela la molette
   ferait glisser les deux à la fois. */
/* (Les règles de transition de vue du passage ont été retirées le 31/08 : le
   geste est désormais fait d'éléments réels, animés par `passage.js`. Elles ne
   servaient plus à rien, et WebKit ne les honorait pas — c'était la cause du
   cadre qui disparaissait d'un coup au retour. `pages.css` garde les siennes,
   qui valent d'un document à l'autre et n'ont rien à voir avec le calque.) */
/* Le compteur et la parallaxe sont pilotés par `site.js` eux aussi : sans lui
   ils ne bougent pas, ce qui est correct — mais le compteur ne doit pas rester
   à zéro au milieu de l'écran. On le retire tant que le passage ne le tient pas. */
/* (Le compteur était masqué dans le calque faute d'être piloté ; il l'est
   désormais par `passage.js`, avec le calque pour racine d'observation.) */

/* ⚠️ LA COUCHE DE PARALLAXE NE DOIT PAS DÉFORMER LE GESTE — 31/08/2026.
   `.page-hero__media` est volontairement plus grand que son cadre — `top:-18%`,
   `height:136%` — pour que `site.js` puisse le faire glisser de ±12 % sans
   jamais découvrir de bord (la « règle des 130 % »). Mais `site.js` n'est PAS
   chargé sur l'accueil, donc dans le passage ce débord ne sert rien : rien ne
   l'anime. Il faussait en revanche le geste, et lourdement.
   Mesuré au cadre de référence 1728 × 1117 :
     départ  `.portal__frame`     [0,    0, 1728, 1117]
     arrivée `.page-hero__media`  [0, −201, 1728, 1519]
   L'élément nommé passait donc d'une boîte de 1117 à une boîte de 1519
   remontée de 201 px — un agrandissement de 36 % doublé d'un glissement vers
   le haut. C'est le soubresaut que voyait Guillaume au clic.
   Remis à plat, le film d'arrivée occupe exactement le cadre de départ, et le
   geste n'a plus rien à redimensionner : « c'est la même vidéo, la
   marie-louise vient par-dessus ».
   La page de rubrique autonome n'est pas touchée : elle, `site.js` l'anime. */
/* ⚠️ ET LE MÊME CADRAGE — sinon la boîte ne bouge plus, mais l'image, si.
   Le film de l'accueil est agrandi de 14 % dans son cadre : c'est la réserve
   qui permet à la dérive de glisser sans découvrir de bord. À l'arrivée la
   vidéo était à l'échelle 1, donc au clic l'image passait d'un cadrage serré
   à un cadrage large — mesuré sur une image IDENTIQUE du film, 60,5 % des
   pixels changeaient. La boîte ne bougeait pas, le sujet, si.
   La même réserve est donc reprise ici. */
/* (L'ancien réglage de la couche de parallaxe et de la réserve de dérive a été
   retiré : le film du calque n'est plus affiché du tout, donc il n'y a plus ni
   boîte à égaliser, ni cadrage à accorder. C'est le film de l'accueil, et lui
   seul, que l'on voit. `--reserve-derive` reste utile côté accueil.) */

/* ── LA MARIE-LOUISE SE DÉPLOIE ───────────────────────────────────────────
   Elle paraissait d'un coup. Guillaume la veut « en glissement, un peu le
   même mouvement que dans le menu » — et le menu, lui, ouvre deux volets qui
   grandissent en `scale`, au même instant, sur la même durée, avec la même
   courbe (`menu.js`, `buildOpen`). C'est ce geste-là qui est repris ici :
   quatre barres, chacune ancrée sur son bord extérieur, qui poussent vers
   l'intérieur. Pas de décalage entre elles — le menu n'en met pas non plus.
   La durée est celle du geste (900 ms) et non les 1,8 s du menu : les deux
   mouvements doivent finir ensemble, sinon le cadre se pose après coup.
   ⚠️ La règle est portée par `html.en-passage`, donc UNIQUEMENT le temps du
   geste. Une arrivée directe sur la rubrique reste sans animation — c'est ce
   que Guillaume a demandé le 30/08. Et comme la classe disparaît à la fin,
   les barres retrouvent leur état naturel, à l'échelle 1. */

.panel__link:focus-visible{outline:2px solid var(--color-text-invert);outline-offset:-6px}

/* Photos du lieu sur les panneaux 02/03 — le dégradé de la charte reste en dessous
   (il sert de repli et de teinte). Les photos sont assombries et désaturées : les
   grands titres blancs doivent rester lisibles par-dessus, quelle que soit la photo. */
/* ⚠️ ASSOMBRIES, MAIS PLUS DÉSATURÉES — 30/08/2026.
   Règle posée par Guillaume : la désaturation ne vaut QUE là où la page est
   encadrée par la marie-louise, c'est-à-dire sur les rubriques. L'accueil n'en
   a pas : ses images y sont donc rendues pleines.
   ⚠️ `brightness(.62)` RESTE, et n'a rien d'esthétique : les grands titres
   blancs doivent rester lisibles par-dessus, quelle que soit la photo. C'est
   la seule des deux fonctions qu'on ne peut pas retirer. */
/* ⚠️ LA SATURATION REVIENT SUR L'ACCUEIL — 31/08/2026, sur demande de
   Guillaume. Elle remplace la règle du 30/08 (« pas de saturation sur
   l'accueil, seulement avec la marie-louise ») : le film du premier écran est
   une image de whisky, très chaude, et il ressortait beaucoup plus coloré que
   les deux autres. Mesuré sur l'accueil, saturation moyenne :
     Private Events 62,9   ·   Lofts 22,0   ·   Conciergerie 13,9
   L'assombrissement, lui, était déjà égalisé — l'écart n'était donc pas une
   affaire de voile mais bien de couleur.
   `saturate(.6)` est exactement ce que portent les pages de rubrique : les
   deux bouts du geste sont désormais traités à l'identique, et le clic ne
   change plus la couleur de l'image. Le voile d'assombrissement reste, lui,
   sur `.panel__scrim` — un filtre et un voile ne font pas le même travail. */
.portal__video,
.panel__bg--photo,
.panel__bg--film{filter:saturate(.6)}

  /* LE FILM D'UN PANNEAU
     Il se pose PAR-DESSUS la photo de fond, dans la même boîte : celle-ci porte
     déjà la dérive (`transform` sur `.panel__bg`), le film la suit donc sans
     qu'on ait à la répéter. Tant que le film n'a pas de source — ou qu'il
     échoue — c'est la photo qu'on voit. Le fondu évite le à-coup au moment où
     la première image remplace l'affiche.
     ⚠️ `object-fit:cover` est indispensable : sans lui, un film au format
     différent de l'écran laisse des bandes, et le cadrage ne correspond plus à
     celui de la photo qu'il recouvre. */
  .panel__video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
    opacity:0;transition:opacity .5s ease}
  .panel__video.joue{opacity:1}

  /* ── REPLI : ni JavaScript, ni animation ────────────────────────────────────
     Tout l'accueil part à `opacity:0` et n'est rallumé que par GSAP. Sans lui —
     JS coupé, bibliothèque non chargée, `prefers-reduced-motion` — la page
     s'ouvrait sur le DERNIER panneau, sans en-tête et sans intro : les trois
     écrans, posés en absolu dans une seule boîte collante, se superposaient.
     Ici on rend la piste à un défilement ordinaire : un écran après l'autre.
     `no-js` est posé par le <head>, `no-tween` par site.js et menu.js. */
  html.no-js .releve, html.no-tween .releve{height:auto}
  html.no-js .releve__vue, html.no-tween .releve__vue{position:static;height:auto;overflow:visible}
  html.no-js .stage, html.no-tween .stage,
  html.no-js .panel, html.no-tween .panel{position:relative;inset:auto;height:100svh}
  html.no-js .panel, html.no-tween .panel{clip-path:none}
  html.no-js .panel__glisse, html.no-tween .panel__glisse{transform:none}
  html.no-js .panel__bg, html.no-tween .panel__bg{transform:none}
  html.no-js .portal__frame, html.no-tween .portal__frame{
    width:100%;height:100%;visibility:visible;-webkit-mask-image:none;mask-image:none}
  html.no-js .portal__veil, html.no-tween .portal__veil{opacity:0}
  html.no-js .portal__scrim, html.no-tween .portal__scrim{opacity:1}
  html.no-js .hero-title, html.no-tween .hero-title,
  html.no-js .hero-dot, html.no-tween .hero-dot,
  html.no-js .intro-labels, html.no-tween .intro-labels,
  html.no-js .site-head, html.no-tween .site-head{opacity:1}
  /* Sans GSAP, personne n'ouvre le masque : on le retire. (Le wordmark est de
     toute façon sorti de l'œil juste en dessous, mais il doit rester cohérent.) */
  html.no-js .hero-word span, html.no-tween .hero-word span{clip-path:none}
  /* Le wordmark se dissout dans l'intro animée. Sans animation il recouvrirait
     le titre — on le retire de l'œil SANS le retirer du document : c'est le
     <h1> de la page, le masquer en `display:none` le retirerait aussi aux
     lecteurs d'écran et aux moteurs. */
  html.no-js .hero-word, html.no-tween .hero-word{
    position:absolute;width:1px;height:1px;inset:auto;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap}
