/* ==========================================================================
   LE TROU — un film piloté par le défilement, et CE QUI MONTE PAR-DESSUS.
   Version de labo, refaite le 22/08 à 23h30 sur consigne du PO : « regarde la
   compo et comment c'est géré sur l'ancienne version ».

   ⚠️ MA PREMIÈRE VERSION S'Y PRENAIT À L'ENVERS, et la comparaison avec
   FilmsSuite.astro l'a montré en trois lignes. Je faisais DÉBORDER LA BOÎTE
   collante — donc je changeais sa hauteur, donc la course du film, qu'il
   fallait ensuite rattraper par un décalage d'origine en JS. Le site fait
   l'inverse, et son commentaire le dit noir sur blanc : « PAS d'overflow
   hidden : c'est la vidéo qui déborde, la boîte garde une hauteur STABLE —
   c'est elle qui fait la course, et une course qui change en cours de
   défilement serait un film qui accélère ».
   Règle retenue : **la boîte mesure, la vidéo peint.** On ne fait jamais
   déborder l'objet dont on se sert comme étalon.

   ⚠️ CE N'EST PAS UN CALQUE : rien de fixe, rien en z-index de chrome. Une
   boîte `absolute` replaquée en JS — donc dans la couche de défilement — et
   du flux par-dessus.

   ⚠️ RÉVISION DU 23/08, mesurée sur l'appareil : cette version disait faire
   « comme le site » en posant un `sticky`. C'était faux — le site pose son
   `.ft-pin` en `absolute` et l'épingle en JS. Le `sticky` promeut la boîte en
   couche compositée, WebKit la rogne au viewport, et la vidéo cesse de peindre
   sous les barres : c'était TOUTE la bande. L'ATTACHE GLISSANTE n'est donc
   plus un calque optionnel, c'est ce qui rend l'épinglage (voir nu-film.js).

   Volontairement SANS : la porte, l'écran d'attente à jauge, la teinte de
   chrome échantillonnée sur le film. Ce sont d'autres calques ; ils viendront
   s'ils doivent venir.
   ========================================================================== */

/* ── L'AMORCE — LA PAGE S'OUVRE DÉJÀ DÉFILÉE ───────────────────────────────
   Loi reprise du site (film-trou.js:366) et revérifiée sur appareil le 23/08 :
   à `scrollY 0`, les 80 px dont la vidéo déborde vers le haut sont AU-DESSUS
   DU DOCUMENT — rien ne les peint, et la zone de statut retombe sur le canevas.
   **Agrandir le débord n'y peut RIEN** : ce n'est pas sa taille qui manque,
   c'est qu'il est hors de la page. Le seul levier est d'ouvrir la page déjà
   défilée de quelques dizaines de pixels.
   ⚠️ ET ELLE DOIT ÊTRE NEUTRE POUR LE FILM : sans le décalage ci-dessous,
   ces 90 px comptaient comme du défilement de film et le rouvraient à
   `currentTime = 1,08 s` (mesuré, test 8). Le décalage rend au film son
   origine : il rouvre à t = 0, plein écran. */
:root {
    --ft-amorce: 90px;
    /* La PORTE BASSE a la même profondeur que la haute : sur un corner
       ventilé la page FINIT sur un film, et un scrub qui tape la butée du
       document fait ressortir la barre basse d'iOS (constat PO 24/08,
       « quand je scroll jusqu'en bas, on retrouve la barre du bas »).
       Même remède qu'en haut : une réserve, et un plafond qui rattrape
       doigt levé (nu-film.js). Continuer À TRAVERS la réserve reste le
       geste sud — la navigation vers L'ACTU prime, comme au nord. */
    --ft-reserve-basse: 90px;
    /* ── LE FONDU — CE QUI REMPLACE LA COUPE FRANCHE ───────────────────────
       Demande PO du 23/08 : « faire monter le film suivant EN POSE, et qu'il
       apparaisse en fondu par-dessus, piloté par le défilement ».
       Avant : la boîte du film sortant remontait pendant que la suivante
       arrivait par le bas — un écran entier de succession, plus un joint de
       90 px (l'amorce s'appliquait à TOUTES les boîtes, pas seulement à la
       première : deux bandes de canevas nu sur un corner à trois films).
       Maintenant : les boîtes se CHEVAUCHENT. Le film sortant reste épinglé
       pendant que l'entrant, déjà épinglé lui aussi et calé sur son image
       zéro, monte en opacité par-dessus. Personne ne bouge, c'est la
       transparence qui travaille — et c'est le défilement qui la pilote.
       ⚠️ 25lvh ET 20 % VONT ENSEMBLE, c'est le seul couplage du montage :
       la plage d'épinglage vaut `100lvh + fondu`, donc le fondu en occupe
       `fondu / (100lvh + fondu)` = 25/125 = **20 %** (voir @keyframes
       ft-fondu). Changer l'un sans l'autre décale le fondu. La valeur a été
       choisie ronde POUR ÇA : un offset en longueur dans `animation-range`
       aurait évité le couplage, mais une plage qui ne parse pas retombe sur
       `normal` — donc sur un film à demi transparent pendant toute sa
       course. On ne prend pas ce risque-là sur WebKit. */
    --ft-fondu: 25lvh;
}
.ft {
    position: relative; width: 100vw; margin-left: calc(50% - 50vw);
    margin-top: var(--ft-amorce, 90px);
    /* ⚠️ CONTEXTE D'EMPILEMENT PAR BOÎTE, et il est la moitié du fondu.
       Sans lui, `.ft-flux` (z-index 1) du film SORTANT peindrait par-dessus
       le `.ft-pin` (z-index 0) du film ENTRANT — son cartel flotterait sur le
       film suivant au lieu de disparaître avec lui. Avec `z-index: 0` chaque
       boîte devient une SCÈNE : film + cartel voyagent ensemble, et l'ordre
       du document suffit à dire qui recouvre qui. */
    z-index: 0;
    /* ⚠️ ET LA RÈGLE REMONTE JUSQU'ICI, à la BOÎTE — vérifié avec
       `elementFromPoint` sur le CTA : ce n'était ni le pin ni le flux qui
       captaient le tap, c'était **le conteneur `.ft` de la boîte suivante**.
       La neutraliser plus bas ne servait à rien : `.ft` est un div ordinaire,
       il reçoit, et le chevauchement le pose par-dessus le cartel du film
       précédent. Toute la boîte est donc décor, et seul le CTA reprend la
       main (`pointer-events: auto`, plus bas). */
    pointer-events: none;
}
/* LE CHEVAUCHEMENT. La boîte fait `200lvh + fondu`, sa plage d'épinglage
   `100lvh + fondu` ; on remonte la suivante de `100lvh + fondu` pour que son
   épinglage COMMENCE là où le film précédent finit sa course. Les deux sont
   donc épinglés ensemble pendant exactement la durée du fondu.
   Et c'est aussi ce qui supprime le joint : l'amorce ne vaut plus que pour la
   PREMIÈRE boîte, ce qu'elle a toujours voulu dire. */
.ft + .ft { margin-top: calc(-100lvh - var(--ft-fondu)); }

/* LA BOÎTE QUI MESURE. Hauteur STABLE de 100lvh — c'est l'étalon de la
   course, il ne bouge pas. En `absolute`, elle est SORTIE DU FLUX : elle ne
   le pousse plus, et le `margin-bottom: -100lvh` qui servait à ça sur la
   version `sticky` n'a plus d'objet (une marge d'absolu n'affecte rien).
   C'est `.ft-flux` qui donne désormais sa hauteur à `.ft`, donc la course.
   ⚠️ LA LOI DU 22/08 EST INFIRMÉE : elle disait « ce n'est pas `sticky` qui
   interdit de peindre dans les bandes réservées, c'est la TRANSPARENCE ».
   Mesuré le 23/08 : la boîte était OPAQUE et la vidéo ne peignait pas pour
   autant — c'est bien l'épinglage natif, par la composition qu'il déclenche,
   qui l'en empêchait. */
