/* Les polices sont chargées par fonts/fonts.css, lié depuis index.html AVANT
   cette feuille — elles sont donc déjà déclarées ici. Il y avait à cette place
   un @import vers fonts.googleapis.com : appel à un tiers hors UE (RGPD) et
   URL que Google servait parfois morte selon l'agent. Voir tools/fetch-fonts.mjs. */

/* ══════════════════════════════════════════════════════════════════
   RESET & ROOT
   ══════════════════════════════════════════════════════════════════ */

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

html,
body {
  width: 100%;
  height: 100%;
  overflow: auto;
  background: #000;
}

#app {
  position: relative;
  width: 100%;
  height: 100%;
  min-width: 500px;   /* écrasé par JS depuis CONFIG */
  min-height: 430px;
  overflow: hidden;
  touch-action: none;   /* le glissé du doigt pilote curseur + torche sur tout le site */
  background: #000;
}

/* ══════════════════════════════════════════════════════════════════
   CURSEUR — natif partout, sauf sur les appareils tactiles
   ──────────────────────────────────────────────────────────────────
   Le site masquait autrefois le pointeur de l'OS PARTOUT (règle `* {cursor:
   none !important}` ci-dessus, aujourd'hui retirée) pour dessiner sa propre
   flèche (#cursor, cf. section « CURSEUR — flèche élégante » plus bas).
   Hors tactile, on revient au curseur natif de l'ordinateur et à son
   comportement par défaut : la flèche devient une main sur tout ce qui est
   cliquable, `cursor: pointer` étant déjà déclaré au fil du fichier — il
   était simplement étouffé jusqu'ici par le blocage universel.

   Le curseur dessiné ne survit que sur téléphone/tablette (body.is-touch,
   posée par app.js) : c'est la seule famille d'appareils sans pointeur
   natif à montrer, et où le curseur custom sert encore à autre chose —
   suivre le doigt dans « Peine démesurée », par exemple. TOUTES les
   déclarations `cursor: none` qui subsistent dans ce fichier et dans les
   feuilles de chapitre sont donc conditionnées à `.is-touch` ; celles qui ne
   le sont plus sont devenues `cursor: pointer` (comportement par défaut).

   ⚠️ `body.is-touch` ET `body.is-touch *` : le SÉLECTEUR SEUL (`body.is-touch
   *`) ne matche que les DESCENDANTS de body, jamais body lui-même — l'ancien
   `* {cursor:none!important}` couvrait tout, body compris (l'universel se
   matche aussi lui-même). Sans la première branche, un pointeur natif
   pourrait resurgir sur les rares appareils tactiles + souris hybrides quand
   le curseur survole un point hors de #app. Même paire déjà utilisée plus
   bas pour body.peine-aim et, avant ce changement, pour body.chp2/3/4-active. */
body.is-touch,
body.is-touch * {
  cursor: none !important;
}

/* Le curseur custom lui-même : invisible hors tactile en toute circonstance
   (pas seulement avant le démarrage, cf. règle suivante) — filet de sécurité
   au cas où une classe d'état (.active/.hotspot…) lui serait posée par
   erreur sur un appareil à pointeur natif. */
body:not(.is-touch) #cursor {
  display: none !important;
}

/* ══════════════════════════════════════════════════════════════════
   TACTILE — HAUTEUR STABLE (100svh)
   ──────────────────────────────────────────────────────────────────
   Sur téléphone, la barre du navigateur apparaît/disparaît AU TOUCHER et
   fait varier la hauteur du viewport : tout ce qui est calé dessus
   (height:100 %, fixed inset:0) « respire » → l'UI saute/clignote et le
   bas de l'image passe sous la barre. svh = hauteur du PETIT viewport
   (barres visibles), qui ne varie JAMAIS : les conteneurs deviennent
   stables et le bas est toujours visible. Quand la barre se masque, il
   reste un bandeau noir en bas — invisible sur ce site au fond noir.
   body.is-touch est posée par app.js. Desktop : svh = vh → aucun effet,
   et la classe n'est de toute façon pas posée.
   ══════════════════════════════════════════════════════════════════ */
@supports (height: 100svh) {
  body.is-touch #app            { height: 100svh; }
  /* #chapitre2-root est fixed top:0 + inset inline : top+height priment
     sur bottom (sur-contrainte CSS) → le root se cale sur svh. */
  body.is-touch #chapitre2-root { height: 100svh; }
}

body:not(.experience-started) * {
  cursor: auto !important;
}

body:not(.experience-started) #cursor {
  display: none;
}

body.resizing * {
  transition: none !important;
}

/* ══════════════════════════════════════════════════════════════════
   BACKGROUNDS
   ══════════════════════════════════════════════════════════════════ */

.bg-layer {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

#bg-vitrine {
  background-image: url("images/vitrineOrfila.webp");
  opacity: 1;
}

#bg-phrenologie {
  background-image: url("images/phrenologie.webp");
  opacity: 0;
}

#bg-collaboration {
  background-image: url("images/collaboration.webp");
  opacity: 0;
  transition:
    opacity 1.2s ease,
    transform 1.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* Chapitre 1 : état de base — tout le style visuel est géré par JS */
#bg-chapitre1 {
  background-color: #000;
  opacity: 0;
  /* Pas de transition CSS ici : le JS gère tout pour éviter les conflits */
}

/* Overlay image chapitre 1 : géré intégralement par JS */
#chapter1-image-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 1;
  transform: scale(1);
  will-change: opacity, transform;
}

/* ══════════════════════════════════════════════════════════════════
   CANVAS & LIGHT
   ══════════════════════════════════════════════════════════════════ */

#overlay-canvas {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Le fondu accompagne celui de #doc-overlay (700 ms) : voir .doc-ov-open. */
  transition: opacity 700ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* Document ouvert : la torche s'efface pour laisser paraître l'image sous le
   voile. La torche noircit tout l'écran sauf son halo — la garder reviendrait
   à ne montrer l'image QUE dans ce halo. Elle est déjà gelée à cet instant
   (TorchSystem.pause) : ce fondu ne coûte qu'une opacité de couche, et le
   dernier cadre peint reste intact dessous pour la réapparition.
   Posée/retirée par DocumentOverlay selon overlay.veil_hides_torch. */
body.doc-ov-open #overlay-canvas { opacity: 0; }

/* Lumière autonome Chapitre 1 */
#chapter1-fixed-light {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
}

body.page3 #chapter1-fixed-light {
  opacity: 1;
}

/* ══════════════════════════════════════════════════════════════════
   HOTSPOTS CHAPITRE 1
   ══════════════════════════════════════════════════════════════════ */

#hotspot-layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

body.page3 #hotspot-layer {
  pointer-events: auto;
}

.hotspot-img {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  transition: opacity 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.hotspot-img.active {
  opacity: 1;
}

.hotspot-zone {
  position: absolute;
  cursor: pointer;
}

/* ══════════════════════════════════════════════════════════════════
   CURSOR
   ──────────────────────────────────────────────────────────────────
   ⚠️ Le rendu du curseur est REDÉFINI plus bas (section « CURSEUR —
   flèche élégante »), qui prime par ordre de source. Les règles
   ci-dessous restent la base historique (dimensions, transitions) et
   servent de repli pour les variantes qui neutralisent le mask.
   ══════════════════════════════════════════════════════════════════ */

#cursor {
  position: fixed;
  z-index: 9999;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  pointer-events: none;
  transform: translate(-50%, -50%);
  background: rgba(255, 248, 220, 0.38);
  will-change: transform, width, height, box-shadow, background;
  transition:
    width 0.22s ease,
    height 0.22s ease,
    background 0.22s ease,
    box-shadow 0.22s ease,
    opacity 0.22s ease,
    transform 0.22s ease;
}

#cursor.active {
  width: 14px;
  height: 14px;
  background: rgba(255, 240, 180, 0.9);
  box-shadow:
    0 0 0 4px rgba(255, 220, 100, 0.2),
    0 0 18px rgba(255, 200, 60, 0.55),
    0 0 36px rgba(255, 160, 20, 0.28);
}

#cursor.hotspot {
  width: 22px;
  height: 22px;
  background: rgba(255, 240, 180, 0.98);
  box-shadow:
    0 0 0 8px rgba(255, 220, 100, 0.16),
    0 0 22px rgba(255, 210, 80, 0.95),
    0 0 42px rgba(255, 170, 30, 0.55),
    0 0 72px rgba(255, 150, 20, 0.28);
  animation: cursorHotspotPulse 1.05s ease-in-out infinite;
}

@keyframes cursorHotspotPulse {
  0% {
    transform: translate(-50%, -50%) scale(1);
    box-shadow:
      0 0 0 0 rgba(255, 220, 100, 0.34),
      0 0 22px rgba(255, 210, 80, 0.95),
      0 0 42px rgba(255, 170, 30, 0.55),
      0 0 72px rgba(255, 150, 20, 0.28);
  }
  50% {
    transform: translate(-50%, -50%) scale(1.16);
    box-shadow:
      0 0 0 10px rgba(255, 220, 100, 0.08),
      0 0 28px rgba(255, 215, 110, 1),
      0 0 56px rgba(255, 170, 30, 0.62),
      0 0 92px rgba(255, 150, 20, 0.34);
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
    box-shadow:
      0 0 0 0 rgba(255, 220, 100, 0),
      0 0 22px rgba(255, 210, 80, 0.95),
      0 0 42px rgba(255, 170, 30, 0.55),
      0 0 72px rgba(255, 150, 20, 0.28);
  }
}

/* ──────────────────────────────────────────────────────────────────
   CURSEUR — variantes Chapitre 2
   ────────────────────────────────────────────────────────────────────
   Le chapitre 2 réutilise le MÊME curseur de base que le reste du site
   (#cursor) et le décline par contexte :
   - .hotspot       : grossissement + pulse doré standard (crânes opening,
                      diapos du cartel) — défini plus haut.
   - .hotspot-soft  : version atténuée et plus petite (yeux d'invisibilisation).
   - body.peine-aim : remplace entièrement le curseur par un viseur noir
                      minimaliste (sous-partie « peine démesurée »), qui se
                      focalise (grossit + pulse) au survol d'un mot-clef.
   ──────────────────────────────────────────────────────────────────── */

/* Variante atténuée — invisibilisation : « grossit et pulse, mais très
   légèrement ». Plus petit et moins lumineux que .hotspot. */
#cursor.hotspot-soft {
  width: 12px;
  height: 12px;
  background: rgba(255, 240, 180, 0.95);
  box-shadow:
    0 0 0 3px rgba(255, 220, 100, 0.12),
    0 0 12px rgba(255, 210, 80, 0.70),
    0 0 22px rgba(255, 170, 30, 0.38);
  animation: cursorHotspotPulseSoft 1.25s ease-in-out infinite;
}

@keyframes cursorHotspotPulseSoft {
  0%, 100% {
    transform: translate(-50%, -50%) scale(1);
    box-shadow:
      0 0 0 2px rgba(255, 220, 100, 0.16),
      0 0 12px rgba(255, 210, 80, 0.70),
      0 0 22px rgba(255, 170, 30, 0.38);
  }
  50% {
    transform: translate(-50%, -50%) scale(1.08);
    box-shadow:
      0 0 0 5px rgba(255, 220, 100, 0.05),
      0 0 16px rgba(255, 215, 110, 0.82),
      0 0 28px rgba(255, 170, 30, 0.46);
  }
}

/* ── Curseur « peine démesurée » ────────────────────────────────────
   Esprit exploration d'un journal d'antan : le curseur devient un
   discret point d'encre sépia. Au survol d'un élément interactif (mot-clef,
   flèche retour, bouton plein écran) il éclôt en un fin anneau de
   focalisation avec un point de visée central — comme une loupe qui se pose
   sur le mot. Sobre, minimaliste, sans croix « technique ».
   Les sélecteurs body.peine-aim #cursor (1,1,1) priment sur #cursor.hotspot
   (1,1,0) : le rendu encre l'emporte même quand app.js pose .hotspot.
   ⚠️ RÉSERVÉ AU TACTILE (.is-touch) : hors tactile, ce viseur cède la place
   au curseur natif de l'OS (avec `cursor: pointer` sur les mots-clefs,
   cf. chp2-peine-demesuree.css) — cohérent avec le reste du site. */
body.peine-aim.is-touch,
body.peine-aim.is-touch * {
  cursor: none !important;
}

/* Repos — petit point d'encre sépia, sobre sur le papier. */
body.peine-aim #cursor {
  width: 6px;
  height: 6px;
  background: rgba(32, 24, 13, 0.72);
  border: 1.3px solid rgba(32, 24, 13, 0);   /* anneau invisible au repos */
  border-radius: 50%;
  box-shadow: none;
  animation: none !important;
  transition:
    width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
    height 0.34s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.28s ease,
    border-color 0.28s ease;
}

/* Point de visée central (apparaît seulement quand l'anneau est ouvert). */
body.peine-aim #cursor::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  border-radius: 50%;
  background: rgba(26, 18, 8, 0.85);
  transform: translate(-50%, -50%);
  transition: width 0.24s ease, height 0.24s ease;
  pointer-events: none;
}

/* Focalisation — éclosion en anneau fin + point central.
   .aim-hot : posé par le module au survol d'un mot-clef.
   .hotspot : posé par app.js sur la flèche retour et le bouton plein écran. */
