/* =========================================================
   LUDO STUDIO — v8 : LE MOTEUR D'INDEX6, LA GÉOMÉTRIE D'INDEX3

   Le moteur d'index6 — chaque pose est une fonction de la position,
   jamais de la vitesse — appliqué au paquet en éventail d'index3.

   Ce n'est pas index3 auquel on ajoute des décorations : c'est
   index6 dont on remplace la géométrie. La différence compte,
   parce que la fluidité d'index6 ne vient pas de ses effets mais
   de sa simplicité : une seule transformée par carte et par image,
   aucune machine à états, aucun amortissement à synchroniser.


   En v3, le défilement déplaçait la rangée puis on DÉDUISAIT
   l'ouverture de la vitesse — « ça ralentit, donc on ouvre ».
   D'où les seuils, l'hystérésis, les minuteurs, les doubles
   ouvertures. Ici une carte est ouverte parce qu'elle EST au
   centre. Il n'y a rien à décider : la position du scroll est
   le curseur de l'animation.

   Conséquences gratuites : c'est réversible (on remonte, ça
   se rejoue à l'envers), interruptible à tout instant, et il
   n'y a aucun état à synchroniser.

   Le JS n'écrit qu'une transformée par carte et par image.
   Le CSS ne fait donc ici que la matière : bords, reflet,
   verre, lisibilité.
   ========================================================= */

.cf-section {
  position: relative;
  /* ---------- LA COURSE ----------

     La hauteur donne la course, et le commentaire d'origine parlait encore
     de 12 films : ils sont 24 depuis que la file boucle. Sur 400vh utiles,
     ça faisait 136 px de défilement par carte — soit SEPT CARTES PAR
     SECONDE à mille pixels par seconde, une vitesse ordinaire de trackpad.
     On survolait la rangée sans jamais pouvoir s'arrêter dessus.

     Le réglage est un arbitrage à deux termes, et les deux ont été mesurés
     sur la page entière :

       500vh   136 px/carte   7,4 c/s    8,5 écrans   on survole
       700vh   203 px/carte   4,9 c/s   10,5 écrans
       900vh   271 px/carte   3,7 c/s   12,5 écrans   trop long

     900 a été essayé et rejeté : la page devenait interminable, barre de
     défilement minuscule et pied de page hors d'atteinte. 700 garde
     l'essentiel du gain — cinquante pour cent de course en plus par
     carte, et on passe de sept cartes par seconde à cinq — pour deux
     écrans de moins.

     Depuis, v30.js ne fait plus parcourir qu'UN tour de file (6 cartes)
     au lieu de quatre. 250vh garde les 25vh par carte de 700vh
     (600vh ÷ 24 = 150vh ÷ 6) pour quatre écrans et demi de moins. */
  height: 250vh;
  background: #000;
}

/* ---------- La lumière ambiante ----------

   Une copie très agrandie et très floue de la vignette du film en cours,
   posée derrière toute la scène. Toute la pièce prend alors sa couleur : un
   coucher de soleil baigne le couloir en orange, un film de nuit l'éteint.

   C'est ce qui manquait le plus. Un fond noir uniforme, quelle que soit la
   qualité de la 3D par-dessus, reste un fond de studio : rien ne relie les
   pochettes à leur environnement. Ici l'environnement EST le film.

   Deux calques et non un : changer l'image de fond d'un seul élément la
   remplace d'un coup, sans transition possible. Ils se relaient en fondu
   croisé, celui qui s'efface gardant son image jusqu'au bout. */
.cf-ambiance {
  position: absolute;
  /* Débordement volontaire : le flou fait fuir les pixels des bords et
     laisserait un liseré sombre tout autour du cadre. */
  inset: -18%;
  z-index: 0;
  pointer-events: none;

  /* ---------- LE FOND REDEVIENT NOIR ----------

     Sur index13 la couronne assombrissait les bords et peignait tout le
     haut de la fenêtre en noir à 62 % : on voyait un dégradé collé. Je l'ai
     ouverte, et la lumière du film s'est mise à baigner le cadre entier —
     correction juste, effet de bord non voulu.

     La bonne réponse n'est pas de reposer un calque noir par-dessus : ça
     ramènerait le même bord franc. C'est de MASQUER la lumière elle-même.
     Là où elle s'éteint, ce qui reste est le noir de la page — aucun calque
     ajouté, donc aucune arête possible.

     Elle disparaît à 74 % du rayon d'un calque qui déborde déjà de 18 % :
     il n'en survit qu'un halo au centre, derrière le paquet. */
  -webkit-mask-image: radial-gradient(54% 50% at 50% 46%,
    #000 0%, rgba(0, 0, 0, 0.5) 46%, transparent 74%);
  mask-image: radial-gradient(54% 50% at 50% 46%,
    #000 0%, rgba(0, 0, 0, 0.5) 46%, transparent 74%);
}
.cf-ambiance span {
  position: absolute;
  inset: 0;
  background-position: 50% 45%;
  background-size: cover;
  /* La saturation compense ce que le flou fait perdre : en moyennant les
     pixels, il tire toujours vers le gris. */
  filter: blur(72px) saturate(1.5) brightness(0.42);
  opacity: 0;
  transition: opacity var(--ample) var(--courbe);
  will-change: opacity;
}
.cf-ambiance span.is-on { opacity: 0.34; }

/* Sombre au centre ET aux bords, clair entre les deux.

   Ma première version ne noircissait que le centre — pour que la pochette
   se détache de sa propre lumière — et laissait tout le pourtour clair. Le
   résultat était un HALO : un anneau lumineux qui cerne le cadre, ce qui
   attire l'œil vers les bords au lieu du sujet, exactement le contraire du
   but recherché.

   La lumière est maintenant contenue dans une couronne : elle enveloppe la
   scène sans déborder aux angles. */
/* La couronne assombrissait le CENTRE et les BORDS. Le centre a une
   raison : sans lui, la lumière d'ambiance — une affiche floutée à 72 px —
   lave la carte qu'on regarde. Le bord n'en avait aucune.

   Et il coûtait cher : le calque couvre y −146..959, donc tout le haut de
   la fenêtre était peint en noir à 62 % par-dessus le fond. C'était le
   dégradé noir qu'on voyait, et il ne servait qu'à assombrir du vide.

   Il s'éteint donc au lieu de foncer. La lumière du film peut atteindre les
   bords du cadre, ce qui est précisément son rôle. */
.cf-ambiance::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(62% 66% at 50% 48%,
    rgba(0, 0, 0, 0.88) 0%,
    rgba(0, 0, 0, 0.30) 62%,
    rgba(0, 0, 0, 0) 100%);
}

/* ---------- Le sol ----------
   Une simple bande dégradée, mais elle donne une ligne d'horizon
   et un plan de pose. Sans elle, les volumes flottaient dans un
   noir sans repère et l'épaisseur ne servait à rien : il n'y avait
   rien pour la confronter. */
.cf-sticky::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  pointer-events: none;
  /* ---------- PLUS DE BORD DU TOUT ----------

     Le calque ne faisait que 64 % de la hauteur, ancré en bas : il
     commençait donc à y = 293 sur une fenêtre de 813. Et son dégradé
     radial est centré à 8 % de SA hauteur, soit 42 px sous ce bord, à sa
     valeur maximale — une ligne claire posée juste sous une zone qui
     n'avait rien du tout.

     Le commentaire d'origine disait que le dégradé partait de transparent
     pour éviter précisément cette ligne. C'était vrai du dégradé linéaire
     et faux du radial, que j'avais ajouté après sans y repenser. D'où la
     barre sombre en haut de l'écran : ce n'était pas un noir ajouté, mais
     un éclairement qui s'arrêtait net.

     Le calque prend maintenant toute la hauteur, donc il n'a plus de bord
     visible. Les positions sont reportées pour que le sol ne bouge pas :
     8 % de 520 px devient 41 % de 813, 26 % devient 53 %, 78 % devient
     86 %. Même image, sans la coupure. */
  background:
    radial-gradient(150% 77% at 40% 41%, rgba(255, 255, 255, 0.055), transparent 58%),
    linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.03) 53%, transparent 86%);
}

.cf-sticky {
  position: sticky;
  top: 0;
  /* `svh` ET NON `vh`, POUR IOS.

     `100vh` vaut la hauteur de l'écran barre d'URL MASQUÉE — la grande
     valeur. Tant que la barre est là, le cadre collant dépasse le visible
     d'une soixantaine de pixels : la rangée n'est pas centrée sur ce qu'on
     voit, et le bas des cartes passe sous la barre. Pire, la barre
     apparaît et disparaît au défilement, donc le cadre change de hauteur
     pendant qu'on lit — la file entière se recale.

     `svh` est la PETITE hauteur, barre affichée. Elle ne bouge jamais.
     Choisie contre `dvh`, qui suit la barre en direct et provoquerait un
     recalcul de toute la scène 3D à chaque pixel de défilement. Ici, une
     bande noire de 60 px quand la barre se rétracte coûte moins qu'un
     recadrage permanent — sur un fond noir, elle ne se voit pas.

     La déclaration `vh` reste au-dessus pour les navigateurs qui ignorent
     `svh` : ils prennent la première, les autres la seconde. */
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
}


/* ---------- La scène ----------
   Les neuf valeurs qui font la géométrie d'index3. La focale est
   très longue : à 12 000 px la projection est pratiquement
   orthographique, les cartes lointaines ne rétrécissent plus. C'est
   l'inverse du couloir d'index5, qui vivait de la fuite.

   La perspective reste sur .cf-stage et les rotations passent sur
   .cf-scene : sur un même élément, un `transform` s'applique APRÈS
   la perspective, donc il ferait tourner l'image projetée au lieu
   de tourner la scène dans la profondeur. */
:root {
  /* ---------- LA FUITE ----------

     12 000 px, c'était la focale d'index3 : une projection pratiquement
     orthographique, où les cartes lointaines ne rétrécissent plus. Mesuré
     sur index12, le rapport entre la plus grande et la plus petite carte
     visible valait 1,17 — autant dire aucune profondeur.

     2 000 px, avec un vrai pas en profondeur, donne 1,56 devant et 0,39 au
     fond : un rapport de 4 sur la plage dessinée, l'ordre de grandeur de la
     référence. Descendre plus bas explose le premier plan — à 1 400 px la
     carte de devant ferait 2,8 fois l'élue et mangerait l'écran. */
  --persp: 2000px;
  --ry: -22deg;    /* fuite des panneaux autour de la verticale */
  --rx: -20deg;    /* bascule caméra, négatif : vue de dessous   */
  --tilt: -3deg;   /* inclinaison de chaque panneau dans son plan */
  /* 125 px et non 80 : la file de la référence monte franchement vers le
     fond. Balayé contre trois pentes et trois pas de profondeur, c'est le
     couple qui approche le mieux son profil — la rangée traverse le cadre
     en largeur (681 px mesurés) et en monte la moitié (438 px). */
  /* 75 px : valeur choisie au banc d'essai, pas calculée. La pente rendue
     tombe à 0,35, sous les 0,42 de la référence Fiber — la file est plus
     couchée que le modèle, et c'est un choix de regard, pas une dérive.

     Le chiffre à retenir sur ce jeton : il n'agit que sur la composante
     verticale du pas. Sur les quatre crans essayés — 125, 105, 95, 85 —
     l'étalement horizontal est resté entre 1403 et 1407 px. La file
     s'aplatit sans jamais se raccourcir. */
  --rise: 75px;    /* montée d'une carte à la suivante = la pente */

  /* Le pas en PROFONDEUR, celui qui manquait. La rangée d'index3 n'avançait
     que de 3 px par carte — un simple départage pour l'ordre de tri, pas
     une profondeur. 240 px en font une vraie file qui s'enfonce. */
  --prof: 180px;
  /* 330 px venait du format 3/4, où la hauteur était le facteur limitant :
     une carte de 330 en faisait 440, puis 509 une fois grossie, pour un
     cadre de 620. Il n'y avait plus de marge.

     En carré, la même largeur ne fait plus que 382 de haut — la hauteur a
     cessé de contraindre et c'est --cw qui bridait tout. 410 px redonne à
     la carte la place que le format carré a libérée, et la borne en
     hauteur reste au-dessus sur toutes les fenêtres testées. */
  --cw: 410px;     /* largeur d'une carte                        */

  /* ---------- LE FORMAT DE LA CARTE ----------

     Un seul jeton, largeur ÷ hauteur. 0,75 était le 3/4 d'index3, hérité du
     jeu de cartes ; 1 le rend carré, comme les pochettes d'index4.

     Le carré n'est pas qu'une affaire de goût : avec `object-fit: cover`,
     une vidéo 16/9 montre 42 % de son image dans un cadre 3/4, et 56 % dans
     un carré. Un tiers de rognage en moins avant le palier « format réel ».

     Quatre valeurs en dépendent et le suivent toutes : aspect-ratio, la
     remontée de la carte sur sa fente, la borne en hauteur, et la hauteur du
     palier 16/9. Les deux dernières retombent exactement sur les anciennes
     constantes à 0,75 — 56 × 0,75 = 42, 94,5 × 0,75 = 70,9 — donc la
     généralisation ne déplace rien tant qu'on ne touche pas au jeton. */
  --cr: 1;         /* format : largeur / hauteur. 0.75 = 3/4, 1 = carré */
  /* 0,62 et non 0,38 : le pas latéral vaut cw × (1 − ov), soit 156 px au
     lieu de 254. Une file qui avance surtout en PROFONDEUR se recouvre
     beaucoup plus qu'un éventail — c'est ce qui donne à la référence son
     empilement serré. Avec l'ancien pas, seules six cartes entraient dans
     le cadre au lieu de treize. */
  --ov: 0.62;      /* part recouverte par la suivante            */
  /* 0 px, et ce n'est pas un abandon du réglage : c'est le résultat d'un
     calcul. La valeur de 175 venait d'index3, dont la mise en page n'avait
     pas de légende sous le paquet — il disposait de toute la hauteur.

     Ici l'intro prend 155 px en haut et la légende 190 en bas : il reste
     463 px pour la CARTE. Le reflet, lui, n'entre pas dans le budget : il
     descend sous le dégradé de la légende, qui est précisément fait pour
     ça — un reflet qui s'éteint n'a pas besoin d'espace réservé.

     Une carte de 330 px de large fait 440 px de haut, ce qui tient. En la
     centrant dans la bande, et compte tenu des 64 px dont l'élue se
     soulève, le décalage juste tombe à zéro. */
  /* Valeur de départ seulement : le JS la recalcule au chargement et à
     chaque redimensionnement, en mesurant l'écart réel entre le centre de
     l'élue et celui du cadre. Une constante ne peut pas convenir, la levée
     de l'élue étant fixe en pixels quand le cadre, lui, dépend de la
     fenêtre. */
  --up: -40px;     /* hauteur du paquet dans son cadre           */
  --alpha: 0.96;

  /* ---------- LE VOCABULAIRE DE MOUVEMENT ----------

     Audit du fichier avant unification : une courbe employée 15 fois, deux
     intruses employées une fois chacune, un `ease` oublié — et DIX durées
     distinctes, de 0,06 à 1,1 s, posées au fil des versions sans échelle.

     Une maison tient une famille et s'y tient : c'est ce qui fait qu'un
     produit paraît d'un seul tenant plutôt qu'assemblé. Une courbe, quatre
     échelons en progression géométrique de raison 1,73.

     Chaque durée a rejoint l'échelon le plus proche. Le plus gros
     déplacement est 0,90 → 0,72 s, tout le reste bouge de moins de 13 %.

     --vif   : ce qui répond au geste, on ne doit pas l'attendre
     --moyen : ce qui accompagne, on doit le voir
     --ample : ce qui transforme, on doit le suivre
     --long  : ce qui n'obéit à aucun geste, on doit le laisser venir */
  --courbe: cubic-bezier(0.22, 1, 0.3, 1);
  --vif: 0.24s;
  --moyen: 0.42s;
  --ample: 0.72s;
  --long: 1.24s;
}

.cf-stage {
  position: relative;
  z-index: 1;
  /* ---------- LE CADRE PREND TOUTE LA HAUTEUR ----------

     Il faisait min(74vh, 620px) et était centré : sur une fenêtre de 813 px
     il allait de 105 à 707, laissant 105 px vides en haut et autant en bas.
     C'était la « marge » qu'on voyait — pas du noir ajouté, mais le rail
     coupé. Mesuré : la carte à d = 4 va de y = 16 à 292 alors que le cadre
     commençait à 106 ; elle était peinte puis effacée par le voile.

     Une hauteur bornée avait un sens quand le paquet était un éventail
     compact posé au milieu, avec une légende dessous. La file en fuite,
     elle, doit pouvoir sortir par le haut du cadre comme dans la
     référence. */
  height: 100%;
  perspective: var(--persp);
  /* 50 % et non 46 : le 46 venait du réglage d'index3, dont le paquet était
     volontairement décentré. Ici la carte élue doit tomber pile au milieu,
     puisque sa légende y est. Un point de fuite décalé déportait sa
     projection de quelques dizaines de pixels. */
  perspective-origin: 50% 44%;
  transform-style: preserve-3d;

  /* Le masque de bords, reprisd'index3 et indispensable.

     Sous une projection presque orthographique, les cartes lointaines ne
     rétrécissent pas : la rangée sort du cadre à pleine taille, et on voit
     des cartes coupées net par le bord de l'écran. Le masque les fait se
     perdre dans le noir, ce qui donne l'illusion que le paquet continue
     au-delà du cadre au lieu d'y buter. */
  /* Le voile vertical passe de 10 %/88 % à 6 %/96 %.

     Mesuré : le cadre fait 602 px, la carte élue 508. Avec des bandes de
     10 % en haut et 12 % en bas, la zone pleinement opaque ne faisait que
     470 px — plus courte que la carte, qui s'y fondait donc forcément par
     un bord. À 6 %/4 % elle fait 542 px et la carte y tient avec de la
     marge.

     En PIXELS et non en pourcentage : une bande proportionnelle grandit
     avec le cadre, donc elle mord d'autant plus qu'on a de la place —
     l'inverse de ce qu'on veut. 24 px suffisent à noyer les cartes
     lointaines, qui montent de 80 px par rang. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 13%, #000 82%, transparent),
                      linear-gradient(180deg, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent, #000 13%, #000 82%, transparent),
              linear-gradient(180deg, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.cf-scene {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: translateY(calc(var(--up) * -1)) rotateX(var(--rx)) rotateY(var(--ry));
}

/* Une carte. Portrait 3/4 comme dans index3, et non carrée comme les
   pochettes du Cover Flow : c'est ce format qui donne au paquet son
   allure de jeu de cartes plutôt que de bac à disques. */
.cf-item {
  position: absolute;
  left: 50%;
  top: 50%;
  /* ---------- UNE TROISIÈME BORNE, EN HAUTEUR ----------

     La largeur n'était bornée que par la fenêtre et par --cw. Sur un écran
     court, ça ne suffit pas : à 726 px de haut, le cadre fait 537 px et la
     carte 509. Elle occupe presque tout, et aucun réglage de position ne
     peut la faire tenir dans la zone que le voile laisse opaque — c'était
     ça, la carte coupée en haut, et --up seul ne pouvait pas y remédier.

     Il faut donc que la carte connaisse la hauteur disponible :

       hauteur rendue = largeur × 4/3 × 1,157

     — le 4/3 du format, le 1,157 du grossissement de l'élue et de la
     projection. Pour tenir dans (cadre − 48 px de voile), la largeur doit
     rester sous 0,648 fois cette hauteur, et le cadre vaut min(74vh, 620px).
     La borne suit la hauteur du cadre : tant qu'il faisait 74vh, c'était
     56vh × le format ; maintenant qu'il prend toute la fenêtre, 76vh. Elle
     ne mord que sur les fenêtres très courtes ; au-delà, --cw et 28vw
     restent les plus serrées et rien ne bouge. */
  width: min(28vw, var(--cw), calc(76vh * var(--cr)));
  aspect-ratio: var(--cr);
  margin-left: calc(min(28vw, var(--cw), calc(76vh * var(--cr))) / -2);
  /* La moitié de la HAUTEUR, qui vaut la largeur divisée par le format. */
  margin-top: calc(min(28vw, var(--cw), calc(76vh * var(--cr))) / var(--cr) * -0.5);
  transform-style: preserve-3d;
  backface-visibility: hidden;
}

/* Pas de will-change ici, et c'est mesuré plutôt que supposé : chaque carte
   porte une transformée 3D dans un parent en preserve-3d, donc le navigateur
   lui donne déjà sa propre surface de rendu — il ne peut pas trier en
   profondeur des éléments qui partagent une couche plate. L'indice n'aurait
   rien promu de plus ; il aurait seulement maintenu la couche en vie. */

/* Pas de tranche d'épaisseur ici. Elle servait au couloir d'index6, où
   les cadres étaient vus très obliquement ; sous une projection presque
   orthographique elle ne se verrait jamais, et le paquet d'index3 tire son
   relief du feuilletage, pas du volume de chaque carte. */

/* La carte est centrée sur sa fente plutôt que collée à ses bords : c'est
   ce qui permet de changer sa taille sans déplacer son centre. */
.cf-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  transform: translate(-50%, -50%);
  border-radius: 4px;
  overflow: hidden;
  /* La mise en page de la carte ne peut rien changer au-dehors : elle est
     absolument positionnée et ses enfants aussi. Le dire au navigateur lui
     évite de vérifier. */
  contain: layout;
  background: #0a0a0a;
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 1);
  /* Une transition sur width/height provoque un recalcul de mise en page.
     Acceptable ici et seulement ici : un seul élément, et seulement au
     moment où une vidéo démarre — pas à chaque image. */
  transition: width var(--ample) var(--courbe), 
              height var(--ample) var(--courbe), 
              box-shadow var(--ample) var(--courbe);
}