.ft-pin {
    /* ⚠️ `absolute`, ET SURTOUT PAS `sticky` — c'est LE correctif du 23/08,
       mesuré sur l'iPhone du PO (tests 1 à 9, pont ios-webkit-debug-proxy).
       Un `sticky` de cette taille promeut sa sous-arborescence en COUCHE
       COMPOSITÉE, et WebKit rogne cette couche au viewport : la vidéo cesse
       alors de peindre dans les zones de barres, quel que soit son débord.
       Safari y montre à la place le `background` de CETTE boîte — d'où la
       bande, taupe ici parce que c'est la couleur du décor.
       Preuves : pin repeint en cyan → la bande devient cyan (test 2) ; pin
       en `absolute` → « ça passe bien dessous » (test 6, verdict PO).
       ⚠️ ET CE QUE FAIT LE SITE, EXACTEMENT — corrigé par Tommy le 23/08,
       vérifié : `FilmsSuite.astro:257` pose bien `sticky` EN BASE ; l'
       `absolute` que j'ai mesuré sur l'iPhone du PO vient d'un bloc
       `@supports (animation-timeline: view())` (l.75-87) que Safari 26
       satisfait. Le site n'épingle donc PAS en JS : il épingle avec
       `animation-timeline`, et retombe sur `sticky` ailleurs — où il porte
       le même défaut. C'est la loi du 21/08, aujourd'hui CONFIRMÉE : « seul
       `animation-timeline` laisse les barres traversantes ».
       ⚠️ COMMENTAIRE CORRIGÉ LE 23/08 À 11:55 — il décrivait l'état d'avant.
       L'épinglage est désormais rendu par `animation-timeline` (bloc
       `@supports` plus bas), donc PAR LE COMPOSITEUR. L'attache de
       nu-film.js, qui écrit le `top` à chaque frame sur le THREAD PRINCIPAL,
       n'est plus que le REPLI des moteurs sans la propriété : elle tremblait
       (verdict PO), exactement comme Sofia l'avait annoncé. Le bon montage,
       et c'est celui qui est en place : `animation-timeline` quand il est
       disponible, l'attache rAF en repli — jamais `sticky`.
       La hauteur, elle, ne bouge pas : la boîte mesure, la vidéo peint. */
    position: absolute; top: 0; left: 0; right: 0;
    height: 100lvh;
    z-index: 0;
    /* ⚠️ LE PIN N'ATTRAPE RIEN — et c'est LE correctif des CTA morts (constat
       PO du 23/08 : « on ne peut pas cliquer sur leurs CTA, ils passent sous
       les vidéos ? »). Il avait raison, et la cause est le CHEVAUCHEMENT
       introduit ce soir avec le fondu.
       Le cartel d'un film vit à ~200lvh du haut de SA boîte ; or la boîte
       SUIVANTE commence 100lvh plus bas. Le cartel est donc physiquement DANS
       le territoire du film suivant, dont le pin — même à `opacity: 0`,
       c'est-à-dire invisible — reste un élément qui reçoit les taps. Chaque
       boîte étant son propre contexte d'empilement (`.ft { z-index: 0 }`), la
       suivante passe TOUJOURS au-dessus de la précédente.
       ⇒ Le film est un décor : il n'a aucune commande, il ne doit rien
       recevoir. C'est déjà ce que dit `nu-film.css` de la vidéo (« un tap ne
       doit même pas l'atteindre ») ; la règle remonte simplement au pin.
       C'est aussi le montage du site (`.ft-flux { pointer-events: none }` +
       `.ft-cta { pointer-events: auto }`) — il l'avait, on ne l'avait pas. */
    pointer-events: none;
    /* ⚠️ LA TEINTE DE LA PAGE, PAS LE NOIR. Le site met ici le #14120f de sa
       porte, et il a raison chez lui : son écran d'attente garantit qu'aucun
       film n'entre à moitié chargé. Le labo n'a pas de porte — un film pas
       encore prêt laisse donc voir cette couleur, et un noir plein sous les
       barres translucides d'iOS, c'est très exactement « on prend les barres »
       (constat PO). Dans la teinte du décor, le même trou se FOND au lieu de
       poser une bande. */
    background: var(--mobile-grad-top, #58534d);
    /* PAS d'`overflow: hidden` : c'est la vidéo qui sort de cette boîte. */
}

/* LA VIDÉO QUI PEINT, et qui déborde DANS LES DEUX SENS.
   En haut, 80 px : sur iOS 26 la barre de statut est TRANSLUCIDE — son verre
   ne montre que ce qui est peint sous le bord. Sans débord, elle retombait
   sur le fond de page : c'était la bande. Et la zone NE SE MESURE PAS —
   `env(safe-area-inset-top)` répond 0 sur l'appareil alors que la barre est
   là. Constante large à dessein ; ce qu'elle coûte au cadrage est marginal
   (cover).
   En bas, `100lvh - 100svh` est la hauteur MESURÉE de la barre d'adresse
   repliée (40 px sur l'appareil du PO), plus 100 px : déployée, la barre
   d'iOS 26 est bien plus haute, et son verre échantillonne au-delà.
   ⚠️ CALC À PLAT, JAMAIS DE PARENTHÈSES IMBRIQUÉES : quatre termes
   parenthésés font segfauter le minifieur CSS (bissection du 20/08). */
.ft-video {
    position: absolute; left: 0; width: 100%;
    top: -80px;
    height: calc(100% + 80px + 100lvh - 100svh + 100px + env(safe-area-inset-bottom, 0px));
    object-fit: cover; display: block;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.45s ease;
}
.ft.est-prete .ft-video { opacity: 1; }

/* ══════════════════════════════════════════════════════════════════════════
   LA SUITE D'IMAGES — le même trou, sans décodeur vidéo (bascule du 23/08,
   verdict PO sur `/suite.html` : « carrément plus fluide même en scrollant
   super vite »).
   La GÉOMÉTRIE NE CHANGE PAS D'UN PIXEL : même `.ft`, même `.ft-pin`, même
   épinglage par le compositeur, même fondu entre films. Seul le CONTENU du
   pin change — deux <img> au lieu d'un <video>. C'est ce qui a rendu la
   bascule sûre : rien de ce qui avait été validé aujourd'hui n'est rejoué.
   ⚠️ POURQUOI DEUX IMAGES : garder une image par position, ce serait 121
   images décodées, soit 319 Mo — les mêmes 319 Mo qui ont fait écarter le
   sprite. Une à l'écran, une qu'on prépare, et `img.decode()` entre les deux
   pour ne jamais montrer un trou blanc (voir nu-suite.js).
   ⚠️ ET AUCUNE TRANSITION SUR L'OPACITÉ : l'échange se fait image décodée
   contre image décodée, à chaque image du scrub. Une transition ajouterait un
   glissement de 300 ms sur CHACUNE — l'inverse exact du but. */
.sq-img, .sq-alt {
    position: absolute; left: 0; width: 100%;
    top: -80px;
    height: calc(100% + 80px + 100lvh - 100svh + 100px + env(safe-area-inset-bottom, 0px));
    object-fit: cover; display: block;
    pointer-events: none;
    opacity: 0;
}
/* ── LE LOADER, DISCRET, SUR LA PREMIÈRE IMAGE (arbitrage PO du 23/08) ─────
   ⚠️ SURTOUT PAS UN CALQUE PLEIN ÉCRAN PEINT. Le site a payé cet os le
   21/08 : sa porte, `fixed` et pleine, faisait passer les barres d'iOS 26 en
   régime OPAQUE — et dès le chargement, avant même qu'on ait vu une image.
   Ici : un filet de 2 px collé en bas. Il ne couvre rien, il ne peut pas
   rallumer ce régime.
   Et il est HONNÊTE, ce qu'un loader de vidéo ne peut pas être : on compte
   les images arrivées sur celles qu'annonce le manifeste. Un décompte, pas
   une estimation d'octets. */
.sq-jauge {
    position: absolute; z-index: 2; left: 0; right: 0;
    bottom: calc(18px + env(safe-area-inset-bottom, 0px));
    display: flex; align-items: center; gap: 10px;
    padding: 0 34px;
    transition: opacity .5s ease;
}
.sq-piste { flex: 1; height: 2px; border-radius: 2px; background: rgba(255, 255, 255, .22); overflow: hidden; }
.sq-fil { display: block; height: 100%; width: 0; background: #b4cd00; transition: width .25s ease; }
.sq-nb {
    font: 500 11px/1 ui-monospace, Menlo, monospace; letter-spacing: .12em;
    color: rgba(255, 255, 255, .62); font-variant-numeric: tabular-nums;
    text-shadow: 0 1px 6px rgba(0, 0, 0, .5);
}
/* La jauge s'efface quand TOUT est là — pas quand le scrub devient utilisable :
   le visiteur n'a pas à savoir qu'il reste des trous à combler. */
.sq-complet .sq-jauge { opacity: 0; }

/* CE QUI DÉFILE PAR-DESSUS — la compo du site : une boîte par pièce, le film
   collé dessous, les éléments qui montent (PO 20/08).
   LA COURSE EST GARANTIE, PAS DÉDUITE : la hauteur du flux moins un écran =
   la course. Le PO a validé 100lvh de défilement pour 10 s (« course
   courte »), donc le flux fait 200lvh — et ça ne peut pas dépendre de la
   longueur d'un titre. `min-height` le planche : un contenu plus long
   allonge, jamais ne raccourcit. */
.ft-flux {
    position: relative; z-index: 1;
    /* Même raison que le pin, et c'est la seconde moitié : le flux de la boîte
       suivante recouvre lui aussi le cartel de la précédente. Il est vide à
       cet endroit, mais un vide qui capte est un vide qui bloque. Seul ce qui
       doit être touché reprend la main, un cran plus bas. */
    pointer-events: none;
    /* `+ fondu` : la boîte s'allonge de la durée du fondu, puisque son
       épinglage dure maintenant `100lvh + fondu`. */
    min-height: calc(200lvh + var(--ft-fondu));
    display: flex; flex-direction: column;
    /* 34px : le couloir qui dégage le rail ouest de la boussole (44 px
       mesurés) sans enfermer le bloc dans la colonne de texte.
       En haut, la réserve de la marque — reprise ici après avoir été retirée
       de la page : ce qui MONTE par-dessus le film ne doit pas percuter le
       logo, mais la boîte, elle, reste calée à zéro.
       ⚠️ + 34px : L'AIR SOUS LA MARQUE, ET IL EST MESURÉ (verdict PO du 23/08,
       « un peu trop top sur la première »). Avec la seule réserve de marque,
       le H1 du héros démarrait à **1 px sous le bord bas du logo** (relevé :
       marque 0 → 107, H1 à 108) et passait EN PLUS sous le badge de recette
       `.v-labo`, qui est calé en absolu à 104 px (nu-film.css plus bas). Tant
       que rien ne montait dans le premier film, la place était vide et le
       défaut n'existait pas ; le héros l'a révélé.
       La valeur n'est pas nouvelle : c'est le MÊME couloir de 34 px que sur
       les côtés — la page garde un seul rythme, on n'invente pas une
       constante verticale pour un seul endroit. */
    /* ⚠️ LE BAS REND LE FONDU, ET C'EST CE QUI PRESERVE LE CARTEL.
       Le cartel est poussé en bas du flux par `margin-top: auto`. La boîte
       ayant grandi de `fondu`, il descendrait d'autant — donc il n'arriverait
       plus au bord bas de l'écran à la fin de la course, mais pendant le
       fondu, déjà recouvert. Cette réserve lui rend sa place : il se pose au
       même pixel qu'avant, à la fin de la course, et ne dérive que pendant le
       fondu — sous le film qui le recouvre. */
    padding: calc(var(--brand-h, 0px) + 34px + env(safe-area-inset-top, 0px)) 34px var(--ft-fondu);
}
/* ── L'ÉPINGLAGE PAR LE COMPOSITEUR — CORRECTIF DU 23/08 ─────────────────
   Verdict PO sur l'attache rAF : « ça tremble en descente ». C'est le
   BLOQUANT ② de Sofia, annoncé AVANT qu'on teste : replaquer le pin en JS à
   chaque image le fait vivre sur le THREAD PRINCIPAL, pendant que le
   défilement d'iOS vit sur le thread COMPOSITEUR. Le pin retarde d'une image,
   et ça se voit. La lecture de teinte lui volait en plus 11 ms par appel
   (mesuré sur appareil : 174 appels, 1 916 ms cumulées) — corrigé d'abord,
   sans que le tremblement cède : la cause est bien l'attache, pas la teinte.
   Une animation pilotée par le défilement tourne SUR LE COMPOSITEUR depuis
   Safari 26.4 (l'appareil du PO est en 26.6) : elle ne peut plus trembler,
   quoi que fasse le thread principal.
   C'est exactement ce que fait le site (`FilmsSuite.astro:74-87`), et c'est
   la loi du 21/08 enfin confirmée : « seul `animation-timeline` laisse les
   barres traversantes ». L'attache rAF reste en REPLI pour les moteurs qui
   n'ont pas la propriété — jamais `sticky`, qui rapporterait la bande.

   ⚠️ LE FLUX PASSE DE `min-height` À `height` DANS CE BLOC, ET CE N'EST PAS
   COSMÉTIQUE. La keyframe déplace le pin de 100lvh EXACTEMENT ; elle n'est
   juste que si la course vaut 100lvh, donc si la boîte fait 200lvh PILE.
   Avec `min-height`, un contenu plus long allongerait la boîte, la course
   dépasserait la keyframe et le pin se décrocherait avant la fin. Le site
   fait le même arbitrage.
   ⚠️ ET LE JOUR OÙ CE BLOC PASSERA DANS LE PIPELINE CSS D'ASTRO : le
   minifieur fusionne `animation-timeline` dans le raccourci `animation`, la
   syntaxe devient invalide, Safari jette TOUT et le pin reste figé (os payé
   par le site la nuit du 20-21/08 — le site l'injecte donc en `set:html`).
   Ici le fichier est servi TEL QUEL depuis `public/`, le piège ne se pose
   pas encore. Il se posera. */
@supports (animation-timeline: view()) {
    .ft { view-timeline: --ft-boite block; }
    .ft .ft-pin {
        animation-name: ft-epingle;
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: --ft-boite;
        animation-range: contain 0% contain 100%;
    }
    /* LE FONDU N'EST POSÉ QUE SUR LES BOÎTES SUIVANTES — `.ft + .ft`, donc
       jamais sur la première. La première n'a personne à recouvrir, et un
       fondu sur elle voudrait dire ouvrir la page à 0 d'opacité : on arrive
       DEDANS le film (loi du 22/08), pas devant.
       Deux animations, UNE seule plage : `animation-range` n'a qu'une valeur,
       elle s'applique aux deux. L'épinglage court sur toute la plage, le
       fondu se termine à 20 % d'elle (voir @keyframes ft-fondu). */
    .ft + .ft .ft-pin { animation-name: ft-epingle, ft-fondu; }
    /* LE CONTENU ENTRE AVEC SON FILM — constat PO du 24/08 (capture) :
       pendant le fondu, le PIN de la boîte entrante fondait mais pas son
       FLUX — le bloc de récit arrivait plein pot sur le cartel du film
       sortant, encore en scène. Même plage, même courbe que le pin : c'est
       la loi du 23/08, « tout ce qui bouge porte avec lui ce qu'il
       montre » — son image ET son texte. Pas d'épinglage ici : le flux
       défile, seul le fondu s'applique. (Aucun risque de CTA fantôme : à
       ce moment le cartel de l'entrante est à deux écrans sous le bord.) */
    /* ⚠️ SUR LES ENFANTS, PAS SUR LE FLUX — révision de la Boucle du 24/08
       (Antoine + Sofia, spec filter-effects-2) : une opacité ANIMÉE sur le
       conteneur en fait une racine de backdrop — le verre du récit perdrait
       le droit d'échantillonner le film sous lui. On fond donc chaque enfant
       directement. Le récit, lui, n'est PAS dans cette liste : sa fenêtre
       (ft-recit-fenetre, plus bas) commence à opacité 0 et couvre toute la
       période du fondu — lui donner les deux animations multiplierait les
       courbes d'opacité. */
    /* ── LE LEVER DU PRODUIT — demande PO du 24/08 ────────────────────────
       « Un fade-in sur les produits, asynchrone des blocs textes. » Le
       cartel a donc SA fenêtre d'opacité, décalée de celle du récit : le
       texte rend l'antenne en haut (62→70 %) pendant que le produit se lève
       en bas (62→74 %). Elle REMPLACE l'ancien ft-fondu d'entrée des boîtes
       2-7 : commencer à 0 jusqu'à 62 % couvre a fortiori toute la période
       du fondu — même argument que le récit ci-dessus, une seule courbe
       d'opacité par élément. La visibility suit (discrète) : sans elle, le
       CTA invisible serait cliquable dès que le cartel entre à l'écran
       (~50 % de course) — un lien fantôme. Pas de transform : le produit
       n'est pas épinglé, il monte avec le flux, seul son lever est piloté. */
    .ft .ft-bloc {
        animation-name: ft-bloc-lever;
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: --ft-boite;
        animation-range: contain 0% contain 100%;
    }
    /* ── LE LEVER CALÉ PAR FILM — demande PO du 24/08 soir ────────────────
       « Le cartel tombe exactement au meilleur moment pour chaque vidéo. »
       Le meilleur moment = la pièce ENTIÈRE à sa taille maximale, avant que
       le recul la rapetisse — MESURÉ sur planches-contact des 7 suites
       (13 échantillons/film, scratchpad du 24/08). Le fondu du cartel se
       termine PILE au début du pic. Un film sans réglage garde le lever
       générique 62→74 (ft-bloc-lever). Sélecteur avec slash final : le
       jour où `immortelle-curvy-2` existera, `*="curvy"` l'attraperait. */
    .ft[data-suite*="immortelle-dentelle/"]  .ft-bloc { animation-name: ft-bloc-lever-dentelle; }   /* pic 45-50 % */
    .ft[data-suite*="immortelle-grid/"]      .ft-bloc { animation-name: ft-bloc-lever-grid; }       /* pic 42-50 % */
    .ft[data-suite*="immortelle-curvy/"]     .ft-bloc { animation-name: ft-bloc-lever-curvy; }      /* pic 33-42 % */
    .ft[data-suite*="immortelle-camelia/"]   .ft-bloc { animation-name: ft-bloc-lever-camelia; }    /* pic 58-67 % */
    .ft[data-suite*="immortelle-bee/"]       .ft-bloc { animation-name: ft-bloc-lever-bee; }        /* pic 40-50 % */
    .ft[data-suite*="immortelle-entrelas/"]  .ft-bloc { animation-name: ft-bloc-lever-entrelas; }   /* pic 50-58 % */
    .ft[data-suite*="immortelle-mon-amour/"] .ft-bloc { animation-name: ft-bloc-lever-mon-amour; }  /* pic 45-60 % */
    /* ── LA FENÊTRE DU RÉCIT — synthèse de la Boucle du 24/08 ─────────────
       Diagnostic unanime (Hélène, Sofia) : un bloc qui défile librement ne
       peut PAS être lisible — il occupe ~85 % de la course et sort de
       l'écran pendant le fondu même (« trop tôt ou trop tard », constat PO).
       Le patron : invisible pendant l'entrée et le fondu, il SE LÈVE à 22 %,
       TIENT immobile à l'écran de 22 à 62 % (translation compensatoire au
       rythme exact du défilement — la mécanique de ft-epingle, jamais de
       sticky), s'efface à 70 % — avant que le détourage du cartel monte
       dans sa zone (Hélène : le PNG atteint l'altitude du récit vers 54-65 %
       de plage). Chaque récit tient à SA position de flux (sous la marque
       pour les boîtes 2-7, sous l'emplacement du héros pour la première).
       Bornes 22/70 loin des seuils 0 %/100 % (bug de progress corrigé
       seulement en Safari 26.5 — Sofia, release notes vérifiées).
       S'applique à TOUTES les boîtes : à l'arrivée sur le corner, le récit
       de Dentelle est éteint et se lève au premier geste (validé PO).
       Repli sans animation-timeline : récit toujours visible, comme avant —
       dégradé, pas cassé. */
    .ft .ft-recit {
        animation-name: ft-recit-fenetre;
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: --ft-boite;
        animation-range: contain 0% contain 100%;
    }
    /* ── LA FENÊTRE POUR TOUT WORDING — normalisation PO du 24/08 ─────────
       « Les fenêtres stickées j'adore : on normalise ça pour tous les
       contenus du corner, même le start, tous en fondus, en haut sous le
       logo. » Le héros cesse donc de défiler librement : il devient une
       fenêtre comme le récit — il se lève dès le premier geste, tient sous
       la marque, s'efface ; PUIS le récit de la première boîte prend la
       MÊME place (fenêtre décalée 44-70 %, au lieu de vivre sous le héros).
       Un battement de film nu entre les deux (40→44 %) : c'est la
       respiration, pas un trou. Les boîtes 2-7 ne changent pas : leur récit
       tenait déjà sous le logo. Le produit (png + titre + cotes + CTA)
       reste en bas — décision PO du même jour. */
    .ft .ft-hero {
        animation-name: ft-hero-fenetre;
        animation-timing-function: linear;
        animation-fill-mode: both;
        animation-timeline: --ft-boite;
        animation-range: contain 0% contain 100%;
    }
    .ft .ft-hero + .ft-recit { animation-name: ft-recit-fenetre-second; }
    /* Le même ancrage pour les deux fenêtres de la première boîte : une
       GRILLE dont elles partagent la première cellule (le flux garde son
       couloir de padding, les marges du verre continuent de dégager les
       rails). En repli sans timeline, le flex d'origine reste : hero puis
       récit empilés, toujours visibles — dégradé, pas cassé. */
    .ft .ft-flux {
        min-height: 0; height: calc(200lvh + var(--ft-fondu));
        display: grid; grid-template-rows: 1fr auto;
    }
    .ft .ft-hero, .ft .ft-recit {
        grid-row: 1; grid-column: 1;
        align-self: start; justify-self: start;
    }
    .ft .ft-hero + .ft-recit { margin-top: 0; }
    .ft .ft-bloc { grid-row: 2; grid-column: 1; align-self: end; }
}
@keyframes ft-epingle {
    /* ⚠️ LA DESTINATION EST EN `dvh`, ET C'EST LE CORRECTIF DU SAUT — mesuré
       par le câble sur l'iPhone (24/08, WebDriver). La plage `contain` dure
       (hauteur de boîte − viewport RÉEL) : 1045 px barres effacées, 1085 px
       en mode réservé (796). Une translation figée à `100lvh + fondu`
       (1045 px) fait donc DÉRIVER le pin en mode réservé : 40 px perdus sur
       la course, 0,037 px/px mesuré (= 40/1085), et la bascule de mode en
       plein geste rejoue ces 40 px d'un coup — le « ça saute » résiduel,
       invisible de toutes les métriques d'IMAGES (FIGE 0, CONTRESENS 0).
       La destination s'écrit donc comme la plage se mesure :
       boîte (200lvh + fondu) − viewport réel.
       ⚠️ ET LE VIEWPORT RÉEL N'EST PAS `100dvh` : mesuré sur l'appareil
       (24/08, matrice du compositeur lue par la sonde puis par câble), un
       `dvh` écrit DANS une keyframe est résolu AU CHARGEMENT et ne se remet
       pas à jour à la bascule de barres — `att` passait de +39 à −40 selon
       le mode de naissance de la page : même dérive, miroir. C'est donc une
       VARIABLE que le viewport traverse : `--vh-reel`, tenue à `innerHeight`
       par nu-film.js à chaque frame où elle change. Une custom property qui
       change invalide la keyframe et recommet l'animation au compositeur —
       vérifié par câble : la matrice suit en une frame (1045 ↔ 1006), pin à
       0 px pile avec la valeur juste. Le repli `100dvh` (sans JS) reste
       juste dans le mode de chargement, faux de 40 px dans l'autre — comme
       avant, jamais pire. Calc à PLAT. */
    to { transform: translateY(calc(200lvh + var(--ft-fondu) - var(--vh-reel, 100dvh))); }
}
/* ⚠️ 20 % = fondu / (100lvh + fondu), avec fondu = 25lvh. Le nombre est ROND
   par construction (voir `--ft-fondu`) : c'est la seule façon d'exprimer la
   longueur du fondu en pourcentage d'une plage `contain` sans passer par un
   offset en longueur dans `animation-range`, dont l'échec de parsing serait
   silencieux et catastrophique. Les deux valeurs se tiennent : on ne touche
   à l'une qu'en touchant à l'autre. */
/* La tenue : entre 22 % et 70 %, la translation interpole de 0,22·L à
   0,70·L — exactement la distance défilée sur ce segment : le bloc est
   immobile à l'écran pendant que le film joue derrière. Avant la fenêtre
   il est déjà posé à 0,22·L (invisible, peu importe où).
   ⚠️ L'ÉPINGLAGE COUVRE DÉSORMAIS L'EXTINCTION — demande PO du 24/08 soir
   (« les textes, fixe-les aussi, tout en fade in/out ») : avant, la
   translation gelait à 62 % et le bloc repartait avec le flux EN
   s'effaçant — un glissement visible pendant le fondu de sortie. Le
   transform tient maintenant jusqu'à 70 % : le texte naît, tient et meurt
   AU MÊME PIXEL, seule l'opacité vit. Les keyframes 28/62 ne portent que
   l'opacité : le transform interpole 22→70 sans elles (par propriété).
   ⚠️ L = plage contain = (200lvh + fondu − viewport réel) : les
   pourcentages BOUGENT si --ft-fondu bouge — même couplage documenté que
   le 20 % du fondu. Calc À PLAT, servi tel quel depuis public/. */
@keyframes ft-recit-fenetre {
    0%, 22% { opacity: 0;
              transform: translateY(calc(44lvh + 0.22*var(--ft-fondu) - 0.22*var(--vh-reel, 100dvh))); }
    28%     { opacity: 1; }
    62%     { opacity: 1; }
    70%, 100% { opacity: 0;
              transform: translateY(calc(140lvh + 0.70*var(--ft-fondu) - 0.70*var(--vh-reel, 100dvh))); }
}
@keyframes ft-fondu {
    from { opacity: 0; }
    20%  { opacity: 1; }
    to   { opacity: 1; }
}
/* Le lever du produit (voir le bloc .ft-bloc plus haut) : éteint et
   inatteignable jusqu'à 62 %, levé à 74 %, tenu jusqu'au bout — c'est la
   destination de la course, le fondu de la boîte suivante le recouvrira.
   ⚠️ ET IL EST FIXÉ (demande PO du 24/08 soir : « on peut fixer les
   cartels ? ») — même algèbre que les fenêtres, mais À REBOURS : le cartel
   vit en BAS du flux, sa place naturelle à l'écran n'est atteinte qu'à
   100 % de course. Pour qu'il TIENNE à cette place dès son lever, on le
   remonte de (1−p)·L : à 62 % il est tiré de −0,38·L (déjà posé au pixel
   final, invisible), la translation fond linéairement vers 0 au rythme
   exact du défilement — immobile à l'écran, translateY(0) pile à la fin.
   La 74 % ne porte que l'opacité (interpolation par propriété).
   L = 200lvh + fondu − viewport réel ; calc À PLAT. */
@keyframes ft-bloc-lever {
    0%, 62% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-76lvh - 0.38*var(--ft-fondu) + 0.38*var(--vh-reel, 100dvh))); }
    74%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