body.peine-aim #cursor.aim-hot,
body.peine-aim #cursor.hotspot {
  width: 30px;
  height: 30px;
  background: rgba(32, 24, 13, 0);            /* centre vidé → anneau */
  border-color: rgba(26, 18, 8, 0.80);
  animation: peineAimPulse 1.5s ease-in-out infinite !important;
}
body.peine-aim #cursor.aim-hot::after,
body.peine-aim #cursor.hotspot::after {
  width: 3px;
  height: 3px;
}

@keyframes peineAimPulse {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-50%, -50%) scale(1.07); }

}

/* ══════════════════════════════════════════════════════════════════
   TITLES
   ──────────────────────────────────────────────────────────────────
   LA COLONNE HAUT-GAUCHE : la boussole l'ouvre, à gauche ; les trois
   niveaux de titre s'écrivent à sa droite.

   --col-titres est ÉCRIT PAR app.js (poserColonne) depuis
   CONFIG.MAP.titres_gauche_pct : c'est le seul moyen que le réglage
   vive dans config.js sans être recopié ici. La place de la boussole,
   elle, se règle À PART (MAP.gauche_pct / haut_pct) — ajuster l'une ne
   doit pas emmener les autres.
   ══════════════════════════════════════════════════════════════════ */

#site-title {
  position: absolute;
  top: 3.2%;
  left: var(--col-titres-courant);
  z-index: 15;
  pointer-events: none;
  text-transform: uppercase;
  color: rgba(210, 175, 90, 0);
  transition:
    opacity 0.6s ease,
    transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    left var(--col-glisse) cubic-bezier(0.22, 1, 0.36, 1);
}

/* (.fading-out et .fading-in ont été retirées : la première habillait le
   fondu-vers-le-haut du titre AVANT sa bascule — un geste qui se joue
   désormais à la FRONTIÈRE entre deux scènes, c'est-à-dire sur un écran noir,
   donc invisible ; la seconde n'a jamais eu personne pour la poser.) */

#site-title .char {
  display: inline-block;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}

#site-title .sep {
  display: inline-block;
  margin: 0 0.4em;
  opacity: 0;
  transition: opacity 0.8s ease;
}

#chapitre-subtitle {
  position: absolute;
  top: calc(3.2% + 2.6em);
  left: var(--col-titres-courant);
  z-index: 15;
  max-width: 42%;
  line-height: 1.6;
  pointer-events: none;
  text-transform: uppercase;
  /* Valeur de départ : invisible et décalé */
  color: rgba(210, 175, 90, 0.78);
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.3s,
    transform 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.3s,
    left var(--col-glisse) cubic-bezier(0.22, 1, 0.36, 1);
}

#chapitre-subtitle.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Titre de sous-partie (tier 3) — sous le sous-titre, même grammaire visuelle */
#chapitre-part-title {
  position: absolute;
  top: calc(3.2% + 5.0em);
  left: var(--col-titres-courant);
  z-index: 15;
  max-width: 42%;
  line-height: 1.6;
  pointer-events: none;
  text-transform: uppercase;
  color: rgba(210, 175, 90, 0.92);
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.15s,
    transform 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.15s,
    left var(--col-glisse) cubic-bezier(0.22, 1, 0.36, 1);
}

#chapitre-part-title.visible {
  opacity: 1;
  transform: translateY(0);
}

#hover-title {
  position: absolute;
  right: 0;
  bottom: 7%;
  left: 0;
  z-index: 9;
  padding: 0 10%;
  line-height: 1.5;
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transform: translateY(12px);
  transition:
    opacity 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    transform 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

#hover-title.visible {
  opacity: 1;
  transform: translateY(0);
}

#hover-title::after {
  content: '';
  display: block;
  width: 0;
  height: 1px;
  margin: 0.5em auto 0;
  opacity: 0;
  background: rgba(255, 255, 255, 0.25);
  transition:
    width 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.3s,
    opacity 0.35s ease;
}

#hover-title.visible::after {
  width: clamp(80px, 18vw, 260px);
  opacity: 1;
}

#hover-title span.ht-text {
  display: inline;
  color: rgba(255, 255, 255, 0.82);
  text-shadow:
    0 0 40px rgba(0, 0, 0, 1),
    0 0 18px rgba(0, 0, 0, 0.98),
    0 3px 8px rgba(0, 0, 0, 0.9);
  transition: opacity 0.22s ease;
}

#hover-title span.ht-text.fading {
  opacity: 0;
}

/* ══════════════════════════════════════════════════════════════════
   ARROWS & NAVIGATION
   ══════════════════════════════════════════════════════════════════ */

#nav-arrow {
  position: absolute;
  left: 50%;
  z-index: 9;
  cursor: pointer;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%);
}

#nav-arrow.pos-bottom {
  top: auto;
  bottom: 5%;
  left: 50%;
  transform: translateX(-50%);
}

#nav-arrow.pos-top {
  top: 5%;
  bottom: auto;
  left: 50%;
  transform: translateX(-50%);
}

#nav-arrow.pos-left {
  top: auto;
  bottom: 5%;
  left: 3.5%;
  transform: none;
}

#nav-arrow.visible {
  pointer-events: auto;
}

#nav-arrow svg {
  display: block;
  overflow: visible;
  transform-origin: center;
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}

#nav-arrow:hover svg {
  transform: scale(1.22);
}

#nav-bar {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 8;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.9s ease;
}

#nav-bar.visible {
  /* ⚠️ Le conteneur est dimensionné en PLEIN ÉCRAN par NavigationBar.draw()
     (width = vW, height = vH) et vit en z-index 8, donc au-dessus de
     #doc-overlay (7). En `pointer-events: auto` il interceptait TOUS les clics
     et la molette de la page : les documents ne se fermaient plus au clic et
     les sites incrustés devenaient inertes.
     `pointer-events` étant héritée, on neutralise le conteneur et on ne
     réactive que .nav-btn-zone (ci-dessous). */
  pointer-events: none;
  opacity: 1;
}

#nav-bar svg {
  display: block;
  overflow: visible;
}

.nav-rect-path {
  fill: none;
  stroke-linecap: square;
}

.nav-btn-zone {
  cursor: pointer;
  /* Réactive la capture sur les seules cellules cliquables : le reste du SVG
     plein écran laisse passer clics et molette (pointer-events est héritée). */
  pointer-events: auto;
}

.nav-btn-label {
  dominant-baseline: middle;
  text-anchor: middle;
  font-weight: 300;
  text-transform: uppercase;
  transition: letter-spacing 0.4s ease;
}

/* ══════════════════════════════════════════════════════════════════
   DOCUMENT BUTTONS
   ══════════════════════════════════════════════════════════════════ */

#doc-btns {
  position: absolute;
  top: 3.2%;
  right: 3.5%;
  z-index: 9;
  display: flex;
  flex-direction: column;
  gap: 1.8%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
}

#doc-btns.visible {
  /* Seuls les boutons captent : un clic dans les gouttières de la colonne doit
     atteindre l'overlay et refermer le document. */
  pointer-events: none;
  opacity: 1;
}

.doc-btn {
  position: relative;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: center center;
  will-change: transform;
  transition: transform 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* Bouton « À Propos » : identique aux boutons documents (blanc). Seul son
   placement en tête de colonne (marginBottom en JS) le distingue. */

.doc-btn.hovered {
  z-index: 2;
  transform: scaleX(1.08) scaleY(1.12) !important;
}

.doc-btn.push-up {
  transform: translateY(-0.9%);
}

.doc-btn.push-down {
  transform: translateY(0.9%);
}

.doc-btn svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.doc-rect {
  fill: none;
  stroke: rgba(255, 255, 255, 0.72);
  stroke-width: 0.8;
  transition:
    stroke-dashoffset 0.9s cubic-bezier(0.4, 0, 0.2, 1),
    stroke 0.3s ease,
    filter 0.3s ease;
}

.doc-rect.drawn {
  stroke-dashoffset: 0;
}

.doc-label {
  fill: rgba(255, 255, 255, 0.82);
  opacity: 0;
  font-family: 'Cinzel', serif;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  dominant-baseline: middle;
  text-anchor: middle;
  transition: opacity 0.4s ease, fill 0.3s ease;
}

.doc-label.drawn {
  opacity: 1;
}

/* ══════════════════════════════════════════════════════════════════
   ROMAN CIRCLES
   ══════════════════════════════════════════════════════════════════ */

#roman-circles {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 9;
  display: flex;
  flex-direction: row;
  align-items: center;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50%);
  transition: opacity 0.6s ease;
}

#roman-circles.visible {
  pointer-events: auto;
  opacity: 1;
}

.roman-btn {
  position: relative;
  transform-origin: center center;
  will-change: transform;
  cursor: pointer;
  transition:
    transform 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    opacity 0.3s ease;
}

.roman-btn.hovered {
  z-index: 2;
  transform: scale(1.32) !important;
}

.roman-btn.push-up {
  transform: translateX(-1.4%);
}

.roman-btn.push-down {
  transform: translateX(1.4%);
}

.roman-btn svg {
  display: block;
  overflow: visible;
}

.roman-c {
  fill: none;
  stroke-width: 1;
  transition: stroke 0.3s ease, filter 0.3s ease;
}

.roman-num {
  font-family: 'Cinzel', serif;
  font-weight: 400;
  dominant-baseline: middle;
  text-anchor: middle;
  transition: fill 0.3s ease;
}

/* ══════════════════════════════════════════════════════════════════
   SKIP BUTTON & FULLSCREEN
   ══════════════════════════════════════════════════════════════════ */

#skip-btn {
  position: absolute;
  bottom: 5%;
  left: 50%;
  z-index: 10;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%);
  transition: opacity 0.6s ease;
}

#skip-btn.visible {
  pointer-events: auto;
  opacity: 1;
}

#skip-btn .skip-wrap {
  position: relative;
  /* Dimensions gérées par JS via _getSkipSize() — proportionnelles à l'arrow */
  cursor: pointer;
  transform-origin: center center;
  will-change: transform;
  transition: transform 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

#skip-btn .skip-wrap.hovered {
  transform: scaleX(1.08) scaleY(1.12);
}

#skip-btn svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

#skip-btn .skip-rect {
  fill: none;
  stroke: rgba(255, 255, 255, 0.72);
  stroke-width: 0.8;
  transition:
    stroke-dashoffset 0.9s cubic-bezier(0.4, 0, 0.2, 1),
    stroke 0.3s ease,
    filter 0.3s ease;
}

#skip-btn .skip-label {
  fill: rgba(255, 255, 255, 0.82);
  opacity: 0;
  font-family: 'Cinzel', serif;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  dominant-baseline: middle;
  text-anchor: middle;
  transition: opacity 0.4s ease, fill 0.3s ease;
}

#skip-btn .skip-rect.drawn {
  stroke-dashoffset: 0;
}

#skip-btn .skip-label.drawn {
  opacity: 1;
}

#fs-btn {
  position: absolute;
  right: 3.5%;
  bottom: 5%;
  z-index: 15;
  cursor: pointer;
  opacity: 0.85;
}

#fs-btn svg {
  display: block;
  overflow: visible;
}

#fs-btn .icon-expand,
#fs-btn .icon-collapse {
  display: none;
}

/* ══════════════════════════════════════════════════════════════════
   VEIL & QUOTE
   ══════════════════════════════════════════════════════════════════ */

#veil {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  background: #000;
}

#chapter-quote {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 9vh 10vw;
  pointer-events: none;
  opacity: 0;
  transition: opacity 2.6s ease;
}

#chapter-quote.visible {
  opacity: 1;
}

#chapter-quote .quote-inner {
  width: min(860px, 100%);
  color: rgba(236, 228, 212, 0.95);
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: clamp(1.22rem, 1.75vw, 1.9rem);
  line-height: 1.82;
  letter-spacing: 0.01em;
  font-weight: 300;
  text-align: left;
  text-wrap: pretty;
  white-space: pre-line;
  text-shadow: 0 0 22px rgba(255, 255, 255, 0.04);
}

#chapter-quote .quote-body {
  display: inline;
}

#chapter-quote .cursor {
  display: inline-block;
  width: 0.55ch;
  margin-left: 0.03em;
  vertical-align: baseline;
  color: rgba(236, 228, 212, 0.82);
  animation: quoteBlink 1.25s steps(1) infinite;
}

@keyframes quoteBlink {
  0%, 44% {
    opacity: 1;
  }
  45%, 100% {
    opacity: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════
   START SCREEN — Editorial Contemporain
   ══════════════════════════════════════════════════════════════════ */

/* (Un second @import Google se trouvait ici. Outre l'appel au tiers, il était
   INERTE : une règle @import doit précéder toute autre règle d'une feuille, et
   celle-ci arrivait après un millier de lignes — les navigateurs l'ignoraient
   donc déjà. Les polices viennent de fonts/fonts.css.) */

#start-screen {
  position: absolute;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  /* Fond avec un léger grain pour la profondeur */
  background: radial-gradient(circle at center, #121212 0%, #080808 100%);
  transition: opacity 1.8s cubic-bezier(0.4, 0, 0.2, 1);
  overflow: hidden;
}

/* Effet de grain subtil en superposition.
   ⚠️ Le data-URI contenait un caractère corrompu (« %3微%3C » au lieu de
   « %3E%3C ») : le SVG était invalide, donc le grain ne s'affichait pas. */
#start-screen::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.03;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}

#start-screen.hidden {
  pointer-events: none;
  opacity: 0;
  transform: scale(1.05); /* Effet d'immersion à l'entrée */
}

/* ── Grille Architecturale ── */

.ss-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  padding: clamp(20px, 4vw, 60px);
}

