*,
*::before,
*::after {
  box-sizing: border-box;
}

/* La page DÉFILE désormais : la section 2 vit un viewport plus bas, et le
   brouillard montant (.brume-montante, en fin de fichier) recouvre toute la
   scène — cube compris — pendant la descente : c'est lui qui libère l'espace.
   Les pages sans section 2 ne bougent pas : rien à faire défiler, et la
   teinte au wheel (orbe.js) fonctionne dans les deux mondes. */
html,
body {
  height: 100%;
  overflow-x: clip;
}

/* ---- PAS DE BARRE DE DEFILEMENT ----
   La page defile toujours — seule la barre disparait. Elle coupait le bord
   droit du decor, et le defilement est de toute facon inertiel (Lenis) : la
   barre native ne repond meme pas au meme temps que la page.
   Trois ecritures pour trois moteurs ; aucune n'empeche la molette, le
   clavier ni le tactile. */
html {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* anciens Edge */
}
html::-webkit-scrollbar { display: none; }   /* Chrome, Safari */

/* Reglages recommandes par Lenis (defilement inertiel, assets/defilement.js) :
   il pose la classe `lenis` sur <html> quand il prend la main. */
html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}

.lenis.lenis-stopped {
  overflow: hidden;
}

body {
  margin: 0;
  /* CRAIE, et non plus le gris : la page est le monde « apres la brume ». La
     scene d'aurore n'est plus le fond, elle vit dans une fenetre (.fenetre). */
  background-color: var(--degrade-craie);
  color: var(--color-brand-studio);
  font-family: var(--font-main);
  font-size: var(--text-body);
  font-weight: var(--fw-light);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-normal);
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4,
p {
  margin: 0;
}

.display {
  font-size: var(--text-display);
  line-height: var(--lh-display);
  font-weight: var(--fw-extralight);
  letter-spacing: var(--ls-tight);
}

/* ---- Le texte ne se surligne pas a la souris ----

   AUCUN EFFET SEO, et c'est le point : `user-select` ne touche qu'au GESTE de
   selection. Le texte reste un vrai contenu dans le DOM — les crawlers le
   lisent, les lecteurs d'ecran l'annoncent, la vue source le donne. Rien n'est
   masque, rien n'est remplace par une image, aucun contenu n'est injecte en JS.

   Le champ de saisie fait exception : un champ dont on ne peut pas selectionner
   le contenu est un champ casse (pas de double-clic, pas de correction). */
body {
  -webkit-user-select: none;
  user-select: none;
}

/* PAS DE REBOND EN BOUT DE PAGE (demande de Quentin, 23/08/2026) : arrive en
   bas, le pied ne se « souleve » pas — l'elastique d'Edge/Chromium (et le
   glow) est coupe. Vaut aussi pour le haut, et desarme au passage la
   navigation au geste. */
html, body {
  overscroll-behavior: none;
}

.barre-champ {
  -webkit-user-select: text;
  user-select: text;
}

/* La non-selection est desormais posee sur `body` (voir plus haut) : elle vaut
   pour toute la page, pas pour le seul titre. */
h1 {
  font-size: var(--text-h1);
  line-height: var(--lh-h1);
  /* Hauteur réservée : le titre change de longueur d'une page à l'autre, or le
     groupe est centré verticalement. Sans réserve, le logotype glisserait. */
  min-height: calc(var(--lh-h1) * var(--titre-lignes));
  font-weight: var(--fw-extralight);
  letter-spacing: var(--ls-tight);
}

h2 {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-snug);
}

h3 {
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-snug);
}

h4 {
  font-size: var(--text-h4);
  line-height: var(--lh-h4);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-normal);
}

.text-lg {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body-lg);
}

.text-sm {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}