/* Les levers par film (voir le bloc de sélecteurs plus haut) — même algèbre
   à rebours que le générique : au seuil S, le cartel est tiré de −(1−S)·L
   (déjà posé au pixel final, invisible), et la translation fond vers 0 au
   rythme du défilement. Constantes : −(2·(100−S))lvh − (1−S)·fondu +
   (1−S)·vh-reel. Fondu de 8 points, terminé au début du pic. Calc À PLAT. */
@keyframes ft-bloc-lever-dentelle {   /* levé 38→46, pic à 45 */
    0%, 38% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-124lvh - 0.62*var(--ft-fondu) + 0.62*var(--vh-reel, 100dvh))); }
    46%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-grid {       /* levé 36→44, pic à 42 */
    0%, 36% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-128lvh - 0.64*var(--ft-fondu) + 0.64*var(--vh-reel, 100dvh))); }
    44%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-curvy {      /* levé 28→36, pic à 33 */
    0%, 28% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-144lvh - 0.72*var(--ft-fondu) + 0.72*var(--vh-reel, 100dvh))); }
    36%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-camelia {    /* levé 52→60, pic à 58 */
    0%, 52% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-96lvh - 0.48*var(--ft-fondu) + 0.48*var(--vh-reel, 100dvh))); }
    60%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-bee {        /* levé 34→42, pic à 40 */
    0%, 34% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-132lvh - 0.66*var(--ft-fondu) + 0.66*var(--vh-reel, 100dvh))); }
    42%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-entrelas {   /* levé 44→52, pic à 50 */
    0%, 44% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-112lvh - 0.56*var(--ft-fondu) + 0.56*var(--vh-reel, 100dvh))); }
    52%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