/* ---------- LE VRAI FORMAT DE LA VIDÉO ----------

   La carte est en 3/4 et les films en 16/9. Avec object-fit: cover, la
   vidéo est mise à la hauteur de la carte puis rognée : il faudrait 782 px
   de large pour la voir entière, on n'en montre que 330 — 58 % de l'image
   est coupée.

   Quand la vidéo démarre vraiment, la carte s'élargit à 168 % et
   s'aplatit à 71 %, ce qui donne 554 × 312, soit du 16/9 exact. Plus aucun
   rognage : on voit le cadre que le réalisateur a composé.

   La classe est posée sur l'événement `playing`, pas sur notre appel à
   play() : tant qu'il n'y a pas d'images, il n'y a rien à révéler. */
.cf-card.is-plein {
  width: 168%;
  /* 94,5 % × le format : à 0,75 ça redonne les 71 % d'avant, et la boîte
     tombe sur le 16/9 quel que soit le format de la carte. */
  height: calc(94.5% * var(--cr));
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 1);
}

/* ---------- TROISIÈME PALIER : LE PLEIN ÉCRAN ----------

   Si le défilement s'arrête plus de deux secondes sur une carte, on cesse
   de considérer qu'elle est regardée en passant : elle est regardée. Elle
   quitte alors le paquet et prend tout l'écran.

   min(100vw, 100vh × 16/9) : bornée par la largeur sur un écran large,
   par la hauteur sur un écran haut. Aucune barre noire d'un côté ni
   débordement de l'autre — c'est le plus grand 16/9 qui tient.

   Cette taille n'est juste QUE parce que le JS défait le paquet en même
   temps : sans annuler le grossissement de l'élue (×1,12) et sa projection
   à z = 260 (×1,022), ce `100vw` s'afficherait en 114,5 vw. Les deux
   moitiés vont ensemble.

   Ni coin arrondi ni ombre portée : un film qui remplit l'écran n'est plus
   un objet posé sur un fond, c'est le fond. */
.cf-card.is-immersif {
  /* --gonfle est publié par le JS : il défait exactement la projection de
     l'avancée Z_PLEIN, si bien que ce min() se rend au pixel. */
  /* ---------- COUVRIR, ET NON TENIR ----------

     min() faisait TENIR le 16/9 dans la fenêtre : sur tout écran qui n'est
     pas exactement en 16/9, il restait du noir sur les côtés — mesuré 75 px
     de chaque côté à 1440 × 726, 77 px à 1920 × 993.

     max() le fait COUVRIR. Plus aucune bande, au prix d'un rognage latéral
     que .cf-sticky recadre — et qui vaut exactement zéro sur un écran 16/9,
     où les deux formules coïncident. C'est le bon arbitrage ici : le palier
     précédent, lui, montre le format réel sans rien couper. Voir le film
     entier et remplir l'écran sont deux moments distincts. */
  --imm: calc(max(100vw, 100vh * 16 / 9) * var(--gonfle, 1));
  width: var(--imm);
  height: calc(var(--imm) * 9 / 16);

  /* Le décalage du paquet ne doit pas suivre.

     --up descend la file de 40 px pour la centrer optiquement dans son
     cadre, et la scène l'applique à tout le monde. En plein écran, la carte
     se retrouvait 78 px trop bas : c'était la bande noire du haut. On
     l'annule ici, et seulement ici.

     SANS facteur de projection, et c'est le piège où je suis tombé d'abord.
     J'avais divisé par --gonfle en raisonnant que le translateY de la carte
     vit à 620 px de profondeur vue. C'est vrai — mais le translateY de la
     SCÈNE aussi : il est appliqué avant les rotations, donc projeté comme
     tout le reste. Les deux subissent le même grossissement, qui s'annule
     dans le rapport. Compenser au facteur près laissait 72 à 123 px de
     bande selon la fenêtre. */
  transform: translate(-50%, calc(-50% + var(--up) + var(--dy-plein, 0px)));
  border-radius: 0;
  box-shadow: none;
  transition: width var(--long) var(--courbe), 
              height var(--long) var(--courbe), 
              border-radius var(--long) var(--courbe), 
              box-shadow var(--long) var(--courbe);
}

/* Le masque de bords doit tomber.

   Il fait se perdre la rangée dans le noir à 13 % et 82 % de la largeur —
   indispensable au paquet, fatal au plein écran : il éteindrait les deux
   bords de l'image. C'est `.cf-sticky` qui recadre alors, à 100vh et
   overflow hidden, donc exactement au bord de la fenêtre. */
.cf-sticky.is-immersif .cf-stage {
  -webkit-mask-image: none;
  mask-image: none;
}

/* L'interface s'en va pour de bon. À ce stade il n'y a plus une carte
   légendée mais un film ; le reste n'a plus rien à dire. */
.cf-sticky.is-immersif .cf-meter,
.cf-sticky.is-immersif .cf-reflect,
.cf-sticky.is-immersif .cf-item::after,
.cf-sticky.is-immersif .cf-ambiance { opacity: 0; }
.cf-meter { transition: opacity var(--long) var(--courbe); }
.cf-ambiance, .cf-reflect { transition: opacity var(--long) var(--courbe); }

/* Le reste du paquet s'efface derrière.

   Un MULTIPLICATEUR plutôt qu'une valeur fixe : --voile vaut déjà zéro sur
   l'élue, donc la multiplier la laisse claire, tandis que les autres
   passent de 0,62 à 0,87. Aucune exception à écrire, aucune classe à
   exclure — la même règle pour tout le monde. */
.cf-sticky.is-immersif { --vm: 1.4; }

/* Hors du flux. `position: static` mettait le média dans le flux de la
   carte, donc chaque image de la transition de format en recalculait la
   mise en page complète, alors que ses trois frères sont déjà absolus. Le
   rendu est identique — l'élément occupait déjà toute la carte — mais le
   navigateur n'a plus de flux à recomposer. */
.cf-face {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* La vidéo se superpose à la vignette et n'apparaît qu'une fois qu'elle
   joue vraiment. `is-vue` est posée par le JS sur l'événement `playing`,
   pas au moment où on demande la lecture : entre les deux, il n'y a encore
   aucune image à montrer. */
.cf-card > video.cf-face {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--moyen) var(--courbe);
}
.cf-card > video.cf-face.is-vue { opacity: 1; }

/* ---------- Le voile ----------

   Pour assombrir une carte qui n'est pas l'élue, on superpose un CALQUE
   NOIR et on anime son opacité. On ne baisse jamais l'opacité de la carte
   elle-même : 44 % d'opacité, ce n'est pas sombre, c'est TRANSPARENT — on
   voit à travers les cartes celles qui sont derrière et le fond, ce qui les
   transforme en calques de verre au lieu de cartes dans l'ombre.

   Le voile est le dernier calque, au-dessus du reflet spéculaire : une
   carte dans l'ombre a aussi ses reflets éteints. */
.cf-veil {
  position: absolute;
  inset: 0;
  /* Au-dessus du rebord (z-index 1) : il doit s'éteindre avec l'image. */
  z-index: 2;
  background: #000;
  opacity: calc(var(--voile, 0) * var(--vm, 1));
  pointer-events: none;
  transition: opacity var(--ample) var(--courbe);
}

/* Le verre : un reflet oblique fixe, qui donne la matière.
   Il n'est pas animé — c'est la carte qui tourne dessous, et
   c'est ce mouvement relatif qui fait lire une surface. */
.cf-gloss {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(115deg,
    rgba(255, 255, 255, 0.16) 0%,
    rgba(255, 255, 255, 0.04) 26%,
    rgba(255, 255, 255, 0) 48%,
    rgba(0, 0, 0, 0.28) 100%);
}

/* ---------- LA MISE AU POINT ----------

   Mesuré : dans cet éventail la carte du fond fait 329 px et celle du
   centre 383 — un rapport de 1,16, quand le couloir d'index6 est à 23.
   La projection est presque orthographique, donc la profondeur ne peut
   pas venir de la taille. Elle vient de la netteté.

   Le flou porte sur le MÉDIA et non sur la carte : filtrer la carte
   entière emporterait son bord, son ombre portée et son reflet spéculaire,
   et une image floue dans un cadre net se lit comme de la profondeur de
   champ, alors qu'un cadre flou se lit comme un défaut d'affichage.

   --flou est écrit par le JS, et seulement quand il change. */
/* Le filtre n'existe que si la carte est réellement floue : blur(0px) reste
   un filtre, donc une passe de composition, pour un effet nul. */
.cf-item.is-flou .cf-face {
  filter: blur(var(--flou, 0px));
}

/* ---------- LE TITRE DANS LA CARTE ----------

   La légende vivait sous le paquet, au milieu de la fenêtre, et le titre y
   était réécrit par le JS à chaque relais — avec un jeton de fraîcheur pour
   qu'une pochette déjà dépassée n'écrase pas le titre de la suivante. Toute
   cette mécanique disparaît : chaque carte porte son propre titre, écrit
   dans le HTML. Rien à synchroniser, rien à faire clignoter, et le texte
   existe pour un lecteur d'écran comme pour un moteur de recherche même si
   le JS ne tourne pas.

   Il n'apparaît que sur l'élue, par --sel, et il ACCOMPAGNE la carte au
   lieu de la commenter : il monte de 14 px en même temps qu'il se révèle,
   donc l'œil le lit comme une partie de l'objet et non comme une étiquette
   posée dessous.

   Il s'efface dès que le film montre son vrai format. La séquence dit
   quelque chose : tant que c'est une affiche, on annonce ce que c'est ;
   quand le film joue pour de bon, il parle tout seul. */
/* ---------- DEUX RÉGIMES, DEUX ÉLÉMENTS ----------

   Le titre obéit à deux choses qui ne vont pas à la même vitesse.

   Il suit --sel, réécrit à chaque image par le JS, et ce suivi doit être
   INSTANTANÉ : une transition dessus ferait traîner le titre derrière le
   défilement. C'est le piège déjà rencontré sur la mise à plat.

   Et il doit s'effacer en FONDU quand le film montre son vrai format.

   J'ai d'abord voulu tout tenir sur un seul élément, en animant une
   variable déclarée par @property et en la faisant entrer dans le calcul
   de l'opacité. C'était élégant et je n'ai pas pu le vérifier : les
   transitions de propriétés personnalisées restent figées dans le
   navigateur sans tête dont je dispose, exactement comme rAF. Je ne livre
   pas ce que je ne peux pas mesurer.

   Deux éléments, donc. L'enveloppe porte --sel sans transition ; le
   contenu porte le fondu, avec des transitions ordinaires. Chacun son
   régime, et rien à vérifier sur parole. */
.cf-nom {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  pointer-events: none;
  opacity: var(--sel, 0);
  transform: translateY(calc((1 - var(--sel, 0)) * 14px));
}
.cf-nom-in {
  /* Un dégradé et non un bandeau : il doit se poser sur l'image sans qu'on
     voie où il commence. */
  padding: 44px 22px 18px;
  background: linear-gradient(to top,
    rgba(0, 0, 0, 0.88) 0%,
    rgba(0, 0, 0, 0.6) 38%,
    rgba(0, 0, 0, 0.16) 68%,
    transparent 100%);
  transition: opacity var(--moyen) var(--courbe), 
              transform var(--moyen) var(--courbe);
}
.cf-nom strong {
  display: block;
  font-size: clamp(18px, 1.55vw, 24px);
  font-weight: 400;
  letter-spacing: -0.025em;
  line-height: 1.1;
}
.cf-nom span {
  display: block;
  margin-top: 6px;
  font-size: 11px;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}
/* La carte élue est déjà cliquable — c'est le JS qui ouvre le lecteur au
   clic sur elle. Ceci n'est donc pas un bouton mais l'indication qu'il y a
   quelque chose à cliquer, et ça ne prend pas le pointeur. */
.cf-nom em {
  display: block;
  margin-top: 12px;
  font-style: normal;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.72);
}
.cf-nom em i {
  font-style: normal;
  display: inline-block;
  margin-left: 5px;
  transition: transform var(--vif) var(--courbe);
}
.cf-item.is-elue .cf-card:hover .cf-nom em i { transform: translateX(4px); }

/* LE TITRE RESTE, SUR LES DEUX PALIERS.

   Il s'effaçait dès le format réel, au motif qu'un film qui joue parle
   seul. C'était faux dans les deux sens : au moment précis où on s'arrête
   sur un film, c'est là qu'on veut savoir lequel c'est — et une fois en
   plein écran, plus rien à l'écran ne porte son nom.

   Il ne survit pas tel quel, il change d'échelle. Une légende dessinée
   pour une pochette de 250 px devient une inscription minuscule sur une
   image de 1400 : ce qui doit rester constant est le rapport au cadre,
   pas la taille en pixels.

   L'incitation au clic, elle, disparaît au format réel : la carte est
   ouverte, il n'y a plus rien à ouvrir. */
.cf-card.is-plein .cf-nom-in {
  /* La marge NE SUIT PAS la carte.

     J'avais fait grossir les marges avec le corps du texte : le titre
     passait de 21 px du bord gauche sur une pochette a 58 px en plein
     ecran. C'est le raisonnement inverse du bon. Un cartel de film est
     pose DANS le coin ; ce qui grandit est la lettre, pas le vide autour.
     Plus l'image est grande, plus une marge constante parait petite — et
     c'est exactement ce qu'on veut, le texte se cale dans l'angle au lieu
     de flotter au milieu du bas.

     Seul le haut du degrade suit l'echelle, parce que lui doit couvrir la
     hauteur du texte qu'il protege. */
  padding: 64px 30px 22px;
}
.cf-card.is-plein .cf-nom strong { font-size: clamp(22px, 1.9vw, 30px); }
.cf-card.is-plein .cf-nom span   { margin-top: 8px; font-size: 12px; }
/* Et il LIBÈRE sa place, ce que `opacity: 0` ne fait pas.

   C'était ça, l'espace sous le sous-titre. L'invitation restait dans le
   flux, invisible, et poussait le texte vers le haut de 64 px au format
   réel et de 89 en plein écran — d'autant plus que sa hauteur grandit avec
   le corps du texte. Aucun réglage de padding ne pouvait le rattraper :
   je resserrais une marge de 30 px pendant qu'un bloc vide en occupait
   trois fois plus juste au-dessus.

   `display: none` et non une hauteur nulle : il n'y a rien à animer ici,
   la carte vient de changer d'état d'un bloc. */
.cf-card.is-plein .cf-nom em {
  display: none;
}

/* En plein écran, la légende se pose comme un cartel de générique : dans
   la marge basse, à l'échelle de l'écran. Le dégradé s'étend, sinon il
   coupe net au milieu d'une image qui occupe tout.

   --dy-plein compense le décalage vertical du plein écran, déjà calculé
   par le JS pour la carte elle-même : sans lui la légende resterait où la
   carte était, pas où elle est. */
.cf-card.is-immersif .cf-nom-in {
  /* 26 et 24 px, quasiment les memes qu'au format reel : le texte reste au
     coin. Seul le haut du degrade grandit, pour rester proportionne a une
     image qui occupe tout l'ecran. */
  padding: clamp(90px, 14vh, 180px) 26px 20px;
  background: linear-gradient(to top,
    rgba(0, 0, 0, 0.82) 0%,
    rgba(0, 0, 0, 0.52) 42%,
    rgba(0, 0, 0, 0.14) 72%,
    transparent 100%);
}
.cf-card.is-immersif .cf-nom strong { font-size: clamp(24px, 2.4vw, 40px); }
.cf-card.is-immersif .cf-nom span   { margin-top: 10px; font-size: 12px; }

/* ---------- LE BALAYAGE SPÉCULAIRE ----------

   Repris d'index3, où il avait été mis au point puis perdu au portage.
   Une bande claire et oblique qui traverse la carte quand elle devient
   l'élue. C'est ce qui la fait lire comme un panneau verni pris dans une
   lumière plutôt que comme une image qu'on oriente : une surface réelle
   change d'aspect quand elle bouge, et c'est ce changement, plus que le
   mouvement lui-même, que l'œil interprète comme de la matière.

   Les proportions sont celles qu'index3 avait fini par trouver, et elles
   comptent. Une bande inclinée occupe en horizontal
   `largeur × cos(angle) + hauteur × sin(angle)` : à 46 % de large, 210 %
   de haut et 16°, elle couvrait 102 % de la carte — elle la lavait au lieu
   de la traverser. À 24 %, 130 % et 10°, l'emprise retombe à 46 %, et
   c'est bien un reflet qui passe.

   Sur ::after plutôt que sur un élément à part : .cf-card ne s'en servait
   pas, et ça évite d'ajouter une balise aux douze cartes. Le débordement
   vertical est recadré par l'overflow de la carte, donc la bande entre par
   un bord et sort par l'autre sans jamais montrer ses extrémités. */

/* Le liseré intérieur.

   La carte n'avait aucune arête : l'affiche allait jusqu'au bord, et sur un
   fond noir une image sombre s'y fondait sans qu'on sache où la carte
   finissait. Un trait fin EN DEDANS lui rend un bord — il mord sur l'image
   d'un pixel au lieu de s'ajouter autour, donc la géométrie ne bouge pas et
   le reflet reste au contact.

   Par une ombre interne et non un `border` : un border déplacerait la boîte
   de l'image, et il faudrait le défalquer des 100 % de largeur et de
   hauteur. L'ombre se peint par-dessus sans rien décaler.

   z-index 4 : au-dessus de l'affiche, du lustre, du voile et de la légende
   — c'est le bord de la carte, rien ne passe par-dessus. `border-radius:
   inherit` lui fait suivre les 4 px d'arrondi.

   Éteint en plein écran : un bord sur un film qui occupe tout l'écran
   n'entoure plus une carte, il encadre la fenêtre.

   CLAIR ET NON SOMBRE, et c'est le point important.

   J'ai d'abord posé un liseré noir plein, puis une vignette noire diffuse.
   Les deux étaient faux pour la même raison : sur un fond NOIR, assombrir
   les bords rapproche la carte de son fond au lieu de l'en détacher. La
   vignette dissolvait l'arête qu'elle prétendait poser, et se lisait comme
   un filtre appliqué à l'affiche — pas comme un objet qui a un rebord.

   Un panneau posé dans une scène éclairée prend la lumière sur son arête.
   D'où un filet clair sur tout le pourtour, et une arête haute plus vive :
   la lumière vient d'en haut, c'est le bord supérieur qui l'accroche. Ça
   ne touche pas à l'image, ça ajoute un rebord.

   SOUS LE VOILE, et c'est ce qui rend le rebord juste. Posé au-dessus, il
   gardait sa pleine force sur une carte voilée : mesuré, 28,2 de rebord sur
   une image tombée à 11,6 — les cartes du fond auraient porté des contours
   clairs et nets pendant que leur image s'éteignait, soit l'inverse exact
   de la profondeur. Un rebord est une lumière qui tombe sur une arête ; il
   s'éteint avec le reste de l'objet.

   D'où z-index 1 pour le rebord et 2 pour .cf-veil, qui n'en avait pas et
   se contentait de l'ordre du DOM. Le balayage spéculaire reste à 2 : il ne
   court que sur la carte élue, qui n'a pas de voile. */
.cf-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, var(--rebord, 0.07)),
    inset 0 1px 0 rgba(255, 255, 255, var(--arete, 0.16));
  transition: opacity var(--moyen) var(--courbe);
}
.cf-card.is-immersif::before { opacity: 0; }

.cf-card::after {
  content: "";
  position: absolute;
  z-index: 2;
  top: -15%;
  bottom: -15%;
  left: 0;
  width: 24%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.04) 30%,
    rgba(255, 255, 255, 0.15) 50%,
    rgba(255, 255, 255, 0.04) 70%,
    rgba(255, 255, 255, 0) 100%);
  transform: translateX(-260%) rotate(10deg);
  /* Le retour à gauche ne doit surtout pas s'animer : sans ce `0s linear`
     différé, la bande se téléportait pendant qu'elle était encore visible
     — un éclair blanc traversant la carte à l'envers à chaque relais.
     C'est le bug qu'index3 avait fini par corriger, et il se serait reposé
     ici à l'identique. */
  transition: transform 0s linear 0.24s,  opacity var(--vif) var(--courbe);
}

/* Le balayage est plus long que l'arrivée de la carte, et il démarre avec
   elle : la lumière s'attarde une fraction après qu'elle s'est posée. */
.cf-item.is-elue .cf-card::after {
  opacity: 1;
  transform: translateX(520%) rotate(10deg);
  transition: transform var(--ample) var(--courbe) 0.06s, 
              opacity var(--vif) var(--courbe) 0.06s;
}

/* En plein écran, plus de balayage : à 1440 px de large ce n'est plus une
   pochette vernie, c'est un film.

   Le sélecteur reprend `.cf-item.is-elue` : sans lui il ne pesait que deux
   classes contre trois, et la règle du balayage l'emportait — mesuré, la
   bande restait à opacité 1 par-dessus le plein écran. */
.cf-item.is-elue .cf-card.is-immersif::after { opacity: 0; }

/* ---------- Le reflet au sol ----------
   C'est la signature de Cover Flow, et elle coûte presque rien :
   une copie de la pochette retournée, atténuée et fondue.

   Elle utilise la POSTER et jamais la vidéo : dupliquer un flux
   vidéo pour son reflet doublerait le décodage pour un élément
   qu'on regarde à peine. */
/* ---------- LE REFLET ----------

   Un sol n'est pas un miroir. Trois choses distinguent l'un de l'autre, et
   les trois manquaient :

   Il ABSORBE. Une surface réelle rend moins de lumière qu'elle n'en reçoit,
   et pas également selon la longueur d'onde — d'où saturate(0.7) et
   brightness(0.72) : le reflet est plus terne ET plus gris que l'objet, pas
   seulement plus transparent. C'est ce qui manquait le plus : à opacité
   seule, on lisait une copie pâle, pas un reflet.

   Il DIFFUSE. Un flou uniforme d'1 px se lisait comme un cadrage manqué.
   2,4 px disent une surface mate. Un seul filtre ne peut pas être gradué
   avec la distance, mais l'extinction, elle, peut l'être — et c'est elle
   qui porte la sensation.

   Il S'ÉTEINT VITE, ET PAS DROIT. Le dégradé allait de 12 % à 100 % en
   ligne droite : une extinction linéaire se lit comme un fondu de montage.
   Quatre arrêts la creusent, et le reflet meurt plus tôt qu'il ne
   commençait à le faire.

   Le dégradé est défini dans le repère LOCAL, donc avant le retournement :
   le bas local devient le haut visuel, et c'est lui qui doit être opaque
   puisqu'il touche la pochette. */