.ss-grid-line {
  position: absolute;
  background: rgba(255, 255, 255, 0.05);
}

.ss-grid-line--h {
  left: 4vw;
  right: 4vw;
  height: 1px;
}

.ss-grid-line--v {
  top: 4vh;
  bottom: 4vh;
  width: 1px;
}

.ss-grid-line--top    { top: 8vh; }
.ss-grid-line--bottom { bottom: 8vh; }
.ss-grid-line--left   { left: 8vw; }
.ss-grid-line--right  { right: 8vw; }

.ss-grid-accent {
  position: absolute;
  top: 8vh;
  left: 8vw;
  width: 40px;
  height: 40px;
  border-top: 1px solid rgba(255, 255, 255, 0.3);
  border-left: 1px solid rgba(255, 255, 255, 0.3);
}

/* ── Contenu ── */

.ss-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: min(700px, 85vw);
}

/* Label Projet */
.ss-eyebrow {
  color: #888;
  font-family: 'Inter', sans-serif;
  font-size: clamp(0.6rem, 0.8vw, 0.75rem);
  font-weight: 300;
  letter-spacing: 0.5em;
  text-transform: uppercase;
  margin-bottom: 1.2rem;
  opacity: 0.8;
}

/* Séparateur minimaliste */
.ss-rule {
  width: 40px;
  height: 1px;
  background: rgba(255, 255, 255, 0.2);
  margin-bottom: 1.6rem;
  transition: width 0.6s ease;
}

#start-screen:hover .ss-rule {
  width: 80px;
}

.ss-rule--short {
  margin-bottom: 1.2rem;
}

/* Titre Principal — L'élément central.
   C'est un <h1> depuis l'audit d'accessibilité : `margin-top: 0` annule la
   marge par défaut des titres, la mise en forme ci-dessous fait le reste. */
.ss-title {
  margin-top: 0;
  color: #fff;
  font-family: 'Playfair Display', serif;
  font-size: clamp(2.5rem, 6vw, 5.5rem);
  font-weight: 400;
  line-height: 1.1;
  margin-bottom: 1.6rem;
  letter-spacing: -0.02em;
  word-break: keep-all;
  overflow-wrap: normal;
}

/* Corps de texte */
.ss-body {
  max-width: 45ch;
  color: #aaa;
  font-family: 'Inter', sans-serif;
  font-size: clamp(0.9rem, 1.1vw, 1.05rem);
  font-weight: 300;
  line-height: 1.8;
  letter-spacing: 0.02em;
  margin-bottom: 1.6rem;
}

/* Crédits en bas */
.ss-credits {
  display: flex;
  align-items: center;
  gap: 1rem;
  color: #666;
  font-family: 'Inter', sans-serif;
  font-size: 0.65rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.ss-credits__sep {
  width: 15px;
  height: 1px;
  background: rgba(255, 255, 255, 0.1);
}

/* Call To Action */
/* .ss-cta est désormais un <button> (il était un <div> inatteignable au
   clavier). Ces quatre lignes annulent le style natif du navigateur pour que
   le rendu reste EXACTEMENT celui d'avant — le reste de la règle, inchangé,
   dessinait déjà la pastille. `line-height: inherit` reproduit l'héritage
   qu'avait le div ; sans elle, le `normal` des boutons changerait la hauteur. */
.ss-cta {
  appearance: none;
  -webkit-appearance: none;
  line-height: inherit;
  text-align: center;
  cursor: pointer;
  margin-top: 1.8rem;
  color: rgba(255, 255, 255, 0.75);
  font-family: 'Inter', sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  padding: 12px 24px;
  border: 1px solid rgba(255, 255, 255, 0.45);
  border-radius: 50px;
  background: transparent;
  transition: color 0.4s ease, border-color 0.4s ease, background 0.4s ease;
}

.ss-cta:hover {
  color: #000;
  background: #fff;
  border-color: #fff;
}

/* Animation de flottement très subtile sur le titre */
@keyframes titleFloat {
  0% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}

.ss-title {
  animation: titleFloat 6s ease-in-out infinite;
}

/* ══════════════════════════════════════════════════════════════════
   CHAPITRE 2 — Containment
   ─────────────────────────────────────────────────────────────────
   #chapitre2-root est position:fixed avec z-index:500.
   Il absorbe les événements souris et empêche tout scroll
   du document hôte pendant l'expérience chapitre 2.
   ══════════════════════════════════════════════════════════════════ */

/* position:fixed suffit pour l'isolation — pas de contain:layout (casserait
   les enfants fixed). z-index:500 le garantit toujours au-dessus de tous les
   layers du projet principal (canvas, veil, etc.).
   ⚠️ Il y avait ici, en plus, une neutralisation du curseur propre au chapitre
   2 (« body.chp2-active, body.chp2-active * {cursor:none!important} ») —
   devenue inutile : la règle générale `body.is-touch * {cursor:none!important}`
   (cf. section CURSEUR en tête de fichier) couvre déjà tous les descendants
   de body, chapitre 2 compris, et ne s'applique plus qu'au tactile. */
#chapitre2-root {
  z-index: 500;
}

/* Pendant le chapitre 2, les 3 niveaux de titre, le bouton plein écran, ainsi
   que la citation de sortie (#chapter-quote) et son bouton « Passer »
   (#skip-btn) doivent flotter AU-DESSUS du root plein écran (z 500) et de
   toutes ses sous-parties (confinées dans le contexte d'empilement de
   #chapitre2-root, donc plafonnées à 500 vues d'ici). Sans contenu, la quote et
   le bouton restent invisibles (opacité 0 / pointer-events none) : aucun effet
   de bord hors séquence de sortie. */
body.chp2-active #site-title,
body.chp2-active #chapitre-subtitle,
body.chp2-active #chapitre-part-title,
body.chp2-active #fs-btn,
body.chp2-active #chapter-quote,
body.chp2-active #skip-btn {
  z-index: 600;
}

/* Pendant l'installation « Invisibilisation », l'overlay immersif (et donc son
   onglet de légende déroulant, confiné dans le contexte d'empilement de
   #chapitre2-root) doit passer AU-DESSUS des titres/sous-titres (z 600).
   Comme le contexte d'empilement de #chapitre2-root plafonne ses enfants à sa
   propre valeur vue depuis #app, on élève le root au-dessus des titres le temps
   de l'installation ; il revient à 500 dès la fermeture (classe retirée). */
/* Pendant l'installation « Invisibilisation », les titres/sous-titres top-left
   doivent rester visibles ET le bouton plein écran toujours présent. Le volet
   de légende ne peut pas passer au-dessus des titres par z-index (il est confiné
   dans le contexte d'empilement de #chapitre2-root, z500, alors que les titres
   sont à z600) : on estompe donc les titres dans deux cas — quand le volet de
   légende est déplié (invisibilisation-legend-open) ET pendant la lecture d'un
   média (invisibilisation-media) — puis on les réaffiche en sortant. Le bouton
   plein écran (#fs-btn) n'est touché par AUCUNE de ces règles. */
body.invisibilisation-legend-open #site-title,
body.invisibilisation-legend-open #chapitre-subtitle,
body.invisibilisation-legend-open #chapitre-part-title,
body.invisibilisation-media #site-title,
body.invisibilisation-media #chapitre-subtitle,
body.invisibilisation-media #chapitre-part-title {
  opacity: 0 !important;
  pointer-events: none !important;
  transition: opacity 400ms ease;
}

/* ══════════════════════════════════════════════════════════════════
   CHAPITRE 3 — Containment
   ─────────────────────────────────────────────────────────────────
   Mêmes garanties d'empilement que le chapitre 2. #chapitre3-root est
   position:fixed / z-index:500 (posé aussi en inline par Chapitre3Scene).
   ══════════════════════════════════════════════════════════════════ */

#chapitre3-root {
  z-index: 500;
}

/* Le titre « Espace collaboratif », le sous-titre du chapitre et le bouton
   plein écran sont en z 15 : sans cette remontée ils resteraient ENTERRÉS sous
   #chapitre3-root (z 500). On les fait flotter au-dessus, exactement comme
   body.chp2-active le fait pour le chapitre 2. #chapitre-part-title n'est pas
   listé : le chapitre 3 n'a pas de sous-parties (tier 3 inutilisé).
   #chapter-quote et #skip-btn restent inertes sans contenu (opacité 0 /
   pointer-events none) : aucun effet de bord. */
body.chp3-active #site-title,
body.chp3-active #chapitre-subtitle,
body.chp3-active #fs-btn,
body.chp3-active #chapter-quote,
body.chp3-active #skip-btn {
  z-index: 600;
}

/* ══════════════════════════════════════════════════════════════════
   CHAPITRE 4 — « Une histoire complexe »
   ────────────────────────────────────────────────────────────────────
   LE SEUL ÉCRAN CLAIR DU SITE — mais PAS PARTOUT, et c'est toute la
   subtilité de cette section. L'écran est coupé en deux : la moitié
   gauche est occupée par la photographie du crâne, sur un tissu SOMBRE ;
   la moitié droite est le papier, où se dessine le schéma.

   Conséquence : les éléments d'interface ne se traitent pas en bloc.
     · Titre, sous-titre (haut-gauche) et flèche de retour (bas-gauche)
       tombent sur la PHOTO. Ils gardent donc l'or et le blanc du site —
       les avoir passés à l'encre sombre les avait rendus illisibles
       (essayé, vérifié en capture).
     · Le bouton plein écran (bas-DROITE) tombe sur le PAPIER. Lui, et lui
       seul, doit passer à l'encre sombre : son SVG est tracé en blanc,
       il disparaîtrait — et avec lui le moyen de quitter le plein écran.
     · Le curseur custom (#cursor) traverse les deux, mais ne concerne plus
       QUE le tactile depuis que le reste du site a repris le curseur natif
       de l'OS (cf. section CURSEUR en tête de fichier) : on ne touche pas à
       sa couleur (ivoire, pensée pour les deux fonds) mais on renforce son
       liseré, seul garant de sa lisibilité sur le papier.

   Le fond du chapitre lui-même (papier, dessin, bulles, pop-up de la
   carte) vit dans Chapitre4/chp4-style/chp4-opening.css, injecté par la
   scène. Ici, seulement ce qui appartient au TRONC COMMUN.
   ══════════════════════════════════════════════════════════════════ */

#chapitre4-root {
  z-index: 500;
}

/* Remontée au-dessus de #chapitre4-root (z 500) — même mécanique qu'aux
   chapitres 2 et 3. #chapitre-part-title n'est pas listé : le chapitre 4
   n'a pas de sous-parties. */
body.chp4-active #site-title,
body.chp4-active #chapitre-subtitle,
body.chp4-active #fs-btn,
body.chp4-active #chapter-quote,
body.chp4-active #skip-btn {
  z-index: 600;
}

/* Bouton plein écran → encre sombre : il est seul sur le papier.
   ⚠️ LE SÉLECTEUR VISE LES ENFANTS, et `!important` n'est pas de la
   paresse. Fullscreen.js réécrit le SVG à chaque rendu en posant
   `stroke="rgba(255,255,255,0.75)"` sur CHAQUE <polyline> et <line> :
   une règle sur le <svg> parent ne les atteint pas (un attribut de
   présentation sur l'enfant l'emporte sur l'héritage). Et au survol, le
   même fichier écrit la couleur dorée en style EN LIGNE — que seul
   `!important` peut contredire. Sans lui, le bouton s'effacerait
   précisément au moment où l'on tend la souris vers lui. */
body.chp4-active #fs-btn svg,
body.chp4-active #fs-btn svg * {
  stroke: rgba(28, 24, 19, 0.72) !important;
}

body.chp4-active #fs-btn:hover svg * {
  stroke: rgba(150, 110, 28, 0.95) !important;
}

/* Curseur À L'ENCRE : sur le papier crème, l'ivoire du curseur du site se
   dissolvait — la flèche se devinait à son liseré, pas plus.
   Le liseré est donc INVERSÉ en même temps que le remplissage : flèche
   sombre cernée de blanc. C'est ce cerne clair qui la sauve quand elle
   passe sur la photographie sombre de la moitié gauche — sans lui, on
   n'aurait fait que déplacer le problème d'un fond à l'autre. */
body.chp4-active #cursor {
  background-color: rgba(20, 17, 13, 0.95);
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.92))
          drop-shadow(0 1px 2.5px rgba(0, 0, 0, 0.35));
}

body.chp4-active #cursor.hotspot {
  background-color: rgba(140, 100, 22, 1);
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.9))
          drop-shadow(0 0 9px rgba(176, 132, 42, 0.5));
}

body.chp4-active #cursor.active {
  background-color: rgba(96, 68, 14, 1);
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.9));
}