@keyframes ft-bloc-lever-mon-amour {  /* levé 40→48, pic à 45 */
    0%, 40% { opacity: 0; visibility: hidden;
              transform: translateY(calc(-120lvh - 0.60*var(--ft-fondu) + 0.60*var(--vh-reel, 100dvh))); }
    48%     { opacity: 1; visibility: visible; }
    100%    { opacity: 1; visibility: visible; transform: translateY(0); }
}
/* Les fenêtres de la PREMIÈRE boîte (normalisation PO 24/08) — même algèbre
   que ft-recit-fenetre : à p % la translation vaut p·L, L = plage contain =
   (200lvh + fondu − viewport réel), d'où (2p)lvh + p·fondu − p·vh-reel.
   Calc À PLAT, servi tel quel depuis public/.
   ⚠️ LE HÉROS EST LEVÉ DÈS L'OUVERTURE — révision PO du 25/08 sur capture
   (« en entrée il faut annoncer, inciter au scroll, donner envie ») : le
   patron « éteint au chargement, levé au premier geste » laissait une
   entrée MUETTE — rien n'annonçait le corner. Le héros est donc visible à
   0 %, tient épinglé jusqu'à 34 %, s'éteint sur place à 40 %. */
@keyframes ft-hero-fenetre {
    0%     { opacity: 1; transform: translateY(0); }
    34%    { opacity: 1; }
    40%, 100% { opacity: 0;
             transform: translateY(calc(80lvh + 0.40*var(--ft-fondu) - 0.40*var(--vh-reel, 100dvh))); }
}
/* Le récit de la première boîte, SECOND servi à la même place : il attend
   que le héros ait rendu la fenêtre (40 %), se lève à 44 %, et garde les
   bornes de sortie communes (62/70 — le détourage du cartel monte dans la
   zone vers 54-65 %, constat Hélène). */