/* Le reflet n'existe que là où il se voit : au-delà du cinquième rang il ne
   reste qu'une soixantaine de pixels sombres sous une carte de 200 px, pour
   une passe de composition entière. Et le titre est masqué plutôt que
   transparent — une opacité nulle se peint, une visibilité nulle non. */
.cf-item:not(.is-reflet) .cf-reflect { display: none; }
.cf-item:not(.is-nomme) .cf-nom { visibility: hidden; }

.cf-reflect {
  position: absolute;
  left: 0;
  right: 0;
  /* 100 %, et il n'y a RIEN à compenser.

     J'avais poussé cette valeur à 107 puis 111 % pour « rattraper la
     bascule de la scène ». Elle rattrapait en réalité une mesure fausse :
     je comparais reflet.top à carte.bottom via getBoundingClientRect, qui
     rend la boîte englobante alignée sur les axes. Les cartes sont tournées
     de --rx et --ry — à l'écran ce sont des parallélogrammes. Cette
     comparaison mettait donc en regard le coin le plus BAS de la carte et
     le coin le plus HAUT du reflet, deux points qui ne sont pas à la même
     abscisse. Elle annonçait −2 px là où les vrais coins, mesurés par des
     repères d'un pixel posés dans le repère local, en montraient 24,5.

     La carte occupe exactement [0 ; H] de la fente. Poser le reflet à 100 %
     met son arête de contact sur le bord bas de la carte, dans le même
     plan : deux arêtes confondues le restent sous n'importe quelle
     projection, quelle que soit la rotation. */
  top: 100%;
  /* 38 % de la carte, dont on ne rendra que --k : voir plus bas. */
  height: 38%;
  overflow: hidden;
  pointer-events: none;

  /* ---------- LE REFLET SE RABATTAIT SUR LA CARTE ----------

     `transform-origin: 50% 0` avec `scaleY(-1)` renvoie la boîte VERS LE
     HAUT. Elle était bien placée sous la carte par `top: 100%`, mais la
     transformée la ramenait par-dessus : mesuré, elle chevauchait de 86 à
     100 px selon les cartes, et son bord bas coïncidait exactement avec
     celui de la carte — la signature du repli.

     C'est pourquoi il était si difficile à voir. Ce n'était pas un reflet
     faible : c'était une copie sombre et floue posée sur le tiers bas de la
     carte, au même endroit que le dégradé du titre. On ne voyait pas un
     reflet, on voyait la carte s'assombrir en bas.

     translateY(--k × 100%) avant scaleY(−--k) remet la boîte sous la carte :
     le retournement l'envoie à [−kH ; 0], la translation la ramène à
     [0 ; kH], vers le bas.

     Et --k vaut 0,78, pas 1 : un reflet au sol est vu en RACCOURCI. Un
     miroir rend l'objet à sa taille, un sol le comprime — c'est la
     différence entre une vitre posée à plat et un plancher. */
  --k: 0.78;
  transform-origin: 50% 0;
  transition: transform var(--ample) var(--courbe);
  transform: translateY(calc(var(--k) * 100%))
             scaleY(calc(-1 * var(--k)));
  /* --sel vaut 1 sur la carte ÉLUE, 0 sur les autres. La formule d'avant,
     0.3 x (1 − 0.78 x --sel), tombait donc à 0,066 au centre : le reflet
     disparaissait sur la seule carte qu'on regarde. Son commentaire disait
     « un objet qui s'éloigne du miroir y laisse une image plus faible » —
     mais la carte élue est la plus PROCHE, pas la plus lointaine.

     Troisième vestige du repli, comme le translateY(--sel x 128px) plus
     bas : tant que le reflet se rabattait SUR la carte, il fallait bien
     l'effacer là où il se voyait. Posé au sol, il n'a plus à s'excuser —
     la carte la plus proche est aussi la mieux éclairée, donc celle qui
     doit marquer le plancher le plus franchement. */
  opacity: calc(0.3 + 0.14 * var(--sel, 0));
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, rgba(0, 0, 0, 0.06) 22%, rgba(0, 0, 0, 0.34) 52%,
    rgba(0, 0, 0, 0.72) 78%, #000 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, rgba(0, 0, 0, 0.06) 22%, rgba(0, 0, 0, 0.34) 52%,
    rgba(0, 0, 0, 0.72) 78%, #000 100%);
}
/* Le reflet suit l'ouverture.

   Il portait un translateY(--sel x 128px) : un vestige du temps où il se
   repliait SUR la carte, où il fallait donc le repousser quand la carte
   montait. La géométrie corrigée, ce terme ne compensait plus rien — il
   décollait le reflet de 69 px dès que la carte devenait la carte élue.
   Le reflet est ancré sur l'objet, il n'a rien à rattraper.

   La carte passe à 168 % de large au format réel ; le reflet, lui, est
   dimensionné sur la FENTE, pas sur la carte. Mesuré, il restait à 242 px
   sous une carte de 425 — sous-dimensionné de 43 % et décalé de 92 px. Le
   film s'ouvrait et son reflet restait celui de la carte fermée.

   Par une échelle et non par des marges : c'est une transformée, donc elle
   s'anime avec la carte au lieu de sauter, et elle ne coûte pas de mise en
   page. La verticale n'a rien demandé — mesurée à −5 px dans les deux
   états, la carte gagnant en hauteur ce que le reflet perd en distance. */
.cf-card.is-plein ~ .cf-reflect {
  /* La carte ouverte fait 94,5 % de la fente et reste centrée : son bord bas
     remonte donc de 2,75 % de la fente. Le reflet est ancré sur la fente,
     pas sur elle, et mesure 38 % de sa hauteur (voir `height` plus haut) —
     il faut le remonter de 2,75/38 = 7,24 % de sa PROPRE hauteur, puisque
     les pourcentages d'un translateY sont relatifs à l'élément.

     Ce n'est pas la valeur qu'il y avait ici. J'avais posé 16 %, obtenus en
     tâtonnant contre une mesure de boîte englobante qui ne mesurait pas la
     bonne chose. Celle-ci est dérivée, pas réglée. */
  transform: translateY(-7.24%)
             translateY(calc(var(--k) * 100%))
             scaleY(calc(-1 * var(--k)))
             scaleX(1.68);
}

.cf-reflect img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Le BAS de l'affiche, et non son milieu.

     Un reflet montre ce qui touche le sol. Avec le cadrage par défaut il
     montrait la bande centrale de l'image — donc pas ce qui se trouve juste
     au-dessus de lui. Après le retournement, ce bas se retrouve au contact
     de la carte, là où il doit être. */
  object-position: 50% 100%;
  display: block;
  filter: blur(2.4px) saturate(0.7) brightness(0.72);
}

/* La ligne de contact. Sans elle, la pochette et son reflet se touchent par
   une arête nette et l'ensemble flotte : c'est l'ombre au point de pose qui
   dit qu'un objet repose sur quelque chose. Elle vit sur le reflet et non
   sur la carte, pour ne pas assombrir l'image elle-même — et « bottom »
   dans le repère local, c'est le point de contact une fois retourné. */
/* Le reflet d'une carte qui joue.

   .cf-reflect ne contenait qu'une <img> : la carte élue lisait sa vidéo et
   son reflet restait la photo d'affiche. Un plancher qui ne bouge pas sous
   un objet qui bouge — et depuis que le reflet de la carte élue est passé
   de 0,066 à 0,44, ça se voyait.

   Même principe que la vignette de la carte : la vidéo se SUPERPOSE à
   l'image, elle ne la remplace pas, et elle n'apparaît que sur `playing`.
   Tant qu'elle n'a pas d'images, on voit l'affiche — jamais un trou. */
.cf-reflect video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 100%;
  display: block;
  filter: blur(2.4px) saturate(0.7) brightness(0.72);
  opacity: 0;
  transition: opacity var(--moyen) var(--courbe);
}
.cf-reflect video.is-vue { opacity: 1; }

/* ---------- LES FILMS EN FORMAT LARGE ----------

   Un film tourné plus large que le 16/9 arrive avec des bandes noires DANS
   son image : `object-fit` ne les voit pas. Sa fente porte `--zoom`, le
   rapport qui les pousse hors du cadre — 1,44 pour Iron Man, 720 / 500.

   Dans le carré et dans la carte agrandie (`is-plein`), où il rogne un
   peu les côtés pour ne jamais montrer de noir. En plein écran
   seulement (`is-immersif`), on revient à l'image telle qu'elle a été
   livrée, au même rythme que la carte grandit. Le reflet, lui, est déjà
   éteint à ce stade.

   Le reflet suit, sinon il refléterait la bande du bas. Il montre le BAS
   de l'image : on l'agrandit depuis ce bas, puis on le descend de la
   hauteur de bande agrandie pour que le film touche le bord. Sa boîte fait
   38 % de la fente, soit 0,38 / --cr de sa largeur, et l'image y est
   posée par la largeur, donc haute de 9/16 de celle-ci. La bande
   agrandie vaut (zoom − 1) / 2 de cette hauteur :
   (zoom − 1) / 2 × 0,5625 × --cr / 0,38 ≈ (zoom − 1) × 0,74 × --cr. */
.cf-face {
  transform: scale(var(--zoom, 1));
  transition: transform var(--ample) var(--courbe);
}
.cf-reflect img,
.cf-reflect video {
  transform-origin: 50% 100%;
  transform: translateY(calc((var(--zoom, 1) - 1) * 74% * var(--cr)))
             scale(var(--zoom, 1));
  transition: transform var(--ample) var(--courbe), opacity var(--moyen) var(--courbe);
}
.cf-card.is-immersif .cf-face {
  transform: none;
  transition-duration: var(--long);
}

/* L'ombre de contact appartient au SOL, pas au reflet.

   Elle était un enfant de .cf-reflect — l'élément qui porte l'opacité du
   reflet. Une opacité sur un parent compose tout le groupe : le noir de
   l'ombre était donc multiplié par elle.

     ombre 0,18 x reflet 0,30 = 0,054 de noir réel dans le paquet
     ombre 0,18 x reflet 0,44 = 0,079 sur la carte élue

   5 % de noir sur 7 px, par-dessus une image déjà floutée et assombrie :
   rien d'atteignable à l'œil. Et c'était déjà vrai avant de la réduire —
   à 0,55 elle ne pesait que 0,165. Elle n'a jamais rien peint de
   perceptible, et les quatre valeurs que j'y ai réglées portaient sur un
   nombre qui n'arrivait pas à l'écran.

   Portée par .cf-item, elle échappe à cette multiplication et garde son
   noir. C'est aussi plus juste : l'ombre d'un objet posé assombrit le sol,
   elle ne s'efface pas quand le reflet s'efface. Le dégradé part donc de
   50 % 0 % — le haut, c'est-à-dire le contact — au lieu de 50 % 100 %,
   qui était le contact tant qu'elle vivait dans une boîte retournée.

   L'item porte preserve-3d : le pseudo-élément est dans le même plan que
   la carte et le reflet, et subit la même projection. */
.cf-item::after {
  content: "";
  position: absolute;
  left: 8%;
  right: 8%;
  top: 100%;
  height: var(--ombre-h, 5%);
  background: radial-gradient(70% 100% at 50% 0%,
    rgba(0, 0, 0, var(--ombre-a, 0.26)) 0%, transparent 72%);
  pointer-events: none;
  transition: opacity var(--long) var(--courbe);
}
/* Mêmes conditions d'existence que le reflet : sans lui, pas d'ombre. */
.cf-item:not(.is-reflet)::after { display: none; }


/* ---------- Jauge ---------- */
.cf-meter {
  position: absolute;
  left: var(--pad-x, 40px);
  bottom: clamp(24px, 4vh, 40px);
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 11px;
  letter-spacing: 0.18em;
  color: rgba(255, 255, 255, 0.4);
  z-index: 3;
}
.cf-meter-bar {
  display: block;
  width: clamp(90px, 14vw, 180px);
  height: 1px;
  background: rgba(255, 255, 255, 0.16);
}
.cf-meter-bar > span {
  display: block;
  height: 100%;
  background: #fff;
  transform-origin: 0 50%;
  transform: scaleX(0);
}

/* ---------- Mobile ---------- */
/* LA FILE SUR UN TÉLÉPHONE — retenue au banc (test-carrousel-a.html)
   contre un rail au doigt et un rail en perspective.

   Mesuré sur 390 px avec l'ancien réglage : des cartes de 250 px dont le
   titre passait sous la voisine de devant, un cadre de 385 px centré qui
   laissait deux bandes noires, et 4 000 px de défilement pour six films.

   LA COURSE : 300vh et non plus 600, parce que v30.js ne garde que 12
   cartes au doigt au lieu de 24. Le débit par carte est inchangé, la page
   est deux fois plus courte. Puis 200vh : la course n'y fait plus qu'UN
   tour (6 cartes), toujours au même débit par carte.

   LE CADRE reprend toute la hauteur, comme au bureau : c'était lui qui
   faisait les bandes noires.

   LA GÉOMÉTRIE : cartes de 290 px, moitié de recouvrement au lieu de 62 %,
   et une fuite plus douce — un écran étroit n'a pas la place de l'oblique
   du bureau, où les voisines sortaient du cadre.

   LE PALIER « FORMAT RÉEL » : 112 % × 63 % du carré, soit du 16/9 qui tient
   dans l'écran. Les 168 % du bureau débordaient de 140 px, titre compris.

   La marge haute vaut une demi-largeur : la carte est carrée. L'ancien
   -0,667 venait du format 3/4 et décalait la carte de 48 px. */
@media (max-width: 760px) {
  .cf-section { height: 200vh; }
  .cf-stage { height: 100%; }
  :root {
    --cw: 290px;
    --ov: 0.5;
    --ry: -17deg;
    --rx: -14deg;
    --rise: 42px;
    --prof: 150px;
    --up: 0px;
  }
  .cf-item {
    width: min(74vw, 290px);
    margin-left: calc(min(74vw, 290px) / -2);
    margin-top: calc(min(74vw, 290px) * -0.5);
  }
  .cf-card.is-plein {
    width: 112%;
    height: calc(63% * var(--cr));
  }
}

/* ---------- Accessibilité ----------
   Sans animation : plus de 3D ni de section épinglée, une grille
   simple. Le JS se débranche de lui-même et n'écrit aucune
   transformée, donc rien ne vient contredire ces règles. */
@media (prefers-reduced-motion: reduce) {
  .cf-section { height: auto; }
  .cf-sticky { position: static; height: auto; padding-block: 56px; }
  .cf-stage {
    position: static;
    height: auto;
    perspective: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 18px;
    padding-inline: var(--pad-x, 40px);
  }
  .cf-item { position: static; width: auto; margin: 0; transform: none !important; }
  .cf-reflect,
  .cf-item::after { display: none; }
  .cf-meter { display: none; }
}


/* =========================================================
   LE BLOC-MARQUE
   ========================================================= */

/* Un signe, pas seulement un mot.

   Le bloc-marque n'était que du texte capitalisé : lisible, mais rien à
   quoi se raccrocher — pas d'onglet, pas de favicon, pas de coin d'écran où
   la marque existe sans son nom.

   PLACEHOLDER ASSUMÉ. Un carré arrondi — la fenêtre de projection — et un
   L plein dedans. Il tient les deux rôles minimum d'une marque : il se lit
   à 22 px dans une barre, et il reste reconnaissable réduit à un carré de
   16 px pour un favicon. Il ne prétend pas être le logo final ; il occupe
   la place que le logo final devra prendre, à la bonne échelle et au bon
   endroit, ce qu'un carré gris ne ferait pas.

   En SVG inline et non en fichier : il hérite de `currentColor`, donc il
   suit la couleur du texte au survol et dans le menu ouvert sans qu'on ait
   à gérer deux fichiers. Et il ne coûte pas une requête. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* Meme famille que le nom du hero : c'est le meme role, joue plus petit. */
  font-family: var(--font-marque);
}

/* Le carre et l'interlettrage du nom sont partis avec le texte : le
   logotype porte son propre espacement, et il n'y a plus de signe a cote.
   La boite et sa taille sont dans style.css, partagee par les cinq pages ;
   ici on ne garde que ce qui appartient a l'accueil. */


/* =========================================================
   LE HERO — verrou, filet, phrase de marque
   ========================================================= */

/* Panchang pour le nom, Switzer pour tout le reste.

   Deux familles et deux roles : une geometrique assuree porte le logotype,
   une grotesque suisse porte le texte et disparait derriere ce qu'elle dit.
   C'est la division classique — une voix pour l'identite, une neutralite
   pour la lecture — et elle evite qu'un dessin marque fatigue sur un
   paragraphe.

   La pile de secours est ecrite jusqu'au bout : si Fontshare ne repond
   pas, la page tombe sur la police systeme et reste lisible. Une seule
   variable pour le corps du site, Panchang n'etant appele que sur le nom
   et le bloc-marque. */
:root {
  --font-sans: "Switzer", -apple-system, BlinkMacSystemFont, "Segoe UI",
               Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-marque: "Panchang", "Switzer", -apple-system, BlinkMacSystemFont,
                 "Segoe UI", Roboto, sans-serif;
}

/* Le hero n'a plus de sur-titre, plus de titre en deux lignes, plus de
   paragraphe. Il ne reste que le verrou, la phrase, et les deux commandes.

   Les commandes NE bougent pas : elles restent en bas a droite, la ou
   elles etaient. .hero-meta etait en `space-between` — c'est le paragraphe
   qui tenait la gauche et les poussait a droite. Lui retire, il faut les
   ancrer, sinon elles reviennent a gauche. */
.hero { justify-content: stretch; }
.hero-inner { flex: 1; display: flex; flex-direction: column; }
.hero-meta {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  justify-content: flex-end;
}
.hero-controls { justify-content: flex-end; }

/* Le rang porte la POSITION, et il est dimensionne par son contenu.

   Posee sur .hero-inner — conteneur pleine hauteur — `align-items: center`
   recentrerait le bloc APRES la marge : mesure, la course verticale ne
   comptait plus qu'a moitie et l'ensemble tombait 300 px trop bas. Sur un
   rang qui fait la taille de ce qu'il contient, la marge agit en entier.

   18vh : le hero d'un studio se lit dans le premier tiers de l'ecran, pas
   au milieu — le reste appartient a l'image. */
.hero-rang {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  margin-top: 18vh;
}

/* Le verrou : signe et nom lus comme un seul objet. L'ecart vaut la moitie
   de la hauteur du signe — plus serre ils se touchent, plus large ils
   redeviennent deux choses posees cote a cote. */
.hero-verrou {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 15px;
  flex: none;
}

/* LE SIGNE FAIT LA HAUTEUR DE CAPITALE, ET C'EST TOUT LE CALAGE.

   A cette hauteur, son fut vaut celui des lettres PAR CONSTRUCTION : les
   deux sont dessines dans le meme rapport, 16,5 pour 135. C'est ce qui
   manquait aux essais precedents, ou je calculais l'epaisseur du signe a
   partir de SA taille au lieu de celle du mot — et ou je me suis trompe
   deux fois de suite, de 44 % puis de 5,6 %.

   La hauteur suit donc exactement celle du mot : meme `clamp`, terme a
   terme. La largeur en decoule (rapport 1,378), et on ne l'ecrit pas :
   l'imposer ferait varier la hauteur avec la fenetre et le signe cesserait
   de s'aligner sur les capitales. */
.hero-signe {
  /* 1,14 FOIS LA HAUTEUR DE CAPITALE, releve sur la maquette. Le `clamp`
     du mot est repris terme a terme et multiplie : 12x1,14 = 13,7 et
     18x1,14 = 20,5. Deux fourchettes independantes se desaccorderaient au
     prochain reglage sans que rien ne le signale. */
  /* 14 ET 21, DES ENTIERS. Le rapport releve sur la maquette donne
     12x1,14 = 13,7 et 18x1,14 = 20,5. Mesure a 1x, une boite de 20,5 px
     rend le bord haut a 255 et le bord bas a 128 : le bord inferieur tombe
     au milieu d'un pixel et le trait s'y dilue. Aux bornes entieres il
     tombe juste, et c'est la que se trouve la plupart des visiteurs — le
     `clamp` n'est fluide qu'entre les deux.

     Le rapport passe a 1,167, soit celui de l'en-tete (13 pour 11). Les
     deux echelles sont donc coherentes entre elles, ce qu'elles n'etaient
     pas avec 1,14 et 1,18. */
  height: clamp(14px, 1.56vw, 21px);
  width: auto;
  flex: none;
  display: block;
  /* Le trait du cadre est dessine exactement sur le bord du viewBox, ou un
     SVG est ecrete par defaut : il n'y a aucune marge pour
     l'anticrenelage. Ici la hauteur vient d'un `clamp` en `vw`, donc elle
     est fractionnaire par nature et on ne peut pas l'arrondir comme dans
     l'en-tete. */
  overflow: visible;
  opacity: 0.92;
}

/* LE BATTEMENT EST PARTI AVEC L'ONDE.

   Le signe precedent avait trois futs verticaux branches sur `--pb`,
   `--pm` et `--ph` — le grave, le medium et l'aigu de la bande du film,
   publies par l'analyseur. Un cadre et une lettre n'ont rien a faire
   respirer : les regles sont retirees plutot que laissees a viser des
   elements qui n'existent plus.

   L'analyseur, lui, continue de publier les trois variables : le pouls
   general s'en sert toujours pour la trainee du nom et la lueur du filet,
   et les trois bandes restent disponibles si un signe vivant revient. */

