/* ==========================================================================
   LUNOR — Feuille de style principale
   Police : système monospace/sans pour l'esthétique cinéma technique.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&family=Playfair+Display:ital@1&display=swap');

:root {
  /* NUMÉRO GRAVÉ DANS LA FEUILLE ELLE-MÊME (v=142).
     À incrémenter EN MÊME TEMPS que le ?v= de index.html. Il ne sert qu'à une
     chose, mais elle est décisive : lunorfilms.com/?build compare ce que la
     page DEMANDE et ce qu'elle REÇOIT vraiment. Sans lui, une mise en ligne
     réussie côté FTP et invisible côté téléphone ne se distingue pas d'un
     correctif raté. */
  --lunor-css: "209";

  --bg: #050506;
  --bg-panel: #0a0a0c;
  --fg: #f5f5f3;
  --fg-dim: #9a9a9f;
  --fg-faint: #55555c;
  --line: rgba(255,255,255,0.1);
  --pink: #ff2d78;
  /* L'ACCENT DE L'INTERFACE  (v=149)
     À ne pas confondre avec --cat-color, qui dit « la catégorie dont on parle
     en ce moment » et change dès qu'on épingle une ambiance sur l'accueil.
     Celle-ci dit « la catégorie dans laquelle on EST », et ne vaut autre chose
     que le rose que lorsqu'un panneau de catégorie est réellement ouvert.
     Posée par syncAccent(), et par personne d'autre. */
  --ui-accent: #ff2d78;
  --pink-dim: rgba(255,45,120,0.15);

  /* Couleurs par catégorie (remplacées dynamiquement en JS via data-attribute) */
  --cat-color: var(--pink);
  --cat-color-dim: var(--pink-dim);

  --font-body: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;
  --font-serif: 'Playfair Display', Georgia, serif;

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  overflow-x: hidden;
  min-height: 100vh;
}

img, video { max-width: 100%; display: block; }

button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
a { color: inherit; text-decoration: none; }
input, textarea { font-family: inherit; }

:focus-visible { outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

.badge {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  color: var(--pink);
  border: 1px solid rgba(255,45,120,0.4);
  background: rgba(255,45,120,0.08);
  padding: 5px 12px;
  border-radius: 999px;
  margin-bottom: 20px;
}

.accent { color: var(--pink); }

/* ---------- Boutons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  padding: 15px 24px;
  white-space: nowrap;
  transition: all 0.25s var(--ease);
}
.btn--white { background: #fff; color: #0a0a0a; }
.btn--white:hover { background: #e8e8e8; }
.btn--outline { border: 1px solid rgba(255,255,255,0.3); color: #fff; }
.btn--outline:hover { border-color: #fff; background: rgba(255,255,255,0.05); }
.btn--accent { background: var(--cat-color, var(--pink)); color: #0a0a0a; }
.btn--accent:hover { filter: brightness(1.1); }
.btn--pill { border-radius: 999px; }
.btn--full { width: 100%; justify-content: center; padding: 18px; font-size: 0.8rem; }

.icon-btn {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.15);
  color: #fff;
  transition: background 0.2s var(--ease);
}
.icon-btn:hover { background: rgba(255,255,255,0.08); }

/* ==========================================================================
   HERO / ACCUEIL
   ========================================================================== */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* padding-bas = hauteur de la bande de l'obturateur (74px) + respiration.
     Tout le texte du hero s'arrete au-dessus de la ligne. */
  padding: 40px 48px 118px;
  overflow: hidden;
}

.hero__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
}
/* --------------------------------------------------------------------------
   TRIPTYQUE — fonds verticaux sur écran paysage
   En "cover", un clip 9:16 affiché plein cadre sur un écran 16:9 perd ~70 %
   de sa hauteur : il n'en reste qu'une bande centrale. Trois clips côte à côte
   font 3 x 9:16 = 1.69, contre 1.78 pour l'écran : chaque colonne ne perd que
   ~5 %. Le format vertical devient un parti pris — un mur de moniteurs de
   salle de montage — au lieu d'un accident.
   Posé en JS (.is-triptych) seulement quand la catégorie a au moins 3 clips
   ET que l'écran est paysage. Sur mobile, l'écran est déjà vertical : une
   seule vidéo tombe juste, on ne triptyque rien.
   -------------------------------------------------------------------------- */
.hero__triptych { position: absolute; inset: 0; z-index: 0; display: none; }
.hero.is-triptych .hero__triptych { display: flex; }
.hero.is-triptych .hero__video { display: none; }
.hero__cell { position: relative; flex: 1; overflow: hidden; }
.hero__cell + .hero__cell { border-left: 1px solid rgba(255,255,255,0.14); }
.hero__cell video {
  width: 100%; height: 100%; object-fit: cover;
  transition: filter 1.6s var(--ease), transform 1.6s var(--ease);
}
/* La colonne dont le son est ouvert reste pleine ; les deux autres reculent,
   pour qu'on voie ce qu'on écoute.

   ATTENTION au découpage des règles : le voile est déclaré dès .is-triptych,
   à opacité 0, et non sous .has-sound. Sinon le pseudo-élément n'existerait
   pas avant le clic, serait créé directement à 75 %, et n'aurait aucun état
   précédent à animer — le fondu serait purement décoratif, l'assombrissement
   tomberait d'un coup.

   Deux paliers : 75 % au clic, 90 % quand la souris s'arrête (le hero passe
   en is-idle au bout de 3 s, et seulement si un son est ouvert). La durée est
   lue sur l'état d'arrivée : 1,6 s pour le premier palier, plus posé, et 1 s
   pour le passage en veille.
   La règle .is-live vient en dernier : à spécificité égale elle l'emporte, et
   la colonne écoutée ne s'assombrit jamais, même en mode immersif. */
.hero.is-triptych .hero__cell::after {
  content: ''; position: absolute; inset: 0;
  background: #050506;
  opacity: 0;
  transition: opacity 1.6s var(--ease);
  pointer-events: none;
}
.hero.is-triptych.has-sound .hero__cell::after { opacity: 0.75; }
.hero.is-triptych.has-sound.is-idle .hero__cell::after { opacity: 0.9; transition-duration: 1s; }
.hero.is-triptych.has-sound .hero__cell.is-live::after { opacity: 0; }

/* Le flou décroche les colonnes du point de netteté ; monté sur une échelle
   de 16px : 25 % = 4px, 50 % = 8px.
   Le léger scale n'est pas décoratif — un filtre de flou rend les bords de
   l'image translucides, on verrait le noir du dessous suinter sur les bords
   de chaque colonne. Le grossissement pousse ces bords hors cadre. */
.hero.is-triptych.has-sound .hero__cell video {
  filter: blur(4px);
  transform: scale(1.03);
}
.hero.is-triptych.has-sound.is-idle .hero__cell video {
  filter: blur(8px);
  transform: scale(1.06);
  transition-duration: 1s;
}
.hero.is-triptych.has-sound .hero__cell.is-live video {
  filter: none;
  transform: none;
}

/* --------------------- SÉLECTEUR DE SON (un bouton par clip) --------------
   Il remplace l'ancien bouton blanc "Visionner avec son" : il doit garder le
   même poids visuel, sinon personne ne le voit sur une vidéo. D'où le fond
   blanc plein, et un halo qui bat tant qu'on n'y a pas touché.
   -------------------------------------------------------------------------- */
.hero__sound {
  position: relative;
  display: inline-flex; align-items: center; gap: 14px;
  padding: 0 14px 0 18px; height: 46px;
  background: #fff; color: #0a0a0a;
}
.hero__sound-label {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.06em; color: #0a0a0a; white-space: nowrap;
}
.hero__sound-btns { display: inline-flex; gap: 6px; }
.hero__sound-btn {
  min-width: 34px; height: 32px; padding: 0 8px;
  border: 1px solid rgba(0,0,0,0.28);
  font-family: var(--font-mono); font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.04em; color: #0a0a0a;
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease), color 0.25s var(--ease);
}
.hero__sound-btn:hover { background: rgba(0,0,0,0.08); border-color: #0a0a0a; }
.hero__sound-btn.is-active {
  background: var(--cat-color, var(--pink)); border-color: var(--cat-color, var(--pink)); color: #fff;
}
/* Halo d'appel : il bat jusqu'au premier clic, puis ne revient plus. */
.hero__sound.is-hint::after {
  content: ''; position: absolute; inset: 0;
  border: 1px solid #fff;
  animation: sound-hint 2.6s var(--ease) infinite;
  pointer-events: none;
}
@keyframes sound-hint {
  0%   { transform: scale(1); opacity: 0.8; }
  70%  { transform: scale(1.12); opacity: 0; }
  100% { transform: scale(1.12); opacity: 0; }
}
.hero__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(5,5,6,0.35) 0%, rgba(5,5,6,0.15) 40%, rgba(5,5,6,0.55) 75%, rgba(5,5,6,0.9) 100%);
  z-index: 1;
  transition: background 0.4s var(--ease);
}
.hero.is-dimmed .hero__scrim { background: rgba(5,5,6,0.75); }

/* --------------------------------------------------------------------------
   AMBILIGHT — variante MARIAGE (data-cat pose par setHeroBackground en JS).
   La video de mariage est VERTICALE (9:16). En cover plein ecran, on perdait
   les deux tiers du plan ; dans un cadre reduit, on obtenait une bande etroite
   cernee de noir. Traitement retenu, celui du vertical en salle :
     - la video monte sur TOUTE LA HAUTEUR, largeur automatique -> plan entier,
       jamais recadre ;
     - .hero__ambilight (le meme fichier, en cover, agrandi et floute) remplit
       tout l'ecran derriere et deborde donc sur les cotes : ce sont ces zones
       laterales qui portent la lueur, et l'ecran reste plein.
   -------------------------------------------------------------------------- */
.hero__ambilight { display: none; }

.hero.is-portrait { background: #050506; }

/* La lueur : plein cadre, floutee, saturee. Elle n'est visible que la ou la
   video ne passe pas, c'est-a-dire les cotes. */
.hero.is-portrait .hero__ambilight {
  display: block;
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.25);
  filter: blur(80px) saturate(1.9) brightness(1.05);
  opacity: 0.9;
  z-index: 0;
  pointer-events: none;
}
/* Le plan complet, sur toute la hauteur.
   CENTRAGE PAR MARGES, PAS PAR TRANSFORM (v=113). L'ancien centrage
   (top/left 50% + translate(-50%,-50%)) passait par transform — or transform
   est dans la liste de transition des videos du hero (1,6 s, pour le zoom du
   mode son). Au basculement vers un fond vertical, le translate S'ANIMAIT :
   la video glissait depuis la gauche avant de se poser au centre. Safari
   aggravait le cas en peignant d'abord une image non transformee.
   inset:0 + margin:auto centre un element remplace (video, img) dans tous les
   navigateurs, sans transform : plus rien a animer, la video apparait en
   place. Le zoom du triptyque (scale sur .hero__cell video) n'est pas
   concerne et garde sa transition. */
.hero.is-portrait .hero__video {
  position: absolute; inset: 0; margin: auto;
  width: auto; height: 100%; max-width: 100%;
  object-fit: contain;
  z-index: 1;
  box-shadow: 0 0 90px rgba(0,0,0,0.55);
}
/* Le scrim est calibre pour une video plein cadre : on l'allege pour ne pas
   eteindre la lueur sur les cotes. */
.hero.is-portrait .hero__scrim {
  background: linear-gradient(180deg, rgba(5,5,6,0.45) 0%, transparent 28%, transparent 64%, rgba(5,5,6,0.9) 100%);
}
/* --------------------------------------------------------------------------
   LE GLOOM — la lueur qui borde un plan vertical pose seul (v=93).
   Il n'appartient plus au Mariage : il appartient au FORMAT. N'importe quelle
   page dont le fond est une seule video verticale y a droit, et le Mariage
   peut desormais y renoncer — il suffit de lui donner trois clips depuis
   l'intranet.
   Quand le son est ouvert ET que la souris s'arrete (le hero passe en .is-idle
   au bout de 3 s), le Gloom s'eteint en 0,5 s : la piece se fait noire autour
   du plan, il ne reste que le film. Il se rallume des le premier mouvement.
   Deux etats seulement, tout est dans la transition — pas d'animation, donc
   rien qui tourne en boucle sur le GPU pendant qu'on regarde.
   Le triptyque n'est pas concerne : il a deja son propre voile par colonne.
   -------------------------------------------------------------------------- */
.hero.is-portrait .hero__ambilight {
  transition: opacity 0.5s var(--ease), filter 0.5s var(--ease);
}
.hero.is-portrait.has-sound.is-idle .hero__ambilight {
  opacity: 0.22;
  filter: blur(80px) saturate(1.2) brightness(0.4);
}

/* Le triptyque, ce sont 3 clips cote a cote : l'ambilight n'a aucun sens. */
.hero.is-triptych.is-portrait .hero__ambilight { display: none; }
@media (prefers-reduced-motion: reduce) {
  .hero.is-portrait .hero__ambilight { filter: blur(80px) saturate(1.4); opacity: 0.75; }
}

/* Mode immersif : après 3s d'inactivité souris (son activé), tout disparaît sauf la vidéo */
.hero__scrim,
.hover-hint,
.nav-zone,
.hero__content,
.hero__meta {
  transition: opacity 0.6s var(--ease);
}
.hero.is-idle .hero__scrim,
.hero.is-idle .hover-hint,
.hero.is-idle .nav-zone,
.hero.is-idle .hero__content,
.hero.is-idle .hero__meta {
  opacity: 0;
  pointer-events: none;
}

.hover-hint {
  position: absolute; top: 24px; left: 50%; transform: translateX(-50%); z-index: 3;
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em;
  color: var(--fg-dim);
  display: flex; align-items: center; gap: 8px;
  /* Absent par defaut : c'est l'animation qui le fait venir, et elle ne se
     declenche qu'une fois la barre repartie (.nav-intro retiree en JS). */
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}
/* Deux animations qui se relaient sur la meme propriete : hint-in amene
   l'indice de 0 a 0,45 apres le depart de la barre et s'y tient (forwards),
   puis hint-breathe prend le relais a 1 s — et comme son image a 0 % vaut
   justement 0,45, le passage de l'une a l'autre ne se voit pas. Sans ce
   relais, l'indice serait apparu d'un coup a mi-respiration. */
.hero:not(.nav-intro) .hover-hint {
  animation: hint-in 0.6s var(--ease) 0.45s forwards,
             hint-breathe 3.2s ease-in-out 1.05s infinite;
}
.hover-hint .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cat-color, var(--pink)); transition: background-color 620ms var(--ease); }
.hero.nav-open .hover-hint { opacity: 0 !important; animation-play-state: paused; }

@keyframes hint-in {
  from { opacity: 0; }
  to   { opacity: 0.45; }
}
@keyframes hint-breathe {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .hero:not(.nav-intro) .hover-hint { animation: hint-in 0.4s var(--ease) 0.45s forwards; opacity: 0.7; }
}

/* Zone déclencheur de survol (bande invisible en haut de l'écran) */
.nav-zone {
  position: fixed; top: 0; left: 0; right: 0; height: 110px; z-index: 20;
}

.nav {
  position: absolute; top: 20px; left: 24px; right: 24px;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 14px 26px;
  background: rgba(14,14,18,0.42);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255,255,255,0.09);
  border-radius: 999px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.25);
  transform: translateY(-140%);
  opacity: 0;
  transition: transform 0.4s var(--ease), opacity 0.4s var(--ease),
              border-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
  pointer-events: none;
}
.nav-zone:hover .nav,
.nav-zone.force-open .nav {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}
/* LE GLOOM SUIT L'AMBIANCE ÉPINGLÉE  (v=98)
   Le rose etait ecrit en dur : la barre restait rose meme quand tout le reste
   de la page — le soulignement, le seuil « ENTRER · AFTERMOVIES », le point du
   panneau — etait passe au vert. C'etait la seule piece qui ne suivait pas, et
   une couleur qui ne suit pas se lit comme une erreur.
   --cat-color est deja pose sur :root par syncSeuil(), d'apres la categorie
   EPINGLEE (et vaut var(--pink) sur Accueil, d'ou le rose conserve la-bas
   sans avoir a le dire). Le gloom n'a donc qu'a le lire : il change au clic,
   pas au survol — comme le fond, comme le seuil.
   color-mix plutot que des rgba() : la couleur arrive en hexa depuis
   content.json, on ne peut pas en composer un rgba() a la main. */
.nav:hover {
  border-color: color-mix(in srgb, var(--cat-color, var(--pink)) 55%, transparent);
  box-shadow:
    0 0 32px color-mix(in srgb, var(--cat-color, var(--pink)) 30%, transparent),
    0 0 60px color-mix(in srgb, var(--cat-color, var(--pink)) 12%, transparent),
    0 10px 40px rgba(0,0,0,0.3);
}

.nav__logo { display: flex; align-items: center; gap: 10px; justify-self: start; }
.nav__logo-badge {
  width: 34px; height: 34px; border-radius: 50%;
  /* suit la couleur de la page epinglee (rose sur l'accueil par defaut) */
  background: var(--cat-color, var(--pink)); color: #0a0a0a;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 0.8rem;
  transition: background-color 620ms var(--ease);
}
.nav__logo-text { font-weight: 700; font-size: 0.85rem; line-height: 1.1; }
.nav__logo-text small { font-weight: 500; color: var(--fg-dim); font-size: 0.6rem; letter-spacing: 0.1em; }

.nav__links { display: flex; align-items: center; gap: 28px; justify-self: center; }
.nav__link {
  position: relative;
  font-size: 0.82rem; color: var(--fg-dim);
  padding: 6px 0;
  transition: color 0.2s var(--ease);
}
.nav__link::after {
  content: ''; position: absolute; left: 0; bottom: 0;
  width: 0; height: 1px;
  /* --link-color est pose sur chaque lien au rendu (renderNav), d'apres sa
     PROPRE categorie — a ne pas confondre avec --cat-color, qui est celle de
     la categorie epinglee. C'est bien celle du lien qu'on veut ici : survoler
     Mariage doit annoncer l'or, meme si Nightlife est epingle.
     Le rose en dur ne se justifiait que pour Accueil, qui n'est pas une
     categorie — et qui retombe justement sur var(--pink) tout seul. */
  background: var(--link-color, var(--pink));
  transition: width 0.3s var(--ease), background 0.25s var(--ease);
}
.nav__link:hover, .nav__link.is-active { color: #fff; }
.nav__link:hover::after, .nav__link.is-active::after { width: 100%; }

.nav__actions { display: flex; align-items: center; gap: 10px; justify-self: end; }
.nav__actions .btn--pill {
  font-size: 0.7rem; padding: 12px 18px;
  transition: background 0.3s var(--ease), box-shadow 0.3s var(--ease), color 0.3s var(--ease), transform 0.2s var(--ease);
}
/* NOUS CONTACTER suit l'ambiance epinglee  (v=99)
   Le texte reste #0a0a0a : les quatre couleurs de categorie sont toutes assez
   claires pour porter du noir (le bleu #4f8ef7 est la plus sombre des quatre,
   et tient encore un contraste de ~6,7:1). Rien a prevoir de ce cote.
   #mobileContactTrigger suit le meme chemin — il vit dans le menu plein ecran,
   hors du formulaire, donc il herite bien du --cat-color de :root. */
#contactTrigger, #mobileContactTrigger { background: var(--cat-color, var(--pink)); color: #0a0a0a; }
#contactTrigger:hover, #mobileContactTrigger:hover {
  background: var(--cat-color, var(--pink));
  box-shadow:
    0 0 24px color-mix(in srgb, var(--cat-color, var(--pink)) 65%, transparent),
    0 0 0 1px color-mix(in srgb, var(--cat-color, var(--pink)) 40%, transparent);
  transform: translateY(-1px);
}
#muteToggle { transition: border-color 0.3s var(--ease), box-shadow 0.3s var(--ease), color 0.3s var(--ease); }
#muteToggle.is-active {
  border-color: var(--cat-color, var(--pink)); color: var(--cat-color, var(--pink));
  box-shadow: 0 0 16px color-mix(in srgb, var(--cat-color, var(--pink)) 50%, transparent),
              0 0 0 1px color-mix(in srgb, var(--cat-color, var(--pink)) 25%, transparent);
}

.hero__content { position: relative; z-index: 2; max-width: 900px; width: min(92vw, 900px); }
.hero__title {
  font-family: var(--font-body);
  font-weight: 900;
  line-height: 0.9;
  margin-bottom: 24px;
}
.hero__title-fill {
  display: block;
  font-size: clamp(2.2rem, 5.5vw, 4.3rem);
  color: #fff;
  white-space: nowrap;
}
.hero__title-outline {
  display: block;
  font-size: clamp(2.2rem, 5.5vw, 4.3rem);
  color: transparent;
  /* repli pour les navigateurs sans color-mix */
  -webkit-text-stroke: 1px rgba(255,255,255,0.25);
  /* le contour prend la couleur de la catégorie épinglée (rose sur Accueil) */
  -webkit-text-stroke-color: color-mix(in srgb, var(--cat-color) 62%, transparent);
  transition: -webkit-text-stroke-color 620ms cubic-bezier(0.22, 1, 0.36, 1);
  white-space: nowrap;
}
.hero__desc {
  max-width: 480px;
  color: var(--fg-dim);
  font-size: 1rem;
  line-height: 1.6;
  margin-bottom: 32px;
}
.hero__actions { display: flex; gap: 14px; flex-wrap: wrap; }

.hero__meta {
  position: absolute; bottom: 118px; right: 48px; z-index: 2;
  text-align: right;
}
.hero__meta-info p {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.03em;
  color: var(--fg-dim); margin-bottom: 1px; line-height: 1.5;
}
/* LUNOR COLLECTIVE 2026 suit l'ambiance epinglee  (v=99)
   La regle est bien scopee a .hero__meta-info : la classe .accent generique
   (ligne 66) sert AUSSI aux pieds de panneau et au formulaire — la passer a
   --cat-color aurait repeint « PRODUCTION CINÉMATOGRAPHIQUE » au passage,
   ce qui n'a pas ete demande. Seule la signature du hero bascule. */
.hero__meta-info p.accent {
  color: var(--cat-color, var(--pink));
  transition: color 0.35s var(--ease);
}
.hero__meta-bottom { display: flex; flex-direction: column; align-items: flex-end; gap: 18px; margin-top: 14px; }
/* Le glyphe seul, sans pastille blanche : la pastille servait à garantir la
   lisibilité sur une image claire, on la remplace par une ombre portée douce
   qui détache l'icône sans poser un rond opaque sur la vidéo.
   La zone cliquable reste plus large que le glyphe : confortable au clic sans
   encombrer visuellement. */
.hero__social { display: flex; gap: 12px; }
.hero__social a {
  width: 38px; height: 38px;
  background: none;
  color: rgba(255,255,255,0.92);
  display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 1px 7px rgba(0,0,0,0.6));
  transition: color 0.25s var(--ease), transform 0.25s var(--ease);
}
.hero__social a:hover { color: var(--cat-color, var(--pink)); transform: translateY(-2px); }

/* ==========================================================================
   PANNEAUX CATÉGORIE (glissants depuis la droite)
   ========================================================================== */
/* LE MENU RESTE VISIBLE, MAIS IL RECULE  (v=95)
   Un noir a 40% cachait le menu sans le mettre a distance : on le voyait
   aussi net qu'avant, juste plus sombre. Le flou fait le travail que
   l'opacite ne sait pas faire — il dit « ceci est derriere », l'oeil cesse
   d'essayer de le lire et se pose sur le panneau. Le noir descend a 22% en
   echange : c'est le flou qui assombrit, via brightness.
   Meme geste que la porte de l'intranet, en deux fois moins appuye : ici le
   fond doit rester reconnaissable, on n'est pas en train de fermer le site. */
.panel-overlay {
  position: fixed; inset: 0; z-index: 47;
  background: rgba(0,0,0,0.22);
  -webkit-backdrop-filter: blur(9px) brightness(0.5) saturate(0.85);
  backdrop-filter: blur(9px) brightness(0.5) saturate(0.85);
  opacity: 0; pointer-events: none;
  transition: opacity 0.45s var(--ease);
}
.panel-overlay.is-visible { opacity: 1; pointer-events: auto; }
#videoModalOverlay.is-visible { background: rgba(4,4,5,0.94); z-index: 47; }
/* L'Atelier est déjà opaque et plein écran : ce fond n'a jamais eu d'utilité visuelle,
   et son z-index ne doit surtout pas passer au-dessus du contenu de l'Atelier. */
#atelierOverlay { display: none !important; }

.panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 48;
  width: min(78vw, 1600px);
  background: var(--bg-panel);
  border-left: 1px solid var(--line);
  transform: translateX(100%);
  transition: transform 0.45s var(--ease);
  display: flex; flex-direction: column;
  overflow: hidden;
}
/* LE RELIEF DU PANNEAU  (v=95)
   Le panneau ne se posait pas SUR le site, il le remplacait : un simple filet
   a gauche, et rien qui dise l'epaisseur. Trois couches, de la plus serree a
   la plus large :
     1. une arete claire d'un pixel sur le bord gauche — la lumiere qui accroche
        le chant du panneau au moment ou il sort ;
     2. la couleur de la categorie, tres courte, juste derriere cette arete ;
     3. l'ombre portee, large et molle, qui creuse le fond flou.
   L'ombre est declaree ici et non sur .is-open : sinon elle apparaissait d'un
   coup en fin de glissement au lieu d'accompagner le mouvement. */
.panel.is-open {
  transform: translateX(0);
  /* Repli sans color-mix (navigateurs < 2023) : sans lui, TOUTE l'ombre — y
     compris la profondeur noire — tombait d'un coup et le panneau paraissait
     plat. Ici l'ombre teintee est fixee au rose de marque. */
  box-shadow:
    -1px 0 0 rgba(255, 255, 255, 0.14),
    -6px 0 26px -6px rgba(255, 45, 120, 0.30),
    -46px 0 110px rgba(0, 0, 0, 0.82);
  /* Version qui suit la couleur de la categorie (navigateurs recents) : elle
     ecrase le repli ci-dessus la ou color-mix est compris. */
  box-shadow:
    -1px 0 0 rgba(255, 255, 255, 0.14),
    -6px 0 26px -6px color-mix(in srgb, var(--cat-color, var(--pink)) 42%, transparent),
    -46px 0 110px rgba(0, 0, 0, 0.82);
}

.panel__close {
  position: absolute; top: 24px; right: 32px; z-index: 2;
  width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.2);
  display: flex; align-items: center; justify-content: center;
  color: #fff; transition: background 0.2s var(--ease);
}
.panel__close:hover { background: rgba(255,255,255,0.08); }

.panel__eyebrow {
  padding: 32px 48px 0;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.08em;
  color: var(--fg-dim);
  display: flex; align-items: center; gap: 8px;
}
.panel__eyebrow .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cat-color, var(--pink)); }

/* L'ÉQUILIBRE TEXTE / FILMS  (v=95)
   1fr 1fr donnait autant de place a la fiche commerciale qu'aux films. Le
   texte n'a pas besoin de la moitie d'un panneau de 78vw : une colonne de
   lecture confortable plafonne vers 480px, au-dela on ne lit pas mieux, on lit
   moins bien (l'oeil perd sa ligne). Le rab va donc aux films, et la grille de
   droite y gagne une a deux colonnes de cartes en plus — c'est-a-dire des
   films visibles sans avoir a cliquer sur PLUS.
   minmax() et non un pourcentage sec : sur un ecran etroit, la colonne ne
   descend pas sous 300px, sous quoi les puces de services se casseraient. */
.panel__body {
  flex: 1;
  display: grid; grid-template-columns: minmax(300px, 34%) 1fr;
  gap: 44px;
  padding: 20px 48px 24px;
  overflow: hidden;
}
.panel__left { overflow-y: auto; padding-right: 12px; }
.panel__right { display: flex; flex-direction: column; overflow: hidden; }

/* --------------------------------------------------------------------------
   LA COLONNE DE LECTURE  (v=95)
   Moins large ne veut pas dire moins lisible : c'est meme l'inverse. En
   rendant la moitie de sa largeur, cette colonne gagne le droit d'etre lue
   pour de bon — on lui donne donc un interlignage de vrai texte (1.7), une
   couleur qui ne demande plus d'effort (#c8c8cc et non #9a9a9f), et un rythme
   vertical serre pour que la fiche tienne d'un regard.
   -------------------------------------------------------------------------- */
.panel__left h2 {
  font-size: 1.72rem; font-weight: 800; line-height: 1.12;
  letter-spacing: -0.015em; margin-bottom: 12px;
}
.panel__left > p.desc {
  color: #c8c8cc; line-height: 1.7; font-size: 0.94rem; margin-bottom: 22px;
}
/* Un filet a la couleur de la categorie sous le chapeau : il tient la colonne
   et rappelle de quel univers on parle, sans coûter une ligne de texte. */
.panel__left > p.desc::after {
  content: ''; display: block; width: 44px; height: 2px; margin-top: 22px;
  background: var(--cat-color, var(--pink)); opacity: 0.8;
}

.info-box {
  border: 1px solid var(--line);
  padding: 15px 17px;
  margin-bottom: 14px;
  border-radius: 4px;
}
.info-box__label { font-family: var(--font-mono); font-size: 0.64rem; color: var(--fg-dim); letter-spacing: 0.08em; margin-bottom: 11px; }
.info-box .desc { color: #b8b8be; line-height: 1.62; font-size: 0.88rem; }

/* LES SERVICES EN PASTILLES  (v=95)
   La liste en deux colonnes gaspillait : quatre lignes hautes pour quatre
   mots. En pastilles, les memes services tiennent sur deux lignes courtes, se
   lisent d'un coup d'oeil, et cessent de ressembler a un cahier des charges —
   ils ressemblent a une offre. Le ✓ disparait : le cadre le dit deja. */
.check-list { display: flex; flex-wrap: wrap; gap: 7px; list-style: none; }
.check-list li {
  font-size: 0.79rem; line-height: 1;
  padding: 7px 11px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--cat-color, var(--pink)) 34%, transparent);
  background: color-mix(in srgb, var(--cat-color, var(--pink)) 8%, transparent);
  color: #e6e6e8; white-space: nowrap;
}

.case-study { border: 1px solid var(--cat-color, var(--pink)); background: color-mix(in srgb, var(--cat-color, var(--pink)) 8%, transparent); }
.case-study .info-box__label { color: var(--cat-color, var(--pink)); }
.case-study h4 { margin-bottom: 8px; }

/* Playfair italique : la DA le reserve aux citations, c'est exactement ici.
   Il tranche avec l'Inter du reste de la colonne et fait respirer la fiche. */
.quote-box {
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 16px 0; margin: 16px 0;
  font-family: var(--font-serif); font-style: italic;
  font-size: 1rem; line-height: 1.55; color: #c8c8cc;
}
.quote-box cite { display: block; margin-top: 9px; font-family: var(--font-mono); font-style: normal; color: var(--fg-dim); font-size: 0.7rem; letter-spacing: 0.04em; }

.cta-row {
  display: block; width: 100%; text-align: center;
  border: 1px solid var(--line); padding: 16px;
  font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.05em;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.cta-row:hover { border-color: var(--cat-color, var(--pink)); background: color-mix(in srgb, var(--cat-color, var(--pink)) 6%, transparent); }

/* ==========================================================================
   LE SÉLECTEUR FILMS / PHOTOS  (v=95)
   Une bascule a deux volets, posee au-dessus de la grille. Le curseur
   (.panel__switch-thumb) glisse d'un volet a l'autre : c'est lui qui porte la
   couleur de la categorie, les deux boutons ne font que s'allumer par-dessus.
   Un fond par bouton aurait clignote a chaque bascule ; un curseur qui glisse
   dit le mouvement, et le mouvement se remarque.
   ========================================================================== */
/* L'ENVELOPPE DU SELECTEUR (v=183) — inerte partout sauf sur telephone, ou
   elle devient le bandeau collant (section 58). Elle reprend le `align-self`
   du selecteur : dans la colonne en flex du panneau desktop, elle se retrecit
   donc a son contenu exactement comme le selecteur le faisait seul, et rien
   ne bouge d'un pixel. */
.panel__switchbar { align-self: flex-start; }

.panel__switch {
  position: relative; align-self: flex-start;
  display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 2px; padding: 3px; margin-bottom: 16px;
  border: 1px solid var(--line); border-radius: 999px;
  background: rgba(255,255,255,0.03);
}
/* Le curseur est SOUS les boutons (z-index) et se deplace en translateX : la
   largeur d'un volet vaut 50% de la piste, moins les 3px de padding. */
.panel__switch-thumb {
  position: absolute; z-index: 0; top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px); border-radius: 999px;
  background: color-mix(in srgb, var(--cat-color, var(--pink)) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--cat-color, var(--pink)) 55%, transparent);
  transition: transform 0.38s var(--ease), background 0.3s var(--ease);
}
.panel__switch[data-view="photos"] .panel__switch-thumb { transform: translateX(100%); }
/* Une seule collection (categorie sans photo, ou sans film) : le volet restant
   occupe toute la piste. Sans ce cas, le curseur gardait sa demi-largeur et
   flottait a cote du seul bouton, ou partait hors de la piste en translateX. */
.panel__switch--solo .panel__switch-thumb { width: calc(100% - 6px); transform: none; }
.panel__switch--solo .panel__switch-btn { cursor: default; }

.panel__switch-btn {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 9px 26px; border-radius: 999px; border: 1px solid transparent;
  background: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.74rem;
  letter-spacing: 0.16em; text-indent: 0.16em;
  color: var(--fg-dim);
  transition: color 0.3s var(--ease);
}
.panel__switch-btn:hover { color: #fff; }
.panel__switch-btn.is-active { color: #fff; }
.panel__switch-count {
  font-size: 0.62rem; letter-spacing: 0.04em; text-indent: 0;
  color: var(--fg-faint); transition: color 0.3s var(--ease);
}
.panel__switch-btn.is-active .panel__switch-count { color: var(--cat-color, var(--pink)); }
.panel__switch-btn:focus-visible { outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 3px; }

/* La lueur d'appel a ete retiree (v=106) : le halo pulsant sous le selecteur
   FILMS/PHOTOS ne tombait jamais juste et n'apportait rien. Le selecteur reste
   parfaitement utilisable sans — le curseur colore et les libelles suffisent. */

@media (prefers-reduced-motion: reduce) {
  .panel__switch-thumb { transition: none; }
}

/* Les deux vues occupent le meme creneau ; une seule est montee a la fois
   (l'autre est retiree du DOM en JS, pas seulement cachee — sinon ses images
   continuaient d'etre chargees et decodees pour rien). */
/* LA MARGE DE LA LUEUR  (v=97)
   overflow-y: auto suffit a rogner AUSSI l'horizontale : des qu'un axe cesse
   d'etre `visible`, l'autre passe en `auto`. Le debordement lateral etait donc
   coupe a zero pixel, et la lueur coloree du survol — qui deborde de 7px —
   se faisait trancher net contre la colonne de texte.
   La reponse n'est pas de retirer le rognage (il tient le defilement) mais de
   lui donner de quoi mordre : ces 16px sont la reserve dans laquelle la lueur
   a le droit de deborder. En haut, 12px couvrent les 7px de lueur plus les 3px
   d'elevation de la carte survolee.
   L'ombre NOIRE, elle, deborde de 22px et reste coupee — mais du noir coupe
   sur du noir ne se voit pas, ces 6px manquants ne coutent rien. */
.panel__cards {
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  padding: 12px 16px 24px;
}
.panel__cards--photos .genre-row + .genre-row { margin-top: 30px; }

/* ==========================================================================
   LIGNES PAR GENRE — colonne de droite du panneau categorie
   Cartes verticales, une ligne par genre. Le nombre de colonnes s'adapte a la
   largeur (auto-fill) ; le JS masque ensuite le surplus et pose la carte
   "PLUS" au dernier emplacement. La colonne defile si les lignes debordent.
   ========================================================================== */
.genre-row + .genre-row { margin-top: 34px; }

/* Filet + libelle : le trait file jusqu'au compteur, il structure la ligne
   sans ajouter de bloc. */
.genre-row__head {
  display: flex; align-items: baseline; gap: 14px;
  margin-bottom: 14px;
}
.genre-row__title {
  font-family: var(--font-body);
  font-size: 0.82rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: #fff;
  white-space: nowrap;
}
.genre-row__head::after {
  content: ''; flex: 1; height: 1px; order: 1;
  background: linear-gradient(90deg, var(--cat-color, var(--pink)) 0%, rgba(255,255,255,0.08) 60%, transparent 100%);
  opacity: 0.5;
}
.genre-row__count {
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.1em;
  color: var(--fg-faint); white-space: nowrap;
}

/* -- RÉDUIRE : le chemin du retour  (v=95) -------------------------------
   PLUS ouvrait une porte sans poignee de l'autre cote : une fois la rangee
   depliee, on ne pouvait plus la replier, et les rangees suivantes partaient
   hors de l'ecran. Le retour se pose dans l'en-tete plutot que dans la grille :
   la ou PLUS occupe une case, REDUIRE ne doit rien deranger — la grille est
   pleine, justement.
   Il n'existe que sur une rangee depliee (pose en JS via .is-expanded). */
.genre-row__less {
  display: none;
  /* order : le filet (::after) est du contenu genere, il vient toujours en
     dernier dans le DOM. Sans ces ordres, le bouton se serait plante entre le
     compteur et le filet, au milieu de la ligne. */
  order: 2;
  align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.1em;
  color: var(--fg-dim); background: none; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px;
  white-space: nowrap;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.genre-row.is-expanded .genre-row__less { display: inline-flex; }
.genre-row__less:hover { color: #fff; border-color: var(--cat-color, var(--pink)); }
.genre-row__less:focus-visible { outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 2px; }
.genre-row__less svg { transition: transform 0.25s var(--ease); }
.genre-row__less:hover svg { transform: translateY(-2px); }

/* -- LES CARTES PHOTO ----------------------------------------------------
   Elles reprennent .card entier — meme forme, meme retrait des voisines, meme
   carte PLUS. Une photo et un film se cliquent de la meme facon dans ce
   panneau ; leur seule difference est ce qui s'ouvre ensuite. */
/* ==========================================================================
   LA MOSAÏQUE PHOTO  (v=97)
   Les photos etaient des vignettes de 128px recadrees en 3/4 : une planche
   contact, pas une galerie. Or une photo recadree n'est plus la photo — le
   cadrage EST le travail, le rogner revient a montrer autre chose.
   Trois colonnes larges, chaque image a son rapport vrai (--ar, pose au rendu
   depuis w/h) : une verticale reste verticale, une horizontale reste
   horizontale, et les hauteurs de la grille varient d'elles-memes. C'est ce
   depaysement des hauteurs qui fait respirer une mosaique — une grille dont
   toutes les cases ont la meme taille se lit comme un tableau Excel.
   align-items: center — la ligne prend la hauteur de sa plus grande photo, les
   autres s'y centrent plutot que de coller en haut : le vide se repartit au
   lieu de tomber tout entier sous l'image.
   ========================================================================== */
.genre-row__grid--photos {
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
  align-items: start;
  /* dense : quand une horizontale (2 colonnes) laisse un trou, une carte plus
     petite vient le combler au lieu de laisser un vide — mosaique serree facon
     LunorStills. */
  grid-auto-flow: row dense;
}
/* Une horizontale prend deux colonnes : largeur doublee -> hauteur alignee sur
   une verticale a une colonne. Si la grille n'a qu'UNE colonne (petit ecran),
   le span 2 est ignore par le navigateur (il ne peut pas depasser le nombre de
   colonnes), donc rien ne casse. */
.genre-row__grid--photos .card--wide { grid-column: span 2; }
/* card--tall a ete retire de la carte photo : plus rien ne lui impose 3/4, et
   --ar peut faire son travail. */
.genre-row__grid--photos .card--photo {
  aspect-ratio: var(--ar, 3/4);
  border-radius: 3px;
}
.genre-row__grid--photos .card--more { aspect-ratio: 1/1; }

/* LA LEGENDE NE S'INVITE PLUS
   Les titres viennent des noms de fichiers (« Photo Album 1 00000021 ») : ils
   n'apprennent rien et posaient un bandeau noir en bas de chaque image. Ils
   restent lisibles — au survol, quand on a justement demande a en savoir plus
   sur CETTE photo. Au repos, la mosaique n'est que des photos. */
.genre-row__grid--photos .card__scrim,
.genre-row__grid--photos .card__body {
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}
.genre-row__grid--photos .card--photo:hover .card__scrim,
.genre-row__grid--photos .card--photo:hover .card__body { opacity: 1; }

.genre-row__grid--photos .card--photo .card__title {
  font-size: 0.68rem; font-weight: 500; font-family: var(--font-mono);
  letter-spacing: 0.02em; line-height: 1.35; color: #e2e2e6;
}
.card--photo .card__scrim { background: linear-gradient(180deg, transparent 55%, rgba(0,0,0,0.88) 100%); }

.genre-row__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 10px;
}
.genre-row__grid .card--tall { aspect-ratio: 3/4; }
.genre-row__grid .card--tall .card__title { font-size: 0.82rem; line-height: 1.25; }
.genre-row__grid .card--tall .card__director,
.genre-row__grid .card--tall .card__year,
.genre-row__grid .card--tall .card__featuring { display: none; }
.genre-row__grid .card--tall .card__body { left: 10px; right: 10px; bottom: 10px; }
.genre-row__grid .card--tall .card__tag { font-size: 0.55rem; padding: 3px 6px; }
/* filter et box-shadow sont repris ici : cette regle est PLUS specifique que
   `.card` et, sans eux, elle effacait la transition du retrait des voisines —
   les cartes de la grille auraient vire au gris sans transition. */
.genre-row__grid .card {
  transition: transform 0.3s var(--ease), filter 0.45s var(--ease),
              box-shadow 0.35s var(--ease);
}
.genre-row__grid .card:hover { transform: translateY(-3px); }

/* -- Carte "PLUS" : le surplus, devine mais pas montre -------------------- */
.card--more {
  position: relative; aspect-ratio: 3/4;
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 4px; overflow: hidden;
  background: #0d0d10; cursor: pointer; padding: 0;
  transition: border-color 0.3s var(--ease), transform 0.3s var(--ease);
}
/* Noir et blanc + floue : on lit qu'il y a des images derriere, sans les lire. */
.card--more .card__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover;
  filter: grayscale(1) blur(7px) brightness(0.42) contrast(1.1);
  transform: scale(1.15);
  transition: transform 0.5s var(--ease), filter 0.5s var(--ease);
}
.card--more__veil {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(8,8,10,0.55) 0%, rgba(8,8,10,0.82) 100%);
}
.card--more__inner {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px;
}
.card--more__count {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.08em;
  color: var(--cat-color, var(--pink));
}
.card--more__label {
  font-family: var(--font-body); font-weight: 700;
  font-size: 0.78rem; letter-spacing: 0.28em; text-indent: 0.28em;
  color: #fff;
}
.card--more__arrow { color: rgba(255,255,255,0.55); line-height: 0; }
.card--more__arrow svg { transition: transform 0.35s var(--ease); }
/* Au survol : le trait s'allonge vers la droite, la couleur de la categorie
   prend le dessus. Le mouvement dit "il y a une suite" mieux qu'un libelle. */
.card--more:hover { border-color: var(--cat-color, var(--pink)); transform: translateY(-3px); }
.card--more:hover .card__img { transform: scale(1.22); filter: grayscale(1) blur(5px) brightness(0.5) contrast(1.1); }
.card--more:hover .card--more__arrow { color: var(--cat-color, var(--pink)); }
.card--more:hover .card--more__arrow svg { transform: translateX(5px); }
.card--more:focus-visible { outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .genre-row__grid .card, .card--more, .card--more .card__img, .card--more__arrow svg { transition: none; }
}

.card {
  position: relative;
  aspect-ratio: 16/9;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  background: #111;
  /* filter est dans la transition : c'est lui qui porte le retrait des
     voisines, plus bas. Sans lui, elles viraient au gris d'un coup sec. */
  transition: transform 0.35s var(--ease), filter 0.45s var(--ease),
              box-shadow 0.35s var(--ease);
}
.card__img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  /* LA COULEUR AU REPOS  (v=94)
     Le noir et blanc au repos rendait la grille inerte : rien n'y appelait le
     clic, et le survol devait tout faire. La couleur est la des le depart,
     juste retenue d'un cran de lumiere — le survol garde ainsi ou monter. */
  filter: brightness(0.86) saturate(1.04);
  transition: filter 0.5s var(--ease), transform 0.5s var(--ease);
}
.card:hover .card__img { filter: brightness(1.06) saturate(1.16) contrast(1.03); transform: scale(1.04); }

/* --------------------------------------------------------------------------
   LE RETRAIT DES VOISINES — grille du panneau  (v=94)
   Meme geste que dans LunorFilms et dans LunorStills : le relief ne vient pas
   de la carte survolee qui s'allume, il vient des AUTRES qui se retirent —
   noir et blanc, un souffle de flou, un cheveu plus petites. La couleur ne
   subsiste alors qu'a un seul endroit de l'ecran : celui que la souris
   designe. C'est ce contraste qui fait la main, pas la luminosite.
   :has() et non :hover sur la grille : avec :hover, tout s'eteignait des que
   la souris passait dans une gouttiere, sans qu'aucune carte ne s'allume
   (lecon deja payee sur .stills__gallery et .discover-row__track).
   Le :not(:hover) suffit a epargner la carte active : inutile de lui reecrire
   une contre-regle, aucune des deux ne s'applique a elle.
   Deux selecteurs, car les cartes ont deux logements : dans .genre-row__grid
   pour les categories a sous-categories (Nightlife, Brand Content), et a meme
   #panelCards pour les autres (Mariage, Artistes) — d'ou le > .
   -------------------------------------------------------------------------- */
@media (hover: hover) {
  /* Le voile grisant ne vise QUE les cartes video (v=106) : sur les photos, le
     :not(.card--photo) l'exclut. Une galerie photo dont tout s'eteint sauf une
     image faisait "vitrine" ; l'utilisateur veut que les photos restent nettes.
     Les cartes video, elles, gardent l'effet. */
  .genre-row__grid:has(.card:not(.card--photo):hover) .card:not(:hover):not(.card--photo),
  #panelCards:has(> .card:not(.card--photo):hover) > .card:not(:hover):not(.card--photo) {
    filter: grayscale(1) brightness(0.5) blur(1.4px);
    transform: scale(0.985);
  }
  /* La carte survolee se decolle : liseré blanc, nappe a la couleur de la
     categorie, ombre portee. Sobre — la couleur retrouvee fait deja le gros
     du travail. */
  .genre-row__grid:has(.card:hover) .card:hover,
  #panelCards:has(> .card:hover) > .card:hover {
    z-index: 2;
    box-shadow:
      0 0 0 1px rgba(255, 255, 255, 0.4),
      0 0 26px -6px var(--cat-color, var(--pink)),
      0 18px 44px rgba(0, 0, 0, 0.7);
  }
}
@media (prefers-reduced-motion: reduce) {
  .card, .card__img { transition: none; }
}
.card__scrim { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 40%, rgba(0,0,0,0.85) 100%); }
.card__tag {
  position: absolute; top: 14px; left: 14px;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.06em;
  padding: 4px 10px; background: rgba(0,0,0,0.6);
  border: 1px solid var(--cat-color, var(--pink)); color: var(--cat-color, var(--pink));
}
.card__year { position: absolute; top: 14px; right: 14px; font-family: var(--font-mono); font-size: 0.65rem; color: var(--fg-dim); }
.card__body { position: absolute; left: 16px; right: 16px; bottom: 14px; }
.card__director { font-family: var(--font-mono); font-size: 0.65rem; color: var(--fg-dim); margin-bottom: 4px; }
/* Titres de films : même typo que la bannière LunorFilms — Inter en 800, droit.
   Playfair italique reste réservé aux citations (manifeste, témoignages, intro). */
.card__title {
  font-family: var(--font-body); font-style: normal; font-weight: 800;
  font-size: 1.3rem; line-height: 1.15; letter-spacing: -0.01em;
}
.card__tagline {
  font-size: 0.8rem; line-height: 1.4; color: var(--cat-color, var(--pink)); margin-top: 6px;
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.35s var(--ease), opacity 0.35s var(--ease);
}
/* 3 lignes de 0,8rem a 1,4 d'interligne = ~3.4em ; on prend 3.6em pour l'air
   en bas. Au-dela de 3 lignes, line-clamp coupe avec des points de suspension
   plutot qu'en plein milieu d'un mot (v=109). Avant : max-height 40px (~2
   lignes) + overflow hidden -> une 3e ligne etait tranchee net.
   Le clamp s'applique a la tagline entiere : le tiret ::before, etant inline,
   fait partie de la premiere ligne du bloc. */
.card:hover .card__tagline {
  /* 3 lignes x 1,4 d'interligne = 4,2em. L'ancienne valeur 3,6em coupait la 3e
     ligne en deux dans le sens de la HAUTEUR (le line-clamp gere le nombre de
     lignes, mais max-height le rognait avant). 4,5em laisse les 3 lignes
     entieres respirer. (v=110) */
  max-height: 4.5em; opacity: 1;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
}
.card__tagline::before { content: ''; display: inline-block; width: 22px; height: 1px; background: var(--cat-color, var(--pink)); vertical-align: middle; margin-right: 8px; }

.panel__footer {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px 48px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 0.68rem; color: var(--fg-dim);
}
.panel__footer-social { display: flex; gap: 10px; }
.panel__footer-social a { color: var(--fg-dim); transition: color 0.2s var(--ease); }
.panel__footer-social a:hover { color: #fff; }

/* ==========================================================================
   MODALE VIDÉO (façon Netflix)
   ========================================================================== */
.video-modal {
  position: fixed; top: 50%; left: 50%; z-index: 50;
  width: min(84vw, 1280px);
  height: 92vh;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 40px 100px rgba(0,0,0,0.7);
  opacity: 0; pointer-events: none;
  transform: translate(-50%, -50%) scale(0.96);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.video-modal.is-open { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }

.video-modal__close {
  position: absolute; top: 16px; right: 16px; z-index: 30;
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(10,10,12,0.6); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  border: 1px solid rgba(255,255,255,0.15);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.video-modal__close:hover { background: var(--cat-color, var(--pink)); color: #0a0a0a; transform: rotate(90deg); }

.video-modal__media { position: relative; flex: 0 0 46%; background: #111; overflow: hidden; }
.video-modal__media img { width: 100%; height: 100%; object-fit: cover; }
.video-modal__media video { width: 100%; height: 100%; object-fit: contain; background: #000; }
/* Bouton de sortie du plein écran : superposé à la vidéo, donc visible
   même quand le navigateur masque le reste de la page en plein écran. */
.video-modal__exit-fs {
  position: absolute; top: 20px; right: 20px; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: rgba(10,10,12,0.45); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,0.16);
  color: #fff;
  cursor: pointer;
  opacity: 0.55;
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease),
              opacity 0.2s var(--ease), transform 0.2s var(--ease);
}
/* Discrète au repos, franche au survol : elle ne mange plus l'image. */
.video-modal__exit-fs:hover {
  opacity: 1;
  background: var(--cat-color, var(--pink));
  border-color: var(--cat-color, var(--pink));
  color: #0a0a0a;
  transform: rotate(90deg);
}
.video-modal__exit-fs svg { flex-shrink: 0; }

/* --------------------------------------------------------------------------
   MODE LECTURE "plein écran maison" : le lecteur couvre toute la fenêtre,
   sans passer par le plein écran natif du navigateur (qui superposait sa
   propre interface). is-playing est posée en JS au lancement de la vidéo.

   IMPORTANT — pourquoi c'est la MODALE qui s'agrandit, et pas le média :
   .video-modal est centrée avec transform: translate(-50%,-50%). Or un
   ancêtre porteur d'un transform devient le "containing block" de ses
   descendants en position: fixed. Un enfant en fixed + inset: 0 se calait
   donc sur la boîte de la modale (84vw x 92vh) et non sur la fenêtre :
   le plein écran ne prenait pas tout l'écran. On neutralise donc le
   transform sur .video-modal en mode lecture et c'est elle qui occupe la
   fenêtre ; le média n'a plus qu'à la remplir en absolute.
   -------------------------------------------------------------------------- */
.video-modal.is-playing,
.video-modal.is-open.is-playing,
.video-modal.is-portrait.is-playing,
.video-modal.is-open.is-portrait.is-playing {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  width: 100%;
  height: 100vh;      /* repli pour les vieux navigateurs */
  height: 100dvh;     /* tient compte des barres mobiles */
  max-width: none;
  transform: none;
  transition: none;
  flex-direction: column;
  background: #000;
  border: none;
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
  z-index: 90;
}
/* Le média occupe toute la modale devenue plein écran. */
.video-modal.is-playing.is-portrait .video-modal__media,
.video-modal.is-playing.is-landscape .video-modal__media,
.video-modal.is-playing .video-modal__media {
  position: absolute !important;
  inset: 0 !important;
  z-index: 1 !important;
  flex: none !important;
  width: 100% !important;
  height: 100% !important;
  border: none !important;
  border-right: none !important;
  border-radius: 0 !important;
  background: #000 !important;
}
.video-modal.is-playing .video-modal__media video {
  width: 100% !important; height: 100% !important; object-fit: contain !important; background: #000 !important;
}
/* La fiche info est masquée pendant la lecture (sinon elle garde sa place
   dans le flex et rogne la hauteur du lecteur). */
.video-modal.is-playing .video-modal__info { display: none !important; }
/* Décalé vers le bas : en haut à droite, le bouton recouvrait le curseur de
   volume des contrôles natifs du lecteur (Safari), qui devenait inutilisable. */
/* Descendue : a 24px du haut, la croix recouvrait le curseur de volume des
   controles natifs de Safari, qui devenait inutilisable. */
.video-modal.is-playing .video-modal__exit-fs {
  position: absolute; top: 78px; right: 24px; z-index: 95;
}
/* La croix de fermeture ferait doublon avec "Quitter le plein écran" au même
   endroit : on la masque pendant la lecture. */
.video-modal.is-playing .video-modal__close { display: none; }
/* On masque le dégradé du bas et la vignette en mode lecture. */
.video-modal.is-playing .video-modal__media::after { display: none; }
.video-modal__loading {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5,5,6,0.55);
}
.video-modal__spinner {
  width: 34px; height: 34px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.2);
  border-top-color: var(--cat-color, var(--pink));
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.video-modal__media::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 55%, rgba(10,10,12,0.9) 100%);
  pointer-events: none;
}
.video-modal__tags {
  position: absolute; bottom: 18px; right: 20px; z-index: 1;
  display: inline-flex; align-items: center; gap: 10px;
  width: fit-content;
  background: rgba(10,10,12,0.5); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  border: 1px solid rgba(255,255,255,0.12);
  padding: 6px 12px; border-radius: 3px;
  font-family: var(--font-mono); font-size: 0.66rem;
  color: var(--fg-dim);
}
.video-modal__tags .tag-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.video-modal__tags .tag-sep { width: 1px; height: 11px; background: rgba(255,255,255,0.15); }
.video-modal__tags .tag-icon { color: var(--cat-color, var(--pink)); flex-shrink: 0; }
/* --------------------------------------------------------------------------
   BOUTON LECTURE : anneau circulaire en verre dépoli + libellé en dessous.
   Pas de cadre autour du texte (l'ancienne boîte rectangulaire faisait
   "lecteur générique") : le cercle porte l'action, le libellé accompagne.
   -------------------------------------------------------------------------- */
.video-modal__play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 18px;
  background: none; border: none; padding: 0;
}
.video-modal__play-ring {
  position: relative;
  width: 78px; height: 78px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,10,12,0.3);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.55);
  color: #fff;
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease),
              color 0.35s var(--ease), transform 0.35s var(--ease);
}
/* Le triangle est optiquement décentré : on le rattrape de 2px. */
.video-modal__play-ring svg { margin-left: 3px; }
/* Halo qui respire, pour attirer l'œil sans clignoter. */
.video-modal__play-ring::before {
  content: ''; position: absolute; inset: -1px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.35);
  animation: play-halo 2.8s var(--ease) infinite;
  pointer-events: none;
}
@keyframes play-halo {
  0%   { transform: scale(1);    opacity: 0.7; }
  70%  { transform: scale(1.45); opacity: 0; }
  100% { transform: scale(1.45); opacity: 0; }
}
.video-modal__play-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.72);
  white-space: nowrap;
  /* L'interlettrage ajoute une espace après la dernière lettre : le mot
     paraîtrait décalé à gauche. On le recentre optiquement. */
  margin-left: 0.34em;
  transition: color 0.35s var(--ease);
}
.video-modal__play:hover .video-modal__play-ring {
  background: var(--cat-color, var(--pink));
  border-color: var(--cat-color, var(--pink));
  color: #0a0a0a;
  transform: scale(1.06);
}
.video-modal__play:hover .video-modal__play-ring::before { animation: none; opacity: 0; }
.video-modal__play:hover .video-modal__play-label { color: #fff; }
.video-modal__play:focus-visible { outline: none; }
.video-modal__play:focus-visible .video-modal__play-ring {
  outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 4px;
}

.video-modal__info {
  flex: 1; overflow-y: auto;
  display: grid; grid-template-columns: 2fr 1fr; gap: 48px;
  padding: 36px 44px 44px;
}
.video-modal__breadcrumb { font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em; color: var(--cat-color, var(--pink)); margin-bottom: 8px; }
.video-modal__title {
  font-family: var(--font-body); font-style: normal; font-weight: 800;
  font-size: 2.3rem; line-height: 1.05; letter-spacing: -0.015em;
  margin-bottom: 22px;
}
.video-modal__main h4 { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em; color: var(--fg-dim); margin-bottom: 10px; }
.video-modal__synopsis { font-size: 0.92rem; line-height: 1.7; color: var(--fg-dim); white-space: pre-line; }

.video-modal__tech h4 { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em; color: var(--fg-dim); margin-bottom: 16px; }
/* Chaque paire libelle+valeur est un bloc : la valeur est TOUJOURS sous son
   libelle (v=107). Avant, label et value etaient des freres directs qui
   tombaient dans les cases successives de la grille 2 colonnes — le libelle a
   gauche, sa valeur a droite, au lieu de dessous. Le regroupement corrige ca
   sans dependre du sens de lecture de la grille. */
.video-modal__tech .video-modal__spec { margin-top: 14px; }
.video-modal__tech .label { font-family: var(--font-mono); font-size: 0.62rem; color: var(--fg-faint); }
.video-modal__tech .value { font-size: 0.9rem; margin-top: 4px; }
.video-modal__swatches { display: flex; gap: 8px; margin-top: 8px; }
.video-modal__swatches span { width: 30px; height: 30px; border-radius: 4px; border: 1px solid var(--line); }
.video-modal__lut-desc { font-size: 0.76rem; font-style: italic; color: var(--fg-dim); margin-top: 12px; line-height: 1.5; }

/* --------------------------------------------------------------------------
   VARIANTE PORTRAIT (vidéos 9:16) — média vertical à gauche, infos à droite.
   Sélectionnée automatiquement en JS via aspectRatio (classe .is-portrait).
   La disposition par défaut (média large en haut) reste celle du paysage.
   -------------------------------------------------------------------------- */
.video-modal.is-portrait {
  width: min(78vw, 880px);
  height: 88vh;
  flex-direction: row;
}
.video-modal.is-portrait .video-modal__media {
  flex: 0 0 46%;
  height: 100%;
  border-right: 1px solid var(--line);
}
/* En portrait, le visuel remplit toute la colonne média (cadrage plein,
   sans bande vide autour). */
.video-modal.is-portrait .video-modal__media img { object-fit: cover; background: #0a0a0c; }
.video-modal.is-portrait .video-modal__media::after {
  background: linear-gradient(180deg, transparent 70%, rgba(10,10,12,0.85) 100%);
}
.video-modal.is-portrait .video-modal__info {
  flex: 1;
  display: block;
  padding: 40px 42px;
}
.video-modal.is-portrait .video-modal__title { font-size: 1.9rem; }
.video-modal.is-portrait .video-modal__main { margin-bottom: 32px; }
/* La fiche technique passe sous le synopsis, separee par un filet. */
.video-modal.is-portrait .video-modal__tech {
  border-top: 1px solid var(--line);
  padding-top: 28px;
}
/* En portrait la colonne est plus etroite : on aere la fiche technique
   sur deux colonnes pour eviter une longue liste verticale. */
.video-modal.is-portrait .video-modal__tech {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 32px;
}
.video-modal.is-portrait .video-modal__tech h4 { grid-column: 1 / -1; }
.video-modal.is-portrait .video-modal__tech .video-modal__spec { margin-top: 0; }
.video-modal.is-portrait .video-modal__tech .video-modal__lut-block { grid-column: 1 / -1; margin-top: 8px; }

/* --------------------------------------------------------------------------
   VARIANTE PAYSAGE (16:9, Univisium…) — média large en haut.
   Avant : largeur min(84vw,1280px) + object-fit:cover -> la boîte média était
   bien plus large que haute, donc la vignette 16:9 était rognée en haut/bas.
   Maintenant : la largeur est bornée par la hauteur de la fenêtre (92vh) pour
   que le média garde son ratio 16/9 sans jamais déborder, et l'image est en
   object-fit:contain -> on voit la totalité du visuel.
   :not(.is-playing) : en lecture plein écran, les règles .is-playing priment.
   -------------------------------------------------------------------------- */
.video-modal.is-landscape:not(.is-playing) {
  width: min(72vw, 1080px, 92vh);
  height: auto;
  max-height: 92vh;
}
.video-modal.is-landscape:not(.is-playing) .video-modal__media {
  flex: 0 0 auto;
  width: 100%;
  aspect-ratio: 16 / 9;
}
/* Visuel entier, jamais recadré ; le fond comble les éventuelles bandes. */
.video-modal.is-landscape:not(.is-playing) .video-modal__media img {
  object-fit: contain;
  background: #0a0a0c;
}
/* La fiche info prend le reste et défile si le synopsis est long. */
.video-modal.is-landscape:not(.is-playing) .video-modal__info {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* ==========================================================================
   L'ATELIER (page à onglets)
   ========================================================================== */
.atelier {
  position: fixed; inset: 0; z-index: 45;
  background: var(--bg);
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none; transform: translateY(12px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.atelier.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }

.atelier__header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 48px; border-bottom: 1px solid var(--line);
}
.atelier__title-block { display: flex; align-items: center; gap: 20px; }
.panel__close--inline { position: static; }
.atelier__eyebrow { font-family: var(--font-mono); font-size: 0.7rem; color: var(--pink); letter-spacing: 0.06em; margin-bottom: 4px; }
.atelier__title-block h2 { font-size: 1rem; font-weight: 700; }

.atelier__tabs { display: flex; gap: 4px; }
.atelier__tab {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
  color: var(--fg-dim); padding: 12px 18px;
  transition: background 0.2s var(--ease), color 0.2s var(--ease);
}
.atelier__tab.is-active { background: var(--pink); color: #0a0a0a; font-weight: 600; }
.atelier__tab:not(.is-active):hover { color: #fff; }

.atelier__body { flex: 1; overflow-y: auto; padding: 88px 72px; }
.atelier__panel { display: none; max-width: 1400px; margin: 0 auto; }
.atelier__panel.is-active { display: block; }

/* -- Onglet Manifeste (traitement premium : espace généreux, hairlines, serif discret) -- */
.manifeste { display: grid; grid-template-columns: 1.5fr 1fr; gap: 100px; align-items: start; }

.eyebrow-mark {
  display: inline-flex; align-items: center; gap: 14px;
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--fg-dim); margin-bottom: 48px;
}
.eyebrow-mark::before { content: ''; width: 28px; height: 1px; background: var(--pink); }

.manifeste h3 {
  font-family: var(--font-body); font-weight: 800; font-style: normal;
  font-size: clamp(1.9rem, 2.4vw, 2.5rem);
  line-height: 1.2; letter-spacing: -0.005em;
  max-width: 16ch;
  color: #fff;
  margin-bottom: 36px;
}
.manifeste .quote {
  font-family: var(--font-serif); font-style: italic; font-weight: 400;
  color: var(--fg-dim); font-size: 1.05rem; line-height: 1.6;
  max-width: 42ch;
  padding-left: 22px; border-left: 1px solid rgba(255,45,120,0.35);
  margin-bottom: 48px;
}
.manifeste p {
  color: var(--fg-dim); font-size: 0.94rem; line-height: 1.9;
  max-width: 58ch; letter-spacing: 0.001em;
  margin-bottom: 26px;
}

.manifeste .side-card { border: 1px solid var(--line); overflow: hidden; }
.manifeste .side-card__media { aspect-ratio: 4/3; background: linear-gradient(135deg, #14141a, #1c1420); position: relative; overflow: hidden; }
/* Le visuel remplit le cadre 4:3 ; le voile assure la lisibilite du libelle. */
.manifeste .side-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.manifeste .side-card__media::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 55%, rgba(10,10,12,0.7) 100%);
}
.manifeste .side-card__media .label { z-index: 2; 
  position: absolute; bottom: 20px; left: 20px;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fg-dim);
}
.manifeste .side-card__body { padding: 36px 32px; }
.manifeste .side-card__body h4 {
  font-family: var(--font-body); font-weight: 700; font-size: 1.2rem;
  line-height: 1.4; margin-bottom: 16px; color: #fff;
}
.manifeste .side-card__body p { font-size: 0.86rem; line-height: 1.75; color: var(--fg-dim); max-width: none; margin-bottom: 0; }
.manifeste .side-card .cta-row {
  margin-top: 28px; border: none; border-top: 1px solid var(--line);
  padding: 20px 0 0; text-align: left; color: var(--pink);
  font-size: 0.68rem; letter-spacing: 0.1em;
}
.manifeste .side-card .cta-row:hover { background: none; border-top-color: var(--pink); }

.day-night { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; margin: 56px 0; padding-top: 40px; border-top: 1px solid var(--line); }
.day-night > div { padding: 0; }
.day-night h5 { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 16px; color: var(--fg-faint); }
.day-night p { color: var(--fg-dim); font-size: 0.88rem; line-height: 1.75; margin: 0; max-width: 32ch; }

.manifeste .info-box__label {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--fg-faint); margin-bottom: 28px;
}
.team-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 48px; margin-top: 0; }
.team-grid > div { padding: 24px 0 0; border-top: 1px solid var(--line); }
.team-grid .team-label { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 14px; color: var(--fg-faint); }
.team-grid h5 { font-family: var(--font-body); font-weight: 700; font-size: 1.15rem; margin-bottom: 12px; color: #fff; }
.team-grid p { color: var(--fg-dim); font-size: 0.85rem; line-height: 1.7; margin-bottom: 20px; max-width: none; }
.team-grid .loc { font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.06em; color: var(--fg-faint); }
.manifeste-footer-note {
  margin-top: 64px; padding-top: 28px; border-top: 1px solid var(--line);
  display: flex; align-items: center; gap: 10px; color: var(--fg-faint); font-size: 0.72rem; letter-spacing: 0.04em;
}

/* -- Onglet Méthode -- */
.methode { text-align: center; }
.methode .eyebrow { font-family: var(--font-mono); font-size: 0.75rem; color: var(--pink); letter-spacing: 0.08em; margin-bottom: 12px; }
.methode h3 { font-size: 2.2rem; font-weight: 800; margin-bottom: 14px; }
.methode > p { color: var(--fg-dim); max-width: 700px; margin: 0 auto 40px; }
.steps { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; text-align: left; }
.steps > div {
  border: 1px solid var(--line); padding: 22px;
  position: relative;
  background: rgba(255,255,255,0.012);
  /* 0.55s : assez lent pour que le relief se pose au lieu de claquer. */
  transition: transform 0.55s var(--ease), border-color 0.55s var(--ease),
              background 0.55s var(--ease), box-shadow 0.55s var(--ease),
              opacity 0.55s var(--ease), filter 0.55s var(--ease);
}
/* Filet a la couleur de l'etape, qui se trace de gauche a droite au survol. */
.steps > div::after {
  content: ''; position: absolute; left: -1px; right: -1px; top: -1px; height: 2px;
  background: var(--step-color, var(--pink));
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.55s var(--ease);
}

/* Le relief ne se joue qu'a la souris : sur ecran tactile, :hover reste colle
   apres le tap et donnerait une carte figee en surbrillance. */
@media (hover: hover) {
  /* Des qu'une carte est survolee, les AUTRES reculent : c'est le contraste
     qui cree le relief, pas seulement l'elevation de la carte active. */
  .steps:hover > div {
    opacity: 0.4;
    filter: saturate(0.35);
    transform: scale(0.985);
  }
  .steps > div:hover {
    opacity: 1;
    filter: none;
    transform: translateY(-8px) scale(1.015);
    border-color: rgba(255,255,255,0.16);
    background: rgba(255,255,255,0.03);
    box-shadow: 0 22px 50px rgba(0,0,0,0.6), 0 0 45px -18px var(--step-color, var(--pink));
    z-index: 2;
  }
  .steps > div:hover::after { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .steps > div, .steps > div::after { transition: none; }
  .steps:hover > div { opacity: 1; filter: none; transform: none; }
  .steps > div:hover { transform: none; }
}
.steps .num { font-family: var(--font-mono); font-size: 1.3rem; font-weight: 700; margin-bottom: 14px; }
.steps h5 { margin-bottom: 10px; font-size: 1rem; }
.steps p { color: var(--fg-dim); font-size: 0.85rem; line-height: 1.5; }

/* -- Onglet Réalisateurs -- */
/* Les cartes tenaient sur 640px fixes + 40px de marge : ca depassait la zone
   visible et forcait le scroll. On leur fait remplir la hauteur disponible. */
.atelier__panel[data-panel="realisateurs"].is-active {
  height: 100%;
  display: flex; align-items: stretch;
}
/* 88px de marge haute + basse mangeaient la hauteur utile : sur cet onglet on
   les reduit, ce qui rend les cartes plus grandes sans faire apparaitre de
   scroll. overflow:hidden verrouille la promesse "pas de scroll". */
.atelier__body[data-tab="realisateurs"] { padding: 28px 72px; overflow: hidden; }
.realisateurs {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 22px;
  max-width: 1180px; margin: 0 auto;
  width: 100%; height: 100%;
}
.director-card {
  border: 1px solid var(--line);
  min-height: 0;           /* sans ca, le contenu impose sa hauteur et deborde */
  display: flex; flex-direction: column;
  background: rgba(255,255,255,0.012);
  transition: transform 0.55s var(--ease), border-color 0.55s var(--ease),
              background 0.55s var(--ease), box-shadow 0.55s var(--ease),
              opacity 0.55s var(--ease), filter 0.55s var(--ease);
}

/* Meme relief que les etapes de la Methode : la carte survolee monte, l'autre
   recule. Souris uniquement : au tactile, :hover resterait colle apres le tap. */
@media (hover: hover) {
  .realisateurs:hover .director-card {
    opacity: 0.42;
    filter: saturate(0.3);
    transform: scale(0.985);
  }
  .realisateurs .director-card:hover {
    opacity: 1;
    filter: none;
    transform: translateY(-8px) scale(1.015);
    border-color: rgba(255,255,255,0.18);
    background: rgba(255,255,255,0.03);
    box-shadow: 0 24px 55px rgba(0,0,0,0.62), 0 0 45px -18px var(--pink);
    z-index: 2;
  }
}
@media (prefers-reduced-motion: reduce) {
  .director-card { transition: none; }
  .realisateurs:hover .director-card { opacity: 1; filter: none; transform: none; }
  .realisateurs .director-card:hover { transform: none; }
}
.director-card__photo { flex: 1; background: #111; position: relative; overflow: hidden; }
/* Le portrait remplit la carte ; le dégradé du bas assure la lisibilité si la
   photo est claire à cet endroit. */
/* object-position est pose en inline depuis content.json (champ photoPosition) :
   chaque portrait a son propre point d'ancrage, sinon cover recadre au centre
   et coupe les tetes des que la carte se raccourcit. */
.director-card__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.director-card__photo::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(10,10,12,0.35) 0%, transparent 30%, transparent 70%, rgba(10,10,12,0.55) 100%);
}
.director-card__badge { position: absolute; top: 16px; left: 16px; z-index: 2; font-family: var(--font-mono); font-size: 0.62rem; background: var(--pink); color: #0a0a0a; padding: 4px 10px; }
.director-card__body { padding: 20px; flex: 0 0 auto; }
.director-card__role { font-family: var(--font-mono); font-size: 0.65rem; color: var(--fg-dim); letter-spacing: 0.05em; margin-bottom: 8px; }
.director-card__body h4 { font-size: 1.4rem; margin-bottom: 8px; }
.director-card__body p { color: var(--fg-dim); font-size: 0.88rem; }

/* -- Onglet Expertise -- */
.expertise { display: grid; grid-template-columns: 1.4fr 1fr; gap: 50px; align-items: start; }
/* Le defilement interne de cette colonne a ete retire en v=172 : la liste
   est devenue un accordeon et tient a l'ecran — voir la section 51. */
.expertise__list { padding-right: 12px; }
.expertise__item { border: 1px solid var(--line); padding: 22px; margin-bottom: 16px; }
.expertise__item .num { color: var(--pink); font-family: var(--font-mono); font-weight: 700; margin-right: 10px; }
.expertise__item h5 { display: inline; font-size: 1.05rem; }
.expertise__item p { color: var(--fg-dim); margin: 12px 0 14px; line-height: 1.6; }
.tag-list { display: flex; flex-wrap: wrap; gap: 8px; }
.tag-list span { font-family: var(--font-mono); font-size: 0.68rem; border: 1px solid var(--line); padding: 5px 10px; color: var(--fg-dim); }

.testimonials-label { font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em; color: #a679f0; margin-bottom: 18px; }
.testimonial-card { border: 1px solid var(--line); padding: 24px; margin-bottom: 16px; position: relative; }
.testimonial-card::before { content: '"'; font-family: var(--font-serif); font-size: 3rem; color: var(--line); position: absolute; top: 4px; left: 16px; }
.testimonial-card p.q { font-style: italic; color: var(--fg-dim); line-height: 1.6; margin-bottom: 14px; padding-left: 8px; }
.testimonial-card cite { font-size: 0.82rem; color: #a679f0; font-style: normal; }

/* ==========================================================================
   FORMULAIRE DE CONTACT / BRIEF
   ========================================================================== */
.panel--form { width: min(58vw, 1040px); --cat-color: var(--pink); --cat-color-dim: rgba(255,45,120,0.13); }
.brief-form { flex: 1; min-height: 0; overflow-y: auto; padding: 80px 60px 0; max-width: 760px; }
.brief-form h2 { font-size: 2rem; font-weight: 700; margin-bottom: 20px; letter-spacing: -0.005em; }
.brief-form__intro { color: var(--fg-dim); max-width: 540px; line-height: 1.8; font-size: 0.88rem; margin-bottom: 8px; }

.brief-form__section-num {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.16em;
  color: var(--pink); margin: 56px 0 24px; padding-bottom: 14px;
  border-bottom: 1px solid rgba(255,45,120,0.25);
}
.brief-form .req { color: var(--pink); }

.brief-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; margin-bottom: 44px; }
.brief-form label {
  display: block; font-family: var(--font-mono); font-size: 0.62rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-faint); margin-bottom: 12px;
}
.brief-form input, .brief-form textarea {
  width: 100%; margin-top: 12px;
  background: rgba(255,255,255,0.02); border: 1px solid var(--line);
  color: #fff; padding: 17px 18px; font-size: 0.82rem; border-radius: 2px;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}
.brief-form input:focus, .brief-form textarea:focus { border-color: var(--pink); background: rgba(255,45,120,0.03); }
.brief-form textarea { min-height: 120px; resize: vertical; }

.brief-form__label { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-faint); margin: 36px 0 18px; }
.brief-form__choices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 8px; }
.brief-form__choices--row { grid-template-columns: repeat(3, 1fr); }
.choice-btn {
  border: 1px solid var(--line); padding: 18px 20px; text-align: left;
  font-size: 0.82rem; color: var(--fg-dim); background: transparent;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease), color 0.25s var(--ease), transform 0.25s var(--ease);
}
.brief-form__choices--row .choice-btn { text-align: center; font-family: var(--font-mono); font-size: 0.72rem; }
.choice-btn:hover { border-color: rgba(255,45,120,0.5); transform: translateY(-1px); }
.choice-btn.is-selected { border-color: var(--pink); background: rgba(255,45,120,0.08); color: var(--pink); font-weight: 600; }

.brief-form__message { margin: 52px 0; }
.brief-form__note { font-size: 0.7rem; color: var(--fg-faint); text-align: center; margin: 20px 0 72px; }

.brief-form .btn--full {
  padding: 22px; font-size: 0.82rem; letter-spacing: 0.08em;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), filter 0.25s var(--ease);
}
.brief-form .btn--full:hover { transform: translateY(-2px); box-shadow: 0 16px 40px rgba(255,45,120,0.25); filter: brightness(1.08); }

/* ==========================================================================
   ESPACE DÉCOUVERTE "LUNORFILMS" — vitrine libre, distincte du site commercial
   ========================================================================== */
.discover {
  position: fixed; inset: 0; z-index: 46;
  background: var(--bg);
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none; transform: translateY(14px);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}
.discover.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }

.discover__header {
  position: absolute; top: 0; left: 0; right: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between;
  padding: 26px 48px;
  background: linear-gradient(180deg, rgba(5,5,6,0.85) 0%, transparent 100%);
}
.discover__back {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em;
  color: var(--fg-dim); transition: color 0.2s var(--ease);
}
.discover__back:hover { color: #fff; }
.discover__kicker {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.16em;
  color: var(--cat-color, var(--pink)); text-transform: uppercase;
  transition: color 620ms var(--ease);
}
/* Rose fixe dans les DEUX univers (v=108) : « GALERIE DES CRÉATIONS » comme
   « GALERIE PHOTOGRAPHIQUE » restent à la marque quelle que soit la catégorie.
   La règle de base (--cat-color) ne sert plus qu'au cas par défaut ; les deux
   pages la surchargent. */
#discoverPage .discover__kicker,
#stillsPage .discover__kicker { color: var(--pink); }
.discover__header-actions { display: flex; align-items: center; gap: 12px; }
.discover__contact-btn { font-size: 0.66rem; padding: 11px 18px; }

.discover__body { flex: 1; overflow-y: auto; }

.discover__hero {
  position: relative; height: 68vh; min-height: 480px;
  display: flex; align-items: flex-end;
  padding: 0 64px 56px;
  background-size: cover; background-position: center;
  overflow: hidden;
  transition: height 2.2s var(--ease), padding-bottom 2.2s var(--ease);
}
.discover__hero-video {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
}
.discover__hero::before {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(180deg, rgba(5,5,6,0.15) 0%, rgba(5,5,6,0.35) 55%, var(--bg) 100%),
    linear-gradient(90deg, rgba(5,5,6,0.75) 0%, transparent 45%);
  transition: opacity 1.4s var(--ease);
}

/* --------------------------------------------------------------------------
   MODE CINÉMA — bannière d'exploration
   Quand le son est ouvert et que la souris s'arrête 1,5 s, la bannière prend
   tout l'écran : le dégradé, les textes et les rangées s'effacent, il ne reste
   que l'image et le son. Le moindre mouvement de souris ramène tout en place.
   La montée dure 2,2 s pour que ce soit un glissement, pas un basculement.
   -------------------------------------------------------------------------- */
.discover.is-cinema .discover__body { overflow: hidden; }
.discover.is-cinema .discover__hero { height: 100vh; padding-bottom: 0; }
.discover.is-cinema .discover__hero::before { opacity: 0; }
.discover__header,
.discover__brand,
.discover__hero-content,
.discover__rows,
.discover__intro {
  transition: opacity 1.4s var(--ease);
}
.discover.is-cinema .discover__header,
.discover.is-cinema .discover__brand,
.discover.is-cinema .discover__hero-content,
.discover.is-cinema .discover__rows,
.discover.is-cinema .discover__intro {
  opacity: 0; pointer-events: none;
}
.discover__hero-content { position: relative; z-index: 2; max-width: 640px; }
.discover__hero-tag {
  display: inline-block; font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.1em;
  /* Rose, et non la couleur de la categorie (v=106) : dans LunorFilms le badge
     doit rester a la marque. Le contour de "FILMS" et le kicker, eux, suivent
     toujours la categorie de la banniere — c'est --cat-color qui les porte. */
  color: var(--pink); border: 1px solid var(--pink);
  padding: 5px 12px; margin-bottom: 20px;
}
.discover__hero-title {
  font-family: var(--font-body); font-style: normal; font-weight: 700;
  font-size: clamp(1.7rem, 2.9vw, 2.8rem); line-height: 1.12;
  letter-spacing: -0.015em; color: #fff; margin-bottom: 16px;
}
.discover__hero-desc { color: var(--fg-dim); font-size: 0.95rem; line-height: 1.7; max-width: 46ch; margin-bottom: 28px; }
.discover__hero-actions { display: flex; gap: 14px; }

/* Le logotype de LunorFilms : meme construction que LunorStills (.brandmark),
   un cran plus petit — il est pose sur la banniere video, pas sur du noir, et
   il ne doit pas ecraser l'image. */
.discover__brand {
  position: absolute; top: 84px; left: 64px; z-index: 2;
}
.discover__wordmark { font-size: clamp(1.8rem, 4.4vw, 3.6rem); }
/* Le contour de « FILMS » repasse rose (v=107), quelle que soit la catégorie
   de la bannière — même logique que le kicker : la marque prime ici. Borné a
   #discoverPage pour ne pas toucher LunorStills, qui partage .brandmark. */
#discoverPage .discover__wordmark .m-line {
  -webkit-text-stroke-color: color-mix(in srgb, var(--pink) 62%, transparent);
}
/* STILLS aussi en rose fixe (v=109) : le contour de LunorStills ne suit plus la
   catégorie épinglée. Même logique que FILMS — la marque prime. Le contour de
   STUDIO CRÉATIF sur l'accueil, lui, garde --cat-color (autre élément). */
#stillsPage .stills__brand-mark .m-line {
  -webkit-text-stroke-color: color-mix(in srgb, var(--pink) 62%, transparent);
}

.discover__rows { padding: 12px 0 8px; }
.discover-row { position: relative; margin-bottom: 52px; }
.discover-row__title {
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fg-dim); margin: 0 64px 16px;
}
/* position: relative — c'est le repere de la carte qui s'agrandit. Elle passe
   en position: absolute SANS quitter sa piste, donc elle suit le defilement
   horizontal de la rangee au lieu d'etre collee a l'ecran.

   La generosite verticale du padding n'est pas decorative : une piste qui
   defile horizontalement rogne aussi ce qui deborde en HAUT et en BAS (des
   qu'un overflow n'est pas "visible", l'autre axe cesse de l'etre). Sans cette
   marge interieure, la carte basculee serait coupee. Les marges negatives
   rendent a la mise en page les pixels que le padding lui prend : a l'oeil,
   l'espacement des rangees ne bouge pas d'un poil. */
.discover-row__track {
  position: relative;
  display: flex; gap: 14px; overflow-x: auto; scroll-behavior: smooth;
  /* LA RESERVE. Une piste qui defile horizontalement rogne aussi ce qui
     deborde en HAUT et en BAS : des qu'un overflow n'est pas "visible",
     l'autre axe cesse de l'etre. Tout ce qui sort de ce cadre est coupe NET —
     c'est ce qui tranchait la lueur a l'horizontale.
     Le compte, pour une carte basculee de 336px sur une place de 158 :
       en haut  : (336-158)/2 = 89, plus 26 de lueur       -> 115 minimum
       en bas   : 89, plus 61 d'ombre portee (26 de decalage + 35 de flou) -> 150
     D'ou 130px de rab de chaque cote, avec de la marge. Les marges negatives
     rendent ces pixels a la mise en page : a l'oeil, l'espacement des rangees
     ne bouge pas d'un cheveu. */
  padding: 136px 64px 160px; scrollbar-width: none;
  margin: -130px 0;

  /* LA PISTE NE CAPTE PLUS RIEN, SEULES LES CARTES LE FONT.
     Sans ca, cette reserve serait un piege : les pistes voisines, agrandies de
     130px chacune, se chevauchent largement, et celle du dessous — plus loin
     dans le HTML — volerait le survol des cartes de celle du dessus. Un cadre
     transparent au pointeur ne vole rien a personne.
     Le defilement a la molette continue de fonctionner : l'evenement vise la
     carte, puis remonte jusqu'a la piste qui defile. Les fleches < > sont
     soeurs de la piste, elles ne sont pas concernees. */
  pointer-events: none;
}
.discover-row__track::-webkit-scrollbar { display: none; }

.discover-row__card {
  position: relative; flex: 0 0 280px; aspect-ratio: 16/9;
  border-radius: 4px; overflow: hidden; cursor: pointer;
  /* filter fait partie de la transition : sans lui, les voisines s'eteignaient
     d'un coup sec au lieu de se retirer. Il est volontairement plus lent que
     le reste (0.5s contre 0.35s) : la couleur doit se retirer plus lentement
     que la carte ne monte, sinon le noir et blanc arrive avant le relief et
     on voit deux gestes au lieu d'un. */
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease),
              filter 0.5s var(--ease);
  background: #111;
}
.discover-row__card:hover { transform: scale(1.09); z-index: 3; }

/* Seules les cartes captent la souris ; leur piste, elle, est transparente au
   pointeur (voir .discover-row__track).
   IMPERATIF : ce "auto" est cantonne a .discover.is-open. Ecrit tel quel sur
   la carte, il RALLUMERAIT les clics alors meme que la galerie est fermee :
   .discover est en pointer-events:none au repos pour laisser passer les clics
   vers l'accueil, mais un descendant qui redit "auto" reprend la main sur cet
   ancetre. Les cartes de la galerie fermee interceptaient alors le bouton
   LunorFilms de la page d'accueil, par-dessus tout le reste. */
.discover.is-open .discover-row__card { pointer-events: auto; }

/* --------------------------------------------------------------------------
   LA LUEUR — survol d'une carte  (v=90)
   Quatre couches d'ombre, et l'ordre compte : la plus serree en premier.
     1. un liseré blanc d'un pixel, colle au cadre : c'est l'arete eclairee ;
     2. un halo blanc court, qui fait deborder cette lumiere sur le noir ;
     3. une nappe COLOREE courte et discrete — la lueur qu'on croit voir passer
        DERRIERE la carte, comme un projecteur cache dessous. Elle se tient
        pres du cadre : sa portee compte autant que son intensite, une nappe
        large finit par teinter tout le noir de la rangee au lieu de designer
        une carte ;
     4. l'ombre portee, qui la decolle du fond.
   Tout est en box-shadow, jamais en pseudo-element : la carte est en
   overflow:hidden (les coins arrondis rognent la video), et un ::before aurait
   ete coupe net par ce meme rognage. Une ombre, elle, se peint dehors.

   LA NAPPE SUIT LA CATEGORIE DU FILM — MAIS PAS D'ICI  (v=174)
   Ce qui est ecrit ci-dessous est le ROSE DE REPLI, et il ne bouge plus.
   La couleur reelle vient d'une feuille fabriquee au chargement par
   injecterLueursCategories() (script.js), qui ecrit une regle par categorie
   avec des valeurs entierement litterales — `[data-cat="brand-content"]:hover`
   et ainsi de suite, plus specifique que la regle generique, donc gagnante.

   POURQUOI PAS UNE VARIABLE CSS ICI, alors que ce serait plus court.
   La v=173 l'avait fait : `color-mix(in srgb, var(--cat-color-item) 34%,
   transparent)`, la variable etant posee en ligne sur chaque carte. Le rendu
   etait juste, mais `box-shadow` est EN TRANSITION sur cette carte (0,35 s, en
   meme temps que le scale du survol). Faire dependre d'une variable une
   propriete animee oblige a la resoudre a chaque recalcul de style, et une
   variable en ligne sur la carte est heritee par tous ses descendants — le
   voile, les infos, le titre — qui n'en ont aucun usage. C'est precisement le
   genre de travail en plus qui, en plein survol et au demarrage de l'apercu,
   se paye par le trou d'une fraction de seconde dans le tag et les infos.
   NE PAS REINTRODUIRE DE var() NI DE color-mix DANS CETTE DECLARATION.
   -------------------------------------------------------------------------- */
.discover-row__card:hover,
.discover-row__card.is-expanded {
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.55),
    0 0 20px 1px rgba(255, 255, 255, 0.22),
    0 0 46px 3px rgba(255, 45, 120, 0.34),
    0 26px 70px rgba(0, 0, 0, 0.75);
}

/* --------------------------------------------------------------------------
   LE RETRAIT DES VOISINES
   L'assombrissement ne vient pas d'un voile pose sur la page : ce sont les
   AUTRES cartes de la rangee qui s'effacent et se troublent. Le noir se creuse
   donc autour de la carte survolee, et nulle part ailleurs — le reste de la
   page, le heros, les autres rangees ne bougent pas.
   :has() et non :hover sur la piste : avec :hover, tout s'eteignait des que la
   souris passait dans une gouttiere, sans qu'aucune carte ne s'allume.
   -------------------------------------------------------------------------- */
@media (hover: hover) {
  .discover-row__track:has(.discover-row__card:hover) .discover-row__card:not(:hover) {
    /* Les voisines se retirent, elles ne s'eteignent pas : on doit continuer a
       les lire pour savoir ou aller ensuite. Le noir et blanc est franc — le
       saturate(0.75) d'avant laissait une couleur tiede qui ne tranchait avec
       rien, alors que c'est precisement ce tranchage qui designe la carte
       vivante.

       LE `blur(1.4px)` A ETE RETIRE  (v=166), ET C'EST UNE DECISION DE COUT.
       Il ne se voyait presque pas a cote d'un noir et blanc a 52 % de
       luminosite, mais c'etait de loin le plus cher des trois : `grayscale` et
       `brightness` se calculent pixel par pixel, un flou est une CONVOLUTION —
       chaque pixel lit ses voisins. Et il ne s'applique pas a une image, mais
       aux SEPT autres cartes de la rangee, toutes en meme temps, au moment
       precis du survol. Tant que ces images ne sont pas encore decodees — les
       dix a vingt premieres secondes de la page — ce filtre force leur decodage
       ET leur convolution d'un seul coup, en pleine animation de la carte
       survolee. C'est la depense la plus lourde qui ait lieu a cet instant, et
       la seule dont le cout DIMINUE une fois la page installee : ca correspond
       exactement a la fenetre ou le defaut se produit. */
    filter: grayscale(1) brightness(0.52);
    transform: scale(0.98);
  }
}
.discover-row__card.is-expanded {
  /* transform fait partie de la transition. Sans lui, le scale(1.09) du survol
     retombait a 1 d'un seul coup au moment ou la bascule commencait : c'etait
     le a-coup qu'on voyait au demarrage. Les quatre cotes sont animes
     ensemble, sinon la largeur sautait pendant que la hauteur glissait. */
  transform: none !important;
  z-index: 5;
  border-radius: 6px;
  transition: left 0.45s var(--ease), top 0.45s var(--ease),
              width 0.45s var(--ease), height 0.45s var(--ease),
              transform 0.45s var(--ease), box-shadow 0.45s var(--ease);
  will-change: width, height;
}
/* Le fantome garde la place de la carte dans la file pendant sa bascule. Ses
   dimensions sont posees en JS, d'apres la carte elle-meme. */
.discover-row__card-placeholder { flex: 0 0 280px; visibility: hidden; }
.discover-row__card img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  /* LA COULEUR AU REPOS  (v=94) — voir .card__img.
     Une rangee grise se parcourt des yeux ; une rangee en couleur se clique.
     Le noir et blanc n'a pas disparu pour autant : il est passe du repos au
     retrait, sur les voisines de la carte survolee (regle plus haut). */
  filter: brightness(0.88) saturate(1.04);
  transition: filter 0.5s var(--ease);
}
.discover-row__card:hover img { filter: brightness(1.06) saturate(1.16); }
.discover-row__preview {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 0.4s var(--ease); pointer-events: none;
  /* z-index explicite BAS (v=110) : la video reste sous les surcouches, quoi
     qu'il arrive. */
  z-index: 0;
  /* ET PROMUE DES LE DEPART (v=165). C'est la ligne qui compte. Une <video> en
     lecture est promue sur sa propre couche par le navigateur ; si elle ne
     l'est pas avant, cette couche NAIT au demarrage de l'apercu, en plein
     survol, et l'arbre des couches de la carte est reconstruit sous nos pieds —
     c'est ce trou d'une fraction de seconde dans le tag et les infos. En la
     promouvant alors qu'elle est encore vide et invisible, la couche existe
     deja quand la lecture commence : il ne se cree plus rien. */
  transform: translateZ(0);
}
/* ==========================================================================
   L'ARBRE DES COUCHES D'UNE CARTE EST FIGE AVANT TOUT SURVOL  (v=165)
   --------------------------------------------------------------------------
   TROISIEME PASSE SUR LE MEME DEFAUT : categorie, titre, realisateur et lieu
   disparaissent une fraction de seconde au demarrage de l'apercu, puis
   reviennent. Les deux tentatives precedentes ont echoue, et elles echouaient
   pour la meme raison — elles se disputaient QUI devait etre sur une couche,
   alors que le probleme est QUAND les couches sont creees.

     v=110 : on avait promu les surcouches (translateZ) pour qu'elles passent
             devant la video. Elles etaient bien devant, et elles clignotaient
             quand meme.
     v=164 : on avait retire cette promotion, en pensant que la re-rasterisation
             des couches manuelles etait la cause. Le clignotement est reste.

   CE QUI RESTE VRAI DANS LES DEUX CAS : la video, elle, n'est PAS sur une
   couche au repos. Elle le devient au moment ou elle demarre — une <video> en
   lecture est toujours promue. Le navigateur doit alors reconstruire l'arbre
   des couches de la carte EN PLEIN SURVOL, pendant qu'elle grandit, et tout ce
   qui la recouvre doit etre re-decoupe. C'est cette reconstruction qu'on voit,
   et elle a lieu que les surcouches soient promues a la main ou non.

   D'OU LE CHANGEMENT D'ANGLE : on ne discute plus des plans, on fait exister
   toutes les couches DES LA CONSTRUCTION DE LA PAGE. La video est promue alors
   qu'elle est encore vide et invisible, les surcouches aussi. Quand l'apercu
   demarre, il ne se cree plus rien : la video remplit une couche qui l'attend
   deja. Il n'y a plus de reconstruction, donc plus de trou.

   LE COUT EST BORNE, ET IL FALLAIT LE VERIFIER : la <video> d'apercu n'existe
   que sur ordinateur (renderDiscoverRow ne l'ecrit pas sous 1024px), et une
   couche vide de 280x158 ne coute qu'une allocation, pas un decodage. Rien de
   tout ceci n'atteint le telephone, ou les couches sont justement comptees.

   SI LE DEFAUT REVENAIT ENCORE : ne pas repartir sur les z-index, ils sont
   justes et verifies trois fois. Demander alors le navigateur exact, si ca
   touche aussi les cartes horizontales (qui ne basculent pas) et combien de
   temps dure le trou — ces trois reponses separent une reconstruction de
   couches d'un probleme de decodage.
   ========================================================================== */
.discover-row__card { isolation: isolate; }
.discover-row__card .scrim,
.discover-row__card .tag,
.discover-row__card .info,
.discover-row__card .discover-row__hover-actions {
  z-index: 2;
  transform: translateZ(0);
}
.discover-row__card .scrim { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 45%, rgba(0,0,0,0.9) 100%); }
.discover-row__card .tag {
  position: absolute; top: 10px; left: 10px;
  font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.05em;
  padding: 3px 9px; background: rgba(0,0,0,0.6); color: var(--cat-color-item, var(--pink));
  border: 1px solid var(--cat-color-item, var(--pink));
}
/* z-index: 3, ET PAS 1 (v=156). Cette ligne disait 1 depuis l'origine, alors
   que la regle groupee juste au-dessus pose .scrim, .tag et .info a 2 — meme
   specificite (0,2,0), mais elle arrive apres, donc elle gagnait. Le degrade
   etait donc peint PAR-DESSUS le titre : a l'endroit du texte il vaut 0,7 a
   0,9 de noir, et un blanc pur en ressortait gris. Peu visible ici parce que
   l'affiche est grande et que le texte tombe haut dans la rampe ; flagrant sur
   telephone, ou l'affiche fait 195 points. 3 et pas 2 : a valeur egale c'est
   l'ordre du HTML qui tranche, et on ne veut pas que ca depende de lui. */
.discover-row__card .info { position: absolute; left: 14px; right: 14px; bottom: 12px; z-index: 3; }
.discover-row__card .director { font-family: var(--font-mono); font-size: 0.6rem; color: var(--fg-dim); margin-bottom: 3px; }
.discover-row__card .title { font-family: var(--font-body); font-weight: 700; font-size: 1rem; color: #fff; }

.discover-row__hover-actions {
  position: absolute; top: 10px; right: 10px; z-index: 2;
  display: flex; gap: 8px;
  opacity: 0; transform: translateY(-4px);
  transition: opacity 0.25s var(--ease), transform 0.25s var(--ease);
}
.discover-row__card:hover .discover-row__hover-actions { opacity: 1; transform: translateY(0); }
.dr-icon-btn {
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,10,12,0.7); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  border: 1px solid rgba(255,255,255,0.25); color: #fff;
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.dr-icon-btn:hover { border-color: var(--cat-color-item, var(--pink)); color: var(--cat-color-item, var(--pink)); transform: scale(1.1); }
.dr-icon-btn--play svg { margin-left: 1px; }

.discover-row__arrow {
  position: absolute; top: 0; bottom: 30px; width: 60px; z-index: 4;
  display: flex; align-items: center; justify-content: center;
  color: #fff; opacity: 0; transition: opacity 0.25s var(--ease);
}
.discover-row:hover .discover-row__arrow { opacity: 1; }
.discover-row__arrow--left { left: 0; background: linear-gradient(90deg, var(--bg) 15%, transparent 100%); }
.discover-row__arrow--right { right: 0; background: linear-gradient(270deg, var(--bg) 15%, transparent 100%); }

.discover__intro {
  max-width: 56ch; margin: 20px auto 90px; padding: 0 64px;
  text-align: center; font-style: italic; font-family: var(--font-serif);
  color: var(--fg-faint); font-size: 0.9rem; line-height: 1.7;
}

/* ==========================================================================
   MENU MOBILE / TABLETTE
   ========================================================================== */
.mobile-menu-btn { display: none; }

.mobile-nav {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(8,8,10,0.85);
  backdrop-filter: blur(24px) saturate(160%);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 40px;
  opacity: 0; pointer-events: none; transform: translateY(-12px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.mobile-nav.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.mobile-nav__close {
  position: absolute; top: 24px; right: 24px;
  width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.15);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
}
.mobile-nav__links { display: flex; flex-direction: column; align-items: center; gap: 28px; }
.mobile-nav__links a {
  font-family: var(--font-body); font-weight: 700; font-size: 1.6rem; color: var(--fg-dim);
  transition: color 0.2s var(--ease);
}
.mobile-nav__links a:hover, .mobile-nav__links a.is-active { color: #fff; }

/* ==========================================================================
   RESPONSIVE — tablette & téléphone
   ========================================================================== */
@media (max-width: 1100px) {
  .panel__body, .video-modal__info, .manifeste, .expertise { grid-template-columns: 1fr; }
  /* LE VRAI PROBLEME : en une seule colonne, .panel__left et .panel__right
     restent chacun un bloc a defilement INDEPENDANT (overflow-y:auto sur
     l'un, sur l'autre) empile dans une grille dont le PARENT est lui-meme en
     overflow:hidden. Resultat : la fiche texte se comporte comme sa propre
     fenetre a hauteur figee, avec son propre ascenseur, AVANT meme d'arriver
     aux films — d'ou l'impression que "le texte prend toute la place".
     On n'imbrique plus qu'un seul defilement naturel, celui de .panel__body
     lui-meme : le texte, puis la grille de films, se lisent d'affilee. */
  .panel__body { overflow-y: auto; overflow-x: hidden; display: block; }
  .panel__left, .panel__right, .panel__cards { overflow: visible; }
  .panel__left { padding-right: 0; margin-bottom: 8px; }
  /* Sur petit écran, la modale portrait repasse en pile verticale (média en haut). */
  .video-modal.is-portrait { flex-direction: column; }
  .video-modal.is-portrait .video-modal__media { flex: 0 0 46%; border-right: none; }
  .video-modal.is-portrait .video-modal__tech { grid-template-columns: 1fr; }
  .steps, .team-grid, .realisateurs { grid-template-columns: repeat(2, 1fr); }
  .panel, .panel--form { width: 92vw; }

  .nav { grid-template-columns: auto auto; left: 16px; right: 16px; padding: 12px 18px; }
  .nav__links { display: none; }
  .nav__actions .icon-btn:not(.mobile-menu-btn):not(#muteToggle) { display: none; }
  .mobile-menu-btn { display: flex; }
  .nav__actions #contactTrigger { padding: 12px 14px; font-size: 0.62rem; }

  /* Sur mobile/tablette, le menu doit rester accessible sans avoir à survoler */
  .nav-zone { height: auto; }
  .nav { transform: translateY(0); opacity: 1; pointer-events: auto; position: relative; top: 16px; margin: 0 16px; left: 0; right: 0; }
  .hover-hint { display: none; }
}

/* ==========================================================================
   NOUVEAUX ÉLÉMENTS : badge Instagram, galerie photo, sous-groupe de portfolio
   ========================================================================== */
.ig-badge {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--cat-color, var(--pink)); font-size: 0.72rem;
}
.card__featuring { font-size: 0.78rem; color: var(--fg-dim); margin-top: 4px; }


.video-modal__featuring { margin: -10px 0 24px; }
.video-modal__featuring .featuring-name { font-weight: 700; font-size: 1rem; margin-right: 14px; }
.video-modal__featuring .featuring-note { color: var(--fg-dim); font-size: 0.82rem; margin-top: 10px; line-height: 1.5; }

/* --------------------------------------------------------------------------
   À L'AFFICHE — fiche Instagram des artistes
   Une fiche par personne : un film peut en réunir deux (CLS & Heatfool,
   Wazy & Seyes). La couleur de catégorie tient tout : glyphe, pseudo, bordure
   au survol. Pas de logo Instagram en couleurs — le dégradé de la marque
   jurerait avec la DA, le glyphe seul suffit à se faire reconnaître.
   -------------------------------------------------------------------------- */
.talents { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.talent {
  display: flex; align-items: center; gap: 16px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  background: rgba(255,255,255,0.02);
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}
.talent:hover { border-color: var(--cat-color, var(--pink)); background: rgba(255,255,255,0.05); }
.talent__ig {
  flex-shrink: 0;
  width: 42px; height: 42px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--cat-color, var(--pink));
  color: var(--cat-color, var(--pink));
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.talent:hover .talent__ig { background: var(--cat-color, var(--pink)); color: #0a0a0a; }
.talent__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.talent__top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.talent__name { font-weight: 700; font-size: 0.95rem; color: #fff; }
.talent__handle {
  font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.02em;
  color: var(--cat-color, var(--pink));
}
.talent__meta {
  font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.06em;
  color: var(--fg-dim);
}
.talent__note { font-size: 0.76rem; font-style: italic; color: var(--fg-dim); line-height: 1.45; }
.talent__arrow {
  flex-shrink: 0; margin-left: auto; color: var(--fg-faint);
  transition: color 0.25s var(--ease), transform 0.25s var(--ease);
}
.talent:hover .talent__arrow { color: var(--cat-color, var(--pink)); transform: translate(2px, -2px); }

#videoModalInstagram { margin-top: 24px; }

/* ==========================================================================
   ÉPINGLAGE, SEUIL & OBTURATEUR  (refonte navigation)
   ========================================================================== */

/* --- 1. MENU : survolé = éclairci, épinglé = soulignement à la couleur --- */
/* LE POINT EST PARTI  (v=98)
   Il disait la meme chose que le soulignement, 7px plus bas et en plus petit.
   Deux marques pour un seul etat, c'est une de trop : le trait suffit — il
   reste allume sur le lien epingle quand la souris est ailleurs, ce qui est
   exactement l'information que le point apportait.
   La regle .is-active::after part avec lui : le soulignement lit maintenant
   --link-color des le repos (voir .nav__link::after), elle ne faisait plus que
   redire ce qui etait deja vrai. */

/* --- 2. LE SEUIL --- */
.seuil {
  position: relative; display: inline-flex; align-items: center; gap: 16px;
  padding: 15px 26px 15px 22px; cursor: pointer; overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--cat-color) 45%, transparent);
  background: rgba(0,0,0,0.28);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  color: #fff; font-family: var(--font-mono);
  font-size: 0.63rem; letter-spacing: 0.2em; text-transform: uppercase;
  transition: border-color 0.45s var(--ease), background 0.45s var(--ease), transform 0.45s var(--ease);
}
.seuil::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--cat-color); box-shadow: 0 0 14px var(--cat-color);
  transform: scaleY(0.25); transition: transform 0.5s var(--ease);
}
.seuil:hover { background: rgba(0,0,0,0.5); transform: translateX(4px);
  border-color: color-mix(in srgb, var(--cat-color) 85%, transparent); }
.seuil:hover::before { transform: scaleY(1); }
.seuil__arrow { display: flex; align-items: center; gap: 3px; }
.seuil__arrow i { width: 5px; height: 1px; background: var(--cat-color); transition: width 0.45s var(--ease); }
.seuil:hover .seuil__arrow i { width: 16px; }

/* Le coucou : le panneau sort de 34px, tranche allumée à la couleur de la catégorie */
.panel.is-peek {
  transform: translateX(calc(100% - 34px));
  box-shadow: -14px 0 60px rgba(0,0,0,0.8),
              -1px 0 0 color-mix(in srgb, var(--cat-color) 70%, transparent),
              0 0 44px color-mix(in srgb, var(--cat-color) 22%, transparent);
}
/* L'Atelier est plein écran : son coucou est un fondu fantôme, pas un glissement */
.atelier.is-peek { opacity: 0.16; transform: translateY(6px); pointer-events: none; }

/* --- 3. L'OBTURATEUR --- */
.shutter-rule {
  position: absolute; left: 0; right: 0; bottom: 74px; height: 1px; z-index: 24;
  background: rgba(255,255,255,0.16); pointer-events: none;
}
.shutter-rule::before, .shutter-rule::after {
  content: ''; position: absolute; top: -1px; height: 3px; width: 0;
  transition: width 0.55s var(--ease);
}
/* La tranche LunorFilms prend la couleur de la catégorie épinglée (rose sur
   Accueil). La tranche LunorStills garde son blanc cassé : c'est l'argentique,
   il ne suit pas l'ambiance. */
.shutter-rule::before {
  left: 0; background: var(--cat-color, var(--pink));
  box-shadow: 0 0 12px var(--cat-color, var(--pink));
  transition: width 0.55s var(--ease), background-color 620ms var(--ease), box-shadow 620ms var(--ease);
}
.shutter-rule::after { right: 0; background: #e8e2d4; box-shadow: 0 0 12px rgba(232,226,212,0.7); }
.hero.hov-films .shutter-rule::before { width: 50%; }
.hero.hov-stills .shutter-rule::after { width: 50%; }
.shutter-rule i {
  position: absolute; left: 50%; top: 50%; width: 7px; height: 7px;
  border: 1px solid rgba(255,255,255,0.45); background: var(--bg);
  transform: translate(-50%,-50%) rotate(45deg); transition: transform 0.5s var(--ease), border-color 0.5s var(--ease);
}
.hero.hov-films .shutter-rule i, .hero.hov-stills .shutter-rule i {
  transform: translate(-50%,-50%) rotate(135deg) scale(1.5); border-color: #fff;
}

.shutter { position: absolute; left: 0; right: 0; bottom: 0; height: 74px; z-index: 25; display: flex; }
.shutter__half {
  flex: 1; display: flex; align-items: center; padding: 0 48px;
  background: none; border: 0; cursor: pointer;
}
.shutter__half--films { justify-content: flex-start; }
.shutter__half--stills { justify-content: flex-end; }
.shutter__label {
  display: flex; align-items: center; gap: 12px;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.24em; text-transform: uppercase;
  color: rgba(255,255,255,0.42);
  transition: color 0.45s var(--ease), transform 0.45s var(--ease), letter-spacing 0.45s var(--ease);
}
.shutter__label b { font-weight: 500; color: rgba(255,255,255,0.72); transition: color 0.45s var(--ease); }
.shutter__label em { font-style: normal; opacity: 0.4; }
.shutter__half:hover .shutter__label { color: #fff; transform: translateY(-5px); letter-spacing: 0.3em; }
.shutter__half--films:hover .shutter__label b { color: var(--cat-color, var(--pink)); }
.shutter__half--stills:hover .shutter__label b { color: #e8e2d4; }

/* L'image arrêtée : le hero se fige réellement (les vidéos sont mises en pause en JS) */
/* transform 1.6s AJOUTE (v=112) : cette regle, plus bas dans la cascade que
   celle de .hero__cell video (« filter/transform 1.6s »), l'ecrasait et
   OUBLIAIT transform. Du coup le scale(1.03) des colonnes voisines, applique a
   l'ouverture du son (.has-sound), sautait d'un coup au lieu de glisser : un
   micro-zoom/dezoom instantane a chaque clic sur un bouton de son. Retabli ici
   a 1,6 s, comme prevu par la regle du triptyque. Le palier immersif (is-idle)
   garde son transition-duration: 1s propre. */
.hero__video, .hero__cell video, .hero__ambilight { transition: filter 0.5s var(--ease), opacity 0.7s var(--ease), transform 1.6s var(--ease); }
.hero.is-frozen .hero__video, .hero.is-frozen .hero__cell video {
  filter: contrast(1.12) saturate(0.55) brightness(0.9);
}
.hero__grain {
  position: absolute; inset: 0; z-index: 3; opacity: 0; pointer-events: none;
  transition: opacity 0.45s var(--ease); mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
.hero.is-frozen .hero__grain { opacity: 0.5; }
/* Le cadre de visee doit rester dans l'IMAGE. Son bas s'arrete au-dessus de la
   bande de l'obturateur (74px) : sinon les coins du bas tombent a cheval sur la
   ligne et sur les libelles LunorFilms / LunorStills. */
.hero__frame { position: absolute; inset: 6vh 5vw 100px; z-index: 4; opacity: 0; pointer-events: none; transition: opacity 0.45s var(--ease); }
.hero.is-frozen .hero__frame { opacity: 1; }
.hero__frame span { position: absolute; width: 28px; height: 28px; border: 1px solid rgba(255,255,255,0.7); }
.hero__frame span:nth-child(1) { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.hero__frame span:nth-child(2) { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.hero__frame span:nth-child(3) { bottom: 0; left: 0; border-right: 0; border-top: 0; }
.hero__frame span:nth-child(4) { bottom: 0; right: 0; border-left: 0; border-top: 0; }

/* --- ESPACE LUNORSTILLS --- */
.stills {
  position: fixed; inset: 0; z-index: 46; background: var(--bg);
  display: flex; flex-direction: column;
  opacity: 0; pointer-events: none; transform: translateY(14px);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}
.stills.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }
/* 6vw et non 5vw : la marge doit valoir la gouttiere, sinon les photos de bord
   sont plus serrees contre l'ecran qu'elles ne le sont entre elles, et la
   galerie parait poussee vers l'exterieur. */
.stills__body { flex: 1; overflow-y: auto; padding: 120px 6vw 90px; }
.stills__intro { max-width: 980px; margin-bottom: 60px; }

/* --------------------------------------------------------------------------
   LE LOGOTYPE DES DEUX UNIVERS  (v=103)
   LunorStills et LunorFilms sont deux entites, pas deux sous-pages : elles ont
   le meme logotype, construit comme le hero d'accueil — LUNOR plein, le reste
   en contour a la couleur de la categorie epinglee. Meme regle pour les deux,
   une seule classe : si l'une bouge, l'autre suit.
   -------------------------------------------------------------------------- */
.brandmark {
  display: block;
  font-family: var(--font-body); font-weight: 900;
  line-height: 0.86; letter-spacing: -0.03em; text-transform: uppercase;
  white-space: nowrap;
}
.brandmark .m-fill { color: #fff; }
.brandmark .m-line {
  color: transparent;
  -webkit-text-stroke: 1.5px rgba(255,255,255,0.3);
  -webkit-text-stroke-color: color-mix(in srgb, var(--cat-color, var(--pink)) 62%, transparent);
  transition: -webkit-text-stroke-color 620ms var(--ease);
}
.brandmark__tag {
  display: flex; align-items: center; gap: 10px; margin-top: 18px;
  font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.26em;
  text-transform: uppercase; color: var(--fg-faint);
}
.brandmark__tag::before {
  content: ''; width: 26px; height: 1px; background: var(--accent); flex: none;
}

.stills__brand { margin-bottom: 26px; }
.stills__brand-mark { font-size: clamp(2.6rem, 7vw, 6rem); }

/* Le titre passe en Inter : c'est la typo du site. Le serif italique en faisait
   une citation posee sur la page, pas le titre de la maison. */
.stills__title {
  font-family: var(--font-body); font-style: normal; font-weight: 800;
  font-size: clamp(1.5rem, 2.4vw, 2.1rem); line-height: 1.15;
  letter-spacing: -0.01em; color: #fff; margin: 30px 0 16px;
}
.stills__desc { color: var(--fg-dim); font-size: 0.95rem; line-height: 1.75; font-weight: 300; max-width: 620px; }

/* --------------------------------------------------------------------------
   PHOTOS VEDETTES
   Une rangee justifiee : hauteur commune, largeurs proportionnelles au ratio.
   flex-grow: var(--r) sur une base 0 -> les largeurs sont dans le rapport exact
   des proportions, donc aucune photo n'est recadree et toutes ont EXACTEMENT
   la meme hauteur. Les hauts sont alignes par construction, pas par chance.
   -------------------------------------------------------------------------- */
.stills__featured { margin-bottom: 74px; }
.stills__head {
  display: flex; align-items: center; gap: 18px;
  margin-bottom: 22px;
}
.stills__head-title {
  font-family: var(--font-body); font-weight: 700; font-size: 0.95rem;
  letter-spacing: 0.02em; color: #fff; flex: none;
}
.stills__head-rule { flex: 1; height: 1px; background: rgba(255,255,255,0.1); }
.stills__head-num {
  font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.24em;
  text-transform: uppercase; color: var(--fg-faint); flex: none;
}
.stills__featured-row {
  display: flex; align-items: flex-start; gap: 14px;
  height: clamp(300px, 46vh, 560px);
}
.still--feat {
  --r: 1.5;
  flex: var(--r) 1 0; min-width: 0; width: auto; height: 100%;
  aspect-ratio: auto;
  position: relative; padding: 0; border: 0; background: #0d0d0f;
  cursor: zoom-in; overflow: hidden;
  opacity: 0; transition: opacity 0.7s var(--ease), transform 0.45s var(--ease), box-shadow 0.45s var(--ease);
}
.still--feat.is-loaded { opacity: 1; }
.still--feat.is-dead { display: none; }
.still--feat img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 0.9s var(--ease), filter 0.5s var(--ease); }
.still--feat .still__caption { padding: 30px 12px 10px; }
@media (hover: hover) {
  .stills__featured-row:has(.still--feat:hover) .still--feat img { filter: brightness(0.38) saturate(0.75); }
  .stills__featured-row:has(.still--feat:hover) .still--feat:hover img { filter: none; transform: scale(1.03); }
  .still--feat.is-loaded:hover {
    z-index: 2;
    box-shadow: 0 28px 64px -14px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.16);
  }
}
.still--feat:hover .still__caption, .still--feat:focus-visible .still__caption { opacity: 1; transform: none; }
.still--feat:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
@media (max-width: 900px) {
  .stills__featured-row { height: 34vh; gap: 8px; }
}

/* ==========================================================================
   GALERIE LUNORSTILLS  (v=81)
   Une seule suite continue, sans categorie. Mise en page "justifiee" : chaque
   photo garde ses proportions exactes et les rangees se calent pile sur la
   largeur — comme une planche-contact, pas une grille qui recadre tout au
   carre.

   Comment ca tient sans une ligne de JS de mise en page :
     flex-basis: --r x hauteur-cible   -> la place demandee, proportionnelle
     flex-grow:  --r                    -> le rab se repartit dans la meme
                                           proportion
   Resultat : dans une rangee, toutes les photos sont etirees du meme facteur,
   donc elles ont toutes la meme hauteur, et aucune n'est deformee. La rangee
   se recalcule seule au redimensionnement, sans ResizeObserver.
   --r est pose en JS depuis content.json (ou lu sur l'image si l'intranet ne
   l'a pas encore mesure).
   ========================================================================== */
.stills__count {
  font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.24em;
  text-transform: uppercase; color: var(--fg-faint);
  padding-bottom: 14px; margin-bottom: 26px;
  border-bottom: 1px solid rgba(255,255,255,0.07);
}

/* Mosaique en colonnes. Chaque photo garde ses proportions exactes et remplit
   la largeur de sa colonne ; les colonnes s'equilibrent toutes seules en
   hauteur. Aucun recadrage, aucun trou a droite, aucune ligne de JS.

   Le multi-colonnes CSS fait tout : column-count pose le nombre de colonnes,
   break-inside: avoid interdit de couper une photo en deux, et le navigateur
   repartit la pile pour que les colonnes finissent a la meme hauteur.
   --r (le ratio, pose en JS depuis content.json) sert a reserver la hauteur
   AVANT que l'image arrive : c'est ce qui supprime les sauts au chargement.

   A savoir : la lecture se fait en colonnes, de haut en bas, puis colonne
   suivante — et non de gauche a droite. Pour une galerie d'ambiance, sans
   ordre narratif, c'est sans consequence ; c'est le prix de la mosaique. */
/* LES PROPORTIONS DE LA RÉFÉRENCE  (v=100)
   Mesure faite sur les captures, les deux a l'echelle 1:1 (fenetre 2380px) :
     thewildstrawberry -> 3 colonnes de 602px, gouttieres ET marges de 143px
     LunorStills       -> 4 colonnes de 524px, gouttieres de 14px
   143/2380 = 6.0vw exactement, pour la gouttiere comme pour la marge : chez
   eux, le vide entre deux photos vaut le vide qui borde la page.
   On ne fige donc pas des pixels mais le RAPPORT.

   POURQUOI PLUS DE column-count  (v=101)
   Le multi-colonnes CSS ne garantit pas que les colonnes DEMARRENT a la meme
   ligne : des qu'un element ne tient pas dans la colonne courante, le
   navigateur reporte le fragment — et la marge du report se retrouve en haut
   de la colonne suivante. C'est le decalage visible sur la capture : la
   colonne du milieu commencait 6vw plus bas que ses voisines.
   Les colonnes sont maintenant de vraies piles (une <div> par colonne), et
   `align-items: flex-start` les fait toutes partir de la meme ligne. Le
   remplissage se fait en JS, photo par photo, dans la colonne la plus courte
   a cet instant : les colonnes finissent a des hauteurs proches, comme avant,
   mais les hauts sont alignes par construction. */
.stills__gallery {
  display: flex;
  align-items: flex-start;
  gap: 6vw;
}
.stills__col {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column;
  gap: 6vw;
}
@media (max-width: 1100px) { .stills__gallery, .stills__col { gap: 5vw; } }
@media (max-width: 700px)  { .stills__gallery { gap: 0; } .stills__col { gap: 7vw; } }

.still {
  --r: 1.5;
  position: relative; display: block; padding: 0;
  /* La gouttiere verticale vaut l'horizontale : c'est le `gap` de la pile qui
     s'en charge maintenant, plus une marge sur la carte. */
  width: 100%; margin: 0;
  border: 0; background: #0d0d0f; cursor: zoom-in; overflow: hidden;
  aspect-ratio: var(--r);
  opacity: 0; transform: translateY(10px);
  /* opacite lente : c'est l'arrivee de la photo dans la mosaique.
     transform/ombre rapides : c'est le relief au survol, il doit repondre. */
  transition: opacity 0.7s var(--ease), transform 0.45s var(--ease),
              box-shadow 0.45s var(--ease);
}
.still.is-loaded { opacity: 1; transform: none; }
.still.is-dead { display: none; }   /* image absente : on ne laisse pas de trou */

/* --------------------------------------------------------------------------
   LA PHOTO EST INTACTE AU REPOS  (v=88)
   Plus aucun filtre par defaut : ce que l'oeil voit, c'est l'etalonnage du
   photographe, pas une couche de contraste posee par le site. Une galerie qui
   n'ose montrer ses photos qu'au survol ne sert a rien.
   Le contraste ne vient plus de la photo survolee : il vient des AUTRES, qui
   s'effacent autour d'elle. C'est le principe de la table lumineuse — on
   n'eclaire pas le tirage qu'on regarde, on eteint ceux d'a cote.
   -------------------------------------------------------------------------- */
.still img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 0.9s var(--ease), filter 0.5s var(--ease);
}

.still__caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 34px 14px 12px; text-align: left;
  background: linear-gradient(transparent, rgba(0,0,0,0.88));
  font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: rgba(255,255,255,0.8);
  opacity: 0; transform: translateY(6px); pointer-events: none;
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.still:hover .still__caption, .still:focus-visible .still__caption { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   LE RELIEF — survol d'une photo
   Trois choses ensemble, sinon ca ne decolle pas :
     1. les VOISINES tombent a 38 % de luminosite et se desaturent un peu ;
     2. la photo survolee monte de 6px, grandit de 2 % et passe en z-index 2 —
        elle sort donc de la mosaique au lieu de pousser ses voisines (une
        transformation ne deplace rien dans la mise en page) ;
     3. une ombre portee large et une fine arete claire sur le pourtour : c'est
        elle qui donne l'epaisseur, l'ombre seule ne fait que salir le noir.
   Le tout est verrouille sur (hover: hover) : au doigt, il n'y a pas de survol,
   et une photo resterait allumee apres le tap.
   -------------------------------------------------------------------------- */
@media (hover: hover) {
  /* Les autres photos ne s'assombrissent plus au survol (v=106) : dans la
     mosaique, une photo mise en avant en eteignant toutes les autres faisait
     "vitrine", pas galerie. Seule la photo survolee reagit, par son propre
     relief plus bas. Les VEDETTES gardent l'effet (regle .stills__featured-row
     plus haut) : la, l'assombrissement des voisines est voulu. */
  .stills__gallery .still:hover img {
    transform: scale(1.03);
  }
  .still.is-loaded:hover,
  .still.is-loaded:focus-visible {
    z-index: 2;
    transform: translateY(-6px) scale(1.02);
    box-shadow:
      0 28px 64px -14px rgba(0,0,0,0.9),
      0 6px 18px rgba(0,0,0,0.5),
      0 0 0 1px rgba(255,255,255,0.16);
  }
}
/* Au clavier, la photo focalisee se detache aussi, meme sans souris. */
.still.is-loaded:focus-visible img { filter: none; }

/* Le liseré rose ne sert qu'au clavier : a la souris, la photo se suffit. */
.still::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  border: 1px solid transparent; transition: border-color 0.35s var(--ease);
}
.still:focus-visible { outline: none; }
.still:focus-visible::after { border-color: var(--accent); }

/* ==========================================================================
   VISIONNEUSE LUNORSTILLS  (v=81)
   ========================================================================== */
.stills__lightbox {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 0.3s var(--ease);
}
.stills__lightbox.is-open { opacity: 1; pointer-events: auto; }
.stills__lightbox[hidden] { display: none; }

/* Fond separe du conteneur : c'est LUI qui recoit le clic "je veux sortir".
   Un clic sur la photo ne doit pas fermer — on peut vouloir la regarder.
   (v=106) On ne masque plus le site : on le LAISSE VOIR, floute et assombri
   derriere la photo. L'ancien rgba(4,4,5,0.96) etait un cache quasi opaque —
   autant dire du noir. Ici 0.62 laisse transparaitre la page, et le blur en
   fait un arriere-plan sourd. C'est une photo posee SUR le site, pas un plein
   ecran deguise (le vrai plein ecran est un bouton, plus bas). */
.stills__lightbox-bg {
  position: absolute; inset: 0; background: rgba(4,4,5,0.62);
  -webkit-backdrop-filter: blur(20px) saturate(0.9) brightness(0.55);
  backdrop-filter: blur(20px) saturate(0.9) brightness(0.55);
  cursor: zoom-out;
}
.stills__lightbox-figure {
  position: relative; margin: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
}
.stills__lightbox-stage { position: relative; display: flex; align-items: center; justify-content: center; }
/* --------------------------------------------------------------------------
   LA PHOTO PREND L'ECRAN  (v=101)
   Avant : 88vw x 76vh, et la legende, posee SOUS l'image dans la meme colonne,
   lui prenait encore sa hauteur. La photo occupait la moitie de l'ecran.
   Maintenant la legende est en surimpression (HUD) : elle ne coute plus un
   pixel a l'image, qui monte a 96vw x 94vh. En plein ecran reel, elle prend
   tout. Le HUD dort tant que la souris ne bouge pas.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   LA PHOTO EST GRANDE, PAS PLEIN ECRAN  (v=106)
   Par defaut elle occupe 88vw x 82vh : large, mais posee sur le site qu'on
   voit floute derriere. Le vrai plein ecran (100vw/100vh, site masque) est un
   BOUTON dans le HUD — l'utilisateur choisit. Avant, l'ouverture basculait
   d'office en plein ecran reel ; ce n'est plus le cas.

   LE SELECTEUR EST BORNE A LA SCENE
   Il visait avant TOUTES les images de la visionneuse et leur imposait
   opacity:0 (que seule .is-in de la grande image levait) — les vignettes du
   mini-carrousel restaient donc noires. Borne a .stills__lightbox-stage img.
   Ne pas reelargir. */
.stills__lightbox-stage img {
  max-width: 88vw; max-height: 82vh; object-fit: contain; display: block;
  box-shadow: 0 40px 120px rgba(0,0,0,0.7);
  opacity: 0; transform: scale(0.985);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.stills__lightbox-stage img.is-in { opacity: 1; transform: none; }

/* Attente : un simple anneau qui tourne, visible seulement si le reseau traine. */
.stills__lightbox-spin {
  position: absolute; width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.15); border-top-color: var(--accent);
  opacity: 0; transition: opacity 0.2s var(--ease) 0.25s;
}
.stills__lightbox-spin.is-on { opacity: 1; animation: stills-spin 0.8s linear infinite; }
@keyframes stills-spin { to { transform: rotate(360deg); } }

/* ==========================================================================
   LE HUD — legende + mini-carrousel
   Il ne s'affiche que quand la souris bouge, et se retire seul apres 2,4 s
   d'immobilite : au repos, il n'y a que la photo. La remontee se fait en
   translation + fondu, sur la meme courbe que le reste du site.
   ========================================================================== */
.stills__hud {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  padding: 70px 24px 26px;
  background: linear-gradient(transparent, rgba(0,0,0,0.55) 42%, rgba(0,0,0,0.86));
  opacity: 0; transform: translateY(22px); pointer-events: none;
  transition: opacity 0.5s var(--ease), transform 0.6s var(--ease);
}
.stills__hud.is-awake { opacity: 1; transform: none; pointer-events: auto; }

.stills__lightbox-bar {
  display: flex; align-items: center; justify-content: center;
  gap: 18px; flex-wrap: wrap; max-width: 60ch; text-align: center;
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.18em;
  text-transform: uppercase;
}
/* La description sort du mono en capitales : c'est une phrase, elle doit se
   lire comme une phrase — d'ou le retour a la ligne force (flex-basis 100%). */
.stills__lightbox-desc {
  flex-basis: 100%;
  font-family: var(--font-body, inherit); font-size: 0.85rem; font-weight: 300;
  letter-spacing: 0; text-transform: none; line-height: 1.7;
  color: var(--fg-dim); margin-top: 4px;
}
.stills__lightbox-desc[hidden] { display: none; }
.stills__lightbox-caption { color: var(--fg-dim); }
.stills__lightbox-counter { color: var(--fg-faint); }
.stills__lightbox-counter::before { content: '— '; }

/* Le mini-carrousel : la photo ouverte, puis les suivantes. Aucune barre de
   defilement — la fenetre glisse toute seule avec la lecture. */
.stills__strip { display: flex; align-items: center; gap: 8px; }
.stills__strip-item {
  --r: 1.5;
  position: relative; flex: none; padding: 0; border: 0; background: #0d0d0f;
  height: 64px; width: calc(64px * var(--r));
  overflow: hidden; cursor: pointer; opacity: 0.42;
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease),
              box-shadow 0.35s var(--ease), height 0.35s var(--ease), width 0.35s var(--ease);
}
.stills__strip-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.stills__strip-item:hover { opacity: 0.85; transform: translateY(-3px); }
.stills__strip-item:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent); opacity: 1; }
/* La photo en cours : plus haute, pleine opacite, liseree a la couleur de
   marque. On voit d'un coup d'oeil ou on en est dans la serie. */
.stills__strip-item.is-current {
  opacity: 1; height: 82px; width: calc(82px * var(--r));
  box-shadow: 0 0 0 1px var(--accent), 0 10px 26px rgba(0,0,0,0.6);
}

@media (max-width: 700px) {
  .stills__strip { display: none; }   /* au doigt, le balayage suffit */
  .stills__hud { padding: 50px 16px 18px; }
}

.stills__lightbox-close, .stills__lightbox-nav, .stills__lightbox-fs {
  position: absolute; z-index: 2; border-radius: 50%; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.18);
  background: rgba(12,12,14,0.55);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: #fff; display: flex; align-items: center; justify-content: center;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease), transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.stills__lightbox-close:hover, .stills__lightbox-nav:hover, .stills__lightbox-fs:hover {
  background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.45);
}
.stills__lightbox-close:focus-visible, .stills__lightbox-nav:focus-visible, .stills__lightbox-fs:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}
.stills__lightbox-close { top: 26px; right: 32px; width: 40px; height: 40px; }
.stills__lightbox-close:hover { transform: rotate(90deg); }
/* Le bouton plein ecran, sous la croix. Il montre l'icone "entrer" par defaut,
   et "sortir" quand on EST en plein ecran (la classe .is-fs est posee en JS au
   fullscreenchange). */
.stills__lightbox-fs { top: 74px; right: 32px; width: 40px; height: 40px; }
.stills__lightbox-fs .ic-exit { display: none; }
.stills__lightbox.is-fs .stills__lightbox-fs .ic-enter { display: none; }
.stills__lightbox.is-fs .stills__lightbox-fs .ic-exit { display: block; }
.stills__lightbox-nav { top: 50%; margin-top: -25px; width: 50px; height: 50px; }
.stills__lightbox-nav--prev { left: 24px; }
.stills__lightbox-nav--next { right: 24px; }
.stills__lightbox-nav:hover { transform: scale(1.08); }
.stills__lightbox-nav[hidden] { display: none; }
/* Croix, fleches et plein ecran suivent le HUD : au repos, il ne reste que la photo. */
.stills__lightbox.is-idle .stills__lightbox-close,
.stills__lightbox.is-idle .stills__lightbox-fs,
.stills__lightbox.is-idle .stills__lightbox-nav { opacity: 0; pointer-events: none; }

/* Plein ecran reel : plus rien derriere, la photo prend tout le cadre. */
.stills__lightbox:fullscreen .stills__lightbox-bg,
.stills__lightbox:-webkit-full-screen .stills__lightbox-bg { background: #050506; }
.stills__lightbox:fullscreen .stills__lightbox-stage img { max-width: 100vw; max-height: 100vh; box-shadow: none; }
.stills__lightbox:-webkit-full-screen .stills__lightbox-stage img { max-width: 100vw; max-height: 100vh; box-shadow: none; }

/* Sur telephone il n'y a pas de survol : les fleches encombrent, et le
   balayage fait deja le travail. On ne garde que la croix. */
@media (max-width: 700px) {
  .stills__lightbox-nav { display: none; }
  .stills__lightbox-stage img { max-width: 96vw; max-height: 86vh; }
  .stills__lightbox-close { top: 16px; right: 16px; }
  .stills__lightbox-bar { flex-direction: column; gap: 6px; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
  .still, .still img, .stills__lightbox-stage img, .stills__lightbox-close, .stills__lightbox-nav {
    transition-duration: 0.01ms !important;
  }
  .still { opacity: 1; transform: none; }
  .stills__lightbox-spin.is-on { animation: none; }
}

@media (max-width: 900px) {
  .hero { padding-bottom: 40px; }
  .hero__meta { position: static; text-align: left; margin-top: 28px; }
  .shutter { height: auto; position: static; padding: 22px 0 8px; }
  /* LE BUG : flex:1 ne force pas un flex-item a retrecir sous la largeur de
     son propre contenu (min-width:auto par defaut) — sur un ecran etroit,
     chaque moitie gardait donc sa largeur "naturelle" (calculee sur un texte
     qui ne passe jamais a la ligne), et les deux moities cumulees debordaient
     largement du cadre. min-width:0 autorise enfin le retrecissement ; le
     texte, lui, est deja en white-space normal et peut passer a la ligne. */
  .shutter__half { padding: 10px 20px; min-width: 0; }
  .shutter__half--stills { justify-content: flex-start; }
  .shutter-rule { display: none; }
  .stills__body { padding: 100px 6vw 60px; }

  /* Le label ne repose plus sur un survol pour devenir lisible (il n'y en a
     pas au doigt) : la couleur de repos doit donc suffire toute seule. Le
     flex d'origine (gap entre 3 segments inline) devient un simple bloc qui
     laisse le texte s'enrouler naturellement dans la largeur disponible. */
  .shutter__label {
    display: block;
    color: rgba(255,255,255,0.7);
    letter-spacing: 0.12em;
    line-height: 1.5;
  }
  .shutter__label b { color: #fff; }
  .shutter__label em { margin: 0 5px; opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
  .seuil, .panel, .shutter__label, .still img { transition-duration: 0.01ms !important; }
}

/* ==========================================================================
   FONDS VIDEO : aucun bouton de lecture, jamais
   Safari/iOS affiche un gros bouton play par-dessus une video quand la lecture
   automatique est refusee ou que le fichier ne se charge pas. Sur un fond,
   ce bouton n'a aucun sens : on le supprime. Le lecteur de la modale video
   (#videoModalPlayer) n'est pas concerne, lui garde ses controles.
   ========================================================================== */
.hero__video,
.hero__ambilight,
.hero__cell video,
.discover__hero-video {
  pointer-events: none;
}
/* Un seul pseudo-element inconnu dans une liste separee par des virgules
   invalide TOUTE la regle. Chaque pseudo-element a donc sa propre regle :
   si le navigateur n'en connait pas un, les autres s'appliquent quand meme. */
.hero__video::-webkit-media-controls,
.hero__ambilight::-webkit-media-controls,
.hero__cell video::-webkit-media-controls,
.discover__hero-video::-webkit-media-controls {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-enclosure,
.hero__ambilight::-webkit-media-controls-enclosure,
.hero__cell video::-webkit-media-controls-enclosure,
.discover__hero-video::-webkit-media-controls-enclosure {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-panel,
.hero__ambilight::-webkit-media-controls-panel,
.hero__cell video::-webkit-media-controls-panel,
.discover__hero-video::-webkit-media-controls-panel {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-panel-container,
.hero__ambilight::-webkit-media-controls-panel-container,
.hero__cell video::-webkit-media-controls-panel-container,
.discover__hero-video::-webkit-media-controls-panel-container {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-container,
.hero__ambilight::-webkit-media-controls-container,
.hero__cell video::-webkit-media-controls-container,
.discover__hero-video::-webkit-media-controls-container {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-overlay-enclosure,
.hero__ambilight::-webkit-media-controls-overlay-enclosure,
.hero__cell video::-webkit-media-controls-overlay-enclosure,
.discover__hero-video::-webkit-media-controls-overlay-enclosure {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-overlay-play-button,
.hero__ambilight::-webkit-media-controls-overlay-play-button,
.hero__cell video::-webkit-media-controls-overlay-play-button,
.discover__hero-video::-webkit-media-controls-overlay-play-button {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-start-playback-button,
.hero__ambilight::-webkit-media-controls-start-playback-button,
.hero__cell video::-webkit-media-controls-start-playback-button,
.discover__hero-video::-webkit-media-controls-start-playback-button {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-webkit-media-controls-play-button,
.hero__ambilight::-webkit-media-controls-play-button,
.hero__cell video::-webkit-media-controls-play-button,
.discover__hero-video::-webkit-media-controls-play-button {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}
.hero__video::-internal-media-controls-overlay-cast-button,
.hero__ambilight::-internal-media-controls-overlay-cast-button,
.hero__cell video::-internal-media-controls-overlay-cast-button,
.discover__hero-video::-internal-media-controls-overlay-cast-button {
  display: none !important;
  -webkit-appearance: none;
  appearance: none;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Le poster de repli doit cadrer comme la video, pas s'etirer. */
.hero__video, .hero__cell video { object-fit: cover; }

/* ==========================================================================
   IMAGE DE REPLI DU FOND  (v=72)
   L'attribut "poster" d'une <video> est ce qui autorise Safari a afficher son
   bouton play par-dessus. On ne s'en sert plus : le repli est une couche CSS
   posee sous la video, visible seulement si la video manque.
   Aujourd'hui : noir uni. Pour y remettre une image, ajouter dans la regle
   ci-dessous : url('../assets/images/LE-FICHIER.jpg?v=1') center / cover no-repeat
   ========================================================================== */
.hero__fallback {
  position: absolute; inset: 0; z-index: 0;
  background: var(--bg);
  background-size: cover; background-position: center;
}
/* Image figee volee a la video (voir freezeFrame en JS) : elle doit cadrer
   exactement comme la video qu'elle remplace, jamais s'etirer. */
.hero__cell.has-still { background-size: cover; background-position: center; }
/* Une video qui n'a pas pu se charger est retiree : rien a dessiner, donc
   aucun bouton play, dans aucun navigateur. */
.hero__video.is-dead,
.hero__cell video.is-dead,
.discover__hero-video.is-dead { display: none !important; }

/* ==========================================================================
   EN-TETE DES GALERIES (LunorFilms / LunorStills)  (v=75)
   ========================================================================== */

/* Le titre est cale au centre exact de l'ecran, et non plus au milieu de ce
   que veut bien laisser le flux entre "Accueil" et les boutons de droite.
   On le sort du flex : "Accueil" et les actions se repartissent librement. */
.discover__kicker {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  pointer-events: none;
}

/* Bouton son : au repos son contour est a 0.15, presque invisible. Quand le
   son est actif, il monte a 0.30 — la valeur exacte du contour de NOUS
   CONTACTER juste a sa droite — et une onde discrete pulse autour pour dire
   que ca joue. La classe .is-active est deja posee par le JS. */
.discover__header-actions .icon-btn {
  position: relative;
  transition: background 0.2s var(--ease), border-color 0.45s var(--ease);
}
.discover__header-actions .icon-btn.is-active {
  /* Rose fixe (v=108) : le bouton son actif reste a la marque, quelle que soit
     la categorie de la page. */
  border-color: color-mix(in srgb, var(--pink) 55%, transparent);
  color: var(--pink);
}
.discover__header-actions .icon-btn.is-active::after {
  content: '';
  position: absolute; inset: -1px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--pink) 55%, transparent);
  pointer-events: none;
  animation: soundPulse 2.4s var(--ease) infinite;
}
@keyframes soundPulse {
  0%   { transform: scale(1);    opacity: 0.75; }
  70%  { transform: scale(1.55); opacity: 0; }
  100% { transform: scale(1.55); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .discover__header-actions .icon-btn.is-active::after { animation: none; opacity: 0; }
}

/* ==========================================================================
   PORTE DE L'INTRANET  (v=78)
   Le site passe derriere un verre depoli sombre, le formulaire reste seul net.
   --------------------------------------------------------------------------
   POURQUOI CE N'EST PAS UN SIMPLE FONDU
   Faire varier l'opacite d'un element qui porte un backdrop-filter ne fonctionne
   pas : le navigateur compose le flou d'un bloc, puis fait varier l'opacite du
   resultat. Le fond apparaissait donc deja floute et deja assombri — d'ou la
   sensation de coupure nette.
   Ici on ne touche plus a l'opacite du voile : on anime le flou et la teinte
   EUX-MEMES, de 0 a leur valeur finale. Le fond s'enfonce progressivement.
   La visibilite prend le relais de pointer-events et attend la fin de la
   transition pour se couper (d'ou le transition-delay a la fermeture).
   ========================================================================== */
.gate {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center;
  visibility: hidden; pointer-events: none;
  transition: visibility 0s linear 0.65s;
}
.gate.is-open { visibility: visible; pointer-events: auto; transition-delay: 0s; }

.gate__backdrop {
  position: absolute; inset: 0;
  background: rgba(4, 4, 5, 0);
  -webkit-backdrop-filter: blur(0px) saturate(1) brightness(1);
  backdrop-filter: blur(0px) saturate(1) brightness(1);
  transition: background 0.65s var(--ease),
              -webkit-backdrop-filter 0.65s var(--ease),
              backdrop-filter 0.65s var(--ease);
}
.gate.is-open .gate__backdrop {
  background: rgba(4, 4, 5, 0.72);
  -webkit-backdrop-filter: blur(22px) saturate(0.6) brightness(0.55);
  backdrop-filter: blur(22px) saturate(0.6) brightness(0.55);
}

.gate__box {
  position: relative;
  width: min(92vw, 400px);
  padding: 44px 40px 36px;
  background: rgba(12, 12, 14, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 14px;
  box-shadow: 0 40px 120px rgba(0, 0, 0, 0.75), 0 0 60px rgba(255, 45, 120, 0.06);
  text-align: center;
  /* Le formulaire entre APRES le fond : le regard suit le mouvement au lieu de
     recevoir les deux d'un coup. */
  opacity: 0;
  transform: translateY(16px) scale(0.98);
  transition: opacity 0.35s var(--ease), transform 0.45s var(--ease);
}
.gate.is-open .gate__box {
  opacity: 1;
  transform: none;
  transition: opacity 0.4s var(--ease) 0.18s, transform 0.55s var(--ease) 0.12s;
}

/* Fondu au noir avant de partir vers l'intranet : la page suivante remonte du
   noir de son cote, les deux se rejoignent sans a-coup. */
.gate.is-leaving .gate__backdrop {
  background: rgba(4, 4, 5, 1);
  -webkit-backdrop-filter: blur(30px) saturate(0.3) brightness(0.2);
  backdrop-filter: blur(30px) saturate(0.3) brightness(0.2);
  transition-duration: 0.5s;
}
.gate.is-leaving .gate__box {
  opacity: 0;
  transform: translateY(-8px) scale(0.99);
  transition: opacity 0.35s var(--ease), transform 0.5s var(--ease);
}

.gate__close {
  position: absolute; top: 14px; right: 14px;
  width: 32px; height: 32px; border-radius: 50%;
  border: 0; background: none; cursor: pointer;
  color: var(--fg-faint);
  display: flex; align-items: center; justify-content: center;
  transition: color 0.25s var(--ease);
}
.gate__close:hover { color: #fff; }

.gate__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-bottom: 18px;
  border-radius: 50%;
  border: 1px solid rgba(255, 45, 120, 0.35);
  background: rgba(255, 45, 120, 0.1);
  color: var(--pink);
}
.gate__kicker {
  display: block;
  font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.26em;
  color: var(--fg-faint);
}
.gate__title { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.01em; margin: 10px 0 6px; }
.gate__desc { color: var(--fg-faint); font-size: 0.8rem; font-weight: 300; margin-bottom: 26px; }

.gate__field { position: relative; display: flex; }
.gate__field input {
  flex: 1;
  padding: 14px 52px 14px 16px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  color: #fff;
  font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.14em;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.gate__field input:focus {
  outline: none;
  border-color: rgba(255, 45, 120, 0.65);
  background: rgba(255, 255, 255, 0.06);
}
.gate__field input::placeholder { color: rgba(255, 255, 255, 0.22); letter-spacing: 0.06em; }

.gate__submit {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 36px; height: 36px; border-radius: 6px;
  border: 0; background: var(--pink); color: #fff; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: filter 0.25s var(--ease), opacity 0.25s var(--ease);
}
.gate__submit:hover { filter: brightness(1.15); }
.gate__submit:disabled { opacity: 0.45; cursor: wait; }

.gate__msg {
  min-height: 18px; margin-top: 14px;
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.08em;
  color: var(--fg-faint);
}
.gate__msg.is-err { color: #ff5a5a; }
.gate__msg.is-ok { color: #3ddc84; }

/* Secousse breve sur mot de passe refuse. */
.gate__box.is-wrong { animation: gateShake 0.4s var(--ease); }
@keyframes gateShake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}
@media (prefers-reduced-motion: reduce) {
  .gate__box.is-wrong { animation: none; }
  .gate, .gate__backdrop, .gate__box { transition-duration: 0.01ms; transition-delay: 0s; }
}


/* ==========================================================================
   AFFICHES DES FONDS (posters)  (v=85)
   L'image d'abord, la video ensuite. L'affiche est un <img> pose juste sous la
   video, avec EXACTEMENT le meme cadrage qu'elle : plein cadre en cover
   partout, et sur toute la hauteur en contain pour le Mariage (dont le plan
   est vertical). Le raccord est donc invisible quand la video prend le relais.

   Les videos de fond partent a 0 d'opacite et ne montent qu'une fois qu'elles
   JOUENT (.is-shown, pose en JS sur l'evenement "playing"). Une video refusee
   par le navigateur reste donc invisible, et l'affiche tient le fond : pas de
   noir, pas de bouton play, personne ne voit qu'il manque quelque chose.
   ========================================================================== */
.hero__poster {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
  opacity: 0;
  transition: opacity 0.45s var(--ease);
  pointer-events: none;
  user-select: none;
}
.hero__poster.is-shown { opacity: 1; }
/* La video joue : l'affiche s'en va. */
.hero__poster.is-gone  { opacity: 0; }
.hero__poster.is-dead  { display: none; }   /* affiche absente : le noir du repli reprend */
/* En triptyque, l'affiche du fond unique n'a plus lieu d'etre : chaque colonne
   porte la sienne. Sans ca, celle du Mariage (z-index 1) passerait devant. */
.hero.is-triptych > .hero__poster { display: none; }

/* Mariage : l'affiche epouse la video contenue, jamais le plein cadre.
   z-index 1 comme la video, et posee apres elle dans le HTML : elle la couvre
   exactement, sans jamais passer devant le scrim. */
.hero.is-portrait .hero__poster {
  /* Meme centrage que la video (inset:0 + margin:auto, voir plus haut) : les
     deux couches doivent se superposer exactement, et aucune ne doit glisser. */
  inset: 0; margin: auto;
  width: auto; height: 100%; max-width: 100%;
  object-fit: contain;
  z-index: 1;
}

/* LES VIDEOS DE FOND NE SONT JAMAIS MASQUEES.
   Safari refuse de lancer tout seul une video a opacite 0 : il la juge
   invisible. C'est l'affiche, posee par-dessus, qui s'efface quand la video
   demarre — l'effet est le meme a l'ecran, sans le blocage. */

@media (prefers-reduced-motion: reduce) {
  .hero__poster { transition-duration: 0.01ms; }
}

/* ==========================================================================
   ADAPTATION MOBILE & TABLETTE  (v=115)
   --------------------------------------------------------------------------
   Tout ce qui suit est ADDITIF : rien au-dessus n'est modifié, et chaque
   règle ici vise explicitement le tactile et/ou les petits écrans. Le
   desktop (≥1024px, souris) reste rigoureusement identique à avant — cette
   section n'existant que sous des media queries qui ne le concernent pas.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. LA PAGE NE SE LIT QUE VERTICALEMENT
   Plusieurs panneaux (contact, catégorie, atelier...) restent en permanence
   dans le DOM en position:fixed, translatés hors-cadre pour se cacher
   (translateX(100%)) plutôt que retirés. C'est invisible et sans
   conséquence... sauf sur Safari iOS, où le rebond élastique ("rubber-band")
   du geste de balayage prend en compte cette géométrie hors-écran même à
   travers overflow-x:hidden, et autorise donc un léger glissement latéral de
   toute la page. touch-action:pan-y est la réponse standard : elle indique
   au moteur de gestes de ne reconnaître QUE le geste vertical sur la page,
   quelle qu'en soit la cause — indépendant de tout calcul de largeur.
   On restaure explicitement pan-x sur les DEUX carrousels horizontaux
   volontaires (rangées façon Netflix, onglets de L'Atelier) : sans ça,
   l'interdiction posée sur la page s'appliquerait aussi à eux et bloquerait
   leur propre balayage.
   -------------------------------------------------------------------------- */
html, body { touch-action: pan-y; }
.discover-row__track, .atelier__tabs { touch-action: pan-x pan-y; }

/* --------------------------------------------------------------------------
   1. LE TOUCHER REMPLACE LE SURVOL
   Deux familles de cas :
     a) de l'info qui n'existait qu'au survol (légendes, actions rapides) et
        qui, sans rien faire, ne serait plus JAMAIS visible au doigt — on la
        rend permanente ;
     b) des effets décoratifs (grossissement, halo, assombrissement des
        voisines) qui, eux, ont un vrai risque de rester "collés" après un
        tap sur mobile (le point:hover peut s'activer sans jamais se
        désactiver) — on les neutralise pour retomber sur l'état de repos.
   -------------------------------------------------------------------------- */
@media (hover: none) {
  /* a) info qui doit rester visible */
  .still__caption,
  .still--feat .still__caption {
    opacity: 1;
    transform: none;
  }
  .genre-row__grid--photos .card--photo .card__scrim,
  .genre-row__grid--photos .card--photo .card__body {
    opacity: 1;
  }
  .discover-row__hover-actions {
    opacity: 1;
    transform: none;
  }
  /* Les flèches de défilement de rangée n'ont pas de sens au doigt (le
     balayage natif les remplace) : autant ne jamais les afficher. */
  .discover-row__arrow { display: none; }

  /* b) survols décoratifs à neutraliser (retour à l'état de repos, déjà
     pensé pour rester lisible seul — voir les commentaires v=94 plus haut) */
  .discover-row__card:hover:not(.is-expanded) { transform: none; box-shadow: none; }
  .discover-row__card:hover img { filter: brightness(0.88) saturate(1.04); }
  .card:hover .card__img { filter: none; transform: none; }
  .card--more:hover { border-color: var(--line); transform: none; }
  .card--more:hover .card__img { transform: none; filter: none; }
  .card--more:hover .card--more__arrow { color: rgba(255,255,255,0.55); }
  .card--more:hover .card--more__arrow svg { transform: none; }
  .genre-row__grid .card:hover { transform: none; }
  .still--feat:hover .still__caption { transform: none; }
}

/* --------------------------------------------------------------------------
   2. CIBLES TACTILES PLUS GÉNÉREUSES
   34px/30px se visent bien à la souris ; au doigt, la zone confortable
   commence vers 40-44px (repère courant : Apple HIG / Material Design).
   On agrandit seulement les boutons ronds les plus sollicités, sans toucher
   à leur mise en page desktop (aucune de ces règles n'entre en jeu à la
   souris, pointer:coarse ne matchant jamais un pointeur de précision).
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .icon-btn { width: 40px; height: 40px; }
  .panel__close, .video-modal__close, .mobile-nav__close { width: 42px; height: 42px; }
  .dr-icon-btn { width: 34px; height: 34px; }
  .stills__lightbox-close, .stills__lightbox-fs { min-width: 42px; min-height: 42px; }
  .stills__lightbox-nav { min-width: 46px; min-height: 46px; }
  .panel__switch-btn { padding: 11px 22px; }
  .choice-btn { padding: 13px 14px; }

  /* Pas de halo bleu ni de fond gris rémanent au tap sur iOS/Android : la
     rétroaction vient déjà des transitions propres à chaque élément. */
  a, button, .card, .discover-row__card, .still, .choice-btn, .genre-row__less {
    -webkit-tap-highlight-color: transparent;
  }
}

/* --------------------------------------------------------------------------
   3. ZONES SÛRES (encoches / barres système)
   Les téléphones récents ont une encoche ou une île dynamique en haut, et
   une barre de gestes en bas. env() vaut 0 sur les écrans qui n'en ont pas :
   ces ajouts sont sans risque et ne concernent de toute façon que des
   éléments déjà masqués au-dessus de 1100px.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .mobile-nav__close { top: calc(24px + env(safe-area-inset-top)); right: calc(24px + env(safe-area-inset-right)); }
  .mobile-nav { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); }
  .panel__close { top: calc(24px + env(safe-area-inset-top)); right: calc(20px + env(safe-area-inset-right)); }
  .video-modal__close { top: calc(16px + env(safe-area-inset-top)); }
  .video-modal__exit-fs { top: calc(16px + env(safe-area-inset-top)); }
  .atelier__header { padding-top: env(safe-area-inset-top); }
  .discover__header, #stillsPage .discover__header { padding-top: calc(26px + env(safe-area-inset-top)); }
}

/* --------------------------------------------------------------------------
   4. PALIER TÉLÉPHONE (≤ 600px)
   Les marges fixes (48-72px) pensées pour un grand panneau desktop
   deviennent disproportionnées sur un écran de poche : on les resserre, sans
   toucher aux mêmes propriétés au-dessus de ce seuil.
   -------------------------------------------------------------------------- */
@media (max-width: 600px) {
  .hero { padding: 24px 20px 118px; }
  .hero__meta { right: 20px; }
  .hero__frame { inset: 4vh 4vw 100px; }
  .hero__title-fill, .hero__title-outline { white-space: normal; }

  .panel__eyebrow { padding: calc(26px + env(safe-area-inset-top)) 20px 0; }
  .panel__body { padding: 16px 20px 20px; gap: 24px; }
  .panel__cards { padding: 12px 4px 24px; }
  .panel__footer { padding-left: 20px; padding-right: 20px; }

  /* LA FICHE CATÉGORIE, EN PLUS DENSE
     1.72rem/1.7 de hauteur de ligne, pensés pour une colonne de 34% d'un
     panneau de 1600px, occupent une portion disproportionnée d'un écran de
     poche — plusieurs lignes de titre, puis un pavé de texte entier, avant
     même d'apercevoir un film. On resserre le rythme vertical (titres,
     marges, boîtes) sans rien retirer : la même information tient dans
     nettement moins de hauteur, la grille de films arrive plus vite. */
  .panel__left h2 { font-size: 1.32rem; line-height: 1.2; margin-bottom: 8px; }
  .panel__left > p.desc { font-size: 0.85rem; line-height: 1.55; margin-bottom: 16px; }
  .panel__left > p.desc::after { margin-top: 14px; }
  .info-box { padding: 12px 14px; margin-bottom: 10px; }
  .info-box .desc { font-size: 0.82rem; line-height: 1.5; }
  .quote-box { padding: 12px 0; margin: 12px 0; font-size: 0.9rem; }
  .cta-row { padding: 13px; font-size: 0.7rem; }
  .check-list li { font-size: 0.74rem; padding: 6px 10px; }
  .panel__switch-btn { padding: 9px 16px; }

  .brief-form { padding: 56px 20px 0; }
  .brief-form__row { grid-template-columns: 1fr; gap: 18px; margin-bottom: 32px; }
  .brief-form__choices { grid-template-columns: 1fr 1fr; }
  .brief-form__choices--row { grid-template-columns: repeat(2, 1fr); }
  .brief-form h2 { font-size: 1.5rem; }

  .atelier__header { padding: 16px 20px; flex-wrap: wrap; gap: 12px; }
  .atelier__tabs { overflow-x: auto; -ms-overflow-style: none; scrollbar-width: none; width: 100%; }
  .atelier__tabs::-webkit-scrollbar { display: none; }
  .atelier__tab { white-space: nowrap; }
  .atelier__body { padding: 32px 20px; }
  .atelier__body[data-tab="realisateurs"] { padding: 20px 20px; }
  .manifeste, .expertise, .day-night, .steps, .team-grid, .realisateurs { grid-template-columns: 1fr; }
  .manifeste h3 { font-size: 1.5rem; }

  .video-modal__info { padding: 24px 20px 28px; gap: 28px; }
  .video-modal__title { font-size: 1.5rem; }
  .video-modal__synopsis { font-size: 0.85rem; line-height: 1.6; }

  .discover__header, #stillsPage .discover__header { padding-left: 20px; padding-right: 20px; }
  .discover__intro { padding: 0 24px; }
  .discover__hero-content { max-width: 100%; }

  .stills__body { padding: 100px 5vw 70px; }
  .stills__intro { margin-bottom: 40px; }
}

/* --------------------------------------------------------------------------
   5. GALERIES : DÉFILEMENT AU DOIGT PLUS FRANC
   overflow-x:auto suffisait déjà pour le balayage ; scroll-snap donne en plus
   ce petit "clic" qui cale une carte bien en place à la fin du geste — un
   confort tactile que la souris n'attend pas (elle, elle a les flèches).
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .discover-row__track { scroll-snap-type: x proximity; }
  .discover-row__card, .discover-row__card-placeholder { scroll-snap-align: start; }
}

/* --------------------------------------------------------------------------
   6. CHARGE ALLÉGÉE SUR MOBILE
   Un grand rayon de flou (backdrop-filter) coûte cher à recalculer sur un
   GPU de téléphone, surtout sur un élément qui occupe tout l'écran (le menu
   plein écran). On réduit le rayon uniquement quand souris ET grand écran
   sont tous deux absents — jamais sur desktop, même en fenêtre resserrée
   avec une souris.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) and (max-width: 1100px) {
  .mobile-nav {
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
  }
  .panel-overlay {
    -webkit-backdrop-filter: blur(6px) brightness(0.55) saturate(0.9);
    backdrop-filter: blur(6px) brightness(0.55) saturate(0.9);
  }
}

/* --------------------------------------------------------------------------
   7. FORMULAIRE : BOUTON D'ENVOI TOUJOURS ACCESSIBLE
   Sur un écran de poche, le formulaire est long : on colle le bouton d'envoi
   en bas de la zone de défilement plutôt que de le laisser se perdre après
   le pavé RGPD. position:sticky sur le conteneur qui défile déjà (le
   <form>) — aucun JS, aucun risque de superposition ailleurs.
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .brief-form__note { margin-bottom: 14px; }
  #formSubmit {
    position: sticky;
    bottom: 0;
    box-shadow: 0 -18px 24px 6px var(--bg-panel);
  }
}

/* --------------------------------------------------------------------------
   8. MOSAÏQUE / GRILLES PHOTO : COLONNES SERRÉES SUR TRÈS PETIT ÉCRAN
   auto-fill + minmax(280px, 1fr) laisse déjà le nombre de colonnes se
   calculer tout seul ; sous 480px, une seule colonne large est plus
   confortable qu'une colonne de 280px comprimée dans une marge étroite.
   -------------------------------------------------------------------------- */
@media (max-width: 480px) {
  .genre-row__grid--photos { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
}

/* --------------------------------------------------------------------------
   9. PANNEAU CATÉGORIE EN PLEIN ÉCRAN  (v=117)
   Sur téléphone / tablette portrait, le panneau qui glisse depuis la droite
   (Nightlife, Mariage, Brand Content, Artistes) prend TOUTE la largeur au lieu
   de laisser un liseré du fond à gauche : sur un écran de poche, ces quelques
   pour-cent visibles n'apportent rien et le contenu du panneau gagne de la
   place. Le mouvement ne change pas — il entre toujours de la droite
   (translateX(100%) → 0), simplement le point d'arrivée couvre l'écran entier.

   SEUIL VOLONTAIREMENT CALÉ SUR LA PAUSE (< 1024px).
   suspendHeroSound() met les vidéos de fond du hero EN PAUSE dès que
   innerWidth < 1024 (isMobileTier), parce que le fond est alors entièrement
   recouvert. On borne donc ce plein écran au même seuil (max-width:1023.98px,
   soit « < 1024 » côté pixel) : partout où le panneau couvre tout l'écran, la
   vidéo derrière est déjà à l'arrêt — les deux comportements sont synchrones,
   et le desktop (≥ 1024px) n'est jamais concerné.

   Le palier 1024–1100px conserve son comportement actuel (largeur 92vw, fond
   toujours visible en liseré et donc toujours en lecture) : rien n'y change.

   Scopé à #categoryPanel : le formulaire (.panel--form) garde sa largeur.
   La bordure gauche du panneau n'a plus de fond à révéler en plein écran : on
   la retire pour un bord franc.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #categoryPanel { width: 100vw; border-left: 0; }
}

/* --------------------------------------------------------------------------
   10. BARRE D'ONGLETS DU BAS  (v=118)
   Architecture mobile façon application : les cinq destinations principales
   (Accueil · LunorFilms · LunorStills · L'Atelier · Contact) deviennent
   accessibles en permanence depuis une barre basse, au lieu d'être dispersées
   entre le bouton du header, la bande de l'obturateur et le menu burger.
   Le burger du header, lui, ne change pas de rôle : il reste le choix de la
   CATÉGORIE (Nightlife, Mariage, Brand Content, Artistes).

   .tabbar est une classe NEUVE : aucune règle existante n'est réécrite, et
   l'élément est en display:none tant qu'on n'est pas passé sous 1100px — le
   desktop ne la voit jamais.

   Seuil 1100px : le même que celui où le header bascule déjà en mode mobile
   (liens masqués, burger affiché). Les deux barres apparaissent donc ensemble.
   -------------------------------------------------------------------------- */
.tabbar { display: none; }

@media (max-width: 1100px) {
  /* Hauteur de la barre + son décollement du bas, exposés en variables : tout
     ce qui doit lui laisser la place s'y réfère, une seule valeur à ajuster. */
  :root {
    --tabbar-h: 60px;
    --tabbar-gap: 10px;
    /* Espace total à réserver sous un contenu pour qu'il ne finisse pas
       masqué par la barre (encoche du bas des téléphones comprise). */
    --tabbar-space: calc(var(--tabbar-h) + var(--tabbar-gap) * 2 + env(safe-area-inset-bottom));
  }

  /* LA BARRE
     z-index 49 : au-dessus des panneaux et des pages (45 à 48) pour rester
     atteignable partout, mais SOUS la modale vidéo (50), le menu burger et la
     visionneuse photo (60) et la porte intranet (70) — ces trois-là sont des
     moments pleins écran qui doivent la recouvrir. */
  .tabbar {
    position: fixed; z-index: 49;
    left: 12px; right: 12px;
    bottom: calc(var(--tabbar-gap) + env(safe-area-inset-bottom));
    height: var(--tabbar-h);
    display: flex; align-items: stretch;
    padding: 6px;
    border-radius: calc(var(--tabbar-h) / 2);
    background: rgba(14, 14, 16, 0.72);
    -webkit-backdrop-filter: blur(18px) saturate(150%);
    backdrop-filter: blur(18px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.09);
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
    /* Le glissement horizontal sur la barre nous appartient : sans ça, le
       moteur de gestes happerait le mouvement avant nos écouteurs et le
       balayage d'un onglet à l'autre resterait sans effet. */
    touch-action: none;
    /* Un balayage prolongé sur du texte déclenche la sélection (et la loupe
       sur iOS) : le geste finissait en surlignage au lieu de changer d'onglet. */
    -webkit-user-select: none;
    user-select: none;
  }

  /* LE CURSEUR GLISSANT
     Sa position tient dans une seule variable, --tab-i (l'index de l'onglet,
     0 à 4), posée en JS. Le déplacement passe par translate3d : c'est une
     composition GPU, sans recalcul de mise en page — c'est ce qui rend le
     mouvement fluide même sur un téléphone modeste.
     La courbe est volontairement "sortie rapide" (0.22, 1, 0.36, 1) : le
     curseur part vite et se pose en douceur, au lieu d'un glissement mou. */
  .tabbar__thumb {
    position: absolute; z-index: 0;
    top: 6px; bottom: 6px; left: 6px;
    width: calc((100% - 12px) / 5);
    border-radius: calc((var(--tabbar-h) - 12px) / 2);
    background: rgba(255, 255, 255, 0.085);
    transform: translate3d(calc(var(--tab-i, 0) * 100%), 0, 0);
    transition: transform 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                opacity 0.2s var(--ease);
    will-change: transform;
    pointer-events: none;
  }
  /* Pendant le balayage, le curseur colle au doigt : aucune transition, sinon
     il traînerait derrière le geste et donnerait une impression de latence. */
  .tabbar.is-dragging .tabbar__thumb { transition: opacity 0.2s var(--ease); }
  /* Panneau catégorie ouvert : aucun onglet ne correspond, le curseur s'efface. */
  .tabbar.is-none .tabbar__thumb { opacity: 0; }

  .tabbar__btn {
    position: relative; z-index: 1;
    flex: 1; min-width: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 3px;
    border: 0; background: none; cursor: pointer;
    color: rgba(255, 255, 255, 0.52);
    transition: color 0.16s var(--ease);
    -webkit-tap-highlight-color: transparent;
  }
  .tabbar__icon {
    display: flex; align-items: center; justify-content: center;
    transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .tabbar__label {
    font-family: var(--font-mono);
    font-size: 0.42rem; letter-spacing: 0.13em;
    white-space: nowrap;
  }
  /* Réponse immédiate au doigt, avant même que la page ne s'ouvre : l'icône
     s'enfonce légèrement. C'est ce micro-retour qui fait qu'une barre paraît
     "réactive" plutôt que lente. */
  .tabbar__btn:active .tabbar__icon { transform: scale(0.88); }

  /* ONGLET ACTIF
     La couleur suit --cat-color : la barre reste accordée à l'ambiance en
     cours (verte en Nightlife, or en Mariage...). La pastille, elle, est
     portée par le curseur glissant ci-dessus. */
  .tabbar__btn.is-active {
    /* Le fondu est déjà porté par .tabbar__btn (transition: color) : rien à
       redéclarer ici, la sortie d'une catégorie se fond toute seule. */
    color: var(--ui-accent, var(--pink));
  }
  .tabbar__btn.is-active .tabbar__icon { transform: scale(1.06); }
  .tabbar__btn.is-active:active .tabbar__icon { transform: scale(0.94); }

  /* Le header perd son bouton NOUS CONTACTER : la destination vit maintenant
     dans la barre du bas, en cinquième onglet. On ne retire rien au-dessus de
     1100px — la règle est bornée à ce media query. */
  .nav__actions #contactTrigger { display: none; }

  /* LA BANDE DE L'OBTURATEUR EST REDONDANTE EN MOBILE
     .shutter occupe les 74px du bas de l'accueil et n'y propose que
     LunorFilms et LunorStills — exactement les onglets 2 et 3. Deux barres
     empilées mangeraient ~135px d'un écran de poche pour les mêmes deux
     liens. On masque donc l'obturateur SOUS 1100px seulement ; il reste
     intact sur desktop. (Pour le rétablir : supprimer ces deux lignes.) */
  .shutter { display: none; }

  /* DE LA PLACE SOUS CHAQUE CONTENU
     Sans ça, la barre recouvrirait la fin des zones qui défilent (dernière
     rangée de la galerie, pied de panneau, bouton d'envoi du formulaire). */
  .hero { padding-bottom: calc(20px + var(--tabbar-space)); }
  .panel { padding-bottom: var(--tabbar-space); }
  .discover__body { padding-bottom: var(--tabbar-space); }
  .stills__body { padding-bottom: calc(40px + var(--tabbar-space)); }
  .atelier__body { padding-bottom: calc(32px + var(--tabbar-space)); }
}

@media (max-width: 360px) {
  /* Cinq libellés sur un très petit écran : on garde les icônes lisibles et
     on laisse les mots se resserrer plutôt que se chevaucher. */
  .tabbar__label { font-size: 0.38rem; letter-spacing: 0.08em; }
}

@media (prefers-reduced-motion: reduce) {
  .tabbar__btn { transition-duration: 0.01ms; }
  .tabbar__thumb, .tabbar__icon { transition-duration: 0.01ms; }
}

/* --------------------------------------------------------------------------
   11. ACCUEIL ÉPURÉ SUR MOBILE  (v=119)
   Sur un écran de poche, l'accueil portait trois blocs devenus superflus :
     - le paragraphe de présentation (« Atelier de production exécutive
       basé à Rennes... ») : long, il repoussait le titre et le bouton vers
       le haut alors que c'est l'image qui doit parler ;
     - le sélecteur ÉCOUTER : il sert à choisir LEQUEL des trois clips parle.
       Sur téléphone il n'y a qu'un seul fond à l'écran — le choix n'a plus
       d'objet, et l'icône son du header suffit à couper ou rendre le son ;
     - le bloc de coordonnées (ATELIER / COORDS / LUNOR COLLECTIVE) : de
       l'habillage, qui encombre une colonne étroite.

   Ce qui reste se réorganise : le titre descend naturellement (il n'a plus
   le paragraphe sous lui), le bouton ENTRER se place juste dessous, et le
   logo Instagram vient lui faire face à droite, sur la même ligne.

   Aucun de ces éléments n'est retiré du DOM : ils restent en display:none,
   donc le JS qui les manipule (son du hero, coordonnées) continue de
   fonctionner sans le moindre garde-fou supplémentaire. Au-dessus de
   1100px, tout réapparaît à l'identique.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .hero__desc { display: none; }
  .hero__sound { display: none; }
  .hero__meta-info { display: none; }
  /* LE BOUTON ENTRER EST DE RETOUR  (v=123)
     Il avait été masqué au profit d'un balayage vers le haut qui ouvrait la
     page. Le balayage vertical sert désormais à AUTRE CHOSE — il change
     d'ambiance (section 14) — donc l'entrée redevient un geste explicite,
     comme sur desktop : on choisit du pouce, puis on entre d'un appui.
     .hero__sound reste masqué juste au-dessus : le sélecteur ÉCOUTER n'a
     toujours pas d'objet avec un seul fond à l'écran. */

  /* Un balayage qui part sur le titre ne doit pas le surligner. */
  .hero__title { -webkit-user-select: none; user-select: none; }
}

/* --------------------------------------------------------------------------
   12. UN ÉCRAN, PAS UNE PAGE QUI DÉFILE  (v=120)
   L'accueil mobile devient un plan fixe, façon application : plus aucun
   défilement vertical, donc plus jamais d'ascenseur sur le bord droit.

   POURQUOI 100dvh ET PAS 100vh
   Sur iOS, 100vh vaut la hauteur du navigateur BARRES MASQUÉES — soit plus
   que la surface réellement visible tant qu'elles sont là. Le hero dépassait
   donc du cadre, et c'est ce dépassement qui créait l'ascenseur. dvh mesure
   la hauteur réellement disponible à l'instant T. On garde 100vh juste
   au-dessus comme repli pour les navigateurs qui ignorent dvh (ils
   appliqueront l'ancienne valeur, sans régression).

   min-height est remis à 0 : sans ça la règle d'origine (min-height:100vh)
   l'aurait emporté sur la hauteur en dvh.

   Rien de tout ceci ne gêne les pages qui se superposent (galerie, Stills,
   L'Atelier, panneaux) : elles sont en position:fixed et défilent à
   l'intérieur d'elles-mêmes, pas dans le document.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  html, body {
    height: 100%;
    min-height: 0;
    overflow: hidden;
    /* Coupe aussi le rebond élastique de Safari, qui laissait entrevoir du
       vide au-dessus et en dessous du plan. */
    overscroll-behavior: none;
  }
  .hero {
    min-height: 0;
    height: 100vh;
    height: 100dvh;
  }
}

/* --------------------------------------------------------------------------
   13. LE RAIL D'AMBIANCES  (v=123)
   L'incitation d'avant disait « balayez vers le haut pour entrer », avec le
   nom de la destination recopié du bouton. Le geste ayant changé de sens, ce
   message serait devenu faux : le balayage vertical ne fait plus entrer, il
   fait DÉFILER les ambiances (Accueil, Nightlife, Mariage, Brand Content,
   Artistes). Et le bouton ENTRER, revenu, dit déjà où l'on va.

   Ce qu'il reste à dire est donc d'une autre nature : non plus « fais ce
   geste » mais « tu es ici, sur cinq ». C'est exactement ce qu'un rail de
   points raconte sans un mot — et il donne au passage la seule chose qui
   manquait vraiment : la certitude que le geste a été pris en compte, avant
   même que la vidéo de fond n'ait changé.

   Bord droit, à mi-hauteur : il ne croise ni le titre (aligné en bas à
   gauche), ni le logo Instagram, ni la barre d'onglets. Le point actif
   s'allonge et prend la couleur de la catégorie ; les autres restent des
   points. Purement décoratif — le geste se fait sur tout l'écran.

   .hero-pager est une classe NEUVE, en display:none par défaut : le desktop
   ne la voit jamais.
   -------------------------------------------------------------------------- */
.hero-pager { display: none; }

@media (max-width: 1100px) {
  .hero-pager {
    position: absolute; z-index: 3;
    right: 9px; top: 50%; transform: translateY(-50%);
    display: flex; flex-direction: column; align-items: center; gap: 7px;
    pointer-events: none;
    -webkit-user-select: none; user-select: none;
  }
  .hero-pager__dot {
    width: 3px; height: 3px; border-radius: 2px;
    background: rgba(255, 255, 255, 0.34);
    /* La hauteur ET la couleur bougent : deux propriétés bon marché, aucune
       mise en page à recalculer ailleurs — le rail garde sa largeur. */
    transition: height 0.32s var(--ease), background 0.32s var(--ease);
  }
  .hero-pager__dot.is-on {
    height: 17px;
    background: var(--cat-color, var(--pink));
  }

  /* Écran au repos (inactivité) : le rail s'efface avec le reste. */
  .hero.is-idle .hero-pager { opacity: 0; transition: opacity 0.6s var(--ease); }

  /* LE BAS DE L'ÉCRAN, DE BAS EN HAUT : barre d'onglets, puis la ligne
     ENTRER / Instagram, puis le titre. L'ancien étage « incitation » (54px)
     disparaît avec elle : c'est ce qu'on rend au bouton.
     ATTENTION : un palier plus haut (≤900px) repasse .hero__meta en
     position:static — bottom/right n'auraient alors plus aucun effet. On
     rétablit donc explicitement le positionnement absolu ici. */
  .hero__meta {
    position: absolute;
    bottom: calc(var(--tabbar-space) + 19px);
    right: 20px;
    margin-top: 0;
    text-align: right;
  }
  .hero__meta-bottom { margin-top: 0; gap: 0; }

  /* Le hero aligne son contenu en bas (justify-content:flex-end) : c'est son
     padding bas qui décide où se pose le bouton ENTRER, dernier élément du
     flux. On y réserve la barre d'onglets et rien de plus. */
  .hero {
    padding-bottom: calc(var(--tabbar-space) + 14px);
  }
  /* Le bouton ne doit pas s'étirer sur toute la largeur ni buter contre le
     logo Instagram qui lui fait face, à 20px du bord droit. */
  .hero .seuil { max-width: calc(100% - 62px); }
  .hero .seuil__label { overflow: hidden; text-overflow: ellipsis; }
}

/* --------------------------------------------------------------------------
   14. LE BALAYAGE VERTICAL CHANGE D'AMBIANCE  (v=123)
   REMPLACE l'ancien pager vertical, qui faisait monter la page d'entrée
   depuis le bas. Nouveau partage des gestes, calqué sur le modèle desktop :
     - balayage vertical = on parcourt les ambiances, sans quitter l'accueil ;
     - appui sur ENTRER   = on ouvre celle qu'on a choisie ;
   et la page qui s'ouvre revient donc par la DROITE, avec sa croix, comme
   partout ailleurs. Tout ce que la section 14 précédente redéfinissait
   (arrivée par le bas, croix masquée, voile supprimé) a été retiré : les
   règles d'origine reprennent la main d'elles-mêmes, sans surcharge à
   entretenir.

   POURQUOI UN FONDU AU NOIR ET PAS UN GLISSEMENT
   Une page qui suit le doigt suppose de pouvoir afficher les deux états à la
   fois. Ici l'état, c'est une VIDÉO — parfois trois. Les empiler pour les
   faire glisser côte à côte reviendrait à décoder deux fonds simultanément
   sur un téléphone : le geste serait fluide en théorie et saccadé en
   pratique. On fait donc ce que fait le cinéma quand il change de plan : un
   fondu court par le noir. Le fond sortant s'efface, le fond entrant arrive,
   et le montage se fait dans l'intervalle où il n'y a rien à décoder.

   UNE SEULE ANIMATION, DEUX MOITIÉS. La classe est posée une fois, le JS
   change la catégorie au point mort (~45%) — quand tout est déjà à opacité 0
   — puis la retire à la fin. Aucune synchronisation à tenir entre deux
   animations séparées : c'est la même image-clé qui garantit que le
   changement ne se voit pas.

   Le contenu de premier plan (titre, bouton) part dans le SENS du geste et
   revient de l'autre côté : c'est ce décalage, et lui seul, qui donne la
   sensation d'avoir tourné une page plutôt que rechargé la même.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  :root { --swap-time: 0.40s; }

  /* LE COMBINATEUR « > » N'EST PAS UN DÉTAIL DE STYLE.
     renderHeroClips reconstruit le contenu du triptyque (tri.innerHTML) au
     point mort du fondu. Les cellules qu'il crée contiennent chacune une
     <video> et une .hero__poster NEUVES. Avec un sélecteur descendant, ces
     éléments nés à 45% se mettraient à jouer l'animation depuis son début :
     le fond entrant repartirait à zéro, disparaîtrait une seconde fois, puis
     reviendrait — un double clignotement. En ne visant que les ENFANTS
     DIRECTS du hero, on n'anime que des éléments stables, qui traversent le
     changement de fond sans être remplacés ; le sous-arbre suit
     l'opacité de son conteneur, ce qui est exactement l'effet voulu. */
  .hero.is-swap > .hero__triptych,
  .hero.is-swap > .hero__video,
  .hero.is-swap > .hero__poster,
  .hero.is-swap > .hero__fallback,
  .hero.is-swap > .hero__ambilight {
    animation: heroSwapFade var(--swap-time) var(--ease);
  }

  .hero.is-swap-up .hero__content,
  .hero.is-swap-up .hero__meta { animation: heroSwapUp var(--swap-time) var(--ease); }
  .hero.is-swap-down .hero__content,
  .hero.is-swap-down .hero__meta { animation: heroSwapDown var(--swap-time) var(--ease); }

  /* Pendant le fondu, plus rien n'est cliquable : un appui à l'aveugle sur un
     bouton à opacité 0, qui appartient déjà à l'ambiance suivante, ouvrirait
     une page que l'utilisateur n'a pas choisie. */
  .hero.is-swap .hero__content,
  .hero.is-swap .hero__meta { pointer-events: none; }

  /* Le rail, lui, ne clignote PAS : il bascule immédiatement sur le nouveau
     point. C'est la seule confirmation disponible pendant les 200 ms où
     l'écran est au noir — la retarder rendrait le geste incertain. */
}

/* Le palier à zéro est volontairement COURT (42% → 52%, soit ~40 ms) : il
   n'a qu'à couvrir l'instant où le fond est remplacé. Plus long, il devient
   un noir que l'on regarde ; plus court, le montage se voit. */
@keyframes heroSwapFade {
  0%   { opacity: 1; }
  42%  { opacity: 0; }
  52%  { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes heroSwapUp {
  0%   { transform: translateY(0);     opacity: 1; }
  42%  { transform: translateY(-24px); opacity: 0; }
  52%  { transform: translateY(24px);  opacity: 0; }
  100% { transform: translateY(0);     opacity: 1; }
}
@keyframes heroSwapDown {
  0%   { transform: translateY(0);     opacity: 1; }
  42%  { transform: translateY(24px);  opacity: 0; }
  52%  { transform: translateY(-24px); opacity: 0; }
  100% { transform: translateY(0);     opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hero.is-swap > .hero__triptych,
  .hero.is-swap > .hero__video,
  .hero.is-swap > .hero__poster,
  .hero.is-swap > .hero__fallback,
  .hero.is-swap > .hero__ambilight,
  .hero.is-swap-up .hero__content,   .hero.is-swap-up .hero__meta,
  .hero.is-swap-down .hero__content, .hero.is-swap-down .hero__meta {
    animation-duration: 0.01ms;
  }
}

/* --------------------------------------------------------------------------
   15. LE DÉFILEMENT DU PANNEAU RESTE DANS LE PANNEAU  (v=122, revu v=123)
   Cette section portait aussi une transform neutre permanente sur le hero :
   elle évitait un basculement de « bloc conteneur » au moment où l'ancien
   pager poussait l'accueil hors cadre. Ce pager n'existe plus (section 14),
   le hero ne reçoit donc plus jamais de transform — la règle est retirée
   plutôt que laissée à ne rien faire, un calque promu en permanence pour
   personne étant un coût sans contrepartie.

   Ce qui reste est indépendant du geste et vaut toujours.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  /* Le défilement du panneau ne remonte plus au document : sans ça, arriver
     en bas de la liste des films relançait le rebond élastique de la page
     entière — un à-coup au milieu d'un geste qui devait être continu. */
  .panel__body { overscroll-behavior: contain; }
}

/* --------------------------------------------------------------------------
   16. L'EN-TÊTE DE CATÉGORIE, ET LE PIED DE PAGE QUI S'EN VA  (v=122)

   LE PIED DE PAGE PART
   « © 2026 LUNOR RENNES · Instagram · PRODUCTION CINÉMATOGRAPHIQUE » occupe
   une bande pleine largeur, bordure comprise, pour trois informations dont
   aucune n'est une destination : le contact a son onglet, l'Instagram est
   déjà sur l'accueil, et la mention légale n'a pas à coûter un huitième d'un
   écran de poche. Sur desktop il reste, il y a la place.

   L'EN-TÊTE PREND FORME
   Le libellé « CATÉGORIE : AFTERMOVIES » était une ligne de texte flottante
   à 16px du bloc suivant : rien ne disait où finissait l'en-tête et où
   commençait la fiche, d'où l'impression qu'il « collait » au bloc du bas.
   On en fait une vraie barre de titre — respiration au-dessus, filet en
   dessous, léger dégradé qui la détache du fond — et le point de couleur
   gagne un halo à la teinte de la catégorie : c'est lui qui porte
   l'identification, le texte n'a plus à grossir pour se faire voir.

   LA POIGNÉE A ÉTÉ RETIRÉE (v=123)
   Elle annonçait un geste vers le bas, du temps où la page arrivait du bas.
   La page revient maintenant par la droite et la croix est de retour : une
   poignée en haut au centre désignerait un geste qui n'existe plus.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  #categoryPanel .panel__footer { display: none; }

  /* LA CROIX REJOINT L'EN-TÊTE  (v=123)
     Elle est revenue en haut à droite (le panneau entre de nouveau par la
     droite). À sa position d'origine — 24px du haut, 42px de haut au doigt —
     elle flottait au-dessus du filet de l'en-tête et le traversait. On la
     remonte pour que son centre tombe sur celui du libellé : les deux ne sont
     plus deux éléments superposés mais une seule barre de titre. */
  #categoryPanel .panel__close {
    /* La taille est FIXÉE ici, et pas laissée aux règles de cible tactile
       (42px en pointer:coarse, 38px sinon) : tout l'alignement ci-dessous
       repose sur elle, et un écart de 4px selon l'appareil décalerait la
       croix par rapport au libellé sans que rien ne l'explique. */
    width: 42px; height: 42px;
    top: calc(12px + env(safe-area-inset-top));
    right: calc(16px + env(safe-area-inset-right));
  }

  /* L'ID l'emporte volontairement sur la règle ≤600px (section 4) : un seul
     en-tête pour toute la plage mobile, au lieu de deux réglages à suivre.
     Le padding droit réserve la place de la croix — sans lui, un libellé long
     (« CATÉGORIE : BRAND CONTENT ») passerait dessous. */
  #categoryPanel .panel__eyebrow {
    /* ALIGNEMENT DE LA CROIX SUR LE LIBELLÉ, SANS ARITHMÉTIQUE DE POLICE
       (v=126). La croix flottait plus bas que le texte et touchait le filet.
       On aurait pu la remonter d'un nombre de pixels trouvé à l'œil, mais ce
       nombre aurait dépendu de la taille de police, de sa hauteur de ligne et
       du rendu de chaque navigateur — donc juste ici et faux ailleurs.

       On donne plutôt à l'en-tête une BANDE de la hauteur exacte de la croix
       (42px), et on centre le texte dedans : .panel__eyebrow est déjà en
       display:flex avec align-items:center, il n'y a rien à ajouter. La croix
       partant du même bord haut (12px) et faisant la même hauteur, les deux
       centres tombent au même endroit par construction, quelle que soit la
       police.

       box-sizing:border-box (règle globale, ligne 29) : min-height comprend
       le padding ET la bordure. La bande utile vaut donc
       min-height − padding-haut − padding-bas − filet, soit
       (67 + encoche) − (12 + encoche) − 12 − 1 = 42px. L'encoche apparaît des
       deux côtés de la soustraction et s'annule : la bande fait 42px sur tous
       les téléphones, et la croix reste alignée.

       LE RETRAIT DE TÊTE EST UN PADDING, PAS UNE MARGE (v=128). Avec une
       marge, la barre commençait 12px + encoche PLUS BAS que le haut de
       l'écran, et cet espace laissait voir le fond nu du panneau : c'est la
       « bande noire » au-dessus de CATÉGORIE. En padding, le fond de la barre
       — son dégradé et son filet — part du bord réel de l'écran et passe SOUS
       la barre d'état, tandis que le texte reste exactement où il était.
       C'est déjà le procédé des en-têtes de LunorFilms, LunorStills et
       L'Atelier (section 3) : cet en-tête-ci était le seul à faire autrement. */
    padding: calc(12px + env(safe-area-inset-top)) 62px 12px 20px;
    min-height: calc(67px + env(safe-area-inset-top));
    gap: 10px;
    font-size: 0.66rem; letter-spacing: 0.18em;
    color: #f0f0f2;
    border-bottom: 1px solid var(--line);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent);
  }
  #categoryPanel .panel__eyebrow .dot {
    width: 8px; height: 8px; flex: 0 0 auto;
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--cat-color, var(--pink)) 20%, transparent);
  }

  /* Le filet ci-dessus sépare déjà : l'air qui suit n'a plus à faire ce
     travail, il n'a qu'à laisser respirer la fiche. */
  #categoryPanel .panel__body { padding-top: 20px; }
}

/* --------------------------------------------------------------------------
   17. PLUS DE FILMS À L'ÉCRAN, D'UN SEUL REGARD  (v=122)
   minmax(128px) a été calibré pour une colonne de droite de panneau desktop.
   Sur un téléphone, la même valeur ne laisse tenir que DEUX cartes par ligne,
   hautes de 220px : une rangée de genre remplit la moitié de l'écran pour y
   montrer deux films sur vingt.

   96px descend le calcul d'un cran partout : trois cartes sur un téléphone au
   lieu de deux, six sur une tablette au lieu de quatre — soit une carte
   30 à 35% plus étroite, ce qui était la demande. La grille reste en
   auto-fill : rien n'est figé, c'est toujours la largeur disponible qui
   décide du nombre de colonnes, et clampGenreRows() mesure ce nombre au lieu
   de le supposer (gridColumnCount) — aucun JS à retoucher.

   Tout ce qui est POSÉ SUR la carte suit la même réduction : à 100px de
   large, un titre de 0.82rem et un badge de catégorie de 0.55rem occupent
   l'image entière. Le titre est en plus borné à deux lignes : au-delà il
   débordait sous la carte suivante.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .genre-row__grid {
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 8px;
  }

  .genre-row__grid .card--tall .card__title {
    font-size: 0.66rem; line-height: 1.2;
    /* Deux lignes maximum, coupées par des points de suspension plutôt qu'au
       milieu d'un mot — même traitement que les taglines (v=109). */
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
  }
  .genre-row__grid .card--tall .card__body { left: 7px; right: 7px; bottom: 7px; }
  .genre-row__grid .card--tall .card__tag {
    top: 7px; left: 7px;
    font-size: 0.46rem; letter-spacing: 0.04em; padding: 2px 5px;
  }

  /* La carte PLUS partage la grille : elle rétrécit avec les autres. Son
     libellé espacé de 0.28em ne tenait plus sur 100px. */
  .card--more__inner { gap: 4px; }
  .card--more__count { font-size: 0.52rem; }
  .card--more__label { font-size: 0.6rem; letter-spacing: 0.16em; text-indent: 0.16em; }
  .card--more__arrow svg { width: 16px; height: 16px; }

  /* Des cartes plus basses rendent l'ancien interligne de rangée
     disproportionné : il annonçait des blocs deux fois plus hauts. */
  .genre-row + .genre-row { margin-top: 24px; }
  .genre-row__head { margin-bottom: 10px; gap: 10px; }
  .genre-row__title { font-size: 0.72rem; letter-spacing: 0.1em; }
}

/* --------------------------------------------------------------------------
   18. LA RÉPONSE AU DOIGT DOIT ÊTRE IMMÉDIATE  (v=122)
   Sur mobile, deux règles se sont additionnées sans qu'on y prenne garde :
   la section 1 neutralise les effets de survol (ils resteraient collés après
   un tap), et la section 2 supprime le halo gris natif d'iOS/Android
   (-webkit-tap-highlight-color). Résultat : un appui sur une carte ne
   produisait plus RIEN tant que la fiche ne s'ouvrait pas. Or ce délai — le
   temps de charger une vignette, de monter une modale — est justement celui
   pendant lequel l'utilisateur se demande si son geste a été pris.

   :active se déclenche à la pression, avant tout code : c'est la seule
   rétroaction qui ne peut pas être en retard. 0.12s et un enfoncement de 4%,
   assez pour être senti, trop court pour être vu comme une animation.
   Borné à (hover: none) : la souris, elle, a déjà son survol.
   -------------------------------------------------------------------------- */
@media (hover: none) {
  /* Chaque transition existante est RECOPIEE telle quelle, seule celle de
     transform est raccourcie : remplacer la propriete entiere aurait supprime
     au passage les fondus de couleur et de bordure de ces memes boutons. */
  .genre-row__grid .card {
    transition: transform 0.12s cubic-bezier(0.2, 0, 0, 1),
                filter 0.45s var(--ease), box-shadow 0.35s var(--ease);
  }
  .card--more {
    transition: border-color 0.3s var(--ease),
                transform 0.12s cubic-bezier(0.2, 0, 0, 1);
  }
  .panel__switch-btn {
    transition: color 0.3s var(--ease),
                transform 0.12s cubic-bezier(0.2, 0, 0, 1);
  }
  .genre-row__less {
    transition: color 0.2s var(--ease), border-color 0.2s var(--ease),
                transform 0.12s cubic-bezier(0.2, 0, 0, 1);
  }

  .genre-row__grid .card:active,
  .card--more:active,
  .genre-row__less:active { transform: scale(0.96); }
  .panel__switch-btn:active { transform: scale(0.94); }
  /* La barre d'onglets est deja servie : .tabbar__btn:active enfonce son
     icone (section 10). Rien a ajouter ici, sous peine de cumuler les deux
     reductions d'echelle sur le meme appui. */
}

/* « and (hover: none) » n'est pas decoratif : sans lui, un poste de travail
   ayant coche « reduire les animations » aurait vu ces regles :active
   l'emporter sur le survol des memes cartes — soit une modification du
   desktop, ce que cette section entiere s'interdit. */
@media (prefers-reduced-motion: reduce) and (hover: none) {
  .genre-row__grid .card:active,
  .card--more:active,
  .panel__switch-btn:active,
  .genre-row__less:active { transform: none; }
}

/* --------------------------------------------------------------------------
   19. LA BARRE SE FAIT OUBLIER  (v=124)
   Deux moments, et deux seulement, où la barre d'onglets n'est plus le sujet :

     1. LE PANNEAU CATÉGORIE est ouvert (Nightlife, Mariage, Brand Content,
        Artistes). On y est entré par le bouton ENTRER, et cette destination
        n'appartient PAS à la barre : aucun onglet ne s'y allume. La barre n'a
        donc plus rien à annoncer, elle se réduit à des raccourcis.
     2. LE MODE IMMERSIF (son ouvert, écran au repos : .hero.is-idle). Tout
        l'accueil s'efface pour ne laisser que la vidéo. Une barre restée à
        pleine taille était alors le seul élément d'interface encore présent
        — donc le seul à regarder, exactement l'inverse de l'effet cherché.

   L'ATELIER, LUNORFILMS, LUNORSTILLS ET LE CONTACT GARDENT LA BARRE PLEINE.
   Ce sont les destinations de la barre elle-même : un onglet y est allumé, et
   c'est précisément là qu'elle sert de repérage. Replier ses libellés à ce
   moment-là reviendrait à lui retirer sa fonction au moment où elle est
   utile. Le critère n'est donc pas « une page est ouverte » mais « la barre
   a-t-elle encore quelque chose à dire » — et la réponse se lit dans
   currentTabbarTarget(), qui renvoie null pour le seul panneau catégorie.

   CE QUI CHANGE : les libellés se replient (opacité ET hauteur, sinon ils
   laisseraient leur place vide), la barre perd 14px de haut, se resserre
   latéralement et descend à 4px du bord. Le principe est celui des barres
   d'onglets d'applications : les pictogrammes suffisent une fois que
   l'utilisateur sait où il est.

   POURQUOI --tabbar-space N'EST PAS TOUCHÉ
   Cette variable est définie sur :root et sert de réserve de place à tout ce
   qui doit rester au-dessus de la barre (hero, panneaux, galeries). On
   redéfinit --tabbar-h sur la BARRE seulement : les valeurs internes suivent
   (hauteur, arrondis, curseur), mais la réserve de place ne bouge pas. Sans
   cette précaution, chaque passage en mode compact ferait se recalculer et
   remonter le contenu de toutes les pages — un reflux général pour gagner
   14px, au pire moment.

   Le rembourrage de 6px reste inchangé de propos délibéré : idxFromX(), qui
   traduit une abscisse en numéro d'onglet pendant le balayage de la barre,
   raisonne sur cette valeur. La modifier ici décalerait les zones tactiles
   sans que rien ne le signale.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .tabbar {
    transition: height 0.34s var(--ease), bottom 0.34s var(--ease),
                left 0.34s var(--ease), right 0.34s var(--ease),
                border-radius 0.34s var(--ease), background 0.34s var(--ease);
  }
  /* max-height plutôt que display:none : on veut voir le libellé se replier,
     et une hauteur nulle rend vraiment la place à l'icône, qui se recentre. */
  .tabbar__label {
    max-height: 1.4em; opacity: 1; overflow: hidden;
    transition: opacity 0.18s var(--ease), max-height 0.3s var(--ease);
  }
  /* La couleur est recopiée : remplacer la transition entière l'aurait
     supprimée au passage (voir .tabbar__btn, section 10). */
  .tabbar__btn { transition: color 0.16s var(--ease), gap 0.3s var(--ease); }

  .tabbar.is-compact {
    --tabbar-h: 46px;
    height: 46px;
    border-radius: 23px;
    bottom: calc(4px + env(safe-area-inset-bottom));
    left: 30px; right: 30px;
    /* Un peu plus transparente : moins de matière, moins de présence. */
    background: rgba(14, 14, 16, 0.60);
  }
  .tabbar.is-compact .tabbar__label { opacity: 0; max-height: 0; }
  .tabbar.is-compact .tabbar__btn { gap: 0; }
}

/* Borné à la plage mobile ET complété de .tabbar__btn : la règle de la
   section 10 qui neutralisait déjà sa durée est plus haut dans le fichier,
   celle que l'on vient d'écrire l'aurait donc emporté sur elle. */
@media (prefers-reduced-motion: reduce) and (max-width: 1100px) {
  .tabbar, .tabbar__label, .tabbar__btn { transition-duration: 0.01ms; }
}

/* --------------------------------------------------------------------------
   20. LA PAGE VA JUSQU'AU BAS DE L'ÉCRAN  (v=126)
   La barre d'onglets réservait sa place en poussant le bas du panneau vers le
   haut (.panel { padding-bottom: var(--tabbar-space) }, section 10) : une
   bande morte de ~74px, sur laquelle il n'y avait rien à voir, et un filet
   qui dessinait une fin de page là où l'écran continuait.

   C'est un contresens sur ce qu'est cette barre. Une pilule translucide au
   verre dépoli n'est pas une bordure de mise en page : elle est FAITE pour
   flotter au-dessus du contenu, et c'est ce contenu qui passe dessous — en
   partie visible à travers elle, entièrement visible de part et d'autre
   puisqu'elle ne touche pas les bords. Lui réserver un couloir vide revient
   à payer le prix d'une barre opaque tout en affichant une barre translucide.

   RÉSERVER LA PLACE AU BOUT DU DÉFILEMENT, PAS DANS LA MISE EN PAGE.
   Le padding quitte le panneau (qui reprend donc toute la hauteur de l'écran,
   soit une rangée de films de plus visible d'un coup) et se pose en bas de la
   zone qui DÉFILE. Effet : le contenu passe librement sous la barre pendant
   la lecture, et en fin de course la dernière rangée peut quand même remonter
   au-dessus d'elle. On ne perd rien, on ne cache rien.

   La réserve est calculée sur la barre COMPACTE (46px + 4px de décollement),
   la seule que l'on voie dans ce panneau — voir section 19. Utiliser
   --tabbar-space, taillé pour la barre pleine, aurait rendu 20px de trop.

   Borné à #categoryPanel : le formulaire de contact garde son comportement,
   la barre y est pleine.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  :root {
    /* Bas de la barre compacte + sa hauteur + un peu d'air. */
    --tabbar-space-compact: calc(46px + 4px + 12px + env(safe-area-inset-bottom));
  }

  #categoryPanel { padding-bottom: 0; }

  /* L'ID l'emporte sur le padding raccourci de la section 4 (≤600px), quel
     que soit l'ordre des deux règles dans le fichier. */
  #categoryPanel .panel__body { padding-bottom: var(--tabbar-space-compact); }
}

/* --------------------------------------------------------------------------
   21. LA PAGE OCCUPE TOUT L'ÉCRAN  (v=127)
   Deux bandes noires encadraient le site sur téléphone : l'une au-dessus de
   l'en-tête de catégorie, l'autre sous la barre d'onglets. Ce n'était pas du
   vide laissé par une marge — c'était le hors-champ. Sans viewport-fit=cover,
   iOS retire les zones de sécurité (encoche, barre de gestes) de la fenêtre
   et les remplit avec le fond du document (--bg #050506), pendant que le
   panneau, lui, est en --bg-panel #0a0a0c. Deux noirs voisins mais distincts :
   d'où la couture visible. La balise est désormais posée dans index.html.

   CE QUE ÇA NE DÉPLACE PAS
   Tout ce qui se cale déjà sur env(safe-area-inset-*) reste EXACTEMENT où il
   est : la barre d'onglets, l'en-tête de catégorie, la croix, les en-têtes
   des univers. C'est le principe même de ces variables — elles valaient zéro
   tant que la fenêtre s'arrêtait au bord sûr, elles valent maintenant la
   hauteur de l'encoche, et la somme ne change pas. La barre du bas ne bouge
   donc pas d'un pixel ; c'est seulement le FOND qui s'étend derrière elle,
   jusqu'au bas réel de l'écran — et donc sous la barre de Safari, qui est
   translucide et laisse voir le site au travers.

   CE QUE ÇA DÉPLACE, ET QU'IL FAUT RATTRAPER
   À l'inverse, ce qui touchait un bord SANS passer par env() se retrouve
   maintenant sous l'encoche ou sous la barre de gestes. C'est ce que cette
   section rattrape, élément par élément. Les valeurs d'origine sont
   conservées telles quelles et simplement augmentées de l'inset : à l'écran,
   rien ne change par rapport à avant.

   Les insets GAUCHE et DROITE ne valent zéro qu'en portrait ; en paysage sur
   un téléphone à encoche ils atteignent une quarantaine de pixels. D'où leur
   présence sur les éléments ancrés à un bord latéral.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  /* L'en-tête de l'accueil : .nav y est en position relative (top:16px), donc
     c'est à la zone qui le contient de descendre sous l'encoche. */
  .nav-zone { padding-top: env(safe-area-inset-top); }

  .tabbar {
    left: calc(12px + env(safe-area-inset-left));
    right: calc(12px + env(safe-area-inset-right));
  }
  .tabbar.is-compact {
    left: calc(30px + env(safe-area-inset-left));
    right: calc(30px + env(safe-area-inset-right));
  }

  /* Le rail d'ambiances et le logo Instagram sont ancrés au bord droit. */
  .hero-pager { right: calc(9px + env(safe-area-inset-right)); }
  .hero__meta { right: calc(20px + env(safe-area-inset-right)); }

  /* Le titre de l'accueil ne doit pas passer sous l'encoche en paysage. */
  .hero {
    padding-left: calc(20px + env(safe-area-inset-left));
    padding-right: calc(20px + env(safe-area-inset-right));
  }

  /* Visionneuse photo : ses commandes sont posées en absolu sur les bords. */
  .stills__lightbox-close,
  .stills__lightbox-fs { top: calc(26px + env(safe-area-inset-top)); }

  /* Formulaire de contact : sa croix se cale déjà sur l'inset (section 3), le
     texte doit suivre, sinon les deux se chevauchent. */
  .brief-form { padding-top: calc(80px + env(safe-area-inset-top)); }
}

/* Le palier téléphone garde son propre retrait de tête, augmenté de l'inset.
   Placé APRÈS le bloc ci-dessus pour l'emporter dessus sur petit écran. */
@media (max-width: 600px) {
  .brief-form { padding-top: calc(56px + env(safe-area-inset-top)); }
}

/* --------------------------------------------------------------------------
   22. LA MÊME CHOSE POUR TOUT LE SITE  (v=127)
   La section 20 a rendu au panneau catégorie toute la hauteur de l'écran, en
   déplaçant la réserve de la mise en page vers le bout du défilement. Restait
   à généraliser — et l'inventaire est plus court qu'il n'y paraît :

     · LunorFilms, LunorStills, L'Atelier : DÉJÀ CORRECTS. Leur réserve était
       posée sur .discover__body, .stills__body et .atelier__body, c'est-à-dire
       sur les blocs qui défilent eux-mêmes. Ces blocs occupent déjà toute la
       hauteur restante ; le rembourrage n'y a jamais été qu'une fin de course.
       Rien à changer, et surtout rien à toucher pour rien.

     · L'accueil : son fond est la vidéo, qui couvre tout le cadre. Le
       rembourrage n'y sert qu'à poser le titre et le bouton au-dessus de la
       barre. Rien à changer non plus.

     · Le FORMULAIRE DE CONTACT : seul cas restant. Comme le panneau
       catégorie, il portait sa réserve sur .panel — une bande morte sous son
       pied de page. On la déplace sur le pied de page lui-même : son fond
       descend alors jusqu'au bas réel de l'écran (donc sous la barre de
       Safari, qui le laisse voir) pendant que son texte reste au-dessus de la
       barre d'onglets.

   La barre y est PLEINE (voir section 19) : la réserve se calcule donc sur
   --tabbar-space, et non sur la variante compacte de la section 20.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  #contactPanel { padding-bottom: 0; }
  #contactPanel .panel__footer {
    padding-bottom: calc(18px + var(--tabbar-space));
  }
}

/* ==========================================================================
   AFFINAGE MOBILE  (v=129) — sections 23 à 28
   --------------------------------------------------------------------------
   NOTE DE MÉTHODE, POUR PLUS TARD.
   Une piste a été tentée puis ABANDONNÉE : faire défiler les pages dans le
   DOCUMENT (au lieu de leur boîte interne) pour que Safari replie sa barre
   d'adresse. Ça marchait — la barre se repliait — mais ce n'était pas le
   besoin : le besoin, c'est que les vignettes remplissent l'écran jusqu'en
   bas et se voient DERRIÈRE la barre. Or l'architecture d'origine le fait
   déjà : les pages sont en position:fixed et, depuis viewport-fit=cover
   (section 21), leur cadre descend jusqu'au bas physique de l'écran, sous une
   barre translucide. Deux choses seulement s'y opposaient, et ce sont elles
   qu'on corrige ici — pas l'architecture.
   ========================================================================== */

/* --------------------------------------------------------------------------
   23. LA SÉLECTION DE FILMS VA JUSQU'AU BAS DE L'ÉCRAN  (v=129)
   Section 20 posait une réserve de fin de défilement (~96px) sous la grille de
   films, pour que la dernière rangée puisse remonter au-dessus de la barre
   d'onglets. C'est ELLE, le « bloc noir gris » : un aplat de --bg-panel,
   sans rien dedans, occupant exactement la bande où l'on veut voir passer les
   vignettes derrière le verre de Safari.

   Le raisonnement de la section 20 vaut pour une barre d'onglets OPAQUE. La
   nôtre est une pilule translucide qui ne touche même pas les bords : ce qui
   passe dessous reste largement lisible, et de part et d'autre entièrement.
   La réserve payait donc le prix d'une barre pleine tout en en affichant une
   flottante — exactement le contresens que la section 20 dénonçait pour la
   mise en page, et qu'elle a reproduit en fin de défilement.

   On la retire. La sélection remplit l'écran jusqu'au bord physique, elle
   passe sous la barre d'onglets ET sous celle de Safari. C'est le rendu des
   catalogues mobiles (Netflix, Prime, Amazon) et c'est ce qui était demandé.

   scroll-padding-bottom prend le relais pour ce qui compte vraiment : quand
   le navigateur amène lui-même un élément à l'écran (ancre, focus clavier),
   il ne le colle plus sous la barre.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #categoryPanel .panel__body {
    padding-bottom: 0;
    scroll-padding-bottom: var(--tabbar-space-compact);
  }
  #discoverPage .discover__body,
  #stillsPage .stills__body {
    padding-bottom: 0;
    scroll-padding-bottom: var(--tabbar-space);
  }
}

/* --------------------------------------------------------------------------
   24. LE PIED DE L'ACCUEIL REDESCEND JUSQU'EN BAS  (v=129)
   L'accueil ne défile pas (section 12) : sa hauteur est bornée à 100dvh,
   c'est-à-dire la surface visible BARRE COMPRISE. Le dégradé du .hero__scrim,
   qui court en inset:0, s'arrête donc au ras de cette barre — et la bande
   qu'on aperçoit derrière elle est de la vidéo brute, coupée net.

   ON NE TOUCHE PAS À LA HAUTEUR DU HERO : la borner en dvh est précisément ce
   qui a supprimé l'ascenseur de l'accueil. On superpose un voile en
   position:fixed — pour un élément fixe, bottom:0 désigne le bas du viewport
   de mise en page, qui descend SOUS la barre. Le dégradé va donc chercher le
   bas physique de l'écran sans que la page ne gagne un pixel de hauteur.

   ::after sur .hero plutôt qu'un élément de plus dans index.html : le voile
   est purement décoratif. z-index 1, comme le scrim : étant le dernier
   enfant, il se peint par-dessus lui, mais reste sous le titre et le bouton
   ENTRER (2), sous le rail d'ambiances (3), l'en-tête (20) et la barre (49).
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .hero::after {
    content: '';
    position: fixed;
    left: 0; right: 0; bottom: 0;
    height: calc(190px + env(safe-area-inset-bottom));
    background: linear-gradient(180deg,
      rgba(5, 5, 6, 0) 0%,
      rgba(5, 5, 6, 0.42) 44%,
      rgba(5, 5, 6, 0.86) 78%,
      #050506 100%);
    z-index: 1;
    pointer-events: none;
    transition: opacity 0.6s var(--ease);
  }
  .hero.is-idle::after { opacity: 0; }
}

/* --------------------------------------------------------------------------
   25. LES FILMS EN RANGÉES D'AFFICHES  (v=129)
   La grille de films de la fiche catégorie devient une rangée HORIZONTALE par
   genre, au format affiche 2/3, qui déborde des marges et se balaie au doigt :
   la grammaire exacte des catalogues de streaming. Deux gains, pas un :
     · on voit d'un coup d'œil qu'un genre contient plus que ce qui tient en
       largeur — une grille, elle, se referme sur « +6 PLUS » ;
     · une rangée occupe une hauteur fixe et modeste, donc plusieurs genres
       cohabitent à l'écran au lieu d'un seul.
   Les PHOTOS gardent leur grille : une mosaïque se parcourt verticalement,
   c'est sa nature.

   scroll-snap en proximity et non mandatory : on cale sans emprisonner le
   doigt. touch-action:pan-x pan-y rend à la rangée le geste horizontal que la
   section 0 avait retiré à toute la page.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos) {
    display: flex;
    gap: 10px;
    overflow-x: auto;
    /* Débordement des marges du panneau (20px, section 4) : les affiches
       filent jusqu'au bord de l'écran, et la rangée dit d'elle-même qu'elle
       continue au-delà. */
    margin-left: -20px; margin-right: -20px;
    padding: 2px 20px 8px;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    touch-action: pan-x pan-y;
  }
  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos)::-webkit-scrollbar { display: none; }

  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos) > .card {
    flex: 0 0 clamp(118px, 30vw, 162px);
    scroll-snap-align: start;
  }
  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos) .card__img {
    width: 100%; height: auto;
    aspect-ratio: 2 / 3;
    object-fit: cover;
  }
  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos) .card--more {
    aspect-ratio: 2 / 3; height: auto;
  }

  /* LUNORFILMS : les rangées existaient déjà, on règle le geste. Une carte
     pleine plus l'amorce de la suivante — l'invitation à balayer — et des
     marges qui saignent au bord de l'écran. */
  .discover-row__track {
    gap: 10px;
    padding-left: 20px; padding-right: 28px;
    scrollbar-width: none;
  }
  .discover-row__track::-webkit-scrollbar { display: none; }
  .discover-row__card,
  .discover-row__card-placeholder {
    flex-basis: clamp(228px, 66vw, 300px);
  }
}

/* --------------------------------------------------------------------------
   26. LA FICHE CATÉGORIE PREND DE L'ALLURE  (v=129)
   Un bandeau teinté à la couleur de l'univers en tête de fiche — le geste des
   fiches de titres — posé en IMAGE DE FOND sur .panel__body : pas de calque
   supplémentaire, pas de pseudo-élément à glisser au bon niveau d'empilement,
   et il défile naturellement avec le contenu. Le titre prend enfin la taille
   d'un titre, et l'appel à projet la couleur de l'univers : c'est LE bouton
   de la fiche, il n'a pas à rester gris.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #categoryPanel .panel__body {
    background-image: linear-gradient(180deg, var(--cat-color-dim, rgba(255,45,120,0.13)), rgba(10,10,12,0) 82%);
    background-image: linear-gradient(180deg,
      color-mix(in srgb, var(--cat-color, var(--pink)) 20%, transparent),
      rgba(10, 10, 12, 0) 82%);
    background-repeat: no-repeat;
    background-size: 100% 320px;
  }
  #categoryPanel .panel__left h2 {
    font-size: 1.9rem; line-height: 1.08; letter-spacing: -0.02em;
    margin-bottom: 10px;
  }
  #categoryPanel .cta-row {
    border-color: var(--cat-color, var(--pink));
    background: var(--cat-color-dim, rgba(255, 45, 120, 0.13));
  }
  /* Les onglets Films/Photos restent sous la main pendant toute la lecture.
     .panel__body est le conteneur qui défile : un sticky top:0 s'y colle. */
  #categoryPanel .panel__switch {
    position: sticky; top: 0; z-index: 4;
    padding: 10px 0 12px;
    background: linear-gradient(180deg, var(--bg-panel) 62%, rgba(10, 10, 12, 0));
  }
}

/* --------------------------------------------------------------------------
   27. CE QUI COÛTE CHER ET NE SE VOIT PAS  (v=129)

   A. LE FLOU DU VOILE, DERRIÈRE UN PANNEAU OPAQUE.
   .panel-overlay est un backdrop-filter plein écran (flou 9px + luminosité +
   saturation). Sur desktop il a un sens : le panneau ne fait que 78vw, on voit
   le site flouté à côté. Sous 1024px le panneau couvre TOUT l'écran
   (section 9) : ce flou est recalculé sur la surface entière, à chaque image,
   pour quelque chose que personne ne voit. C'est l'un des postes les plus
   coûteux d'un GPU de téléphone, et il tombait pile pendant le défilement.
   On coupe le filtre, PAS l'élément : il reste la cible de fermeture au clic
   et son opacité pilote toujours la transition. Rien ne change à l'écran.

   B. LE GRAIN, QUI FORCE UN CALQUE POUR RIEN.
   mix-blend-mode oblige le navigateur à composer le calque séparément, même à
   opacité nulle. Le grain ne s'allume qu'au SURVOL de LunorStills
   (.hero.is-frozen) : sur un écran tactile, jamais.

   C. UN FOND DERRIÈRE CHAQUE VIGNETTE.
   Avec le chargement différé ajouté en v=115, une carte pas encore chargée
   serait un trou noir. Un fond légèrement plus clair en fait une carte qui
   attend son image — la différence entre « ça charge » et « c'est cassé ».

   D. LA RÉPONSE AU DOIGT.
   Un léger enfoncement à l'appui, en transform pure : le compositeur seul,
   aucune mise en page recalculée.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .panel-overlay {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .card__img,
  .discover-row__thumb {
    background-color: #101013;
  }
}

@media (hover: none) {
  .hero__grain { display: none; }

  .card:active,
  .discover-row__card:active,
  .still:active {
    transform: scale(0.965);
    transition: transform 0.12s var(--ease);
  }
}

/* --------------------------------------------------------------------------
   28. LA BARRE D'ONGLETS, FINITION  (v=129)
   Une arête de lumière d'un pixel sur son bord haut et une ombre portée qui
   la décolle du contenu qui passe désormais dessous : c'est ce qui fait
   qu'une pilule FLOTTE au lieu d'être posée. Aucune géométrie ne change —
   idxFromX() et les zones tactiles restent exactement ce qu'elles étaient.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  .tabbar {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.07),
      0 12px 32px rgba(0, 0, 0, 0.5);
  }
}

/* --------------------------------------------------------------------------
   29. LE DOCUMENT QUI DÉFILE — POUR PASSER SOUS LE VERRE DE SAFARI  (v=130)

   POURQUOI ON Y REVIENT, ET POURQUOI C'EST LA SEULE VOIE.
   Comparaison faite sur téléphone entre ce site et Amazon : chez eux, les
   vignettes passent derrière l'interface en verre de Safari ; chez nous, un
   bloc noir coupe la page net. La différence n'est ni une couleur ni une
   marge : chez eux LA PAGE EST LE DOCUMENT QUI DÉFILE, et Safari l'étend
   alors jusqu'au bas physique de l'écran, son verre posé par-dessus. Une page
   figée (position:fixed, défilement interne) ne défile pas AUX YEUX de
   Safari : il la cantonne au-dessus de sa barre, et la zone de la barre n'a
   rien à laisser voir. C'est tout ou rien — donc c'est tout.

   CE QUI AVAIT FAIT ÉCHOUER LA PREMIÈRE TENTATIVE, ET LA CORRECTION.
   La généralisation posait en flux TOUTES les pages, y compris les FERMÉES
   (invisibles mais hautes de tout leur contenu). Le document devenait plus
   haut que la page ouverte : en défilant au-delà de sa fin, elle sortait par
   le haut et l'accueil — peint en fixe derrière — apparaissait. D'où le
   « retour à l'accueil au défilement ». Correction de racine : les règles de
   flux ne s'appliquent qu'à .is-docpage, une classe que le script pose sur LA
   SEULE page active, après sa glissade d'entrée, et retire après sa sortie.
   Une page fermée ne pèse jamais dans le document.

   L'ordre d'empilement ne change pas : la page garde son z-index, l'accueil
   fixé derrière n'a pas le sien, la barre d'onglets reste au-dessus.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* La fenêtre redevient la surface qui défile. overflow-x en CLIP et non en
     hidden : hidden sur la racine est un tueur connu du traitement « page qui
     défile » chez Safari (il fabrique un conteneur de défilement là où on
     veut la fenêtre). clip rogne pareil — la glissade de sortie vers la
     droite ne crée pas d'ascenseur horizontal — sans rien fabriquer. hidden
     reste en repli pour les Safari d'avant clip. */
  html.doc-scroll {
    height: auto; min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overflow-x: clip;
    background: var(--bg);
    overscroll-behavior: auto;
    /* scroll-behavior:smooth global : les remises à zéro du script
       s'animeraient, on verrait la page se rembobiner. */
    scroll-behavior: auto;
  }
  html.doc-scroll body {
    height: auto; min-height: 0;
    overflow: visible;
    overscroll-behavior: auto;
  }

  /* L'accueil ne pèse pas dans la hauteur du document ; entièrement recouvert,
     ses vidéos déjà en pause à ce palier (isMobileTier). Son voile de bas de
     page (section 24), arrimé à la fenêtre, n'a rien à faire par-dessus une
     page ouverte. */
  html.doc-scroll .hero { position: fixed; top: 0; left: 0; right: 0; }
  html.doc-scroll .hero::after { display: none; }

  /* Le voile de fermeture ne prend plus le tap : pendant un rebond élastique
     en bout de course, un doigt posé au-delà du bord de la page tombait sur
     lui — et fermait tout. La croix, le balayage et la barre suffisent. */
  html.doc-scroll .panel-overlay { pointer-events: none; }

  /* LA PAGE ACTIVE, ET ELLE SEULE. Quand le document est en haut, cette boîte
     est PIXEL POUR PIXEL celle du position:fixed qu'elle remplace : rien ne
     bouge à l'écran, seul le destinataire du geste change. lvh et non svh ni
     dvh : la hauteur barres masquées, la seule qui couvre le bas physique de
     l'écran et ne change pas de valeur pendant que la barre se replie. */
  html.doc-scroll .is-docpage {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: auto;
    height: auto;
    min-height: 100vh;
    min-height: 100lvh;
    overflow: visible;
  }
  /* translateX(0)/translateY(0) n'est pas `none` : une transform, même
     neutre, fait de la page le repère de ses position:fixed internes (croix,
     en-têtes), qui seraient emportés par le défilement. Visuellement, c'est
     le même point d'arrivée. */
  html.doc-scroll .is-docpage.is-open { transform: none; }

  /* ---- PANNEAU CATÉGORIE ---- */
  html.doc-scroll #categoryPanel.is-docpage .panel__body {
    overflow: visible;
    overscroll-behavior: auto;
  }
  /* L'en-tête tenait en place parce que seul .panel__body défilait ; sticky
     le fait tenir pareil quand c'est la fenêtre. Fond rendu opaque : son
     dégradé s'éteint en transparent et la grille défilerait au travers. */
  html.doc-scroll #categoryPanel.is-docpage .panel__eyebrow {
    position: sticky; top: 0; z-index: 5;
    background-color: var(--bg-panel);
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent);
  }
  html.doc-scroll #categoryPanel.is-docpage .panel__close { position: fixed; }
  /* Les onglets Films/Photos (collants, section 26) changent de référence :
     leur conteneur de défilement est maintenant la fenêtre, ils doivent donc
     s'arrêter SOUS l'en-tête et non à 0. Même bande de 67px + encoche que lui
     (voir le calcul de la section 16). */
  html.doc-scroll #categoryPanel.is-docpage .panel__switch {
    top: calc(67px + env(safe-area-inset-top));
  }

  /* ---- LUNORFILMS · LUNORSTILLS · L'ATELIER ---- */
  /* display:block : leur colonne flex ne servait qu'à répartir une hauteur
     figée ; en flux, la hauteur vient du contenu. */
  html.doc-scroll #discoverPage.is-docpage,
  html.doc-scroll #stillsPage.is-docpage,
  html.doc-scroll #atelierPanel.is-docpage { display: block; }

  html.doc-scroll #discoverPage.is-docpage .discover__body,
  html.doc-scroll #stillsPage.is-docpage .stills__body,
  html.doc-scroll #atelierPanel.is-docpage .atelier__body {
    overflow: visible;
    flex: none;
  }
  /* Les en-têtes de LunorFilms et LunorStills sont en absolute top:0 dans la
     page : dans une page qui défile, ils partiraient avec elle. fixed les
     rend à la fenêtre (transform retirée plus haut), au même endroit. Celui
     de L'Atelier est dans le flux : sticky, avec un fond. */
  html.doc-scroll #discoverPage.is-docpage .discover__header,
  html.doc-scroll #stillsPage.is-docpage .discover__header { position: fixed; }
  html.doc-scroll #atelierPanel.is-docpage .atelier__header {
    position: sticky; top: 0; z-index: 6;
    background: var(--bg);
  }

  /* Le MODE CINÉMA de LunorFilms (bannière plein écran, défilement gelé) est
     un recouvrement total : il reprend le contrat d'une page fixe le temps
     qu'il dure. Le gel du document est posé par le script (doc-lock). */
  html.doc-scroll #discoverPage.is-cinema {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    height: auto; min-height: 0; overflow: hidden;
  }
  html.doc-scroll #discoverPage.is-cinema .discover__hero { height: 100lvh; }

  /* ---- FORMULAIRE DE CONTACT ---- */
  /* Pleine largeur, comme la catégorie : le liseré de fond à gauche
     n'apporte rien sous 1024px. */
  html.doc-scroll #contactPanel.is-docpage { width: 100vw; border-left: 0; }
  html.doc-scroll #contactPanel.is-docpage .brief-form {
    overflow: visible; flex: none; min-height: 0;
  }
  html.doc-scroll #contactPanel.is-docpage .panel__close { position: fixed; }
  /* Le bouton d'envoi collait au bas de la zone de défilement du formulaire
     (section 7). Cette zone est désormais la fenêtre : sans retrait, il se
     poserait SOUS la barre d'onglets, qui est pleine sur cette page. */
  html.doc-scroll #contactPanel.is-docpage #formSubmit { bottom: var(--tabbar-space); }

  /* ---- LE VERROU DE LECTURE (doc-lock) ---- */
  /* Une surface modale (visionneuse vidéo, visionneuse photo, mode cinéma)
     recouvre une page qui défile dans le document : celui-ci ne doit plus
     bouger derrière — SANS perdre la position. Le script fige body à
     l'endroit exact (top négatif en ligne) et restaure au déverrouillage. */
  html.doc-lock body {
    position: fixed;
    left: 0; right: 0; width: 100%;
  }
}

/* --------------------------------------------------------------------------
   30. LES FANTÔMES QUI TEIGNAIENT LE VERRE  (v=131)

   LE VRAI COUPABLE DU BLOC NOIR, DOCUMENTÉ.
   Safari 26 (Liquid Glass) ne lit plus theme-color. Pour teinter sa barre, il
   ÉCHANTILLONNE les éléments en position fixed ou sticky proches des bords de
   l'écran, et en lit deux propriétés : background-color et backdrop-filter —
   Y COMPRIS sur les éléments à opacity:0 et pointer-events:none. Invisible ne
   veut pas dire ignoré : tant que l'élément est dans l'arbre de rendu, il est
   échantillonné. (Règles non documentées par Apple, établies par
   rétro-ingénierie communautaire — voir 1ar.io/updates/safari-26-liquid-glass
   -web et github.com/andesco/safari-color-tinting : zones d'échantillonnage
   ~4px du haut, ~3px du bas, ≥80% de largeur.)

   Or ce site garde en permanence dans le DOM, cachés mais peints, des écrans
   FIXES QUASI NOIRS qui touchent le bord bas : LunorFilms, LunorStills et
   L'Atelier (opacity:0, translateY(14px) — donc DANS la fenêtre), le menu
   plein écran (fond sombre + flou de 24px), les deux voiles, les panneaux.
   Safari les échantillonne, conclut « cette page est noire jusqu'en bas », et
   peint sa barre en aplat sombre — LE bloc qui masquait les vignettes, quoi
   qu'on fasse par ailleurs.

   LA CORRECTION : UN ÉLÉMENT CACHÉ NE DOIT PLUS RIEN DONNER À ÉCHANTILLONNER.
   On retire background-color et backdrop-filter de chaque surface fixe TANT
   QU'ELLE EST FERMÉE. C'est visuellement neutre par construction : ces
   surfaces sont à opacity:0. À l'ouverture, le fond revient à la frame zéro,
   sous une opacité qui part de zéro — aucune couture visible, aucune
   animation perdue, aucun JS.

   DEUX SURFACES SONT DÉJÀ SAINES, ET C'EST INSTRUCTIF : la visionneuse photo
   (son fond sombre est sur un enfant en position:absolute, que
   l'échantillonneur ignore) et la porte intranet (visibility:hidden : non
   peinte, donc non lue).

   Le fond de secours, lui, doit être EXPLICITE : quand aucun élément fixe ne
   qualifie, Safari retombe sur le fond de <html>/<body> — et un fond
   transparent retombe sur du BLANC. On le déclare donc sur <html> aussi.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* Le fond de secours du verre : explicite, jamais transparent. */
  html { background-color: var(--bg); }

  /* Les trois univers plein écran, fermés : rien à échantillonner. */
  .discover:not(.is-open),
  .stills:not(.is-open),
  .atelier:not(.is-open) { background: transparent; }

  /* Les panneaux fermés sont déjà hors cadre (translateX(100%)) mais un
     échantillonneur à géométrie près n'a pas à trancher : on retire aussi. */
  .panel:not(.is-open):not(.is-peek) { background: transparent; }

  /* La visionneuse vidéo fermée. */
  .video-modal:not(.is-open) { background: transparent; }

  /* Le menu plein écran fermé : son fond ET son flou — le backdrop-filter
     est échantillonné au même titre que la couleur. */
  .mobile-nav:not(.is-open) {
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* Les voiles fermés. */
  .panel-overlay:not(.is-visible) { background: transparent; }

  /* Pendant qu'une page défile dans le document, le voile — même visible —
     est un plein écran sombre plaqué sur le bord bas, DERRIÈRE une page
     opaque : personne ne le voit, mais l'échantillonneur si, et il peut
     l'emporter sur les vrais pixels de la page. Il ne sert plus à rien là
     (le tap y est déjà désactivé, section 29) : on le sort de l'arbre. */
  html.doc-scroll .panel-overlay { display: none; }
}

/* --------------------------------------------------------------------------
   31. LUNORFILMS EN FORMAT POCHE — EN-TÊTE DE MARQUE, AFFICHES VERTICALES  (v=134)

   Adaptation de la page « galerie des créations » (LunorFilms) au téléphone.
   Tout est sous le seuil tactile — le desktop ne voit rien de tout ça.

   A. L'EN-TÊTE. Le kicker « GALERIE DES CRÉATIONS » est calé au centre EXACT
      (réglage global desktop) et passait SOUS « NOUS CONTACTER » : masqué. Le
      bouton SON, sans objet (la vidéo de bannière est retirée), est masqué aussi.
      Restent « Accueil » à gauche, « NOUS CONTACTER » à droite.

   B. LA BANNIÈRE = LA MARQUE, RIEN D'AUTRE. Plus de vidéo (retirée du DOM en JS),
      et on retire aussi TOUT ce qui l'accompagnait : le badge, le titre, le pitch,
      les boutons LECTURE / PLUS D'INFOS. Il ne reste que le logotype LUNORFILMS,
      AGRANDI, et sa ligne « GALERIE DES CRÉATIONS · RENNES, FR » — un en-tête de
      marque sobre, puis les rangées.

   C. LES RANGÉES, DENSES ET CLIQUABLES.
      · Affiches verticales 2:3, trois de front plus l'amorce (recadrage central
        de la vignette 16:9, comme la fiche catégorie, section 25).
      · AUCUN bouton sur la carte (ni play ni flèche, retirés du DOM en JS) : c'est
        la carte ENTIÈRE qui est cliquable et ouvre la fiche du film. Sur mobile on
        n'attache plus la machinerie de survol (aperçu vidéo + agrandissement),
        qui, émulée au doigt, interférait avec le tap et alourdissait la page.
      · Rythme resserré façon catalogue mobile : on annule la grande RÉSERVE
        verticale de la piste (136/160px, prévue pour la lueur du survol desktop,
        inutile ici) et on rapproche les rangées.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* A. En-tête : kicker centré + bouton son retirés (bornés à #discoverPage :
     LunorStills partage l'en-tête, non touché ici). */
  #discoverPage .discover__kicker { display: none; }
  #discoverPage .discover__header-actions .icon-btn { display: none; }

  /* B. Bannière réduite à la marque. La vidéo est retirée en JS ; on masque en
     plus, par sécurité, la vidéo (si elle subsistait) ET tout le bloc de contenu
     (badge / titre / pitch / boutons). */
  #discoverPage:not(.is-cinema) .discover__hero {
    height: auto; min-height: 0;
    display: block;
    /* réserve la hauteur de l'en-tête absolu (top:0) sous lequel la marque
       ne doit pas passer */
    padding: calc(78px + env(safe-area-inset-top)) 0 22px;
    background-image: none !important;   /* !important : bat le style inline du JS */
  }
  #discoverPage:not(.is-cinema) .discover__hero::before { display: none; }
  #discoverPage:not(.is-cinema) .discover__hero-video { display: none; }
  #discoverPage:not(.is-cinema) .discover__hero-content { display: none; }

  /* La marque passe dans le flux ; le logotype est agrandi, la ligne
     « GALERIE DES CRÉATIONS · RENNES, FR » reste dessous. */
  #discoverPage:not(.is-cinema) .discover__brand {
    position: static;
    padding: 0 20px;
  }
  #discoverPage:not(.is-cinema) .discover__wordmark {
    font-size: clamp(2.6rem, 12vw, 3.4rem);
  }
  #discoverPage:not(.is-cinema) .discover__brand .brandmark__tag {
    margin-top: 18px; font-size: 0.62rem;
  }

  /* C. Rangées façon Netflix mobile : affiches verticales 2:3, resserrées. */
  #discoverPage .discover__rows { padding-top: 6px; }
  #discoverPage .discover-row { margin-bottom: 16px; }
  #discoverPage .discover-row__title {
    margin-left: 20px; margin-right: 20px; margin-bottom: 8px;
  }

  /* La piste : on annule la grande réserve verticale du survol desktop
     (padding 136/160 + margin -130) — inutile sans agrandissement, elle gonflait
     la hauteur et le coût de peinture. Les marges latérales viennent de la
     section 25. */
  #discoverPage .discover-row__track {
    padding-top: 0; padding-bottom: 0;
    margin-top: 0; margin-bottom: 0;
  }

  #discoverPage .discover-row__card,
  #discoverPage .discover-row__card-placeholder {
    flex-basis: clamp(112px, 30vw, 150px);
    aspect-ratio: 2 / 3;
  }
  #discoverPage .discover-row__card .tag {
    top: 7px; left: 7px; font-size: 0.5rem; padding: 2px 6px;
  }
  #discoverPage .discover-row__card .info { left: 9px; right: 9px; bottom: 9px; }
  #discoverPage .discover-row__card .director { font-size: 0.55rem; }
  #discoverPage .discover-row__card .title {
    font-size: 0.8rem; line-height: 1.2;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
  }

  /* ---- D. Tags & textes qui s'affichent partout (et pas qu'en haut) --------
     LA PROMOTION SUR COUCHE GPU RESTE RETIRÉE SUR TÉLÉPHONE, et il ne faut pas
     l'y remettre en recopiant le desktop : iOS Safari, quand les couches se
     comptent par dizaines, en LÂCHE certaines, et les cartes arrivées en cours
     de défilement perdaient leur tag et leur titre. Le desktop, lui, a besoin
     de cette promotion (v=165) parce que l'aperçu vidéo y crée une couche en
     plein survol — mais cet aperçu N'EXISTE PAS ici, renderDiscoverRow ne
     l'écrit pas sous 1024px. Deux cibles, deux réglages, et la raison n'est pas
     la même des deux côtés. */
  #discoverPage .discover-row__card .scrim,
  #discoverPage .discover-row__card .tag,
  #discoverPage .discover-row__card .info {
    transform: none;
  }
  #discoverPage .discover-row__card .tag,
  #discoverPage .discover-row__card .info { opacity: 1; }

  /* ---- E. Le tap ouvre la fiche, et la rangée défile au doigt --------------
     La piste était en pointer-events:none (héritage desktop : elle laissait
     passer le survol vers les cartes agrandies, qui se chevauchaient). Sur
     mobile il n'y a plus ni agrandissement ni chevauchement — cette valeur
     empêchait juste d'attraper la piste pour la faire défiler. On la rétablit,
     avec l'inertie iOS. Et on rend les enfants de la carte transparents au
     doigt : chaque tap va DIRECTEMENT à la carte, qui ouvre la fiche — plus rien
     n'intercepte. */
  #discoverPage .discover-row__track {
    pointer-events: auto;
    -webkit-overflow-scrolling: touch;
  }
  #discoverPage .discover-row__card { pointer-events: auto; cursor: pointer; }
  #discoverPage .discover-row__card * { pointer-events: none; }
}

/* --------------------------------------------------------------------------
   32. UNE PAGE OUVERTE EST SEULE À L'ÉCRAN  (v=136)

   CE QU'ON VOYAIT, ET CE QUE ÇA VOULAIT VRAIMENT DIRE.
   En haut et en bas d'une page, le rebond élastique de Safari décollait le
   contenu et laissait apparaître l'accueil derrière — vidéo comprise. La
   lecture naturelle est « le site charge deux pages à la fois ». Ce n'est pas
   ça : rien n'est chargé deux fois. La page ouverte défile DANS LE DOCUMENT
   depuis la section 29, alors que l'accueil, lui, est resté en position:fixed
   juste dessous. Au rebond, la page bouge et l'accueil ne bouge pas : l'écart
   entre les deux, c'est ce qu'on voyait.

   Le laisser là ne coûtait pas que cet effet de bord. C'était une surface
   plein écran de plus à composer à chaque image, ET une surface qui reçoit
   toujours le toucher : un tap qui tombait entre deux éléments de la page
   ouverte atterrissait sur l'accueil.

   LA RÈGLE, ET POURQUOI ELLE TIENT À UNE CLASSE À ELLE.
   Tant qu'une page est ouverte, l'accueil sort de l'arbre de rendu. On ne se
   sert pas de « doc-scroll » pour ça : cette classe-là ne tombe que 520 ms
   après le début de la fermeture, et l'accueil serait revenu trop tard — on
   aurait vu du noir sous la page pendant qu'elle glisse. « page-seule » naît
   en même temps mais meurt au premier instant de la fermeture.

   Ses vidéos étaient déjà en pause à ce palier (suspendHeroSound) : il ne
   restait qu'une image fixe à peindre pour rien.

   POURQUOI visibility ET NON display:none — VÉRIFIÉ, PAS SUPPOSÉ.
   display:none aurait été plus radical : plus de peinture du tout. Mais à la
   fermeture, closeAllPanels() retire « is-open » puis appelle resumeHeroSound()
   dans la foulée, de façon SYNCHRONE — alors que le retrait de « page-seule »
   passe par l'observateur, donc par une micro-tâche, donc plus tard. Le
   play() serait tombé sur une vidéo encore dans un parent display:none, où
   iOS suspend le décodage : la promesse part en échec, elle est avalée par le
   .catch() existant, et on retrouvait un accueil figé au retour, sans le
   moindre message. visibility:hidden n'a pas ce défaut : l'élément reste
   rendu, il cesse seulement d'être peint et d'être touchable.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  html.page-seule .hero {
    visibility: hidden;
    /* La moitié qui compte pour les taps fantômes : plus rien à atteindre. */
    pointer-events: none;
  }
}

/* --------------------------------------------------------------------------
   33. LA VISIONNEUSE VIDÉO AU FORMAT DU POUCE  (v=136)

   Le rond de lecture fait 78px et porte le mot « PROJECTION » en dessous :
   dimensionné pour un écran de bureau, où il flotte au centre d'une grande
   affiche. Sur un téléphone il mange l'image qu'il est censé annoncer, et le
   libellé n'apprend rien qu'un triangle dans un cercle ne dise déjà.
   On garde le halo qui respire : c'est lui qui désigne la cible.
   Le desktop n'est pas concerné — la règle vit sous le seuil tactile.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .video-modal__play-ring { width: 54px; height: 54px; }
  .video-modal__play-ring svg { width: 17px; height: 17px; }
  .video-modal__play-label { display: none; }
  /* Le libellé parti, l'écart qui l'en séparait n'a plus d'objet. */
  .video-modal__play { gap: 0; }
}

/* --------------------------------------------------------------------------
   34. LE BALAYAGE VERTICAL NOUS APPARTIENT  (v=137)

   CE QU'ON DÉCLARAIT SANS LE VOULOIR.
   La section 0 pose `touch-action: pan-y` sur html et body, pour couper le
   glissement latéral parasite de Safari. Mais `pan-y` veut dire, mot pour mot,
   « le défilement VERTICAL est l'affaire du navigateur ». Sur l'accueil il n'y
   a rien à faire défiler (section 12 : plan fixe, overflow caché) — Safari
   n'en prend pas moins le geste en charge : il arbitre, il anime sa barre
   d'outils, et il ne rend nos événements tactiles qu'ensuite. Notre propre
   balayage d'ambiance passait donc en second, ce qui se lit à l'usage comme
   « il répond quand il veut ».

   `pan-x pinch-zoom` dit l'inverse : le vertical est à nous, tout de suite.
   On garde `pan-x` (aucun effet ici, rien ne défile horizontalement) et
   surtout `pinch-zoom`, parce que `touch-action: none` aurait aussi supprimé
   le zoom à deux doigts — un réglage d'accessibilité, pas une fioriture.

   C'est le procédé déjà employé par la barre d'onglets pour son propre
   glissement (section 10) : quand un geste nous appartient, on le déclare.
   Borné à l'accueil : les pages, elles, défilent pour de bon.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .hero { touch-action: pan-x pinch-zoom; }
}

/* --------------------------------------------------------------------------
   35. LE CONTACT EST UNE PAGE, PLUS UN TIROIR  (v=137)

   Les cinq destinations de la barre d'onglets se comportent désormais toutes
   pareil sur téléphone : elles occupent l'écran entier et rien ne subsiste
   dessous (section 32). Trois d'entre elles apparaissaient déjà en fondu —
   LunorFilms, LunorStills et L'Atelier montent de 12px et s'allument. Le
   formulaire de contact, lui, restait un tiroir qui glisse depuis la droite
   sur 92vw : le seul à raconter qu'il y a autre chose à côté, alors qu'il n'y
   a plus rien à côté.

   Il prend donc toute la largeur et se contente d'apparaître.

   `visibility` et pas seulement `opacity` : à opacité nulle un élément reste
   touchable et reste échantillonné par Safari pour teinter sa barre
   (section 30). Le retard de 0.3s à la fermeture laisse le fondu se terminer
   avant que la page ne disparaisse pour de bon.

   Le panneau de CATÉGORIE garde sa glissade : elle a été demandée, elle dit
   qu'on entre dans une catégorie depuis l'accueil, et elle se referme du même
   geste vers la droite. Le contact, lui, s'atteint depuis la barre d'onglets,
   où aucune direction n'est promise.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #contactPanel {
    width: 100vw;
    border-left: 0;
    transform: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s var(--ease), visibility 0s linear 0.3s;
  }
  #contactPanel.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.3s var(--ease);
  }
}

/* --------------------------------------------------------------------------
   36. LA BARRE D'ONGLETS SUFFIT À NAVIGUER  (v=138)

   Les commandes de sortie de chaque page datent d'avant la barre du bas :
   une croix sur L'Atelier et sur le Contact, une flèche « retour accueil » en
   haut à gauche de LunorFilms et de LunorStills. Depuis que les cinq
   destinations sont accessibles en permanence en bas de l'écran, ces boutons
   ne font plus que répéter ce que la barre dit déjà — et ils prennent la place
   la plus chère de l'écran, le bord haut.

   On les MASQUE, on ne les retire pas du HTML : le desktop les garde (il n'a
   pas de barre d'onglets, ils y sont la seule sortie), et leurs écouteurs
   restent posés sans erreur.

   LE PANNEAU DE CATÉGORIE GARDE SA CROIX, ET C'EST VOULU : aucun onglet de la
   barre ne lui correspond (currentTabbarTarget() y renvoie null, c'est même
   ce qui déclenche la barre compacte, section 19). Sans sa croix ni le
   balayage vers la droite, on y serait enfermé.

   L'en-tête du contact réservait 62px à droite pour sa croix : cette réserve
   n'a plus d'objet, le texte reprend la largeur.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #atelierClose,
  #contactClose,
  #discoverBack,
  #stillsBack { display: none; }

  #contactPanel .panel__eyebrow { padding-right: 20px; }
}

/* --------------------------------------------------------------------------
   37. UNE PAGE FERMÉE N'EST PLUS DANS LE CADRE  (v=140)

   CE QUI CLOCHAIT — ET C'EST LA CAUSE PRINCIPALE DE LA LOURDEUR AU DÉMARRAGE.
   LunorFilms, LunorStills et L'Atelier se ferment comme ceci :

       position: fixed; inset: 0; opacity: 0; pointer-events: none;

   « opacity: 0 » ne retire rien À L'ÉCRAN aux yeux du navigateur : la boîte
   occupe toujours toute la fenêtre. Or c'est la GÉOMÉTRIE, et elle seule, qui
   décide du chargement paresseux des images — ni l'opacité, ni la visibilité
   n'entrent dans ce calcul. Le loading="lazy" posé sur les 101 photos de
   LunorStills, les 31 affiches de LunorFilms et les visuels de L'Atelier ne
   servait donc à rien : le navigateur les tenait toutes pour visibles, et les
   téléchargeait puis les DÉCODAIT au démarrage, pour ne rien montrer. Trois
   surfaces plein écran composées à chaque image, par-dessus le marché.

   LA CORRECTION, ET POURQUOI CELLE-CI PLUTÔT QU'UNE AUTRE.
   On déplace la page fermée hors du cadre, et rien d'autre. « top » ne fait
   pas partie des propriétés animées ici (seules opacity et transform le
   sont) : le déplacement est donc INSTANTANÉ, invisible, et le fondu
   d'ouverture comme celui de fermeture restent exactement ceux d'avant.

   Écarté — display:none : il aurait supprimé la boîte, et toutes les mesures
   internes seraient retombées à zéro (c'est le piège déjà documenté plus haut
   pour la bascule Films/Photos). Écarté — content-visibility : Safari ne le
   connaît que depuis la version 18, la moitié des iPhone n'en verrait rien.
   Une coordonnée, elle, marche partout et depuis toujours.

   height:100% et non 100vh : sur un élément fixe, le pourcentage se résout
   sur le même cadre que « inset: 0 » — la boîte garde très exactement la
   taille qu'elle avait, elle change seulement de place. Sur iOS, 100vh aurait
   donné la hauteur écran-entier, donc une boîte légèrement plus haute qu'à
   l'ouverture, donc un recalcul de plus au moment précis où l'on ouvre.

   500vh et non 200 : les navigateurs chargent en avance ce qui APPROCHE du
   cadre (jusqu'à ~3000px chez Chrome, selon la connexion). On se met
   franchement hors de portée plutôt que de raser la limite.

   visibility:hidden par-dessus : ceinture et bretelles. Hors du cadre, rien
   ne se peint déjà ; cette ligne le dit en toutes lettres au navigateur et
   coupe aussi le test de collision, au cas où un doigt maladroit irait par là.

   La classe est posée et retirée par le script — voir « LES PAGES FERMÉES
   SORTENT DU CADRE » dans script.js. Elle est présente sur desktop aussi, où
   cette règle n'existe pas : le rendu au-dessus de 1024px est inchangé.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #discoverPage.est-repliee,
  #stillsPage.est-repliee,
  #atelierPanel.est-repliee {
    /* position:fixed EXPLICITE, et ce n'est pas une redite. Pendant les
       quelques centaines de millisecondes qui suivent une fermeture, la page
       peut encore porter « is-docpage » (section 29), qui la met en
       position:absolute. Un élément absolu rallonge la zone de défilement du
       document : à 500vh, on fabriquerait un ascenseur de cinq écrans de vide.
       Un élément fixe, jamais. */
    position: fixed;
    top: 500vh;
    bottom: auto;
    height: 100%;
    visibility: hidden;
  }
}

/* --------------------------------------------------------------------------
   38. L'ÉCRAN DE CHARGEMENT DES UNIVERS  (v=142)

   Un voile posé DANS la page, pas par-dessus tout le site : il naît et meurt
   avec elle, il ne peut pas survivre à une fermeture ni recouvrir la barre
   d'onglets. z-index 20, donc au-dessus du contenu et de l'en-tête (5 et 6),
   sous la barre (60) et sous la visionneuse (50) — on ne s'enferme jamais.

   position:absolute et non fixed : dans le régime document (section 29) la
   page défile pour de bon, un voile fixe resterait planté à l'écran pendant
   qu'elle s'en va. Attaché à la page, il la suit.

   La sortie se fait en fondu SANS transition de retour : `is-done` éteint le
   voile en 0,35 s, mais sa réapparition à l'ouverture suivante doit être
   INSTANTANÉE — sinon on verrait la page nue une demi-seconde avant que le
   voile ne revienne, exactement l'inverse de ce qu'on veut. D'où la
   transition portée par `.is-done` et non par l'état de repos.

   Le fond est celui de la page (--bg), pas un noir arbitraire : quand le voile
   s'efface, rien ne change de couleur, le contenu apparaît simplement.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .page-load {
    position: absolute; inset: 0; z-index: 20;
    display: flex; align-items: center; justify-content: center;
    background: var(--bg);
    opacity: 1;
    pointer-events: none;   /* il informe, il ne bloque rien */
  }
  .page-load.is-done {
    opacity: 0;
    transition: opacity 0.35s var(--ease);
  }

  /* Un anneau qui tourne, dans la couleur de la page. Rien d'autre : un
     libellé « CHARGEMENT » demanderait d'être lu, alors que le mouvement se
     comprend sans un mot et sans traduction. */
  .page-load__ring {
    width: 26px; height: 26px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.14);
    border-top-color: var(--cat-color, var(--pink));
    animation: page-load-spin 0.75s linear infinite;
  }

  /* Certaines personnes règlent leur téléphone pour supprimer les animations —
     c'est un réglage d'accessibilité, pas une préférence esthétique. On garde
     alors un point fixe : l'information reste, le mouvement part. */
  @media (prefers-reduced-motion: reduce) {
    .page-load__ring { animation: none; border-top-color: var(--cat-color, var(--pink)); }
  }
}

@keyframes page-load-spin {
  to { transform: rotate(360deg); }
}

/* ==========================================================================
   39. L'AMBILIGHT NE TRAVERSE PAS SUR TÉLÉPHONE  (v=143)
   --------------------------------------------------------------------------
   CE QU'ON A TROUVÉ, ET POURQUOI ÇA A MIS SI LONGTEMPS.

   Le « gloom » a été écrit pour le desktop : quand un fond est UNE SEULE vidéo
   VERTICALE, elle ne peut pas remplir un écran 16:9 sans être massacrée. On la
   pose donc entière, au centre, et on comble les côtés avec une copie du même
   fichier, agrandie et floutée à 80 px. C'est beau, c'est justifié, et sur un
   ordinateur ça ne coûte rien de visible.

   La condition, côté script, est `heroClips.length === 1 && format Vertical`.
   Sur desktop la première moitié est presque toujours fausse : le triptyque
   met trois clips côte à côte, et `.hero.is-triptych.is-portrait` éteint
   d'ailleurs l'ambilight explicitement.

   SUR TÉLÉPHONE IL N'Y A JAMAIS QU'UN SEUL CLIP. La première moitié est donc
   TOUJOURS vraie, et il ne reste que le format pour décider. Or les fonds sont
   verticaux sur Nightlife (8 sur 8), Mariage, Brand Content (5 sur 5) et
   Artistes (3 sur 3) — soit 17 des 18 fonds des pages du menu, Nightlife étant
   la page sur laquelle le site s'ouvre.

   Autrement dit, dès la première seconde et sur quasiment chaque page, un
   téléphone décodait DEUX FLUX H.264 du même fichier en parallèle et
   recomposait un flou gaussien plein écran de 80 px à chaque image, plus une
   resynchronisation quatre fois par seconde. C'est le calcul le plus cher
   qu'un navigateur sache faire, et il tournait en fond en permanence. Rien
   dans le CSS mobile (sections 10 à 38) ne l'avait jamais éteint : personne ne
   l'a cherché là, puisque c'était une règle « du Mariage ».

   CE QU'ON MET À LA PLACE, ET POURQUOI CE N'EST PAS UNE PERTE.
   Éteindre la lueur ne suffit PAS : la vidéo verticale est posée en
   `object-fit: contain` avec `max-width: 100%`, donc sur un écran de téléphone
   (plus étroit que 9:16) elle se retrouve bordée de bandes en haut et en bas —
   ce sont ces bandes que la lueur remplissait. On rend donc au fond sa
   géométrie normale : plein cadre, `cover`. Un clip 9:16 sur un écran 390×844
   ne perd qu'environ 18 % de sa largeur, contre 70 % de sa hauteur dans le cas
   desktop qui avait motivé tout ce dispositif. L'écran est plein, il n'y a
   plus rien à combler, et la raison d'être de l'ambilight disparaît avec les
   bandes.

   L'affiche suit exactement la même géométrie que la vidéo — elles doivent se
   superposer au pixel près, sinon on voit l'image sauter au moment où la
   vidéo prend le relais.

   Le voile (`scrim`) revient à son dégradé plein cadre : celui de la variante
   verticale était volontairement allégé pour ne pas éteindre la lueur sur les
   côtés. Sans lueur, il laissait le texte du hero sur un fond trop clair.

   DESKTOP : RIEN. Tout est sous 1024px, et rien n'est retiré au-dessus.
   ========================================================================== */
@media (max-width: 1023.98px) {

  /* La copie floutée : elle ne sert plus à rien puisqu'il n'y a plus de bande
     à remplir. `display: none` et pas `opacity: 0` — une couche transparente
     serait toujours décodée et toujours composée. */
  .hero.is-portrait .hero__ambilight { display: none; }

  /* Retour à la géométrie plein cadre — celle de .hero__video (ligne ~123) et
     de .hero__poster (ligne ~3094), reprise ici telle quelle plutôt que par
     `all: revert`, dont le comportement varie encore d'un moteur à l'autre. */
  .hero.is-portrait .hero__video,
  .hero.is-portrait .hero__poster {
    position: absolute; inset: 0; margin: 0;
    width: 100%; height: 100%;
    max-width: none;
    object-fit: cover;
    box-shadow: none;
  }

  /* Le dégradé standard, repris à l'identique de .hero__scrim. */
  .hero.is-portrait .hero__scrim {
    background: linear-gradient(180deg, rgba(5,5,6,0.35) 0%, rgba(5,5,6,0.15) 40%, rgba(5,5,6,0.55) 75%, rgba(5,5,6,0.9) 100%);
  }
}

/* ==========================================================================
   40. RENDRE LE DÉFILEMENT À LA FENÊTRE  (v=147)
   --------------------------------------------------------------------------
   CE QUE LES MESURES ONT ÉTABLI.

   CETTE SECTION NE SUFFIT PAS SEULE : elle doit rester appairée avec la
   section 42, qui fait mesurer la nouvelle hauteur aux surfaces plein écran.
   Celle-ci obtient la fenêtre, celle-là la remplit. Retirer l'une rend l'autre
   invisible — c'est arrivé une fois, et le seul indice était la COULEUR de la
   bande du bas : #030303 (fond de Safari) quand la fenêtre est trop petite,
   #050505 (fond du site) quand elle est bonne mais que le contenu n'y descend
   pas. Mesurer la couleur avant de conclure.

   Une troisième tentative, elle, a bien été retirée pour de bon : donner
   100lvh au HERO le rendait plus haut que sa fenêtre visible (l'accueil ne
   défile pas, Safari y garde sa barre dépliée), ce qui poussait le bouton
   ENTRER sous la barre d'onglets. Ne pas la réintroduire.

   Relevé pixel par pixel sur une capture de LunorStills (iPhone, 390×844) :

     · au-dessus de y = 786, le noir mesure #050505 — c'est --bg, le fond du
       site, exactement ;
     · en dessous, il mesure #030303 — une couleur qui n'existe NULLE PART
       dans cette feuille de style ;
     · à y = 0, sous l'heure et la batterie, on lit #050505 : le site peint
       donc bien jusqu'au bord physique EN HAUT ;
     · et la coupure tombe au même pixel qu'avant les sections 40 et 41.

   Deux noirs différents, c'est toute la démonstration : les 58 points du bas
   ne sont pas une bande dans la page, ils sont DEHORS. La fenêtre du site
   mesure 786 points, pas 844, et Safari peint son propre fond en dessous.
   Aucune hauteur donnée à un élément ne peut remplir ça — c'est pourquoi
   agrandir les surfaces (section 41) n'a rigoureusement rien changé.

   POURQUOI AMAZON A 844 ET PAS NOUS.
   Safari n'étend la fenêtre sur toute la hauteur de l'écran que lorsque c'est
   LE DOCUMENT qui défile. Amazon est une page ordinaire : le document défile,
   Safari lui donne l'écran entier et replie sa barre par-dessus.

   Ici, la section 29 confie le défilement à `overflow-y: auto` posé sur la
   RACINE. La barre se replie quand même — la pilule le prouve — mais la
   fenêtre, elle, n'est jamais agrandie : un débordement déclaré sur <html>
   fabrique un conteneur de défilement là où on veut la fenêtre. C'est
   exactement le piège que la section 29 documente déjà pour `overflow-x`,
   sans voir qu'il valait aussi pour `overflow-y`.

   CE QU'ON POSE : rien. On retire les trois déclarations de débordement, et
   la racine redevient `visible` — l'état de n'importe quelle page du web.
   La fenêtre reprend son rôle de surface qui défile, et Safari l'étend.

   CE QU'ON PERD, ET C'EST ASSUMÉ.
   `overflow-x: clip` empêchait un défilement latéral pendant la glissade de
   sortie du panneau catégorie. On ne peut pas le déplacer sur <body> : en CSS,
   le débordement de la racine — ou du corps quand la racine est `visible` —
   remonte à la fenêtre. Le déménager, c'est reproduire le défaut. Il faut donc
   l'enlever. Si une dérive latérale apparaît pendant cette fermeture, elle est
   bornée à la durée de l'animation et se traitera à sa source, sur le panneau.

   L'ACCUEIL N'EST PAS CONCERNÉ, ET NE PEUT PAS L'ÊTRE À CE PRIX-LÀ. Il est en
   `overflow: hidden` par construction (section 12) : c'est un plan fixe, il ne
   défile pas. Or la condition de Safari est justement qu'il y ait défilement.
   Lui donner l'écran entier suppose de le rendre défilant, donc de renoncer à
   ce que la section 12 a établi. Décision à prendre séparément, pas ici.

   DESKTOP : RIEN. Tout est sous 1024px.
   ========================================================================== */
@media (max-width: 1023.98px) {
  html.doc-scroll {
    overflow-x: visible;
    overflow-y: visible;
  }
}

/* ==========================================================================
   41. LUNORSTILLS EN FORMAT DE POCHE  (v=147)
   --------------------------------------------------------------------------
   TROIS DÉFAUTS, TROIS RÉPONSES.

   ---- 1. L'EN-TÊTE SE CHEVAUCHAIT ----
   « NOUS CONTACTER » passait par-dessus « GALERIE PHOTOGRAPHIQUE ». Ce n'est
   pas un décalage à rattraper : l'en-tête est une rangée `space-between` de
   trois éléments dont le premier (retour Accueil) est masqué depuis la
   section 36. Restent le kicker et le bouton, qui demandent ensemble plus que
   les 350 points disponibles.

   Le raisonnement de la section 36 s'applique mot pour mot : depuis que les
   cinq destinations sont en permanence en bas de l'écran, un bouton
   « NOUS CONTACTER » en haut ne fait que répéter l'onglet CONTACT, et il le
   fait sur le bord le plus cher de l'écran. On le masque dans les DEUX
   univers — sur desktop il reste, il n'y a pas de barre d'onglets là-bas.
   Masqué et non retiré du HTML : les écouteurs restent posés sans erreur.

   ---- 2. LES PHOTOS VEDETTES ÉTAIENT ILLISIBLES ----
   La rangée vedette est une rangée JUSTIFIÉE : six photos, hauteur commune,
   largeurs proportionnelles aux ratios. Sur un écran de bureau c'est une
   planche-contact, et c'est juste. Sur 390 points, six photos verticales
   donnent six bandes de 55 points de large : on ne voit plus une photo, on
   voit une mire.

   Le format juste sur téléphone n'est pas une rangée plus serrée, c'est un
   CARROUSEL. Une photo occupe la largeur utile, on fait glisser le pouce, et
   le calage magnétique (`scroll-snap`) repose chaque image au même endroit —
   c'est le geste que tout le monde connaît, et il ne coûte pas une ligne de
   JS.

   CE QU'ON CONSERVE DE L'ESPRIT D'ORIGINE : la hauteur reste COMMUNE et les
   largeurs restent proportionnelles au ratio. Les photos ne sont donc jamais
   recadrées et défilent en s'alignant haut et bas, exactement comme la rangée
   justifiée du desktop. Une horizontale serait deux fois plus large qu'une
   verticale : on plafonne à 86vw pour qu'aucune image ne dépasse l'écran.

   Le débordement latéral est VOULU : marges négatives puis rembourrage de la
   même valeur, pour que le carrousel parte du bord physique et non de la
   colonne de texte. C'est ce qui fait la différence entre une galerie et une
   liste. `scroll-padding` remet le calage à la bonne place malgré ce décalage.

   ---- 3. LA MOSAÏQUE N'AVAIT QU'UNE COLONNE ----
   Une photo par ligne, sur 101 photos, c'est un défilement interminable et une
   page qui ne raconte rien. Deux colonnes tiennent largement (2 × 158 points),
   c'est l'agencement des galeries de téléphone, et ça divise la hauteur totale
   par deux. Le nombre de colonnes est décidé en JS (`stillsColumnCount`) ; ici
   on ne fait que rendre la gouttière horizontale, qui avait été mise à zéro
   précisément parce qu'il n'y avait qu'une colonne.

   DESKTOP : RIEN. Tout est sous 1024px, et le nombre de colonnes au-dessus de
   700px n'est pas touché.
   ========================================================================== */
@media (max-width: 1023.98px) {

  /* ---- 1 ---- */
  #stillsContactBtn,
  #discoverContactBtn { display: none; }

  /* ---- 2 ---- */
  .stills__featured-row {
    height: clamp(300px, 44vh, 460px);
    gap: 10px;
    align-items: stretch;

    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    /* Le carrousel part du bord de l'écran, pas de la colonne de texte. */
    margin-left: -6vw; margin-right: -6vw;
    padding-left: 6vw; padding-right: 6vw;
    scroll-padding-left: 6vw;
    /* Sans ça, le rembourrage de droite est ignoré en fin de course sur
       plusieurs moteurs et la dernière photo colle au bord. */
    scrollbar-width: none;
  }
  .stills__featured-row::-webkit-scrollbar { display: none; }

  .still--feat {
    /* On ne partage plus la largeur : chacune prend celle que son ratio
       demande, à hauteur commune. flex-basis auto + flex-grow 0, l'inverse
       exact de la rangée justifiée du desktop. */
    flex: 0 0 auto;
    width: auto; height: 100%;
    aspect-ratio: var(--r);
    max-width: 86vw;
    scroll-snap-align: center;
  }

  /* La légende est déjà rendue permanente au doigt (section 1 du bloc
     tactile) ; sur un carrousel elle mérite d'être lisible pour de bon. */
  .still--feat .still__caption {
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    padding: 40px 14px 14px;
  }

  /* ---- 3 ---- */
  .stills__gallery { gap: 4vw; }
  .stills__col { gap: 4vw; }
}

/* ==========================================================================
   42. LES SURFACES PLEIN ÉCRAN MESURENT L'ÉCRAN, PAS LA FENÊTRE  (v=148)
   --------------------------------------------------------------------------
   CETTE SECTION AVAIT ÉTÉ ÉCRITE, PUIS RETIRÉE À TORT. VOICI POURQUOI, POUR
   QUE PERSONNE NE LA RETIRE UNE DEUXIÈME FOIS.

   Elle a d'abord été livrée SEULE. Mesure : aucun effet, la coupure au même
   pixel. J'en ai conclu qu'elle était fondée sur un diagnostic faux, et je
   l'ai supprimée en même temps que sa voisine.

   C'était une erreur de raisonnement, et la mesure suivante l'a montrée :
   après suppression, la bande du bas est revenue — mais en #050505, le fond
   du SITE, alors qu'elle était en #030303, celui de Safari. Deux couleurs
   différentes, donc deux états différents. La section 40 (le défilement rendu
   à la fenêtre) fait bien son travail : le fond du document couvre désormais
   l'écran entier. Ce qui manque, c'est que le CONTENU y descende.

   LES DEUX SECTIONS NE VALENT QUE L'UNE AVEC L'AUTRE.
     · la 40 obtient de Safari une fenêtre à la hauteur de l'écran — sans elle,
       il n'y a rien de plus à occuper ;
     · celle-ci fait mesurer cette hauteur-là aux surfaces — sans elle, elles
       restent calées sur la fenêtre d'avant et leur contenu est coupé net,
       à plat, sur toute la largeur, exactement à la hauteur relevée.
   Chacune seule ne donne rien de visible. Ensemble elles donnent le résultat.
   C'est ce qui m'a fait juger celle-ci inutile alors qu'elle est porteuse.

   `bottom: auto` est indispensable : sans lui, l'ancrage bas continuerait de
   commander et la hauteur déclarée serait ignorée.

   LE HERO N'EST PAS DANS LA LISTE, ET C'EST DÉLIBÉRÉ. Lui donner 100lvh
   l'a déjà rendu plus haut que sa fenêtre visible — l'accueil ne défile pas,
   Safari y garde sa barre dépliée — et le bouton ENTRER passait sous la barre
   d'onglets avec sa réserve. L'accueil reste sur 100dvh, la hauteur réellement
   visible. Ne pas l'ajouter ici.

   DESKTOP : RIEN. Tout est sous 1024px.
   ========================================================================== */
@media (max-width: 1023.98px) {
  .discover,
  .stills,
  .atelier,
  .panel,
  .video-modal.is-playing,
  .video-modal.is-open.is-playing,
  .video-modal.is-portrait.is-playing,
  .video-modal.is-open.is-portrait.is-playing {
    height: 100vh;     /* repli : sur les moteurs sans lvh, vaut déjà l'écran */
    height: 100lvh;
    bottom: auto;
  }

  /* Les pages en régime document sont en `position: absolute` et portent déjà
     `min-height: 100lvh` (section 29). La règle ci-dessus leur imposerait une
     hauteur FIXE d'un écran : la page serait tronquée dès que son contenu
     dépasse. On leur rend leur hauteur automatique. */
  html.doc-scroll .is-docpage {
    height: auto;
    bottom: auto;
  }

  /* Le mode cinéma de LunorFilms est un recouvrement total, volontairement
     ancré aux quatre bords (section 29). On lui rend les deux valeurs que la
     règle générale vient de lui retirer, sinon la bannière ne remplit plus rien. */
  html.doc-scroll #discoverPage.is-cinema {
    height: auto;
    bottom: 0;
  }
}

/* ==========================================================================
   43. TROIS AJUSTEMENTS  (v=150)
   ==========================================================================

   ---- 1. LA BARRE D'ONGLETS SE RESSERRE ----
   Elle courait à 12 points des bords, presque d'un côté à l'autre de l'écran :
   à cette largeur elle se lit comme un bandeau, pas comme un objet posé
   par-dessus la page. On la rentre à 34, et la version compacte à 64 — l'écart
   entre les deux états devient aussi plus lisible, la barre a visiblement
   deux tailles au lieu de rétrécir un peu.

   Le rembourrage intérieur de 6 points N'EST PAS TOUCHÉ, volontairement :
   idxFromX(), qui traduit une abscisse en numéro d'onglet pendant le balayage,
   raisonne dessus. Les largeurs de bord, elles, sont recalculées à partir du
   rectangle réel de la barre — le balayage suit tout seul.

   ---- 2. LE CERCLE DE CHARGEMENT ÉTAIT LÀ, MAIS HORS DE L'ÉCRAN ----
   `.page-load` est en `position: absolute; inset: 0` : il couvre son parent.
   Tant que les pages étaient hautes d'un écran, ce parent VALAIT l'écran, et
   l'anneau tombait donc au milieu de l'écran. Depuis le régime document, la
   page ouverte fait la hauteur de TOUT son contenu — plusieurs milliers de
   points pour la galerie. Le voile les couvre toujours, mais son centre est
   désormais à la moitié de la galerie : l'anneau tourne quelque part vers la
   cinquantième photo, là où personne ne le voit.

   `position: fixed` AURAIT ÉTÉ UN PIÈGE, et c'est le genre de détail qui coûte
   une livraison : `.discover` et `.stills` portent un `transform` quand elles
   sont ouvertes (la glissade d'entrée, translateY(14px) -> 0). Or un transform,
   même à l'identité, fait de l'élément le bloc conteneur de ses descendants
   fixes. Le voile serait redevenu relatif à la page, donc exactement aussi mal
   placé qu'avant, sans que rien ne le signale.

   On garde donc `absolute` et on cale l'anneau depuis le HAUT, à un demi-écran.
   C'est correct pour le seul instant qui compte : la page vient de s'ouvrir,
   le document a été remis à zéro, et le voile disparaît avant que quiconque
   ait eu le temps de défiler.

   Et il prend --ui-accent plutôt que --cat-color : sur LunorStills, la seconde
   valait encore la couleur de l'ambiance laissée sur l'accueil, donc un anneau
   vert ou or dans une page rose.

   ---- 3. LE BAS DE L'ÉCRAN — TENTATIVE ABANDONNÉE ----
   On avait ajouté ici un voile dégradé jusqu'au noir plein derrière la pilule
   de Safari, pour uniformiser le fond à cet endroit. Il uniformisait, mais en
   supprimant ce qu'on avait passé six versions à obtenir : le contenu qui se
   prolonge sous la pilule. Retiré en v=152 — voir le commentaire à sa place.
   ========================================================================== */
@media (max-width: 1023.98px) {

  /* ---- 1 ---- */
  .tabbar {
    left: calc(34px + env(safe-area-inset-left));
    right: calc(34px + env(safe-area-inset-right));
  }
  .tabbar.is-compact {
    left: calc(64px + env(safe-area-inset-left));
    right: calc(64px + env(safe-area-inset-right));
  }

  /* ---- 2 ---- */
  .page-load {
    align-items: flex-start;
    padding-top: calc(50vh - 13px);
    padding-top: calc(50lvh - 13px);   /* 13 = la moitié de l'anneau */
  }
  .page-load__ring { border-top-color: var(--ui-accent, var(--pink)); }
  @media (prefers-reduced-motion: reduce) {
    .page-load__ring { border-top-color: var(--ui-accent, var(--pink)); }
  }

  /* ---- 3 : LE VOILE DE BAS D'ÉCRAN EST RETIRÉ  (v=152) ----
     Il posait un dégradé jusqu'au noir plein derrière la pilule de Safari,
     dans l'idée d'uniformiser le fond à cet endroit. Il uniformisait, mais en
     supprimant justement ce qu'on avait passé six versions à obtenir : voir le
     contenu se prolonger sous la pilule. Le remède était pire que le défaut.

     Ce qui reste — un très léger éclaircissement du rectangle de la pilule —
     n'est pas dans la page : c'est le verre dépoli de Safari, qui éclaircit
     toujours ce qu'il recouvre. Aucune règle de style ne l'atteint. Le seul
     levier reste theme-color, qui remplit la barre d'un aplat opaque et masque
     le contenu ; c'est un arbitrage, pas un correctif, et il n'est pas pris
     ici. */
}

/* ==========================================================================
   44. LE FOND DU HERO ET LA FICHE VIDÉO  (v=151)
   ==========================================================================

   ---- 1. PLUS DE PREMIÈRE IMAGE À LA MAUVAISE TAILLE ----
   L'affiche s'effaçait au premier `playing`, mais la <video> était visible
   dessous depuis le début. Entre le décodage de sa première image et le vrai
   départ de la lecture, cette image se peint — et tant que les métadonnées ne
   sont pas lues, le navigateur ignore les proportions du média : `object-fit`
   n'a rien sur quoi s'appuyer et l'image sort à sa taille naturelle, au centre
   du cadre. C'est le petit rectangle aperçu juste avant que ça démarre.

   La classe est posée par revealOnPlay(), sur l'événement `playing` et pas
   avant. Les deux mouvements se croisent : l'affiche s'efface pendant que la
   vidéo se révèle, il n'y a jamais de trou.

   Si la lecture ne part jamais — autoplay refusé, fichier absent — la vidéo
   reste masquée et l'affiche reste à l'écran. C'est exactement le repli qu'on
   veut, et il est obtenu sans une ligne de plus.

   ---- 2. L'OUVERTURE DE LA FICHE ----
   Le grossissement (scale 0.96 -> 1) est une bonne animation sur un écran de
   bureau, où la fiche est une fenêtre posée au milieu d'un grand fond. Sur un
   téléphone elle occupe presque tout l'écran : mettre à l'échelle une surface
   de cette taille demande au processeur graphique de recomposer chaque pixel à
   chaque image, et le mouvement paraît lourd précisément parce qu'il est gros.

   Une montée de 18 points coûte beaucoup moins — un simple déplacement, sans
   remise à l'échelle — et se lit mieux : la fiche vient du bas, comme les
   feuilles du système. La sortie est raccourcie à 0,24 s : à l'ouverture on
   présente quelque chose, à la fermeture on veut surtout retrouver la page.

   À LIRE AVEC LE CORRECTIF DE armDocScroll() : une bonne part de la lourdeur
   ne venait pas de l'animation mais du fait que la page des Films était remise
   à zéro au même instant. On animait par-dessus un repeint complet.

   ---- 3. LE FOND SE FERME AU DOIGT ----
   L'écouteur existait déjà sur l'overlay, mais la bande touchable était de
   8vw de chaque côté et 4vh en haut et en bas — une quinzaine de points
   utilisables une fois le pouce posé. On resserre la fiche à 90vh et 88vw pour
   dégager une vraie bordure, et on écarte le cas de la lecture plein écran,
   où il n'y a plus de fond et où un doigt posé à côté de l'image ne doit
   surtout rien fermer.
   ========================================================================== */
@media (max-width: 1023.98px) {

  /* ---- 1 ---- */
  .hero__video,
  .hero__cell video {
    opacity: 0;
    transition: opacity 0.45s var(--ease);
  }
  .hero__video.est-en-vue,
  .hero__cell video.est-en-vue { opacity: 1; }

  /* ---- 2 ---- */
  .video-modal {
    transform: translate(-50%, calc(-50% + 18px));
    transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
  }
  .video-modal.is-open {
    transform: translate(-50%, -50%);
  }
  .video-modal:not(.is-open) {
    transition: opacity 0.24s var(--ease), transform 0.24s var(--ease);
  }

  /* ---- 3 ---- */
  .video-modal:not(.is-playing) {
    width: 88vw;
    height: 90vh;
  }
}

/* Le mouvement est une décoration : qui l'a désactivé sur son téléphone ne
   doit pas le retrouver ici. La fiche apparaît alors sans glissement. */
@media (prefers-reduced-motion: reduce) and (max-width: 1023.98px) {
  .hero__video, .hero__cell video { transition-duration: 0.01ms; }
  .video-modal, .video-modal:not(.is-open) { transition-duration: 0.01ms; }
  .video-modal { transform: translate(-50%, -50%); }
}

/* ==========================================================================
   45. LE FOND SE FLOUTE DERRIÈRE LA FICHE  (v=152)
   --------------------------------------------------------------------------
   Le fond de la fiche était un aplat à 94 % d'opacité : il masquait la page
   au lieu de la mettre en retrait. On voyait un rectangle noir, pas un
   arrière-plan.

   POURQUOI CE FLOU-CI EST ACCEPTABLE ALORS QU'ON EN A COUPÉ UN AUTRE.
   La section 39 a supprimé l'ambilight de l'accueil, qui floutait à 80 px une
   VIDÉO EN LECTURE, en permanence, plein écran : chaque image redemandait le
   calcul, indéfiniment. Ici le fond est une page IMMOBILE et le flou ne vit
   que le temps de la consultation d'une fiche. Ce n'est pas la même dépense.

   Trois précautions quand même :

   1. 14 px, pas 80. Le coût d'un flou croît vite avec son rayon ; à 14 px la
      page est illisible tout en restant reconnaissable, ce qui est exactement
      ce qu'on demande à un arrière-plan.

   2. LE FLOU NE S'ANIME PAS. Il est déclaré sur l'élément, pas sur son état
      visible : le navigateur le prépare une fois, et seule l'OPACITÉ du voile
      est animée. Faire varier un rayon de flou obligerait à tout recalculer à
      chaque image de la transition — c'est la faute classique, et elle coûte
      plus cher que le flou lui-même.

   3. IL S'ÉTEINT PENDANT LE FILM. Une fois la lecture lancée, la fiche occupe
      tout l'écran : le flou n'est plus vu de personne, mais il continuerait à
      être recalculé — pendant la lecture, c'est-à-dire au pire moment. La
      classe est posée par le script en même temps que `is-playing`.

   La couleur passe de 0,94 à 0,55 d'opacité : sans ça, le flou existerait sous
   un aplat qui le cache, et on paierait un calcul pour rien.

   REPLI : un navigateur sans backdrop-filter ignore ces deux lignes et garde
   l'aplat. On remonte donc l'opacité à 0,88 dans ce cas — sinon la page
   resterait lisible derrière un simple voile, et la fiche perdrait son
   détachement. `@supports not` est fait exactement pour ça.
   ========================================================================== */
@media (max-width: 1023.98px) {
  #videoModalOverlay {
    -webkit-backdrop-filter: blur(14px) brightness(0.42) saturate(0.85);
    backdrop-filter: blur(14px) brightness(0.42) saturate(0.85);
    transition: opacity 0.32s var(--ease);
  }
  #videoModalOverlay.is-visible { background: rgba(4, 4, 5, 0.55); }

  #videoModalOverlay.sous-film {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(4, 4, 5, 0.96);
  }

  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #videoModalOverlay.is-visible { background: rgba(4, 4, 5, 0.88); }
  }
}

/* ==========================================================================
   46. LES LIBELLES DU MENU  (v=161)  — DESKTOP (>= 1101px)
   --------------------------------------------------------------------------
   Sous 1100px `.nav__links` est en display:none et la navigation passe par le
   menu plein ecran : cette section ne le concerne pas.

   DEUX SIGNAUX, ET ILS NE SE MELANGENT PAS :
     SURVOL       -> le libelle GROSSIT. Rien d'autre. Pas de couleur.
     PAGE EN COURS-> le libelle est BLANC. Rien d'autre. Pas de grossissement.
   C'est ce qui les rend lisibles cote a cote : le mouvement dit « ce que la
   souris designe », la couleur dit « ou l'on est ». Deux questions, deux
   reponses, aucune propriete en commun. Si un jour on ajoute la couleur au
   survol, on perd la distinction — c'est exactement ce qui s'etait passe avec
   les versions a lueur.

   LA LUEUR EST RETIREE. Elle etait la en v158 (quatre halos concentriques a la
   couleur de la page) : l'effet etait juste, mais il repeignait les deux
   signaux avec le meme vocabulaire. On garde le plus simple des deux.

   LE GROSSISSEMENT PASSE PAR `transform`, jamais par la taille de police : une
   transformation ne touche pas a la mise en page, les entrees voisines ne
   bougent donc pas d'un pixel et la rangee ne se recalcule pas.

   ON NEUTRALISE AUSSI LA REGLE HISTORIQUE `.nav__link:hover { color: #fff }`
   (ligne ~456), qui blanchissait le survol : elle ferait dire au survol la
   meme chose qu'a la page en cours.
   ========================================================================== */
@media (min-width: 1101px) {
  .nav__links { gap: 18px; }

  .nav__link {
    padding: 8px 10px;
    border-radius: 999px;
    transform-origin: 50% 50%;
    transition: color 0.26s var(--ease), transform 0.24s var(--ease);
  }
  /* Les decors par entree des versions precedentes — bulle, anneau, trait du
     bas — n'existent plus. Rien ne se dessine derriere le texte. */
  .nav__link::before,
  .nav__link::after { content: none; }

  /* SURVOL : il grossit, et c'est tout. La couleur reste celle du repos. */
  .nav__link:hover,
  .nav__link:focus-visible {
    color: var(--fg-dim);
    text-shadow: none;
    transform: scale(1.07);
  }

  /* PAGE EN COURS : elle est blanche, et elle ne bouge pas. Ecrit APRES le
     survol pour que survoler la page ou l'on est ne la fasse pas repasser en
     gris — elle grossit alors, tout en restant blanche, ce qui est juste : les
     deux signaux sont vrais en meme temps. */
  .nav__link.is-active {
    color: #fff;
    text-shadow: none;
  }
}

/* Sans animation demandee : le seul signal qui bouge disparait, la page en
   cours reste blanche et continue de dire l'essentiel. */
@media (min-width: 1101px) and (prefers-reduced-motion: reduce) {
  .nav__link:hover, .nav__link:focus-visible { transform: none; }
}

/* ==========================================================================
   47. LE TEXTE DES AFFICHES LUNORFILMS  (v=156)
   --------------------------------------------------------------------------
   LA CAUSE EST UNE INVERSION D'EMPILEMENT, ET ELLE ETAIT LA DEPUIS LE DEBUT.
   Trois elements se superposent sur chaque affiche : l'image, le degrade
   (`.scrim`) et le texte (`.info`). Une regle groupee, ligne ~1978, les pose
   tous a `z-index: 2`. Mais la regle propre a `.info`, ligne ~1992, arrive
   APRES avec la meme specificite (0,2,0) et redescend a `z-index: 1`.
   Resultat : le degrade est peint PAR-DESSUS le titre. A l'endroit ou le texte
   se trouve, il vaut 0,7 a 0,9 de noir — un blanc pur en ressort a peu pres
   gris fonce. Le texte n'etait donc pas trop discret, il etait couvert.

L'EMPILEMENT EST CORRIGE A LA SOURCE, ligne ~1992 : le `z-index: 1` de `.info`
   est passe a 3, pour les deux versions du site (v=156). Cette section-ci ne
   s'occupe donc plus que de ce qui est PROPRE a la petite affiche — le dessin
   du degrade et la protection du texte. Ne pas y remettre de regle de
   z-index : elle redirait ce qui est deja vrai.

   LE DEGRADE EST REDESSINE POUR UNE PETITE AFFICHE. L'original passe de
   transparent a 0,9 sur les 55 % du bas : sur une carte de 195 points ca fait
   une rampe tres courte, dont la moitie basse est presque opaque. Ici la rampe
   est plus longue et plus progressive — six paliers au lieu de deux, ce qui
   evite aussi la bande visible que produit un degrade a deux points sur un
   ecran a forte densite. Le haut de l'image reste net : le degrade ne commence
   vraiment qu'a 38 %, la ou il n'y a jamais de sujet.

   ET UNE OMBRE PORTEE SUR LE TEXTE, en plus du degrade. Le degrade traite le
   cas general, l'ombre traite le cas particulier : une robe blanche ou un mur
   clair qui remonte jusqu'en bas de l'affiche. Sans elle, une photo sur dix
   reste illisible malgre le degrade.
   ========================================================================== */
@media (max-width: 1023.98px) {
  #discoverPage .discover-row__card .scrim {
    background: linear-gradient(180deg,
      rgba(0,0,0,0)    0%,
      rgba(0,0,0,0.05) 38%,
      rgba(0,0,0,0.26) 55%,
      rgba(0,0,0,0.58) 70%,
      rgba(0,0,0,0.85) 85%,
      rgba(0,0,0,0.95) 100%);
  }

  #discoverPage .discover-row__card .info { left: 10px; right: 10px; bottom: 10px; }

  #discoverPage .discover-row__card .director {
    font-size: 0.53rem;
    letter-spacing: 0.03em;
    /* var(--fg-dim) est un gris pense pour un fond noir uni ; sur une photo il
       disparait. Un blanc a 68 % garde la hierarchie avec le titre sans tomber
       sous le seuil de lisibilite. */
    color: rgba(255,255,255,0.68);
    margin-bottom: 4px;
    text-shadow: 0 1px 2px rgba(0,0,0,0.9);
  }

  #discoverPage .discover-row__card .title {
    font-size: 0.79rem;
    line-height: 1.24;
    letter-spacing: -0.005em;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.5);
  }

  /* La pastille de categorie gagne la meme protection : elle est en couleur sur
     un fond translucide, c'est ce qui souffre le plus d'une image claire. */
  #discoverPage .discover-row__card .tag {
    background: rgba(0,0,0,0.72);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
  }
}

/* ==========================================================================
   48. LES LUEURS DE L'OBTURATEUR  (v=163)
   --------------------------------------------------------------------------
   Les deux entrees du bas de l'accueil — LunorFilms a gauche, LunorStills a
   droite. Une lueur qui longe le bas de la barre et qui respire, pour qu'elles
   appellent l'oeil au lieu d'attendre qu'on les trouve.

   ELLE S'EFFACE QUAND ON ARRIVE DESSUS, et c'est la logique de l'effet : la
   lueur est un APPEL. Une fois la souris sur le bouton, l'appel a fait son
   travail — le libelle prend le relais (il blanchit, se decale et s'ecarte,
   voir .shutter__half:hover .shutter__label). Laisser la lueur pulser sous le
   curseur reviendrait a continuer d'appeler quelqu'un qui est deja la.

   LE FONDU PASSE PAR `filter: opacity()`, PAS PAR `opacity`, ET C'EST LE POINT
   TECHNIQUE DE CETTE SECTION. Le pouls est une ANIMATION sur `opacity` : une
   animation et une transition ne peuvent pas se partager la meme propriete,
   celle qui perd est simplement ecrasee. On a donc besoin d'un SECOND canal
   alpha, independant du premier — `filter: opacity()` en est un, et les deux se
   multiplient. La lueur continue de respirer pendant qu'elle s'efface, au lieu
   de sauter a sa valeur d'arrivee.
   (La v162 avait resolu ca avec une deuxieme couche empilee ; c'etait juste,
   mais ca faisait deux degrades a peindre pour un effet qui n'en demande qu'un.
   Et la v160, qui changeait d'`animation-name` au survol, sautait : changer de
   jeu d'images cles REDEMARRE l'animation a 0 %.)

   ELLE TIENT EXACTEMENT DANS LE BOUTON. La boite vaut `inset: 0` sur sa
   moitie : elle ne peut pas depasser en haut, par construction et non parce
   qu'on l'aurait rognee.

   ET AUCUNE DEMARCATION, POUR LA MEME RAISON. Le degrade est un
   `radial-gradient(100% 100% at <coin>)` : ses rayons valent exactement la
   largeur et la hauteur de la boite, donc il atteint `transparent` PILE sur les
   bords. NE PAS remonter ces deux valeurs au-dessus de 100 % : au premier
   depassement le lisere revient, et il revient au milieu de l'ecran, la ou les
   deux moities se rejoignent. Chaque moitie est vive dans son coin exterieur et
   s'eteint au centre : mises bout a bout elles eclairent tout le bas du site,
   avec une jonction invisible puisque les deux y valent zero.

   FILMS PREND LA COULEUR DE LA PAGE (`--cat-color`, posee sur <html> par
   setHeroBackground, donc elle suit l'ambiance en cours), STILLS RESTE BLANC —
   creme #e8e2d4, la meme teinte que sa moitie du trait au-dessus.

   LE POULS NE PORTE QUE SUR L'OPACITE. Pas de `transform: scale()` : agrandir
   une boite calee sur le bouton la ferait ressortir par le haut a chaque cycle.
   NE PAS animer `background`, ne pas ajouter de `filter: blur()` — un flou se
   recalcule a chaque image, une opacite non. Le degrade est peint UNE FOIS dans
   sa couche. L'obturateur est deja masque sous 1100px (section 30) : rien ne
   tourne sur telephone.
   ========================================================================== */
.shutter__half { position: relative; }
.shutter__label { position: relative; z-index: 1; }

.shutter__half::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  animation: obturateurPouls 4.6s ease-in-out infinite;
  filter: opacity(1);
  transition: filter 0.75s var(--ease);
}
/* On arrive sur le bouton : l'appel s'eteint, doucement. Et il se rallume
   aussi doucement quand on repart — meme transition, dans l'autre sens. */
.shutter__half:hover::before { filter: opacity(0); }

.shutter__half--films::before {
  background: radial-gradient(100% 100% at 0% 100%,
    color-mix(in srgb, var(--cat-color, var(--pink)) 72%, transparent) 0%,
    color-mix(in srgb, var(--cat-color, var(--pink)) 44%, transparent) 24%,
    color-mix(in srgb, var(--cat-color, var(--pink)) 22%, transparent) 48%,
    color-mix(in srgb, var(--cat-color, var(--pink)) 8%,  transparent) 74%,
    transparent 100%);
}

.shutter__half--stills::before {
  /* Le decalage d'un tiers de cycle evite que les deux coins ne battent
     ensemble — deux lueurs synchrones se lisent comme un clignotement de la
     page entiere, deux lueurs decalees comme deux appels distincts. */
  animation-delay: -1.5s;
  background: radial-gradient(100% 100% at 100% 100%,
    rgba(232,226,212,0.58) 0%,
    rgba(232,226,212,0.34) 24%,
    rgba(232,226,212,0.17) 48%,
    rgba(232,226,212,0.06) 74%,
    transparent 100%);
}

@keyframes obturateurPouls {
  0%, 100% { opacity: 0.38; }
  50%      { opacity: 1; }
}

/* Sans animation demandee : la lueur reste, elle ne respire plus. Elle
   s'efface toujours au survol, mais plus vite — c'est une reponse, pas un
   effet. */
@media (prefers-reduced-motion: reduce) {
  .shutter__half::before { animation: none; opacity: 0.8; transition-duration: 0.25s; }
}

/* ============================================================================
   49. LES PREUVES DE L'ATELIER — bandeau de fin de l'onglet 01 / LE MANIFESTE
   Structure : une pastille de residence, puis deux chiffres. Les deux chiffres
   sont des COMPTAGES faits sur content.json a chaque chargement, jamais des
   valeurs ecrites a la main — c'etait la condition posee au depart. Les
   mesures « des 48h » et « jusqu'ici 1000 » ont existe puis ont ete retirees a
   sa demande : ne pas les remettre.
   AUCUNE MEDIA QUERY : la grille est en auto-fit, elle passe de 4 a 2 colonnes
   toute seule selon la place. C'est volontaire — cette colonne est en 1.5fr
   sur ordinateur et en pleine largeur sous 1024px, deux largeurs qui ne se
   deduisent pas d'un point de rupture.
   Le pouls de la pastille n'anime QUE l'opacite, comme la section 48 : pas de
   filter, pas de background anime, rien qui se repeigne.
   ============================================================================ */
.preuves { margin-top: 56px; padding-top: 40px; border-top: 1px solid var(--line); }

.preuves__badge {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: #fff;
  padding: 9px 16px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--pink) 34%, transparent);
  background: color-mix(in srgb, var(--pink) 9%, transparent);
}
.preuves__dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--pink);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--pink) 16%, transparent);
  animation: preuvePouls 2.6s ease-in-out infinite;
}

@keyframes preuvePouls {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1; }
}

.preuves__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  gap: 26px 24px; margin-top: 36px;
}
.preuve { border-top: 1px solid var(--line); padding-top: 18px; }
.preuve__val {
  display: flex; align-items: baseline; gap: 1px;
  font-family: var(--font-body); font-weight: 800;
  font-size: clamp(1.75rem, 2.5vw, 2.4rem); line-height: 1;
  letter-spacing: -0.02em; color: #fff;
  /* La valeur est tabulaire : sans ca, chaque chiffre du comptage aurait sa
     propre largeur et le nombre tremblerait pendant qu'il monte. */
  font-variant-numeric: tabular-nums;
  margin: 0;
}
.preuve__label {
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fg-faint);
  line-height: 1.6; margin: 14px 0 0; max-width: 18ch;
}

@media (prefers-reduced-motion: reduce) {
  .preuves__dot { animation: none; opacity: 1; }
}

/* ============================================================================
   50. AVANT / APRES — la comparaison LOG / etalonne (onglet 02 / LA METHODE)
   Un seul cadre, deux videos empilees. Celle du dessous est l'etalonnee, celle
   du dessus la version LOG, DECOUPEE A DROITE par la variable --x que le
   curseur ecrit. Consequence directe du sens demande : curseur a droite = plus
   de LOG, moins d'etalonne. Si le sens doit s'inverser un jour, c'est cette
   seule ligne de clip-path qu'il faut lire, rien d'autre.
   LE CURSEUR EST L'<input type="range"> EN OPACITE 0, etale sur tout le cadre.
   Il porte le clic-a-la-position, le glisser, le tactile et les fleches du
   clavier gratuitement. La barre blanche et son bouton ne sont QUE du decor :
   ils sont en pointer-events:none, sinon ils voleraient les evenements au
   curseur qu'ils representent.
   AUCUNE MEDIA QUERY : ce bloc n'est meme pas construit sous 1024px (garde
   isMobileTier dans blocEtalonnage). La version telephone reste a faire.
   ============================================================================ */
.avap {
  --x: 50%;
  margin-top: 72px; padding-top: 48px;
  border-top: 1px solid var(--line);
  text-align: left;
}

.avap__head { display: flex; align-items: baseline; gap: 16px; margin-bottom: 22px; }
.avap__eyebrow {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--pink); margin: 0;
}
.avap__title { font-size: 0.95rem; font-weight: 600; color: var(--fg-dim); margin: 0; }

.avap__stage {
  position: relative; overflow: hidden;
  width: 100%; aspect-ratio: 16 / 9;
  border: 1px solid var(--line); border-radius: 14px;
  background: #000;
}

.avap__vid {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover;
}
/* LA LIGNE QUI PORTE TOUT LE MECANISME. inset(0 <droite> 0 0) garde la partie
   GAUCHE de la version LOG : quand --x grandit, la marge de droite retrecit et
   le LOG gagne du terrain. */
.avap__vid--log { clip-path: inset(0 calc(100% - var(--x)) 0 0); }

.avap__tag {
  position: absolute; top: 16px; z-index: 3;
  font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: #fff;
  padding: 6px 13px; border-radius: 999px;
  background: rgba(10, 10, 12, 0.62);
  border: 1px solid rgba(255, 255, 255, 0.16);
  pointer-events: none;
}
.avap__tag--log  { left: 16px; }
.avap__tag--colo { right: 16px; }

.avap__range {
  position: absolute; inset: 0; z-index: 4;
  width: 100%; height: 100%; margin: 0; padding: 0;
  -webkit-appearance: none; appearance: none;
  background: none; border: 0; opacity: 0;
  cursor: ew-resize;
}
/* Le pouce fait la hauteur du cadre : on peut donc l'attraper n'importe ou sur
   la barre, pas seulement au milieu. */
.avap__range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 46px; height: 400px; background: none; cursor: ew-resize;
}
.avap__range::-moz-range-thumb {
  width: 46px; height: 400px; border: 0; border-radius: 0;
  background: none; cursor: ew-resize;
}

.avap__line {
  position: absolute; top: 0; bottom: 0; left: var(--x); z-index: 5;
  width: 2px; margin-left: -1px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.65);
  pointer-events: none;
}
.avap__line i {
  position: absolute; top: 50%; left: 50%;
  width: 40px; height: 40px; margin: -20px 0 0 -20px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.92);
  background: rgba(10, 10, 12, 0.5);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5);
}
.avap__line i::before,
.avap__line i::after {
  content: ''; position: absolute; top: 50%;
  width: 6px; height: 6px; margin-top: -3px;
  border-top: 2px solid #fff; border-right: 2px solid #fff;
}
.avap__line i::before { left: 10px;  transform: rotate(-135deg); }
.avap__line i::after  { right: 10px; transform: rotate(45deg); }

/* Le curseur etant invisible, le focus clavier n'aurait rien montre : c'est le
   bouton de la barre qui l'indique a sa place. */
.avap__range:focus-visible ~ .avap__line i { border-color: var(--pink); }

/* ==========================================================================
   SECTION 51 — EXPERTISE & AVIS                                    (v=172)

   Les quatre terrains sont un accordeon : un seul ouvert a la fois, les trois
   autres reduits a leur ligne de titre. Tout tient donc a l'ecran et le
   defilement interne de l'ancienne colonne (max-height + overflow) a disparu —
   c'etait lui qui coupait le quatrieme terrain en plein milieu.

   LA LIGNE QUI PORTE L'ANIMATION : .terrain__wrap passe de grid-template-rows
   0fr a 1fr. C'est la seule facon d'animer une hauteur INCONNUE sans la
   mesurer en JavaScript ; un max-height chiffre aurait fixe une limite fausse
   des qu'un texte s'allonge. Sur un navigateur qui ne sait pas animer les
   unites fr, l'ouverture est instantanee — degrade propre, rien ne casse.

   --terrain est pose EN LIGNE sur chaque bloc, avec la couleur de la categorie
   lue dans content.json. Ajouter une categorie un jour lui donnera sa teinte
   sans qu'aucune regle ne soit a ecrire ici.
   ========================================================================== */

.expertise__list { display: flex; flex-direction: column; gap: 10px; }

.terrain {
  border: 1px solid var(--line);
  position: relative;
  transition: border-color 0.3s ease, background 0.3s ease;
}
.terrain::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--terrain); opacity: 0;
  transition: opacity 0.3s ease;
}
.terrain:hover { border-color: rgba(255,255,255,0.18); }
.terrain.is-open { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.015); }
.terrain.is-open::before { opacity: 1; }

.terrain__head {
  width: 100%; background: none; border: 0; cursor: pointer;
  display: grid; grid-template-columns: auto 1fr auto auto;
  align-items: center; gap: 14px;
  padding: 18px 20px; text-align: left; color: inherit; font: inherit;
}
.terrain__num {
  font-family: var(--font-mono); font-weight: 700; font-size: 0.82rem;
  color: var(--terrain);
}
.terrain__title { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }
.terrain__count {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.05em;
  color: var(--fg-faint); white-space: nowrap;
}
.terrain__chev {
  color: var(--fg-faint);
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), color 0.3s ease;
}
.terrain.is-open .terrain__chev { transform: rotate(180deg); color: var(--terrain); }

.terrain__wrap {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}
.terrain.is-open .terrain__wrap { grid-template-rows: 1fr; }
.terrain__body { overflow: hidden; }
.terrain__body > * { margin: 0 20px; }
.terrain__text {
  color: var(--fg-dim); line-height: 1.65; padding-bottom: 14px;
  max-width: 62ch;
}
.terrain__body .tag-list { padding-bottom: 4px; }

.terrain__go {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 16px; margin-bottom: 20px;
  background: none; border: 0; cursor: pointer; padding: 0;
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--terrain);
  transition: gap 0.25s ease, opacity 0.25s ease;
}
.terrain__go:hover { gap: 13px; opacity: 0.75; }

/* --------------------------------------------------------------------------
   LA COLONNE DE DROITE
   -------------------------------------------------------------------------- */

.avis-col { display: flex; flex-direction: column; }

.avis__note {
  font-family: var(--font-mono); font-size: 0.62rem; line-height: 1.6;
  letter-spacing: 0.03em; color: var(--fg-faint);
  border: 1px dashed rgba(255,255,255,0.16);
  padding: 10px 12px; margin-bottom: 16px;
}

/* Le fondu au changement de terrain. La transition est portee par l'etat de
   REPOS et non par .is-swap : la nouvelle colonne doit remonter en douceur,
   mais partir a zero doit etre instantane, sinon on verrait l'ancienne liste
   s'effacer lentement avant que la nouvelle arrive. */
.avis-flux { display: flex; flex-direction: column; gap: 14px;
  transition: opacity 0.32s ease, transform 0.32s ease; }
.avis-flux.is-swap { opacity: 0; transform: translateY(8px); transition: none; }

.avis {
  border: 1px solid var(--line); border-left: 2px solid var(--terrain);
  padding: 22px 22px 18px; position: relative;
  transition: border-color 0.3s ease, background 0.3s ease,
              transform 0.3s ease, box-shadow 0.3s ease;
}
/* ---------------------------------------------------------------------------
   LE SURVOL D'UNE CARTE D'AVIS                                       (v=209)

   Trois choses bougent ensemble : le fond se leve, la carte monte de deux
   pixels, et le filet de couleur a gauche parait s'epaissir.

   CE FILET EST UNE OMBRE, PAS UNE BORDURE. Passer `border-left` de 2 a 5px
   au survol repousserait tout le contenu de la carte de trois pixels vers la
   droite — le texte sauterait a chaque passage de souris, et sur une colonne
   de huit avis ce serait un tremblement permanent. Une `box-shadow` etalee
   vers la gauche (decalage negatif, flou nul) dessine exactement la meme
   barre pleine, en dehors de la boite : rien ne se recalcule, rien ne bouge.
   La seconde ombre, elle, est un simple aplomb porte sous la carte.
   --------------------------------------------------------------------------- */
.avis:hover {
  background: rgba(255,255,255,0.030);
  border-color: rgba(255,255,255,0.15);
  transform: translateY(-2px);
  box-shadow: -3px 0 0 0 var(--terrain), 0 12px 28px -20px rgba(0,0,0,0.95);
}
.avis__q, .avis__ph { transition: color 0.3s ease, border-color 0.3s ease; }
.avis:hover .avis__q { color: #fff; }
.avis:hover .avis__ph { border-color: var(--terrain); }
.avis__demo {
  position: absolute; top: 12px; right: 14px;
  font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--fg-faint);
  border: 1px solid rgba(255,255,255,0.16); padding: 2px 7px;
}
/* LA CITATION EST DANS LA FONTE DU SITE, PLUS EN SERIF ITALIQUE  (v=209).
   Playfair en italique donnait a ces cartes un air de page de magazine, alors
   que tout ce qui les entoure — titres de terrains, descriptions, bandeau des
   lieux — est en Inter. Meme correction que sur la fiche film (section 75) :
   dans les deux endroits, un avis se lit maintenant dans la meme fonte que le
   reste du site. */
.avis__q {
  font-family: var(--font-body); font-size: 1rem; font-weight: 400;
  line-height: 1.66; color: var(--fg); margin: 0 0 16px;
  max-width: 46ch; white-space: pre-line;
}
.avis__by { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; margin: 0; }
/* Le portrait : un visage plutot qu'un prenom seul. Il est deja carre et
   recadre a l'envoi — le CSS n'a qu'a l'arrondir. */
.avis__ph {
  width: 38px; height: 38px; border-radius: 50%; object-fit: cover;
  display: block; flex: 0 0 auto; border: 1px solid var(--line); background: #111;
}
.avis__nom { font-size: 0.86rem; font-weight: 600; color: var(--terrain); }
.avis__role,
.avis__date {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--fg-faint);
}
.avis__date::before { content: '\00b7'; margin-right: 10px; }

/* Le rattachement a un vrai film : c'est ce qui separe une preuve d'un
   compliment. La vignette est demandee en 480 px et posee en 64x40 — on ne
   telecharge jamais un original pour une image de cette taille. */
.avis__projet {
  display: flex; align-items: center; gap: 12px; width: 100%;
  margin-top: 18px; padding: 10px 12px 10px 10px;
  background: none; border: 1px solid var(--line); cursor: pointer;
  text-align: left; color: inherit; font: inherit;
  transition: border-color 0.28s ease, background 0.28s ease;
}
/* Plusieurs films sur un meme avis : les vignettes se suivent, collees, sans
   reprendre la marge du haut. Le libelle « Les films » n'est ecrit que sur la
   premiere — le repeter transformerait la pile en liste bavarde. */
.avis__projet + .avis__projet { margin-top: -1px; }
.avis__projet:hover { border-color: var(--terrain); background: rgba(255,255,255,0.02); position: relative; z-index: 1; }
.avis__projet img {
  width: 64px; height: 40px; object-fit: cover; flex-shrink: 0;
  display: block; background: #111;
}
.avis__projet-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.avis__projet-txt em {
  font-family: var(--font-mono); font-style: normal;
  font-size: 0.55rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--fg-faint);
}
.avis__projet-txt b {
  font-size: 0.82rem; font-weight: 500; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avis__projet-fl {
  color: var(--fg-faint); flex-shrink: 0;
  transition: color 0.28s ease, transform 0.28s ease;
}
.avis__projet:hover .avis__projet-fl { color: var(--terrain); transform: translate(2px, -2px); }

.avis__vide {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.04em;
  color: var(--fg-faint); border: 1px dashed var(--line); padding: 22px;
  text-align: center;
}

/* --------------------------------------------------------------------------
   LE BANDEAU DES LIEUX
   Relu dans content.json a chaque chargement, trie du plus filme au moins
   filme. Meme source que le compteur « lieux & scenes » du Manifeste : les
   deux onglets ne peuvent pas se contredire.
   -------------------------------------------------------------------------- */

.enseignes { margin-top: 28px; padding-top: 22px; border-top: 1px solid var(--line); }
.enseignes__label {
  font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--fg-faint); margin: 0 0 12px;
}
.enseignes__list { display: flex; flex-wrap: wrap; gap: 6px 0; margin: 0; }
.enseignes__list span {
  font-size: 0.8rem; color: var(--fg-dim); white-space: nowrap;
}
.enseignes__list span + span::before {
  content: '\00b7'; margin: 0 10px; color: var(--fg-faint);
}

@media (prefers-reduced-motion: reduce) {
  .terrain__wrap { transition: none; }
  .terrain__chev { transition: color 0.3s ease; }
  .avis-flux { transition: none; }
}

/* --------------------------------------------------------------------------
   52. LE FORMULAIRE DE CONTACT AU DOIGT  (v=175)  — MOBILE / TABLETTE

   Deux défauts visibles sur la capture iPhone de la page Contact, tous les
   deux propres au tactile : le champ DATE SOUHAITÉE dépassait à droite, et
   toucher n'importe quel champ faisait zoomer la page entière.

   1) LE CHAMP DATE QUI DÉPASSE
   Ce n'est pas une marge oubliée : les quatre champs partagent exactement les
   mêmes règles (`width: 100%`, `box-sizing: border-box` global). C'est la
   GRILLE qui cède. `.brief-form__row` est en `1fr`, et « 1fr » signifie en
   réalité « minmax(auto, 1fr) » : une colonne ne descend jamais sous la
   largeur MINIMALE de ce qu'elle contient. Or un `input[type="date"]` n'est
   pas une boîte de texte ordinaire — iOS lui donne une largeur intrinsèque
   calculée sur le gabarit de date complet, bien plus large qu'un écran de
   poche moins ses marges. La colonne s'élargit donc pour le loger, et le
   champ sort du cadre. Les trois autres champs, eux, ont un minimum nul :
   ils se rangent.

   Le correctif tient en `min-width: 0` sur l'étiquette, qui EST l'élément de
   grille : le minimum automatique est alors ramené à zéro et la colonne peut
   enfin rétrécir. On NE TOUCHE PAS à `grid-template-columns` — le palier
   ≤600px le repasse à une seule colonne (section 4) et cette feuille étant
   plus bas dans le fichier, y réécrire la grille aurait remis deux colonnes
   sur téléphone. `-webkit-appearance: none` complète la manœuvre : sans lui,
   certaines versions de Safari iOS dessinent le contenu du widget natif à sa
   taille intrinsèque même quand la boîte, elle, a été rétrécie.

   2) LE ZOOM À LA MISE AU POINT
   Comportement volontaire d'iOS, pas un bug de la page : Safari agrandit
   automatiquement le viewport dès qu'un champ reçoit le focus avec une
   police calculée à MOINS DE 16px. Les champs sont à 0.82rem, soit ~13px.
   La page saute donc à chaque tap, et ne revient jamais tout à fait à sa
   place ensuite.

   Le seul remède fiable est de passer les champs à 16px sous 1024px. L'autre
   piste courante — `maximum-scale=1` dans la balise viewport — est écartée
   sciemment : Safari l'ignore depuis iOS 10 pour ce cas précis, et surtout
   elle interdirait au visiteur d'agrandir la page au pincement, sur TOUT le
   site. On paie ce correctif par des champs 3px plus hauts ; le reste de la
   mise en page (étiquettes, titres, boutons) ne bouge pas.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  /* L'étiquette est l'élément de grille : c'est à elle de pouvoir rétrécir. */
  .brief-form label { min-width: 0; }

  .brief-form input,
  .brief-form textarea {
    /* 16px pile : le seuil sous lequel iOS zoome à la mise au point. */
    font-size: 16px;
    min-width: 0;
    max-width: 100%;
  }

  /* Le widget de date natif refuse de se laisser rétrécir tant qu'il garde
     son habillage système. On lui retire, la boîte reprend la main ;
     le sélecteur de date iOS s'ouvre exactement comme avant. */
  .brief-form input[type="date"] {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    /* Un champ de date VIDE n'a rien à afficher : ce plancher lui garde la
       hauteur des trois autres champs, rempli ou non. */
    min-height: 55px;
  }
}

/* --------------------------------------------------------------------------
   53. LE FORMULAIRE, SUITE : LE BOUTON REDESCEND, LA BARRE S'EFFACE  (v=176)

   1) LE BOUTON D'ENVOI REPREND SA PLACE
   La section 7 le collait en bas de l'écran (`position: sticky`) pour qu'il
   reste atteignable sur un formulaire long. L'intention était bonne, le
   résultat non : il flotte au milieu de la page, par-dessus les choix de
   type de projet et de budget, dès qu'on est en haut du formulaire. Un
   bouton d'envoi qui recouvre les champs qu'on n'a pas encore remplis
   propose d'envoyer un brief vide.

   On revient donc au comportement du PC : le bouton est le dernier élément
   du formulaire, on l'atteint en finissant de le remplir. Le `box-shadow`
   part avec lui — il n'existait que pour masquer le contenu qui défilait
   sous le bouton collant.

   La réserve de bas de page n'a pas à bouger : elle est posée sur
   `#contactPanel .panel__footer` (section 22), qui suit le bouton et la
   mention RGPD, pas sur le bouton lui-même.

   2) LA BARRE D'ONGLETS S'EFFACE QUAND LE CLAVIER MONTE
   Sur iOS la barre est `position: fixed` : elle se recale sur le bas de la
   zone visible, donc juste au-dessus du clavier — pile devant le champ qu'on
   est en train de remplir, ou devant les suggestions du clavier. Et elle n'y
   sert à rien : personne ne change d'onglet en pleine saisie.

   Le repérage se fait en JS (`setupClavierMobile`, classe `clavier-ouvert`
   sur <html>), pas en CSS : il n'existe aucun sélecteur pour « un champ de
   la page a le focus ». On ne mesure pas non plus la hauteur du clavier —
   `visualViewport` ment sur plusieurs versions d'iOS et le seul fait qu'un
   champ ait le focus suffit à répondre à la question posée.

   La liste de transition est recopiée de la section 27 avec `opacity` en
   plus : `transition` ne s'ajoute pas, elle se remplace — la déclarer ici
   sans les six autres propriétés aurait supprimé l'animation de la barre
   compacte. Si la section 27 change un jour, cette liste est à reprendre.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  #formSubmit {
    position: static;
    box-shadow: none;
  }

  .tabbar {
    transition: height 0.34s var(--ease), bottom 0.34s var(--ease),
                left 0.34s var(--ease), right 0.34s var(--ease),
                border-radius 0.34s var(--ease), background 0.34s var(--ease),
                opacity 0.18s var(--ease);
  }
  /* Pas `visibility: hidden` : la barre n'est pas dans le flux (fixed), et
     `pointer-events: none` suffit à la rendre intraversable au doigt pendant
     le fondu. */
  html.clavier-ouvert .tabbar {
    opacity: 0;
    pointer-events: none;
  }
}

/* --------------------------------------------------------------------------
   54. L'OUVERTURE DE LA FICHE VIDÉO — DEUXIÈME MÉTHODE  (v=178)
        MOBILE / TABLETTE

   La première tentative (même numéro, réécrite ici) partait du principe que le
   voile derrière la fiche existait et que la fiche s'animait. Capture à
   l'appui : ni l'un ni l'autre. Voici les deux vraies causes, trouvées dans le
   code et pas devinées.

   ── CAUSE 1 : LE VOILE N'EST PAS À L'ÉCRAN, IL EST HORS DE L'ARBRE ──
   La page LunorFilms est en RÉGIME DOCUMENT sur téléphone (section 29 : elle
   ne défile pas dans une boîte, c'est la fenêtre qui défile). Et la section 30
   contient cette ligne, écrite pour le voile du panneau CATÉGORIE :

       html.doc-scroll .panel-overlay { display: none; }

   `#videoModalOverlay` porte la classe `.panel-overlay`. Il était donc
   purement et simplement retiré de l'affichage à chaque fois qu'on ouvrait une
   fiche depuis LunorFilms — c'est-à-dire dans le seul cas qui nous intéresse.
   Tout ce que la section 45 puis la v177 avaient réglé (rayon du flou,
   luminosité, opacité du voile) s'appliquait à un élément absent. On ne
   changeait rien parce qu'il n'y avait rien à changer.

   Le motif de cette ligne reste valable pour les autres voiles : pendant
   qu'une page défile, ils sont plaqués derrière une page opaque et ne servent
   qu'à tromper l'échantillonneur de `theme-color`. On EXEMPTE donc le seul
   voile qui, lui, est fait pour être vu — et seulement quand il est
   effectivement visible, pour que rien ne soit échantillonné le reste du
   temps.

   ── CAUSE 2 : L'ANIMATION EST MANGÉE PAR LE VERROU DE LECTURE ──
   Poser `is-open` sur la fiche déclenche, dans la microtâche qui suit,
   `syncDocLock()` → `lockDoc()` : `body` passe en `position: fixed` avec un
   `top` négatif. C'est une remise en page du DOCUMENT ENTIER, et elle tombe
   exactement entre le changement de classe et la première image. Le navigateur
   n'a alors jamais peint l'état de DÉPART de la transition : il passe
   directement à l'état d'arrivée. D'où une fiche qui apparaît d'un bloc,
   quelles que soient la durée et la courbe qu'on lui écrit.

   MÉTHODE : ON SÉPARE L'ÉTAT LOGIQUE DE L'ÉTAT VISUEL.
   `is-open` ne dit plus que « la fiche est ouverte » (verrou, aria, clics).
   C'est une seconde classe, `est-montee`, qui porte l'animation, et le script
   ne la pose qu'après DEUX `requestAnimationFrame` : la première image sert à
   peindre l'état de départ une fois la remise en page finie, la seconde
   démarre la transition sur une base stable. Tant que `est-montee` n'est pas
   là, la fiche est explicitement en `transition: none` — sinon elle
   commencerait à bouger toute seule au moment du montage.

   `will-change` est posé pendant que la fiche est ouverte, et pas avant : la
   couche graphique est préparée sur l'image d'attente, donc prête quand le
   mouvement démarre, et rendue dès la fermeture.

   Le reste des économies de la première tentative est conservé, elles restent
   justes : pas d'ombre portée de 100px sur une surface plein écran, pas de
   `backdrop-filter` sur la croix de fermeture qui se déplace avec la fiche.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* ---- 1. LE VOILE REVIENT, POUR LA FICHE VIDÉO SEULEMENT ---- */
  html.doc-scroll #videoModalOverlay.is-visible { display: block; }

  #videoModalOverlay {
    -webkit-backdrop-filter: blur(12px) brightness(0.5) saturate(0.9);
    backdrop-filter: blur(12px) brightness(0.5) saturate(0.9);
  }
  /* Le voile monte avec la fiche, pas avant : même raison, même méthode. */
  #videoModalOverlay.is-visible {
    background: rgba(4, 4, 5, 0.48);
    opacity: 0;
    transition: none;
  }
  #videoModalOverlay.is-visible.est-montee {
    opacity: 1;
    transition: opacity 0.44s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* Le repli de la section 45 doit être REDIT ici : il pèse le même poids que
     la règle ci-dessus et, à poids égal, la dernière écrite gagne. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #videoModalOverlay.is-visible { background: rgba(4, 4, 5, 0.86); }
  }

  /* ---- 2. LA FICHE ---- */
  .video-modal:not(.is-playing) {
    box-shadow: none;
  }
  .video-modal.is-open:not(.is-playing) {
    will-change: opacity, transform;
  }
  /* L'image d'attente : ouverte pour le script, encore invisible et rétrécie
     pour l'œil, et surtout SANS transition — c'est l'état qui doit être peint
     tel quel. */
  .video-modal.is-open:not(.est-montee):not(.is-playing) {
    opacity: 0;
    transform: translate(-50%, calc(-50% + 14px)) scale(0.94);
    transition: none;
  }
  .video-modal.is-open.est-montee:not(.is-playing) {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
    transition: opacity 0.44s cubic-bezier(0.22, 1, 0.36, 1),
                transform 0.44s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* La fermeture reste courte : à l'ouverture on présente quelque chose, à la
     fermeture on veut surtout retrouver la page. */
  .video-modal:not(.is-open):not(.is-playing) {
    transform: translate(-50%, calc(-50% + 10px)) scale(0.97);
    transition: opacity 0.24s var(--ease), transform 0.24s var(--ease);
  }

  .video-modal__close {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(10, 10, 12, 0.78);
  }
}

/* Qui a coché « réduire les animations » ne récupère ni grossissement ni
   fondu : la fiche et son voile arrivent en place. */
@media (prefers-reduced-motion: reduce) and (max-width: 1023.98px) {
  .video-modal.is-open:not(.est-montee):not(.is-playing),
  .video-modal.is-open.est-montee:not(.is-playing) {
    opacity: 1;
    transform: translate(-50%, -50%);
    transition-duration: 0.01ms;
  }
  #videoModalOverlay.is-visible { opacity: 1; }
}

/* --------------------------------------------------------------------------
   55. LA BARRE D'ONGLETS S'EFFACE SOUS LA FICHE VIDÉO  (v=179)  — MOBILE

   Même raisonnement que pour le clavier (section 53) : la barre est en
   `position: fixed`, donc elle reste collée au bas de l'écran par-dessus le
   voile — elle flotte devant la fiche et redonne l'impression qu'on est
   toujours sur la page des films. Or une fiche ouverte est un moment plein
   écran : on la lit, ou on la ferme.

   La classe est posée sur <html> par le script, EN MÊME TEMPS que la montée
   de la fiche (`est-montee`) et pas à l'ouverture : le retrait de la barre
   fait ainsi partie du même mouvement, et il échappe comme lui à la remise en
   page du verrou de lecture. Elle est retirée dès le début de la fermeture,
   pour que la barre soit déjà revenue quand la fiche a fini de partir.

   Le fondu de la barre est plus court que celui de la fiche (0,18 s, hérité
   de la section 53) : elle doit s'être effacée AVANT que la fiche n'arrive
   dessus, pas en même temps.
   -------------------------------------------------------------------------- */
@media (max-width: 1100px) {
  html.fiche-ouverte .tabbar {
    opacity: 0;
    pointer-events: none;
  }
}

/* --------------------------------------------------------------------------
   56. LA FICHE CATÉGORIE, FINITION MOBILE  (v=180)

   Ce panneau, c'est celui qui s'ouvre sur « ENTRER · AFTERMOVIES » et sur ses
   équivalents des autres univers. Dans le code il s'appelle
   **`#categoryPanel`, « le panneau catégorie »** — c'est le nom à employer
   entre nous pour ne plus le confondre avec LunorFilms (la page de toutes les
   rangées) ni avec la fiche vidéo (`#videoModal`).

   ── CE QUI N'ALLAIT PAS, ET CE N'ÉTAIT PAS QU'UNE QUESTION DE GOÛT ──
   La rangée de films était DÉJÀ une bande horizontale sur téléphone
   (section 25), mais elle n'en montrait qu'UNE SEULE AFFICHE suivie du bouton
   PLUS. Cause exacte, corrigée dans le script : `clampGenreRows()` comptait
   les colonnes avec `gridTemplateColumns`, qui vaut `none` sur un conteneur
   passé en `display: flex` — il en déduisait une colonne, donc une case, donc
   une affiche. Le PLUS et le RÉDUIRE n'ont plus lieu d'être : une bande se lit
   d'un balayage et son débordement dit tout seul qu'il y a une suite. Ils
   disparaissent d'eux-mêmes une fois le comptage réparé (le RÉDUIRE ne naît
   que sur une rangée dépliée, le PLUS que sur un surplus).

   La MOSAÏQUE PHOTO, elle, garde sa grille et son PLUS : soixante photos ne se
   parcourent pas d'un balayage horizontal, et tout charger d'un coup serait
   payé au décodage.

   ── MOINS DE TEXTE AVANT LES FILMS ──
   L'intro se replie à quatre lignes avec un bouton pour l'ouvrir. Rien n'est
   retiré : c'est l'ORDRE de lecture qui change, les films arrivent d'abord.
   Le filet de couleur sous l'intro est masqué tant qu'elle est repliée — il
   deviendrait une des lignes de la boîte `-webkit-box` et se planterait au
   milieu du texte.

   NOM DE CLASSE : `desc--repliee`, et surtout PAS `est-repliee`, qui existe
   déjà dans ce fichier pour tout autre chose (les pages plein écran mises de
   côté, section 37).

   ── LE RESTE ──
   Croix plus grande et posée sur un fond opaque : elle passe au-dessus d'une
   affiche claire comme d'un aplat noir, et elle reste la seule sortie. Titres
   de rangée resserrés, marges entre rangées réduites : la même information
   dans moins de hauteur, encore une fois au profit des affiches.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* ---- LA SORTIE ---- */
  #categoryPanel .panel__close {
    width: 42px; height: 42px;
    background: rgba(10, 10, 12, 0.72);
    border-color: rgba(255, 255, 255, 0.18);
  }

  /* ---- L'INTRO REPLIÉE ---- */
  #categoryPanel .panel__left > p.desc.desc--repliee {
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-bottom: 10px;
  }
  #categoryPanel .panel__left > p.desc.desc--repliee::after { display: none; }

  #categoryPanel .desc-plus {
    display: inline-flex; align-items: center;
    margin: 0 0 18px;
    padding: 8px 15px;
    border: 1px solid var(--cat-color, var(--pink));
    border-radius: 999px;
    background: none;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.14em;
    color: var(--cat-color, var(--pink));
  }
  #categoryPanel .desc-plus:active { transform: scale(0.96); }

  /* ---- LES EN-TÊTES DE RANGÉE ---- */
  #categoryPanel .genre-row__head { margin-bottom: 10px; gap: 10px; }
  #categoryPanel .genre-row__title { font-size: 0.7rem; letter-spacing: 0.14em; }
  #categoryPanel .genre-row + .genre-row { margin-top: 24px; }

  /* ---- L'APPEL À PROJET ---- */
  /* Il ferme le bloc de texte, avant les films : un peu d'air au-dessus pour
     qu'il ne se lise pas comme la suite des pastilles de services. */
  #categoryPanel .cta-row { margin-top: 4px; }
}

/* --------------------------------------------------------------------------
   57. LE PANNEAU CATÉGORIE, QUATRE CORRECTIFS  (v=181)  — MOBILE

   ── 1. LA CROIX PASSAIT SOUS LA BARRE DE TITRE ──
   Elle n'était pas mal placée, elle était DESSOUS : en régime document,
   `.panel__eyebrow` devient collante avec `z-index: 5` et un fond, alors que
   `.panel__close` est restée au `z-index: 2` de sa règle d'origine. La croix
   se glissait donc derrière la barre, et on ne l'apercevait qu'au rebond
   élastique du haut de page, quand la barre se décolle. Elle passe à 6, soit
   au-dessus de la barre (5) et du sélecteur Films/Photos (4) — les trois
   seuls éléments empilés de cette page.

   ── 2. (RETIRÉ EN v=182 : la barre de titre teintée est supprimée, voir
   section 58. Ses règles ont été enlevées d'ici pour ne pas laisser de CSS
   mort derrière un `display: none`.)

   ── 3. LE DÉGRADÉ DE FOND EST RETIRÉ ──
   Le bandeau teinté posé en image de fond sur `.panel__body` (section 26)
   s'en va. La couleur de l'univers est maintenant dite par la barre de titre,
   et deux dégradés l'un sous l'autre se contrariaient.

   ── 4. LA PREMIÈRE AFFICHE D'UNE RANGÉE QUI DÉBORDE ÉTAIT DÉCALÉE ──
   Et c'est `scroll-snap` qui la décalait, pas une marge. Le rail déborde des
   marges du panneau (`margin: 0 -20px`) et les rattrape par son propre
   `padding: 0 20px`. Mais un point d'accroche `scroll-snap-align: start`
   s'aligne sur le bord de la BOÎTE DE DÉFILEMENT, pas sur son padding : le
   navigateur faisait donc défiler la rangée de 20px pour y coller la première
   affiche, qui se retrouvait 20px à gauche de tout le reste. Une rangée d'une
   seule affiche, elle, ne défile pas — d'où deux alignements différents sur
   la même page.

   `scroll-padding-left` est fait exactement pour ça : il dit à l'accrochage
   où commence vraiment le contenu. Les deux rangées repartent du même bord.

   ── 5. PLUS D'ACCROCHE AU SURVOL SUR UNE VIGNETTE ──
   `.card__tagline` (le synopsis court en couleur) est révélée par `:hover`,
   qu'iOS synthétise à chaque tap : elle s'ouvrait sur l'affiche qu'on venait
   de toucher, juste avant que la fiche ne s'ouvre par-dessus. Elle n'a de
   sens qu'avec un curseur qui survole sans cliquer. Les DEUX règles sont
   annulées, l'état de repos ET l'état survolé : la seconde est plus
   spécifique, la neutraliser seule n'aurait rien changé.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* ---- 1 ---- */
  #categoryPanel .panel__close { z-index: 6; }

  /* ---- 3 ---- */
  #categoryPanel .panel__body { background-image: none; }

  /* ---- 4 ---- */
  #categoryPanel .genre-row__grid:not(.genre-row__grid--photos) {
    scroll-padding-left: 20px;
  }
}

/* ---- 5 ---- */
@media (hover: none) {
  .card__tagline,
  .card:hover .card__tagline { display: none; }
}

/* --------------------------------------------------------------------------
   58. LE PANNEAU CATÉGORIE SANS BARRE DE TITRE  (v=182)  — MOBILE

   ── LA BARRE DE TITRE DISPARAÎT ──
   Plus de bandeau « CATÉGORIE : … » : ni teinté, ni transparent, ni rien. Il
   répétait une information déjà donnée par le titre en gros juste en dessous,
   et il coûtait 67px de hauteur en haut de chaque fiche. La croix reste, elle
   est maintenant la seule chose posée en haut de l'écran.

   Trois conséquences à traiter, sinon ça casse :
   • Le contenu remontait sous la barre d'état, la barre lui servant de
     retrait de tête. C'est le padding du corps qui reprend ce rôle, encoche
     comprise.
   • Le titre passait sous la croix, qui flotte en haut à droite. Il réserve
     désormais sa place.
   • Le sélecteur Films/Photos se collait à 67px du haut, c'est-à-dire sous une
     barre qui n'existe plus. Il remonte.

   ── LE SÉLECTEUR DEVIENT UN VRAI BANDEAU ──
   Il était déjà collant, mais son fond était un DÉGRADÉ qui s'éteignait en
   transparent (section 26) : les affiches réapparaissaient au travers de sa
   moitié basse en défilant, et il ne faisait la largeur que de son texte —
   tout ce qui passait à droite n'était masqué par rien. D'où l'impression
   qu'il flottait au lieu de tenir.

   Maintenant : piste sur toute la largeur (moins la place de la croix), fond
   OPAQUE et plat, filet net en bas, et des volets plus fins.

   LE FOND PLEIN ÉCRAN EST UN `::before`, PAS LE FOND DE L'ÉLÉMENT. Le
   sélecteur est aussi le repère du curseur qui glisse entre Films et Photos
   (`.panel__switch-thumb`, calé sur les 3px de padding de la piste). Lui
   donner le padding et les marges négatives d'un bandeau aurait déformé ce
   curseur. On sépare donc les deux rôles : l'élément reste la piste, avec sa
   géométrie d'origine ; un calque derrière lui fait le bandeau et va chercher
   les bords de l'écran par des décalages négatifs. `z-index: -1` le place
   sous la piste sans le sortir du flux — `.panel__switch` n'ouvre pas de
   contexte d'empilement, le calque reste donc au-dessus du fond de la page.

   `display: grid` remplace `inline-grid` : en bloc, la piste prend la largeur
   disponible. `align-self` ne servait plus à rien depuis que le corps est
   passé en `display: block` sur téléphone.

   Les 52px de marge à droite (plus les 20px de padding du corps) dégagent les
   72px dont la croix a besoin : elle fait 42px et s'arrête à 16px du bord.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  #categoryPanel .panel__eyebrow { display: none; }

  /* Le retrait de tête que la barre assurait, encoche comprise. */
  #categoryPanel .panel__body { padding-top: calc(14px + env(safe-area-inset-top)); }

  /* La croix flotte au-dessus du titre : on lui réserve sa place plutôt que
     de la laisser recouvrir la première ligne. */
  #categoryPanel .panel__left h2 { padding-right: 58px; }

  /* ---- LE BANDEAU FILMS / PHOTOS ---- */
  /* C'est L'ENVELOPPE qui colle et qui porte le fond, pas la piste. */
  #categoryPanel .panel__switchbar {
    display: block;
    /* .panel__right reste une colonne flex sur téléphone : sans ce stretch,
       l'enveloppe hériterait du `align-self: flex-start` de sa règle de base
       et se rétrécirait à la largeur de la piste — le bandeau ne couvrirait
       plus toute la largeur, ce qui était justement le défaut à corriger. */
    align-self: stretch;
    position: sticky;
    top: 0;
    z-index: 4;
    margin: 0 -20px 18px;
    padding: calc(10px + env(safe-area-inset-top)) 72px 9px 20px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--line);
  }
  /* La piste redevient une simple piste : sa géométrie d'origine, celle sur
     laquelle le curseur coulissant est calibré. */
  #categoryPanel .panel__switch {
    position: relative;
    z-index: auto;
    display: grid;
    margin: 0;
    padding: 3px;
    background: rgba(255, 255, 255, 0.04);
  }
  /* La section 29 posait un `top` sur le sélecteur du temps où c'était LUI qui
     collait. En `position: relative`, ce même `top` ne cale plus rien : il
     DÉCALE la piste de 67px vers le bas. On le neutralise, avec le sélecteur
     exact de la section 29 pour pouvoir l'emporter. */
  html.doc-scroll #categoryPanel.is-docpage .panel__switch { top: auto; }

  #categoryPanel .panel__switch-btn {
    padding: 10px 14px;
    font-size: 0.66rem;
  }

  /* ---- UNE FICHE COURTE S'ARRÊTE JUSTE APRÈS SA DERNIÈRE RANGÉE ---- */
  /* Sur Artistes ou Mariage il n'y a qu'une ou deux rangées. Le défaut à
     corriger n'était pas la longueur de la page mais la barre d'onglets, qui
     flotte au-dessus du bas de l'écran et mordait sur la dernière rangée : on
     ne pouvait pas la lire, et rien ne permettait de la faire remonter.
     (v=184 — j'avais d'abord donné au bloc des films la hauteur d'un écran
     entier. Ça réglait la lisibilité, mais au prix d'un écran de gris vide à
     faire défiler après la dernière affiche. La réserve suffit et ne coûte
     rien : la page s'arrête juste après la rangée, dégagée de la barre.)

     La réserve est calculée sur la barre COMPACTE, la seule visible dans ce
     panneau (section 19), plus une trentaine de pixels d'air pour que la
     rangée ne la frôle pas. */
  #categoryPanel .panel__body {
    padding-bottom: calc(var(--tabbar-space-compact) + 30px);
  }
}

/* --------------------------------------------------------------------------
   59. LE BAS DE PAGE FINIT EN GRIS  (v=183)  — MOBILE

   Sous la barre de Safari, et pendant le rebond élastique du bas, ce n'est
   plus la page qu'on voit mais la TOILE DU DOCUMENT : `html` est peint en
   `--bg` (#050506), le noir du site. Or le panneau catégorie, lui, est en
   `--bg-panel` (#0a0a0c), un gris. D'où la bande sombre en fin de fiche, qui
   n'existe pas sur LunorFilms, LunorStills ni L'Atelier — ces trois-là sont
   déjà peints en `--bg`, comme la toile.

   Aucun sélecteur CSS ne permet de dire « la toile prend la couleur de la
   page ouverte » : c'est donc le script qui pose `doc-fond-panneau` sur
   <html> quand la page en régime document est un PANNEAU (catégorie ou
   contact), et qui la retire à la fermeture. Deux valeurs possibles, un seul
   endroit où elle est décidée.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  html.doc-fond-panneau { background-color: var(--bg-panel); }
}

/* --------------------------------------------------------------------------
   60. LES DEUX COMMANDES DE LA VISIONNEUSE NE SE SUPERPOSENT PLUS  (v=185)

   La croix et le plein écran étaient EMPILÉES l'une sur l'autre en haut à
   droite. Ce n'était pas un oubli de placement mais un effet de bord de la
   section 21 : pour descendre les commandes sous l'encoche, elle a écrit un
   `top` COMMUN aux deux boutons — écrasant du même coup le `top: 74px` qui
   plaçait le plein écran sous la croix. Les deux se sont retrouvés au même
   point, à 26px du haut et 32px de la droite.

   On les remet côte à côte sur une seule ligne, plein écran à droite comme
   demandé. La croix se décale de 52px, soit la largeur d'un bouton tactile
   (42px) plus 10px d'air : les deux cibles restent séparées au doigt.

   Le `top` reste commun aux deux, et c'est maintenant volontaire : sur une
   même ligne, ils doivent partager exactement le même bord haut.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .stills__lightbox-fs {
    top: calc(20px + env(safe-area-inset-top));
    right: calc(16px + env(safe-area-inset-right));
  }
  .stills__lightbox-close {
    top: calc(20px + env(safe-area-inset-top));
    right: calc(68px + env(safe-area-inset-right));
  }
}

/* --------------------------------------------------------------------------
   61. LE PLEIN ÉCRAN SIMULÉ DE LA VISIONNEUSE  (v=186)  — MOBILE

   Safari sur iPhone n'implémente pas l'API plein écran sur un élément
   quelconque : seule une balise <video> peut y passer, par une méthode à
   part. Le bouton n'avait donc rien à appeler et ne faisait rien. Le script
   pose désormais `is-fs-simule` là où le vrai plein écran n'existe pas.

   On ne peut pas escamoter les barres de Safari — c'est de l'interface
   navigateur, dessinée hors de la vue web (voir la réponse de fond de la
   section 23). Ce qu'on peut faire, et qui était la vraie promesse du bouton,
   c'est rendre à la photo tout ce que la page lui prenait : les marges de
   sécurité (96vw / 86vh), la légende, le numéro, le fond assombri. Elle
   occupe alors la fenêtre entière.

   Les commandes restent, elles : sans elles il n'y aurait plus aucun moyen de
   revenir — et sur un plein écran simulé, la touche Échap du système ne fait
   rien puisque le système n'est au courant de rien.
   -------------------------------------------------------------------------- */
.stills__lightbox.is-fs-simule .stills__lightbox-bg { background: #050506; }
.stills__lightbox.is-fs-simule .stills__lightbox-stage img {
  max-width: 100vw;
  max-height: 100vh;
  max-height: 100lvh;
  box-shadow: none;
}
.stills__lightbox.is-fs-simule .stills__lightbox-bar,
.stills__lightbox.is-fs-simule .stills__strip { display: none; }

/* --------------------------------------------------------------------------
   62. LA VISIONNEUSE SE PINCE  (v=187)  — MOBILE

   ── LE BOUTON PLEIN ÉCRAN DISPARAÎT SUR TÉLÉPHONE ──
   Il ne pouvait pas tenir sa promesse. Une page web n'a AUCUN moyen d'ouvrir
   une photo dans le visualiseur d'iOS : ce passage de main n'existe que pour
   une balise <video>, qui a sa propre méthode — c'est pour ça que la lecture
   d'un film donne bien le lecteur Apple, et qu'une photo ne donnera jamais
   l'équivalent. Le repli simulé de la section 61 restait un demi-plein-écran
   qui ne zoomait pas. Plutôt qu'un bouton qui déçoit, on donne le geste que
   les gens font vraiment : le pincement. Le bouton reste sur PC, où le vrai
   plein écran, lui, existe.

   ── `touch-action: none` SUR LA SCÈNE ──
   C'est la ligne qui rend le zoom possible. Sans elle, iOS interprète le
   pincement à sa façon (zoom de page) et le balayage comme un défilement,
   avant même que le script ne voie l'événement. On lui retire la scène, et
   uniquement elle : tout le reste de la page garde ses gestes natifs.

   ── L'ANIMATION D'ENTRÉE ET LE ZOOM PARTAGENT `transform` ──
   D'où la règle du script : au retour à l'échelle 1, le style en ligne est
   SUPPRIMÉ et non remis à `scale(1)` — sinon il resterait devant la règle CSS
   d'apparition, pour toujours.

   ── LE TIRE-POUR-RECHARGER ──
   `overscroll-behavior` dit au navigateur de ne rien déclencher quand on tire
   au-delà du haut. C'est la ceinture ; les bretelles sont dans le script, qui
   reprend la main sur le geste dès qu'il est franchement vertical. Les deux
   sont utiles : la propriété n'est pas honorée partout, et elle ne peut rien
   contre l'élan une fois qu'il est lancé.
   -------------------------------------------------------------------------- */
html, body { overscroll-behavior-y: none; }

@media (max-width: 1023.98px) {
  .stills__lightbox-fs { display: none; }

  /* La croix reprend la place du plein écran, seule en haut à droite. */
  .stills__lightbox-close {
    right: calc(16px + env(safe-area-inset-right));
  }

  .stills__lightbox-stage {
    touch-action: none;
  }
  .stills__lightbox-stage img {
    transform-origin: center center;
    /* La photo zoomée peut déborder de son cadre : on la laisse déborder
       plutôt que de la rogner, c'est le comportement attendu d'un zoom. */
    will-change: transform;
  }
}

/* --------------------------------------------------------------------------
   63. LA CROIX A SA BANDE, LA PHOTO A LA SIENNE  (v=188)  — MOBILE

   La croix était posée sur la photo, à quelques pixels de son bord : sur une
   image claire elle devenait illisible, et rien ne disait qu'elle appartenait
   à la visionneuse plutôt qu'à la photo.

   On réserve donc une bande en haut, au-dessus de l'image, et la croix s'y
   centre. Le calcul est le même que celui de l'en-tête des panneaux
   (section 16) : bande de 74px + encoche, croix de 44px, donc 15px d'air de
   chaque côté — (74 − 44) / 2. L'encoche apparaît des deux côtés et s'annule,
   la croix reste centrée sur tous les téléphones.

   La hauteur maximale de la photo est réduite d'autant, sans quoi elle
   remonterait sous la croix : la scène est centrée dans ce qui reste, une fois
   la bande du haut et l'air du bas retirés.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .stills__lightbox-figure {
    padding-top: calc(74px + env(safe-area-inset-top));
    padding-bottom: 24px;
  }
  .stills__lightbox-stage img {
    max-width: 94vw;
    max-height: calc(100vh - 190px);
    max-height: calc(100lvh - 190px - env(safe-area-inset-top));
  }
  .stills__lightbox-close {
    top: calc(15px + env(safe-area-inset-top));
    right: calc(15px + env(safe-area-inset-right));
    width: 44px; height: 44px;
  }
  /* Plein écran simulé : plus de bande, la photo reprend tout. */
  .stills__lightbox.is-fs-simule .stills__lightbox-figure {
    padding-top: calc(64px + env(safe-area-inset-top));
    padding-bottom: 0;
  }
}

/* --------------------------------------------------------------------------
   64. CE QUE LE GESTE DE FERMETURE DEMANDE À LA FEUILLE DE STYLE  (v=189)

   Le glissement est écrit en JS (voir « GLISSER POUR FERMER — VERSION 2 »),
   mais il ne peut être fluide que si la CSS lui laisse la place :

   • `transform-origin: center top` sur les surfaces qui glissent. Par défaut
     une surface rétrécit vers son centre : le haut descend en même temps que
     le bas remonte, et l'œil lit deux mouvements contraires. Ancrée en haut,
     elle s'éloigne d'un seul tenant, comme une carte qu'on repousse.

   • Pas d'ombre portée pendant le geste. Une ombre floue sur une surface qui
     bouge est re-rastérisée à chaque image ; c'est le poste qui fait décrocher
     un glissement autrement composité. La fiche vidéo n'en a déjà plus sur
     téléphone (section 54), la visionneuse en gardait une de 120px.

   • La photo de la visionneuse ne doit pas se battre avec le geste : son
     animation d'entrée passe elle aussi par `transform`, mais elle vit sur
     l'image, et c'est la FIGURE qui glisse. Deux éléments, deux rôles, aucun
     conflit — c'est pour ça que le script déplace la figure et pas l'image.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .video-modal,
  #categoryPanel,
  .stills__lightbox-figure {
    transform-origin: center top;
  }
  .stills__lightbox-stage img { box-shadow: none; }
}

/* --------------------------------------------------------------------------
   65. LA VISIONNEUSE S'OUVRE SANS À-COUPS  (v=190)  — MOBILE

   ── LE FOND QUI S'ASSOMBRISSAIT PAR PALIERS ──
   Le fondu portait sur `.stills__lightbox` TOUT ENTIER. Or ce conteneur abrite
   un `backdrop-filter` : faire varier l'opacité d'un parent oblige le moteur à
   composer tout son contenu dans un calque à part, et à refiltrer l'arrière-
   plan à chaque valeur intermédiaire. Sur un téléphone, il n'y arrive pas à
   soixante images par seconde, alors il en saute — d'où l'assombrissement par
   marches d'escalier.

   On ne fait plus varier l'opacité du parent : il reste à 1, et ce sont ses
   DEUX enfants qui se fondent séparément, le voile d'un côté, la photo de
   l'autre. Chacun est un calque simple, le filtre n'est calculé qu'une fois et
   c'est sa composition qui varie. Le rayon passe de 20 à 14px au passage : au
   travers d'un fond déjà assombri à 55%, l'œil ne fait pas la différence, mais
   le GPU si.

   ── LE BAS DE PAGE QUI RÉAPPARAISSAIT D'UN COUP ──
   La visionneuse figeait le document (`body` en `position: fixed`) pour que la
   page ne défile pas dessous. Mais un `body` fixe perd sa hauteur : le
   document n'est plus défilable, Safari redéploie sa barre du bas, et au
   déverrouillage la hauteur revient d'un bloc.

   `touch-action: none` fait le même travail sans rien déplacer. La visionneuse
   couvre tout l'écran en `position: fixed` : tout geste tombe sur elle, et
   avec cette règle le navigateur n'en fait rien de lui-même. La mise en page
   du document n'est jamais touchée, donc il n'y a rien à restaurer, donc rien
   ne réapparaît. C'est aussi ce qui rend le pincer-pour-zoomer possible
   (section 62) : la même règle sert les deux.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .stills__lightbox {
    opacity: 1;
    transition: none;
    touch-action: none;
    overscroll-behavior: contain;
  }

  .stills__lightbox-bg {
    opacity: 0;
    transition: opacity 0.34s cubic-bezier(0.32, 0.72, 0, 1);
    -webkit-backdrop-filter: blur(14px) saturate(0.9) brightness(0.55);
    backdrop-filter: blur(14px) saturate(0.9) brightness(0.55);
  }
  .stills__lightbox.is-open .stills__lightbox-bg { opacity: 1; }

  .stills__lightbox-figure {
    opacity: 0;
    transition: opacity 0.28s cubic-bezier(0.32, 0.72, 0, 1);
  }
  .stills__lightbox.is-open .stills__lightbox-figure { opacity: 1; }

  /* La croix et le bandeau du bas suivent la photo : sans ça, le parent étant
     désormais opaque en permanence, ils resteraient affichés pendant que tout
     le reste s'efface. */
  .stills__lightbox .stills__hud,
  .stills__lightbox .stills__lightbox-close {
    transition: opacity 0.24s cubic-bezier(0.32, 0.72, 0, 1);
  }
  .stills__lightbox:not(.is-open) .stills__hud,
  .stills__lightbox:not(.is-open) .stills__lightbox-close { opacity: 0; }
}

/* --------------------------------------------------------------------------
   66. L'ATELIER NE FAIT PLUS TRAVAILLER LA MISE EN PAGE  (v=191)  — MOBILE

   ── L'ACCORDÉON DES TERRAINS (onglet 02 / LA MÉTHODE) ──
   `.terrain__wrap` s'ouvre en animant `grid-template-rows` de `0fr` à `1fr`
   (règle d'origine, plus haut dans ce fichier). C'est une propriété de MISE EN
   PAGE : à chaque image de la transition, le navigateur recalcule la grille et
   tout son sous-arbre — le texte, les étiquettes, le bouton. Sur un téléphone
   c'est le genre de dépense qui se voit immédiatement, et le compositeur ne
   peut rien en absorber.

   POURQUOI ON COUPE PLUTÔT QUE DE RÉÉCRIRE L'ANIMATION.
   L'alternative honnête serait d'animer une `transform` sur un conteneur de
   hauteur fixe — mais la hauteur d'un terrain dépend de son texte, elle n'est
   connue qu'après mesure, et mesurer à l'ouverture nous ramènerait au même
   coût par un autre chemin. `max-height` avec une valeur haute arbitraire
   donnerait une courbe fausse (l'accordéon paraîtrait accélérer à la fin).
   Sur un écran de poche, un accordéon qui s'ouvre NET est une réponse
   parfaitement lisible : on garde l'information, on retire la dépense.
   Le chevron, lui, continue de tourner — c'est une `transform`, elle ne coûte
   rien, et c'est elle qui dit que l'état a changé.

   ── LE BOUTON « VOIR LES FILMS » ──
   `.terrain__go:hover` fait passer son `gap` de 8 à 13px. iOS SYNTHÉTISE
   `:hover` sur l'élément touché à chaque tap : au doigt, l'écart s'ouvre donc
   juste avant que la page ne s'ouvre — un déplacement parasite, et lui aussi
   sur une propriété de mise en page.
   Il manquait à la liste de la section 1, où ses semblables (cartes, photos,
   vignettes) sont neutralisés depuis longtemps. On l'y rattache ici.
   Les DEUX règles sont annulées, l'état de repos et l'état survolé : la
   seconde est plus spécifique, neutraliser la première seule ne changerait
   rien.

   La liste de `transition` du bouton est RECOPIÉE de sa règle d'origine
   `.terrain__go` (SECTION 51 — EXPERTISE & AVIS, hors media query) moins
   `gap` : une transition ne s'ajoute pas, elle se remplace. Si cette règle
   d'origine change un jour, cette copie est à reprendre.

   NOTE POUR PLUS TARD : l'en-tête de la section 51 justifie explicitement le
   choix de `grid-template-rows` (« la seule façon d'animer une hauteur
   INCONNUE sans la mesurer en JavaScript »). Ce raisonnement reste juste — on
   ne le contredit pas, on constate seulement que sur téléphone le prix de
   cette animation dépasse ce qu'elle apporte. Le desktop la garde.

   RÈGLES D'ORIGINE NON TOUCHÉES : elles vivent hors media query et servent
   aussi le desktop, qui reste validé tel quel.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .terrain__wrap { transition: none; }
}

@media (hover: none) {
  .terrain__go       { transition: opacity 0.25s ease; }
  .terrain__go:hover { gap: 8px; opacity: 1; }
}

/* --------------------------------------------------------------------------
   67. LE BALAYAGE D'AMBIANCE VAUT AUSSI POUR LA TABLETTE  (v=191)

   La section 34 rend le balayage vertical du hero au script — sans quoi Safari
   arbitre le geste, anime sa barre d'outils, et ne nous rend l'événement
   qu'ensuite : « il répond quand il veut ». Elle est bornée à 1023.98px.

   Or le hero est en régime mobile dès 1100px : c'est la section 12 qui le met
   en plan fixe, sans défilement, et c'est la même borne qui fait apparaître le
   rail d'ambiances (section 13) et la barre d'onglets (section 10). Côté
   script, le balayage s'arme lui aussi jusqu'à 1100 (`onPhone()`). Entre 1024
   et 1100px on avait donc le geste, le rail qui l'annonce, et rien pour le
   déclarer au navigateur.

   POURQUOI CETTE RÈGLE PEUT MONTER AU-DESSUS DE 1024 SANS RIEN CHANGER AU
   RENDU : `touch-action` ne peint rien. Elle ne déclare qu'à qui appartient un
   geste. Aucune couleur, aucune taille, aucune position n'en dépend — le
   desktop garde très exactement l'image qu'il a aujourd'hui.

   Écrit en bande fermée (min-width ET max-width) plutôt qu'en remontant la
   borne de la section 34 : celle-ci reste lisible telle qu'elle a été écrite,
   et on voit ici, noir sur blanc, quel palier exact est concerné.
   -------------------------------------------------------------------------- */
@media (min-width: 1024px) and (max-width: 1100px) {
  .hero { touch-action: pan-x pinch-zoom; }
}

/* --------------------------------------------------------------------------
   68. LE MANIFESTE AU DOIGT  (v=192)  — MOBILE (< 1024px)

   CE QUI N'ALLAIT PAS, ET C'EST TOUJOURS LA MEME CAUSE : le desktop fait son
   luxe avec de l'ESPACE (deux colonnes, gouttiere de 100px, respirations de
   48 a 64px). Sur un ecran de poche, la meme feuille de style ne donne pas du
   luxe, elle donne du VIDE — un ruban de texte gris de 2000px de haut, sans
   un seul point d'arret. Sur mobile, le premium se fait avec de la MATIERE
   (cadres, filets, contrastes de taille), pas avec du blanc.

   LES CINQ DEFAUTS RELEVES, tous verifies dans le fichier :
   1. `gap: 100px` (regle d'origine) n'est surchargee nulle part sous 1100px :
      en UNE colonne, cette gouttiere devient un trou vertical de 100px entre
      le texte et la carte d'intention.
   2. `.day-night p { max-width: 32ch }` : sur 350px de large, le texte est
      contraint a un ruban maigre a gauche avec du vide a droite. Le plafond
      de 32 caracteres est une regle de CONFORT DE LECTURE ecrite pour une
      demi-colonne de bureau ; en pleine largeur de telephone elle produit
      exactement l'inverse de ce pour quoi elle existe.
   3. `.manifeste h3 { max-width: 16ch }` casse le titre en lignes tres
      courtes alors que la place est la.
   4. `--fg-faint` (#55555c) sur le noir de la page donne un contraste de
      ~2,6:1. C'est en dessous du minimum lisible, et c'est justement la
      couleur des libelles les plus petits (`.preuve__label` a 0.6rem, les
      titres du diptyque). Ils passent en `--fg-dim` (#9a9a9f, ~6,5:1).
   5. La CARTE D'INTENTION — celle qui porte le seul bouton d'action de la
      page, « Parler de votre projet » — tombe TOUT EN BAS, apres la note de
      pied qui, elle, ressemble a une fin. Sur ordinateur elle est a droite,
      visible des l'arrivee. Un client sur telephone doit tout lire avant de
      trouver comment nous joindre.

   COMMENT LA CARTE REMONTE SANS TOUCHER AU GABARIT JS.
   Le HTML fait deux blocs : le texte, puis la carte. `display: contents` sur
   le premier bloc fait de ses enfants des elements de la grille `.manifeste`
   directement — on peut alors les ordonner avec `order`. Le DOM n'est pas
   modifie, donc l'ordre de lecture au clavier et au lecteur d'ecran ne bouge
   pas ; SEUL L'ORDRE VISUEL change, et d'un cran (la carte passe devant la
   note de pied). Ces deux blocs sont independants, l'echange se lit
   correctement dans les deux sens — c'est la limite que je me suis fixee :
   remonter la carte tout en haut aurait ete plus vendeur et aurait vraiment
   casse l'ordre de lecture.
   `display: contents` OBLIGE a passer `gap` a 0 : la gouttiere s'appliquerait
   desormais entre CHAQUE enfant et s'ajouterait a leurs marges. Tout
   l'espacement est donc repris ici, bloc par bloc.

   POURQUOI JE N'INVENTE PAS DE GESTE INTERACTIF SUR LE DIPTYQUE JOUR/NUIT.
   Un accordeon ou un basculement y cacherait du texte derriere une
   manipulation, pour deux paragraphes de trois lignes — on paierait de
   l'accessibilite pour du decor. Le diptyque devient donc deux CARTES (fond,
   cadre, filet de couleur a gauche : blanc pour le jour, rose pour la nuit) :
   le contraste dont parle le manifeste se voit enfin au lieu de se lire.
   L'interaction reelle de la page est ailleurs, et elle est honnete : la
   revelation au defilement et le comptage des chiffres au moment ou ils
   entrent a l'ecran (voir la fonction `armerRevelationManifeste`).

   BORNE : 1023.98px, la meme que `isMobileTier()` cote script — les deux
   moities de cette livraison doivent s'allumer ensemble. Le desktop et la
   bande tablette 1024-1100px gardent trait pour trait ce qu'ils ont
   aujourd'hui.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* -- 1. la colonne unique ne garde pas la gouttiere du bureau -- */
  .manifeste { gap: 0; }
  .manifeste > div:first-child { display: contents; }
  .manifeste-footer-note { order: 3; }

  /* -- 2. l'en-tete pese plus et respire moins -- */
  .eyebrow-mark {
    margin-bottom: 18px;
    font-size: 0.62rem; letter-spacing: 0.2em;
    color: var(--fg-dim);
  }
  .eyebrow-mark::before { width: 18px; }

  .manifeste h3 {
    max-width: none;
    font-size: clamp(1.7rem, 7.4vw, 2.3rem);
    line-height: 1.16;
    margin-bottom: 22px;
  }

  /* -- 3. le diptyque devient deux cartes -- */
  .day-night {
    gap: 12px;
    margin: 28px 0 30px;
    padding-top: 26px;
  }
  .day-night > div {
    padding: 18px 16px 20px;
    border: 1px solid var(--line);
    border-left-width: 2px;
    border-radius: 4px;
    background: rgba(255,255,255,0.022);
  }
  .day-night > div:first-child { border-left-color: rgba(255,255,255,0.4); }
  .day-night > div:last-child  { border-left-color: var(--pink); }
  .day-night h5 { margin-bottom: 9px; font-size: 0.62rem; color: var(--fg-dim); }
  .day-night > div:last-child h5 { color: var(--pink); }
  .day-night p { max-width: none; font-size: 0.9rem; line-height: 1.7; }

  /* -- 4. le recit : un cran plus grand, c'est le texte qu'on vient lire -- */
  .manifeste > div > p:not(.manifeste-footer-note) {
    font-size: 0.98rem; line-height: 1.78;
    margin-bottom: 0;
  }

  /* -- 5. les preuves : deux chiffres cote a cote, toujours -- */
  /* `auto-fit` avec un minimum de 155px tombe a UNE colonne sur un ecran de
     320px : les deux chiffres s'empileraient et ne se compareraient plus d'un
     coup d'oeil. On fixe deux colonnes. */
  .preuves { margin-top: 32px; padding-top: 26px; }
  .preuves__badge { font-size: 0.6rem; padding: 8px 14px; }
  .preuves__grid { grid-template-columns: 1fr 1fr; gap: 22px 16px; margin-top: 26px; }
  .preuve__val { font-size: 2rem; }
  .preuve__label { max-width: none; font-size: 0.63rem; color: var(--fg-dim); }

  /* -- 6. la carte d'intention remonte, et son bouton devient une vraie cible -- */
  .manifeste .side-card {
    order: 2;
    margin-top: 34px;
    border-radius: 5px;
  }
  /* 4/3 en pleine largeur mange 260px de haut avant le premier mot. */
  .manifeste .side-card__media { aspect-ratio: 16/9; }
  .manifeste .side-card__body { padding: 22px 18px 20px; }
  .manifeste .side-card__body h4 { font-size: 1.05rem; line-height: 1.35; }
  .manifeste .side-card__body p { font-size: 0.88rem; line-height: 1.7; }
  .manifeste .side-card .cta-row {
    margin-top: 20px;
    padding: 18px 0 4px;
    min-height: 50px;          /* cible tactile confortable */
    font-size: 0.7rem; letter-spacing: 0.12em;
  }

  /* -- 7. la note de pied ferme, elle ne conclut plus -- */
  .manifeste-footer-note {
    margin-top: 30px; padding-top: 20px;
    font-size: 0.68rem; color: var(--fg-dim);
  }

  /* -- 8. la revelation au defilement --
     `opacity` et `transform` seulement : deux proprietes que le compositeur
     traite sans jamais rappeler la mise en page. C'est la difference avec
     l'accordeon de la section 66, qui animait `grid-template-rows` et qu'on a
     du couper sur telephone.
     L'etat de depart est pose PAR LE SCRIPT (classe `a-reveler`) et jamais par
     defaut : si le script tombe ou si IntersectionObserver manque, la classe
     n'est pas posee et la page reste entierement lisible. Un contenu qui
     n'existe qu'apres l'execution d'un observateur est un contenu qu'on peut
     perdre. */
  .a-reveler {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
  }
  .a-reveler.est-revele { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .a-reveler { opacity: 1; transform: none; transition: none; }
}

/* --------------------------------------------------------------------------
   69. LA METHODE AU DOIGT  (v=193)  — MOBILE (< 1024px)

   ── CE QUI N'ALLAIT PAS ──
   1. UN VRAI BUG DE PALIER, et c'est le plus grave : `.steps` n'est ramene a
      une colonne que dans le palier <= 600px. Entre 600 et 1024px, les cinq
      etapes restent en `repeat(5, 1fr)` — cinq colonnes de ~110px de large,
      c'est-a-dire cinq rubans de deux mots par ligne. Toute cette section
      ramene le basculement a 1024px, la borne de `isMobileTier()`.
   2. `.methode { text-align: center }` : un texte centre se lit mal des qu'il
      depasse deux lignes, l'oeil ne retrouve pas le debut de la suivante. Sur
      une colonne etroite, ca coute a chaque paragraphe.
   3. Cinq cartes empilees, meme cadre, meme fond, meme taille : la page dit
      « voici 5 blocs », alors que le contenu dit « voici un PARCOURS, de la
      premiere discussion au fichier final ». La forme contredisait le fond.
   4. Le relief au survol (les autres cartes reculent) est deja borne a
      `hover: hover`, donc absent au doigt — sur telephone il ne restait plus
      aucune hierarchie du tout.

   ── L'AGENCEMENT RETENU : LE FIL ──
   Les cinq etapes deviennent une frise verticale. Un filet relie les numeros,
   chaque numero est une pastille cerclee A LA COULEUR DE SON ETAPE — et cette
   couleur est obtenue par `currentColor` : le gabarit JS pose deja
   `style="color:<couleur>"` sur le numero, la pastille n'a donc rien a
   recevoir de neuf. Une sixieme etape ajoutee un jour prendrait sa teinte
   toute seule.
   La pastille a un fond OPAQUE (`--bg-panel`) : c'est lui qui interrompt le
   filet a chaque noeud. Sans ce fond, le trait traverserait les chiffres.
   ALTERNATIVES ECARTEES : le rail horizontal a defilement (il cache quatre
   etapes sur cinq, et un parcours dont on ne voit pas la fin n'est plus un
   parcours) et l'accordeon (la section 66 vient precisement d'en retirer un
   de cet onglet pour son cout, et il aurait cache six lignes de texte derriere
   une manipulation).

   ── LE COMPARATEUR AVANT / APRES PASSE EN VERTICAL ──
   Le cadre passe de 16/9 a 9/16 et sert des paires PROPRES au telephone
   (`etalonnageMobile`, voir `paireEtalonnage` cote script).
   LA HAUTEUR EST BORNEE PAR L'ECRAN, PAS PAR LA LARGEUR : un 9/16 en pleine
   largeur ferait 620px de haut sur un telephone courant, soit plus que la
   zone visible — on ne verrait jamais les deux extremites du curseur en meme
   temps. D'ou `height: min(58vh, 168vw)` avec `aspect-ratio`, ce qui exige un
   parent en flex : c'est la seule facon d'obtenir `width: auto` = largeur
   DEDUITE du ratio. En bloc, `width: auto` vaut « toute la place
   disponible » et le ratio est alors ignore pour la largeur.

   ── LE DECOUPAGE PASSE A L'HORIZONTALE  (v=194) ──
   La barre de separation est desormais COUCHEE : le LOG occupe le HAUT du
   cadre, l'etalonne le BAS, et on fait glisser de haut en bas. Demande
   explicite, et le vertical se defend : dans un cadre 9/16, une barre debout
   compare deux rubans de 130px de large, une barre couchee compare deux
   images entieres.

   COMMENT, SANS TOUCHER A UNE SEULE LIGNE DE SCRIPT : `poser()` continue
   d'ecrire `--x: <valeur>%` et ne sait rien de tout ceci. C'est le CURSEUR
   QUI EST TOURNE — `rotate(-90deg)` — et une rotation de -90deg envoie le
   BORD GAUCHE de l'element (valeur 0) VERS LE BAS. Donc, sur telephone
   seulement, `--x` se lit « depuis le bas » : la barre est a
   `top: calc(100% - var(--x))` et le LOG garde le haut avec
   `inset(0 0 var(--x) 0)`. Les deux vont ensemble : toucher a l'un sans
   l'autre desolidarise la barre de l'image.
   Le curseur tourne doit etre dimensionne A LA MAIN (une rotation ne change
   pas la boite qui sert au calcul de la valeur) : sa LARGEUR vaut la HAUTEUR
   du cadre. D'ou `--avap-h`, declaree une fois et utilisee par les deux.

   `touch-action: none` REMPLACE `pan-y`, ET C'EST LE PRIX DU GESTE : le
   glisser est maintenant vertical, exactement comme le defilement de la page.
   Laisser `pan-y` reviendrait a donner tous les glissers verticaux a la page
   et le curseur ne repondrait plus au doigt. Le cadre devient donc une zone
   ou la page ne defile pas ; elle defile toujours de part et d'autre (le
   cadre ne fait que 9/16 de sa hauteur, il reste une marge a gauche et a
   droite) et partout ailleurs.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* -- 1. l'en-tete de l'onglet -- */
  .methode { text-align: left; }
  .methode .eyebrow { font-size: 0.62rem; letter-spacing: 0.18em; margin-bottom: 10px; }
  .methode h3 { font-size: clamp(1.6rem, 6.8vw, 2.1rem); line-height: 1.18; margin-bottom: 14px; }
  .methode > p {
    margin: 0 0 34px; max-width: none;
    font-size: 0.95rem; line-height: 1.72;
  }

  /* -- 2. les cinq etapes deviennent un fil -- */
  .steps {
    grid-template-columns: 1fr;
    gap: 0;
    text-align: left;
    padding-left: 15px;   /* la moitie de la pastille, qui deborde a gauche */
  }
  .steps > div {
    padding: 0 0 30px 26px;
    border: 0;
    border-left: 1px solid var(--line);
    border-radius: 0;
    background: none;
    box-shadow: none;
    transform: none;
  }
  /* Le fil s'arrete au dernier noeud : il relie les etapes, il ne pend pas
     dans le vide sous la derniere. */
  .steps > div:last-child { border-left-color: transparent; padding-bottom: 0; }
  /* Le filet de couleur du haut appartient au relief de bureau : ici il
     couperait le fil en travers a chaque etape. */
  .steps > div::after { display: none; }

  .steps .num {
    position: absolute; left: -15px; top: -3px;
    width: 30px; height: 30px; margin: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.66rem; font-weight: 700; letter-spacing: 0.02em;
    border-radius: 50%;
    border: 1px solid currentColor;   /* la couleur de l'etape, deja posee en ligne */
    background: var(--bg-panel);      /* opaque : c'est lui qui coupe le fil */
  }
  .steps h5 { margin: 3px 0 8px; font-size: 1rem; }
  .steps p { font-size: 0.88rem; line-height: 1.68; }

  /* -- 3. le comparateur, en vertical -- */
  .avap {
    /* AGRANDI (v=201) : 58vh -> 70vh. Le second terme n'est qu'un garde-fou —
       il empeche la largeur deduite (hauteur x 9/16) de depasser l'ecran. */
    --avap-h: min(70vh, 176vw);   /* la hauteur du cadre, relue par le curseur */
    margin-top: 40px; padding-top: 30px;
    display: flex; flex-direction: column; align-items: center;
  }
  .avap__head {
    align-self: stretch;
    flex-direction: column; align-items: flex-start; gap: 4px;
    margin-bottom: 16px;
  }
  .avap__stage {
    aspect-ratio: 9 / 16;
    height: var(--avap-h);
    width: auto;
    max-width: 100%;
    border-radius: 12px;
  }

  /* LE LOG GARDE LE HAUT. `inset(0 0 var(--x) 0)` : la marge du BAS grandit
     avec --x, donc plus on monte la barre, plus l'etalonne se decouvre. */
  .avap__vid--log { clip-path: inset(0 0 var(--x) 0); }

  /* Le LOG est en haut, l'etalonne en bas : les deux etiquettes suivent. */
  .avap__tag { top: 12px; font-size: 0.52rem; padding: 5px 11px; }
  .avap__tag--log  { left: 12px; }
  .avap__tag--colo { top: auto; bottom: 12px; right: 12px; }

  /* LE CURSEUR EST COUCHE SUR LE FLANC. Sa largeur = la hauteur du cadre :
     apres rotation, sa course couvre le cadre du haut en bas au pixel pres.
     `inset: auto` d'abord, sinon les quatre cotes de la regle de base
     continueraient de l'etirer. */
  .avap__range {
    inset: auto; top: 50%; left: 50%;
    width: var(--avap-h);
    height: calc(var(--avap-h) * 9 / 16);
    transform: translate(-50%, -50%) rotate(-90deg);
    transform-origin: 50% 50%;
    cursor: ns-resize;
    touch-action: none;   /* voir l'en-tete : le glisser vertical lui revient */
  }
  .avap__range::-webkit-slider-thumb { cursor: ns-resize; }
  .avap__range::-moz-range-thumb     { cursor: ns-resize; }

  /* La barre se couche. Elle se lit DEPUIS LE BAS, comme le curseur tourne. */
  .avap__line {
    top: calc(100% - var(--x)); bottom: auto; left: 0; right: 0;
    width: auto; height: 2px;
    margin: -1px 0 0 0;
  }
  .avap__line i { width: 44px; height: 44px; margin: -22px 0 0 -22px; }
  /* Les deux chevrons pivotent avec la barre : ils pointent haut et bas. */
  .avap__line i::before,
  .avap__line i::after {
    top: auto; bottom: auto; left: 50%; right: auto;
    margin: 0 0 0 -3px;
  }
  .avap__line i::before { top: 12px;    transform: rotate(-45deg); }
  .avap__line i::after  { bottom: 12px; transform: rotate(135deg); }
}

/* ==========================================================================
   SECTION 70 — LES REALISATEURS AU DOIGT                            (v=195)

   CE QUI N'ALLAIT PAS, ET CE N'ETAIT PAS UNE QUESTION DE GOUT
   --------------------------------------------------------------------------
   Cet onglet etait le SEUL du site a interdire le defilement : panneau en
   `height: 100%`, grille en `height: 100%`, corps en `overflow: hidden`. Sur
   un ecran large c'etait le bon calcul — deux cartes cote a cote, plein
   cadre, rien qui defile. En une colonne de telephone, la meme regle oblige
   DEUX portraits a se partager la hauteur visible : environ 310px chacun,
   dont 195 pris par le role, le nom et le texte. Il restait ~120px de haut
   pour une photo verticale affichee en `cover` sur 350px de large, soit un
   bandeau au ratio 3:1 decoupe dans un portrait. Les `photoPosition` de
   content.json sont regles pour une carte haute ; dans cette fente ils ne
   cadrent plus une tete. Et ce qui depassait etait perdu, faute de
   defilement.
   AU PASSAGE, UN VRAI DEFAUT : la reserve de place de la barre du bas
   (`.atelier__body { padding-bottom: calc(32px + var(--tabbar-space)) }`,
   section 19) etait annulee sur cet onglet par son propre `padding` en
   raccourci, PLUS SPECIFIQUE. Le bas de la deuxieme carte passait donc sous
   la barre flottante, sans aucun defilement pour aller le rechercher.

   L'AGENCEMENT RETENU : LE DIPTYQUE A FOND PERDU
   --------------------------------------------------------------------------
   Chaque realisateur occupe un bloc pleine largeur, la photo touche les deux
   bords, le nom est pose EN GRAND dans le degrade du bas de l'image, le texte
   suit sur le noir. LE CADRE ET LE FOND DE LA CARTE SONT SUPPRIMES : ce filet
   servait a separer deux colonnes ; en une seule colonne il n'y a plus rien a
   separer, et un cadre autour d'un portrait de 350px de large ne fait pas
   luxe, il fait vignette. Une photo qui touche les bords se lit comme une
   affiche.
   La hauteur (4/5, plafonnee a 62vh) n'est pas decorative : elle fait
   DEPASSER le haut du second portrait sous la ligne de flottaison. C'est ce
   debord qui donne envie de defiler — aucune fleche, aucun indice a inventer.
   L'accrochage au defilement est en `proximity` et NON en `mandatory` : un
   coup de pouce tombe pile sur la personne suivante, un demi-geste ne bloque
   personne. Rien n'est cache derriere un geste.
   ECARTES : le carrousel horizontal (a deux personnes, il cache la moitie de
   l'equipe derriere un geste que rien n'annonce, et il rejouerait le conflit
   glisser-horizontal / defilement que la section 69 vient de payer) et
   l'accordeon / la carte qui se retourne (quatre lignes de texte derriere une
   manipulation, meme refus qu'aux sections 68 et 69).

   DEUX PIEGES TECHNIQUES, A NE PAS DEFAIRE
   --------------------------------------------------------------------------
   1. `.director-card__photo` est en `flex: 1` dans la regle de base, ce qui
      vaut `flex-basis: 0`. Dans une carte en COLONNE de hauteur automatique,
      il n'y a aucun espace libre a distribuer : la photo se retrouverait donc
      a HAUTEUR NULLE. D'ou `flex: 0 0 auto` ici, sans quoi tout le reste est
      inutile.
   2. `display: contents` sur `.director-card__body` fait de ses trois enfants
      des elements de la carte elle-meme — c'est ce qui permet de remonter le
      role et le nom SUR l'image par une marge negative, sans toucher au
      gabarit JS ni au DOM. L'ordre de lecture au clavier et au lecteur
      d'ecran ne bouge pas. Conséquence obligatoire : le `padding: 20px` de
      `__body` disparait avec sa boite, il est donc repris enfant par enfant.
      Le degrade du bas de la photo est renforce en consequence : du texte
      blanc pose sur une image quelconque a besoin d'un fond, pas d'un voile.

   BORNE 1023.98px, comme les sections 68 et 69 : c'est celle d'`isMobileTier`
   cote script, pour que la feuille et la revelation s'allument ensemble.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* -- 1. on rend le defilement, et la place de la barre du bas -- */
  .atelier__panel[data-panel="realisateurs"].is-active { height: auto; display: block; }
  .atelier__body[data-tab="realisateurs"] {
    padding: 0 0 calc(40px + var(--tabbar-space));
    overflow-y: auto; overflow-x: hidden;
    scroll-snap-type: y proximity;
  }

  /* -- 2. la grille redevient un simple flux -- */
  .realisateurs { display: block; height: auto; max-width: none; gap: 0; }

  /* -- 3. la carte n'est plus une carte -- */
  .director-card {
    border: 0; background: none; box-shadow: none;
    padding-bottom: 46px;
    scroll-snap-align: start;
  }
  .director-card:last-child { padding-bottom: 0; }

  /* -- 4. le portrait, a fond perdu -- */
  .director-card__photo {
    flex: 0 0 auto;             /* voir le piege 1 en tete de section */
    height: auto;
    aspect-ratio: 4 / 5;
    max-height: 62vh;
  }
  .director-card__photo::after {
    background: linear-gradient(180deg,
      rgba(10,10,12,0.28) 0%, transparent 24%, transparent 44%,
      rgba(10,10,12,0.58) 74%, rgba(10,10,12,0.94) 100%);
  }
  .director-card__badge { top: 14px; left: 14px; }

  /* -- 5. le nom passe sur l'image -- */
  .director-card__body { display: contents; }   /* voir le piege 2 */
  .director-card__body p {
    margin: 0; padding: 0 20px;
    font-size: 0.95rem; line-height: 1.72; max-width: 46ch;
  }
  /* Deux classes : la regle de base `.director-card__body p` l'emporterait
     sinon sur `.director-card__role`, plus faible d'un cran. */
  .director-card__body .director-card__role {
    position: relative; z-index: 2;
    margin: -62px 0 4px; padding: 0 20px;
    font-size: 0.6rem; letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--pink);
  }
  .director-card__body h4 {
    position: relative; z-index: 2;
    margin: 0 0 20px; padding: 0 20px;
    font-size: clamp(1.7rem, 7.4vw, 2.3rem); line-height: 1.06;
    letter-spacing: -0.01em;
  }
}

/* ==========================================================================
   SECTION 71 — EXPERTISE & AVIS AU DOIGT                            (v=196)

   LE DEFAUT DE FOND : LE MECANISME DE L'ONGLET DISPARAISSAIT
   --------------------------------------------------------------------------
   Tout cet onglet repose sur UN geste qui fait DEUX choses (section 51) : on
   ouvre un terrain a gauche, et la colonne d'avis de droite se filtre en face.
   En une colonne de telephone, la grille tombe a `1fr` et la colonne d'avis
   passe SOUS l'accordeon — a plus d'un ecran de la ligne qu'on vient de
   toucher. On tapait donc « Mariages », et la moitie de l'effet se jouait hors
   cadre. Le geste restait, sa recompense etait invisible : un accordeon qui
   filtre quelque chose qu'on ne voit pas n'est plus qu'un accordeon.

   CE QU'ON FAIT : LE BLOC D'AVIS SE GLISSE SOUS LE TERRAIN OUVERT
   --------------------------------------------------------------------------
   Les avis viennent se placer JUSTE SOUS la ligne qu'on vient d'ouvrir, entre
   elle et la suivante. La cause et l'effet sont enfin dans le meme regard, et
   le filet de couleur a gauche de chaque avis (`--terrain`, deja pose en
   ligne) rattache la preuve au terrain qui l'a appelee.

   ET C'EST DU CSS PUR — AUCUN DEPLACEMENT DE NOEUD, AUCUNE LIGNE DE SCRIPT.
   `display: contents` sur `.expertise__list` ET sur `.avis-col` fait des
   terrains et des blocs d'avis des FRERES dans la meme colonne. Des lors,
   `order` suffit a les intercaler, et `:has()` lit quel terrain est ouvert :
   terrain n = ordre 2n, bloc d'avis = ordre 2n+1. Le DOM ne bouge pas d'un
   pouce : l'ordre de lecture au clavier et au lecteur d'ecran reste celui
   d'aujourd'hui (les quatre terrains, puis les avis), seul l'ordre VISUEL
   change. Meme doctrine qu'aux sections 68 et 70.

   LE REPLI EST L'ETAT ACTUEL, PAS UNE PANNE. Sans `:has()`, aucune regle
   d'intercalage ne s'applique : les blocs d'avis gardent leur ordre par
   defaut (95) et se retrouvent apres les terrains, exactement comme
   aujourd'hui. Rien a detecter, rien a rattraper.
   L'ORDRE PAR DEFAUT DES TERRAINS EST 90 ET NON 0 : un cinquieme terrain
   ajoute un jour dans content.json n'a pas de regle `nth-child` ici — a
   l'ordre 0 il remonterait EN TETE de l'onglet. A 90 il se range sagement
   apres les quatre autres, et le bandeau des lieux (100) reste dernier.
   Les ordres sont ecrits jusqu'a SIX terrains : au-dela, deux lignes a
   ajouter et rien d'autre.

   ECARTES : le carrousel horizontal d'avis (il rejouerait le conflit
   glisser-horizontal / defilement de la section 69, et il cacherait la moitie
   des preuves derriere un geste que rien n'annonce) ; tout deplier d'un coup
   sans accordeon (quatre terrains x texte + tags + bouton + deux avis, on
   perd la vue d'ensemble et la page devient un couloir) ; et deplacer les
   noeuds en JavaScript a l'ouverture (memes pixels a l'arrivee, mais un DOM
   qui bouge sous le lecteur d'ecran et un ordinateur a reparer si la fenetre
   repasse au-dessus de 1024px).

   LES DEUX AUTRES CORRECTIFS
   --------------------------------------------------------------------------
   1. CONTRASTE. `--fg-faint` (#55555c) sur le noir vaut ~2,6:1, sous le seuil
      du lisible, et c'est la couleur de TOUS les petits libelles de cet
      onglet : le compte de films, le chevron, la pastille « Exemple », le
      role et la date de chaque avis, la mention « Le film », l'avertissement
      de demonstration, le label des lieux. Ils passent en `--fg-dim`
      (#9a9a9f, ~6,5:1), comme au Manifeste en section 68.
   2. L'EN-TETE DE TERRAIN. Quatre colonnes (numero, titre, compte, chevron)
      sur 390px laissaient ~215px au titre, qui cassait en deux lignes en
      passant sous le compte. Il passe en deux lignes NOMMEES : titre en
      haut, compte dessous, numero a cheval sur les deux, chevron a droite.
   Le bouton « Voir les films » devient une vraie ligne cadree : c'est la
   seule action de l'onglet, elle ne peut pas rester un lien de 0,68rem.

   BORNE 1023.98px, comme les sections 68 a 70.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {

  /* -- 1. terrains et avis deviennent freres dans une seule colonne -- */
  /* `align-items: stretch` N'EST PAS DECORATIF, C'EST LA LIGNE QUI TIENT LA
     LARGEUR  (v=197). La regle de base de `.expertise` est une GRILLE de
     bureau qui porte `align-items: start` — utile pour deux colonnes qui ne
     doivent pas s'aligner en hauteur, desastreux ici : en passant la boite en
     colonne FLEX, ce meme mot cesse d'etirer les enfants sur la largeur et
     chacun se dimensionne sur son contenu. Un paragraphe s'en accommode, mais
     `.avis-flux` est lui-meme un conteneur flex : sa largeur est alors calculee
     sur le contenu le plus large, c'est-a-dire la citation ecrite SUR UNE
     SEULE LIGNE. Les cartes d'avis debordaient donc l'ecran par la droite et
     le texte etait coupe. */
  .expertise {
    display: flex; flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  .expertise__list { display: contents; }
  .avis-col       { display: contents; }
  /* Un enfant flex ne descend pas sous sa largeur minimale sans ceci : la
     vignette du film et son titre a points de suspension en dependent. */
  .expertise .avis-flux, .expertise .avis { min-width: 0; }

  /* Ordres par defaut = l'agencement d'aujourd'hui (terrains, puis avis,
     puis les lieux). C'est aussi le repli sans `:has()`. */
  .terrain { order: 90; }
  .expertise .testimonials-label,
  .expertise .avis__note,
  .expertise .avis-flux { order: 95; }
  .expertise .enseignes { order: 100; }

  .expertise .testimonials-label { margin: 22px 0 12px; }
  .expertise .enseignes { margin-top: 20px; }

  /* -- 2. l'intercalage : bloc d'avis juste sous le terrain ouvert -- */
  .terrain:nth-child(1) { order: 2; }
  .terrain:nth-child(2) { order: 4; }
  .terrain:nth-child(3) { order: 6; }
  .terrain:nth-child(4) { order: 8; }
  .terrain:nth-child(5) { order: 10; }
  .terrain:nth-child(6) { order: 12; }

  .expertise:has(.terrain:nth-child(1).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 3; }
  .expertise:has(.terrain:nth-child(2).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 5; }
  .expertise:has(.terrain:nth-child(3).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 7; }
  .expertise:has(.terrain:nth-child(4).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 9; }
  .expertise:has(.terrain:nth-child(5).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 11; }
  .expertise:has(.terrain:nth-child(6).is-open) :is(.testimonials-label, .avis__note, .avis-flux) { order: 13; }

  /* -- 3. l'en-tete de terrain sur deux lignes -- */
  .terrain__head {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "num titre chev"
                         "num compte chev";
    gap: 3px 12px;
    padding: 15px 16px;
  }
  .terrain__num   { grid-area: num;    align-self: start; padding-top: 2px; }
  .terrain__title { grid-area: titre;  font-size: 1rem; line-height: 1.3; }
  .terrain__count { grid-area: compte; color: var(--fg-dim); }
  .terrain__chev  { grid-area: chev;   color: var(--fg-dim); align-self: center; }

  .terrain__body > * { margin-left: 16px; margin-right: 16px; }
  .terrain__text { max-width: none; font-size: 0.92rem; }

  /* -- 4. la seule action de l'onglet devient une vraie ligne -- */
  .terrain__go {
    display: flex; align-items: center; justify-content: space-between;
    width: auto; padding: 13px 15px;
    border: 1px solid rgba(255, 255, 255, 0.14);
    margin: 16px 16px 18px;
  }

  /* -- 5. les avis -- */
  .avis { padding: 20px 18px 16px; }
  .avis__q { max-width: none; font-size: 1rem; }
  .avis__demo,
  .avis__role,
  .avis__date,
  .avis__vide,
  .avis__projet-txt em,
  .avis__projet-fl,
  .enseignes__label { color: var(--fg-dim); }
  .avis__note {
    color: var(--fg-dim); font-size: 0.64rem;
    border-color: rgba(255, 255, 255, 0.22);
  }
  .enseignes__list span + span::before { color: var(--fg-dim); }
  .avis__projet img { width: 72px; height: 46px; }
}

/* ==========================================================================
   SECTION 72 — IL Y A UNE PAGE DERRIERE                            (v=204)

   La section 32 met l'accueil en `visibility: hidden` des qu'une page est
   ouverte, et c'est une bonne regle : une surface plein ecran de moins a
   peindre, et plus de tap fantome qui la traverse. Elle a une consequence dont
   on ne s'apercoit qu'en tirant le panneau sur le cote — il n'y a RIEN
   derriere lui. Le balayage de retour avait beau suivre le doigt, il decouvrait
   du noir.

   `geste-retour` est posee par le script au moment PRECIS ou le balayage est
   reconnu, et retiree des qu'il est fini. L'accueil redevient donc visible le
   temps du geste seulement — jamais pendant la lecture, ou la regle de la
   section 32 continue de s'appliquer telle quelle. Il reste inerte au toucher :
   c'est une page qu'on decouvre, pas encore une page dans laquelle on est.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  html.page-seule.geste-retour .hero {
    visibility: visible;
    pointer-events: none;
  }
}

/* ==========================================================================
   SECTION 73 — LES QUATRE ONGLETS DE L'ATELIER TIENNENT EN HAUT   (v=205)

   CE QUI CLOCHAIT. Les quatre libelles complets — « 01 / LE MANIFESTE », « 03 /
   LES REALISATEURS »… — ne tiennent pas dans 390px. La regle de la section 20
   s'en sortait en rendant la barre defilante horizontalement : les onglets 03
   et 04 vivaient donc hors de l'ecran, et rien ne disait combien il y en avait
   ni ou l'on se trouvait. Un sommaire qu'il faut faire defiler pour se lire
   n'est plus un sommaire.

   CE QU'ON FAIT. Au repos, un onglet ne montre que SON NUMERO. L'onglet actif,
   lui, deplie son nom. Les quatre tiennent alors ensemble, la position est
   lisible d'un coup d'oeil (trois numeros et un titre), et le titre du chapitre
   ou l'on est reste ecrit en toutes lettres.

   POURQUOI LE NOM S'OUVRE EN `max-width` ET NON EN `width`. Une largeur
   automatique ne s'anime pas : le navigateur ne sait pas interpoler vers
   « la place qu'il faudra ». On ouvre donc une borne large, et c'est le
   contenu qui decide de la largeur reelle en dessous. La transition court sur
   la borne, le rendu suit le texte.

   LA BARRE RESTE DEFILANTE en dernier recours : sur un tres petit ecran, ou si
   un libelle devenait tres long, mieux vaut pouvoir atteindre le quatrieme
   onglet que le voir deborder.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  .atelier__tabs {
    display: flex;
    gap: 6px;
    width: 100%;
    align-items: stretch;
  }

  .atelier__tab {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    flex: 0 0 auto;
    min-width: 0;
    padding: 9px 13px;
    overflow: hidden;
    transition: background-color 0.24s var(--ease), color 0.24s var(--ease);
  }

  .atelier__tab-num {
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
    opacity: 0.85;
  }

  /* Au repos : le nom est la, mesurable et lisible par les lecteurs d'ecran,
     mais sa borne est nulle. `display: none` l'aurait fait apparaitre d'un
     coup au changement d'onglet, sans rien a animer. */
  .atelier__tab-nom {
    display: block;
    max-width: 0;
    opacity: 0;
    white-space: nowrap;
    overflow: hidden;
    transition: max-width 0.3s var(--ease), opacity 0.22s var(--ease);
  }

  .atelier__tab.is-active .atelier__tab-nom {
    max-width: 14ch;
    opacity: 1;
  }
  .atelier__tab.is-active .atelier__tab-num { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) and (max-width: 1023.98px) {
  .atelier__tab, .atelier__tab-nom { transition-duration: 0.01ms; }
}

/* ==========================================================================
   SECTION 74 — LE PIED DE L'ATELIER SUR TELEPHONE                 (v=206)

   La regle de base (section 5) met les deux mentions sur UNE ligne, aux deux
   bouts (`justify-content: space-between`). Sur 390px la seconde se replie sur
   deux lignes et vient chevaucher la premiere, restee a gauche : c'est ce
   qu'on voyait, deux textes imbriques et illisibles.

   Ici on empile, on centre, et on aere. `space-between` n'a plus de sens quand
   il n'y a plus de ligne a tenir : c'est une regle de bureau, elle reste au
   bureau.
   -------------------------------------------------------------------------- */
@media (max-width: 1023.98px) {
  #atelierPanel .panel__footer {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    text-align: center;
    padding: 18px 20px calc(18px + var(--tabbar-space-compact, 0px));
    line-height: 1.5;
  }
}

/* ==========================================================================
   SECTION 75 — LE MOT DU CLIENT SUR LA FICHE FILM                  (v=208)

   OU, ET POURQUOI LA. Le bloc est le TROISIEME enfant de `.video-modal__info`
   et s'etend sur ses deux colonnes : il ferme la fiche en bande, sous le
   synopsis ET sous la fiche technique. Dans la colonne de gauche il n'avait
   qu'un tiers de la largeur pendant que la moitie droite restait vide en
   face — un avis n'a aucune raison d'etre plus etroit que le film.

   LA CARTE EST COUPEE EN DEUX A PARTIR DE 720px : la citation a gauche, la
   signature a droite derriere un filet. C'est ce qui remplit la bande sans
   etirer les lignes : au-dela de ~70 signes par ligne l'oeil perd le debut de
   la suivante, et c'est exactement ce qui arrive a un paragraphe pose sur
   toute la largeur d'une fiche.

   TYPOGRAPHIE : `--font-body`, la fonte du site. Le serif italique venait des
   cartes de L'Atelier et detonnait ici, ou tout le reste — titre, synopsis,
   fiche technique — est en Inter. Le grand guillemet decoratif est retire
   avec lui : la citation se signale par son filet de couleur et sa
   signature, elle n'a pas besoin d'un ornement pose par-dessus.

   `--cat-color` est ecrite sur <html> par openVideoModal a chaque ouverture :
   le filet, le nom et l'etat de survol prennent donc la couleur du terrain du
   film sans qu'une ligne de script ait a toucher ce bloc. Repli sur
   `var(--pink)` si la variable n'a pas encore ete posee.

   LES LIBELLES SONT EN `--fg-dim`, JAMAIS EN `--fg-faint` : meme correction
   qu'aux sections 68 et 71 — `--fg-faint` (#55555c) sur le noir tombe a
   2,6:1, sous le seuil du lisible.
   -------------------------------------------------------------------------- */
.fiche-avis {
  grid-column: 1 / -1;
  border-top: 1px solid var(--line);
  padding-top: 26px;
  margin-top: 4px;
}

/* Le libelle et l'acces a la colonne complete sur la meme ligne : le titre dit
   ce qu'on lit, le bouton dit ou aller. Ils se repartissent la largeur au lieu
   de s'empiler l'un sous l'autre. */
.fiche-avis__tete {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin-bottom: 14px;
}
/* LE LIBELLE EST RESTYLE ICI, ET CE N'EST PAS UNE REDITE. La regle mono du
   site est ecrite `.video-modal__main h4` — or ce bloc a justement quitte la
   colonne principale pour prendre les deux colonnes, il n'en herite donc plus.
   Sans ces quatre lignes, « LE MOT DU CLIENT » repasserait en gros titre gras
   par defaut du navigateur. */
.fiche-avis__tete h4 {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em;
  color: var(--fg-dim); font-weight: 400; margin: 0;
}

.fiche-avis__tous {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--fg-dim);
  transition: color 0.28s ease;
}
.fiche-avis__tous .fiche-avis__fl { transition: transform 0.28s ease; }
.fiche-avis__tous:hover { color: var(--cat-color, var(--pink)); }
.fiche-avis__tous:hover .fiche-avis__fl { transform: translate(2px, -2px); }
.fiche-avis__tous:focus-visible {
  outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 3px;
}

.fiche-avis__pile { display: grid; gap: 12px; }

/* LA CARTE. `text-align: left` et `font: inherit` sont obligatoires : un
   <button> centre son texte et impose la fonte du systeme, deux reglages du
   navigateur qui n'ont rien a faire ici. */
.fiche-avis__carte {
  position: relative;
  display: grid; gap: 14px;
  width: 100%; margin: 0;
  padding: 20px 22px 18px;
  text-align: left; font: inherit; color: inherit; cursor: pointer;
  border: 1px solid var(--line);
  border-left: 2px solid var(--cat-color, var(--pink));
  background: linear-gradient(180deg, rgba(255,255,255,0.030), rgba(255,255,255,0.008));
  transition: border-color 0.3s ease, background 0.3s ease;
}
.fiche-avis__carte:hover {
  border-color: var(--cat-color, var(--pink));
  background: linear-gradient(180deg, rgba(255,255,255,0.052), rgba(255,255,255,0.014));
}
.fiche-avis__carte:focus-visible {
  outline: 2px solid var(--cat-color, var(--pink)); outline-offset: 3px;
}

/* La fleche est le seul indice que la carte mene quelque part. Au repos elle
   est presque effacee : la citation doit se lire avant qu'on pense a cliquer. */
.fiche-avis__carte > .fiche-avis__fl {
  position: absolute; top: 16px; right: 16px;
  color: var(--fg-dim); opacity: 0.5;
  transition: opacity 0.28s ease, transform 0.28s ease, color 0.28s ease;
}
.fiche-avis__carte:hover > .fiche-avis__fl,
.fiche-avis__carte:focus-visible > .fiche-avis__fl {
  opacity: 1; color: var(--cat-color, var(--pink)); transform: translate(2px, -2px);
}

.fiche-avis__q {
  display: block;
  font-family: var(--font-body); font-size: 0.98rem; line-height: 1.66;
  color: var(--fg); white-space: pre-line;
  /* La reserve de droite laisse passer la fleche sans que la premiere ligne
     vienne buter dessus. */
  padding-right: 26px;
}

.fiche-avis__sig {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
}
.fiche-avis__ph {
  width: 36px; height: 36px; border-radius: 50%; object-fit: cover;
  display: block; flex: 0 0 auto;
  border: 1px solid var(--line); background: #111;
}
.fiche-avis__nom {
  font-size: 0.84rem; font-weight: 600;
  color: var(--cat-color, var(--pink));
}
.fiche-avis__role,
.fiche-avis__date {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--fg-dim);
}
.fiche-avis__date::before { content: '\00b7'; margin-right: 10px; }

/* --------------------------------------------------------------------------
   A PARTIR DE 720px : citation a gauche, signature a droite.
   La colonne de signature est BORNEE EN PIXELS et non en fraction : elle porte
   un prenom et deux libelles courts, une fraction la ferait grandir avec la
   fenetre pour n'y mettre que du vide. `min-width: 0` sur la citation est ce
   qui l'autorise a se retrecir dans une grille — sans lui, un mot long
   imposerait sa largeur a toute la carte.
   -------------------------------------------------------------------------- */
@media (min-width: 720px) {
  .fiche-avis__carte {
    grid-template-columns: minmax(0, 1fr) 200px;
    gap: 30px;
    align-items: start;
  }
  .fiche-avis__q { min-width: 0; padding-right: 0; }
  .fiche-avis__sig {
    flex-direction: column; align-items: flex-start; gap: 7px;
    padding-left: 26px; border-left: 1px solid var(--line);
    align-self: stretch;
  }
  .fiche-avis__date::before { content: none; margin-right: 0; }
  /* La fleche reprend sa place au-dessus de la colonne de signature. */
  .fiche-avis__carte > .fiche-avis__fl { top: 18px; right: 18px; }
}

/* Sur telephone la fiche tient dans une colonne : la citation garde sa taille
   (c'est elle qu'on vient lire), le reste se resserre. */
@media (max-width: 1023.98px) {
  .fiche-avis { padding-top: 22px; }
  .fiche-avis__carte { padding: 17px 18px 16px; }
  .fiche-avis__q { font-size: 0.93rem; }
}

/* Rien a ajouter pour le mouvement reduit : la regle globale en tete de
   feuille coupe deja toutes les transitions du document, en !important. */