.caption {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* Les deux colonnes principales : rail de marque à gauche, canevas à droite.
   FIXE : le logo et le h1 ne bougent pas d'un pixel à la descente — le
   brouillard (z-index 4) les recouvre, il ne les emporte pas. C'est la
   section 2, seule dans le flux, qui donne sa hauteur de défilement.
   Ils S'EFFACENT en phase avec la descente, mais seulement quand le
   brouillard commence vraiment à tout recouvrir : fenêtre 34% → 62% du
   défilement (la haze devient sérieuse vers un tiers). Avant : pleine
   présence ; après : la brume règne seule. */
.shell {
  position: fixed;
  inset: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: stretch;
  opacity: clamp(0, calc(1 - (var(--defilement) - 0.34) * 3.6), 1);
  /* TRANSPARENT AUX CLICS, et ce n'est pas une precaution : c'est une panne
     qui a ete constatee. Ce calque est `fixed; inset: 0` — il couvre TOUT
     l'ecran, en permanence. Son opacite tombe a zero quand on descend, mais
     un element invisible RECOIT TOUJOURS LES CLICS : il restait donc une
     vitre sur toute la page, et tout ce qu'on cliquait plus bas lui revenait.
     C'est ce qui empechait le bouton « Activer le son » de la vitrine de
     repondre (elementFromPoint sur le bouton renvoyait DIV.shell).

     Sans danger : ce bloc ne contient que du texte — le logotype, la ligne du
     panneau, la barre laterale. Pas un lien, pas un bouton. Verifie. */
  pointer-events: none;
}

/* L'ENTREE, MOT PAR MOT : neant total au depart — aucun deplacement,
   aucune remontee, RIEN — puis chaque mot fond depuis zero, un a un :
   « graphen » (0), « studio » (987), puis les six mots du h1 en cadence
   (2584 + n x 233 ms). Fondu PUR : les mots naissent sur place, en
   position fixe. Retards Fibonacci. L'entree ATTEND la video : tant que
   cadre.js n'a pas leve attente-video (canplaythrough, plafond 5.5s),
   tout reste suspendu au neant. Supprimee en reduced-motion (plus bas). */
@keyframes paraitre {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* LES ETIQUETTES DE L'EN-TETE prennent les creneaux du logotype parti : la
   premiere a 610ms (le tempo), puis 1597, 2584 — les termes de la suite — et
   l'acces client a 3571 (987 de plus). Meme fondu pur, memes retards. */
.entete .etiquette {
  animation: paraitre 2100ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.entete-nav .etiquette:nth-child(1) { animation-delay: 610ms; }
.entete-nav .etiquette:nth-child(2) { animation-delay: 1597ms; }
.entete-nav .etiquette:nth-child(3) { animation-delay: 2584ms; }
.entete-droite .etiquette { animation-delay: 3571ms; }

/* Le logotype revenu : « graphen » a 610 (le tempo), « studio » a 1597 — ses
   creneaux d'origine. Le paragraphe de droite suit le premier mot du titre,
   « Scroll » ferme la marche. */
.logo span {
  animation: paraitre 3400ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.logo-graphen { animation-delay: 610ms; }
.logo-studio  { animation-delay: 1597ms; }

.hero-texte {
  animation: paraitre 2100ms cubic-bezier(0.22, 1, 0.36, 1) 4414ms both;
}

.hero-scroll {
  animation: paraitre 2100ms cubic-bezier(0.22, 1, 0.36, 1) 5579ms both;
}

/* le h1 des pages sans decoupage fond d'un bloc ; celui de l'accueil est
   en spans, dont la cadence s'ajoute au fondu du bloc */
.sidebar .hero-titre {
  animation: paraitre 2100ms cubic-bezier(0.22, 1, 0.36, 1) 4181ms both;
}

.sidebar .hero-titre .mot-titre {
  animation: paraitre 2100ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.sidebar .hero-titre .mot-titre:nth-child(1) { animation-delay: 4181ms; }
.sidebar .hero-titre .mot-titre:nth-child(2) { animation-delay: 4414ms; }
.sidebar .hero-titre .mot-titre:nth-child(3) { animation-delay: 4647ms; }
.sidebar .hero-titre .mot-titre:nth-child(4) { animation-delay: 4880ms; }
.sidebar .hero-titre .mot-titre:nth-child(5) { animation-delay: 5113ms; }
.sidebar .hero-titre .mot-titre:nth-child(6) { animation-delay: 5346ms; }

html.attente-video .entete .etiquette,
html.attente-video .logo span,
html.attente-video .hero-texte,
html.attente-video .hero-scroll,
html.attente-video .sidebar .hero-titre,
html.attente-video .sidebar .hero-titre .mot-titre {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .sidebar .hero-titre,
  .sidebar .hero-titre .mot-titre {
    animation: none;
  }
}

/* ---- Le dégradé animé ----

   Un champ unique : UN dégradé linéaire diagonal — bleu → gris → pêche → orangé —
   porté par un élément qui déborde largement de l'écran et pivote très lentement
   sur lui-même. Les versions précédentes composaient le fond par morceaux
   (taches radiales, puis bandes obliques floutées) : chaque morceau restait
   lisible comme une forme. Ici il n'y a aucune forme — que de la couleur pleine,
   d'un bord à l'autre, sans flou.

   Empilement, du fond vers l'avant :
   champ → voile de verre → scrim linéaire → grain

   POURQUOI SEUL `transform` EST ANIMÉ
   -----------------------------------
   `transform` et `opacity` sont les deux seules propriétés que le compositeur
   traite sans repeindre. Animer `background-position` sur un grand dégradé — la
   recette la plus répandue — force un repaint plein écran à chaque image.

   Le flou et les corrections de couleur, eux, ne changent JAMAIS : ils sont
   calculés une fois et mis en cache dans la couche ; seul le déplacement bouge. */
.fond {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background-color: var(--color-bg);
}

/* Le champ : un carré de 200vmax centré sur l'écran, entièrement couvert par un
   seul dégradé linéaire à quatre arrêts. Pas d'opacité partielle, pas de flou,
   pas de recouvrement : la couleur affichée est celle du dégradé, point. */
.fond span,
.fond-aurore {
  position: absolute;
  display: block;
  width: var(--champ-taille);
  height: var(--champ-taille);
  top: calc(50% - var(--champ-taille) / 2);
  left: calc(50% - var(--champ-taille) / 2);
  will-change: transform;
}

/* Le champ : interpolation OKLCH — le trajet bleu → orange passe par des
   teintes pleines, jamais par le gris sale du sRGB. Le repli sans `in oklch`
   est déclaré AVANT : un navigateur qui l'ignore garde la version simple. */
.fond span {
  background: linear-gradient(135deg,
    var(--degrade-abysse) 0%,
    var(--degrade-bleu) 30%,
    var(--degrade-gris) 52%,
    var(--degrade-peche) 74%,
    var(--degrade-orange) 100%);
  background: linear-gradient(135deg in oklch,
    var(--degrade-abysse) 0%,
    var(--degrade-bleu) 30%,
    var(--degrade-gris) 52%,
    var(--degrade-peche) 74%,
    var(--degrade-orange) 100%);
  filter: saturate(var(--champ-saturation));
  animation: balancement var(--champ-duree) ease-in-out infinite alternate;
}

/* L'aurore : pétales coniques doux qui tournent EN SENS INVERSE du champ.
   L'interférence des deux rotations fait un mouvement de couleur organique —
   deux transforms, aucun repaint. Le voile dépoli la fond dans le champ. */
.fond-aurore {
  background: conic-gradient(from 0deg,
    transparent 0%,
    var(--degrade-peche) 9%,
    transparent 22%,
    var(--degrade-bleu) 37%,
    transparent 52%,
    var(--degrade-orange) 68%,
    transparent 83%,
    transparent 100%);
  background: conic-gradient(from 0deg in oklch,
    transparent 0%,
    var(--degrade-peche) 9%,
    transparent 22%,
    var(--degrade-bleu) 37%,
    transparent 52%,
    var(--degrade-orange) 68%,
    transparent 83%,
    transparent 100%);
  opacity: var(--aurore-opacite);
  animation: rotation-aurore var(--aurore-duree) linear infinite;
}

@keyframes rotation-aurore {
  from { transform: rotate(360deg); }
  to   { transform: rotate(0deg); }
}

/* Le SOCLE : la brume lumineuse du bas de page. C'est le dégradé lui-même qui
   fabrique le brouillard où le cube s'enfonce — le fondu du cube et cette
   nappe se rejoignent. */
.fond-socle {
  position: absolute;
  inset: 0;
  display: block;
  background:
    radial-gradient(ellipse 105% 72% at 58% 106%,
      var(--degrade-socle) 0%,
      color-mix(in oklch, var(--degrade-socle) 60%, transparent) 48%,
      transparent 78%),
    linear-gradient(to top,
      color-mix(in oklch, var(--degrade-socle) 85%, transparent) 0%,
      transparent var(--socle-hauteur));
}

/* Le brouillard fait PARTIE du gradient : deux voiles teintes — un froid,
   un orange — respirent SUR le socle, chacun synchronise sur SA tache en
   orbite (meme periode 199 s, meme retard) : son opacite culmine au moment
   ou la tache passe devant, en bas — la ou vit le brouillard. Le socle
   bleuit quand le bleu passe, s'embrase quand l'orange passe. Opacity
   seule : compositeur pur. */
.fond-socle::before,
.fond-socle::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

.fond-socle::before {
  background:
    radial-gradient(ellipse 105% 72% at 58% 106%,
      color-mix(in oklch, var(--degrade-bleu) 45%, var(--degrade-socle)) 0%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 45%, var(--degrade-socle)) 60%, transparent) 48%,
      transparent 78%);
  animation-delay: -33.2s; /* la tache bleue */
}

.fond-socle::after {
  background:
    radial-gradient(ellipse 105% 72% at 58% 106%,
      color-mix(in oklch, var(--degrade-orange) 45%, var(--degrade-socle)) 0%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 45%, var(--degrade-socle)) 60%, transparent) 48%,
      transparent 78%);
  animation-delay: -99.5s; /* la tache orange */
}

/* culmine a 75% du cycle : la position DEVANT-BAS de l'orbite des taches */
@keyframes souffle-brume {
  0%, 45% { opacity: 0; }
  75%     { opacity: 0.55; }
  95%, 100% { opacity: 0; }
}

/* Le pivot fait tourner la diagonale de couleur elle-même : le bleu migre d'un
   coin vers l'autre, l'orangé le suit à l'opposé — une lumière qui se déplace,
   sans qu'aucun élément ne semble bouger. */
@keyframes balancement {
  from { transform: rotate(calc(-1 * var(--champ-balancement))); }
  to   { transform: rotate(var(--champ-balancement)); }
}

/* Un fond qui bouge en continu est un déclencheur reconnu de gêne vestibulaire.
   Le champ reste, figé à mi-course. */
@media (prefers-reduced-motion: reduce) {
  .fond span,
  .fond-aurore,
  .fond-tache,
  .fond-socle::before,
  .fond-socle::after,
  .cube3-brume::before,
  .cube3-brume::after,
  .cube3-brume-interne::before,
  .cube3-brume-interne::after,
  .cube3-orbe-pied::before,
  .cube3-orbe-pied::after,
  .brume-montante::before,
  .brume-montante::after,
  .brume-ambiante::before,
  .brume-ambiante::after,
  .cube3-brume-interne b,
  .c3-reflets::before,
  .c3-reflets::after,
  .c3-reflets-chaud::before,
  .c3-reflets-chaud::after {
    animation: none;
    /* figees a une position d'orbite lisible */
    transform: translate(calc(-50% + 34vmin), calc(-50% - 19vmin)) scale(0.9);
  }

  .fond span,
  .fond-aurore,
  .cube3-brume-interne b {
    transform: none;
  }
}

/* Le voile de verre dépoli. Un élément réel, placé APRÈS le champ et AVANT le
   scrim : `backdrop-filter` ne floute que ce qui est peint dessous — le champ —
   tandis que scrim et grain restent nets par-dessus. C'est ce contraste verre
   lisse / matière qui fait la lecture « à travers une vitre ». */
.fond-voile {
  position: absolute;
  inset: 0;
  display: block;
  -webkit-backdrop-filter: blur(var(--voile-flou)) saturate(var(--voile-saturation));
  backdrop-filter: blur(var(--voile-flou)) saturate(var(--voile-saturation));
}

/* La figure : deux voiles masqués par la luminance du buste, peints AU-DESSUS
   du verre dépoli — la figure reste définie pendant que le fond est flouté,
   c'est l'émergence — et SOUS le scrim et le grain, qui unifient le tout.

   Aucune couleur propre : un voile sombre modulé par le modelé, un voile clair
   modulé par les hautes lumières. Ce qui colore la figure, c'est le dégradé
   qui passe à travers — la fusion est parfaite par construction. */
.figure-ombres,
.figure-lumieres {
  position: absolute;
  bottom: 0;
  right: var(--figure-droite);
  height: var(--figure-hauteur);
  aspect-ratio: 749 / 937;
  mask-size: 100% 100%;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
}

.figure-ombres {
  background: var(--figure-ombre);
  -webkit-mask-image: url("assets/figure-ombres.webp");
  mask-image: url("assets/figure-ombres.webp");
}

.figure-lumieres {
  background: var(--figure-lumiere);
  -webkit-mask-image: url("assets/figure-lumieres.webp");
  mask-image: url("assets/figure-lumieres.webp");
}

/* Le canvas de l'objet 3D, en melange normal. Place entre le voile de verre
   (l'objet reste defini sur un fond floute : l'emergence) et le scrim + grain
   qui unifient le tout. La fusion est faite PAR le shader : volume par
   temperature, bords dissous par Fresnel, evaporation vers le bas. */
.fond-scene {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---- Le cube géant : une LENTILLE dans le champ ----

   Lecture attentive de la référence : le champ de couleur y est CONTINU à
   travers le cube — la tache violette du fond entre dans la face du dessus, la
   tache jaune traverse la face droite. Le cube n'a aucune couleur à lui : c'est
   une lentille qui ÉCLAIRCIT le champ qu'elle traverse, différemment par face.

   Construction :
   — des TACHES DE COULEUR DANS LE MONDE (éléments du fond), placées à cheval
     sur la frontière du cube ;
   — des faces en backdrop-filter SANS flou — brightness/saturate seulement :
     la continuité éclaircie, c'est ça le verre ici. Un flou n'aurait rien
     montré (le fond est lisse), et des dégradés propres aux faces cassaient
     la continuité (des panneaux, pas une lentille).

   BACKDROP ROOT : un ancêtre à mask/opacity/filter couperait les faces du
   fond. Le conteneur reste donc vierge ; la dissolution du bas est portée par
   le mask DE CHAQUE FACE (un mask sur l'élément lui-même découpe sa propre
   sortie, y compris l'effet de fond — même mécanisme que le clip-path). */
.cube3 {
  position: absolute;
  left: var(--cube3-x);
  top: var(--cube3-y);
  width: var(--cube3-taille);
  height: var(--cube3-taille);
  transform: translate(-50%, -50%);
}

.cube3 b {
  position: absolute;
  inset: 0;
  display: block;
}

/* Les taches du monde : posées AVANT le voile (elles appartiennent au fond,
   le dépoli les adoucit), à cheval sur la silhouette du cube. */
/* Les taches sont EN ORBITE autour du cube : le gradient du monde tourne
   lentement autour du volume. La 3D est jouee par la profondeur simulee —
   petites et attenuees quand elles passent DERRIERE (en haut de l'ellipse),
   larges et pleines quand elles passent DEVANT (en bas). Une seule orbite
   partagee (199 s, premier de la suite : jamais de battement avec le champ
   47 s ni l'aurore 89 s), trois departs decales d'un tiers de tour — les
   couleurs restent toujours reparties autour du cube. Transform pur. */
.fond-tache {
  position: absolute;
  display: block;
  left: var(--cube3-x);
  top: var(--cube3-y);
  animation: orbite-tache 199s linear infinite;
  will-change: transform;
}

@keyframes orbite-tache {
  0%    { transform: translate(calc(-50% + 47vmin), -50%) scale(1); opacity: 1; }
  12.5% { transform: translate(calc(-50% + 34vmin), calc(-50% - 19vmin)) scale(0.87); opacity: 0.92; }
  25%   { transform: translate(-50%, calc(-50% - 27vmin)) scale(0.76); opacity: 0.84; }
  37.5% { transform: translate(calc(-50% - 34vmin), calc(-50% - 19vmin)) scale(0.87); opacity: 0.92; }
  50%   { transform: translate(calc(-50% - 47vmin), -50%) scale(1); opacity: 1; }
  62.5% { transform: translate(calc(-50% - 34vmin), calc(-50% + 19vmin)) scale(1.08); opacity: 1; }
  75%   { transform: translate(-50%, calc(-50% + 27vmin)) scale(1.13); opacity: 1; }
  87.5% { transform: translate(calc(-50% + 34vmin), calc(-50% + 19vmin)) scale(1.08); opacity: 1; }
  100%  { transform: translate(calc(-50% + 47vmin), -50%) scale(1); opacity: 1; }
}

.fond-tache-bleue {
  width: calc(var(--cube3-taille) * 1.0);
  height: calc(var(--cube3-taille) * 0.8);
  background: radial-gradient(closest-side, color-mix(in srgb, var(--degrade-bleu) 85%, transparent), transparent 72%);
  /* departs decales d'un tiers de periode : les trois couleurs se suivent
     autour du cube sans jamais se rattraper */
  animation-delay: -33.2s;
}

.fond-tache-orange {
  width: calc(var(--cube3-taille) * 1.1);
  height: calc(var(--cube3-taille) * 0.95);
  background: radial-gradient(closest-side, var(--degrade-orange), transparent 72%);
  animation-delay: -99.5s;
}

.fond-tache-claire {
  width: calc(var(--cube3-taille) * 0.9);
  height: calc(var(--cube3-taille) * 0.7);
  background: radial-gradient(closest-side, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.55) 45%, transparent 74%);
  animation-delay: -165.8s;
}

/* Les trois faces de la lentille. Le fondu bas est PAR FACE (voir backdrop
   root ci-dessus). Les voiles blancs très légers « laitent » le verre sans
   apporter de teinte. */
.c3-haut,
.c3-gauche,
.c3-droite {
  -webkit-mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
}

/* ---- L'ORBE : la forme qui vit dans la masse ----

   Le mecanisme de la reference aux cubes laiteux : une sphere lumineuse
   peinte DERRIERE les faces-lentilles (premier enfant du cube, donc dans le
   backdrop des faces) — c'est le verre lui-meme qui la diffuse, avec son
   propre flou par face. La PRESENCE visible est portee par .cube3-orbe,
   posee au-dessus de la brume (voir plus bas) : ici, on ne fait que
   l'ambiance profonde.

   Sa teinte est UNE VARIABLE (--orbe-teinte) : la molette la fait tourner
   sur tout le spectre (orbe.js), et le pouls du scroll passe par la classe
   .cube3-excite — transitions compositeur, aucun canvas, aucun rAF. */
.c3-orbe {
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  -webkit-mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  transition: --orbe-teinte 987ms cubic-bezier(0.2, 0.6, 0.3, 1);
}

/* Le corps profond : cœur sombre et sature, halo chaud, aura froide — dense
   assez pour survivre au brightness des lentilles. */
.c3-orbe {
  background:
    radial-gradient(21% 19% at 50% 50%,
      oklch(0.78 0.10 calc(var(--orbe-teinte) + 15deg) / 0.55) 0%,
      oklch(0.85 0.07 var(--orbe-teinte) / 0.34) 45%,
      transparent 85%),
    radial-gradient(42% 38% at 50% 50%,
      oklch(0.88 0.06 calc(var(--orbe-teinte) + 34deg) / 0.29) 0%,
      transparent 80%);
  animation: orbe-respire 29s ease-in-out infinite alternate;
}

@keyframes orbe-respire {
  from { transform: scale(0.95); opacity: 0.89; }
  to   { transform: scale(1.06); opacity: 1; }
}


.c3-haut {
  clip-path: polygon(50% 0, 100% 20%, 50% 40%, 0 20%);
  -webkit-backdrop-filter: blur(34px) brightness(1.21) saturate(1.08);
  backdrop-filter: blur(34px) brightness(1.21) saturate(1.08);
  background: linear-gradient(115deg, rgba(255,255,255,0.34), rgba(255,255,255,0.12) 60%, rgba(255,255,255,0.05));
}

.c3-gauche {
  clip-path: polygon(0 20%, 50% 40%, 50% 100%, 0 80%);
  -webkit-backdrop-filter: blur(47px) brightness(1.29) saturate(0.92);
  backdrop-filter: blur(47px) brightness(1.29) saturate(0.92);
  /* Le froid occupe le quart HAUT-GAUCHE de cette face — la continuité de la
     nappe froide du fond, qui déborde du coin (0%,20%) et fond vers le blanc
     chaud du bas. */
  background:
    radial-gradient(ellipse 58% 50% at 0% 20%,
      color-mix(in oklch, var(--degrade-bleu) 78%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 34%, transparent) 48%,
      transparent 80%),
    linear-gradient(20deg, rgba(255,255,255,0.24), rgba(255,255,255,0.38) 60%, rgba(255,255,255,0.14));
}

.c3-droite {
  /* Tronquée à 60% de la hauteur de l'arête droite (y = 25% + 0.6×50% = 55%) :
     une lentille ne se fond pas (backdrop-filter ignore les masques), elle se
     COUPE — par le clip-path, le seul qui la découpe vraiment. La coupe est
     parallèle à l'arête basse d'origine et la brume l'ensevelit. */
  clip-path: polygon(50% 40%, 100% 20%, 100% 56%, 50% 76%);
  -webkit-backdrop-filter: blur(47px) brightness(1.10) saturate(1.18);
  backdrop-filter: blur(47px) brightness(1.10) saturate(1.18);
  /* La TRANSMISSION s'accroche aux COINS HAUTS, comme dans la référence. Les
     radiales ne sont PAS posées pile sur les sommets : la brume frontale opaque
     couvre déjà ~50% au sommet avant et le masque vertical attaque à 38% — un
     reflet ancré là est avalé. Elles sont donc ancrées sur la bande haute
     VISIBLE de la face (le point (60%,36%) est sur l'arête haute), avec une
     densité qui compense l'absorption. */
  background:
    radial-gradient(ellipse 46% 34% at 60% 36%,
      color-mix(in oklch, var(--degrade-bleu) 88%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 42%, transparent) 45%,
      transparent 78%),
    radial-gradient(ellipse 48% 44% at 100% 20%,
      color-mix(in oklch, var(--degrade-bleu) 72%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 30%, transparent) 50%,
      transparent 78%),
    linear-gradient(250deg, rgba(255,255,255,0.14), rgba(255,255,255,0.05) 55%, rgba(255,255,255,0.22));
}

/* L'ABSORPTION DE PAROI : dans un bloc épais, le trajet optique s'allonge vers
   les bords de la silhouette — la matière s'y densifie et s'y teinte. Un anneau
   interne, transparent au cœur, teinté au bord : c'est lui qui donne
   l'épaisseur, là où un verre creux resterait uniforme jusqu'au bord. */
.c3-paroi {
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  -webkit-mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  background: radial-gradient(ellipse 52% 52% at 50% 50%,
    transparent 52%,
    color-mix(in srgb, var(--degrade-peche) 29%, transparent) 76%,
    color-mix(in srgb, var(--degrade-gris) 45%, transparent) 96%);
}

/* ---- L'INTÉRIEUR du bloc ----

   Deux couches qui font lire un verre PLEIN et non trois vitres :

   1. .c3-fantomes : les ARÊTES ARRIÈRE vues à travers la masse. En isométrie,
      le sommet arrière caché se projette exactement au centre de l'hexagone ;
      ses trois arêtes filent du centre vers (50%,0), (0,75%) et (100%,75%).
      Des veines floues et faibles sur ces trajets = l'œil comprend qu'il voit
      L'INTÉRIEUR du volume.

   2. .c3-interieur : la DENSITÉ du cœur — lueurs qui flottent dans la masse,
      à cheval sur les arêtes internes (clip à la silhouette, pas aux faces),
      plus denses au centre où le trajet optique est le plus long. */
.c3-fantomes {
  opacity: 0.45;
  transition: --orbe-teinte 987ms cubic-bezier(0.2, 0.6, 0.3, 1);
}

.c3-fantomes,
.c3-interieur {
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  -webkit-mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
}

/* Les aretes arriere en DISPERSION : le mecanisme des references prisme —
   un cœur blanc flanque de deux franges spectrales (cote chaud / cote froid,
   derivees de la teinte de l'orbe). Le degrade court EN TRAVERS du fil ; le
   fondu LE LONG du fil passe par un mask. Elles flambent au scroll. */
.c3-fantomes i {
  position: absolute;
  display: block;
  filter: blur(3px);
}

/* du point arriere (50%,60%) vers le sommet haut */
.c3-fantome-haut {
  left: calc(50% - 4px);
  top: 0;
  width: 8px;
  height: 60%;
  transform: scaleY(-1);
  background: linear-gradient(to right,
    transparent,
    oklch(0.80 0.10 calc(var(--orbe-teinte) - 50deg) / 0.16) 21%,
    rgba(255, 255, 255, 0.29) 50%,
    oklch(0.84 0.08 calc(var(--orbe-teinte) + 150deg) / 0.16) 79%,
    transparent);
  -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.85), rgba(0,0,0,0.3) 60%, transparent 90%);
  mask-image: linear-gradient(to bottom, rgba(0,0,0,0.85), rgba(0,0,0,0.3) 60%, transparent 90%);
}

/* du centre vers les coins bas gauche et droit : longueur hypot(50,25)=55,9%,
   angles +/-26,57 degres sous l'horizontale */
.c3-fantome-gauche,
.c3-fantome-droit {
  left: 50%;
  top: calc(60% - 4px);
  width: 53.85%;
  height: 8px;
  transform-origin: 0 50%;
  background: linear-gradient(to bottom,
    transparent,
    oklch(0.80 0.10 calc(var(--orbe-teinte) - 50deg) / 0.16) 21%,
    rgba(255, 255, 255, 0.29) 50%,
    oklch(0.84 0.08 calc(var(--orbe-teinte) + 150deg) / 0.16) 79%,
    transparent);
  -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0.85), rgba(0,0,0,0.3) 60%, transparent 90%);
  mask-image: linear-gradient(to right, rgba(0,0,0,0.85), rgba(0,0,0,0.3) 60%, transparent 90%);
}

.c3-fantome-gauche { transform: rotate(158.2deg); }
.c3-fantome-droit { transform: rotate(21.8deg); }

/* la matière du cœur : dense au centre, les lueurs traversent les arêtes */
/* Beer-Lambert : I = I0*e^(-aD). Vu par le coin, le trajet optique D est
   MAXIMAL au centre projete du volume et quasi nul aux sommets de la
   silhouette — un bloc plein voile donc le fond au centre, pas aux bords.
   C'est LA difference entre une armature vide et une masse de verre.
   S'y ajoute le subsurface scattering du givre : la matiere luit de lumiere
   diffusee, surtout sous la face eclairee du dessus. */