/* LE NOM N'EST PLUS DU TEXTE, DONC PLUS RIEN NE SE REGLE EN `font-size`.

   Panchang portait le nom, et tout le bloc ci-dessous reglait une police :
   corps, graisse, interlettrage, capitales. Le logotype est un trace ; son
   interlettrage et ses proportions sont DANS le dessin. Il ne reste donc
   qu'une chose a decider, la hauteur.

   ON REGLE LA HAUTEUR ET NON LA LARGEUR. Le rapport du trace est fixe
   (1189 sur 135, soit 8,8 pour 1) et une largeur imposee ferait varier la
   hauteur avec la fenetre — le mot passerait au-dessus du filet de la
   phrase, qui lui ne bouge pas. En hauteur, la largeur suit et le rang
   reste equilibre.

   18px : la hauteur de capitale que donnait Panchang a 25px de corps. Le
   hero garde donc exactement le poids visuel qu'il avait, et la fourchette
   reprend celle de l'ancien `clamp` mise a la meme echelle. */
.hero-nom {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* L'ecart entre les deux lignes suit la hauteur du mot, sinon il se
     defait aux extremes de la fourchette. */
  gap: 0.46em;
  font-size: clamp(12px, 1.37vw, 18px);
  line-height: 1;
}

.hero-mot {
  display: block;
  height: 1em;
  width: auto;
}

/* LA LIGNE DU DESSOUS EST DANS LE RAPPORT DU MODELE, PAS DANS UN RAPPORT
   CHOISI. Mesure sur le fichier fourni : 135 de haut pour le mot, 39 pour
   le descripteur, soit 1 a 3,5. On reporte le rapport plutot qu'une
   valeur, pour que les deux lignes restent solidaires a toute taille. */
/* LE DESCRIPTEUR A EXACTEMENT LA LARGEUR DU MOT.

   Son trace a ete rejustifie — les lettres reparties, pas etirees — et son
   viewBox fait desormais la meme largeur que celui du mot, 1189. Il suffit
   donc de leur donner la meme largeur en CSS pour qu'ils s'alignent aux
   deux bords.

   8,807em : c'est 1189/135, le rapport du viewBox du mot, qui s'affiche a
   1em de haut. Ecrire `width: 100%` marcherait aussi, mais dependrait de
   la largeur du parent et non de celle du mot — ce sont deux choses
   differentes des que le rang se reorganise. */
.hero-sous {
  display: block;
  width: 8.807em;
  height: auto;
  opacity: 0.85;
}

/* La phrase, separee par un filet a la hauteur du bloc.

   LA COUPURE EST EXPLICITE, PAR UN <br> POSE SUR LE POINT : la negation
   sur la premiere ligne, l'affirmation sur la seconde. C'est la structure
   de la phrase, et un repli au fil de l'eau la casserait n'importe ou.

   ELLE EST REVERSIBLE, ET C'EST LA SEULE CHOSE A SAVOIR SI ON LA REMPLACE.
   « Chase the chills » a ete essayee ici : trois mots, un imperatif, rien
   a opposer, donc pas de coupure. Le bloc de droite passe alors de 42 a
   21 px de haut, contre 31 pour le logotype a sa gauche — c'est lui qui
   commande la hauteur du rang, et `align-items: center` du `.hero-rang`
   s'en charge sans qu'on ait rien a ecrire. Aucun reglage a reprendre dans
   un sens comme dans l'autre.

   `max-width: none` reste, pour la meme raison qu'avant : la largeur d'une
   ligne varie d'un dessin de police a l'autre, donc un repli calcule par
   la largeur donnerait une ligne ici et deux ailleurs.

   1.5 et non 1.55 : deux lignes courtes et centrees se tiennent mieux un
   peu plus serrees — a 1.55 elles commencaient a se separer. */
.hero-phrase {
  margin: 0 0 0 30px;
  padding-left: 30px;
  border-left: 1px solid rgba(255, 255, 255, 0.2);
  max-width: none;
  font-size: 14px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.62);
}

/* Le nom passe la main.

   Le studio est ecrit en grand dans le hero : le repeter en haut a gauche
   au meme moment fait redite. Mais le faire disparaitre pour de bon ferait
   perdre son nom a l'en-tete sur tout le reste du site.

   Il s'efface donc tant qu'on est dans le hero, et revient des qu'on l'a
   quitte. Par l'opacite et non `display` : la largeur reste reservee, donc
   l'en-tete ne saute pas au moment du relais. */
.brand-nom {
  transition: opacity var(--long, 1.2s) var(--courbe, ease);
}
html:not(.a-defile) .brand-nom { opacity: 0; }

/* LES COMMANDES PASSENT AU-DESSUS DE « DÉFILER ».

   Sur un téléphone le hero n'a que 32 px de marge basse, et l'indication
   de défilement est posée à 22 px du bas : « Voir les films » tombait
   exactement sur elle, les deux textes l'un dans l'autre. On relève le
   bloc plutôt que de retirer l'indication, et les deux commandes
   s'étalent sur la largeur au lieu de se serrer à droite. */
@media (max-width: 720px) {
  .hero-rang { flex-direction: column; align-items: flex-start; gap: 20px; }
  .hero-phrase { margin: 0; padding: 0 0 0 16px; }
  .hero { padding-bottom: 72px; }
  .hero-meta { left: 0; }
  .hero-controls { justify-content: space-between; }

  /* LE LOGOTYPE DOUBLE SUR UN TÉLÉPHONE.

     Le `clamp` du bureau tombe à sa borne basse, 12 px : « POLAR » ne
     faisait que 106 px sur un écran de 390, une signature en bas de page
     plutôt que le nom du studio sur son premier écran. Il passe à 24 px, soit
     211 px de large.

     Les trois valeurs gardent les rapports du bureau : signe à 1,167 fois
     le mot (28 pour 24, comme 21 pour 18), écart à la moitié du signe
     environ. Des entiers pairs, pour la même raison que plus haut : le
     trait du cadre doit tomber sur des pixels pleins. */
  .hero-nom { font-size: 24px; }
  .hero-signe { height: 28px; }
  .hero-verrou { gap: 20px; }
}


/* =========================================================
   LES SORTIES DU PLEIN ÉCRAN
   ========================================================= */

/* Une fois le film en plein écran, le seul moyen d'en sortir était de
   défiler. C'est une sortie qu'on trouve par accident, pas une qu'on voit.

   Une seule commande, en bas à droite : « Voir tous les films ». Elle ne
   ramène pas au paquet — le défilement le fait déjà, et un second bouton
   pour la même chose n'aurait fait qu'encombrer une image plein écran.
   Elle mène ailleurs, ce que le défilement ne fait pas.

   Portée par .cf-sticky et non par une carte : le paquet s'éteint en
   immersion, et une commande posée dessus disparaîtrait avec lui. */
.cf-sortie {
  position: absolute;
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 10px 16px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.34);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  color: rgba(255, 255, 255, 0.82);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  /* Elles arrivent APRÈS le film : d'abord l'image, puis de quoi la
     quitter. Un délai de 0,5 s, le temps qu'on ait vu où on est. */
  /* ELLE EXISTE AUSSI HORS DU PLEIN ÉCRAN.

     Elle n'apparaissait qu'en immersion, où elle sert à sortir. Mais la
     section est un rail de six films dont on ne voit qu'une poignée : le
     visiteur qui veut la suite n'avait aucun endroit où le dire, sinon la
     navigation du haut, qui parle d'autre chose. Une sortie vers le
     catalogue appartient au rail lui-même.

     Elle reste au même endroit dans les deux états — en bas à droite — et
     ne change que de retard : immédiate ici, différée de 0,5 s en plein
     écran, le temps qu'on ait vu où on est. */
  opacity: 1;
  transform: none;
  transition: opacity var(--moyen) var(--courbe),
              transform var(--moyen) var(--courbe),
              border-color var(--vif) var(--courbe),
              background var(--vif) var(--courbe);
  pointer-events: auto;
}
.cf-sortie { bottom: clamp(18px, 3vh, 34px); right: clamp(18px, 3vw, 40px); }

/* En plein écran elle arrive APRÈS le film : d'abord l'image, puis de quoi
   la quitter. Ailleurs elle est là tout de suite — rien à attendre. */
.cf-sticky.is-immersif .cf-sortie { transition-delay: 0.5s, 0.5s, 0s, 0s; }
.cf-sticky:not(.is-immersif) .cf-sortie { transition-delay: 0s; }

.cf-sortie:hover {
  border-color: rgba(255, 255, 255, 0.42);
  background: rgba(0, 0, 0, 0.52);
  color: #fff;
}
.cf-sortie:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.7);
  outline-offset: 3px;
}
.cf-sortie-fleche {
  font-size: 13px;
  line-height: 1;
  transition: transform var(--vif) var(--courbe);
}
.cf-sortie:hover .cf-sortie-fleche { transform: translateX(3px); }

/* Sur un téléphone, le libellé du bas s'abrège : deux commandes et une
   légende de film ne tiennent pas côte à côte en bas d'un écran étroit. */
@media (max-width: 560px) {
  .cf-sortie { padding: 9px 13px; font-size: 11px; letter-spacing: 0.1em; }
  .cf-sortie span:first-child { display: none; }
  .cf-sortie::before { content: "Tous les films"; }
}

@media (prefers-reduced-motion: reduce) {
  /* ELLE RESTE. Elle disparaissait ici, et c'était défendable tant qu'elle
     n'était qu'une commande d'immersion apparaissant en fondu : sans
     animation, un élément qui surgit sans prévenir vaut mieux absent.

     Elle est maintenant le seul lien vers le catalogue depuis le rail.
     Supprimer une navigation parce que le visiteur a demandé moins de
     mouvement, c'est le punir d'un réglage d'accessibilité. On enlève
     l'animation, pas le lien. */
  .cf-sortie { transition: none; }
}


/* =========================================================
   L'EN-TÊTE PENDANT LE PLEIN ÉCRAN
   ========================================================= */

/* Il disparaissait, et pas à cause de l'immersion : main.js le masque dès
   qu'on défile VERS LE BAS, et il faut descendre pour atteindre le plein
   écran. Il était donc toujours parti quand on y arrivait — le visiteur se
   retrouvait devant une image plein écran sans marque, sans navigation, et
   sans autre issue que de deviner qu'il fallait défiler.

   On le rappelle, et `.header--hidden` ne peut rien contre ça : la règle
   est plus spécifique — deux classes contre une — donc le JS peut
   continuer d'écrire la sienne sans effet tant que dure l'immersion, et
   elle reprend la main dès qu'on en sort. Aucun état à synchroniser entre
   les deux fichiers.

   `mix-blend-mode: difference`, déjà porté par l'en-tête, fait le reste :
   sur un film qui change de plan, le texte s'inverse au lieu de se perdre
   dans une zone claire. C'est ce qui permet de le poser sur une image
   quelconque sans caisson ni ombre. */
html:has(.cf-sticky.is-immersif) .header,
body.cf-immersif .header {
  transform: none;
  opacity: 1;
}



/* =========================================================
   LA LÉGENDE DU PORTRAIT
   ========================================================= */

/* Elle n'était pas mal réglée : elle n'était pas réglée du tout.

   style.css vise `.portrait figcaption`, or la légende est la SŒUR de
   .portrait et non sa descendante — le sélecteur ne l'a jamais atteinte.
   Elle s'affichait donc en 16 px blanc, avec ses deux mentions collées à
   0 px l'une de l'autre : « Ludo — RéalisateurParis ».

   La règle est reprise ici sur `figure`, qui est bien l'ancêtre commun.
   Elle reste dans v29.css et non dans style.css : la feuille partagée sert
   une vingtaine de pages que je ne peux pas vérifier, et la même règle
   morte y attend le même correctif — mais c'est un autre chantier, à faire
   d'un coup et en les regardant.

   Les deux mentions rejetées aux extrémités : le nom à gauche, le lieu à
   droite, comme une ligne de générique. C'est ce que voulait le flex
   d'origine, et c'est la seule disposition où l'écart entre les deux ne
   dépend pas de la longueur du nom. */
#studio figure figcaption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--grey-60);
}

/* Le nom porte le poids, le lieu reste une indication. Sans cette nuance
   les deux se valent, et une légende de portrait n'a pas deux sujets. */
#studio figure figcaption span:first-child {
  color: rgba(255, 255, 255, 0.78);
}


/* =========================================================
   LA SECTION STUDIO, ET SON GLOBE
   ========================================================= */

/* Le globe déborde par la droite au lieu de flotter dans la colonne.

   Centré, il occupait toute la largeur et le contenu se posait DESSUS —
   99 700 px² de recouvrement sur le texte, 10 100 sur le portrait, mesurés
   au banc. Poussé à droite, la colonne de gauche est entièrement libérée :
   le portrait et le début des lignes retrouvent un fond noir, et le globe
   n'accompagne plus que leur fin.

   Ce qui l'empêche de flotter n'est pas un fondu mais le CADRAGE. Il sort
   par le bord de la fenêtre, on n'en voit qu'une partie, et un objet coupé
   par le bord de l'écran appartient à la page — là où un cercle entier
   posé dans une colonne se lit comme une illustration déposée là.

   `right` calculé sur 100vw et non sur la largeur du conteneur : la
   référence devient la fenêtre, donc le débordement reste le même quelle
   que soit la largeur de la colonne de texte. */
/* La hauteur : 620 px au lieu de 760.

   La section faisait 1 077 px pour 356 px de contenu — deux tiers de vide.
   C'est le globe qui l'imposait, pas le texte, et un globe n'a pas besoin
   d'être entier pour se lire : il déborde déjà par la droite, il peut
   aussi bien déborder un peu par le bas.

   Et ce n'est pas qu'une question de proportions. Plus le bloc est haut,
   plus le point de recalage est loin quand on arrive par le haut — c'est
   ce qui mettait l'aimant hors de portée à l'entrée. */
.studio-globe {
  position: relative;
  min-height: clamp(440px, 48vw, 620px);
  display: flex;
  align-items: flex-end;
}

/* LA SECTION ROGNE SON GLOBE.

   `.studio-sphere` est poussée hors du cadre par un `right` négatif — c'est
   voulu, le globe doit sortir par la droite. Mais rien ne la rognait : elle
   débordait de 101 px la fenêtre d'un téléphone, et la page se laissait
   défiler latéralement de 116 px. Sur un téléphone, une page qui glisse de
   côté au moindre geste oblique donne l'impression d'être cassée.

   `clip` et non `hidden` : `hidden` ferait de la section un conteneur de
   défilement, ce qui casserait le positionnement collant qu'elle abrite.
   `clip` rogne sans rien rendre défilable — c'est exactement l'outil.

   `body` porte pourtant `overflow-x: hidden` : la propagation de cette
   valeur vers la fenêtre ne suffit pas ici, mesuré plutôt que supposé.

   LE ROGNAGE VA SUR `main`, ET C'EST TOUT LE POINT. Posé sur la section,
   il tombait au bord de la COLONNE — 1 368 px sur une fenêtre de 1 440 —
   et tranchait le globe d'un trait vertical net en plein milieu de l'écran.
   Le globe doit sortir du cadre par la droite ; il ne doit pas allonger la
   page. Seul un élément qui fait toute la largeur peut faire la différence
   entre les deux. */
main { overflow-x: clip; }

.studio-sphere {
  position: absolute;
  top: 38%;
  right: calc((100vw - min(1280px, 100vw - 2 * var(--pad-x))) / -2 - 12vw);
  translate: 0 -50%;
  width: min(64vw, 760px);
  aspect-ratio: 1;
  /* 0,62 et non plus bas : le globe ne passe plus sous le texte, il n'a
     donc plus à s'effacer pour lui. Quand un réglage résout un problème,
     celui qui le compensait doit remonter. */
  opacity: 0.62;
  z-index: 0;
}
.studio-sphere canvas { width: 100%; height: 100%; display: block; }

/* La grille de .studio est resserrée pour cette section seulement.

   style.css la donne en 0,8fr / 1,2fr : le texte allait jusqu'à x=1168
   pendant que le globe commençait à 861, soit 10 451 px² de recouvrement —
   toute la fin des lignes passait sur la sphère. La grille du banc n'était
   pas la vraie, et la mise en page ne se vérifie que dans la page.

   0,9 / 1 et une largeur de ligne bornée : le portrait garde sa place, le
   texte s'arrête avant le globe. La borne en `ch` plutôt qu'en pixels
   parce que c'est la longueur de LIGNE qui doit être tenue — 62 signes est
   la limite au-delà de laquelle l'œil perd le début de la ligne suivante,
   et elle ne dépend pas de la largeur de l'écran. */
/* Le portrait passe à 260 px, et c'est lui le réglage — pas le texte.

   Il faisait 580 px : à trois éléments sur une ligne, il poussait le texte
   sous le globe, et aucune largeur de ligne ne rattrapait ça. La grille du
   banc en donnait 230 ; c'est en la reprenant telle quelle que la mise en
   page a tenu, et c'est ce que j'aurais dû transposer d'emblée plutôt que
   de garder la grille par défaut de style.css.

   Une colonne en pixels et non en fraction : le portrait n'a pas à grandir
   avec l'écran, il accompagne. Ce qui doit prendre la place gagnée, c'est
   le texte. */
.studio-globe > .studio {
  position: relative;
  z-index: 1;
  width: 100%;
  grid-template-columns: 260px minmax(0, 1fr);
  align-items: end;
}
.studio-globe > .studio .lead,
.studio-globe > .studio .muted { max-width: 38ch; }

/* Sur un écran étroit, le globe passe derrière et s'efface : deux colonnes
   deviennent une, et un globe à 62 % sous du texte le rendrait illisible. */
/* LA GRILLE REPASSE À UNE COLONNE, ET IL FAUT LE DIRE ICI.

   style.css empile bien `.studio` sous 1 024 px, mais la règle du dessus —
   `.studio-globe > .studio`, deux sélecteurs contre un — l'emportait à
   toute largeur. Mesuré sur un téléphone de 390 px : 260 px de portrait,
   24 de gouttière, et 66 px pour le texte, qui s'étirait sur 977 px de haut
   hors de l'écran. Le texte du studio était tout simplement illisible. */
@media (max-width: 960px) {
  .studio-globe { min-height: 0; display: block; }
  .studio-sphere {
    position: absolute; top: 50%; right: -30vw;
    width: 110vw; opacity: 0.34;
  }
  .studio-globe > .studio {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }
  /* Une colonne pleine largeur ferait un portrait de 592 px sur une
     tablette de 768 : il accompagne le texte, il ne doit pas le précéder
     d'un écran entier. Sans effet sur un téléphone, déjà plus étroit. */
  .studio-globe figure { max-width: 440px; }
}

/* Sous prefers-reduced-motion, pas de globe du tout : c'est un objet qui
   tourne en permanence, et aucune version immobile n'en dirait autant. */
@media (prefers-reduced-motion: reduce) {
  .studio-sphere { display: none; }
}


/* =========================================================
   LE PORTRAIT — un boîtier posé
   =========================================================

   La version précédente inclinait la photo au pointeur. Deux défauts, et
   le second est le vrai.

   Elle DÉFORMAIT l'image : une rotation en perspective rend un rectangle
   trapézoïdal, et une photographie trapézoïdale se lit comme une
   photographie tordue — pas comme un objet vu de biais. Ce qui donne le
   volume n'est pas la déformation de la face, c'est de voir SA TRANCHE.

   Et elle ne se voyait qu'à la souris : rien sur un téléphone, rien au
   défilement.

   Ici, la pose est FIXE — un boîtier posé de trois quarts, comme un objet
   photographié en studio. Il a une face, une tranche, une ombre. Ce qui
   bouge n'est plus l'angle mais l'ARRIVÉE : il se redresse quand la
   section entre dans l'écran, et ça marche partout, y compris au doigt. */
.studio-globe figure {
  /* 1400 px : une perspective LONGUE. C'est l'inverse du réglage
     précédent, et c'est ce qui distingue un objet photographié au
     téléobjectif — fuyantes presque parallèles, aucune déformation — d'un
     objet vu au grand-angle, qui s'étire. Le premier est le langage des
     pages produit, le second celui des jeux vidéo. */
  perspective: 1400px;
  perspective-origin: 62% 45%;
}

.plateau {
  position: relative;
  transform-style: preserve-3d;
  will-change: transform;
  /* La pose au repos : 15° de trois quarts, un demi-degré de plongée.

     Le boîtier lit `--a` et non `--ar` : c'est l'avancement CORRIGÉ DE SON
     RANG. Il lisait l'avancement brut, donc son rang ne servait à rien et
     il arrivait en même temps que le globe — la mise en scène existait
     dans la feuille de style et pas à l'écran.

     La règle : `--ar` est écrit par le script, `--a` est ce que chaque
     élément en fait. Aucune forme ne doit lire le premier. */
  transform:
    translateY(calc(var(--a, 0) * 34px))
    /* Deux termes de rotation, et ils ne disent pas la même chose.

       `--a` est l'ARRIVÉE : il ferme le boîtier de 13° tant qu'il entre,
       puis disparaît. `--tr` est la TRAVERSÉE : il va de +1 sous la
       fenêtre à −1 au-dessus, donc le boîtier se présente en montant, se
       met de face au passage, et se referme en partant.

       MESURÉ AVANT DE CORRIGER : sur toute la traversée de la section, le
       boîtier balayait 20,9° — de 26° à 5,1° — répartis sur 1 200 px de
       défilement, soit 1,7° pour 100 px. La rotation existait bel et bien ;
       elle était trop diluée pour se voir. Ce n'était pas une amplitude
       insuffisante, c'était un DÉBIT insuffisant, et augmenter la seule
       amplitude n'aurait réglé que la moitié du défaut.

       Donc les deux : 18° au lieu de 11°, et la fenêtre de traversée
       resserrée dans le script (0,72 → 0,55 de hauteur d'écran). Le balayage
       passe à 36° sur 1 000 px — 3,6° pour 100 px, deux fois plus vif.

       L'arrivée cède 5° à la traversée (13° → 8°) : les deux termes
       s'additionnent au moment de l'entrée, et sans cela le boîtier
       démarrait à 53°, presque de profil.

       LA POSE FINALE SE RÈGLE PAR LA BASE, ET ELLE NE TOMBE PAS SUR --tr = 0.

       L'aimant centre la SECTION, or le boîtier est 170 px sous son milieu :
       au repos la traversée vaut 0,338, pas 0. La pose finale n'est donc pas
       la base mais la base plus un tiers d'amplitude — c'est ce qui l'avait
       portée à 28,1°, franchement trop pivotée.

       Base 15° et amplitude 14° : le repos retombe à 19,7°, et la course va
       de 1° à 29°. On perd un cinquième du débit (3,58 → 2,79° pour 100 px,
       toujours 1,6 fois l'original), et c'est le prix à payer — l'amplitude
       et la pose de repos se disputent la même base tant que la rotation est
       linéaire en --tr.

       Le bas de course à 1° ne se voit pas : à --tr = −1 le boîtier est déjà
       45 px au-dessus du haut de la fenêtre. Ce qu'on regarde va de −0,5 à
       +1, soit de 8° à 29°. */
    rotateY(calc(15deg + var(--a, 0) * 8deg + var(--tr, 0) * 14deg))
    rotateX(calc(0.5deg + var(--a, 0) * 1.5deg + var(--tr, 0) * -2.6deg));
  opacity: calc(1 - var(--a, 0) * 0.9);
}