/* ══════════════════════════════════════════════════════════════════
   CURSEUR — flèche élégante (remplace le point pâle)
   ────────────────────────────────────────────────────────────────────
   Placé EN FIN DE FICHIER : ces règles priment sur celles de la section
   CURSOR plus haut par ordre de source (même spécificité).

   Principe :
   - #cursor devient une flèche (silhouette SVG via `mask`), remplie par
     `background-color` → recolorable par état (ivoire → doré au survol).
   - La POINTE est au coin haut-gauche du SVG (0,0) ; `transform-origin:0 0`
     garde la pointe fixée sur le point de clic même quand la flèche grossit.
   - `filter: drop-shadow(...)` donne un liseré sombre + une profondeur →
     lisible sur fond clair comme sombre, et remplace les `box-shadow`
     (qui, sur une forme masquée, produiraient un halo rectangulaire).
   - Le viseur encre de la sous-partie « peine démesurée » (body.peine-aim)
     est préservé : on y neutralise le mask.
   ════════════════════════════════════════════════════════════════════ */

#cursor {
  /* Silhouette flèche — fill blanc pour être robuste (alpha ET luminance). */
  --cursor-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 20'%3E%3Cpath fill='%23fff' d='M0 0 L0 14.6 L4 11.3 L6.35 17.3 L8.6 16.4 L6.2 10.6 L11.1 10.6 Z'/%3E%3C/svg%3E");

  width: 17px;
  height: 19px;
  border-radius: 0;
  background: none;
  background-color: rgba(255, 249, 233, 0.97);   /* ivoire chaud */

  -webkit-mask: var(--cursor-arrow) no-repeat 0 0 / contain;
          mask: var(--cursor-arrow) no-repeat 0 0 / contain;

  /* Pointe (coin haut-gauche du SVG) posée sur le point de clic. */
  transform-origin: 0 0;
  transform: translate(-1px, -1px);

  /* Liseré sombre net (visibilité universelle) + légère profondeur. */
  filter:
    drop-shadow(0 0 0.6px rgba(0, 0, 0, 0.9))
    drop-shadow(0 1.5px 2px rgba(0, 0, 0, 0.38));

  box-shadow: none;
  transition:
    transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1),
    background-color 0.22s ease,
    filter 0.22s ease,
    opacity 0.22s ease;
}

/* Clic (mousedown) — la flèche s'enfonce légèrement, teinte dorée. */
#cursor.active {
  width: 17px;
  height: 19px;
  background-color: rgba(255, 226, 158, 1);
  transform: translate(-1px, -1px) scale(0.88);
  box-shadow: none;
  filter:
    drop-shadow(0 0 0.6px rgba(0, 0, 0, 0.85))
    drop-shadow(0 0 6px rgba(255, 200, 70, 0.55));
  animation: none;
}

/* Survol d'un élément cliquable — flèche dorée, plus grande, glow doux. */
#cursor.hotspot {
  width: 17px;
  height: 19px;
  background-color: rgba(255, 216, 122, 1);
  transform: translate(-1px, -1px) scale(1.32);
  box-shadow: none;
  filter:
    drop-shadow(0 0 0.6px rgba(120, 80, 0, 0.55))
    drop-shadow(0 0 8px rgba(255, 205, 80, 0.85))
    drop-shadow(0 0 16px rgba(255, 170, 30, 0.45));
  animation: cursorArrowPulse 1.15s ease-in-out infinite;
}

/* Variante atténuée (yeux d'invisibilisation, chapitre 2). */
#cursor.hotspot-soft {
  width: 17px;
  height: 19px;
  background-color: rgba(255, 232, 178, 0.98);
  transform: translate(-1px, -1px) scale(1.12);
  box-shadow: none;
  filter:
    drop-shadow(0 0 0.6px rgba(0, 0, 0, 0.7))
    drop-shadow(0 0 6px rgba(255, 205, 80, 0.55));
  animation: none;
}

/* Pulsation douce ancrée sur la pointe (transform-origin:0 0 hérité). */
@keyframes cursorArrowPulse {
  0%, 100% { transform: translate(-1px, -1px) scale(1.32); }
  50%      { transform: translate(-1px, -1px) scale(1.46); }
}

/* ── Préservation du viseur « peine démesurée » (chapitre 2) ──────────
   On neutralise le mask flèche pour rendre au curseur sa forme d'origine
   (point/viseur encre + ::after) et on restaure l'ancrage CENTRÉ (au repos
   il n'a pas de transform propre — il héritait donc de la base). Les états
   focalisés .aim-hot/.hotspot repositionnent eux-mêmes via l'animation
   peineAimPulse. Spécificité (1,1,1) > #cursor (1,0,0). */
body.peine-aim #cursor,
body.peine-aim #cursor.hotspot,
body.peine-aim #cursor.aim-hot {
  -webkit-mask: none;
          mask: none;
  transform-origin: center;
  transform: translate(-50%, -50%);
  filter: none;
}

/* ══════════════════════════════════════════════════════════════════
   DOCUMENTS & « À PROPOS » — overlay de la scène phrénologie
   ──────────────────────────────────────────────────────────────────
   EMPILEMENT :  #veil (6) < #doc-overlay (7) < #nav-bar (8) < #doc-btns (9)
   L'overlay capte le clic « n'importe où » pour se fermer tout en laissant
   boutons et navbar cliquables au-dessus.

   MISE EN PAGE : la zone utile évite les régions occupées —
     droite : colonne des boutons · haut : titre · bas : barre de navigation.
   Les cadres sont DIMENSIONNÉS EN JS (hauteur commune, largeurs déduites des
   ratios réels des médias) : le CSS ne fait que définir la zone disponible.
   ══════════════════════════════════════════════════════════════════ */

#doc-overlay {
  position: absolute;
  inset: 0;
  z-index: 7;
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

#doc-overlay.visible {
  opacity: 1;
  pointer-events: auto;
}

/* LE VOILE — pas un fond, un voile : l'image phrénologie reste perceptible
   dessous. Le dégradé radial garde son creux au centre (là où l'on lit) et se
   ferme aux bords ; le facteur 1.15 conserve l'écart entre centre et bords quel
   que soit le réglage.
   ⚠️ AUCUN repli dans var() : l'opacité se règle dans config.js
   (DOCS.overlay.veil_opacity) et nulle part ailleurs. Un repli ici serait un
   second chiffre, invisible et sans effet — ce fichier en a porté un (0.82)
   pendant que la config disait 0.80. _applyVeil() pose toujours la variable
   avant que ce fond s'affiche, et crie en console si la clé manque. */
.doc-ov-backdrop {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 42% 50%,
    rgba(8, 8, 8, var(--doc-ov-veil)) 0%,
    rgba(0, 0, 0, calc(var(--doc-ov-veil) * 1.15)) 100%);
}

/* ══════════════════════════════════════════════════════════════════
   STAGE — zone utile.
   inset vertical = marges calculées en JS (--doc-ov-margin-top/-bot), qui
   intègrent un plancher : on ne chevauche JAMAIS le titre en haut ni la navbar
   en bas. inset horizontal = 0 : on gère les côtés au niveau des enfants.
   La zone est un repère de POSITIONNEMENT (position:relative) : le document et
   la légende y sont placés INDÉPENDAMMENT l'un de l'autre → aucune interférence
   de hauteur, la légende ne peut plus "partir en haut".
   ══════════════════════════════════════════════════════════════════ */
.doc-ov-stage {
  position: absolute;
  top:    var(--doc-ov-margin-top, clamp(74px, 13vh, 150px));
  bottom: var(--doc-ov-margin-bot, clamp(74px, 13vh, 150px));
  left:  0;
  right: 0;
}

.doc-ov-inner {
  position: relative;
  width: 100%;
  height: 100%;
}

/* ── DOCUMENT — centré dans la fenêtre, décalé de la moitié de l'asymétrie ──
   La zone centrale disponible = fenêtre − 2·(colonne latérale). Le document est
   centré horizontalement dans CETTE zone, donc au milieu de la fenêtre (les deux
   colonnes latérales étant égales). Centré verticalement dans le stage. */
.doc-ov-row {
  position: absolute;
  top: 0;
  bottom: 0;
  left:  var(--doc-ov-side, clamp(140px, 20vw, 300px));
  right: var(--doc-ov-side, clamp(140px, 20vw, 300px));
  padding: 0 clamp(8px, 1.2vw, 20px);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2.2vw, 38px);
}

/* ── LÉGENDE — colonne gauche, ancrée. Sa position ne dépend QUE du stage :
   collée au bord gauche, largeur = colonne latérale, centrée verticalement,
   texte calé à droite (contre le document). Indépendante du document. ── */
.doc-ov-caption {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--doc-ov-side, clamp(140px, 20vw, 300px));
  display: flex;
  flex-direction: column;
  /* Le BAS de la légende s'aligne sur le BAS du document : on pousse le contenu
     vers le bas, avec un retrait égal à --doc-ov-cap-bottom (posé en JS =
     distance entre le bas de la zone et le bas du rectangle du document). */
  justify-content: flex-end;
  align-items: flex-end;
  padding: 0 clamp(24px, 2.2vw, 40px) var(--doc-ov-cap-bottom, 0px) clamp(18px, 2.4vw, 40px);
  text-align: right;
}

/* ── Barre verticale fine à droite de la légende ────────────────────────────
   Filet doré vertical, centré sur la hauteur de la légende. Apparition
   cinématographique : il se DESSINE du centre vers les deux extrémités
   (scaleY 0 → 1), après quoi le texte se révèle. */
.doc-ov-caption::after {
  content: '';
  position: absolute;
  right: clamp(11px, 1vw, 18px);
  /* Alignée sur le texte : même retrait bas que la légende, hauteur = celle du
     bloc de texte (le contenu est calé en bas). */
  bottom: var(--doc-ov-cap-bottom, 0px);
  height: min(58%, 11em);
  width: 1px;
  background: linear-gradient(
      to bottom,
      rgba(210, 175, 90, 0) 0%,
      rgba(210, 175, 90, 0.75) 20%,
      rgba(210, 175, 90, 0.75) 80%,
      rgba(210, 175, 90, 0) 100%);
  transform: scaleY(0);
  transform-origin: center;
  opacity: 0;
  transition:
    transform 1100ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 700ms ease;
}

/* Le contenu textuel : légère montée + fondu, décalé APRÈS le tracé du filet. */
.doc-ov-caption .doc-ov-cap-text,
.doc-ov-caption .doc-ov-source {
  opacity: 0;
  transform: translateX(-8px);
  transition:
    opacity 1000ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
    transform 1100ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* ── Séquence d'apparition, déclenchée par .in (posé par le JS) ──────────── */
.doc-ov-caption.in::after {
  opacity: 1;
  transform: scaleY(1);                        /* le filet se dessine d'abord */
}
.doc-ov-caption.in .doc-ov-cap-text {
  opacity: 1;
  transform: translateX(0);
  transition-delay: 620ms;                      /* puis le titre */
}
.doc-ov-caption.in .doc-ov-source {
  opacity: 1;
  transform: translateX(0);
  transition-delay: 860ms;                      /* enfin la source */
}


/* ── TEXTE « À PROPOS » — centré dans la zone centrale (mêmes bornes que le
   document : entre les deux colonnes), défilable, texte aligné à gauche. ── */
.doc-ov-text {
  position: absolute;
  top: 0;
  bottom: 0;
  left:  var(--doc-ov-side, clamp(140px, 20vw, 300px));
  right: var(--doc-ov-side, clamp(140px, 20vw, 300px));

  overflow-y: auto;
  touch-action: pan-y;   /* le texte défile au doigt (dérogation au #app) */
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;

  /* Barre de défilement à GAUCHE : on inverse le sens d'écriture du conteneur
     (la barre se place alors du côté "début" = gauche). Le contenu est remis
     en ltr juste en dessous. */
  direction: rtl;

  color: rgba(255, 255, 255, 0.94);
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 300;
  letter-spacing: 0.012em;

  /* Fondu haut/bas : le texte s'estompe vers les bords quand il y a de la
     matière à faire défiler au-dessus / en dessous. Les zones de fondu
     (--fade-top / --fade-bot, en px) sont pilotées par le JS selon la position
     de défilement → l'indice n'apparaît que s'il reste du texte de ce côté. */
  --fade-top: 0px;
  --fade-bot: 0px;
  -webkit-mask: linear-gradient(
      to bottom,
      transparent 0,
      #000 var(--fade-top),
      #000 calc(100% - var(--fade-bot)),
      transparent 100%);
          mask: linear-gradient(
      to bottom,
      transparent 0,
      #000 var(--fade-top),
      #000 calc(100% - var(--fade-bot)),
      transparent 100%);
}

/* Contenu remis dans le sens normal (le rtl ne sert qu'à placer la barre). */
.doc-ov-text-body {
  direction: ltr;
  width: 100%;
  max-width: calc(74ch * var(--doc-ov-about-frac, 1));
  margin: 0 auto;
  padding: 0.5em 0;
  text-align: left;
}
/* Barre de défilement — accord DA : filet doré patiné, très fin, contre le
   texte (à gauche via direction:rtl du conteneur). */
.doc-ov-text {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 700ms ease;
}
.doc-ov-text.is-scroll { scrollbar-color: rgba(210, 175, 90, 0.55) transparent; }
.doc-ov-text::-webkit-scrollbar        { width: 5px; }
.doc-ov-text::-webkit-scrollbar-track  { background: transparent; }
.doc-ov-text::-webkit-scrollbar-thumb  {
  background: linear-gradient(rgba(210,175,90,0.0), rgba(203,167,90,0.0));
  border-radius: 3px;
  transition: background 700ms ease;
}
/* Apparition : le filet doré se révèle en fondu quand le texte devient
   défilable (.is-scroll posé par le JS après mesure). */
.doc-ov-text.is-scroll::-webkit-scrollbar-thumb {
  background: linear-gradient(rgba(210,175,90,0.65), rgba(203,167,90,0.35));
}
.doc-ov-text::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(rgba(231,206,142,0.85), rgba(210,175,90,0.55));
}