.c3-interieur {
  background:
    /* le champ d'absorption : dense au centre (trajet long), rien aux bords */
    radial-gradient(52% 52% at 50% 50%,
      color-mix(in srgb, var(--degrade-socle) 42%, transparent) 0%,
      color-mix(in srgb, var(--degrade-gris) 24%, transparent) 45%,
      transparent 88%),
    /* l'INVERSION refractive — la signature du verre PLEIN : une lentille
       epaisse renverse le fond. La page a le froid en haut, le chaud en bas ;
       le volume porte donc l'image inverse : chaud dans son haut, bleu dans
       son bas (avant que la brume ne l'avale). Legerement desaxee (178deg) :
       l'image refractee est aussi DEPLACEE, jamais alignee. */
    linear-gradient(178deg,
      transparent 16%,
      color-mix(in oklch, var(--degrade-orange) 45%, transparent) 27%,
      color-mix(in oklch, var(--degrade-peche) 26%, transparent) 38%,
      color-mix(in oklch, var(--degrade-bleu) 16%, transparent) 47%,
      color-mix(in oklch, var(--degrade-bleu) 55%, transparent) 58%,
      transparent 70%),
    /* la lumiere du dessus, diffusee dans la masse : une colonne qui decroit */
    linear-gradient(to bottom,
      transparent 13%,
      rgba(255, 255, 255, 0.18) 26%,
      rgba(255, 255, 255, 0.06) 47%,
      transparent 66%),
    /* les teintes du monde dissoutes dans le verre, adoucies */
    radial-gradient(circle at 64% 48%, color-mix(in srgb, var(--degrade-orange) 34%, transparent) 0%, transparent 45%),
    radial-gradient(circle at 37% 30%, color-mix(in srgb, var(--degrade-bleu) 21%, transparent) 0%, transparent 42%);
}

/* ---- Les REFLETS, au sommet de la pile interne — SYNCHRONISES sur
   l'orbite du gradient ----

   La position des couleurs du monde commande l'angle des reflets dans le
   verre : quand la tache bleue orbite a DROITE du cube, le froid frappe la
   face droite ; passee a GAUCHE, il glisse sur la face gauche — idem pour
   la chaude, decalee d'un tiers de tour. Meme horloge que l'orbite (199 s,
   memes retards que les taches), etats croises en opacity pur :

   .c3-reflets          : base ambiante discrete (toujours la), + ::before
                          « bleu a droite » et ::after « bleu a gauche » ;
   .c3-reflets-chaud    : ::before « chaud a droite », ::after « chaud a
                          gauche ». */
.c3-reflets,
.c3-reflets-chaud {
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  -webkit-mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 38%, rgba(0,0,0,0.45) 55%, transparent 74%);
}

/* la base ambiante : le schema de la reference, a demi-densite — c'est le
   mouvement qui apporte le reste */
.c3-reflets {
  background:
    radial-gradient(ellipse 52% 30% at 28% 15%,
      color-mix(in oklch, var(--degrade-bleu) 38%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 18%, transparent) 48%,
      transparent 80%),
    radial-gradient(ellipse 42% 26% at 58% 34%,
      color-mix(in oklch, var(--degrade-bleu) 38%, transparent) 0%,
      transparent 76%),
    radial-gradient(ellipse 50% 44% at 2% 22%,
      color-mix(in oklch, var(--degrade-bleu) 34%, transparent) 0%,
      transparent 78%);
}

.c3-reflets::before,
.c3-reflets::after,
.c3-reflets-chaud::before,
.c3-reflets-chaud::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: 199s linear infinite;
}

/* LE BLEU SUIT SA TACHE (retard -33.2s, comme elle) */
.c3-reflets::before {
  /* la source est a DROITE : le froid enveloppe la face droite */
  background:
    radial-gradient(ellipse 42% 26% at 58% 34%,
      color-mix(in oklch, var(--degrade-bleu) 76%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 34%, transparent) 45%,
      transparent 76%),
    radial-gradient(ellipse 34% 30% at 100% 22%,
      color-mix(in oklch, var(--degrade-bleu) 55%, transparent) 0%,
      transparent 70%),
    radial-gradient(ellipse 30% 24% at 51% 41%,
      color-mix(in oklch, var(--degrade-bleu) 66%, transparent) 0%,
      transparent 75%);
  animation-name: reflet-cote-droit;
  animation-delay: -33.2s;
}

.c3-reflets::after {
  /* la source est a GAUCHE : le froid glisse sur la face gauche et le
     flanc gauche du dessus */
  background:
    radial-gradient(ellipse 50% 44% at 2% 22%,
      color-mix(in oklch, var(--degrade-bleu) 72%, transparent) 0%,
      color-mix(in oklch, var(--degrade-bleu) 30%, transparent) 46%,
      transparent 78%),
    radial-gradient(ellipse 46% 26% at 22% 14%,
      color-mix(in oklch, var(--degrade-bleu) 55%, transparent) 0%,
      transparent 76%);
  animation-name: reflet-cote-gauche;
  animation-delay: -33.2s;
}

/* LE CHAUD SUIT LA SIENNE (retard -99.5s) */
.c3-reflets-chaud::before {
  background:
    radial-gradient(ellipse 44% 30% at 66% 40%,
      color-mix(in oklch, var(--degrade-orange) 55%, transparent) 0%,
      color-mix(in oklch, var(--degrade-orange) 24%, transparent) 48%,
      transparent 78%),
    radial-gradient(ellipse 32% 26% at 100% 30%,
      color-mix(in oklch, var(--degrade-orange) 42%, transparent) 0%,
      transparent 72%);
  animation-name: reflet-cote-droit;
  animation-delay: -99.5s;
}

.c3-reflets-chaud::after {
  background:
    radial-gradient(ellipse 46% 40% at 4% 30%,
      color-mix(in oklch, var(--degrade-orange) 50%, transparent) 0%,
      color-mix(in oklch, var(--degrade-orange) 21%, transparent) 46%,
      transparent 76%),
    radial-gradient(ellipse 40% 24% at 24% 18%,
      color-mix(in oklch, var(--degrade-peche) 45%, transparent) 0%,
      transparent 74%);
  animation-name: reflet-cote-gauche;
  animation-delay: -99.5s;
}

/* les fenetres d'opacite : l'orbite demarre a DROITE (0%), passe DERRIERE
   (25%), a GAUCHE (50%), DEVANT (75%). Le reflet vit quand sa source est de
   son cote, s'eteint quand elle est derriere ou devant. */
@keyframes reflet-cote-droit {
  0%        { opacity: 0.89; }
  21%       { opacity: 0.13; }
  34%, 68%  { opacity: 0; }
  82%       { opacity: 0.21; }
  100%      { opacity: 0.89; }
}

@keyframes reflet-cote-gauche {
  0%, 26%   { opacity: 0; }
  42%       { opacity: 0.34; }
  50%       { opacity: 0.89; }
  63%       { opacity: 0.21; }
  76%, 100% { opacity: 0; }
}

/* Arêtes spéculaires, discrètes comme dans la référence. Sélecteurs préfixés
   .cube3 : la règle `.cube3 b { inset: 0 }` est plus spécifique qu'une classe
   seule et écrasait left/top — les fils se retrouvaient collés en (0,0). */
.cube3 .c3-arete {
  inset: auto;
  background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.42), transparent);
}

.cube3 .c3-arete-avant {
  left: calc(50% - 1px);
  top: 40%;
  width: 2px;
  height: 60%;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.45), transparent 87%);
}

.cube3 .c3-arete-gauche,
.cube3 .c3-arete-droite {
  left: 50%;
  top: calc(40% - 1px);
  width: 53.85%;
  height: 2px;
  transform-origin: 0 50%;
}

.cube3 .c3-arete-gauche { transform: rotate(-158.2deg); }
.cube3 .c3-arete-droite { transform: rotate(-21.8deg); }

/* La brume DEVANT le pied : le brouillard du monde qui avale la base. */
/* Le brouillard est OPAQUE en son cœur, et c'est nécessaire : un backdrop-filter
   ignore le mask-image posé sur son propre élément (Chromium) — l'éclaircissement
   de la lentille continuait sous la ligne de fondu, en silhouette fantôme jusqu'au
   sommet bas. On ne peut pas fondre une lentille : on la RECOUVRE, comme dans la
   référence — la disparition y est un brouillard épais peint devant, pas une
   transparence du cube. */
/* Le BROUILLARD du pied du cube. Sa premiere version, centree sur le viewport
   et plus etroite, laissait ressortir les flancs bas du cube quand la tache
   blanche orbitait devant-bas. La nappe est donc plus large (3.8x) et son
   plateau opaque pousse jusqu'a 40% : les voiles n'apportent plus que la
   couleur, jamais la couverture.

   Le plateau a ete pousse a 50% puis REMIS a 40% : la mesure a montre qu'il n'y
   gagnait rien — la fuite venait des hexagones de decoupe des calques du
   dessus, pas de l'opacite d'ici — et il ne coutait que de la hauteur, le
   brouillard remontant trop haut sur les flancs du grand cube.

   Ancree au CUBE (--cube3-x), pas au viewport : centree sur l'ecran, le cube
   (a 62%) derivait hors du cœur opaque sur les ecrans tres larges et ses
   flancs bas reapparaissaient. Le brouillard suit l'objet qu'il avale. */
.cube3-brume {
  position: absolute;
  left: var(--cube3-x);
  /* 0.38 et non 0.26 : le halo a ete DESCENDU. A 0.26 il remontait trop haut
     sur les flancs du grand cube et le noyait a mi-hauteur ; a 0.38 le cube
     emerge, et la base reste avalee (mesure ci-dessous). */
  top: calc(var(--cube3-y) + var(--cube3-taille) * 0.38);
  width: calc(var(--cube3-taille) * 3.8);
  height: calc(var(--cube3-taille) * 1.55);
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    var(--degrade-socle) 0%,
    var(--degrade-socle) 40%,
    color-mix(in oklch, var(--degrade-socle) 92%, transparent) 55%,
    color-mix(in oklch, var(--degrade-socle) 55%, transparent) 68%,
    color-mix(in oklch, var(--degrade-peche) 30%, transparent) 78%,
    transparent 92%);
}

/* la brume du pied du cube respire avec le socle : memes voiles, memes
   phases — un seul brouillard, une seule vie */
.cube3-brume::before,
.cube3-brume::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

.cube3-brume::before {
  background: radial-gradient(closest-side,
    color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) 0%,
    color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) 40%,
    color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) 85%, transparent) 55%,
    transparent 92%);
  animation-delay: -33.2s;
}

.cube3-brume::after {
  background: radial-gradient(closest-side,
    color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) 0%,
    color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) 40%,
    color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) 85%, transparent) 55%,
    transparent 92%);
  animation-delay: -99.5s;
}

/* ---- LE CUBE INTERNE : l'orbe devenu verre ----

   Un second cube isometrique, glassmorphe, au centre exact du grand — meme
   projection (pentes 0.4, verticale 0.6), echelle 0.29 autour de (50%,50%).
   Pose APRES la brume (lecon durement apprise : le cœur opaque de la brume
   efface tout ce qui vit dessous).

   Deux vies s'y superposent :
   — sa TEMPETE propre : deux disques coniques contra-rotatifs (21 s / 34 s,
     transform pur) peints DERRIERE ses faces, que son verre diffuse ;
   — le GRADIENT GENERAL : les faces sont des backdrop-filter qui saturent
     fortement ce qu'elles traversent — l'aurore du fond, le grand cube, la
     brume passent A TRAVERS le petit verre.

   Le conteneur reste VIERGE (ni mask, ni clip, ni opacity) : un ancetre
   filtre aveuglerait les backdrop-filter des enfants (backdrop root). */
.cube3-orbe {
  position: absolute;
  left: var(--cube3-x);
  top: var(--cube3-y);
  width: var(--cube3-taille);
  height: var(--cube3-taille);
  transform: translate(-50%, -50%);
  pointer-events: none;
  /* IMMOBILE : aucune respiration, aucun pouls — un objet de verre pose. */
}

.cube3-orbe b,
.cube3-orbe i {
  position: absolute;
  inset: 0;
  display: block;
}

/* La tempete : clipee a la silhouette hexagonale du petit cube. */
.o-tempete {
  clip-path: polygon(50% 35.5%, 64.5% 41.3%, 64.5% 58.7%, 50% 64.5%, 35.5% 58.7%, 35.5% 41.3%);
  opacity: 0.76;
}

/* Les deux disques : coniques sans couture (meme couleur a 0% et 100%),
   toutes les teintes derivees de --orbe-teinte — le scroll fait tourner la
   palette de la tempete comme celle du reste. Teintes VOISINES, chroma bas :
   un remous laiteux, pas un arc-en-ciel. */
/* Les disques DEBORDENT de la silhouette du petit cube : leur bord circulaire
   n'existe plus a l'ecran — c'est l'hexagone du clip qui coupe, et un masque
   radial doux + un flou fondent l'interieur. Desaxes l'un de l'autre et
   elliptiques : aucune sphere lisible, un remous. */