@keyframes ft-recit-fenetre-second {
    0%, 44% { opacity: 0;
              transform: translateY(calc(88lvh + 0.44*var(--ft-fondu) - 0.44*var(--vh-reel, 100dvh))); }
    50%     { opacity: 1; }
    62%     { opacity: 1; }
    70%, 100% { opacity: 0;
              transform: translateY(calc(140lvh + 0.70*var(--ft-fondu) - 0.70*var(--vh-reel, 100dvh))); }
}

/* ── LE HÉROS, POSÉ SUR LE PREMIER FILM (demande PO du 23/08) ──────────────
   « Le texte d'intro dans le slide 1 ». C'est la loi du site (FilmsSuite,
   parti pris ③) : sans lui, le titre et le chapeau vivaient au-dessus de la
   suite, donc il restait une bande de page AVANT que le film commence — très
   exactement ce qu'on ne veut plus voir. Ils descendent donc DANS la première
   boîte, et le rédactionnel plus bas ne les répète pas : un seul H1.
   Il monte avec le flux, comme le cartel : rien n'est épinglé ici. */
/* ⚠️ PLUS DE VERRE — épure PO du 24/08 : « les cadres gris perturbent la
   lecture, on les retire ». Le panneau du 24/08 matin (rgba .42 + blur 16)
   avait été posé contre l'illisibilité sur salles claires ; l'épure le
   remplace par le TEXTE NU sur le film, l'ombre portée seule au travail —
   et le chantier TYPO qui suit devra reprendre la lisibilité que le verre
   assurait. Les marges restent : ce sont elles qui dégagent les rails de
   la boussole (bord gauche à 50 px, mesuré par le câble). */