.boitier {
  position: relative;
  transform-style: preserve-3d;
}

/* LA TRANCHE — le seul élément qui fasse vraiment le volume.

   Une face de 13 px de profondeur, attachée au bord gauche et rabattue
   vers l'arrière. Ce n'est pas un rectangle posé derrière : c'est un
   véritable côté, dans le plan perpendiculaire, qui se découvre quand
   l'objet tourne et disparaît quand il se met de face.

   Le dégradé va du clair au sombre en descendant : une tranche prend la
   lumière sur son arête haute, comme le dos d'un livre sur une étagère. */
.boitier-tranche {
  position: absolute;
  left: 0;
  top: 0;
  width: 13px;
  height: 100%;
  transform-origin: 0 50%;
  transform: rotateY(-90deg);
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.24) 0%,
    rgba(255, 255, 255, 0.09) 20%,
    rgba(0, 0, 0, 0.5) 100%);
  /* L'ARÊTE SPÉCULAIRE, dont l'intensité suit l'angle.

     Un filet clair là où la tranche rencontre la face. C'est le détail qui
     sépare un objet mat d'un objet fabriqué — une arête vive accroche
     toujours la lumière, quelle que soit la matière.

     Maximale quand la tranche se présente, presque éteinte quand elle se
     referme : une arête qui garderait le même éclat en pivotant dirait que
     la lumière tourne avec l'objet. */
  box-shadow: inset -1px 0 0
    rgba(255, 255, 255, calc(0.18 + var(--tr, 0) * 0.14));
  border-radius: 2px 0 0 2px;
  pointer-events: none;
}

.studio-globe .media.portrait {
  border-radius: 3px 5px 5px 3px;   /* l'arête de la tranche est plus vive */
  overflow: hidden;
  /* DEUX OMBRES, ET C'EST LA SECONDE QUI COMPTE.

     La grande, portée : 26 px vers la droite, du côté opposé à la tranche
     — la lumière vient de la gauche, l'objet se projette à droite. Une
     ombre centrée annulerait tout ce qu'on vient de construire.

     La petite, de CONTACT : 3 px, presque noire, collée à l'objet. C'est
     elle qui dit qu'il touche quelque chose. Une ombre unique et large
     fait flotter — c'est le défaut le plus répandu des cartes en CSS, et
     il se corrige avec quatre pixels.

     Elles s'élargissent à l'arrivée : un objet qui descend est plus loin
     de son support, donc son ombre est plus grande et plus floue. C'est ce
     qui fait qu'on le voit se POSER et pas seulement apparaître. */
  /* L'OMBRE SUIT L'ANGLE, en plus de l'arrivée.

     Elle restait à 26/40 px quelle que soit la rotation : un objet qui
     pivote de 22° projetait exactement la même ombre. C'est le second
     signal de réalisme après le reflet, et il ne coûte qu'un terme.

     Plus le boîtier se présente de trois quarts (--tr positif), plus il
     masque de lumière sur sa droite, donc plus l'ombre s'y allonge. Elle
     se rétracte quand il se met de face. */
  box-shadow:
    calc(26px + var(--a, 0) * 22px + var(--tr, 0) * 13px)
      calc(40px + var(--a, 0) * 34px + var(--tr, 0) * 5px)
      calc(70px + var(--a, 0) * 40px + var(--tr, 0) * 12px)
      -34px rgba(0, 0, 0, 1),
    calc(3px + var(--tr, 0) * 2px) 3px 14px rgba(0, 0, 0, 0.5);
}

/* Le rebord clair, repris des cartes du carrousel. Sur un fond noir,
   assombrir un bord rapproche l'objet de son fond au lieu de l'en
   détacher. */
.studio-globe .media.portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  border-radius: inherit;
  box-shadow:
    /* Trois arêtes et non une seule : le pourtour donne la forme, le bord
       haut prend la lumière du ciel, le bord gauche celle de la source.
       Une arête uniforme se lirait comme un contour tracé, pas comme un
       objet éclairé. */
    inset 0 0 0 1px rgba(255, 255, 255, 0.16),
    inset 0 1px 0 rgba(255, 255, 255, 0.34),
    inset 1px 0 0 rgba(255, 255, 255, 0.26);
}

/* LE LUSTRE BALAIE LA SURFACE.

   Je l'avais figé en écrivant « un objet immobile n'a aucune raison de
   changer de reflet ». L'objet ne l'est plus : il tourne de 22° pendant la
   traversée, et une lumière fixe sur une surface qui pivote est le signal
   le plus sûr qu'on regarde une image et non un objet.

   Deux choses bougent, et il faut les deux. L'ANGLE du dégradé, parce que
   la source ne tourne pas avec l'objet : vue depuis la face, elle semble
   pivoter en sens inverse. Et sa POSITION, parce qu'un reflet spéculaire
   se déplace deux fois plus vite que la surface qui le porte — c'est la
   loi de la réflexion, et c'est ce qui fait qu'un reflet « file » sur une
   carrosserie qu'on contourne.

   `--tr` va de +1 à −1 ; l'inclinaison va donc de 96° à 128°, et le
   balayage traverse la face. */
.studio-globe .media.portrait::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(calc(112deg + var(--tr, 0) * -16deg),
    rgba(255, 255, 255, 0.17) 0%,
    rgba(255, 255, 255, 0.05) 22%,
    rgba(255, 255, 255, 0) 46%,
    rgba(0, 0, 0, 0.30) 100%);
  transform: translateX(calc(var(--tr, 0) * 26%));
}

/* LA LÉGENDE EST DANS LE PLAN DU BOÎTIER, pas devant lui.

   Elle contre-tournait, pour rester lisible de face. C'était défendable
   pour une étiquette, et faux pour celle-ci : elle est posée sous l'objet,
   à sa base, et un texte qui reste horizontal pendant que l'objet pivote
   se détache de lui — il devient une incrustation de l'interface au lieu
   d'une inscription sur la chose.

   Elle n'a donc plus de rotation propre. Enfant du plateau dans un
   contexte `preserve-3d`, elle suit sa pose sans qu'on ait rien à écrire :
   le pivot, la plongée et la perspective lui arrivent par héritage. C'est
   aussi ce qui règle définitivement la divergence des formules — non pas
   en accordant deux expressions, mais en n'en laissant qu'une.

   Reste 2 px d'avance sur le plan. Ils ne se voient pas — 0,9 px de
   décalage latéral à 28° — et ils évitent que la légende et le reflet, qui
   se chevauchent et sont rigoureusement coplanaires, se disputent l'ordre
   de tracé. L'ancienne valeur, 30 px, déportait la légende de 14 px : elle
   flottait devant le boîtier. */
.studio-globe figcaption {
  transform: translateZ(2px);
}

@media (prefers-reduced-motion: reduce) {
  .plateau { transform: none; opacity: 1; }
  .boitier-tranche { display: none; }
  .studio-globe figcaption { transform: none; }
}



/* ---------- Le reflet au sol ---------- */

/* Le carrousel en a un sous chacune de ses douze pochettes ; le portrait
   n'en avait pas. C'est le genre d'écart qui fait qu'une page paraît
   assemblée plutôt que dessinée.

   Il est DANS le boîtier, donc emporté par sa rotation : un reflet qui ne
   tournerait pas avec l'objet le trahirait au premier degré. Et il est
   comprimé à 0,72 — un sol renvoie une image en raccourci, seule une vitre
   la rend à sa taille. */
.boitier-reflet {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 34%;
  overflow: hidden;
  pointer-events: none;
  --k: 0.72;
  transform-origin: 50% 0;
  /* LE REFLET TOUCHE LA PHOTO. Écart mesuré : 0,00 px.

     `transform-origin: 50% 0` avec un scaleY négatif renvoie la boîte VERS
     LE HAUT ; c'est `translateY(--k * 100%)` qui la ramène exactement sous
     son point de départ. Cette translation suffit, et elle est exacte —
     elle est écrite dans la même unité que la compression qu'elle annule.

     Il y avait ici une cale de 8 px, justifiée en commentaire par un
     chevauchement d'arrondi. Ce chevauchement n'existe pas : la mesure
     donne 0,00 px de contact sans elle, et 8,00 px d'écart avec. La cale
     ne corrigeait pas le défaut, elle en était un — et son commentaire,
     écrit avec assurance, a servi d'alibi jusqu'à ce qu'on regarde. */
  transform: translateY(calc(var(--k) * 100%))
             scaleY(calc(-1 * var(--k)));
  /* Il disparaît pendant l'arrivée : l'objet n'a pas encore touché le sol,
     il ne peut pas s'y refléter. */
  /* 0,44 au lieu de 0,26 : le reflet n'est plus un détail mais l'un des
     trois éléments qui portent le volume. Et lui AJOUTE de la lumière —
     donc il se voit, contrairement à l'ombre qu'il remplace. */
  opacity: calc(0.44 * (1 - var(--a, 0)));
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%,
    rgba(0,0,0,0.08) 24%, rgba(0,0,0,0.38) 56%, rgba(0,0,0,0.74) 80%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0%,
    rgba(0,0,0,0.08) 24%, rgba(0,0,0,0.38) 56%, rgba(0,0,0,0.74) 80%, #000 100%);
}
.boitier-reflet img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Le BAS de la photo, pas son milieu : un reflet montre ce qui touche le
     sol, et après le retournement c'est ce bas qui se retrouve au contact. */
  object-position: 50% 100%;
  display: block;
  filter: blur(2.2px) saturate(0.6) brightness(0.78);
}

/* L'ombre de contact au sol : une tache sombre et courte, à la base de
   l'objet. Déportée à droite comme l'ombre portée, et rétrécie quand
   l'objet est encore en l'air. */
.boitier-ombre {
  position: absolute;
  left: 4%;
  right: -6%;
  top: 100%;
  height: 26px;
  pointer-events: none;
  background: radial-gradient(58% 100% at 46% 0%,
    rgba(0, 0, 0, 0.9) 0%, transparent 74%);
  transform: scaleX(calc(1 + var(--a, 0) * 0.28));
  opacity: calc(1 - var(--a, 0) * 0.55);
}

@media (prefers-reduced-motion: reduce) {
  .boitier-reflet { display: none; }
}


/* =========================================================
   L'ARRIVÉE DE LA SECTION
   =========================================================

   Elle avait trois mécaniques pour quatre objets : un ressort pour le
   boîtier, une transition CSS pour le texte, rien pour le globe — qui
   était simplement là. C'est le signal « assemblé » le plus fort qui
   restait : un élément a une belle entrée, les autres montent de 28 px
   avec une autre courbe, et la sphère ne participe pas.

   Un seul avancement pour toute la scène, `--ar`, calculé une fois. Chaque
   élément porte un `--rang` et en déduit son propre retard :

     ar(rang) = ar × (1 + rang × 0,26)  borné à [0, 1]

   Le rang ne décale donc pas dans le TEMPS mais dans l'AVANCEMENT : le
   globe (rang 0) est arrivé quand le texte (rang 2) commence à peine. Un
   retard temporel ferait démarrer tout le monde ensemble et finir en
   décalé ; là, ils démarrent en décalé et finissent ensemble — ce qui est
   ce qu'on lit comme une composition et non comme une file d'attente.

   Et une seule courbe pour tout le monde, celle du carrousel. Le ressort
   du script produit l'avancement ; les formes en sont des fonctions. */
[data-scene] { --pas: 0.26; }

.studio-sphere,
.plateau,
.entre {
  --a: clamp(0, calc((var(--ar, 0) * (1 + var(--rang, 0) * var(--pas)))), 1);
}

/* LE GLOBE ARRIVE AUSSI.

   Il entre un peu plus grand et transparent — un objet lointain qui se
   rapproche, pas un calque qui apparaît. L'échelle est le seul paramètre
   qui donne la distance sans bouger l'objet de sa place. */
/* SEULEMENT L'ÉCHELLE — le centrage reste à la propriété `translate`.

   J'avais écrit `transform: translate(0,-50%) scale(...)` alors que la
   règle de placement utilise déjà la propriété `translate: 0 -50%`. Or ce
   sont deux propriétés DISTINCTES, appliquées l'une après l'autre : le
   globe se retrouvait décalé de −100 % au lieu de −50, soit 414 px trop
   haut, hors de son conteneur.

   `translate`, `rotate` et `scale` existent comme propriétés autonomes
   précisément pour qu'on puisse les animer séparément. Les mélanger avec
   `transform` revient à écrire deux fois la même chose et à espérer qu'une
   des deux gagne — elles s'additionnent. */
.studio-sphere {
  scale: calc(1 + var(--a, 0) * 0.09);
  opacity: calc(0.62 * (1 - var(--a, 0) * 0.92));
  transition: none;
}

/* Le texte suit la même course, avec la courbe du carrousel. */
.entre {
  opacity: calc(1 - var(--a, 0));
  transform: translateY(calc(var(--a, 0) * 26px));
}

@media (prefers-reduced-motion: reduce) {
  .studio-sphere { scale: 1; opacity: 0.62; }
  .entre { opacity: 1; transform: none; }
}


/* L'aimant n'est plus en CSS — voir v29.js.

   `scroll-snap-type: proximity` laisse le navigateur choisir à quelle
   distance il recale, et cette distance n'est pas normalisée. Mesuré ici :
   817 px séparent l'entrée de la section de son point de centrage, et
   aucune implémentation ne couvre ça. L'aimant se déclenchait en sortie,
   où l'on est déjà près, et jamais à l'arrivée — exactement ce qui a été
   constaté.

   Le seuil doit donc être choisi, pas subi. */

/* Le rembourrage de la section, réduit pour elle seule.

   `.section` en donne 158 px de chaque côté — une valeur juste pour une
   section de texte, excessive autour d'un bloc qui a déjà son propre air.
   Ajoutés au bloc, ils faisaient une section de 1 077 px dont 316 de
   marges pour 356 de contenu. */
#studio {
  padding-block: clamp(48px, 7vw, 104px);
}


/* =========================================================
   LA NAPPE DE LUMIÈRE — sans elle, aucune ombre n'existe
   =========================================================

   Mesuré : le fond de la section vaut 0,00 de luminance, et l'ombre du
   boîtier aussi. Une ombre est une SOUSTRACTION de lumière ; sur du noir
   il n'y a rien à soustraire. Tout le travail fait sur la portée, le
   contact et l'angle était rigoureusement invisible.

   C'est le piège propre aux interfaces sombres, et il ne se corrige pas en
   forçant l'ombre — elle est déjà à l'opacité maximale. Il faut donner au
   boîtier quelque chose sur quoi se projeter : une nappe de lumière très
   basse, posée derrière lui, qui rend au fond de quoi être obscurci.

   Deux couches, et elles ne font pas la même chose.

   LA NAPPE, large et froide : l'éclairage d'ambiance de la scène. Elle
   monte à peine à 5 % de blanc — au-delà on voit un halo, et un halo sur
   fond noir se lit comme une tache, pas comme de la lumière.

   LE SOL, une bande horizontale sous le boîtier. C'est elle qui reçoit
   l'ombre de contact et le reflet, et c'est elle qui fait qu'il y a un
   SOL. Sans surface, un objet ne se pose sur rien, et l'ombre la mieux
   dessinée ne décrit que du vide.

   Toutes deux suivent l'arrivée : la lumière se lève avec l'objet. */
.studio-globe::before {
  content: "";
  position: absolute;
  left: -12%;
  right: -6%;
  top: -14%;
  bottom: -22%;
  z-index: -1;
  pointer-events: none;
  /* LE VIGNETTAGE, seule chose que le ::before porte encore : le fond est
     désormais une vidéo, et il faut en éteindre les bords pour qu'elle ne
     se lise pas comme une image posée dans un cadre. */
  background: radial-gradient(120% 140% at 50% 50%,
    transparent 20%, rgba(0, 0, 0, 0.5) 100%);
  opacity: calc(1 - var(--ar, 0) * 0.85);
}

/* ================= LE FOND EST LE FILM =================

   RETENU AU BANC CONTRE TRENTE-TROIS AUTRES, et le seul qui ne soit pas une
   rampe mathématique.

   Toutes les tentatives précédentes étaient des dégradés — lisses par
   construction, dérivée continue, aucun accident. Aucune lumière réelle n'a
   cette propriété, et le lisse parfait est ce que l'œil lit comme
   synthétique. Une image de film floutée à 64 px n'est plus lisible, mais
   elle garde ce qu'aucune formule ne fabrique : des masses inégales, des
   chutes irrégulières, et surtout un MOUVEMENT LENT qui ne boucle pas à
   l'œil. Le fond cesse d'être une surface pour devenir une pièce où quelque
   chose se passe.

   C'est aussi le seul qui ne coûte aucun élément nouveau : le fichier est
   celui du hero, déjà en cache quand on arrive ici. */
.studio-fond {
  position: absolute;
  top: -14%;
  bottom: -22%;
  /* Pleine largeur de fenêtre et non de colonne : un fond qui s'arrête au
     bord du texte se voit comme un panneau. `main` porte `overflow-x: clip`,
     donc les 100vw ne rallongent pas la page. */
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  overflow: hidden;
  z-index: -2;
  pointer-events: none;
  background: #050506;
  /* Le fondu haut et bas supprime la couture avec les sections voisines. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 86%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 86%, transparent 100%);
}
.studio-fond video {
  /* Le débord de 8 % n'est pas cosmétique : un flou de 64 px tire du vide
     depuis l'extérieur du cadre et poserait un liseré sombre sur les quatre
     bords. La marge doit dépasser le rayon du flou. */
  position: absolute;
  inset: -8%;
  width: 116%;
  height: 116%;
  object-fit: cover;
  filter: blur(64px) saturate(0.5) brightness(0.28);
  opacity: 0.55;
}

@media (prefers-reduced-motion: reduce) {
  /* L'affiche suffit : elle porte la même structure de lumière, sans le
     mouvement. `preload="none"` fait qu'elle est tout ce qui est chargé. */
  .studio-fond video { display: none; }
  .studio-fond {
    background: #050506 url("../work/hero-poster.jpg") center/cover no-repeat;
  }
}

/* Le boîtier reprend l'ombre qu'il peut enfin projeter. Elle était à
   l'opacité 1 par désespoir — sur une nappe à 5 %, une ombre à 100 % de
   noir creuse un trou net. Elle redescend à ce qu'elle doit être. */
/* PLUS D'OMBRE PORTÉE. On modèle par la lumière.

   Mesuré : sur ce fond, l'ombre creusait 0,65 unité sur 255 — sous le
   seuil de perception. Elle ne pouvait pas faire mieux : une ombre
   SOUSTRAIT de la lumière, et il n'y en avait presque pas à soustraire. Le
   défaut n'était ni son opacité ni sa taille, c'était le principe.

   Sur un fond noir on détache par le CLAIR. C'est la leçon du rebord des
   pochettes d'index20, où un liseré sombre rapprochait la carte de son
   fond au lieu de l'en séparer ; elle vaut ici pour l'objet entier.

   Ce qui dessine le boîtier désormais : son rebord, l'arête de sa tranche,
   et son reflet. Trois choses qui ajoutent de la lumière. */
.studio-globe .media.portrait {
  box-shadow: none;
}
.boitier-ombre { display: none; }


/* =========================================================
   LA PHOTO EN COULEUR
   ========================================================= */

/* Le noir et blanc ne venait pas d'ici : `style.css` pose
   `.portrait img { filter: grayscale(1) contrast(1.06) }` pour la vingtaine
   de pages du site. On ne le retire pas là-bas — on le neutralise ici, où la
   portée s'arrête à index25.

   Le filtre n'est pas mis à `none` : le contraste reste. Il ne servait pas la
   désaturation, il rattrapait le léger voile de la prise de vue, et la
   couleur ne le rend pas inutile.

   Au passage, ça répare une incohérence que le gris masquait. Le reflet au
   sol est un SECOND <img>, frère de `.portrait` et non son descendant :
   `.portrait img` ne l'a jamais atteint. Il était donc déjà en couleur sous
   un visage gris — un objet ne se reflète pas d'une autre teinte que
   lui-même. Les deux s'accordent enfin. */
.studio-globe .media.portrait img {
  filter: contrast(1.06);
}

/* Le survol perd sa raison d'être avec le gris : il ne servait qu'à rendre
   la couleur, désormais acquise. Restait son `scale(1.03)`, qui ici faisait
   grandir la face SEULE — la tranche et le reflet, eux, ne bougeaient pas.
   Le boîtier se disloquait au passage de la souris. */