.cube3-orbe .o-tempete i {
  inset: auto;
  border-radius: 50%;
  will-change: transform;
  filter: blur(8px);
  -webkit-mask-image: radial-gradient(closest-side, #000 21%, rgba(0,0,0,0.55) 55%, transparent 95%);
  mask-image: radial-gradient(closest-side, #000 21%, rgba(0,0,0,0.55) 55%, transparent 95%);
  transition: --orbe-teinte 987ms cubic-bezier(0.2, 0.6, 0.3, 1);
}

.cube3-orbe .o-tempete i:first-child {
  left: 26%;
  top: 29%;
  width: 46%;
  height: 42%;
  background: conic-gradient(from 0deg,
    oklch(0.80 0.09 var(--orbe-teinte) / 0.62) 0%,
    transparent 26%,
    oklch(0.88 0.06 calc(var(--orbe-teinte) + 34deg) / 0.55) 42%,
    transparent 63%,
    oklch(0.76 0.10 calc(var(--orbe-teinte) - 21deg) / 0.55) 79%,
    oklch(0.80 0.09 var(--orbe-teinte) / 0.62) 100%);
  animation: tempete-sens 21s linear infinite;
}

.cube3-orbe .o-tempete i:last-child {
  left: 31%;
  top: 37%;
  width: 36%;
  height: 32%;
  background: conic-gradient(from 180deg,
    oklch(0.92 0.04 calc(var(--orbe-teinte) + 13deg) / 0.55) 0%,
    transparent 34%,
    oklch(0.84 0.07 calc(var(--orbe-teinte) + 47deg) / 0.45) 55%,
    transparent 76%,
    oklch(0.92 0.04 calc(var(--orbe-teinte) + 13deg) / 0.55) 100%);
  animation: tempete-contre 34s linear infinite;
}

@keyframes tempete-sens { to { transform: rotate(360deg); } }
@keyframes tempete-contre { to { transform: rotate(-360deg); } }

/* Les trois faces de verre : lentilles discretes — le gradient general passe
   a travers le petit cube. Geometrie homothetique du grand (echelle 0.29,
   sommet avant du mini : (50%, 47.1%)). */
.o-haut {
  clip-path: polygon(50% 35.5%, 64.5% 41.3%, 50% 47.1%, 35.5% 41.3%);
  -webkit-backdrop-filter: blur(13px) brightness(1.13) saturate(1.13);
  backdrop-filter: blur(13px) brightness(1.13) saturate(1.13);
  background: linear-gradient(115deg, rgba(255,255,255,0.21), rgba(255,255,255,0.05));
}

.o-gauche {
  clip-path: polygon(35.5% 41.3%, 50% 47.1%, 50% 64.5%, 35.5% 58.7%);
  -webkit-backdrop-filter: blur(21px) brightness(1.18) saturate(1.08);
  backdrop-filter: blur(21px) brightness(1.18) saturate(1.08);
  background: linear-gradient(20deg, rgba(255,255,255,0.13), rgba(255,255,255,0.03));
}

.o-droite {
  clip-path: polygon(50% 47.1%, 64.5% 41.3%, 64.5% 58.7%, 50% 64.5%);
  -webkit-backdrop-filter: blur(21px) brightness(1.08) saturate(1.13);
  backdrop-filter: blur(21px) brightness(1.08) saturate(1.13);
  background: linear-gradient(250deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02));
}

/* ---- Le TUNNEL typographique : « graphen » ecrit en 3D dans le petit cube.

   Le mecanisme de la reference : les quatre parois interieures d'un couloir
   qui fuit vers le fond, tapissees du mot repete — plafond a l'envers, murs
   lateraux le long de la fuite — et le mot seul, plein, sur le fond. La
   perspective CSS fait les cadres emboites toute seule.

   Structure (batie par orbe.js, dimensionnee en px sur la taille du cube) :
   .o-tunnel (clip a la silhouette du petit cube — il flatte son sous-arbre,
   c'est legal : aucun backdrop-filter dedans)
     > .o-tunnel-scene (perspective)
       > .o-tunnel-monde (preserve-3d, legere inclinaison vers l'angle iso)
         > 4 .o-paroi + .o-tunnel-fond */
.o-tunnel {
  position: absolute;
  inset: 0;
  clip-path: polygon(50% 35.5%, 64.5% 41.3%, 64.5% 58.7%, 50% 64.5%, 35.5% 58.7%, 35.5% 41.3%);
}

.o-tunnel-scene {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  perspective: 890px;
  perspective-origin: 50% 50%;
}

.o-tunnel-monde {
  position: relative;
  transform-style: preserve-3d;
  /* un soupcon de l'angle du cube : le tunnel appartient au volume */
  transform: rotateX(-6deg) rotateY(-10deg);
}

.o-paroi {
  position: absolute;
  overflow: hidden;
  color: rgba(255, 255, 255, 0.83);
  font-family: var(--font-main);
  font-weight: var(--fw-extrabold);
  line-height: 1.15;
  white-space: nowrap;
  text-align: center;
  -webkit-user-select: none;
  user-select: none;
}

/* le plafond porte le mot A L'ENVERS, comme la reference */
.o-paroi-haut i {
  display: block;
  transform: rotate(180deg);
}

.o-tunnel-fond {
  position: absolute;
  display: grid;
  place-items: center;
  color: rgba(255, 255, 255, 0.92);
  font-family: var(--font-main);
  font-weight: var(--fw-extrabold);
  -webkit-user-select: none;
  user-select: none;
}

/* La MASSE du petit verre : un cœur laiteux dense au centre et un anneau
   d'absorption contre les parois — clipes a la silhouette, pas aux faces. */
.o-interieur {
  clip-path: polygon(50% 35.5%, 64.5% 41.3%, 64.5% 58.7%, 50% 64.5%, 35.5% 58.7%, 35.5% 41.3%);
  background:
    radial-gradient(11% 10% at 50% 50%, rgba(255, 255, 255, 0.13) 0%, rgba(255, 255, 255, 0.05) 55%, transparent 80%),
    radial-gradient(15% 14% at 50% 50%,
      transparent 55%,
      color-mix(in oklch, var(--degrade-peche) 13%, transparent) 78%,
      color-mix(in oklch, var(--degrade-gris) 21%, transparent) 96%);
}

/* ---- LA NAPPE INTERNE : un PLAN de brouillard dans le volume ----

   La surface de la nappe est un plan horizontal a mi-hauteur du volume.
   En projection isometrique un plan horizontal n'est JAMAIS une ligne
   horizontale : sa trace est un LOSANGE — bord proche en V (pointe basse
   sur l'arete avant, a 78%), bord lointain en Λ derriere le petit cube,
   flancs qui rejoignent les coins lateraux du volume a (0,57.8%) et
   (100%,57.8%) — plan a 0.37 de la hauteur du volume. Toutes les lignes
   d'eau suivent la pente des aretes (0.4, soit ±21.8 degres) — une ligne
   horizontale trahirait la 2D.

   Deux calques la construisent autour du petit cube :
   — .cube3-brume-interne, peint AVANT le petit cube : TOUTE la nappe
     (surface + masse) vue derriere lui — a travers son verre, le
     brouillard se lit en backdrop-filter : le bas du petit cube baigne
     DEDANS ;
   — .cube3-orbe-pied, peint apres : le col de brume DEVANT le petit cube,
     sous SA ligne d'eau (parallele a ses propres aretes basses), qui
     avale son pied.

   Les deux respirent avec le brouillard de la page : memes voiles
   souffle-brume, memes retards, memes melanges — une seule vie.

   Le bord lointain en Λ est une INTERSECTION de deux demi-plans pentus :
   la ligne gauche vit dans le background, la ligne droite dans le mask
   (les couches d'un background s'additionnent, elles ne s'intersectent
   pas). Son sommet est triche vers le bas (54% au lieu du 38% geometrique,
   pente 0.08) : la triche est cachee derriere le petit cube, et une nappe
   qui s'affaisse au loin respire mieux qu'un plan rigide. Des renflements
   creves dans le mask et dans le background font gonfler la crete.

   Au-dessus de la surface, l'air n'est pas net : une COLONNE de brume
   residuelle remplit tout le volume — ~50% juste au-dessus de la crete,
   decroissance forte en montant, ZERO vers 22% de hauteur (la partie
   haute du volume est claire). Deux fades cooperent : une couche de fond
   verticale (la densite voulue) et une troisieme couche de mask (qui
   rouvre le mask au-dessus de la crete, lui qui coupait tout) — sous la
   crete leur produit vaut 1, la nappe pleine ne change pas. */
.cube3-brume-interne {
  position: absolute;
  left: var(--cube3-x);
  top: var(--cube3-y);
  width: var(--cube3-taille);
  height: var(--cube3-taille);
  transform: translate(-50%, -50%);
  pointer-events: none;
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  /* Les couches d'un masque s'ADDITIONNENT (mask-composite: add) : il ne suffit
     pas qu'UNE d'elles se ferme sous la ligne d'eau, il faut que TOUTES s'y
     ferment — une seule couche restee ouverte repeint l'hexagone en entier et
     la base du cube reapparait. La premiere devient donc une BANDE (elle
     s'ouvre a 57%, se referme a 64%) au lieu d'un demi-plan ouvert jusqu'en
     bas, et la troisieme, qui montait depuis le bas de la boite, ne demarre
     plus qu'a 36% de hauteur (soit 64% depuis le haut). Sous la ligne d'eau la
     nappe ne peint plus rien : le plateau opaque de .cube3-brume y suffit. */
  -webkit-mask-image:
    linear-gradient(184.6deg, transparent 50%, #000 57%, #000 60%, transparent 64%),
    radial-gradient(ellipse 8% 3.5% at 78% 56.5%, #000 0%, #000 55%, transparent 100%),
    linear-gradient(to top, transparent 36%, #000 42%, transparent 76%);
  mask-image:
    linear-gradient(184.6deg, transparent 50%, #000 57%, #000 60%, transparent 64%),
    radial-gradient(ellipse 8% 3.5% at 78% 56.5%, #000 0%, #000 55%, transparent 100%),
    linear-gradient(to top, transparent 36%, #000 42%, transparent 76%);
  background:
    /* la crete eclairee : la lumiere du dessus accroche le haut de la
       nappe, derriere le petit cube */
    radial-gradient(ellipse 28% 5.5% at 50% 56%,
      rgba(255, 255, 255, 0.10) 0%,
      transparent 80%),
    /* renflement du flanc gauche (le droit est creve dans le mask) */
    radial-gradient(ellipse 9% 4% at 20% 56.5%,
      color-mix(in oklch, var(--degrade-socle) var(--nappe-densite), transparent) 0%,
      color-mix(in oklch, var(--degrade-socle) var(--nappe-densite), transparent) 55%,
      transparent 100%),
    /* la ligne GAUCHE du bord lointain : demi-plan pentu, TRANSLUCIDE
       dessous — on voit a travers la nappe */
    linear-gradient(175.4deg,
      transparent 49%,
      color-mix(in oklch, var(--degrade-socle) calc(var(--nappe-densite) * 0.85), transparent) 53%,
      color-mix(in oklch, var(--degrade-socle) var(--nappe-densite), transparent) 57%),
    /* la colonne de brume residuelle au-dessus de la surface */
    linear-gradient(to top,
      color-mix(in oklch, var(--degrade-socle) calc(var(--nappe-densite) * 0.65), transparent) 42%,
      transparent 78%);
}

/* les voiles heritent du mask du parent : ils respirent DANS la nappe */
.cube3-brume-interne::before,
.cube3-brume-interne::after {
  content: '';
  position: absolute;
  inset: 0;
  /* au-dessus des volutes : la respiration teinte AUSSI la fumee — la
     nappe garde une seule couleur, celle du brouillard principal */
  z-index: 1;
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

/* teintes a 40% comme les voiles de .cube3-brume — PAS 45% comme le socle
   de page : la nappe se pose sur le brouillard du cube, chaque ecart de
   melange la faisait changer de couleur a chaque pic de respiration */
.cube3-brume-interne::before {
  background:
    linear-gradient(175.4deg,
      transparent 49%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) calc(var(--nappe-densite) * 0.85), transparent) 53%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) var(--nappe-densite), transparent) 57%),
    /* la colonne residuelle respire aussi */
    linear-gradient(to top,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) calc(var(--nappe-densite) * 0.65), transparent) 42%,
      transparent 78%);
  animation-delay: -33.2s;
}

.cube3-brume-interne::after {
  background:
    linear-gradient(175.4deg,
      transparent 49%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) calc(var(--nappe-densite) * 0.85), transparent) 53%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) var(--nappe-densite), transparent) 57%),
    linear-gradient(to top,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) calc(var(--nappe-densite) * 0.65), transparent) 42%,
      transparent 78%);
  animation-delay: -99.5s;
}

/* Le BROUILLARD reprend ses droits : le petit cube vit DANS la brume du
   grand — un voile du meme socle se repose donc sur sa moitie basse, pose
   apres lui. Sans ce voile, il flottait DEVANT le brouillard : impossible. */
.cube3-orbe-brume {
  position: absolute;
  left: var(--cube3-x);
  top: var(--cube3-y);
  width: var(--cube3-taille);
  height: var(--cube3-taille);
  transform: translate(-50%, -50%);
  pointer-events: none;
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  /* Ce calque MEURT dans la brume, et il doit mourir AU-DESSUS de la ligne
     d'eau (57.8%). Le masque descendait a 84% : entre la ligne d'eau et les
     coins bas du volume (80%), son hexagone de decoupe re-dessinait tout seul
     la base du cube que le brouillard avait dissoute — mesure a l'appui, le
     eteindre supprimait la moitie de la fuite. La chute est donc ramenee de
     58/72/84% a 48/54/59% : plus rien de decoupe en hexagone ne se peint sous
     la ligne d'eau, ou seul le plateau opaque de .cube3-brume regne. */
  -webkit-mask-image: linear-gradient(to bottom, #000 48%, rgba(0,0,0,0.5) 54%, transparent 59%);
  mask-image: linear-gradient(to bottom, #000 48%, rgba(0,0,0,0.5) 54%, transparent 59%);
  background:
    /* le BROUILLARD GRADIENT dans la masse : une nappe teintee (peche vers
       socle) posee devant le petit cube — il s'estompe dans la couleur du
       monde, pas seulement dans du blanc */
    radial-gradient(38% 36% at 50% 48%,
      color-mix(in oklch, var(--degrade-peche) 42%, transparent) 0%,
      color-mix(in oklch, var(--degrade-socle) 29%, transparent) 55%,
      transparent 80%),
    /* la densite Beer-Lambert DEVANT le petit cube : au centre du volume,
       l'epaisseur de verre entre l'œil et l'inclusion est maximale */
    radial-gradient(34% 34% at 50% 50%,
      rgba(255, 255, 255, 0.16) 0%,
      rgba(255, 255, 255, 0.06) 50%,
      transparent 76%),
    /* le GIVRE du grand cube, re-pose par-dessus le petit : c'est
       l'epaisseur de verre entre l'œil et l'objet piege (la brume du pied,
       elle, vit dans .cube3-orbe-pied) */
    linear-gradient(115deg,
      rgba(255, 255, 255, 0.13),
      rgba(255, 255, 255, 0.06) 60%,
      rgba(255, 255, 255, 0.02));
}


/* Le COL de brume devant le petit cube : son pied disparait TOTALEMENT
   dedans — le mecanisme exact de la base du grand cube (on ne fond pas
   l'objet, on le recouvre). Sa ligne d'eau est le V du plan sur le petit
   cube : pointe basse sur SON arete avant (63.6%), remontant a 0.4 de
   pente vers ses coins bas (35.5%, 57.8%) et (64.5%, 57.8%) — parallele a ses
   aretes basses, comme toute ligne d'eau isometrique. L'union des deux
   demi-plans pentus donne exactement le V (chaque ligne prolongee passe
   SOUS l'autre). Le mask HORIZONTAL confine le col a la largeur du petit
   cube : partout ailleurs, la nappe vit deja dans .cube3-brume-interne —
   aucun trou, sa crete y est plus haute que ce col. Des billows a cheval
   sur la ligne cassent sa rigidite. Meme couleur que le brouillard de la
   page : la couture est invisible.

   Sa COTE n'a pas bouge. Seule son OPACITE a change : le col est passe a
   --col-densite (100%). A 55% le pied se devinait a travers, et ce qu'on en
   devinait dependait du gradient qui tourne dessous — il y avait donc des
   moments ou le bas du cube reapparaissait. Un col opaque ne depend plus de
   rien, et il n'ajoute pas un millimetre de hauteur. */
.cube3-orbe-pied {
  position: absolute;
  left: var(--cube3-x);
  top: var(--cube3-y);
  width: var(--cube3-taille);
  height: var(--cube3-taille);
  transform: translate(-50%, -50%);
  pointer-events: none;
  clip-path: polygon(50% 0, 100% 20%, 100% 80%, 50% 100%, 0 80%, 0 20%);
  -webkit-mask-image: linear-gradient(to right,
    transparent 26%, #000 35%, #000 65%, transparent 74%);
  mask-image: linear-gradient(to right,
    transparent 26%, #000 35%, #000 65%, transparent 74%);
  background:
    /* le sheen : la lumiere glisse sur la surface du col */
    radial-gradient(ellipse 26% 5% at 46% 61%,
      rgba(255, 255, 255, 0.07) 0%,
      transparent 80%),
    /* trois billows a cheval sur la ligne d'eau */
    radial-gradient(ellipse 12% 4.5% at 36% 59%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 0%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 55%,
      transparent 100%),
    radial-gradient(ellipse 13% 5% at 63% 59.5%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 0%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 55%,
      transparent 100%),
    radial-gradient(ellipse 15% 5% at 50% 63%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 0%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 55%,
      transparent 100%),
    /* la ligne d'eau GAUCHE du V */
    linear-gradient(201.8deg,
      transparent 54.5%,
      color-mix(in oklch, var(--degrade-socle) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 62%),
    /* la ligne d'eau DROITE du V */
    linear-gradient(158.2deg,
      transparent 54.5%,
      color-mix(in oklch, var(--degrade-socle) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, var(--degrade-socle) var(--col-densite), transparent) 62%);
}

/* Cette nappe DOIT respirer avec le brouillard de la page : restee socle
   fige, elle se demarquait au pic de chaque voile et sa decoupe hexagonale
   re-dessinait la base du cube. Memes formes, teintes des voiles, memes
   phases. */
.cube3-orbe-pied::before,
.cube3-orbe-pied::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1; /* au-dessus des volutes, comme pour la nappe */
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

/* ---- Les VOLUTES : la nappe vit comme de la fumee ----

   Le CSS ne sait pas etre aleatoire ; la fumee si. L'illusion vient de la
   superposition : cinq volutes, trois trajets erratiques (waypoints
   irreguliers, jamais equidistants), des periodes INCOMMENSURABLES de la
   suite (34, 55, 89 s) et des departs decales — le motif combine ne se
   repete qu'au PPCM, plusieurs heures. Chaque volute derive, gonfle et se
   dissout (transform + opacity : compositeur pur, zero repaint), et herite
   du mask de son parent : la fumee meurt d'elle-meme a la crete de la
   nappe, elle ne la depasse jamais. Enfants reels (les pseudos portent
   deja les voiles), sous les voiles (z-index) : toujours teintes comme le
   brouillard principal.

   Cinq volutes : trois dans la nappe interne, deux dans le col du petit cube. */
.cube3-brume-interne b,
.cube3-orbe-pied b {
  position: absolute;
  display: block;
  opacity: 0.5;
  background: radial-gradient(closest-side,
    color-mix(in oklch, var(--degrade-socle) var(--nappe-densite), transparent) 0%,
    color-mix(in oklch, var(--degrade-socle) calc(var(--nappe-densite) * 0.55), transparent) 55%,
    transparent 100%);
  will-change: transform, opacity;
}

.cube3-brume-interne b:nth-of-type(1) { left: 14%; top: 51%; width: 34%; height: 11%; animation: volute-a 55s ease-in-out -21s infinite; }
.cube3-brume-interne b:nth-of-type(2) { left: 42%; top: 49%; width: 28%; height: 9%;  animation: volute-b 34s ease-in-out -13s infinite; }
.cube3-brume-interne b:nth-of-type(3) { left: 56%; top: 52%; width: 32%; height: 10%; animation: volute-c 89s ease-in-out -34s infinite; }
.cube3-orbe-pied b:nth-of-type(1)     { left: 30%; top: 57%; width: 30%; height: 9%;  animation: volute-c 34s ease-in-out -21s infinite; }
.cube3-orbe-pied b:nth-of-type(2)     { left: 42%; top: 59%; width: 34%; height: 10%; animation: volute-b 55s ease-in-out -8s infinite; }

/* trois errances : aucun waypoint regulier, aucun aller-retour lisible */
@keyframes volute-a {
  0%, 100% { transform: translate3d(-13%, 5%, 0) scale(0.89); opacity: 0; }
  13%      { opacity: 0.55; }
  34%      { transform: translate3d(21%, -8%, 0) scale(1.13); opacity: 0.34; }
  55%      { transform: translate3d(34%, 8%, 0) scale(0.97); opacity: 0.55; }
  76%      { transform: translate3d(8%, -13%, 0) scale(1.21); opacity: 0.21; }
  89%      { opacity: 0.47; }
}

@keyframes volute-b {
  0%, 100% { transform: translate3d(13%, -5%, 0) scale(1.08); opacity: 0; }
  21%      { transform: translate3d(-13%, 3%, 0) scale(0.92); opacity: 0.50; }
  47%      { transform: translate3d(-29%, -8%, 0) scale(1.13); opacity: 0.26; }
  68%      { transform: translate3d(-5%, 8%, 0) scale(1.00); opacity: 0.55; }
  84%      { opacity: 0.18; }
}

@keyframes volute-c {
  0%, 100% { transform: translate3d(0, 8%, 0) scale(0.95); opacity: 0; }
  18%      { opacity: 0.42; }
  42%      { transform: translate3d(18%, -5%, 0) scale(1.18); opacity: 0.55; }
  63%      { transform: translate3d(-8%, -11%, 0) scale(1.05); opacity: 0.29; }
  81%      { transform: translate3d(-21%, 3%, 0) scale(0.90); opacity: 0.50; }
}
/* teintes a 40%, comme les voiles de .cube3-brume (voir la nappe) */
.cube3-orbe-pied::before {
  background:
    linear-gradient(201.8deg,
      transparent 54.5%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) var(--col-densite), transparent) 62%),
    linear-gradient(158.2deg,
      transparent 54.5%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) var(--col-densite), transparent) 62%);
  animation-delay: -33.2s;
}

.cube3-orbe-pied::after {
  background:
    linear-gradient(201.8deg,
      transparent 54.5%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) var(--col-densite), transparent) 62%),
    linear-gradient(158.2deg,
      transparent 54.5%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) calc(var(--col-densite) * 0.88), transparent) 58%,
      color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) var(--col-densite), transparent) 62%);
  animation-delay: -99.5s;
}