.ft-hero {
    max-width: min(30ch, 100%);
    margin: 0 17px 0 16px;
}
/* ── LE TITRE DU HÉROS, DANS LA MISE EN PAGE DES TITRES D'ITEMS ───────────
   PO 25/08 : « Sculptures Immortelles doit véhiculer cette mise en page,
   mais en mode titre ». C'est la grille superposée de `.ft-titre` (les deux
   voix dans la même cellule), portée à l'échelle titre et FERRÉE À GAUCHE —
   le fer du héros, là où les cartels ferrent à droite. Mêmes rapports que
   l'item : la série fine UN CRAN AU-DESSUS du nom (2.4/2.1 → 2.9/2.5), le
   nom qui chevauche sa base (58 %) et déborde du fer d'un souffle. */
.ft-hero-titre {
    display: grid; justify-items: start;
}
.ft-soustitre {
    grid-area: 1 / 1;
    margin: 0; font-size: 2.9rem; font-weight: 200;
    letter-spacing: 0.05em; line-height: 1;
    /* Sans ombre, comme le filigrane des cartels : un souffle, pas un cri. */
    color: #fff; text-shadow: none;
}
.ft-h1 {
    /* La recette du nom d'item (600, chevauchement 58 %), à 2.5rem —
       l'échelle titre voulue le matin même (« Sculptures en plus gros »). */
    margin: 0; font-size: 2.5rem; font-weight: 600; line-height: 1.05;
    color: #fff; text-shadow: var(--shadow-text-strong, 0 1px 4px rgba(0, 0, 0, .45));
}
/* La superposition ne se fait QUE si la série est là : seul, le h1 reste
   dans le flux — sans ce garde, un corner sans sousTitre verrait son titre
   plonger de 58 % dans l'intro. */
.ft-soustitre + .ft-h1 {
    grid-area: 1 / 1; align-self: end; justify-self: start;
    /* Miroir du cartel (translate(10px, 58%)) : le héros ferre à gauche,
       le débord sort donc à gauche. */
    transform: translate(-2px, 58%);
}
.ft-intro {
    /* 1.22 : le MÊME interlignage que le récit et les autres paragraphes
       (PO 25/08). ⚠️ 36 px et non 10 : le h1 superposé DÉBORDE de sa grille
       de 58 % de sa hauteur (~24 px) — même compensation que .ft-cotes
       sous les cartels (28 = 8 + débord). */
    margin: 36px 0 0; font-size: 1.12rem; line-height: 1.22;
    /* Les retours à la ligne du frontmatter sont un CHOIX de composition
       (PO 25/08) — pre-line les honore, sans figer le reste du pliage. */
    white-space: pre-line;
    color: rgba(255, 255, 255, 0.82);
    text-shadow: var(--shadow-text-strong, 0 1px 4px rgba(0, 0, 0, .45));
}
/* L'invitation du héros — BLANCHE (retouche PO 25/08 : « supprimer le
   vert du "Découvrir…" ») : le texte rejoint la voix du héros, la flèche
   seule garde la couleur de l'avance. */
.ft-defiler {
    margin: 10px 0 0; font-size: 0.98rem; font-weight: 600;
    color: #fff;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
}
/* ── LA FLÈCHE DE DESCENTE, DANS LE HÉROS (PO 25/08 : « remonte la flèche
   sous le texte "Déroulez…" ») — même classe que l'indice global, donc
   même clic délégué (nu-gestes.js) et même respiration ; seule la POSITION
   change : elle quitte le bas fixe de l'écran pour le flux du héros,
   alignée sur le fer gauche du texte. */
.hint-defiler--hero {
    position: static; left: auto; transform: none; bottom: auto;
    /* Centrée sous le texte (retouche PO 25/08 : « la flèche au centre ») —
       margin auto dans la colonne du héros, place-items du socle conservé. */
    margin: 4px auto 0; width: 44px; height: 32px;
}
/* Et sa jumelle fixe se tait sur les pages à héros — deux flèches qui
   disent « descends » se contrediraient sur le même écran. */
html:has(.ft-hero) .hint-defiler:not(.hint-defiler--hero) { display: none; }

/* ── LE RÉCIT VENTILÉ (décision PO 24/08) ─────────────────────────────────
   Chaque boîte porte son bloc du rédactionnel, à la place du héros — même
   couloir, même ombre portée, voix plus basse que le H1 : c'est un
   chapitre, pas un titre de page. Sur la première boîte il vient SOUS le
   héros, d'où la marge haute. */
/* ⚠️ LE RÉCIT EST SUR VERRE SOMBRE — constat PO 24/08 : « les contenus se
   superposent sur les images des sculptures et il manque de visibilité ».
   Du blanc à l'ombre portée ne suffit pas sur ces films-là : les salles sont
   claires et la sculpture blanche passe SOUS le texte au fil du scrub. Le
   panneau de verre est le langage du site (tokens --lg-*) : il rend le bloc
   lisible sur n'importe quelle image sans la cacher — le film reste visible
   au travers. Marges négatives : le TEXTE reste calé au couloir de 34 px,
   c'est le verre qui déborde autour. Le héros, lui, ne change pas. */