.studio-globe .media.portrait:hover img {
  filter: contrast(1.06);
  transform: none;
}



/* =========================================================
   PARLONS-EN — LE VERRE SUR LE GLOBE
   ========================================================= */

/* Retenu au banc (test-contact.html, variante 14) contre dix-sept autres.

   CE QUI LA DISTINGUE DES SEPT AUTRES CARTES DE VERRE : elle a quelque
   chose derrière. `backdrop-filter` floute l'arrière-plan ; sur du noir uni
   il s'exécute et ne produit rien, et la carte ne tient plus que par son
   liseré — un cadre clair, pas du verre. C'est le même piège que l'ombre
   portée de la section studio, qui soustrayait de la lumière là où il n'y
   en avait pas à soustraire.

   Le globe est donc structurel et non décoratif. Il dit aussi « partout
   dans le monde » sans l'écrire, et reprend l'objet de la section
   précédente — la page se referme sur ce qu'elle a ouvert. */
.cta-verre {
  position: relative;
  border-top: 1px solid var(--line);
  padding-block: clamp(90px, 13vw, 180px);
  overflow: hidden;
}
/* LA NOTE EN HAUT À GAUCHE, reprise des étiquettes du banc d'essai.

   Une chasse fixe, 11 px, gris moyen, calée à gauche : le registre d'une
   note technique posée en marge, pas d'un argumentaire. C'est ce qui la
   rend crédible — elle ne cherche pas à convaincre, elle informe, et le
   lecteur la lit justement parce qu'elle ne lui vend rien.

   Elle ne concurrence pas la carte : la carte est centrée et lumineuse, la
   note est à plat contre le bord gauche. Deux niveaux de lecture au lieu de
   deux titres qui se disputent le même. */
.cv-note {
  margin: 0 0 clamp(30px, 4.4vw, 54px);
  color: #6f6f6f;
  font: 11px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.1em;
  /* 74 ch : au-delà, l'œil perd la ligne suivante en revenant à la marge.
     C'est la seule mesure qui compte pour un bloc de texte courant. */
  max-width: 74ch;
}

.cv {
  position: relative;
  display: grid;
  place-items: center;
  min-height: min(70vh, 540px);
}
.cv-globe {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: min(74vw, 760px);
  aspect-ratio: 1;
  /* 0,75 et non 0,62 comme celui du studio : celui-ci passe SOUS un panneau
     qui le floute et l'assombrit. Il doit partir plus haut pour arriver au
     même endroit. */
  opacity: 0.75;
  z-index: 0;
  pointer-events: none;
}
.cv-globe canvas { width: 100%; height: 100%; display: block; }

.cv-carte {
  position: relative;
  z-index: 1;
  width: min(560px, 100%);
  padding: clamp(28px, 3.4vw, 46px);
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(22px) saturate(1.35);
  -webkit-backdrop-filter: blur(22px) saturate(1.35);
  /* Trois arêtes et non un contour uniforme, comme le boîtier du studio :
     le pourtour donne la forme, le bord haut prend la lumière du ciel, le
     bord gauche celle de la source. Un liseré régulier se lit comme un
     tracé, pas comme une épaisseur éclairée. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.24),
    inset 1px 0 0 rgba(255, 255, 255, 0.10),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}
.cv-carte .eyebrow { display: block; }
/* Le titre a une interligne de 0,92 : sa boîte est plus courte que ses
   lettres, et l'étiquette du premier champ venait se coller sous le jambage
   du « PARLONS-EN ». Une marge sur le formulaire et non sur le titre — le
   titre est réglé pour la page, l'écart appartient à la carte. */
.cv-carte form { margin-top: 30px; }
.cv-carte .btn { margin-top: 6px; }
.cv-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 0 22px; }
@media (max-width: 620px) { .cv-duo { grid-template-columns: 1fr; } }

/* SUR UN TÉLÉPHONE, LE VERRE N'AVAIT PLUS RIEN DERRIÈRE LUI.

   Le globe fait 74vw et la carte toute la colonne : mesuré sur 390 px, un
   globe de 289 px sous une carte de 350. Il était entièrement caché, et le
   flou de 22 px effaçait jusqu'à ses lignes — la carte redevenait le cadre
   clair sur fond uni que la section existe pour éviter (voir plus haut).

   À 150vw il sort des deux côtés de la carte, et un flou ramené à 10 px
   laisse deviner ses lignes à travers. Ce n'est pas un réglage de goût :
   c'est rendre au verre la matière qu'il doit traverser.

   LA NOTE PASSE DE 11 À 12,5 PX, ET S'ÉCLAIRCIT. Sur un bureau elle est une
   note en marge ; sur un téléphone elle est la seule phrase de la section,
   et elle porte sa seule promesse — la réponse sous 24 heures. La chasse
   fixe reste : c'est son registre.

   LA CARTE SE RESSERRE de 96 px — marge intérieure, écart des champs,
   hauteur de « L'idée ». C'est exactement ce qu'il fallait pour que la note,
   la carte et le bouton tiennent ensemble sur un iPhone 13 (664 px utiles),
   une fois la page posée par l'aimant (voir v30.js, point « contact »).
   Retenu sur maquette : artefact « Parlons-en au doigt ». */
@media (max-width: 640px) {
  .cv-globe { width: 150vw; opacity: 0.95; }
  .cv-carte {
    padding: 22px;
    backdrop-filter: blur(10px) saturate(1.3);
    -webkit-backdrop-filter: blur(10px) saturate(1.3);
  }
  .cv-carte form { margin-top: 20px; }
  .cv-carte .field { margin-bottom: 14px; }
  .cv-carte .field textarea { min-height: 84px; }
  .cv-note {
    margin-bottom: 20px;
    font-size: 12.5px;
    line-height: 1.6;
    letter-spacing: 0.01em;
    color: #a3a3a3;
  }
}

/* UN SECOND PALIER POUR LES ÉCRANS COURTS — iPhone SE, 568 px utiles.

   Le premier palier laissait le bouton 103 px sous le bas de l'écran. On
   gagne ce qui manque sur la carte seule : champs plus fins, « L'idée » à
   64 px, interligne de la note resserré. La mention « France · Available
   Worldwide » reste — une fois le reste resserré, elle tenait.

   Par la HAUTEUR et non la largeur : c'est elle qui manque. Un iPhone 13, à
   664 px, n'est pas concerné. */
@media (max-width: 640px) and (max-height: 640px) {
  .cv-note { margin-bottom: 14px; line-height: 1.5; }
  .cv-carte { padding: 18px; }
  .cv-carte form { margin-top: 14px; }
  .cv-carte .field { margin-bottom: 10px; }
  .cv-carte .field label { margin-bottom: 4px; }
  .cv-carte .field input,
  .cv-carte .field textarea { padding-block: 8px; }
  .cv-carte .field textarea { min-height: 64px; }
}

/* PAS DE GRAIN ICI, ET C'EST UNE CORRECTION.

   J'en avais posé un contre le banding, annoncé comme un remède. Deux
   mesures l'ont défait. D'abord il était INERTE : `overlay` se comporte
   comme un `multiply` quand le fond est sous 50 % de luminosité, donc il
   annulait le bruit là où il n'y a presque pas de lumière — 27 niveaux
   sans lui, 28 avec, luminance +0,02.

   Ensuite, réglé sur le seul mélange qui agisse — `soft-light` à 0,3 —, il
   se lit comme une photo bruitée : le fond monte en gris et la texture se
   voit. Mes chiffres disaient « écart-type local 0,94 », l'œil dit
   « sale ».

   Enfin il ne réparait rien : les bandes mesurent 2,7 px, elles ne se
   voient pas. Un remède sans maladie. */

@media (prefers-reduced-motion: reduce) {
  .cv-globe { opacity: 0.45; }
}



/* =========================================================
   AU DOIGT — CE QUI NE SE VOIT QU'UNE FOIS SUR UN TÉLÉPHONE
   ========================================================= */

/* DEUX CONDITIONS, ET LA VIRGULE EST UN « OU ».

   `pointer: coarse` pose la bonne question — une tablette large se pilote
   au doigt et a besoin de ces règles, une fenêtre étroite sur un ordinateur
   garde une souris précise et n'en a pas besoin. Mais tous les navigateurs
   ne renseignent pas ce trait honnêtement, et un téléphone qui se déclare
   « fin » retomberait dans le piège.

   La largeur sert donc de filet. Elle rattrape aussi ma propre incapacité à
   vérifier : Chrome sans tête se déclare pointeur fin, et la première
   version de ce bloc n'a produit aucun effet mesurable — la règle était
   juste, mais rien ne le prouvait. */

@media (pointer: coarse), (max-width: 640px) {

  /* LE ZOOM AUTOMATIQUE D'IOS, qui est un vrai piège et pas une coquetterie.

     Safari agrandit la page dès qu'on donne le focus à un champ dont le
     texte fait MOINS DE 16 px. La page reste zoomée après coup, devient
     défilable latéralement, et le visiteur se retrouve à pincer pour
     revenir — au milieu d'un formulaire de contact, c'est-à-dire à
     l'endroit exact où on lui demande un effort.

     Mesuré ici : quatre champs sous le seuil, trois à 15 px et
     l'inscription à la lettre à 14. Ils passent à 16, et seulement au
     doigt : sur un écran de bureau, 15 px reste le bon corps. */
  .field input,
  .field select,
  .field textarea,
  .newsletter input {
    font-size: 16px;
  }

  /* LES CIBLES TROP PETITES.

     Le bouton du menu mesurait 34 × 18 px — la commande principale de
     navigation sur téléphone, deux fois trop petite dans sa hauteur.
     Mesuré aussi : « Voir les films » à 28 px de haut, le bouton du son à
     29, la sortie du carrousel à 37. La recommandation courante est 44.

     On agrandit la ZONE et non le dessin. Un pseudo-élément débordant
     étend la surface sensible sans déplacer d'un pixel ce qu'on voit —
     et surtout sans toucher aux barres du menu, qui sont positionnées en
     absolu depuis le bord du bouton et suivraient une marge intérieure. */
  .burger { position: relative; }
  .burger::after {
    content: "";
    position: absolute;
    inset: -14px -6px;      /* 18 + 28 = 46 de haut, 34 + 12 = 46 de large */
  }
  /* `.cf-sortie` N'EST PAS DANS LA LISTE, ET ELLE N'Y DOIT PAS ÊTRE.

     Elle est déjà en `position: absolute` — ancrée en bas à droite du cadre
     épinglé — et un élément absolu sert de repère à son `::after` aussi bien
     qu'un élément relatif. L'y inclure la faisait RETOMBER dans le flux :
     mesuré sur 390 px, une pastille de toute la largeur, coupée par le bord
     gauche, posée en travers du carrousel. */
  .hero-cta, .sound-btn {
    position: relative;
  }
  .hero-cta::after, .sound-btn::after, .cf-sortie::after {
    content: "";
    position: absolute;
    inset: -10px -4px;
    /* La zone doit passer DEVANT ses voisins, sinon elle ne sert à rien.
       Mesuré : sans cette ligne, « Voir les films » plafonnait à 37 px de
       haut — la flèche de défilement, posée juste dessous et peinte après,
       remportait le test de contact sur la moitié basse de la zone. Une
       surface sensible recouverte par un ornement n'est pas une surface
       sensible. */
    z-index: 3;
  }
  .brand::after {
    content: "";
    position: absolute;
    inset: -11px;
  }
  .brand { position: relative; }
}



/* =========================================================
   L'ENTRÉE DU HERO — TOUT NAÎT DU FILET
   ========================================================= */

/* LE FILET EXISTE DÉJÀ, ET C'EST TOUT L'INTÉRÊT.

   Il sépare le nom de la phrase depuis le début. Le faire grandir, puis
   ouvrir les deux blocs à partir de lui, ne fabrique aucun élément pour
   l'occasion : le geste naît de la maquette au lieu de s'y ajouter. Un
   trait tracé pour l'animation puis effacé aurait dit « décor » ; celui-ci
   reste après, et l'on comprend rétrospectivement qu'il était l'origine.

   TROIS TEMPS. Le filet s'ouvre de son milieu vers ses deux extrémités.
   Le nom se découvre ensuite vers la GAUCHE, la phrase vers la DROITE :
   les deux s'écartent du filet, comme deux volets. Le filet, plus clair
   pendant le geste, retombe enfin à sa valeur de repos.

   Les deux volets partent ensemble et non l'un après l'autre : ils sont
   les deux moitiés d'un même mouvement d'ouverture, et les décaler
   casserait la symétrie qui fait tout l'effet. */

/* Le filet quitte la bordure pour devenir un élément à part entière.
   Immobile, il est identique — même largeur, même couleur, même place. */
.hero-phrase { position: relative; border-left: 0; }
.hero-phrase::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.2);
  transform: scaleY(0);
  transform-origin: 50% 50%;
}

/* L'ÉTAT CACHÉ EST POSÉ PAR LE SCRIPT, JAMAIS PAR LA FEUILLE SEULE.

   Écrit directement ici, il masquerait le nom du studio pour quiconque
   n'exécute pas le script — un titre de page invisible faute de JavaScript
   est un défaut qu'aucune animation ne justifie. `hero-prete` n'est posé
   que si le script tourne, donc il ne peut cacher que ce qu'il saura
   révéler. */
body.hero-prete .hero-verrou { clip-path: inset(0 0 0 100%); opacity: 0; }
/* `inline-block` : SANS LUI, LA PHRASE N'AFFICHE QUE SA PREMIERE LIGNE.

   Constate a l'ecran, puis isole en comparant trois rendus : tel quel une
   ligne, sans `clip-path` une ligne encore, en `inline-block` les deux.
   Ce n'est donc pas la decoupe.

   `heroVoletD` anime `filter: blur(6px)` vers `blur(0)`, et son
   remplissage `forwards` LAISSE le filtre pose une fois l'entree finie —
   `blur(0)` reste un filtre. Or un filtre sur un element INLINE qui court
   sur plusieurs lignes ne s'applique qu'a son premier fragment : la
   seconde ligne existe dans la mise en page, occupe sa hauteur, et n'est
   jamais peinte. Rien ne le signale, ni dans l'inspecteur ni dans le DOM,
   et la boite mesuree fait bien deux lignes.

   Le defaut est ancien et il portait sur la phrase de marque, dont le
   commentaire ci-dessous dit que la coupure en deux lignes EST la
   structure : « la negation sur la premiere ligne, l'affirmation sur la
   seconde ». On n'en voyait que la negation.

   `inline-block` donne au span une boite unique : le filtre, la decoupe et
   l'ombre portent alors sur l'ensemble. Rien d'autre ne bouge — le span
   reste a sa place dans le paragraphe, qui garde son filet. */
body.hero-prete .hero-phrase > span { clip-path: inset(0 100% 0 0); opacity: 0; }
.hero-phrase > span { display: inline-block; }

/* LETTRE À LETTRE — retenue au banc contre dix-neuf autres, à moitié vitesse.

   Le filet s'ouvre, puis les lettres du nom arrivent une à une — DE LA PLUS
   PROCHE DU FILET À LA PLUS LOINTAINE. C'est ce sens qui fait tout : le
   nom se construit en s'éloignant de son origine, au lieu de s'écrire de
   gauche à droite comme une machine. Le filet n'est pas le décor du geste,
   il en est la cause.

   La phrase, elle, s'ouvre d'un seul volet depuis le filet. Deux gestes
   lettre à lettre se seraient concurrencés ; ici l'un détaille, l'autre
   accompagne.

   Les durées sont celles du banc divisées par deux, sur choix du visiteur.
   À pleine vitesse on lisait « le nom apparaît » ; à moitié on lit chaque
   lettre se poser, et le geste dure assez pour qu'on le regarde plutôt
   qu'on le remarque. */

/* ============ LE GESTE EST CALÉ SUR LES COUPES DU FILM ============

   Le film du hero dure 8,16 s et coupe seize fois. Ses coupes ont été
   relevées image par image, seuil de scène 0,30 :

     1,52 · [1,88 1,92 1,96 2,00] · 3,00 · 3,24 · 3,56 · 4,68 · 5,20 …

   LE FILM A ÉTÉ REMONTÉ SANS QUE CE CALAGE BOUGE, et ça valait d'être
   vérifié plutôt que supposé. La version précédente durait 8,94 s ; le
   nouveau montage est plus court de 780 ms et diffère à partir de 5,6 s.
   Mais ses sept premières coupes sont aux mêmes instants, donc les quatre
   temps ci-dessous tombent encore à 0 ms de leur coupe. Rien n'a été
   re-réglé — c'est la mesure qui l'a dit, pas la chance.

   La rafale de quatre entre 1,88 et 2,00 est un stroboscope de quatre
   images, pas quatre plans.

   CE QUI EST CALÉ, C'EST LA FIN DE CHAQUE GESTE, JAMAIS SON DÉBUT. Un
   départ tombe dans le vide — on ne l'a pas encore vu partir quand il
   part. C'est l'arrivée qu'on regarde, et c'est elle qui doit coïncider
   avec la coupe : l'image change à l'instant précis où l'écriture se
   pose. Les retards ci-dessous sont donc tous obtenus en soustrayant la
   durée de l'animation de l'instant de la coupe visée.

     filet à pleine hauteur   416 + 46 % × 2400  = 1520 ms → coupe 1,52
     dernière lettre posée   1760 +      1240    = 3000 ms → coupe 3,00
     le carré signe          2000 +      1240    = 3240 ms → coupe 3,24
     la phrase ouverte       1360 +      2200    = 3560 ms → coupe 3,56

   Quatre coupes de suite, une par geste, dans l'ordre. Ce n'est pas une
   coïncidence trouvée après coup : les retards ont été déplacés jusqu'à
   ce qu'ils tombent dessus, l'écart au réglage précédent valant partout
   entre +200 et +320 ms.

   ET LE FILM EST REMIS À ZÉRO QUAND LE GESTE PART. Sans ça tout ce qui
   précède est faux : la vidéo démarre au chargement, le geste part à
   l'ouverture de la porte, et l'écart entre les deux vaut ce que vaut le
   réseau du visiteur. Le script les fait donc partir du même instant —
   voir `LE DÉCLENCHEUR DE L'ENTRÉE DU HERO`. */

body.hero-entre .hero-phrase::before {
  /* 416 ms de retard : le filet n'existe pas pendant la demi-seconde où le
     film pose son plan d'ouverture. Il pousse ensuite pendant 1104 ms et
     touche sa pleine hauteur pile sur la première coupe.

     `both` ET NON `forwards`, ET C'EST LE RETARD QUI L'EXIGE. Les autres
     éléments du geste sont cachés par `hero-prete` pendant leur attente ;
     le filet, lui, est un pseudo-élément qu'aucune règle ne cache — il
     tenait son état de départ du seul fait que l'animation commençait à
     l'instant zéro. Avec 416 ms de retard et `forwards`, il serait resté
     à pleine hauteur pendant l'attente pour retomber à zéro au moment de
     pousser : le geste joué à l'envers. `backwards` applique l'image clé
     de départ pendant le retard, et `both` ajoute la fin qu'on avait
     déjà. */
  animation: heroFilet 2400ms var(--courbe, ease) 416ms both;
}

/* Le verrou n'a plus de découpe : ce sont ses lettres qui arrivent. */
body.hero-entre .hero-verrou { opacity: 1; clip-path: none; }

/* LES LETTRES SONT DES `path`, ET LE GESTE NE CHANGE PAS POUR AUTANT.

   Le script pose `--d` sur les chemins du trace comme il le posait sur les
   `i` du texte. Les deux selecteurs cohabitent : `i` sert encore aux pages
   qui portent un nom en texte, `path` au logotype. */
body.hero-prete .hero-nom i,
body.hero-prete .hero-mot path { opacity: 0; }
body.hero-entre .hero-nom i {
  display: inline-block;
  font-style: normal;
  animation: heroLettre 1240ms var(--courbe, ease) forwards;
  animation-delay: var(--d);
}
body.hero-entre .hero-mot path {
  animation: heroLettreFine 1240ms var(--courbe, ease) forwards;
  animation-delay: var(--d);
}

/* UN FLOU DE 5 PIXELS EFFACE UN TRACE FIN, ET CE N'EST PAS UNE PRECAUTION
   MAIS UN CONSTAT.

   `heroLettre` floute chaque lettre de 5 px et la decale de 14. Sur le fut
   plein de Panchang a 25 px, ca se lit comme une lettre qui se pose. Sur ce
   trace, dont le fut fait un peu plus d'un pixel a la taille du hero, il
   n'y a pas assez de matiere : le mot se dissout en taches. Vu et mesure au
   banc (test-logo.html, section 4), pas suppose.

   Le flou est donc ramene a 1,6 px et le decalage a 8 : le rapport entre le
   flou et l'epaisseur du trait redevient celui qu'il avait sur la police.
   Le reste — duree, courbe, opacite — est inchange, donc le geste est le
   meme, seule son amplitude suit le dessin. */