/* Débordement → ancrer en haut (sinon justify-content:center coupe le début). */
.doc-ov-text.is-scroll { justify-content: flex-start; }

/* Corps : bloc centré horizontalement, texte aligné à gauche. */


.doc-ov-text p {
  margin: 0 0 0.95em;
  font-size: 1em;
  line-height: inherit;
  text-wrap: pretty;
  text-shadow: 0 0 26px rgba(0, 0, 0, 0.85);

  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 1.25s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    transform 1.35s cubic-bezier(0.16, 1, 0.3, 1);
}

.doc-ov-text p.in { opacity: 1; transform: translateY(0); }
.doc-ov-text p:last-child { margin-bottom: 0; }

.doc-ov-text p:first-child {
  color: rgba(255, 255, 255, 0.99);
  font-style: italic;
  font-size: 1.14em;
  margin-bottom: 1.15em;
}

/* ── Cadres ────────────────────────────────────────────────────────── */
.doc-ov-frame {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  touch-action: none;   /* le glissé du doigt pilote la loupe, pas le scroll */
  background: rgba(0, 0, 0, 0.32);
}

.doc-ov-frame-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 2;
}

.doc-ov-rect {
  fill: none;
  stroke: rgba(255, 255, 255, 0.82);
  stroke-width: 1;
}

.doc-ov-media {
  opacity: 0;
  transition: opacity 1200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.doc-ov-media.in { opacity: 1; }

.doc-ov-frame img.doc-ov-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 8px;
  -webkit-user-drag: none;
  user-select: none;
}

.doc-ov-frame iframe.doc-ov-embed {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: #0b0b0b;
}

.doc-ov-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-height: 140px;
  padding: 2em;
  color: rgba(255, 255, 255, 0.70);
  font-family: 'Cinzel', serif;
  font-size: clamp(0.62rem, 0.86vw, 0.8rem);
  letter-spacing: 0.18em;
  text-align: center;
  text-transform: uppercase;
}

.doc-ov-missing a {
  color: rgba(255, 220, 120, 0.95);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 220, 120, 0.35);
  padding-bottom: 2px;
  transition: color 0.3s ease, border-color 0.3s ease;
}
.doc-ov-missing a:hover { color: rgba(255, 235, 175, 1); border-color: rgba(255, 220, 120, 0.9); }

/* ── Légende : texte + source ──────────────────────────────────────── */
.doc-ov-cap-text {
  display: block;
  color: rgba(255, 255, 255, 0.80);
  font-family: 'Cormorant Garamond', Georgia, serif;
  /* Taille FLUIDE, indexée sur la largeur de la colonne (--doc-ov-side) : la
     légende rapetisse quand la colonne se resserre, au lieu de déborder ou de
     forcer un retour à la ligne disgracieux. Bornée pour rester lisible. */
  font-size: clamp(0.66rem, calc(var(--doc-ov-side, 200px) * 0.072), 1.12rem);
  font-style: italic;
  font-weight: 300;
  line-height: 1.5;
  text-shadow: 0 0 20px rgba(0, 0, 0, 0.9);
}

.doc-ov-source {
  display: inline-block;
  margin-top: 0.85em;
  color: rgba(210, 175, 90, 0.92);
  font-family: 'Cinzel', serif;
  font-size: clamp(0.5rem, calc(var(--doc-ov-side, 200px) * 0.05), 0.72rem);
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid rgba(210, 175, 90, 0.3);
  padding-bottom: 3px;
  pointer-events: auto;
  transition: color 0.35s ease, border-color 0.35s ease;
}
.doc-ov-source:hover { color: rgba(255, 230, 150, 1); border-color: rgba(255, 220, 120, 0.85); }

@media (max-height: 520px) {
  .doc-ov-cap-text { line-height: 1.35; }
  .doc-ov-row      { gap: clamp(8px, 1.6vw, 18px); }
}

/* Écran étroit : on rend de la largeur aux planches, qui restent côte à côte
   (leur intérêt est la comparaison). */
/* Pas de bascule de structure sur écran étroit : la mise en page reste
   IDENTIQUE à toute largeur (légende à gauche, document centré). La légende
   rapetisse via clamp() si nécessaire. En deçà de MIN_SIZE.width (500px), le
   viewport est de toute façon verrouillé par #app. */

/* Mouvement réduit : on garde les apparitions, sans translation ni tracé. */
@media (prefers-reduced-motion: reduce) {
  .doc-ov-text p,
  .doc-ov-caption { transform: none; transition-duration: 400ms; }
  .doc-ov-rect    { transition: none !important; }
}

/* ══════════════════════════════════════════════════════════════════
   LOUPE — documents images (doc-1, doc-2)
   Cercle attaché au curseur : petit indicateur partout, grande loupe ×3
   au survol d'une image. Filet blanc discret.
   ══════════════════════════════════════════════════════════════════ */
.doc-ov-loupe {
  position: fixed;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  z-index: 9998;                       /* sous le curseur custom (#cursor:9999) */
  pointer-events: none;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background-repeat: no-repeat;
  background-color: rgba(0, 0, 0, 0.15);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.25),
    0 6px 26px rgba(0, 0, 0, 0.45);
  /* Transition douce entre l'indicateur et la loupe. La position (left/top) est
     posée en JS à chaque frame : surtout NE PAS la mettre en transition. */
  transition:
    width 260ms cubic-bezier(0.16, 1, 0.3, 1),
    height 260ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 260ms ease,
    border-color 260ms ease;
  will-change: left, top, width, height;
}

/* Mode loupe (sur image) : bord un peu plus present, halo interne subtil. */
.doc-ov-loupe.is-zoom {
  border-color: rgba(255, 255, 255, 0.7);
  background-color: #0b0b0b;
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.4),
    0 10px 34px rgba(0, 0, 0, 0.55),
    inset 0 0 18px rgba(0, 0, 0, 0.35);
}

/* Sur écran tactile, pas de loupe (pas de curseur qui plane). */
/* La loupe fonctionne aussi au TACTILE : elle suit le doigt (pointermove) et
   affiche le zoom au contact d'une image. Pas de blocage mobile. */
@media (prefers-reduced-motion: reduce) {
  .doc-ov-loupe { transition: opacity 200ms ease; }
}


/* ── Flou aux extrémités du texte « À Propos » ──────────────────────────────
   Deux bandes fixes par-dessus le texte défilant : le flou (backdrop-filter)
   renforce l'indice de défilement, en plus du fondu au noir. Leur présence est
   pilotée par --fade-top/--fade-bot (0 quand il n'y a rien à défiler de ce
   côté). N'apparaissent que sur le texte défilable (.is-scroll). */