.ft-recit {
    /* 32ch et non 26 : moins de lignes, drapeau moins haché (Boucle 24/08) */
    max-width: min(32ch, 100%);
    /* ⚠️ PLUS DE VERRE ICI NON PLUS (épure PO 24/08, voir le héros) — mais
       la contrainte des RAILS survit au cadre : rail ouest 0→47 px, rail
       est 328→375 (mesuré par le câble), et le bloc immobile vit pile à la
       hauteur des chevrons pendant la tenue. C'est désormais le TEXTE qui
       doit les dégager : bord gauche à 50 px (47 + air), bord droit ≤ 324.
       Les marges 16/17 dans le couloir de 34 px font exactement ça. */
    margin: 0 17px 0 16px;
}
.ft-hero + .ft-recit { margin-top: 22px; }
.ft-recit-titre {
    margin: 0; font-size: 1.3rem; font-weight: 700; line-height: 1.1;
    text-wrap: balance;   /* Safari 17.5+ ; ignoré ailleurs, sans casse */
    color: #fff; text-shadow: var(--shadow-text-strong, 0 1px 4px rgba(0, 0, 0, .45));
}
.ft-recit-texte {
    /* 1.22 : interlignage resserré un cran de plus (PO 25/08 — « plus
       étroit ») ; le 1.32 du 24/08 était déjà une première passe. */
    margin: 10px 0 0; font-size: 1.02rem; line-height: 1.22;
    /* pretty : l'algorithme paragraphe entier de Safari 26 — fini les
       dernières lignes orphelines (« de la / collection. »). 0.9 et non
       0.82 : c'est LE texte de lecture du corner, le contraste effectif
       sur verre à 0.42 au-dessus des salles claires flirtait avec le AA. */
    text-wrap: pretty;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: var(--shadow-text-strong, 0 1px 4px rgba(0, 0, 0, .45));
}

/* `margin-top: auto` : le cartel vit en BAS de la course, là où le film a
   fini de se dézoomer sur la pièce. Il ne flotte pas au milieu. */
.ft-bloc {
    margin-top: auto;
    /* ⚠️ LE CARTEL DÉGAGE LES RAILS DE LA BOUSSOLE, comme le héros et le
       récit (constat PO 25/08 : « le cartel est collé au CTA ouest ») —
       mesuré au câble sur 440 px : rail ouest 0→47, rail est 393→440, et
       le cartel vivait à 34→406. Mêmes marges 16/17 dans le couloir de
       34 px : bord gauche à 50 px, bord droit à écran−51. */
    margin-inline: 16px 17px;
    /* 84 px au lieu de 96 : douze pixels rendus au cartel, qui en a
       besoin une fois le texte monté d'un cran. */
    padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px));
    display: flex; flex-direction: column; align-items: flex-start;
}
/* ── LE PLUS BEL ENDROIT, PAR FILM — demande PO du 24/08 soir ─────────────
   Le cartel se range du côté que la pièce laisse LIBRE pendant son pic et
   le recul qui suit (lu sur les planches-contact). ⚠️ MIROIR GLOBAL demandé
   par le PO le 25/08 (« inverse la position de tous les cartels ») : la
   lecture d'origine plaçait Grid/Camélia/Mon Amour à droite et le reste à
   gauche — c'est désormais l'inverse, mêmes films d'exception. */
.ft .ft-bloc { align-items: flex-end; text-align: right; }
.ft[data-suite*="immortelle-grid/"]      .ft-bloc,
.ft[data-suite*="immortelle-camelia/"]   .ft-bloc,
.ft[data-suite*="immortelle-entrelas/"]  .ft-bloc,   /* ajusté PO 25/08 */
.ft[data-suite*="immortelle-mon-amour/"] .ft-bloc {
    align-items: flex-start; text-align: left;
}
/* ── LE DÉTOURAGE EST RETIRÉ — épure PO du 24/08 ──────────────────────────
   « Les png ne servent qu'à redire ce que les vidéos font déjà. » Le cartel
   redevient texte pur ; tombent avec lui : le lazy-load qui disputait la
   bande passante, la réserve d'aspect-ratio contre les sauts d'ancrage, et
   les DEUX drop-shadow plein écran que nav-gestures.js avait identifiés
   comme le vrai coût du glissement latéral. Les tirages recadrés restent
   dans le frontmatter (fiches, lightbox). */
/* ── LE TEXTE MONTE D'UN CRAN (« les textes sont trop petits », PO 23/08) ──
   Le nom de la pièce passe de 1,5 à 2 rem : c'est lui qui doit tenir l'écran
   face à une sculpture de 420 px, pas se faire avaler par elle. Les cotes et
   le bouton suivent, sans rattraper le titre — la hiérarchie ne bouge pas,
   c'est l'échelle qui bouge. */
/* ── LE TITRE EN DEUX VOIX — typo PO du 24/08 soir ────────────────────────
   « Le Immortelle écrit en très fin et le nom en petit à la ligne. » La
   série porte la taille mais pas la graisse (200 : le trait le plus fin
   que SF rende encore net sur film) ; le nom passe dessous, petit et
   espacé — c'est lui qu'on retient, c'est la série qu'on admire. */
/* ── LES DEUX MOTS SUPERPOSÉS — révision PO du 24/08 soir ─────────────────
   « Superpose-les, Immortelle ferré à droite. » Les deux voix partagent la
   même cellule de grille : la série, très fine, ferrée À DROITE comme un
   filigrane ; le nom, à la recette du H1 (2.1/600), ferré à gauche et
   descendu de 40 % de sa hauteur — il chevauche la base du filigrane sans
   l'éteindre. Les cotes reçoivent l'air que le débord du nom leur prend. */
.ft-titre {
    display: grid;
    color: #fff; text-shadow: 0 1px 10px rgba(0, 0, 0, 0.45);
}
.ft-serie {
    grid-area: 1 / 1; justify-self: end;
    font-size: 2.4rem; font-weight: 200;
    letter-spacing: 0.05em; line-height: 1;
    /* Sans ombre (PO) : le filigrane reste un souffle, seul le nom porte. */
    text-shadow: none;
}
.ft-nom {
    /* Ferré à droite lui aussi (révision PO) : les deux mots s'alignent sur
       le même fer, le nom chevauche la fin du filigrane. */
    grid-area: 1 / 1; justify-self: end; align-self: end;
    /* Détaché vers le bas et poussé d'un souffle hors du fer (PO) : le nom
       déborde du filigrane au lieu de le calquer. */
    transform: translate(10px, 58%);
    font-size: 2.1rem; font-weight: 600; line-height: 1.05;
    text-shadow: var(--shadow-text-strong, 0 1px 4px rgba(0, 0, 0, .45));
}
.ft-cotes {
    /* 28 px : 8 d'air + ~20 de débord du nom superposé (translateY 58 %). */
    font-size: 0.95rem; font-weight: 600; letter-spacing: 0.06em; margin-top: 28px;
    color: rgba(255, 255, 255, 0.72); text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
    font-variant-numeric: tabular-nums;
}
.ft-cta {
    /* LE SEUL ÉLÉMENT DU FILM QUI REÇOIT — tout le reste est décor.
       ⚠️ PASTILLE DE VERRE, TEXTE BLANC — révision PO du 25/08 (« passer
       en blanc, fond filter blur, léger liseret blanc ») : elle remplace
       le texte nu vert de l'épure du 24/08 soir. SANS AUCUNE TEINTE
       (retouche PO 25/08 : « ils doivent être transparents, mais flou ») —
       le premier essai posait un voile sombre à 0.18, refusé ; seul le
       FLOU fait le verre, le liseret fait le bord.
       `-webkit-backdrop-filter` d'abord : sans le préfixe, Safari iOS ne
       floute rien et la pastille disparaîtrait entièrement. */
    pointer-events: auto;
    /* 16 px : rapproché du texte (PO 25/08 « moins de marge entre le texte
       et le CTA ») — les 34 px dataient du débord du nom superposé. */
    margin-top: 16px;
    /* Vertical serré (PO 25/08 « moins de padding en haut et en bas »). */
    padding: 7px 22px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.45);
    background: transparent;
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
    color: #fff; text-decoration: none;
    font-size: 1.05rem; font-weight: 600; letter-spacing: 0.02em;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
}