@keyframes heroLettreFine {
  from { opacity: 0; transform: translateX(8px); filter: blur(1.6px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* L'ETOILE ET LA LIGNE PRODUCTION ARRIVENT ENSEMBLE, ET C'EST VOULU.

   Elles partent en dernier, apres la derniere lettre du mot : elles
   signent, et une signature ne precede pas ce qu'elle signe. Toutes deux
   se posent sur la coupe de 3,24 s, soit 240 ms apres le mot —
   l'intervalle exact que le monteur a laisse entre ses deux plans.

   POURQUOI PAS UN ESCALIER DE PLUS. On aurait pu donner une coupe a
   chacune. Mais le signe et le descripteur ne sont pas deux gestes : ils
   sont les deux moities d'une meme chose, l'identite qui se referme
   autour du nom. Les separer aurait fait trois arrivees pour un seul
   objet, et la coupe suivante du film est a 4,68 s — une seconde et demie
   plus loin, ce qui aurait laisse l'ecriture en suspens.

   Deux images cles et non une : l'etoile finit a 0,92 d'opacite, le
   descripteur a 0,85. Partager une seule animation obligerait a rattraper
   l'ecart par une regle ordinaire, que le remplissage `forwards` de
   l'animation ecraserait en silence — le piege documente plus bas. */
/* LE SIGNE ET LA LIGNE PRODUCTION ARRIVENT ENSEMBLE.

   Ils partent en dernier, apres la derniere lettre du mot : ils signent,
   et une signature ne precede pas ce qu'elle signe. Tous deux se posent
   sur la coupe de 3,24 s, soit 240 ms apres le mot — l'intervalle exact
   que le monteur a laisse entre ses deux plans.

   On aurait pu leur donner une coupe chacun. Mais le signe et le
   descripteur ne sont pas deux gestes : ce sont les deux moities de la
   meme chose, l'identite qui se referme autour du nom. Et la coupe
   suivante du film est a 4,68 s — une seconde et demie plus loin, ce qui
   aurait laisse l'ecriture en suspens. */
body.hero-prete .hero-sous,
body.hero-prete .hero-signe { opacity: 0; }
body.hero-entre .hero-sous {
  animation: heroSigne 1240ms var(--courbe, ease) 2000ms forwards;
}
body.hero-entre .hero-signe {
  animation: heroMixte 1240ms var(--courbe, ease) 2000ms forwards;
}
/* Il arrive en s'ecartant plutot qu'en glissant : un signe qui vient de la
   droite se lit comme une lettre de plus, et il n'en est pas une. */
@keyframes heroMixte {
  from { opacity: 0; transform: scaleX(0.82); filter: blur(1.6px); }
  to   { opacity: 0.92; transform: none; filter: blur(0); }
}
@keyframes heroSigne {
  from { opacity: 0; transform: translateX(8px); filter: blur(1.6px); }
  to   { opacity: 0.85; transform: none; filter: blur(0); }
}

/* LA PHRASE PART AVEC LA LIGNE PRODUCTION, PAS AVEC LE NOM.

   Elle s'ouvrait à 1360 ms, c'est-à-dire pendant que le mot s'écrivait
   encore — les deux gestes se chevauchaient sur près de deux secondes.
   Elle part maintenant à 2000 ms, l'instant EXACT où le signe et la ligne
   PRODUCTION commencent leur propre arrivée. Le nom est alors presque
   posé, et ce qui reste — le signe, le descripteur, la phrase — se met en
   mouvement d'un seul coup, comme une seconde phase.

   1560 ms, ET C'EST LA LISTE DES COUPES QUI L'ÉCRIT. Partant de 2000, la
   phrase peut se poser sur 3,00 (trop tôt, elle doublerait la fin du mot),
   sur 3,24 (elle finirait pile avec PRODUCTION, donc on ne verrait qu'un
   seul geste) ou sur 3,56. C'est la troisième : elle reste la DERNIÈRE
   posée, 320 ms après le descripteur — le même intervalle que le monteur a
   laissé entre ses deux plans à 3,24 et 3,56.

   ET ÇA REMET `hero-fini` À 3560. La classe est posée sur la fin de cette
   animation ; la version précédente la repoussait à 4680, ce qui retardait
   d'autant le grésillement, le pouls du nom et le réveil de l'aimant du
   héros. Le retard est annulé. */
body.hero-entre .hero-phrase > span {
  animation: heroVoletD 1560ms var(--courbe, ease) 2000ms forwards;
}

@keyframes heroVoletD {
  from { clip-path: inset(0 100% 0 0); opacity: 0; filter: blur(6px); }
  to   { clip-path: inset(-40% -40% -40% -6%); opacity: 1; filter: blur(0); }
}

@keyframes heroFilet {
  0%   { transform: scaleY(0); background: rgba(255, 255, 255, 0.85);
         box-shadow: 0 0 14px rgba(255, 255, 255, 0.5); }
  46%  { transform: scaleY(1); background: rgba(255, 255, 255, 0.85);
         box-shadow: 0 0 14px rgba(255, 255, 255, 0.5); }
  100% { transform: scaleY(1); background: rgba(255, 255, 255, 0.2);
         box-shadow: 0 0 0 rgba(255, 255, 255, 0); }
}

/* Le décalage est vers la DROITE, donc vers le filet : chaque lettre vient
   de lui. Un décalage vers la gauche l'aurait fait fuir son origine. */
@keyframes heroLettre {
  from { opacity: 0; transform: translateX(14px); filter: blur(5px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* =========================================================
   LE FILET BAT AVEC LA MUSIQUE
   ========================================================= */

/* `--pouls` va de 0 à 1 et vaut l'énergie des basses, écrite par le script
   à chaque image. Il ne s'applique QU'APRÈS l'entrée : pendant le geste,
   l'animation possède le filet, et deux sources sur la même propriété se
   voleraient la main.

   Le filet est le bon porteur, et c'est ce qui évite le gadget : il est
   déjà l'origine de tout le bloc, donc le faire respirer prolonge le geste
   d'entrée au lieu d'ajouter un effet. Une pastille qui clignote quelque
   part serait un décor ; ici c'est la même ligne qui continue de vivre.

   Rien ne bat quand le son est coupé : l'analyseur rend zéro, `--pouls`
   retombe, et le filet reprend exactement son aspect de repos. Le réglage
   du son n'a donc rien à piloter — l'absence de musique EST l'état neutre. */
body.hero-fini .hero-phrase::before {
  animation: none;
  transform: scaleY(1);
  background: rgba(255, 255, 255, calc(0.2 + var(--pouls, 0) * 0.55));
  box-shadow: 0 0 calc(var(--pouls, 0) * 20px)
              rgba(255, 255, 255, calc(var(--pouls, 0) * 0.5));
}
/* ---------- LA TRAÎNÉE SYMÉTRIQUE, APRÈS L'ENTRÉE ----------

   Quatre copies du nom, deux de chaque côté, de plus en plus faibles à
   mesure qu'elles s'éloignent. Le mot ne se déplace pas : il vibre.

   LA SYMÉTRIE EST LE CHOIX, ET ELLE DIT AUTRE CHOSE QU'UNE TRAÎNÉE SIMPLE.
   Des échos d'un seul côté racontent un mouvement — un objet qui file, et
   l'obturateur qui le suit mal. Des échos des deux côtés racontent une
   VIBRATION : rien ne se déplace, tout tremble sur place. C'est le geste
   d'un haut-parleur, pas celui d'une caméra — et sur une page où la musique
   commande, c'est la bonne métaphore.

   Le nom reste intact : contrairement à l'obturateur qui le découpait ou au
   flou qui le dissolvait, la lettre garde ses contours nets. Ce sont ses
   fantômes qui bougent.

   4,5 et 9 px, soit les 3 et 6 px du banc portés par l'intensité 1,50 que
   tu as retenue à l'écoute. Les opacités décroissent de moitié d'un écho au
   suivant : une décroissance linéaire donnerait quatre copies également
   présentes, donc un texte quadruple plutôt qu'une traînée.

   5,93 px et un flash à 0,53 : les valeurs trouvées à l'oreille sur la
   console d'index26, avec la musique et l'image. La traînée monte de 4,5 à
   5,93 — plus présente — pendant que l'éclat descend de 0,85 à 0,53. Les
   deux vont dans des sens opposés, ce qui a du sens : plus le nom vibre,
   moins le blanc a besoin de frapper fort pour qu'on sente le coup.

   `--trainee` reste réglable dans l'adresse. */
/* `text-shadow` NE S'APPLIQUE PAS A UN TRACE, ET N'AURAIT RIEN DIT.

   Le nom etait du texte ; il est devenu un SVG. `text-shadow` ne porte que
   sur des glyphes : laisse tel quel, il n'aurait produit aucune traînée et
   rien ne l'aurait signalé — la règle serait restée dans la feuille, valide
   et muette. `drop-shadow` suit la forme, donc il suit le tracé.

   L'AMPLITUDE SUIT L'EPAISSEUR DU TRAIT, ET J'AVAIS ESTIME CELLE-CI AU
   LIEU DE LA MESURER.

   J'ai d'abord divise la trainee par trois, en ecrivant que « le fut de ce
   trace fait un peu plus d'un pixel ». Il en fait 2,27 : 18 px de capitale
   pour un rapport de 16,5 sur 135. Et Panchang, que le trace remplace, a
   ete rendu puis mesure de la meme facon — 18 de fut pour 132 de capitale.
   Les deux sont donc a 1,12 pres, pas dans un rapport de trois.

   Le coefficient vaut 0,90 : l'inverse de ce 1,12. La trainee occupe la
   meme fraction du trait qu'avant le changement de nom, ce qui etait
   l'intention depuis le debut — je m'etais seulement trompe de facteur 2,6
   en estimant au lieu de mesurer.

   Le curseur `--trainee` continue de piloter l'ensemble ; c'est le
   coefficient qui porte le rapport, pas le reglage. */
body.hero-fini .hero-nom {
  filter:
    drop-shadow(calc(var(--pouls, 0) * var(--trainee, 5.93px) * -0.90) 0 rgba(255, 255, 255, 0.30))
    drop-shadow(calc(var(--pouls, 0) * var(--trainee, 5.93px) *  0.90) 0 rgba(255, 255, 255, 0.30))
    drop-shadow(calc(var(--pouls, 0) * var(--trainee, 5.93px) * -1.80) 0 rgba(255, 255, 255, 0.14))
    drop-shadow(calc(var(--pouls, 0) * var(--trainee, 5.93px) *  1.80) 0 rgba(255, 255, 255, 0.14));
}

/* LA PHRASE VIBRE AUSSI, MAIS PAS DE LA MÊME QUANTITÉ.

   Le même décalage sur les deux serait une erreur d'échelle. Le nom fait
   25 px, la phrase 14 : une traînée de 5,93 px représente un quart de la
   hauteur d'une capitale du nom, et près de la moitié pour la phrase — elle
   la réduirait en bouillie là où elle fait vibrer le nom.

   0,56, soit exactement le rapport des deux corps. La traînée occupe alors
   la même fraction de la lettre dans les deux cas, ce qui est la seule
   façon pour que les deux blocs paraissent bouger d'autant.

   Les opacités suivent le même raisonnement : la phrase est déjà à 62 % de
   blanc, donc ses fantômes descendent d'autant. Des échos plus contrastés
   que le texte qu'ils suivent se liraient comme un second texte.

   Un seul curseur pilote les deux : le rapport est dans la feuille, pas
   dans la console. Deux réglages indépendants inviteraient à les désaccorder,
   et rien ne justifie qu'ils le soient. */
body.hero-fini .hero-phrase > span {
  text-shadow:
    calc(var(--pouls, 0) * var(--trainee, 5.93px) * -0.56) 0 rgba(255, 255, 255, 0.19),
    calc(var(--pouls, 0) * var(--trainee, 5.93px) * 0.56)  0 rgba(255, 255, 255, 0.19),
    calc(var(--pouls, 0) * var(--trainee, 5.93px) * -1.12) 0 rgba(255, 255, 255, 0.09),
    calc(var(--pouls, 0) * var(--trainee, 5.93px) * 1.12)  0 rgba(255, 255, 255, 0.09);
}

/* L'animation d'entrée doit rendre la main sur les lettres, sinon son
   remplissage `forwards` garde la priorité sur toute déclaration ordinaire.
   Le piège ne se voit pas dans l'inspecteur : rien n'apparaît barré, la
   valeur est simplement ignorée. L'opacité est réaffirmée dans la foulée,
   sans quoi elle retomberait à l'état caché posé par `hero-prete`. */
body.hero-fini .hero-nom i { animation: none; opacity: 1; }
body.hero-fini .hero-mot path { animation: none; opacity: 1; }
body.hero-fini .hero-sous,
body.hero-fini .hero-signe { animation: none; }

/* La respiration du souffle est écrite plus bas, dans la même déclaration
   que le grésillement : deux règles sur `opacity` ne s'additionnent pas,
   la seconde gagne. Celle qui se trouvait ici ne faisait donc plus rien
   depuis que la seconde existait. */

@media (prefers-reduced-motion: reduce) {
  /* Le geste disparaît, le contenu reste. Ne rien montrer parce qu'on a
     demandé moins de mouvement serait punir un réglage d'accessibilité. */
  .hero-phrase::before { transform: scaleY(1); }
  body.hero-fini .hero-phrase::before {
    background: rgba(255, 255, 255, 0.2); box-shadow: none;
  }
  body.hero-fini .hero-sous { opacity: 0.85; }
  body.hero-fini .hero-signe { opacity: 0.92; }
  body.hero-fini .hero-nom,
  body.hero-fini .hero-phrase > span { text-shadow: none; }
  body.hero-prete .hero-verrou,
  body.hero-prete .hero-phrase > span,
  body.hero-prete .hero-nom i,
  body.hero-prete .hero-mot path,
  body.hero-prete .hero-signe,
  body.hero-prete .hero-sous { clip-path: none; opacity: 1; }
  body.hero-entre .hero-nom i,
  body.hero-entre .hero-mot path,
  body.hero-entre .hero-signe,
  body.hero-entre .hero-sous,
  body.hero-entre .hero-phrase > span,
  body.hero-entre .hero-phrase::before { animation: none; }
}



/* =========================================================
   LE NERF — RETIRÉ AVEC LE SON EMPRUNTÉ QUI LE PRODUISAIT
   ========================================================= */

/* IL Y AVAIT ICI UN FLASH BLANC ET UN CRAN DE ZOOM. Les deux étaient
   déclenchés par le flux spectral de `hero-son.m4a` — la bande de Solaris
   Auto, empruntée parce que le film du hero était muet. On fabriquait donc
   des coupes sur une image à partir d'une musique qui ne la connaissait
   pas : le blanc claquait sur une frappe de batterie, l'image sautait d'un
   cran, et le film continuait son propre montage à côté. Ça tenait tant
   qu'on ne les comparait pas.

   Le film actuel porte sa bande et coupe seize fois pour de bon. La raison
   d'être de la surcouche est partie avec le silence qu'elle comblait, et le
   procédé avec elle : plus de `.hero-flash`, plus de `--cran`, plus de
   `--eclat` ni de `--profondeur`. Ce qui reste du nerf, c'est `--pouls` —
   mais il lit maintenant le son DU film, quelques lignes plus haut, et le
   grésillement du tube lit ses coupes en regardant l'image elle-même. Rien
   n'est plus produit par un tiers.

   Ce qui a été supprimé, et où le retrouver s'il faut : `.hero-flash`
   (position absolue, `mix-blend-mode: screen`), `@keyframes heroEclat`
   (trois paliers en `steps`, 90 ms), et `.hero-bg .hero-video { scale }`
   qui composait le cran par-dessus le dézoom d'entrée sans y toucher.
   Le tout est intact dans v29.css. */



/* =========================================================
   LE NERF DU RAIL — LE GESTE FAIT OFFICE DE RYTHME
   ========================================================= */

/* LE RAIL EST SILENCIEUX, ET C'EST LE POINT DE DÉPART.

   La musique s'arrête avec la vidéo du hero dès qu'on quitte le haut de la
   page. Aucun des procédés du hero — la coupe, le flash, la traînée — n'est
   donc transposable ici : il n'y a rien à quoi réagir.

   Mais le rail a son propre rythme, et c'est LE GESTE qui le donne. Chaque
   changement de carte élue est un temps ; son tempo dépend de la main du
   visiteur. On marque donc le passage, comme on marquait la basse. */

/* ---------- LE COUP ---------- */

/* Un éclat bref sur la carte qui vient d'être élue. `steps` et non une
   courbe : un éclat qui s'atténue en douceur est une lueur, pas un coup.

   Il porte sur la LUMINOSITÉ de l'image et non sur un voile blanc posé
   dessus : un voile grise les noirs, alors qu'une montée de luminosité les
   laisse noirs et fait saturer les hautes lumières. C'est ce que fait une
   surexposition brève, et c'est ce qu'on reconnaît comme un flash. */
/* DEUX CLASSES QUI ALTERNENT, POUR NE PAS FORCER DE RECALCUL.

   Relancer une animation demande normalement de retirer la classe, de lire
   une propriété de mise en page pour vider la file, puis de la remettre. Ce
   recalcul forcé tombait à chaque changement de carte — sept fois par
   seconde en défilement rapide, au pire moment.

   Avec deux classes alternées, chaque coup utilise l'autre que le précédent :
   le changement est réel, l'animation repart, et rien n'est forcé. */
.cf-item.cf-coup-a .cf-face,
.cf-item.cf-coup-b .cf-face {
  animation: cfCoup 190ms steps(1, end);
}
@keyframes cfCoup {
  0%   { filter: brightness(1.75) contrast(1.06); }
  38%  { filter: brightness(1.28); }
  100% { filter: none; }
}

/* ---------- LA TRAÎNÉE ---------- */

/* `--vit` vaut 0 en dessous de deux cartes par seconde et 1 au plafond de
   cinq. Le seuil est l'essentiel : une traînée proportionnelle dès la
   première fraction de carte serait permanente, donc invisible comme effet
   et salissante comme rendu.

   L'ombre portée suit la boîte de la carte, donc sa forme exacte, coins
   arrondis compris — ce qu'aucun flou ne saurait faire. Deux échos, le
   second à moitié moins présent : une décroissance linéaire donnerait deux
   copies également visibles, donc trois cartes plutôt qu'une traînée.

   Ils sont posés du côté d'où l'on vient : la file monte vers la gauche
   quand on descend, donc le fantôme reste à droite. Une traînée en avant du
   mouvement se lirait comme une anticipation. */
.cf-card {
  /* L'OMBRE DE PROFONDEUR EST REPRISE EN PREMIER, ET C'EST INDISPENSABLE.

     `box-shadow` est une propriété unique : la redéclarer ne s'ajoute pas,
     elle REMPLACE. Ma première version ne portait que les deux échos, donc
     elle effaçait le `0 30px 60px -30px` qui donne aux cartes leur assise —
     un effet de vitesse payé par la perte de la profondeur, et le genre de
     régression qu'on ne voit pas tant qu'on regarde l'effet plutôt que la
     carte. */
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 1);
}

/* LA TRAÎNÉE PASSE PAR `drop-shadow` ET NON PAR `box-shadow`, ET LA RAISON
   EST UNE TRANSITION.

   `.cf-card` transitionne son ombre sur 0,72 s — réglage voulu, pour que
   l'élue prenne son assise en douceur quand elle grandit. Mais une traînée
   de vitesse qui met 720 ms à apparaître arrive après le geste : elle
   marquerait le moment où l'on ralentit, exactement l'inverse de ce qu'on
   veut. Et la retirer de la transition coûterait l'assise de l'élue.

   `filter` n'est pas transitionné ici, donc `drop-shadow` répond à l'image.
   Il suit en plus la forme réelle de la carte — coins arrondis compris —
   là où `box-shadow` suit sa boîte.

   Deux échos, le second à moitié moins présent : une décroissance linéaire
   donnerait deux copies également visibles, donc trois cartes plutôt qu'une
   traînée. Ils sont posés du côté d'où l'on vient. */
/* PAS D'EXTINCTION DES REFLETS. Essayée, mesurée, retirée.

   L'idée : les éteindre pendant la vitesse, puisque personne ne regarde le
   reflet d'une pochette qui file. Un essai alterné donnait 80 % d'images
   lourdes avec eux contre 67 sans, ce qui semblait valoir la peine.

   Mais les éteindre n'est pas les supprimer : une transition d'opacité sur
   vingt-quatre éléments les fait tous animer en même temps, au franchissement
   du seuil, c'est-à-dire au pire moment. On échangeait un coût constant
   contre une pointe, et la pointe tombe pendant le geste.

   Le gain mesuré à 67 % supposait les reflets absents du départ, pas
   éteints en route. Ce n'est pas la même chose, et c'est l'erreur. */

/* LE FILTRE N'EXISTE QUE PENDANT LA VITESSE, ET C'EST TOUT LE PROBLÈME.

   Première version : le `drop-shadow` était déclaré en permanence, avec une
   amplitude nulle au repos. Visuellement identique, et pourtant ruineux —
   un filtre force le navigateur à peindre l'élément dans une surface à
   part, qu'il soit à zéro ou non, et il y a dix-sept cartes dessinées.

   Mesuré sur trois traversées du rail : la médiane par image passait de
   33,3 ms sans à 50,0 avec, et la part d'images au-dessus de 25 ms de 64 à
   95 %. Un effet de vitesse qui coûte la moitié de la fluidité est un effet
   qui se retourne contre lui-même.

   La classe est posée par le script quand le débit dépasse le seuil, et
   retirée en dessous. Au repos, il n'y a plus de filtre du tout — pas un
   filtre nul, aucun filtre. */
/* LA TRAÎNÉE NE PORTE QUE SUR L'ÉLUE, ET C'EST UNE CORRECTION.

   Elle portait sur les vingt-quatre cartes. Mesuré contre index.html sur
   quatre traversées : médiane par image 49,9 ms contre 33,3, et 95 %
   d'images au-dessus de 25 ms contre 67. Un filtre force une surface de
   rendu par élément, et vingt-quatre surfaces coûtent la moitié de la
   fluidité — au moment précis où l'on défile vite, c'est-à-dire là où elle
   manque le plus.

   L'effet se retournait donc contre lui-même, ce que j'avais écrit dans le
   commentaire de la première version sans en tirer la conséquence.

   Une seule carte suffit : c'est l'élue qu'on regarde, les autres sont plus
   petites, plus sombres et plus loin. Le coût est divisé par vingt-quatre
   pour une perte visuelle que je n'arrive pas à voir. */
.cf-scene.cf-vite .cf-item.is-elue .cf-card {
  filter:
    drop-shadow(calc(var(--vit, 0) * 16px) 0 0 rgba(255, 255, 255, calc(var(--vit, 0) * 0.13)))
    drop-shadow(calc(var(--vit, 0) * 34px) 0 0 rgba(255, 255, 255, calc(var(--vit, 0) * 0.06)));
}

/* ---------- L'IMPACT ---------- */

/* L'accrochage se terminait sans rien dire : la page cessait de bouger. Une
   secousse de deux pixels transforme un arrêt en ARRIVÉE — la différence
   entre se poser et claquer en place.

   Elle porte sur le cadre entier et non sur la carte : c'est toute la scène
   qui encaisse, comme une caméra sur un rail qui bute. Et elle passe par
   `translate`, propriété indépendante de `transform`, pour ne pas entrer en
   conflit avec les transformées 3D de la scène. */
/* PAS DE SECOUSSE. L'ARRIVÉE SE MARQUE PAR LA LUMIÈRE.

   Deux versions essayées, deux échecs. Une translation de deux pixels
   d'abord — 0,14 % de la largeur, sous le seuil de perception, et je l'avais
   proposée sans la vérifier. Une variation d'échelle ensuite : bien visible,
   celle-là, mais déplaisante à l'usage.

   Le défaut n'était pas l'amplitude, c'était le PRINCIPE. Déformer l'image
   pour signaler un événement d'interface revient à faire payer au contenu ce
   qui ne le concerne pas : la photographie n'a rien fait de mal, elle n'a
   pas à se tasser parce que le défilement s'est arrêté. Et sur une page qui
   montre des films, toute déformation du cadre se lit comme un défaut de
   lecture.

   C'est la même leçon que sur la section studio, où l'ombre portée ne
   pouvait rien sur un fond noir : ici comme là, on marque par la lumière.
   Le liseré suffit, il ne touche à rien, et il dit en plus OÙ l'on est
   arrivé — ce qu'une secousse ne dit jamais. */

/* Le liseré de l'élue s'allume sur le choc : la secousse dit qu'on est
   arrivé, le liseré dit OÙ. Une secousse seule laisse chercher. */
.cf-sticky.cf-choc .cf-item.is-elue .cf-card::after {
  animation: cfLisere 420ms var(--courbe, ease);
}
@keyframes cfLisere {
  0%   { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.85); }
  100% { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .cf-item.cf-coup-a .cf-face,
  .cf-item.cf-coup-b .cf-face,
  .cf-sticky.cf-choc .cf-item.is-elue .cf-card::after { animation: none; }
  .cf-scene.cf-vite .cf-item.is-elue .cf-card { filter: none; }
}



/* =========================================================
   LE NERF DU STUDIO — PAR LA LUMIÈRE, COMME LE RESTE
   ========================================================= */

/* CETTE SECTION N'A NI MUSIQUE NI CARTES QUI DÉFILENT.

   Le hero réagit à la basse, le rail au passage des pochettes. Ici il n'y a
   rien de discret à marquer, sauf un seul événement : le moment où l'aimant
   achève de centrer la section. Et une variable continue : la vitesse à
   laquelle on la traverse.

   Tout passe par la LUMIÈRE. C'est la règle de cette section depuis qu'on y
   a retiré l'ombre portée — sur un fond noir, une ombre ne peut rien
   soustraire, on ne détache que par le clair. Une secousse ou un
   déplacement y seraient des corps étrangers, et c'est exactement ce qu'on
   a retiré du rail deux étapes plus tôt. */

/* ---------- L'ÉLAN ---------- */

/* L'arête de la tranche accroche la lumière quand on traverse vite.

   Elle suit déjà l'angle du boîtier — maximale quand la tranche se
   présente, éteinte quand elle se referme. On y ajoute la vitesse : une
   arête vive brille davantage quand elle balaie une source, et c'est ce
   qui distingue une surface métallique d'une surface peinte.

   Le rebord de la face suit le même élan, moitié moins fort : les deux à
   l'unisson donneraient un objet qui s'allume, pas une lumière qui glisse
   dessus. */
.boitier-tranche {
  box-shadow: inset -1px 0 0
    rgba(255, 255, 255, calc(0.18 + var(--tr, 0) * 0.14 + var(--elan, 0) * 0.3));
}
.studio-globe .media.portrait::after {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, calc(0.16 + var(--elan, 0) * 0.14)),
    inset 0 1px 0 rgba(255, 255, 255, calc(0.34 + var(--elan, 0) * 0.26)),
    inset 1px 0 0 rgba(255, 255, 255, calc(0.26 + var(--elan, 0) * 0.2));
}

/* ---------- L'ARRIVÉE ---------- */

/* Quand l'aimant achève de centrer la section, la lumière du fond monte
   puis retombe. Elle ne clignote pas : elle respire une fois.

   900 ms, soit beaucoup plus long que les 190 ms du coup sur le rail. Le
   rythme d'une section qu'on vient de poser n'est pas celui d'une pochette
   qui passe — un événement rare a droit à une durée longue, et c'est même
   ce qui le distingue d'un défaut d'affichage.

   Sur `filter: brightness` du bloc de lumière plutôt que sur son opacité :
   la nappe est faite de quatre masses de teintes différentes, et une montée
   de luminosité conserve leur rapport là où l'opacité les délave ensemble. */
.studio-globe.studio-arrive::before {
  animation: studioArrive 900ms var(--courbe, ease);
}
@keyframes studioArrive {
  0%   { filter: blur(38px) brightness(1); }
  18%  { filter: blur(38px) brightness(1.9); }
  100% { filter: blur(38px) brightness(1); }
}

/* Le boîtier prend la même bouffée, en retard d'un souffle : la pièce
   s'éclaire, puis l'objet qui s'y trouve. L'inverse dirait que l'objet est
   la source. */
.studio-globe.studio-arrive .media.portrait::after {
  animation: studioRebord 900ms var(--courbe, ease) 90ms;
}
@keyframes studioRebord {
  0%   { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16),
                     inset 0 1px 0 rgba(255, 255, 255, 0.34),
                     inset 1px 0 0 rgba(255, 255, 255, 0.26); }
  20%  { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.42),
                     inset 0 1px 0 rgba(255, 255, 255, 0.8),
                     inset 1px 0 0 rgba(255, 255, 255, 0.6); }
  100% { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16),
                     inset 0 1px 0 rgba(255, 255, 255, 0.34),
                     inset 1px 0 0 rgba(255, 255, 255, 0.26); }
}

@media (prefers-reduced-motion: reduce) {
  .studio-globe.studio-arrive::before,
  .studio-globe.studio-arrive .media.portrait::after { animation: none; }
}


/* =========================================================
   LE GRÉSILLEMENT — v30
   =========================================================

   Deux choses, et rien d'autre : une lumière qui grésille, et une
   secousse de trois pixels. Tout ce qui a été essayé en chemin — la
   déchirure en bandes, la barre de balayage, la séparation des canaux,
   la neige, l'inversion, le fantôme — se lisait comme une panne, parce
   que déformer le contenu est exactement ce que fait un affichage qui
   lâche. Ce site détache par la lumière ; c'est la règle de la section
   studio, et elle vaut aussi ici.

   Les valeurs sont celles du banc `test-final.html`, à l'identique.
   `--imp` est l'état du tube, de 0 à 1, `--sx` et `--sy` la secousse en
   pixels. Tout est écrit par v30.js.
   ========================================================= */

/* LA LUMIÈRE : luminosité PUIS contraste, et l'ordre fait tout.

   Le canevas a son pixel le plus clair à 155 sur 255 et sa médiane à 12 :
   tout est sous le pivot du contraste, et un contraste appliqué seul
   écraserait tout vers le noir, routes comprises. Dans le bon ordre, les
   terres tombent vers zéro et les routes montent — le tri se fait dans
   les valeurs, sans masque et sans rien supposer de la géométrie.

   `filter` est libre sur cet élément : v29 n'en posait aucun. */
.studio-sphere {
  /* LA POINTE REDESCEND, L'AGITATION MONTE, et ce sont deux choses
     différentes. À ×2,80 le globe blanchissait au sommet : passé un
     certain niveau, les routes saturent toutes ensemble et l'image perd
     le dessin qu'on cherchait justement à faire ressortir — plus c'est
     lumineux, moins on voit. Le pic redescend donc à ×1,85, et ce qui
     manquait en présence est repris par le grésillement lui-même, qui
     bat plus souvent et plus profond. */
  filter:
    brightness(calc(1 + var(--imp, 0) * 0.85))
    contrast(calc(1 + var(--imp, 0) * 0.7));
}

/* L'OPACITÉ EST REPRISE EN ENTIER, PAS AJOUTÉE.

   La règle d'origine la fait décroître pendant la sortie de section :
   `0.62 * (1 - var(--a) * 0.92)`. Une seconde déclaration ne s'additionne
   pas, elle remplace — et remplacer aurait fait remonter le globe en
   pleine sortie. Le terme du grésillement est donc glissé DANS
   l'expression existante, et multiplié par le même facteur de sortie :
   un globe qui s'en va ne se met pas à briller. */
.studio-sphere {
  opacity: calc((0.62 + var(--imp, 0) * 0.22) * (1 - var(--a, 0) * 0.92));
}

/* LA SECOUSSE, ET LE PIÈGE DE `translate`.

   Trois pixels au maximum, et seulement pendant l'amorçage. Au-delà on
   voit un objet qu'on bouscule ; sous ce seuil l'œil ne suit plus la
   trajectoire et ne lit plus qu'une instabilité — c'est là qu'elle cesse
   de ressembler à un défaut d'affichage.

   `translate` était DÉJÀ PRIS : la règle de mise en page y pose le
   `0 -50%` qui centre le globe sur sa position verticale. Une déclaration
   qui ne porterait que la secousse l'effacerait, et le globe descendrait
   d'une demi-hauteur. Le décalage d'origine est repris dans le calcul. */
.studio-sphere {
  translate: calc(var(--sx, 0) * 1px) calc(-50% + var(--sy, 0) * 1px);
}

/* Rien ne clignote pour qui a demandé qu'on s'en abstienne. */
@media (prefers-reduced-motion: reduce) {
  .studio-sphere {
    filter: none;
    opacity: calc(0.62 * (1 - var(--a, 0) * 0.92));
    translate: 0 -50%;
  }
}


/* ---------------------------------------------------------
   LE FOND EST SUR LE MÊME CIRCUIT
   ---------------------------------------------------------

   Le globe grésillait seul, et ça se voyait : un objet qui clignote
   devant un fond parfaitement stable se lit comme un élément rapporté,
   pas comme une section sous tension. Le film du fond reçoit donc
   exactement le même `--imp`, au même instant.

   PAS DE RETARD, ET C'EST UN CHOIX. Ailleurs sur cette page la pièce
   répond APRÈS la source, parce qu'une lumière éclaire d'abord la pièce
   et ensuite l'objet. Ici ce n'est pas une lumière qui se propage : les
   deux sont branchés sur la même alimentation qui faiblit. Un décalage
   dirait « le globe éclaire le mur », ce qui est faux, et casserait la
   seule chose qu'on cherche — que les deux battent ensemble.

   L'AMPLITUDE N'EST PAS LA MÊME, et il a fallu la mesurer pour la régler.

   La première raison est de proportion : le fond occupe toute la fenêtre,
   le globe un cinquième. À amplitude égale on ne verrait plus que le
   fond, et le grésillement du globe disparaîtrait dans le sien.

   La seconde est une limite à ne pas franchir. Les règles d'accessibilité
   déconseillent plus de trois éclats par seconde sur une surface notable,
   et le bégaiement d'amorçage en produit cinq en cent millisecondes. Sur
   un objet d'un cinquième de fenêtre c'est hors du champ de la règle ;
   sur un fond plein cadre, c'est exactement le cas qu'elle vise, et le
   seuil est de 10 % de contraste relatif.

   J'avais écrit 0,075 en commentant « 0,28 à 0,30 ». La mesure donnait
   0,28 à 0,355, soit 21,1 % — deux fois le seuil, et mon commentaire
   décrivait un réglage que je n'avais pas écrit. À 0,026 la luminosité va
   de 0,280 à 0,306, soit 8,5 %. Le fond ne clignote pas : il RESPIRE avec
   le tube, et c'est tout ce qu'il faut pour que les deux appartiennent à
   la même scène. */
.studio-fond video {
  filter:
    blur(64px)
    saturate(0.5)
    brightness(calc(0.28 + var(--impDoux, 0) * 0.115));
}

/* AUCUN FILTRE SUR LE CONTENEUR, et ce n'est pas un oubli.

   J'en avais posé un pour que l'affiche fixe respire aussi. Deux erreurs
   d'un coup. Le conteneur est le PARENT de la vidéo : son filtre se
   multipliait à celui du film au lieu de le remplacer, ce qui portait le
   contraste bien au-delà du seuil. Et un `filter` crée un contexte
   d'empilement — sur un élément posé à `z-index: -2` derrière tout le
   reste, c'est le genre de changement qui déplace une couche entière.

   Quant à l'affiche : elle ne sert que sous `prefers-reduced-motion`, où
   il ne doit précisément rien se passer. Elle n'a rien à faire respirer. */

@media (prefers-reduced-motion: reduce) {
  .studio-fond video {
    filter: blur(64px) saturate(0.5) brightness(0.28);
  }
}


/* ---------------------------------------------------------
   LE HÉROS SUR LE MÊME CIRCUIT — v30
   ---------------------------------------------------------

   LE LOGOTYPE EST LE NÉON, LE FILM EST LA PIÈCE.

   Le hérau porte un film plein cadre et un nom posé dessus. Faire grésiller
   le film serait un éclat plein écran — c'est précisément le cas que les
   règles d'accessibilité visent, et ce serait de toute façon illisible.
   C'est donc le NOM qui grésille : une enseigne au néon qui peine à
   s'allumer devant un film qui, lui, ne bronche pas. La surface concernée
   est celle de deux lignes de texte, très loin du seuil.

   Et c'est le même `--imp` que la section studio : les deux sont branchés
   sur la même alimentation qui faiblit, et les deux s'amorcent sur les
   coupes du même film. Deux horloges auraient fait deux phénomènes.

   TOUT EST SOUS `body.hero-fini`. L'animation d'entrée pose ses valeurs en
   `forwards`, qui l'emportent sur toute déclaration ordinaire quelle que
   soit la spécificité — et rien n'apparaît barré dans l'inspecteur. Sous
   `hero-fini`, l'entrée a rendu la main : c'est le seul endroit où l'on
   peut écrire sans se faire ignorer en silence. */

/* LE NÉON BAISSE, IL NE MONTE PAS.

   Le texte est déjà blanc : une luminosité au-dessus de 1 n'a rien à
   éclaircir, elle ne fait rien du tout. Un tube qui grésille ne monte pas
   au-dessus de son régime, il DESCEND par à-coups — c'est l'extinction
   brève qu'on remarque, pas un surcroît d'éclat. L'opacité tombe donc
   quand `--imp` monte.

   45 % au plus : en dessous de zéro le nom disparaîtrait, et un logotype
   qui s'efface complètement se lit comme un défaut de chargement. */
body.hero-fini .hero-nom {
  opacity: calc(1 - var(--imp, 0) * 0.45);
}

/* La phrase suit à la moitié, comme pour la traînée : elle est déjà à 62 %
   de blanc, et une extinction de même amplitude la ferait disparaître avant
   le nom. Le rapport est dans la feuille et non dans un second réglage —
   deux curseurs indépendants inviteraient à les désaccorder. */
body.hero-fini .hero-phrase > span {
  opacity: calc(1 - var(--imp, 0) * 0.24);
}

/* La ligne PRODUCTION et l'étoile grésillent avec le mot, deux fois moins
   fort — même raison qu'ailleurs : à l'unisson exact, les trois se
   liraient comme un seul clignotant.

   LE POULS ET LE GRÉSILLEMENT DANS UNE SEULE DÉCLARATION. Deux règles
   séparées sur la même propriété ne s'additionnent pas : la seconde
   gagne, et la première ne fait plus rien tout en donnant l'impression
   d'agir. Le produit des deux facteurs les compose vraiment — la
   respiration du son module l'opacité de repos, le grésillement la coupe
   par-dessus. */
body.hero-fini .hero-sous {
  opacity: calc((0.85 + var(--pouls, 0) * 0.08) * (1 - var(--imp, 0) * 0.22));
}
body.hero-fini .hero-signe {
  opacity: calc((0.92 + var(--pouls, 0) * 0.08) * (1 - var(--imp, 0) * 0.22));
}

/* LA SECOUSSE PORTE SUR LE VERROU, PAS SUR LE NOM.

   `.hero-verrou` tient les deux lignes du logotype ensemble : les secouer
   séparément les désolidariserait, et une enseigne dont le mot et son
   descripteur tremblent chacun de son côté n'est pas une enseigne, c'est
   deux objets.

   Sur `translate`, propriété indépendante qui se compose avec les
   transformations de l'entrée au lieu de les écraser. Amplitude réduite de
   moitié par rapport au globe : le nom est au premier plan et lisible, un
   texte qui bouge de six pixels devient pénible à lire là où un globe
   flouté ne l'est pas. */
body.hero-fini .hero-verrou {
  translate: calc(var(--sx, 0) * 0.5px) calc(var(--sy, 0) * 0.5px);
}
body.hero-fini .hero-phrase {
  translate: calc(var(--sx, 0) * 0.3px) calc(var(--sy, 0) * 0.3px);
}

@media (prefers-reduced-motion: reduce) {
  body.hero-fini .hero-nom,
  body.hero-fini .hero-phrase > span { opacity: 1; }
  body.hero-fini .hero-sous { opacity: 0.85; }
  body.hero-fini .hero-signe { opacity: 0.92; }
  body.hero-fini .hero-verrou,
  body.hero-fini .hero-phrase { translate: 0 0; }
}


/* =========================================================
   LE STUDIO SUR UN TÉLÉPHONE
   =========================================================

   Retenu sur maquette, artefact « Le studio au doigt ». Tout le bloc est en
   fin de feuille et sous deux classes : il doit passer après les règles du
   globe écrites plus haut (arrivée, pouls, secousse), qui visent
   `.studio-sphere` seul.

   Mesuré avant, sur 390 px puis 320 : le globe à 62 % au milieu des deux
   paragraphes, ses noms de lieux sur les lignes et « France » en double avec
   la légende ; la photo tordue en trapèze ; deux paragraphes au même corps ;
   et sur un iPhone SE, le haut de la photo coupé et le bouton sur deux
   lignes, sorti de l'écran. */
@media (max-width: 640px) {
  /* LE GLOBE QUITTE LE TEXTE, ET SE VOIT.

     Au bureau il est poussé à droite pour ne pas passer sous les lignes ;
     sur un téléphone il n'y a plus de côté libre. Il descend en bas à
     droite, le point de la France juste sous la dernière ligne : ses
     trajets partent du texte vers le bord de l'écran.

     90 % et non 62 : il ne passe plus sous le texte, il n'a plus à s'effacer
     pour lui — la même remontée qu'au bureau quand il a été poussé à droite.
     Seul son premier sixième s'efface, là où il approcherait encore la
     dernière ligne.

     Les noms de lieux sont retirés par globe.js (`data-noms-bureau`). */
  .studio-globe .studio-sphere {
    top: auto;
    bottom: -62vw;
    right: -46vw;
    width: 112vw;
    translate: calc(var(--sx, 0) * 1px) calc(var(--sy, 0) * 1px);
    opacity: calc((0.9 + var(--imp, 0) * 0.1) * (1 - var(--a, 0) * 0.92));
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, transparent 14%, #000 40%);
            mask-image: linear-gradient(to bottom, transparent 0%, transparent 14%, #000 40%);
  }

  /* LE BOUTON PASSE DEVANT LE GLOBE. Les trajets traversent sa ligne ; un
     fond sombre dépoli le garde net — le registre de la pastille « Tous les
     films » du carrousel. */
  .studio-globe .entre .btn {
    background: rgba(8, 8, 9, 0.62);
    -webkit-backdrop-filter: blur(10px);
            backdrop-filter: blur(10px);
  }

  .studio-globe figure { perspective-origin: 50% 45%; }

  /* UNE HIÉRARCHIE. Les deux paragraphes étaient à 16 px : rien ne disait
     lequel lire d'abord. Les marges sont écrites en ligne dans le HTML,
     d'où les `!important` — c'est la seule façon de les atteindre. */
  .studio-globe .entre .lead { font-size: 18px; line-height: 1.4; margin-top: 12px !important; }
  .studio-globe .entre .muted { font-size: 15px; line-height: 1.55; margin-top: 0.9em !important; }
  .studio-globe .entre p:last-child { margin-top: 1.5em !important; }
  #studio figure figcaption { margin-top: 12px; }
}

/* LE BOÎTIER PIVOTE MOINS. 15° de trois quarts se lisent comme un objet
   posé sur 260 px ; étalés sur toute la largeur d'un téléphone, ils
   déforment la photo en trapèze. 5° de base et 6° de traversée : le boîtier
   bouge encore, sans tordre l'image.

   Sous `no-preference` : deux classes battraient le `transform: none` que
   le mouvement réduit pose sur `.plateau`. */
@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {
  .studio-globe .plateau {
    transform:
      translateY(calc(var(--a, 0) * 24px))
      rotateY(calc(5deg + var(--a, 0) * 8deg + var(--tr, 0) * 6deg))
      rotateX(calc(0.3deg + var(--a, 0) * 1.5deg + var(--tr, 0) * -1deg));
  }
}

/* LES ÉCRANS COURTS — iPhone SE, 568 px utiles. La photo prend un format
   un peu plus large et le texte descend d'un demi-point : la section entière
   tient, de la photo (32 px) au bouton (536 px). */
@media (max-width: 640px) and (max-height: 640px) {
  .studio-globe .media.portrait { aspect-ratio: 16 / 9.5; }
  .studio-globe .entre .lead { font-size: 17px; }
  .studio-globe .entre .muted { font-size: 14.5px; }
}

/* Sous 360 px, « Découvrir mon parcours » passait sur deux lignes. */
@media (max-width: 360px) {
  .studio-globe .entre .btn { padding: 15px 20px; letter-spacing: 0.1em; gap: 10px; white-space: nowrap; }
}