/* Scrim de lisibilité, en remplacement de la vignette. La vignette était un
   dégradé radial elliptique assombrissant les bords : elle dessinait elle-même
   un ovale clair au centre de l'écran, quel que soit le dégradé posé dessous.
   Celui-ci est LINÉAIRE — un biais sombre depuis le bord gauche, sous la colonne
   de texte, qui s'éteint avant le milieu de l'écran.

   Un élément réel et non un pseudo-élément : il doit se peindre AU-DESSUS du
   champ, or `::before` s'insère avant les enfants et passerait dessous. */
.scrim {
  position: absolute;
  inset: 0;
  display: block;
  background: linear-gradient(
    100deg,
    rgba(0, 0, 0, var(--scrim-force)) 0%,
    rgba(0, 0, 0, 0) var(--scrim-etendue)
  );
}

/* Le grain se pose SUR le dégradé, dans la même couche : `mix-blend-mode` se
   mélange donc au champ, pas au reste de la page.

   Il est indispensable ici : un dégradé CSS est parfaitement lisse, et un
   dégradé lisse en plein écran révèle le *banding* — ces bandes de transition
   visibles sur les écrans 8 bits. Le grain les casse, et il est statique donc
   gratuit par image.

   `soft-light` et non `overlay` : la formule d'overlay bascule en *screen* dès
   que le fond dépasse 50% de luminosité, ce qui écrase le bruit contre le blanc. */
.fond::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: var(--grain-opacite);
  mix-blend-mode: soft-light;
  background-size: var(--grain-taille) var(--grain-taille);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='2.5' intercept='-0.75'/%3E%3CfeFuncG type='linear' slope='2.5' intercept='-0.75'/%3E%3CfeFuncB type='linear' slope='2.5' intercept='-0.75'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");
}

.sidebar {
  position: relative;
  z-index: 1;
}

/* Largeur non imposée mais déduite du contenu : le retrait est identique à
   gauche et à droite, donc le cube est à la même distance du filet que le logo
   l'est du bord de l'écran. Une largeur fixe casserait cette symétrie.

   La colonne a une largeur EXPLICITE, `--titre-largeur`. Auparavant elle valait
   `min-content`, ce qui la calait sur le mot le plus long du titre — 139px, donc
   trois lignes. `min-content` ignore un `max-width` posé sur l'élément : il n'y
   avait pas moyen d'élargir le titre sans fixer la colonne.

   Ce que cette largeur coûtait avant, elle ne le coûte plus : la contrainte de
   symétrie qui imposait une largeur déduite du contenu portait sur la distance
   entre le cube et le filet. Les deux ont disparu. */
/* LE PANNEAU. Opaque, craie, sur la petite part d'or (38.2 vw), toute la
   hauteur : il COUPE la scene — qui reste le fond plein ecran qu'elle a
   toujours ete — et porte tout le texte. Une grille a cinq rangees :
   le logotype en haut, le h1 qui commence au tiers d'or (la premiere rangee
   fait exactement la hauteur qui l'y amene), un ressort, puis le paragraphe et
   « Scroll » en bas, sur la ligne de la barre (--hero-bas). Le retrait droit
   est plus court que le gauche : le bord droit du panneau est le mur, pas le
   bord de l'ecran. */
.sidebar {
  flex: 0 0 auto;
  width: var(--panneau-largeur);
  box-sizing: border-box;
  display: grid;
  grid-template-rows: calc(var(--hero-titre) - var(--hero-haut)) auto 1fr auto auto;
  align-content: stretch;
  padding: var(--hero-haut) var(--rail-gap) var(--hero-bas) var(--brand-inset);
  background-color: var(--degrade-craie);
}

/* Le logotype : du vrai texte, 600 — l'exception assumee au jeu de graisses,
   c'est celle du logotype d'origine. « graphen » en encre, « studio » en gris
   studio. */
.logo {
  margin: 0;
  align-self: start;
  font-size: var(--brand-size);
  line-height: var(--f-21);
  font-weight: 600;
  letter-spacing: var(--ls-snug);
  white-space: nowrap;
}

.logo-graphen { color: var(--logo-encre); }
.logo-studio  { color: var(--color-brand-studio); }

/* Le titre ne reserve plus trois lignes : sa rangee est `auto`. */
.sidebar .hero-titre {
  min-height: 0;
  color: var(--color-brand-studio);
}

/* Le paragraphe, en corps de lecture (21/34) et non plus en corps de titre :
   la colonne est etroite (38.2 vw moins les retraits), le corps de titre y
   ferait dix lignes. */
.hero-texte {
  grid-row: 4;
  margin: 0 0 var(--f-55);
  font-size: var(--text-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-snug);
  color: var(--color-brand-studio);
}

.hero-scroll {
  grid-row: 5;
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-wide);
  color: var(--color-brand-studio);
}

/* Le scrim — le voile sombre qui rendait le texte lisible sur la scene — n'a
   plus d'objet : le texte est sur le panneau. */
.scrim {
  display: none;
}

/* Palier tablette : le panneau prend toute la largeur ; le mur est derriere,
   on ne le voit plus au repos. */
@media (max-width: 987px) {
  .sidebar {
    width: 100%;
  }
}


.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  gap: var(--grid-gap);
}

/* Partitions dorees : 8/13 et 5/13 */
.col-major {
  grid-column: span 8;
}

.col-minor {
  grid-column: span 5;
}

.col-full {
  grid-column: 1 / -1;
}

/* Sous 610px les deux colonnes n'ont plus la place de cohabiter : le rail
   passe au-dessus du canevas et son filet bascule de droite en bas. */
@media (max-width: 610px) {
  .shell {
    flex-direction: column;
  }

  /* La colonne passe en `1fr` : le rail occupe toute la largeur, le titre n'a
     plus de raison d'être replié sur la largeur du lockup. */
  .sidebar {
    flex: none;
    grid-template-columns: 1fr;
    padding-block: var(--f-21);
  }

  .canvas {
    flex: 1 1 auto;
  }
}

/* ---- LE BANDEAU DU BAS : LE H1 ----

   Le titre de la page n'est pas une ligne : c'est une COLONNE de quatre
   lignes dans le coin bas gauche — le lieu, puis les trois metiers. Google
   lit les morceaux dans l'ordre du code, ou ils forment une phrase (« graphen
   studio, base a Lille, webdesign, contenu photo et video, referencement SEO
   et GEO ») ; l'oeil, lui, voit quatre lignes courtes. Deux lectures, un seul
   element. Les mots de categorie (« contenu », « referencement ») et la
   ponctuation sont en .lu-non-vu : dans la phrase, pas a l'ecran.

   Calee sur le bas de la barre, au retrait de gauche — le pendant de
   l'horloge en haut a gauche. pointer-events: none — c'est du texte, pas
   une commande. */