/* Le film n'apparaît qu'une fois RÉELLEMENT scrubbable. Sans ce gage on
   verrait un cadre noir qui ne répond pas au doigt — et on croirait à un
   défaut de scrub alors que ce serait un défaut de chargement.
   Le libellé se cale sur la zone VISIBLE : la boîte, elle, s'arrête au bord. */
.ft-pin::after {
    content: attr(data-attente);
    position: absolute; z-index: 1;
    left: 0; right: 0; bottom: 24px;
    text-align: center;
    font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
    transition: opacity 0.3s ease;
}
.ft.est-prete .ft-pin::after { opacity: 0; }

/* ==========================================================================
   ?bords — LA SONDE D'IMMERSION, ET SON VERDICT EST BINAIRE.
   Intention PO du 22/08 : « les vidéos doivent prendre tout l'écran pour
   l'immersion, au-dessus et en dessous des éléments iOS ».
   Le problème, c'est qu'on ne peut pas VOIR un débord réussi : s'il marche,
   il est hors de l'écran, donc invisible. On ne peut donc jamais dire « oui
   ça déborde » en regardant — seulement « je ne vois pas de bande », ce qui
   n'est pas la même chose (un film qui s'arrête pile au bord ne montre pas
   de bande non plus, et il cassera au premier repliement de barre).
   D'où cette sonde, qui inverse la question : elle pose un liseré magenta sur
   les bords EXACTS de la vidéo.
       ▸ tu ne vois pas de magenta  → le film sort de l'écran : immersion OK
       ▸ tu vois du magenta          → le film s'arrête DANS l'écran : trop court
   Rien à mesurer, rien à interpréter. À ajouter à l'URL : ?bords
   ========================================================================== */
html[data-bords] .ft-video {
    outline: 4px solid #ff00d0;
    outline-offset: -4px;
}
/* Le fond du pin passe au vert : s'il apparaît quelque part, c'est que la
   vidéo ne le couvre pas — deuxième défaut possible, distinct du premier. */
html[data-bords] .ft-pin { background: #00c853; }

/* ==========================================================================
   LA PAGE À UN SEUL FILM — consigne PO du 22/08, minuit.
   « Vide le contenu de la page et mets juste une seule vidéo. »
   Un écran de vide avant, un écran de vide après : de quoi amener le film
   dans le champ et l'en sortir, sans qu'aucun texte, aucune image et aucune
   grille ne puisse être confondu avec lui. Si une bande apparaît ici, elle
   ne peut venir que du film, du décor ou de Safari.
   ========================================================================== */
/* ⚠️ AUCUNE RÉSERVE EN HAUT SUR UNE PAGE À FILM, et l'`!important` est
   assumé. Le palier « marque » pose `padding-top: --brand-h` sur `.page` pour
   que le rédactionnel ne passe pas sous le logo — juste partout ailleurs, faux
   ici : il repoussait la boîte de 110 px, donc le film n'était PAS plaqué à
   l'arrivée (mesuré : `pin [110, 906]` à `scrollY 0`). Or le PO le veut
   présent avant tout geste, débords compris.
   Le dégagement n'est pas supprimé, il CHANGE DE COUCHE : il passe sur
   `.ft-flux`, c'est-à-dire sur ce qui monte par-dessus le film. C'est ce que
   fait le site — « le haut ventile l'amorce » — et c'est la même règle qu'à
   23h35 : la boîte mesure, elle ne se laisse pas déplacer. */
.page--film { position: relative; padding-top: 0 !important; padding-bottom: 0 !important; }

/* SCULPTURES AU BUREAU (PO 28/09) : la suite de films reste strictement la
   version tactile. Au-dessus de 1023 px, elle cède la place à la composition
   issue de la page dev : récit à gauche, œuvre en situation à droite. */
@media (min-width: 1024px) {
    .page--sculptures-bureau > .ft,
    .page--sculptures-bureau > .ft-reserve,
    .page--sculptures-bureau > .vide { display: none !important; }
    .page--sculptures-bureau { min-height: 100svh; }
}
.vide { height: 100lvh; }

/* LA VERSION FLOTTE PAR-DESSUS LE FILM, ELLE NE LE POUSSE PAS.
   Le PO la veut visible (« laisse le numéro de version ») ET veut le film
   présent dès l'arrivée : dans le flux, la ligne repoussait le film d'une
   ligne et la boîte n'était plus plaquée à zéro. En `absolute` — pas en
   `fixed`, le harnais n'ajoute aucun élément fixe — elle vit dans la couche
   de défilement, se lit à l'arrivée et s'en va avec la page.
   L'`!important` sur l'opacité est délibéré et vaut d'être expliqué : la ligne
   porte un style EN LIGNE (pour rester lisible même si une feuille arrive
   périmée — c'est un repère de cache). À 0.55 sur un film clair, elle
   disparaîtrait ; ici seulement, la feuille reprend la main. */
.page--film .v-labo {
    position: absolute; z-index: 5;
    top: calc(104px + env(safe-area-inset-top)); left: 20px;
    opacity: .9 !important;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}

/* ==========================================================================
   ⚠️ LE CANEVAS NE DOIT RIEN DIRE — et c'est LE défaut du 23/08 à 00h08.
   Constat PO : bande en haut sans défiler, bandes en haut ET en bas dès qu'on
   défile. Couleur de la bande : TAUPE. Or le taupe n'est pas dans le film —
   c'est `html { background: var(--mobile-grad-top) }`, posé par le décor.
   Le verdict du « test zéro fond » (21/08, 00:24) l'avait déjà écrit noir sur
   blanc : **la zone d'une barre n'affiche du contenu que si du contenu est
   PEINT dessous ; sinon elle retombe sur le canevas, toujours.** Le débord de
   la vidéo était donc juste et inutile : Safari ne regardait pas la vidéo, il
   lisait la couleur du canevas. Même bug, même heure, un jour plus tard.
   Le correctif est celui du site, à la ligne près : sur une page à film, le
   canevas se TAIT. Le taupe passe alors sur un ÉLÉMENT (le vide du bas), qui
   peint dans la couche de défilement au lieu de commander les barres.
   Spécificité : `html:has(.ft)` égale celle de `html[data-c*="d"]`, et cette
   feuille est chargée APRÈS calques.css — à égalité, la dernière gagne.
   ========================================================================== */
html:has(.ft), body:has(.ft) { background: transparent; }
/* Le sol garde sa couleur, mais porté par du contenu réel. */
.vide { background: var(--mobile-grad-top, #58534d); }

/* La réserve basse n'existe que sur les corners ventilés — ailleurs le
   contenu suit les films et la page a déjà de la matière sous eux. */
.page--ventile { padding-bottom: var(--ft-reserve-basse, 90px); }

/* LE CTA D'UN FILM COUVERT NE CAPTE PLUS — Boucle du 24/08 (Antoine) : la
   boîte entrante est intégralement `pointer-events: none`, le hit-test la
   traverse et tombait sur le CTA du film SORTANT — invisible sous le film
   opaque, il balayait l'écran pendant toute la course suivante : un tap sur
   Camélia pouvait ouvrir la fiche de Curvy. La classe est posée par
   nu-suite.js (tourDePage) dès que le fondu du suivant commence. */
.ft-couverte .ft-cta { pointer-events: none; }

/* ── LA PAGE PORTÉE NE SCRUBBE PLUS (constat PO 25/08) ────────────────────
   Pendant un suivi VERTICAL, nu-gestes pose `nu-fige` sur la page qui part :
   translater la page sur Y déplace ses boîtes dans le viewport, et toutes
   les animations à timeline de défilement (pin, fenêtres, cartels)
   recalculaient leur progression sous le doigt — « on perd un bout de la
   vidéo ». Figées, elles gardent l'image exacte de l'instant du geste :
   la page part comme une photo d'elle-même. Retirée au geste rendu ;
   au geste conclu, le document entier est remplacé. */
.nu-fige, .nu-fige * { animation-play-state: paused !important; }