.doc-ov-text-blur {
  position: absolute;
  left:  var(--doc-ov-side, clamp(140px, 20vw, 300px));
  right: var(--doc-ov-side, clamp(140px, 20vw, 300px));
  height: 74px;
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  transition: opacity 300ms ease;
}
.doc-ov-text-blur.top {
  top: 0;
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
  -webkit-mask: linear-gradient(to bottom, #000 0%, #000 35%, transparent 100%);
          mask: linear-gradient(to bottom, #000 0%, #000 35%, transparent 100%);
}
.doc-ov-text-blur.bot {
  bottom: 0;
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
  -webkit-mask: linear-gradient(to top, #000 0%, #000 35%, transparent 100%);
          mask: linear-gradient(to top, #000 0%, #000 35%, transparent 100%);
   }

/* ══════════════════════════════════════════════════════════════════
   « À PROPOS » — mise en scène en deux temps (src/ui/AboutReveal.js)
   ──────────────────────────────────────────────────────────────────
   1. L'ACCROCHE se trace lettre à lettre (SVG, comme à l'encre), justifiée.
      Mots-clefs : blanc → embrasement → doré. Soulignement différé.
   2. Elle REMONTE ; le corps se matérialise lettre par lettre (ordre
      aléatoire, Inter). Les passages en relief s'éclairent une fois posés.

   Les DÉLAIS par lettre sont posés en style inline par le JS ; ici ne
   vivent que les DURÉES, les couleurs et les états (survol, skip, reduce).
   Palette : or du site rgba(210,175,90) et ses éclats.
   ══════════════════════════════════════════════════════════════════ */

/* Zone : entre les deux colonnes latérales, mais la boîte est CALÉE SUR LE
   TEXTE (max-width = corps 1040px + marges) et recentrée par margin:auto :
   l'ascenseur, porté par cette boîte, tombe ainsi CONTRE le texte avec une
   petite marge (le padding), jamais au bord lointain de la zone.
   Fermée au défilement en phase 1 ; s'ouvre en phase 2 si le contenu déborde
   (petits écrans / tactile → .is-scroll). */
.doc-ov-about {
  position: absolute;
  top: 0;
  bottom: 0;
  left:  var(--doc-ov-side, clamp(140px, 20vw, 300px));
  right: var(--doc-ov-side, clamp(140px, 20vw, 300px));
  max-width: calc(1040px + 2 * clamp(12px, 2vw, 24px));
  margin: 0 auto;
  /* Marge latérale : le texte respire, jamais collé aux colonnes — et côté
     gauche c'est elle qui sépare le texte de l'ascenseur. Le JS mesure la
     largeur RÉELLE du contenu (via .ab-stack) pour composer l'accroche, ce
     padding est donc pris en compte dans la justification. */
  padding: 0 clamp(12px, 2vw, 24px);
  overflow: hidden;
  /* La place de l'ascenseur est réservée des DEUX côtés dès la phase 1 :
     aucun changement de largeur (donc aucune recomposition du SVG) quand le
     défilement s'ouvre en phase 2. */
  scrollbar-gutter: stable both-edges;
  touch-action: pan-y;     /* lecture au doigt (dérogation au #app) */

  /* Barre de défilement à GAUCHE du texte : on inverse le sens d'écriture du
     conteneur (la barre se place alors du côté "début" = gauche), comme pour
     .doc-ov-text. Le contenu (.ab-stack) est remis en ltr juste en dessous —
     glyphes SVG et bidi latin ne dépendent pas de cette bascule, seule la
     position de l'ascenseur en dépend. */
  direction: rtl;

  /* Fondu haut/bas pendant le défilement (vars pilotées par AboutReveal). */
  --fade-top: 0px;
  --fade-bot: 0px;
}
.doc-ov-about.is-scroll.ab-phase2 {
  overflow-y: auto;
  -webkit-mask: linear-gradient(
      to bottom,
      transparent 0,
      #000 var(--fade-top),
      #000 calc(100% - var(--fade-bot)),
      transparent 100%);
          mask: linear-gradient(
      to bottom,
      transparent 0,
      #000 var(--fade-top),
      #000 calc(100% - var(--fade-bot)),
      transparent 100%);
}

/* Bandes de flou haut/bas (indice de défilement, en plus du fondu) : collées
   aux bords de la fenêtre de lecture par position:sticky, sans emprise dans
   le flux (marges négatives). Opacité pilotée par AboutReveal._updateFade. */
.ab-scrollblur {
  position: sticky;
  height: 74px;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms ease;
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
}
.ab-scrollblur.top {
  top: 0;
  margin-bottom: -74px;
  -webkit-mask: linear-gradient(to bottom, #000 0%, #000 35%, transparent 100%);
          mask: linear-gradient(to bottom, #000 0%, #000 35%, transparent 100%);
}
.ab-scrollblur.bot {
  bottom: 0;
  margin-top: -74px;
  -webkit-mask: linear-gradient(to top, #000 0%, #000 35%, transparent 100%);
          mask: linear-gradient(to top, #000 0%, #000 35%, transparent 100%);
}

/* Barre de défilement — même filet doré patiné que le texte « À Propos »
   d'origine (n'apparaît que si .is-scroll autorise le défilement). */
.doc-ov-about { scrollbar-width: thin; scrollbar-color: rgba(210, 175, 90, 0.55) transparent; }
.doc-ov-about::-webkit-scrollbar       { width: 5px; }
.doc-ov-about::-webkit-scrollbar-track { background: transparent; }
.doc-ov-about::-webkit-scrollbar-thumb {
  background: linear-gradient(rgba(210, 175, 90, 0.65), rgba(203, 167, 90, 0.35));
  border-radius: 3px;
}

/* La colonne accroche + corps. La REMONTÉE de la phase 2 est portée par la
   translation de ce bloc (translateY posé en JS : centré puis 0).
   direction:ltr — annule le rtl du conteneur (posé pour la barre à gauche) :
   le contenu reste dans le sens normal. */
.ab-stack {
  direction: ltr;
  width: 100%;
  transition: transform 1500ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.ab-stack.no-trans { transition: none; }   /* recentrage sec au resize */

/* ── PHASE 1 : l'accroche calligraphiée ───────────────────────────────────── */

/* Cadre de positionnement : porte le SVG principal et, superposées, les
   couches de halo des mots-clefs (clones flous — voir plus bas). */
.ab-hookwrap {
  position: relative;
  width: min(100%, 1040px);   /* même plafond que la composition JS (effW) */
  margin: 0 auto clamp(20px, 3.4vh, 44px);
}

.ab-hook {
  display: block;
  width: 100%;
  height: auto;
  margin: 0;
  overflow: visible;          /* les halos débordent des glyphes */
}

/* Chaque lettre : un tspan positionné par le JS. Le tracé se révèle par
   stroke-dashoffset, puis la lettre « s'encre » (fill) et le contour s'efface. */
.ab-hook tspan {
  fill: transparent;
  stroke-width: 1.15;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 340;      /* réserve > contour du plus long glyphe */
  stroke-dashoffset: 340;
}

/* La fonte de l'accroche : Playfair Display — la voix « titre » du site.
   La TAILLE (unités logiques du viewBox) est posée en inline par le JS,
   pour coller exactement aux mesures de composition. */
.ab-hook text { font-family: 'Playfair Display', Georgia, serif; }
.ab-t--base   { font-weight: 400; }

/* Le passage frappé (« condamnation à mort ») : un peu plus gras que le corps
   du texte, pour porter le poids de la sentence même une fois le trait
   disparu (voir plus bas). GRAISSE RÉELLE — la même fonte 700 romaine que
   « général Kléber » (.ab-t--kw.ab-t--engraved), jamais synthétisée : voir
   _font() (mesure) et hookFont() (fumée de sortie), même accord. */
.ab-t--base.ab-t--struck { font-weight: 700; }

/* Lit commun : tracé blanc, encrage blanc cassé.
   Délais inline (2 valeurs : tracé, encrage). */
.ab-t--base tspan {
  stroke: rgba(255, 255, 255, 0.92);
  animation:
    abDraw 620ms cubic-bezier(0.3, 0, 0.4, 1) both,
    abFillBase 900ms ease-in both;
}

/* Mots-clefs : tracé et encrage BLANCS, puis dépôt doré (abToGold sur le
   fill). Délais inline (3 valeurs : tracé, encrage, embrasement). L'italique
   gras Playfair est composé tel quel par le JS (mesures dans cette fonte).
   ⚠️ AUCUN filtre ici : l'embrasement et la lueur sont portés par les couches
   .ab-hook-glow (clones flous), jamais par le SVG principal — un drop-shadow
   animé sur <text> re-rastérisait le mot à chaque frame (cause du lag). */
/* ITALIQUE par défaut : la main, l'écriture, la personne (Soliman al-Halabi).
   ⚠️ Toute modification de fonte ici doit être répercutée dans _font() (mesure
   canvas) ET sur .ab-g--hand (clone de halo), sinon la justification se décale. */
.ab-t--kw {
  font-style: italic;
  font-weight: 700;
}
/* ROMAIN GRAS (`engraved` en config) : la pierre, le monument, l'institution
   (le général Kléber). Même or, autre voix. */
.ab-t--kw.ab-t--engraved { font-style: normal; }

.ab-t--kw tspan {
  stroke: rgba(255, 255, 255, 0.95);
  animation:
    abDraw 620ms cubic-bezier(0.3, 0, 0.4, 1) both,
    abFillWhite 700ms ease-in both,
    abToGold 1500ms cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}

/* ── HALOS des mots-clefs — clones flous composés par le GPU ────────────────
   Deux <svg> par mot-clef, superposés au SVG principal (même viewBox →
   alignement exact) : blanc = l'embrasement, or = la lueur déposée. Le blur
   (posé en inline par le JS, rayon accordé à la taille de composition) est
   FIXE : le navigateur met le résultat en cache et n'anime que l'OPACITÉ. */
.ab-hook-glow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
}
/* La fonte du clone doit être EXACTEMENT celle du glyphe qu'il double (posée
   par _makeGlow d'après _fontKey) : un halo composé dans une autre fonte se
   décale du mot. Ces trois classes répondent à _font() une pour une. */
.ab-hook-glow text            { font-family: 'Playfair Display', Georgia, serif; }
.ab-hook-glow .ab-g--hand     { font-style: italic; font-weight: 700; }
.ab-hook-glow .ab-g--engraved { font-style: normal; font-weight: 700; }
.ab-hook-glow .ab-g--base     { font-style: normal; font-weight: 400; }

.ab-hook-glow--white  text { fill: #ffffff; }
.ab-hook-glow--strike text { fill: #ffffff; }
.ab-hook-glow--gold   text { fill: rgba(224, 192, 116, 1); }

/* Délais inline (posés par le JS, calés sur la fin de l'encrage du mot). */
.ab-hook-glow--white { animation: abGlowFlash 1800ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both; }
.ab-hook-glow--gold  { animation: abGlowSettle 1300ms ease-out both; }

/* Une fois déposé (classe .is-set posée par le JS) : lueur au repos, halo
   renforcé au survol du mot (.is-hover, relayé par le JS) — opacité seule. */
.ab-hook-glow--gold.is-set {
  animation: none;
  opacity: 0.45;
  transition: opacity 500ms ease;
}
.ab-hook-glow--gold.is-set.is-hover { opacity: 0.95; }

/* L'éclat : le halo blanc flambe puis s'éteint… */
@keyframes abGlowFlash {
  0%   { opacity: 0; }
  25%  { opacity: 0.9; }
  60%  { opacity: 0.35; }
  100% { opacity: 0; }
}
/* …pendant que la lueur dorée se dépose. */
@keyframes abGlowSettle { from { opacity: 0; } to { opacity: 0.45; } }

/* ── LE RATÉ (`draft` en config) — la main se corrige ───────────────────────
   Le brouillon s'écrit dans la fonte de BASE et à la cadence ordinaire : il
   doit avoir l'air d'être le bon mot, sinon il n'y a pas de correction, juste
   une décoration. Ses tspans héritent donc de .ab-t--base — rien à faire ici.
   Puis le calque s'efface : une opacité, un élément.
   ease-OUT (et non ease-in) : l'opacité chute d'emblée puis traîne. Le mot juste
   se réécrit PAR-DESSUS pendant ce temps (T.draft_overlap) — il faut donc que le
   brouillon libère la place tout de suite, sinon les deux mots se superposent en
   pleine pâte et l'instant devient illisible.
   L'effacement est TOTAL, et pas seulement par goût — le mot corrigé occupe
   exactement le même emplacement, un fantôme resterait collé sur lui. */
.ab-draft { animation: abDraftErase 460ms ease-out both; }
@keyframes abDraftErase { from { opacity: 1; } to { opacity: 0; } }

/* La rature : un geste JETÉ — même attaque que la barre du coup (une courbe qui
   accélère et s'arrête net), épaisse, largement débordante. */
.ab-scratch {
  stroke: rgba(255, 255, 255, 0.9);
  stroke-width: 2.4;
  stroke-linecap: round;
  animation: abDraw 140ms cubic-bezier(0.55, 0, 0.9, 0.35) both;
}

/* La secousse du raté. Amplitude moindre que celle du coup (3 px contre 5) et
   SANS éclat : la hiérarchie tient à ça. La main rejette un mot ; le tribunal,
   lui, condamne — et lui seul fait blanchir la page. */
.ab-hookwrap.is-scratch-shock { animation: abScratchShock 240ms cubic-bezier(0.3, 0, 0.2, 1); }
@keyframes abScratchShock {
  0%   { transform: translate3d(0, 0, 0); }
  14%  { transform: translate3d(-3px, 1px, 0); }
  30%  { transform: translate3d(2px, -1px, 0); }
  50%  { transform: translate3d(-2px, 1px, 0); }
  72%  { transform: translate3d(1px, 0, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* ── LE COUP (« condamnation à mort ») ──────────────────────────────────────
   La barre n'est PAS dessinée, elle est PORTÉE : 130 ms au lieu de 950, et une
   courbe qui accélère jusqu'à s'arrêter net (le contraire d'un ease-out, qui
   ralentit en arrivant et raconte une calligraphie). Épaisse, elle déborde du
   mot et penche : géométrie et délai posés en inline par le JS.
   Deux animations, deux délais (posés en inline, valeurs séparées par une
   virgule — même idiome que .ab-t--kw tspan plus haut) : abDraw la porte,
   puis abUnderlineErase l'efface — un temps de lecture plus tard, DANS LE
   MÊME SENS (gauche → droite), jamais en arrière. abUnderlineErase n'a QUE
   `forwards` (pas `both`) : avant son délai elle ne contribue rien, laissant
   abDraw seule tenir le trait tracé jusque-là (cf. T.underline_hold, JS). */
.ab-underline {
  stroke: rgba(255, 255, 255, 0.96);
  stroke-width: 2.8;
  stroke-linecap: round;
  animation:
    abDraw 130ms cubic-bezier(0.55, 0, 0.9, 0.35) both,
    abUnderlineErase 600ms cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
/* Le dashoffset continue au-delà de 0 (trait plein) jusqu'à l'opposé de sa
   longueur (--ab-underline-erase, posé en inline par le JS, DÉJÀ NÉGATIF et
   sans unité) : la même mécanique dash/offset qui DESSINAIT le trait de
   gauche à droite l'EFFACE maintenant dans le même sens — le début du trait
   disparaît en premier.
   ⚠️ Ne pas remplacer par `calc(var(--ab-underline-len) * -1)` : une longueur
   calc ne s'interpole pas contre le `0` unitless de `from`, et le trait
   sautait alors à l'état effacé d'un seul coup, sans balayage (mesuré). */
@keyframes abUnderlineErase {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: var(--ab-underline-erase, 0); }
}

/* L'ÉCLAT — le mot blanchit à l'impact. Même mécanique que les halos : un clone
   flou dont SEULE l'opacité est animée. Montée quasi verticale (le coup), puis
   extinction lente (la brûlure rétinienne). Délai inline. */
.ab-hook-glow--strike { animation: abStrikeFlash 340ms cubic-bezier(0.2, 0, 0.4, 1) both; }
@keyframes abStrikeFlash {
  0%   { opacity: 0; }
  9%   { opacity: 1; }
  100% { opacity: 0; }
}

/* L'ONDE — la plaque encaisse. transform SEUL (compositeur), sur .ab-hookwrap
   qui n'a pas de transform propre : .ab-stack porte la remontée de la phase 2,
   y toucher ferait se contrarier les deux. Amplitude décroissante sur 280 ms :
   une secousse sèche qui retombe, pas un tremblement.
   Pas de fill-mode : l'animation finie, l'élément retrouve son état de base et
   ne garde aucune couche GPU (le JS retire la classe par surcroît). */
.ab-hookwrap.is-shock { animation: abShock 280ms cubic-bezier(0.3, 0, 0.2, 1); }
@keyframes abShock {
  0%   { transform: translate3d(0, 0, 0); }
  12%  { transform: translate3d(-5px, 2px, 0); }
  26%  { transform: translate3d(4px, -2px, 0); }
  42%  { transform: translate3d(-3px, 1px, 0); }
  58%  { transform: translate3d(2px, -1px, 0); }
  74%  { transform: translate3d(-1px, 0, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* ── LE TAMPON (le point d'interrogation final) ─────────────────────────────
   Toute l'accroche est ÉCRITE ; ce signe-là est APPOSÉ. Il ne se trace pas :
   il tombe de haut, s'écrase (0.86), rebondit (1.05) et se pose. La courbe
   dépasse volontairement 1 — un tampon rebondit, une plume jamais.
   transform-box: fill-box → l'origine est la boîte d'encre du signe lui-même
   (sans quoi l'échelle partirait du coin du SVG, à des centaines d'unités de
   là, et le « ? » traverserait la page). Un seul élément animé : on est très
   loin des tempêtes de couches proscrites ailleurs dans ce fichier. */
.ab-t--q {
  transform-box: fill-box;
  transform-origin: 50% 62%;
  animation: abStamp 260ms cubic-bezier(0.2, 0.85, 0.3, 1.2) var(--q-at, 0ms) both;
}
@keyframes abStamp {
  0%   { opacity: 0; transform: scale(3.1); }
  46%  { opacity: 1; transform: scale(0.86); }   /* l'écrasement */
  70%  { transform: scale(1.05); }               /* le rebond */
  100% { opacity: 1; transform: scale(1); }
}
/* Le signe est déjà encré : il n'a ni tracé ni encrage à jouer, c'est le
   tampon qui le donne d'un coup. */
.ab-t--q tspan {
  animation: none;
  stroke: transparent;
  stroke-dashoffset: 0;
  fill: rgba(255, 255, 255, 0.96);
}

/* L'ONDE du tampon — VERTICALE : une presse s'abat, elle ne balaie pas.
   (Le coup, lui, est latéral : abShock. Deux gestes, deux ondes.) */
.ab-hookwrap.is-q-shock { animation: abStampShock 300ms cubic-bezier(0.3, 0, 0.2, 1); }
@keyframes abStampShock {
  0%   { transform: translate3d(0, 0, 0); }
  10%  { transform: translate3d(0, 5px, 0); }
  28%  { transform: translate3d(-2px, -3px, 0); }
  46%  { transform: translate3d(1px, 2px, 0); }
  66%  { transform: translate3d(0, -1px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

@keyframes abDraw     { to { stroke-dashoffset: 0; } }
@keyframes abFillBase { to { fill: rgba(255, 255, 255, 0.96); stroke: transparent; } }
@keyframes abFillWhite{ to { fill: #ffffff; stroke: transparent; } }

/* Blanc incandescent → or déposé. */
@keyframes abToGold {
  0%   { fill: #ffffff; }
  35%  { fill: #fff3d6; }
  100% { fill: rgba(224, 192, 116, 1); }
}

/* ── PHASE 2 : le corps du texte ──────────────────────────────────────────── */

.ab-body {
  width: min(100%, 1040px);   /* bords alignés sur l'accroche */
  margin: 0 auto;
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 300;
  line-height: 1.68;
  letter-spacing: 0.012em;
  color: rgba(228, 226, 218, 0.92);
  text-align: justify;        /* écho au bloc justifié de l'accroche */
  -webkit-hyphens: auto;
          hyphens: auto;
  /* PAS de text-shadow pendant la pluie : l'ombre serait peinte par lettre,
     à chaque frame, pendant ~5 s — pour un effet invisible sur le fond noir à
     82-94 %. Elle est posée en une fois, la séquence finie (.ab-done). */
}
.doc-ov-about.ab-done .ab-body {
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.92);
}
.ab-body p { margin: 0 0 1.05em; }
.ab-body p:last-child { margin-bottom: 0; }

/* ── LE BATTEMENT — la ponctuation entre deux paragraphes ────────────────────
   Un filet doré qui s'ouvre DU CENTRE vers ses deux extrémités : c'est le geste
   signature du site (.doc-ov-caption::after trace ainsi la barre verticale des
   légendes de documents), repris à l'horizontale — un fleuron de manuscrit.

   Il ne s'AJOUTE pas à la mise en page : le paragraphe qu'il suit lui cède sa
   marge (.has-beat) et le battement en reprend exactement la hauteur. L'écart
   entre deux paragraphes est donc inchangé, et _fit() mesure la même chose.
   Une hauteur explicite plutôt qu'un élément vide de hauteur nulle : les marges
   s'effondreraient au travers, et sa position deviendrait affaire de navigateur. */
.ab-body p.has-beat { margin-bottom: 0; }
.ab-beat {
  position: relative;
  height: 1.05em;             /* = la marge cédée par .has-beat */
}
/* Un filet de LUMIÈRE, pas une règle tracée : le dégradé culmine au centre et
   s'éteint aux deux bouts, sans plateau — sur le fond noir, il se lit comme une
   lueur qui affleure. (Le filet des légendes, lui, est un objet permanent : il
   a un plateau. Celui-ci ne fait que passer.) */
.ab-beat::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: clamp(56px, 9%, 120px);
  height: 1px;
  transform-origin: center;
  transform: translate(-50%, -50%) scaleX(0);
  opacity: 0;
  background: linear-gradient(
      to right,
      rgba(210, 175, 90, 0) 0%,
      rgba(232, 205, 143, 0.85) 50%,
      rgba(210, 175, 90, 0) 100%);
}

/* Il s'ouvre du centre, tient, PUIS SE REFERME vers le centre : un souffle
   complet — c'est ce qui en fait un battement plutôt qu'une apparition. Le
   retrait est plus lent que l'ouverture : le geste part avec intention et se
   retire sans bruit.
   Le délai vient de --beat-at, posé par le JS sur le <div> parent : un pseudo-
   élément n'a pas de style inline, et animation-delay ne s'hérite pas — une
   propriété personnalisée, si.
   La cadence de chaque temps est déclarée DANS les keyframes (une courbe unique
   sur la totalité raidirait aussi bien l'ouverture que le retrait).
   transform + opacité : compositeur, coût nul. */
.ab-body.ab-live .ab-beat::before,
.ab-body .ab-beat.ab-live::before {
  animation: abBeat 1400ms linear var(--beat-at, 0ms) both;
}
@keyframes abBeat {
  /* l'ouverture — le filet s'écarte du centre */
  0%   { opacity: 0; transform: translate(-50%, -50%) scaleX(0);
         animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  /* la tenue */
  24%  { opacity: 1; transform: translate(-50%, -50%) scaleX(1);
         animation-timing-function: linear; }
  /* le retrait — il se referme et s'éteint ensemble */
  56%  { opacity: 1; transform: translate(-50%, -50%) scaleX(1);
         animation-timing-function: cubic-bezier(0.5, 0, 0.75, 0); }
  100% { opacity: 0; transform: translate(-50%, -50%) scaleX(0.18); }
}

/* Pluie de lettres : chaque span porte son délai (inline — ordre aléatoire
   dans le paragraphe, les paragraphes se succédant) ; l'animation ne part
   qu'à la pose de .ab-live (pendant la remontée).

   PERFORMANCE — on n'anime QUE l'opacité. C'est la seule propriété (avec
   transform, inapplicable à un <span> inline) que le compositeur gère SANS
   repaint. Un `filter: blur()` par lettre était magnifique mais rédhibitoire :
   des centaines de filtres maintenus pendant toute la phase 1, puis animés en
   parallèle en phase 2 → repaint continu, tout ralentit. L'apparition
   aléatoire suffit à évoquer le décodage d'archive ; la lenteur fait le reste.

   DEUX PORTÉES pour .ab-live : sur le CORPS entier (tout tient à l'écran — la
   pluie suit sa frise cumulée d'un paragraphe à l'autre) ou sur UN PARAGRAPHE
   (révélation au défilement : il pleut quand le regard l'atteint). Même
   animation, même cadence : seul le déclencheur change. */
.ab-body span { opacity: 0; }
.ab-body.ab-live span,
.ab-body p.ab-live span { animation: abLetter 640ms ease forwards; }
@keyframes abLetter { from { opacity: 0; } to { opacity: 1; } }

/* Passages en relief (*…* dans la config). Graisse posée dès le départ
   (aucun décalage de mise en page), lumière donnée une fois le texte posé. */
.ab-em {
  font-style: normal;
  font-weight: 500;
  color: inherit;
  transition: color 900ms ease, text-shadow 900ms ease;
}
.ab-done .ab-em {
  color: rgba(255, 252, 244, 1);
  text-shadow: 0 0 16px rgba(210, 175, 90, 0.30);
}
.ab-done .ab-em:hover {
  color: rgba(255, 230, 160, 1);
  text-shadow: 0 0 20px rgba(231, 206, 142, 0.65);
}

/* ── SÉQUENCE POSÉE (.ab-settled, fin de chorégraphie) ──────────────────────
   Toutes les animations sont remplacées par leur état final statique : plus
   aucun objet d'animation vivant pendant la lecture et le défilement. */
.doc-ov-about.ab-settled .ab-hook tspan {
  animation: none;
  stroke: transparent;
  stroke-dashoffset: 0;
}
.doc-ov-about.ab-settled .ab-t--base tspan { fill: rgba(255, 255, 255, 0.96); }
.doc-ov-about.ab-settled .ab-t--kw   tspan { fill: rgba(224, 192, 116, 1); }
/* ── L'ÉTAT DE REPOS DE LA BARRE : elle n'est plus là ──────────────────────
   Le trait est un GESTE, pas un ornement : il s'efface de lui-même après son
   temps de lecture (abUnderlineErase). Partout où la chorégraphie s'arrête,
   son état est donc EFFACÉ, jamais tracé — sans quoi il resurgirait d'un bloc.
   Les trois arrêts possibles disent la même chose, et la disent ICI :
     .ab-settled → la séquence s'est posée d'elle-même (le trait est parti
                   bien avant, T.underline_hold + T.underline_fade tombant
                   pendant l'accroche) ;
     .ab-skip    → on a sauté ; sauter, c'est renoncer à l'événement — l'éclat
                   et la secousse n'ont pas lieu non plus (voir plus bas).
                   ⚠️ Ne PAS accélérer ses animations comme on le fait pour les
                   lettres : elles sont DEUX (abDraw puis abUnderlineErase) et
                   rejoueraient le geste entier en une milliseconde.
   (Le troisième, prefers-reduced-motion, est en fin de fichier : une media
   query ne peut pas rejoindre cette liste de sélecteurs.)
   La FUMÉE de sortie, elle, ne lit pas ces classes : elle mesure le trait
   réellement affiché (_visibleStrike) et n'emporte que lui. */
.doc-ov-about.ab-settled .ab-underline,
.doc-ov-about.ab-skip .ab-underline {
  animation: none;
  stroke-dashoffset: var(--ab-underline-erase, 0) !important;
}
.doc-ov-about.ab-settled .ab-hook-glow--white,
.doc-ov-about.ab-settled .ab-hook-glow--strike { animation: none; opacity: 0; }
/* Le brouillon a été effacé : il ne doit RIEN laisser (le mot corrigé occupe
   sa place). L'opacité du groupe l'emporte sur l'état de ses lettres. */
.doc-ov-about.ab-settled .ab-draft { animation: none; opacity: 0; }
/* Le battement a eu lieu : il ne reste rien. (Sélecteur volontairement plus
   spécifique que .ab-body.ab-live .ab-beat::before, qui pèse autant — sinon la
   règle gagnante ne dépendrait que de l'ordre du fichier.)
   ⚠️ :not(.ab-live) — en révélation au défilement, .ab-settled tombe dès le
   premier paragraphe posé, alors que les battements suivants n'ont pas encore
   battu : sans cette réserve, cette règle les éteindrait AVANT leur tour. Un
   battement porte .ab-live le temps de son geste, et le perd ensuite : la
   règle le rattrape alors, et n'en laisse rien. */
.doc-ov-about.ab-settled .ab-body .ab-beat:not(.ab-live)::before {
  animation: none;
  opacity: 0;
}
/* Le tampon est tombé : plus de transform vivant sur le signe (une couche GPU
   de moins), et il reste à sa place, à sa taille. */
.doc-ov-about.ab-settled .ab-t--q {
  animation: none;
  opacity: 1;
  transform: none;
}

/* Le DÉSARMEMENT du corps est PAR PARAGRAPHE (.ab-pdone), pas global.
   ⚠️ Il vivait sur .ab-settled — impossible désormais : en révélation au
   défilement (téléphone), .ab-settled tombe dès le premier paragraphe posé
   (c'est lui qui ouvre le relief, les survols et la fumée), et il aurait
   affiché d'un coup TOUT le texte encore à découvrir plus bas.
   Chaque paragraphe se désarme donc quand SA pluie est finie — en révélation
   au défilement comme en mode ordinaire (où ils reçoivent tous la classe
   ensemble). Rien ne reste vivant nulle part, et la borne est la même. */
.doc-ov-about .ab-body p.ab-pdone span { animation: none; opacity: 1; }

/* ══════════════════════════════════════════════════════════════════
   LA FUMÉE — la sortie (src/ui/AboutReveal.js → smokeOut)
   ──────────────────────────────────────────────────────────────────
   Un souffle traverse la page de GAUCHE À DROITE et emporte le texte PAR MOTS,
   hors du cadre de lecture, sur UN SEUL CANVAS posé au niveau de l'overlay —
   l'outil que le site emploie déjà pour la torche. Toute la chorégraphie vit
   dans smokeOut() ; ici, seulement les ÉTATS : ce qui se cache, où vit le
   calque. Le texte réel ne s'anime JAMAIS pendant la fumée — il se cache en
   une frame, le canvas le redessine et l'emporte.

   (Autopsie des deux versions mortes, pour ne pas y revenir : opacité animée
   par lettre sous un transform de conteneur = repaint + re-raster plein cadre
   par frame ; puis ~180 fragments DOM en Web Animations dont les filter:blur()
   retombaient sur le thread principal, et dont l'ouverture de l'overflow
   libérait la gouttière d'ascenseur — d'où un reflux de toute la boîte à
   l'instant du départ.) */

/* Le texte réel disparaît d'un bloc : accroche, corps, halos — le canvas
   reprend tout. visibility (pas display) : la géométrie reste, la position de
   défilement et les mesures avec elle. */
.doc-ov-about.ab-out .ab-stack { visibility: hidden; }

/* L'ascenseur s'éteint (il flotterait sur une boîte vide) — mais la GOUTTIÈRE
   reste réservée (scrollbar-gutter: stable both-edges, posé plus haut) : passer
   de auto à hidden ne rend pas un pixel de largeur, donc AUCUN reflux, et les
   positions mesurées par smokeOut restent vraies. !important : il faut battre
   .is-scroll.ab-phase2 (overflow-y: auto). */
.doc-ov-about.ab-out { overflow: hidden !important; }

/* Les indices de défilement, coupés net : leur backdrop-filter n'a plus rien à
   estomper — et il ré-échantillonnerait ce qui bouge dessous. */
.doc-ov-about.ab-out .ab-scrollblur { display: none; }

/* Le calque de la fumée : DANS #doc-overlay mais HORS du host — il n'est rogné
   que par l'écran, jamais par la boîte de texte, et le fondu final de l'overlay
   (opacity, compositeur) l'emporte avec le fond. Géométrie posée en inline par
   smokeOut (dimensionné sur la zone de vol, pas l'écran entier). */
.ab-smokecanvas {
  position: absolute;
  z-index: 3;
  pointer-events: none;
}

/* ── FAST-FORWARD (clic sur le texte) : tout saute à l'état final ─────────── */
.doc-ov-about.ab-skip .ab-hook tspan,
.doc-ov-about.ab-skip .ab-hook-glow--gold {
  animation-duration: 1ms !important;
  animation-delay: 0ms !important;
}
/* Le coup ne se rejoue pas en accéléré : sauter, c'est renoncer à l'événement.
   L'éclat et la secousse n'ont pas lieu (le JS ne les programme plus — ses
   timers sont purgés), et LA BARRE non plus : son état de repos est d'être
   partie, posé avec celui de .ab-settled (voir « L'ÉTAT DE REPOS DE LA
   BARRE », plus haut) — les deux arrêts disent la même chose au même endroit. */
.doc-ov-about.ab-skip .ab-hook-glow--white,
.doc-ov-about.ab-skip .ab-hook-glow--strike { animation: none; opacity: 0; }
/* Sauter, c'est aller au texte corrigé : le raté n'a pas lieu. */
.doc-ov-about.ab-skip .ab-draft { animation: none; opacity: 0; }
/* Ni le tampon. ⚠️ La règle du fast-forward ci-dessus ne vise que les tspans :
   l'animation du signe vit sur son <text> et se serait jouée à son heure, des
   secondes après un texte déjà posé. Le point est simplement là. */
.doc-ov-about.ab-skip .ab-t--q {
  animation: none;
  opacity: 1;
  transform: none;
}
/* Ni les battements : le corps se pose d'un bloc, il n'y a plus rien à ponctuer. */
.doc-ov-about.ab-skip .ab-body .ab-beat::before { animation: none; opacity: 0; }
.doc-ov-about.ab-skip .ab-stack { transition-duration: 550ms; }
.doc-ov-about.ab-skip .ab-body.ab-live span { animation-duration: 420ms; }

/* ── Mouvement réduit : état final direct, sans chorégraphie ──────────────── */
@media (prefers-reduced-motion: reduce) {
  .doc-ov-about .ab-hook tspan {
    animation: none;
    stroke: transparent;
    stroke-dashoffset: 0;
  }
  .doc-ov-about .ab-t--base tspan { fill: rgba(255, 255, 255, 0.96); }
  .doc-ov-about .ab-t--kw   tspan { fill: rgba(224, 192, 116, 1); }
  .doc-ov-about .ab-hook-glow--white  { animation: none; }
  .doc-ov-about .ab-hook-glow--strike { animation: none; opacity: 0; }
  .doc-ov-about .ab-hook-glow--gold   { animation: none; opacity: 0.45; }
  /* Le trait est un GESTE, et son état de repos est d'être parti (il s'efface
     de lui-même après un temps de lecture). Sans mouvement, on donne le repos
     — pas un trait posé là pour toujours, que la mise en scène ne laisse
     jamais à l'écran. */
  .doc-ov-about .ab-underline {
    animation: none;
    stroke-dashoffset: var(--ab-underline-erase, 0) !important;
  }
  /* ⚠️ La règle ci-dessus rend les lettres du brouillon visibles (tracé nul,
     remplissage plein) : sans ceci, « meurtrier » resterait affiché PAR-DESSUS
     « assassin ». L'opacité du groupe tranche. */
  .doc-ov-about .ab-draft { animation: none; opacity: 0; }
  /* Le point est posé, pas apposé : ni chute, ni rebond, ni onde. */
  .doc-ov-about .ab-t--q {
    animation: none !important;
    opacity: 1;
    transform: none !important;
  }
  /* Ni secousse ni éclat : c'est précisément ce que ce réglage demande. La barre
     reste, le coup n'a pas lieu. (Le JS ne les programme pas non plus ; cette
     règle est le filet de sécurité.) */
  .doc-ov-about .ab-hookwrap { animation: none !important; transform: none !important; }
  .doc-ov-about .ab-stack     { transition: none; }
  .doc-ov-about .ab-body span { opacity: 1; filter: none; animation: none !important; }
  /* Le battement est une chorégraphie : il n'a pas lieu. (!important comme les
     règles voisines — .ab-body.ab-live .ab-beat::before pèse autant.) */
  .doc-ov-about .ab-beat::before { animation: none !important; opacity: 0; }
  /* La fumée de sortie non plus : smokeOut() refuse de partir sous ce réglage
     (this._reduced) — le texte s'efface avec le fond, sans souffle. Pas de
     filet CSS à poser : la fumée est pilotée en Web Animations API, et sans
     smokeOut il n'y a ni classe .ab-out, ni fragments, ni animations. */
}


/* ══════════════════════════════════════════════════════════════════
   ACCESSIBILITÉ — clavier et lecteurs d'écran
   ─────────────────────────────────────────────────────────────────
   Toute l'interface est faite de <div> et de <svg> construits en JS.
   Ils reçoivent leur rôle et leur tabindex depuis src/utils/a11y.js ;
   ce bloc leur donne ce que le CSS seul peut donner : un focus VISIBLE
   et une classe pour les textes réservés aux lecteurs d'écran.

   Le halo doré du focus reprend exactement la couleur de survol du site
   (rgba(255,210,80)) : l'utilisateur au clavier voit la même chose que
   l'utilisateur à la souris, au même endroit.
   ══════════════════════════════════════════════════════════════════ */

/* Contenu réservé aux technologies d'assistance (annonces de scène). */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* :focus-visible seulement — le halo ne doit PAS apparaître au clic souris,
   qui garderait sinon une bague dorée sur le dernier bouton cliqué. */
:focus:not(:focus-visible) { outline: none; }

/* ⚠️ L'ÉCRAN D'ACCUEIL MONTRE SON HALO DÈS LE CHARGEMENT — c'est VOULU.
   #ss-start porte `autofocus` : le navigateur traite ce focus initial comme un
   focus CLAVIER et dessine aussitôt la bague dorée autour de « Cliquez pour
   commencer ». Une passe précédente l'a ajournée jusqu'au premier appui sur une
   touche (`body.using-keyboard`), au motif qu'elle chargeait un écran composé
   au pixel. C'était un arbitrage esthétique, et il coûtait trop cher : ce halo
   est le SEUL indice qu'on peut entrer sans la souris, et il tombait
   précisément là où le site se présente. Il reste donc visible d'emblée —
   ne pas le remasquer. */

:focus-visible,
[role="button"]:focus-visible,
.doc-btn:focus-visible,
.roman-btn:focus-visible,
#fs-btn:focus-visible,
[data-arrow]:focus-visible,
.ss-cta:focus-visible {
  outline: 2px solid rgba(255, 210, 80, 0.95);
  outline-offset: 4px;
  border-radius: 3px;
}

/* Les zones de la navbar sont des <rect> SVG : `outline` n'y est pas fiable
   selon les moteurs, on passe donc par le tracé lui-même. */
.nav-btn-zone:focus-visible {
  outline: none;
  fill: rgba(255, 210, 80, 0.12);
  stroke: rgba(255, 210, 80, 0.95);
  stroke-width: 1.5;
}

/* Les hotspots du chapitre 1 et les bulles du chapitre 4 vivent dans des
   contextes sombres ou clairs selon la moitié d'écran : un halo doré tient
   sur les deux (c'est déjà la couleur de survol commune au site). */
.hotspot-zone:focus-visible,
.chp4-hit:focus-visible {
  outline: 2px solid rgba(255, 210, 80, 0.95);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  :focus-visible { transition: none !important; }
}

/* ══════════════════════════════════════════════════════════════════
   CARTE DU PARCOURS — la boussole du coin haut-gauche
   ──────────────────────────────────────────────────────────────────
   Réglages dans config.js → MAP (interrupteur, libellés, cadences).
   Géométrie et comportement : src/ui/CompassMap.js.

   ⚠️ z-index 600 SOUS body.chpX-active — sans cette remontée, la boussole
   serait ENTERRÉE dans les quatre chapitres : leurs roots sont en
   position:fixed / z-index:500 et couvrent tout l'écran. C'est la même
   mécanique que pour les titres et le bouton plein écran, et le même piège :
   il ne se voit qu'en entrant dans un chapitre.
   ══════════════════════════════════════════════════════════════════ */

#compass-map {
  position: absolute;
  z-index: 16;                  /* au-dessus des titres (15) du tronc commun */
  opacity: 0;
  cursor: pointer;
  /* La carte dépliée déborde largement du conteneur (qui fait la taille de la
     boussole) : ne jamais la rogner. */
  overflow: visible;
}

body.chp2-active #compass-map,
body.chp3-active #compass-map,
body.chp4-active #compass-map {
  z-index: 600;
}

/* ⚠️ LA BOUSSOLE PASSE PAR-DESSUS LA CARTE (z 3 contre z 1). Elle est écrite
   AVANT le panneau dans le DOM : sans z-index, la carte la recouvrait, et le
   fleuron du coin disparaissait sous le cadre au moment même où il se pose.
   `pointer-events: none` reste indispensable — sinon le dessin, désormais
   au-dessus, intercepterait les clics destinés aux points du coin haut-gauche. */
#compass-map .cm-compass {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;         /* le conteneur porte le clic, pas le dessin */
}

#compass-map .cm-compass svg {
  display: block;
  overflow: visible;
}

/* La carte dépliée. Positionnée en JS pour que le coin libre de son dessin
   tombe sur le centre de la boussole (voir _buildPanel). */
#compass-map .cm-panel {
  position: absolute;
  z-index: 1;                   /* sous la boussole — voir .cm-compass */
  pointer-events: auto;
}

/* ⚠️ L'OMBRE PORTÉE REMPLACE LE FOND. Le cadre de la carte est presque
   transparent (config.js → MAP.fond_opacite, 5 %) : ce n'est plus lui qui
   détache les traits de la scène, c'est cette ombre. La retirer rendrait la
   carte illisible sur un fond clair — la page du chapitre 4, notamment. */
#compass-map .cm-panel svg {
  display: block;
  overflow: visible;
  filter:
    drop-shadow(0 0 3px rgba(0, 0, 0, 0.85))
    drop-shadow(0 0 10px rgba(0, 0, 0, 0.55));
}

/* Un point du parcours. Le curseur dit qu'il mène quelque part. */
#compass-map .cm-node {
  cursor: pointer;
}

#compass-map .cm-node circle {
  transition: r 0.28s cubic-bezier(0.34, 1.56, 0.64, 1),
              stroke 0.25s ease,
              filter 0.25s ease;
}

/* ── La légende ───────────────────────────────────────────────────
   SOUS la carte, centrée sur elle et large comme elle : elle se lit comme la
   légende d'une planche, toujours au même endroit, et seul le mot change d'un
   point à l'autre. Un nom long passe à la ligne — il ne déborde jamais du bord
   droit du cadre. Sobre : le nom, et un filet qui s'ouvre sous lui. Aucune
   boîte, aucun fond — l'ombre portée suffit, comme pour #hover-title. */
#compass-map .cm-tip {
  position: absolute;
  z-index: 4;                   /* au-dessus de la carte ET de la boussole */
  pointer-events: none;
  text-align: center;
  line-height: 1.35;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.94);
  text-shadow:
    0 0 20px rgba(0, 0, 0, 1),
    0 0 8px rgba(0, 0, 0, 0.95),
    0 2px 5px rgba(0, 0, 0, 0.9);
  opacity: 0;
  transform: translateY(-5px);
}

/* ⚠️ CHAPITRE 4 : UNE ÉTIQUETTE SUR LE PAPIER, PAS UN TEXTE NU.
   Mesuré sous le bandeau de légende, à trois formats d'écran : le fond y va du
   quasi-noir (25/255) au quasi-blanc (222/255) SUR LA MÊME LIGNE — la légende
   tombe pile sur la lisière entre la photographie du crâne et le papier. Aucune
   couleur de texte ne peut tenir des deux côtés : le blanc à halo noir fait une
   tache sur le papier, l'encre à halo clair disparaît sur le tissu.
   On lui donne donc son propre appui — une étiquette couleur papier, posée sous
   la carte. C'est la matière du chapitre, et elle ne dépend plus du fond.
   (Le seul endroit du site où la légende porte un fond : partout ailleurs, le
   ground est franchement sombre et l'ombre portée suffit.) */
body.chp4-active #compass-map .cm-tip {
  box-sizing: border-box;
  padding: 0.5em 0.7em;
  border-radius: 2px;
  color: rgba(26, 22, 18, 0.95);
  background: rgba(246, 243, 236, 0.93);
  text-shadow: none;
  box-shadow: 0 1px 10px rgba(0, 0, 0, 0.28);
}

#compass-map .cm-tip.visible {
  opacity: 1;
  transform: translateY(0);
}


#compass-map .cm-tip::after {
  content: '';
  display: block;
  width: 40%;
  max-width: 0;
  height: 1px;
  margin: 0.4em auto 0;
  background: rgba(255, 214, 120, 0.45);
  transition: max-width 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.1s;
}

body.chp4-active #compass-map .cm-tip::after {
  background: rgba(60, 48, 32, 0.5);
}

#compass-map .cm-tip.visible::after {
  max-width: 100%;
}


@media (prefers-reduced-motion: reduce) {
  #compass-map .cm-node circle,
  #compass-map .cm-tip,
  #compass-map .cm-tip::after {
    transition: none;
  }
}