.bandeau {
  position: fixed;
  /* La regle globale des h1 reserve deux lignes de titre (min-height 94) —
     ici le titre n'est pas une ligne : sans cette remise a zero, la boite
     garde 94px et le texte remonte au-dessus de la place voulue (mesure).
     Meme correctif que .hero-titre. */
  min-height: 0;
  left: var(--menu-inset);
  bottom: var(--barre-bas);
  z-index: 5;
  margin: 0;
  /* En COLONNE FLEX, pas en bloc : les noeuds de texte (espaces, morceaux
     .lu-non-vu) ne forment pas de lignes vides au corps du h1 — un conteneur
     flex les ignore ; l'espace reste dans le contenu, il ne prend pas de place. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* L'encre de la SCENE, pas le blanc general : le bandeau est posé sur le
     décor, il n'a pas de fond à lui. Le repli en blanc sert aux pages qui
     n'ont pas de scène claire. */
  /* L'encre SUIT LE BLANCHIMENT du fond. Le bandeau est fixe : il reste a
     l'ecran jusqu'au pied, pendant que le decor derriere lui vire au blanc.
     En blanc fixe, il disparaitrait dans le dernier tiers. --page va de 0 en
     haut a 1 en bas (ecrite par verre-statue.js). */
  color: color-mix(in oklab,
    var(--encre-scene, #fff) calc((1 - var(--page, 0)) * 100%),
    #0B1416);
  text-shadow: var(--ombre-scene);
  pointer-events: none;
}
/* Les metiers portent (medium), le lieu precise (light, attenue). Corps des
   etiquettes : la bande est une legende, pas un titre — le titre visuel,
   c'est la scene. Les majuscules de debut de ligne sont typographiques :
   le code reste en minuscules dans la phrase. */
.bandeau b::first-letter, .bandeau i::first-letter { text-transform: uppercase; }
.bandeau b {
  display: block;
  font-size: calc(var(--f-13) + var(--f-2));   /* 15 px : entre les deux crans de la suite (13, 18) — 13 etait trop petit, 18 trop gros (Quentin) */
  line-height: var(--lh-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-normal);
}
.bandeau i {
  display: block;
  font-style: normal;
  font-size: calc(var(--f-13) + var(--f-2));   /* 15 px : entre les deux crans de la suite (13, 18) — 13 etait trop petit, 18 trop gros (Quentin) */
  line-height: var(--lh-sm);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-normal);
  color: var(--encre-scene-attenuee, var(--color-text-muted));
}

/* ================= L'ENTRÉE NE SE JOUE QU'UNE FOIS =================

   La séquence d'ouverture — les étiquettes qui paraissent une à une, le
   logotype mot à mot, le titre en cascade — est une PRÉSENTATION. Une
   présentation ne se refait pas à chaque fois qu'on revient dans la pièce :
   la deuxième fois elle n'impressionne plus, elle retarde.

   DEUX RÈGLES, ET ELLES NE DISENT PAS LA MÊME CHOSE :

   1. `html.deja-vu` — la classe est posée par transition.js AVANT le premier
      rendu, dès la deuxième page de la session. Tout se montre immédiatement,
      partout. C'est ce qui fait qu'un retour à l'accueil ne rejoue pas
      l'entrée.

   2. `body:not([data-page="accueil"])` — l'EN-TÊTE ENTIER (étiquettes ET
      logotype) ne s'anime JAMAIS ailleurs que sur l'accueil, même à la toute
      première visite. Une page intérieure n'a pas de présentation à faire :
      on y arrive déjà entré. Le logotype de l'en-tête est de la partie
      (demande de Quentin) : sans cette ligne, il héritait du fondu de 3.4 s
      écrit pour le grand logo de l'accueil.

   La BARRE DE CHAT suit la règle 1 : au retour sur l'accueil, elle ne rejoue
   pas son entrée (le fondu de 900 ms de hero-eau.css) — elle est déjà là,
   comme l'en-tête.

   `animation: none` et non `animation-delay: 0` : à zéro, l'animation se
   joue quand même, en accéléré. On veut qu'elle n'existe pas. */

html.deja-vu .entete .etiquette,
html.deja-vu .logo span,
html.deja-vu .barre,
html.deja-vu .hero-texte,
html.deja-vu .hero-scroll,
html.deja-vu .sidebar .hero-titre,
html.deja-vu .sidebar .hero-titre .mot-titre,
html.deja-vu .sidebar h1 .mot-titre,
body:not([data-page="accueil"]) .entete .etiquette,
body:not([data-page="accueil"]) .entete-logo span {
  animation: none !important;
  opacity: 1 !important;
}

/* Les attentes n'ont plus lieu d'être non plus : elles servaient à retenir
   l'entrée le temps que la statue se forme. Sans entrée, rien à retenir. */
html.deja-vu.attente-statue .sidebar .hero-titre,
html.deja-vu.attente-statue .sidebar .hero-titre .mot-titre,
html.deja-vu.attente-statue .hero-scroll,
html.deja-vu.attente-video .entete .etiquette,
html.deja-vu.attente-video .logo span,
html.deja-vu.attente-video .hero-texte,
html.deja-vu.attente-video .hero-scroll,
html.deja-vu.attente-video .sidebar .hero-titre {
  animation-play-state: running !important;
  opacity: 1 !important;
}

/* ---- LA PROTECTION DES MÉDIAS ----

   Le pendant CSS de protection.js. Il ferme trois portes que le JavaScript
   ne peut pas fermer :

   — LA SÉLECTION. `user-select: none` empêche de surligner un texte pour le
     copier, et surtout empêche la sélection accidentelle qui accompagne un
     glisser sur une image.

   — L'APPUI LONG SUR iOS. `-webkit-touch-callout: none` est la SEULE façon
     de fermer le menu « Enregistrer dans Photos » de Safari mobile : il ne
     passe pas par l'événement `contextmenu`, donc le script ne le voit
     jamais.

   — LE GLISSER NATIF des images, doublé ici en CSS pour les navigateurs qui
     l'autorisent malgré `draggable="false"`.

   LES CHAMPS DE SAISIE SONT ÉPARGNÉS, et ce n'est pas un détail : sans cette
   exception, on ne pourrait plus sélectionner ni corriger ce qu'on écrit dans
   la barre de chat. Le `:where()` garde la spécificité à zéro — n'importe
   quelle règle du projet peut donc reprendre la main sans lutte. */
:where(body) {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
:where(input, textarea, select, [contenteditable]) {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
:where(img, video, svg) {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
}

/* ---- LU MAIS PAS VU ----

   Un texte retire de la VUE sans etre retire du DOCUMENT. Sa boite tombe a
   1px et se decoupe au clip-path : il reste dans le HTML servi, dans l'arbre
   d'accessibilite, et il est lu par les moteurs.

   POURQUOI PAS visibility: hidden — c'est ce qui etait pose sur .dalle-logo,
   et c'est precisement ce qui ne marche pas : visibility: hidden et
   display: none sortent le texte de l'arbre d'accessibilite ET le font
   ignorer comme contenu. Le mot etait dans le fichier, il n'etait lu par
   personne. Le decoupage a 1px, lui, laisse tout en place.

   OU L'EMPLOYER — uniquement sur du texte REELLEMENT AFFICHE a l'ecran par
   un autre moyen. Le cas d'espece : « graphen studio », que verre-statue.js
   peint dans la texture refractee derriere la statue. Un canvas ne porte ni
   alt ni couche texte ; cette classe en tient lieu. C'est un equivalent
   textuel, pas une cachette a mots-cles — cacher du texte que personne ne
   voit a l'ecran est une infraction aux regles de Google, et le referentiel
   du studio dit la meme chose autrement (« zero texte porte par les
   images »).

   CE QU'ELLE NE FAIT PAS — remplacer un titre. Un texte invisible est indexe
   mais pondere sous le texte visible : il rattrape une absence, il ne porte
   pas un positionnement.

   SANS EFFET SUR LA TEXTURE — verre-statue.js lit le textContent et le style
   CALCULE de l'element (corps, graisse, interlettrage, couleurs), jamais sa
   boite. Le reduire a 1px ne change pas d'un pixel ce qu'il dessine. */
.lu-non-vu {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- L'EN-TETE A ETIQUETTES ----

   Des PAVES NOIRS, texte blanc, coins a peine arrondis (3), corps petit :
   l'inverse du verre. C'est le contraste de matiere qui fait tenir l'ensemble — des
   instruments opaques et nets poses sur une scene floue, comme la barre en
   bas. Les pages au centre (vraiment au centre : colonnes 1fr auto 1fr, la
   premiere vide), l'acces client a droite, rien a gauche. La page courante
   est INVERSEE — clair sur sombre — pas de soulignement, pas de couleur : une
   inversion.

   La bande elle-meme ne capte rien (pointer-events: none) : elle traverse
   toute la largeur au-dessus de la scene, elle ne doit pas voler les gestes.
   Chaque etiquette reprend la main. */
.entete {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: grid;
  /* Trois colonnes dont la premiere est VIDE : c'est elle qui tient la
     navigation au centre exact de l'ecran, a egale distance des bords, meme
     sans rien a gauche. */
  grid-template-columns: 1fr auto 1fr;
  /* La colonne de gauche accueille le logotype quand la page en pose un
     (.entete-logo). Sans lui, la zone reste vide et la navigation garde son
     centrage : c'est la meme grille. */
  grid-template-areas: "logo nav droite";
  align-items: start;
  gap: var(--f-5) var(--f-13);
  padding: var(--f-13) var(--menu-inset);
  pointer-events: none;
  /* Le repli au defilement (voir .entete-replie et entete.js). */
  transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1);
}

.entete-zone {
  display: flex;
  flex-wrap: wrap;
  gap: var(--f-5);
  min-width: 0;
}

.entete-nav    { grid-area: nav; justify-content: center; }
.entete-droite { grid-area: droite; justify-content: end; }

/* LA BANDE REPLIEE : on descend, elle s'en va ; on remonte, elle revient
   (demande de Quentin). Elle se retire d'un bloc vers le haut — sa hauteur
   comprend le retrait, donc rien ne depasse. En mouvement reduit, le geste
   est instantane : la bande est la ou elle n'est pas, sans glissement. */
.entete-replie { transform: translateY(-100%); }

@media (prefers-reduced-motion: reduce) {
  .entete { transition: none; }
}

/* LE LOGOTYPE DE L'EN-TETE. Il herite de .logo (corps de marque, deux tons,
   fondu d'entree). La bande ne capte pas les gestes : le lien reprend la
   main, comme les etiquettes. */
.entete-logo {
  grid-area: logo;
  justify-content: start;
  /* Meme boite de ligne que l'etiquette : meme retrait haut, meme interligne.
     Les deux textes se posent alors sur la meme ligne optique, malgre les
     corps differents (21 pour la marque, 13 pour les paves). */
  padding-top: var(--f-2);
  line-height: var(--l-18);
  /* Un cran SOUS le corps de marque du panneau (21) : dans la bande, le
     logotype accompagne les paves, il ne les ecrase pas. */
  font-size: var(--l-18);
}
@media (max-width: 610px) {
  .entete-logo { font-size: var(--f-13); }
}
.entete-logo-lien {
  pointer-events: auto;
  color: inherit;
  text-decoration: none;
}

/* Comme la chat-barre : le premier mot du logo est blanc en source et se
   retourne automatiquement sur le fond de la section sous le header. */
.entete-logo .logo-graphen {
  color: #FFFFFF;
  mix-blend-mode: difference;
}

/* L'ETIQUETTE. Ligne de 18, padding 2 en haut et en bas : la hauteur d'une
   ligne de texte et pas plus — la reference est dense, ses paves font la
   hauteur des lettres. Mais DU VIDE A DROITE du texte (demande de Quentin,
   et c'est la reference : « Work », « Index » font 125px pour quatre lettres,
   le texte cale a gauche) : padding droit de 21 contre 5 a gauche, et les
   paves de navigation ont une largeur minimale de 123. Chiffres tabulaires :
   l'horloge bat sans trembler. */
.etiquette {
  pointer-events: auto;
  display: inline-block;
  padding: var(--f-2) var(--f-21) var(--f-2) var(--f-5);
  text-align: left;
  background-color: var(--etiquette-fond);
  color: var(--etiquette-encre);
  font-size: var(--text-sm);
  line-height: var(--l-18);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-normal);
  font-variant-numeric: tabular-nums;
  border-radius: var(--radius-xs);
  text-decoration: none;
  white-space: nowrap;
  -webkit-user-select: none;
  user-select: none;
  transition: background-color 123ms, color 123ms;
}

/* Les paves de navigation et d'acces : une largeur minimale, le texte a
   gauche, le vide a droite. */
.entete-nav .etiquette,
.entete-droite .etiquette {
  min-width: var(--etiquette-min);
}

/* La page courante (ou la section courante : « Projets » pendant qu'on est
   dans l'etagere), le survol et le focus clavier : une seule reponse,
   l'INVERSION. */
a.etiquette:hover,
a.etiquette:focus-visible {
  background-color: var(--etiquette-actif-fond);
  color: var(--etiquette-actif-encre);
  outline: none;
}
/* L'onglet COURANT reste un pave noir comme les autres (demande de Quentin) ;
   seuls le survol et le focus s'inversent. */
.etiquette[aria-current] {
  background-color: var(--etiquette-fond);
  color: var(--etiquette-encre);
}
.etiquette[aria-current]:hover,
.etiquette[aria-current]:focus-visible {
  background-color: var(--etiquette-actif-fond);
  color: var(--etiquette-actif-encre);
}

/* Palier tablette : les zones s'empilent, tout a gauche. */
@media (max-width: 987px) {
  .entete {
    grid-template-columns: 1fr;
    grid-template-areas:
      "logo"
      "nav"
      "droite";
    padding: var(--f-8) var(--menu-inset);
  }

  .entete-nav,
  .entete-droite {
    justify-content: start;
  }
}

/* ---- LA BARRE DE L'ASSISTANT : L'ETIQUETTE QUI S'ETIRE ----

   La chatbar IA du site, en bas au centre — choisie par Quentin le 23/08/2026
   parmi cinq directions (maquette-chatbar.html, variante 3), apres le refus
   d'un composer a deux rangees (« trop grande ») et des pills de suggestions.

   C'est UNE COMMANDE DE PLUS DANS LA FAMILLE DE L'EN-TETE : un pave noir de 34
   de haut, « Parler au studio » a gauche, et a droite, TOUJOURS, le carre
   craie de la fleche — ferme comme ouvert, champ vide ou non (demande de
   Quentin : la fleche est l'affordance, elle ne clignote pas selon l'etat).
   Au clic, le pave S'ETIRE de 199 a 377 (610 ms, la courbe du site) et
   devient le champ : le mot fond, le curseur blanc bat. Le Dynamic Island,
   en droit et en noir. Pas de verre, pas de flou, pas de filet : la matiere
   des instruments de ce site est le noir plein, le verre est reserve a la
   scene.

   Le FIL de conversation (.fil) : une colonne de pavés posée au-dessus de la
   barre, sans panneau — la scene est le fond. L'assistant parle en pave noir
   a gauche, le visiteur en pave craie a droite : la meme inversion que la
   page courante dans l'en-tete. Cree par assistant.js, frere de la barre,
   cale sur elle (meme gauche, meme largeur). */
.barre {
  position: fixed;
  left: 50%;
  bottom: var(--barre-bas);
  transform: translateX(-50%);
  z-index: 3;
  box-sizing: border-box;
  width: var(--barre-fermee);
  max-width: calc(100vw - 2 * var(--menu-inset));
  /* min-height, pas height : un message long fait GRANDIR le pave (le champ
     est une zone de texte sans barre de defilement — demande de Quentin).
     Le bas reste fixe (bottom), le pave pousse vers le haut. */
  min-height: var(--barre-hauteur);
  margin: 0;
  display: flex;
  align-items: flex-end;
  gap: var(--f-8);
  padding: 0 var(--f-5) 0 var(--f-13);
  background-color: var(--etiquette-fond);
  color: var(--etiquette-encre);
  /* Meme coin que les etiquettes de l'en-tete (3). */
  border-radius: var(--radius-xs);
  cursor: text;
  /* L'ETIREMENT : doux et progressif (demande de Quentin). 987 ms, et une
     courbe qui PART LENTEMENT et finit lentement — pas la courbe du site
     (0.22, 1, 0.36, 1) qui bondit au depart : sur une largeur, ce bond se
     lisait comme un coup. Le mot et le champ se croisent au milieu. */
  transition: width 987ms cubic-bezier(0.65, 0, 0.2, 1);
}

.barre[data-ouvert] {
  width: var(--barre-largeur);
  cursor: auto;
}

/* Le MOT du pave ferme. Il est pose en absolu pour que le champ, dessous,
   garde sa place : a l'ouverture l'un fond pendant que l'autre parait, sans
   que rien ne saute. */
.barre-mot {
  position: absolute;
  left: var(--f-13);
  top: 0;
  font-size: var(--text-sm);
  line-height: var(--barre-hauteur);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-normal);
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 377ms ease-out;
}
.barre[data-ouvert] .barre-mot { opacity: 0; }

/* Le CHAMP : une ligne, dans le noir. Invisible tant que le pave est ferme
   (il prend le focus a l'ouverture), il parait quand le mot a fini de
   fondre. Entree envoie. */
.barre-champ {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  /* une ligne de 21, centree dans les 34 du pave au repos (box-sizing
     border-box global : 34 = 21 de ligne + 2 x 6.5 de padding) ; chaque
     ligne de plus ajoute 21 et le pave suit (assistant.js pose la hauteur =
     scrollHeight). JAMAIS de barre de defilement : overflow cache. */
  height: var(--barre-hauteur);
  padding: calc((var(--barre-hauteur) - var(--f-21)) / 2) 0;
  resize: none;
  overflow: hidden;
  background: none;
  border: none;
  outline: none;
  color: var(--etiquette-encre);
  caret-color: var(--etiquette-encre);
  font-family: var(--font-main);
  font-size: var(--text-sm);
  line-height: var(--f-21);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-normal);
  opacity: 0;
  /* il parait pendant la seconde moitie de l'etirement */
  transition: opacity 610ms ease-in-out 377ms;
}
.barre[data-ouvert] .barre-champ { opacity: 1; }
.barre:not([data-ouvert]) .barre-champ { pointer-events: none; }

.barre-champ::placeholder {
  color: rgba(255, 255, 255, 0.45);
}

/* L'ENVOI : le carre craie de 21, la fleche noire dedans — l'inversion de
   l'en-tete, en permanence. Au survol il s'eteint un peu ; il ne change pas
   d'etat selon le champ (demande de Quentin). Pendant que l'assistant
   repond, il devient un ARRET (carre plein). */
.barre-envoi {
  flex: none;
  margin-left: auto;
  /* cale en BAS du pave (align-items: flex-end) : quand le champ grandit,
     la fleche reste sur la derniere ligne, comme dans les composers. La
     marge le centre dans les 34 du repos. */
  margin-bottom: calc((var(--barre-hauteur) - var(--f-21)) / 2);
  width: var(--f-21);
  height: var(--f-21);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background-color: var(--etiquette-actif-fond);
  border: none;
  border-radius: var(--radius-xs);
  color: var(--etiquette-actif-encre);
  cursor: pointer;
  transition: opacity 233ms;
}
.barre-envoi:hover,
.barre-envoi:focus-visible { opacity: 0.72; outline: none; }

.barre-envoi svg {
  width: var(--f-13);
  height: var(--f-13);
  display: block;
}
.barre-envoi .be-stop { display: none; }
.barre[data-occupee] .barre-envoi .be-fleche { display: none; }
.barre[data-occupee] .barre-envoi .be-stop { display: block; }

/* ---- LE FIL : la conversation en paves ----
   Pas de panneau : une colonne posee sur la scene, calee sur la barre par
   assistant.js (gauche, largeur, bas = juste au-dessus de la barre). Haut de
   377 au plus ; le corps defile, et fond sur ses 55 premiers pixels — les
   vieux messages s'effacent dans le decor au lieu de s'arreter net. */
.fil {
  position: fixed;
  z-index: 3;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  max-height: var(--f-377);
  padding: 0;
  pointer-events: none;
  font-size: var(--text-sm);
  line-height: var(--f-21);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-normal);
  animation: fil-paraitre 233ms cubic-bezier(0.22, 1, 0.36, 1);
}
.fil[hidden] { display: none; }

.fil-corps {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--f-8);
  min-height: 0;
  padding: var(--f-55) 0 var(--f-8);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  pointer-events: auto;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--f-55));
  mask-image: linear-gradient(to bottom, transparent, #000 var(--f-55));
}
.fil-corps::-webkit-scrollbar { display: none; }

@keyframes fil-paraitre {
  from { opacity: 0; transform: translateY(var(--f-8)); }
  to   { opacity: 1; transform: none; }
}

/* Le texte du fil se selectionne : c'est une reponse, on doit pouvoir la copier. */
.fil {
  -webkit-user-select: text;
  user-select: text;
}

/* Un message : un pave. L'ASSISTANT en noir a gauche (le texte a gauche, du
   vide a droite, comme les etiquettes), le VISITEUR en craie a droite. Une
   ligne de 21 par ligne de texte, 5 de marge en haut et en bas. */
.fil-message {
  max-width: 89%;
  margin: 0;
  padding: var(--f-5) var(--f-21) var(--f-5) var(--f-13);
  border-radius: var(--radius-xs);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  animation: fil-monte 377ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.fil-message[data-de="assistant"] {
  align-self: flex-start;
  background-color: var(--etiquette-fond);
  color: var(--etiquette-encre);
}
.fil-message[data-de="visiteur"] {
  align-self: flex-end;
  background-color: var(--etiquette-actif-fond);
  color: var(--etiquette-actif-encre);
}
@keyframes fil-monte {
  from { opacity: 0; transform: translateY(var(--f-8)); }
  to   { opacity: 1; transform: none; }
}

/* Les LIENS que l'assistant propose : des etiquettes inversees (craie sur le
   pave noir), a la ligne sous le texte. */
.fil-liens {
  display: flex;
  flex-wrap: wrap;
  gap: var(--f-5);
  margin: var(--f-5) 0 0;
  white-space: normal;
}
.fil-liens .etiquette {
  min-width: 0;
  padding: 0 var(--f-13) 0 var(--f-8);
  background-color: var(--etiquette-actif-fond);
  color: var(--etiquette-actif-encre);
  font-size: var(--text-sm);
  line-height: var(--f-21);
  font-weight: var(--fw-light);
}
.fil-liens .etiquette:hover,
.fil-liens .etiquette:focus-visible {
  background-color: var(--etiquette-fond);
  color: var(--etiquette-encre);
  box-shadow: inset 0 0 0 var(--border-hair) var(--etiquette-actif-fond);
}

/* L'assistant REFLECHIT : un petit pave noir avec trois points qui respirent. */
.fil-attente {
  align-self: flex-start;
  display: flex;
  gap: var(--f-5);
  height: var(--f-34);
  padding: 0 var(--f-13);
  align-items: center;
  background-color: var(--etiquette-fond);
  border-radius: var(--radius-xs);
}
.fil-attente b {
  width: var(--f-5);
  height: var(--f-5);
  border-radius: 50%;
  background-color: var(--etiquette-encre);
  opacity: 0.28;
  animation: fil-respire 1220ms ease-in-out infinite;
}
.fil-attente b:nth-child(2) { animation-delay: 203ms; }
.fil-attente b:nth-child(3) { animation-delay: 406ms; }
@keyframes fil-respire {
  0%, 100% { opacity: 0.28; }
  50%      { opacity: 1; }
}

/* Le curseur qui bat au bout du texte pendant que la reponse s'ecrit. */
.fil-message[data-ecrit]::after {
  content: "";
  display: inline-block;
  width: var(--f-2);
  height: var(--f-13);
  margin-left: var(--f-2);
  vertical-align: -2px;
  background-color: currentColor;
  animation: fil-bat 610ms steps(1) infinite;
}
@keyframes fil-bat { 50% { opacity: 0; } }

/* Les OUTILS du fil : « Effacer » et « Fermer », deux mots en 11, attenues,
   a droite, entre le dernier message et la barre. */
.fil-outils {
  display: flex;
  justify-content: flex-end;
  gap: var(--f-13);
  margin: 0;
  padding: 0 var(--f-5) var(--f-5);
  pointer-events: auto;
}
/* ---- SUR LE PIED : L'INVERSION, PIXEL PAR PIXEL ----
   Le pied est noir plein ; un pave noir dessus disparait. Mais la barre ne
   BASCULE pas d'un coup (refuse par Quentin) : elle se PARTAGE sur la ligne
   du pied a mesure qu'elle la franchit. assistant.js ecrit --sur-pied, la
   hauteur (en px, depuis le bas du pave) qui est deja sur le pied : le fond
   est un degrade a deux aplats, craie en bas jusqu'a cette ligne, noir
   au-dessus — le bord du pied traverse le pave et le pave change de couleur
   exactement la ou il le touche.

   Le texte, le curseur et le carre d'envoi n'ont pas besoin d'etre calcules :
   ils sont blancs en `mix-blend-mode: difference` — sur le noir du pave ils
   restent blancs, sur la craie ils deviennent noirs, et a cheval sur la
   ligne ils sont les deux, au pixel. Le pave est un contexte d'empilement
   (z-index), donc le melange se fait avec SON fond, jamais avec la page.
   Le carre d'envoi : blanc en difference (blanc sur noir, noir sur craie) ;
   la fleche dedans, blanche en difference sur le carre : noire sur le carre
   blanc, craie sur le carre noir. Les couleurs de l'en-tete, retrouvees sans
   les ecrire. */
.barre {
  --sur-pied: 0px;
  transition: width 987ms cubic-bezier(0.65, 0, 0.2, 1);
  background-color: transparent;
  background-image: linear-gradient(to top,
    var(--etiquette-actif-fond) 0, var(--etiquette-actif-fond) var(--sur-pied),
    var(--etiquette-fond) var(--sur-pied), var(--etiquette-fond) 100%);
  isolation: isolate;
}
.barre-mot,
.barre-champ,
.barre-envoi {
  color: #fff;
  mix-blend-mode: difference;
}
.barre-champ { caret-color: #fff; }
.barre-champ::placeholder { color: rgba(255, 255, 255, 0.45); }
.barre-envoi {
  background-color: #fff;
  color: #fff;
  isolation: isolate;
}
.barre-envoi svg { mix-blend-mode: difference; }
.barre-envoi:hover,
.barre-envoi:focus-visible { opacity: 0.72; }

/* Le fil suit la meme ligne, pave par pave : chaque message recoit sa propre
   --sur-pied (assistant.js), et se partage de la meme facon. */
.fil-message,
.fil-attente {
  --sur-pied: 0px;
  isolation: isolate;
}
.fil-message[data-de="assistant"],
.fil-attente {
  background-color: transparent;
  background-image: linear-gradient(to top,
    var(--etiquette-actif-fond) 0, var(--etiquette-actif-fond) var(--sur-pied),
    var(--etiquette-fond) var(--sur-pied), var(--etiquette-fond) 100%);
  color: #fff;
}
.fil-message[data-de="visiteur"] {
  background-color: transparent;
  background-image: linear-gradient(to top,
    var(--etiquette-fond) 0, var(--etiquette-fond) var(--sur-pied),
    var(--etiquette-actif-fond) var(--sur-pied), var(--etiquette-actif-fond) 100%);
  color: #fff;
}
.fil-message .fil-texte,
.fil-attente b { mix-blend-mode: difference; }
.fil-attente b { background-color: #fff; }
.fil-liens .etiquette {
  background-color: #fff;
  color: #fff;
  mix-blend-mode: difference;
  isolation: isolate;
}
.fil-liens .etiquette::after { content: none; }
.fil-liens .etiquette { position: relative; }
/* le texte du lien : blanc en difference sur le carre blanc = noir ; sur le pave noir, le carre est devenu noir et le texte craie */
.fil-liens .etiquette span { mix-blend-mode: difference; }
.fil-liens .etiquette:hover,
.fil-liens .etiquette:focus-visible { opacity: 0.72; box-shadow: none; }
.fil-outils button {
  padding: 0;
  background: none;
  border: none;
  color: var(--encre-scene, #fff);
  opacity: 0.72;
  font-family: var(--font-main);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-wide);
  text-shadow: var(--ombre-scene, none);
  cursor: pointer;
}
.fil-outils button:hover,
.fil-outils button:focus-visible { opacity: 1; outline: none; }

@media (prefers-reduced-motion: reduce) {
  .barre, .barre-mot, .barre-champ { transition: none; }
  .fil, .fil-message { animation: none; }
  .fil-attente b { animation: none; opacity: 0.6; }
  .fil-message[data-ecrit]::after { animation: none; }
}

/* Palier mobile : ouverte, la barre prend la largeur de l'ecran moins les retraits. */
@media (max-width: 610px) {
  .barre[data-ouvert] {
    width: calc(100vw - 2 * var(--menu-inset));
    max-width: none;
  }
}

/* ---- LE BROUILLARD QUI ENGLOBE : la transition vers la section 2 ----

   RIEN NE SE DEPLACE. Un calque qui translate — meme avec un front de
   100vh — garde des lignes d'iso-densite horizontales qui suivent le
   geste : on sentait un element qui monte et descend. Ici le brouillard
   S'EPAISSIT SUR PLACE : chaque calque est immobile, seule sa densite
   (opacity, compositeur pur) suit la descente. Quatre calques :

   — .brume-montante : la FLORAISON du bas. Les memes nappes radiales que
     le socle de la page (lignes COURBES, jamais horizontales), qui se
     densifient en premier — le brouillard existant s'approfondit ;
   — .brume-cube : le nuage qui reclame le cube (voir plus bas) ;
   — .brume-ambiante : l'air entier qui s'epaissit, jusqu'a la couverture
     totale ;
   — .brume-grain : le grain de la page sur la brume (voir plus bas).

   Memes voiles souffle-brume, memes retards, memes melanges 40% que le
   brouillard principal : la couleur qui envahit est LA couleur du
   brouillard a cet instant de la respiration. */
.brume-montante {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: clamp(0, calc(var(--defilement) * 1.8), 1);
  will-change: opacity;
  background:
    /* les nappes du socle, approfondies : memes ancres que .fond-socle */
    radial-gradient(ellipse 105% 72% at 58% 106%,
      var(--degrade-socle) 0%,
      color-mix(in oklch, var(--degrade-socle) 60%, transparent) 55%,
      transparent 100%),
    radial-gradient(ellipse 90% 60% at 12% 112%,
      var(--degrade-socle) 0%,
      color-mix(in oklch, var(--degrade-socle) 55%, transparent) 50%,
      transparent 100%),
    radial-gradient(ellipse 120% 55% at 88% 108%,
      color-mix(in oklch, var(--degrade-socle) 80%, transparent) 0%,
      transparent 100%);
}

.brume-montante::before,
.brume-montante::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

.brume-montante::before {
  background: radial-gradient(ellipse 105% 72% at 58% 106%,
    color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) 0%,
    color-mix(in oklch, color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle)) 60%, transparent) 55%,
    transparent 100%);
  animation-delay: -33.2s;
}

.brume-montante::after {
  background: radial-gradient(ellipse 105% 72% at 58% 106%,
    color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) 0%,
    color-mix(in oklch, color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle)) 60%, transparent) 55%,
    transparent 100%);
  animation-delay: -99.5s;
}

/* Le nuage qui reclame LE CUBE en premier. Les faces du cube sont des
   backdrop-filter : elles eclaircissent la brume derriere elles, donc sa
   silhouette percait la haze semi-transparente a mi-descente — il y avait
   toujours un moment ou le brouillard ne le couvrait pas. On ne peut pas
   fondre .cube3 en opacity (un ancetre a opacity partielle devient
   backdrop root : l'effet lentille meurt d'un coup). On le RECOUVRE donc,
   comme toujours ici : un nuage de socle centre sur lui, a la courbe plus
   precoce que la haze — plein des la mi-descente. Le brouillard avale
   d'abord l'objet, puis le monde. */
.brume-cube {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: clamp(0, calc(var(--defilement) * 2.2 - 0.15), 1);
  will-change: opacity;
  background: radial-gradient(ellipse 46% 55% at var(--cube3-x) var(--cube3-y),
    var(--degrade-socle) 0%,
    var(--degrade-socle) 45%,
    color-mix(in oklch, var(--degrade-socle) 72%, transparent) 70%,
    transparent 100%);
}

/* La haze ambiante : l'air qui s'epaissit partout, pas seulement du bas.
   Opacity seule (compositeur) ; a 1 elle est pleine — meme le haut de
   l'ecran est brouillard. Ses voiles respirent dedans (l'opacity du parent
   les module d'elle-meme). */
.brume-ambiante {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: clamp(0, calc((var(--defilement) - 0.08) * 1.09), 1);
  will-change: opacity;
  /* socle PLEIN, aucun degrade : c'est l'opacity qui fait la densite —
     pas d'iso-lignes du tout, et couverture garantie a 1 */
  background: var(--degrade-socle);
}

.brume-ambiante::before,
.brume-ambiante::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: souffle-brume 199s linear infinite;
}

.brume-ambiante::before {
  background: color-mix(in oklch, var(--degrade-bleu) 40%, var(--degrade-socle));
  animation-delay: -33.2s;
}

.brume-ambiante::after {
  background: color-mix(in oklch, var(--degrade-orange) 40%, var(--degrade-socle));
  animation-delay: -99.5s;
}

/* Le BLANCHIMENT : a mesure que la brume prend la page, sa couleur PASSE
   au blanc casse (--degrade-craie). Plafond 0.89, pas 1 : les 11%
   restants laissent transparaitre un souffle du socle et des voiles —
   c'est ce qui fait un blanc CASSE vivant plutot qu'un aplat. */
.brume-craie {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: clamp(0, calc((var(--defilement) - 0.34) * 1.4), 0.89);
  will-change: opacity;
  background: var(--degrade-craie);
}

/* Le grain de la page, repose SUR le brouillard : meme bruit, meme
   soft-light, meme opacite au maximum — la brume a la matiere du fond.

   Il monte VITE (x3, plafonne) : les calques de brume vivent hors de
   .fond, donc hors du grain de la page — sans dither immediat, le front
   de 100vh du brouillard montrait ses bandes horizontales (le banding des
   degrades lisses, la raison d'etre du grain). Des que la brume parait,
   son grain doit etre la. */
.brume-grain {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: calc(min(var(--defilement) * 3, 1) * var(--grain-opacite));
  mix-blend-mode: soft-light;
  background-size: var(--grain-taille) var(--grain-taille);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.6' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='2.5' intercept='-0.75'/%3E%3CfeFuncG type='linear' slope='2.5' intercept='-0.75'/%3E%3CfeFuncB type='linear' slope='2.5' intercept='-0.75'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* Le heros ne bouge pas, et s'efface en phase avec la brume (voir .shell).
   Seules la barre d'intention et l'icone menu restent AU-DESSUS de la
   brume (z-index > 4) : les commandes survivent au brouillard, le decor
   s'y noie. */
.entete {
  z-index: 6;
}
/* La barre et son fil passent AU-DESSUS DU PIED (z-index 7) : une commande
   qui disparait derriere le pied de page est une commande perdue — elle
   s'y inverse en craie (voir data-inverse) au lieu de s'y noyer. */
.barre,
.fil {
  z-index: 8;
}

/* La section 2 : au-dessus du brouillard, seule dans le flux (le shell est
   fixe) — c'est elle qui donne les 200vh de defilement. Squelette : aucune
   surface cliquable tant qu'elle est vide, sinon elle volerait les clics
   des liens du rail sous elle. Ses futurs elements retabliront
   pointer-events: auto chacun pour soi, et vivront dans sa moitie basse
   (la premiere hauteur d'ecran appartient au heros). */
.section2 {
  position: relative;
  z-index: 5;
  min-height: 200vh;
  pointer-events: none;
}

/* ---- LA CARD VIDEO : le premier element de la section 2 ----

   Une card classique, dans le flux : elle monte et descend AVEC le
   defilement, posee sur le brouillard blanchi. Coins arrondis de la
   suite, ombre portee douce teintee d'abysse, la video plein cadre.
   La lecture reste conditionnelle : cadre.js ne la demarre (avec le son)
   que quand la card est entierement visible a l'ecran. */
/* Absolu, pas margin-top : une marge haute sur le premier enfant
   fusionnerait avec celle de la section (margin collapse) et decalerait
   toute la section d'un ecran. */
/* En flux (le padding-top remplace l'absolu — un padding ne fusionne pas,
   contrairement a une marge) : le texte et la card s'empilent dans la
   COLONNE DU RAIL, celle du h1 (--brand-inset), et la section grandit
   avec eux. 68vh, pas 100 : le paragraphe est DEJA dans la page au repos,
   pose dans le bas du premier ecran — il ne surgit pas du bord, il monte
   avec le defilement. */
.cadre-scene {
  /* 34vh de fond de page : sans cette reserve, la card la plus basse ne
     pouvait plus atteindre le centre de l'ecran — la lecture ne se
     declenchait jamais */
  /* 110vh et non 68 : a 68 le paragraphe etait DEJA a l'ecran au repos, et sa
     cascade se declenchait alors qu'il touchait deja le haut. A 110 il commence
     sous la ligne de flottaison — il entre par le bas, se lit en montant, puis
     sort par le haut. */
  /* 55vh et non plus 110 : le paragraphe qui ouvrait la section est remonte
     dans le heros. Le titre WEBDESIGN (volet, marge 110vh) entre donc par le
     bas a 65vh de defilement — juste quand le heros a fini de s'effacer
     (62 %). */
  padding: 55vh var(--canvas-inset) 34vh var(--brand-inset);
  display: grid;
  justify-items: start;
  gap: var(--f-89);
}

/* Le paragraphe de la section 2 : revele par le scroll AVANT la card, la
   TAILLE DU H1 (meme corps, meme interligne, meme graisse), cale sur la
   meme colonne verticale que lui. Encre douce sur le brouillard blanchi.
   Selectionnable malgre le pointer-events:none de la section. */
/* FIXE en bas d'ecran, colonne du rail : le paragraphe ne voyage pas avec
   le flux — il PARAIT a sa place (opacite) une fois le logo et le h1
   eteints (62% du defilement), et s'efface quand la card entre en vue
   (classe carte-en-vue posee par cadre.js) : jamais deux voix en meme
   temps. */
/* DANS LE FLUX, et non plus fixe en bas d'ecran : le paragraphe MONTE avec le
   defilement au lieu de paraitre sur place puis de s'effacer. Il n'a plus
   besoin de `left` — la colonne du rail lui vient du padding de .cadre-scene,
   comme a la card. */
.section2-texte {
  margin: 0;
  max-width: var(--l-843);
  font-size: var(--text-h1);
  line-height: var(--lh-h1);
  font-weight: var(--fw-extralight);
  letter-spacing: var(--ls-tight);
  color: var(--color-brand-studio);
  pointer-events: auto;
  /* La revelation ne vit plus ici mais dans les mots (voir .mot-texte) : le
     bloc reste plein, sinon son opacite se multiplierait a la leur et la
     cascade serait ecrasee. */
}

/* Le paragraphe apparait MOT PAR MOT, comme le logotype et le titre.

   Mais il ne se revele pas au CHARGEMENT : il se revele au DEFILEMENT. Sa
   cascade ne peut donc pas etre une suite de delais d'animation comme celle du
   h1 — elle est indexee sur --defilement, chaque mot ayant son propre seuil,
   decale d'un cran. La transition de 610ms fait le reste : en descendant, les
   mots s'allument l'un apres l'autre ; en remontant, ils s'eteignent dans
   l'ordre inverse, ce qu'une animation a delais ne saurait pas faire.

   Le rang est porte par --i dans le HTML plutot que par des regles
   nth-child : le titre en demande six, ce paragraphe en demanderait
   vingt-trois. */
.section2-texte .mot-texte {
  display: inline-block;
  opacity: clamp(0, calc((var(--defilement) - 0.10 - var(--i) * 0.006) * 8), 1);
  transition: opacity 610ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Sous mouvement reduit, pas de cascade : le paragraphe parait d'un bloc. */
@media (prefers-reduced-motion: reduce) {
  .entete .etiquette,
  .logo span,
  .hero-texte,
  .hero-scroll { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .section2-texte .mot-texte {
    opacity: clamp(0, calc((var(--defilement) - 0.10) * 4), 1);
    transition: none;
  }
}

/* ---- LE VOLET WEBDESIGN ----

   Un titre qui reste accroche en haut de l'ecran pendant que deux cards
   montent et lui passent DEVANT.

   Le mecanisme tient en deux choses, et aucune n'est du JavaScript :
   — `position: sticky` sur le titre, qui le colle a --f-89 du haut tant que
     le volet defile ;
   — un z-index sur les cards, qui decide QUI SURVOLE QUI. Sans lui elles
     passeraient dessous, le titre restant peint par-dessus.

   Le volet doit etre PLUS HAUT QUE L'ECRAN, sinon le sticky n'a aucune course
   et le titre ne tient jamais : d'ou la reserve basse et la marge qui separe
   les cards du titre — ce sont elles qui donnent au titre le temps d'exister
   avant d'etre recouvert. */
.volet {
  position: relative;
  width: 100%;
  /* LA SEQUENCE. Le titre ne doit pas entrer par le bas tant que le paragraphe
     n'est pas sorti par le haut : sinon les deux se lisent en meme temps et on
     n'a le temps ni de l'un ni de l'autre.

     La condition s'ecrit : haut_du_titre - hauteur_ecran >= bas_du_paragraphe.
     Avec le paragraphe pose a 110vh et cette marge a 110vh, elle devient
     1.10 H + gap >= H — vraie a toute hauteur d'ecran. C'est pour ca que la
     valeur est en vh et non en pixels : en pixels la sequence ne tiendrait qu'a
     une seule taille de fenetre. */
  margin-top: 110vh;
  /* La course du sticky : sans cette reserve, le titre serait recouvert des
     qu'il se colle. */
  padding-bottom: 34vh;
}

.volet-titre {
  position: sticky;
  top: var(--f-89);
  /* Explicite, meme si c'est la valeur par defaut : c'est le rapport a
     .volet-liste qui compte, et il se lit mieux ecrit des deux cotes. */
  z-index: 0;
  margin: 0;
  text-transform: uppercase;
  font-size: var(--text-display);
  line-height: 1;
  font-weight: var(--fw-extralight);
  letter-spacing: var(--ls-tight);
  color: var(--color-brand-studio);
}

/* ---- L'ETAGERE : TROIS GRANDES CARDS QUI RALENTISSENT ----

   Le mouvement : la premiere card monte avec la page, et arrivee sous le titre
   elle RALENTIT — elle continue de monter, a la moitie de la vitesse de la page.
   Un ecran plus tard la deuxieme arrive, se range par-dessus, decalee vers la
   droite, et ralentit a son tour. Puis la troisieme. Quand le bas d'une card
   approche du haut de l'ecran, elle REACCELERE et sort franchement.

   Le ralenti est en JavaScript (assets/etagere.js) : un translateY de retard,
   recalcule a chaque defilement selon un profil en trois temps. Rien n'est
   `sticky` — un sticky est un arret, et on ne veut pas d'arret.

   Ce que le CSS fait, lui : la MISE EN PLACE dans le flux.
   — UNE SEULE CELLULE de grille, ou les trois colonnes se superposent
     (grid-area 1/1). C'est ce qui permet aux cards d'etre GRANDES — 61.8% du
     rail chacune — tout en se rangeant cote a cote : elles se recouvrent, la
     suivante par-dessus la precedente, et de chaque precedente reste une bande
     de 19.1% a gauche. Trois colonnes egales les tenaient a un tiers du rail.
   — L'ARRIVEE DECALEE : dans sa colonne, la deuxieme card porte une marge
     haute d'un ecran, la troisieme de deux. Elles atteignent le titre l'une
     apres l'autre.
   — LA RESERVE en fin de colonne (::after, un ecran) : c'est du flux vide que
     le ralenti consomme — une card retenue occupe plus de page qu'une card qui
     file, et la derniere doit avoir fini de sortir avant que la section
     suivante n'arrive sous elle.

   AU-DESSUS du titre (z-index 1) : le titre WEBDESIGN est sticky a --f-89, les
   cards lui passent devant en montant — c'est le survol d'origine du volet. */
.volet-etagere {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr;
  /* La distance que la premiere card parcourt avant d'atteindre le titre. En
     vh : une duree de lecture, pas une distance en pixels. */
  margin-top: 55vh;
}

.etagere-colonne {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
}

.etagere-colonne::after {
  content: "";
  flex: none;
  height: var(--etagere-tenue);
}

.projet {
  margin: 0;
  /* 61.8% du rail — ou moins si l'ecran est bas. La card est en 8:5 et
     ralentit sous le titre, a --etagere-butee ; sur un ecran de 720, a 61.8%
     elle y descendrait a 708px, SOUS la barre d'intention (fixe, 639-686). La
     seconde borne est la hauteur qui reste entre la butee et la barre, legende
     comprise, convertie en largeur par le rapport 8:5 : --f-144 couvre la
     legende (34), la barre avec sa marge basse (81) et une respiration (29).
     A 1080 de haut elle ne mord pas (1070 > 1056) ; a 768 elle ramene la card
     a ~571px. */
  width: min(
    var(--etagere-card),
    calc((100vh - var(--etagere-butee) - var(--f-144)) * 8 / 5)
  );
  /* Le retard est un transform recalcule a chaque defilement : on previent le
     compositeur. Trois elements, le cout est nul. */
  will-change: transform;
}

/* L'arrivee decalee : un ecran entre chaque card (marge haute), et le PAS vers
   la droite (marge gauche) — 19.1% du rail, de sorte que la troisieme finisse
   calee au bord droit : 2 x 19.1 + 61.8 = 100. Les marges sont sur la CARD,
   dans sa colonne — c'est elle qui la place dans le flux. */
.etagere-colonne:nth-child(2) .projet {
  margin-top: var(--etagere-cadence);
  margin-left: var(--etagere-pas);
}

.etagere-colonne:nth-child(3) .projet {
  margin-top: calc(var(--etagere-cadence) * 2);
  margin-left: calc(var(--etagere-pas) * 2);
}

/* LE MOCKUP. 8:5 — c'est le rapport natif de deux des trois mockups (les Miles),
   et le 16:9 de la galerie n'y perd que 5% par flanc. Fond gris uni et AUCUN
   arrondi : regle maison, deja suivie par la card video. Le gris reste dessous
   le temps que l'image arrive (lazy), et c'est lui qu'on voit si elle manque. */
.projet-image {
  aspect-ratio: 8 / 5;
  background-color: var(--carte-fond);
  overflow: hidden;
}

.projet-image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* La legende, sous la card : nom en medium, titre en light, a la suite sur une
   ligne — et sur deux si la colonne est trop etroite. Le registre de la legende
   video (.cadre-titre), un cran au-dessus en corps. */
.projet-legende {
  margin-top: var(--f-13);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-wide);
  color: var(--color-brand-studio);
}

.projet-nom {
  font-weight: var(--fw-medium);
}

.projet-nom::after {
  content: " — ";
}

/* Palier tablette : une colonne, les cards pleine largeur l'une sous l'autre,
   et le meme ralenti (etagere.js ne change pas). Les marges d'arrivee tombent :
   en colonne unique, le flux les decale deja l'une apres l'autre. */
@media (max-width: 987px) {
  .volet-etagere {
    grid-template-columns: 1fr;
    row-gap: var(--f-34);
    margin-top: 34vh;
  }

  /* Chaque colonne reprend sa propre rangee, et les cards toute la largeur. */
  .etagere-colonne {
    grid-area: auto;
  }

  .projet {
    width: 100%;
  }

  .etagere-colonne:nth-child(2) .projet,
  .etagere-colonne:nth-child(3) .projet {
    margin-top: 0;
    margin-left: 0;
  }

  /* En colonne unique, une reserve d'un ecran entier entre deux cards ferait
     attendre la suivante. Un tiers suffit au ralenti. */
  .etagere-colonne::after {
    height: 34vh;
  }
}

/* Paliers : la card s'elargit sous 987 et 610 ; ecran court, la
   respiration se resserre. Le texte suit --text-h1, qui decroit deja
   par les tokens. */
@media (max-width: 987px) {
  .cadre { width: min(76vw, calc((100vh - var(--f-233)) * 16 / 9)); }
}

@media (max-width: 610px) {
  .cadre { width: min(89vw, calc((100vh - var(--f-233)) * 16 / 9)); }
}

@media (max-height: 610px) {
  .cadre-scene { gap: var(--f-34); }
}

/* Coins NETS (regle maison : pas d'arrondi sur la card), largeur reduite —
   55vw et plafond 987px, deux valeurs de la suite. Plus d'overflow:hidden :
   les coins sont droits (rien a rogner) et le titre vit AU-DESSUS de la
   boite, il serait coupe. */
.cadre {
  position: relative;
  /* BIEN plus basse, et pas seulement pour l'œil : le paragraphe (fixe)
     parait a 62% du defilement et s'efface quand la card entre en vue —
     a 76vh elle entrait AVANT 62%, le paragraphe ne paraissait jamais.
     123vh ouvre une fenetre de lecture d'environ 40vh de descente. */
  margin: 123vh 0 0;
  /* bornee aussi par la HAUTEUR : sur ecran court, la card cede la place
     au paragraphe (233 = texte + respirations, valeur de la suite) */
  width: min(55vw, var(--f-987), calc((100vh - var(--f-233)) * 16 / 9));
  aspect-ratio: 16 / 9;
  /* AUCUN fond d'attente : la video s'affiche tout de suite — cadre.js
     force le decodage de sa premiere image des les metadonnees
     (preload auto + currentTime epsilon), il n'y a rien a masquer */
  box-shadow:
    0 var(--f-13) var(--f-34) color-mix(in oklch, var(--degrade-abysse) 21%, transparent),
    0 var(--f-2) var(--f-8) color-mix(in oklch, var(--degrade-abysse) 13%, transparent);
}

.cadre-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Le titre, HORS de la card : au-dessus, cale sur son bord gauche, en
   petit (--text-sm) — une legende d'encre douce sur le brouillard
   blanchi, pas un titre dans l'image. */
.cadre-titre {
  position: absolute;
  bottom: calc(100% + var(--f-13));
  left: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-light);
  letter-spacing: var(--ls-wide);
  color: var(--color-brand-studio);
}
