/* =========================================================
   ÉCRAN TITRE + MENU PRINCIPAL (zoom dans la télévision)
   ========================================================= */

.boot-layer {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: grid;
  place-items: center;
  background: #04030a;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
}

.boot-layer[hidden] {
  display: none !important;
}

.boot-stage {
  position: relative;
  z-index: 1;
  display: block;
  transform-origin: 52.5% 61.5%;
  will-change: transform, opacity, filter;
}

.boot-stage img {
  display: block;
  max-width: 100vw;
  max-height: 100vh;
  width: auto;
  height: auto;
  image-rendering: auto;
  pointer-events: none;
}

/* ---------- écran titre ---------- */

#bootTitleScreen {
  cursor: pointer;
}

#bootTitleScreen::before,
#bootMenuScreen::before {
  content: "";
  position: absolute;
  inset: -6%;
  z-index: 0;
  background-image: url("assets/title-screen.jpg");
  background-size: cover;
  background-position: center 62%;
  filter: blur(46px) brightness(0.4) saturate(1.2);
  opacity: 0.7;
  pointer-events: none;
}

#bootMenuScreen::before {
  background-image: url("assets/menu-loop-poster.jpg");
  background-position: center;
}

#bootTitleStage {
  animation: boot-title-breathe 12s ease-in-out infinite alternate;
}

@keyframes boot-title-breathe {
  from { transform: scale(1); }
  to { transform: scale(1.035); }
}

.boot-title-glow {
  position: absolute;
  left: 39.5%;
  top: 55%;
  width: 26%;
  height: 13.5%;
  pointer-events: none;
  box-shadow: 0 0 60px 18px rgba(255, 94, 168, 0.34);
  animation: boot-screen-pulse 2.6s ease-in-out infinite alternate;
}

@keyframes boot-screen-pulse {
  from { opacity: 0.35; }
  to { opacity: 0.85; }
}

.boot-prompt {
  position: absolute;
  left: 50%;
  bottom: 4.5%;
  transform: translateX(-50%);
  padding: 12px 22px;
  color: #f4f0ff;
  font-family: "Press Start 2P", "Courier New", monospace;
  font-size: clamp(10px, 1.1vw, 15px);
  letter-spacing: 0.12em;
  text-align: center;
  white-space: nowrap;
  text-shadow:
    0 0 8px rgba(102, 238, 255, 0.9),
    0 0 24px rgba(255, 78, 215, 0.55);
  animation: boot-prompt-blink 1.5s steps(1, end) infinite;
}

@keyframes boot-prompt-blink {
  0%, 55% { opacity: 1; }
  56%, 100% { opacity: 0.15; }
}

/* ---------- animation de zoom dans la TV ---------- */

#bootTitleScreen.zooming {
  cursor: default;
}

#bootTitleScreen.zooming #bootTitleStage {
  animation: boot-zoom-in 1750ms cubic-bezier(0.62, 0.02, 0.86, 0.32) forwards;
}

@keyframes boot-zoom-in {
  0% { transform: scale(1); filter: brightness(1); }
  55% { transform: scale(3.2); filter: brightness(1.08); }
  100% { transform: scale(13); filter: brightness(1.9) saturate(1.3); }
}

#bootTitleScreen.zooming .boot-prompt {
  animation: none;
  opacity: 0;
  transition: opacity 240ms linear;
}

.boot-flash {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle at 52.5% 61.5%, #fff 0%, rgba(255, 210, 255, 0.6) 45%, transparent 78%);
}

#bootTitleScreen.zooming .boot-flash {
  animation: boot-flash-in 1750ms ease-in forwards;
}

@keyframes boot-flash-in {
  0%, 62% { opacity: 0; }
  100% { opacity: 1; }
}

/* ---------- menu principal ---------- */

#bootMenuScreen {
  background: #05040b;
}

.boot-video-stage {
  width: min(100vw, calc(100vh * 16 / 9));
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #05040b;
}

.boot-menu-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: #05040b;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

.boot-video-scanlines {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.13;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255,255,255,0.04) 0,
    rgba(255,255,255,0.04) 1px,
    rgba(0,0,0,0.05) 1px,
    rgba(0,0,0,0.05) 3px
  );
  mix-blend-mode: screen;
}

.boot-video-stage .boot-hotspot,
.boot-video-stage .boot-menu-label {
  z-index: 2;
}

#bootMenuScreen.entering #bootMenuStage {
  animation: boot-menu-arrive 900ms cubic-bezier(0.16, 0.84, 0.28, 1) forwards;
}

#bootMenuScreen.entering .boot-flash {
  opacity: 1;
  animation: boot-flash-out 620ms ease-out forwards;
}

@keyframes boot-menu-arrive {
  from { transform: scale(1.28); opacity: 0.2; filter: brightness(1.8); }
  to { transform: scale(1); opacity: 1; filter: brightness(1); }
}

@keyframes boot-flash-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

.boot-hotspot {
  position: absolute;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: transparent;
  cursor: pointer;
  transition:
    box-shadow 180ms ease,
    background 180ms ease,
    transform 180ms ease;
}

.boot-hotspot:hover,
.boot-hotspot:focus-visible {
  outline: none;
  background: rgba(255, 255, 255, 0.06);
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.55),
    0 0 26px rgba(255, 255, 255, 0.28);
  transform: translateY(-2px);
}

.boot-hotspot.selected {
  background: rgba(255, 255, 255, 0.05);
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.75),
    0 0 34px rgba(255, 255, 255, 0.32);
}

.boot-hotspot.boot-card {
  border-radius: 14px;
}

.boot-menu-label {
  position: absolute;
  left: 50%;
  bottom: 3.4%;
  transform: translateX(-50%);
  color: #d9d2ff;
  font-family: "Press Start 2P", "Courier New", monospace;
  font-size: clamp(8px, 0.78vw, 11px);
  letter-spacing: 0.1em;
  white-space: nowrap;
  text-shadow: 0 0 10px rgba(120, 90, 255, 0.7);
  pointer-events: none;
}

/* debug : afficher les zones cliquables */
.boot-layer.debug .boot-hotspot {
  border-color: #00ff88;
  background: rgba(0, 255, 136, 0.14);
}

@media (max-width: 700px) {
  .boot-prompt {
    bottom: 3%;
    padding: 9px 14px;
  }
}


/* V55 — l’accueil CRT doit couvrir tous les panneaux du jeu cible. */
body.boot-crt-active { overflow: hidden; }


/* =========================================================
   V62 — CRT PERFORMANCE
   Réduit la composition GPU et masque totalement le jeu sous le boot.
   ========================================================= */
body.boot-crt-active .app-shell,
body.boot-crt-active #networkModal,
body.boot-crt-active .v50-home-screen {
  visibility: hidden !important;
}

#bootMenuScreen::before {
  inset: 0;
  background:
    radial-gradient(circle at 50% 52%, rgba(72, 24, 116, 0.32), transparent 48%),
    linear-gradient(180deg, #070510 0%, #030208 100%);
  filter: none;
  opacity: 1;
}

.boot-video-stage {
  contain: layout paint size;
  isolation: isolate;
}

.boot-menu-video {
  transform: translateZ(0);
  backface-visibility: hidden;
  contain: paint;
}

.boot-video-scanlines {
  opacity: 0.08;
  mix-blend-mode: normal;
}

#bootMenuScreen.entering #bootMenuStage {
  animation: boot-menu-arrive-v62 620ms cubic-bezier(0.16, 0.84, 0.28, 1) forwards;
}

@keyframes boot-menu-arrive-v62 {
  from { transform: scale(1.08); opacity: 0.25; }
  to { transform: scale(1); opacity: 1; }
}

#bootMenuScreen.video-buffering .boot-menu-video {
  opacity: 0.985;
}

@media (prefers-reduced-motion: reduce) {
  #bootMenuScreen.entering #bootMenuStage {
    animation-duration: 160ms;
  }
}


/* ---------- transition dédiée au mode Chaos ---------- */
.chaos-transition-layer {
  position: fixed;
  inset: 0;
  z-index: 10020;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: #000;
  user-select: none;
  -webkit-user-select: none;
}

.chaos-transition-layer[hidden] {
  display: none !important;
}

.chaos-transition-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
  pointer-events: none;
  transform: translateZ(0);
  backface-visibility: hidden;
}

.chaos-transition-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at center, transparent 58%, rgba(0,0,0,0.28) 100%),
    linear-gradient(to bottom, rgba(0,0,0,0.10), transparent 18%, transparent 82%, rgba(0,0,0,0.20));
}

.chaos-transition-hint {
  position: absolute;
  right: max(18px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: 2;
  margin: 0;
  color: rgba(255,255,255,0.72);
  font-family: "Press Start 2P", "Courier New", monospace;
  font-size: clamp(7px, 0.72vw, 10px);
  letter-spacing: 0.08em;
  text-shadow: 0 0 10px rgba(255, 50, 94, 0.85);
  opacity: 0;
  animation: chaos-transition-hint-in 550ms ease 1.4s forwards;
}

@keyframes chaos-transition-hint-in {
  to { opacity: 0.72; }
}

body.chaos-transition-active {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .chaos-transition-hint { animation: none; opacity: 0.72; }
}

/* =========================================================
   V79 — Cadres CRT recalibrés et translucides
   ========================================================= */
.boot-hotspot {
  /* Le cadre reste exactement sur la zone vidéo : aucun déplacement au hover. */
  --boot-accent-rgb: 255, 255, 255;
  box-sizing: border-box;
  border: 1px solid transparent;
  background: transparent;
  transform: none !important;
  /* Empêche un halo externe de fausser visuellement les limites du panneau. */
  box-shadow: none;
}

.boot-hotspot:hover,
.boot-hotspot:focus-visible {
  outline: none;
  border-color: transparent;
  background: rgba(var(--boot-accent-rgb), 0.055);
  box-shadow:
    inset 0 0 0 2px rgba(var(--boot-accent-rgb), 0.72),
    inset 0 0 24px rgba(var(--boot-accent-rgb), 0.065),
    0 0 20px rgba(var(--boot-accent-rgb), 0.17);
  transform: none !important;
}

.boot-hotspot.selected {
  border-color: transparent;
  background: rgba(var(--boot-accent-rgb), 0.075);
  box-shadow:
    inset 0 0 0 2px rgba(var(--boot-accent-rgb), 0.90),
    inset 0 0 28px rgba(var(--boot-accent-rgb), 0.085),
    0 0 25px rgba(var(--boot-accent-rgb), 0.23);
}

/* Le cadre suit les coins des panneaux sans les recouvrir. */
.boot-hotspot.boot-card {
  border-radius: 11px;
}

.boot-hotspot.boot-action {
  border-radius: 8px;
}

/* Si une carte sélectionnée est aussi survolée, on garde la même géométrie
   et on renforce seulement très légèrement sa lumière. */
.boot-hotspot.selected:hover,
.boot-hotspot.selected:focus-visible {
  background: rgba(var(--boot-accent-rgb), 0.09);
  box-shadow:
    inset 0 0 0 2px rgba(var(--boot-accent-rgb), 0.98),
    inset 0 0 30px rgba(var(--boot-accent-rgb), 0.10),
    0 0 28px rgba(var(--boot-accent-rgb), 0.27);
}

/* =========================================================
   V81 — hotspot développeur secret sur le poster Night Driver
   ========================================================= */
.boot-secret-developer-hotspot {
  position: absolute;
  z-index: 6;
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  background: transparent;
  opacity: 0;
  cursor: default;
  box-shadow: none;
  -webkit-tap-highlight-color: transparent;
}
.boot-secret-developer-hotspot:hover,
.boot-secret-developer-hotspot:active,
.boot-secret-developer-hotspot:focus,
.boot-secret-developer-hotspot:focus-visible {
  border: 0;
  outline: 0;
  background: transparent;
  opacity: 0;
  box-shadow: none;
}


/* =========================================================
   V95 — hotspot Apparence invisible + halo local sur la vidéo
   ========================================================= */
:root {
  --appearance-character-halo-brightness: 1.20;
  --appearance-character-halo-glow: 0.12;
}

.boot-appearance-character-hotspot {
  position: absolute;
  z-index: 6;
  margin: 0;
  padding: 0;
  border: 0 !important;
  outline: 0 !important;
  border-radius: 0;
  background: none !important;
  box-shadow: none !important;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  appearance: none;
  overflow: visible;
}

/*
  Le bouton lui-même n'a plus aucun rendu. L'effet est produit par une couche
  masquée radialement qui augmente uniquement la luminosité de la vidéo située
  derrière le personnage. backdrop-filter est plus judicieux qu'un filtre sur
  toute la vidéo : il ne touche que cette petite zone et conserve l'animation.
*/
.boot-appearance-character-hotspot::before {
  content: "";
  position: absolute;
  inset: -12% -10% -10%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    ellipse at 52% 55%,
    rgba(155, 248, 255, var(--appearance-character-halo-glow)) 0%,
    rgba(104, 231, 255, calc(var(--appearance-character-halo-glow) * 0.58)) 38%,
    transparent 73%
  );
  -webkit-backdrop-filter: brightness(var(--appearance-character-halo-brightness)) saturate(1.08);
  backdrop-filter: brightness(var(--appearance-character-halo-brightness)) saturate(1.08);
  -webkit-mask-image: radial-gradient(ellipse at 52% 55%, #000 0 38%, rgba(0,0,0,.88) 54%, transparent 78%);
  mask-image: radial-gradient(ellipse at 52% 55%, #000 0 38%, rgba(0,0,0,.88) 54%, transparent 78%);
  mix-blend-mode: screen;
  filter: drop-shadow(0 0 18px rgba(103, 235, 255, 0.22));
  transition: opacity 150ms ease-out;
  will-change: opacity, backdrop-filter;
}

.boot-appearance-character-hotspot:hover::before,
.boot-appearance-character-hotspot:focus-visible::before {
  opacity: 1;
}

.boot-appearance-character-hotspot:active::before {
  opacity: 0.88;
}

.boot-appearance-character-hotspot:hover,
.boot-appearance-character-hotspot:focus,
.boot-appearance-character-hotspot:focus-visible,
.boot-appearance-character-hotspot:active {
  border: 0 !important;
  outline: 0 !important;
  background: none !important;
  box-shadow: none !important;
}


/* ---------- V88 : transitions dédiées Histoire / Classique / En ligne / Boutique ---------- */
.mode-transition-layer {
  position: fixed;
  inset: 0;
  z-index: 10019;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: #000;
  user-select: none;
  -webkit-user-select: none;
}

.mode-transition-layer[hidden] {
  display: none !important;
}

.mode-transition-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
  pointer-events: none;
  transform: translateZ(0);
  backface-visibility: hidden;
}

.mode-transition-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at center, transparent 60%, rgba(0,0,0,0.24) 100%),
    linear-gradient(to bottom, rgba(0,0,0,0.08), transparent 18%, transparent 82%, rgba(0,0,0,0.18));
}

.mode-transition-hint {
  position: absolute;
  right: max(18px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: 2;
  margin: 0;
  color: rgba(255,255,255,0.72);
  font-family: "Press Start 2P", "Courier New", monospace;
  font-size: clamp(7px, 0.72vw, 10px);
  letter-spacing: 0.08em;
  text-shadow: 0 0 10px rgba(80, 210, 255, 0.72);
  opacity: 0;
  animation: mode-transition-hint-in 550ms ease 1.4s forwards;
}

@keyframes mode-transition-hint-in {
  to { opacity: 0.72; }
}

body.mode-transition-active {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .mode-transition-hint { animation: none; opacity: 0.72; }
}

/* =========================================================
   V89 — démarrage sans flash noir pour toutes les transitions vidéo
   ========================================================= */
.chaos-transition-layer,
.mode-transition-layer {
  background-color: #000;
  background-image: var(--transition-poster, none);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 1;
  transition: opacity 90ms linear;
}

.chaos-transition-layer.transition-priming,
.mode-transition-layer.transition-priming {
  opacity: 0;
}

.chaos-transition-layer.transition-ready,
.mode-transition-layer.transition-ready {
  opacity: 1;
}

.chaos-transition-video,
.mode-transition-video {
  background: transparent;
  opacity: 0;
  transition: opacity 70ms linear;
}

.chaos-transition-video.transition-video-ready,
.mode-transition-video.transition-video-ready {
  opacity: 1;
}


/* =========================================================
   V95 — handoff vidéo sans frame noire
   ========================================================= */
.chaos-transition-layer,
.mode-transition-layer {
  /* Tant que la vidéo n'a pas réellement peint sa première frame, le CRT
     reste visible derrière cette couche. Aucun fond opaque ne peut donc créer
     un flash noir pendant le décodage. */
  background-color: transparent;
}

.chaos-transition-video,
.mode-transition-video {
  background: transparent;
}

.chaos-transition-layer.transition-priming,
.mode-transition-layer.transition-priming {
  opacity: 0;
  pointer-events: none;
}

.chaos-transition-layer.transition-ready,
.mode-transition-layer.transition-ready {
  opacity: 1;
  pointer-events: auto;
}

.chaos-transition-video.transition-video-ready,
.mode-transition-video.transition-video-ready {
  opacity: 1;
}


/* =========================================================
   V96 — cartes de modes invisibles + halo local sur la vidéo CRT
   ========================================================= */
:root {
  --mode-card-halo-brightness: 2.40;
  --mode-card-halo-glow: 0.204;
}

/*
  Les zones Histoire / Classique / Chaos / En ligne / Boutique restent de vrais
  contrôles accessibles, mais ne dessinent plus aucun rectangle. L'effet est
  appliqué aux pixels du menu vidéo situés derrière chaque carte via un
  backdrop-filter masqué, sur le même principe que le personnage Apparence.
*/
.boot-hotspot.boot-card.boot-mode-halo-hotspot {
  position: absolute;
  z-index: 5;
  margin: 0;
  padding: 0;
  border: 0 !important;
  outline: 0 !important;
  border-radius: 0 !important;
  background: none !important;
  box-shadow: none !important;
  transform: none !important;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  appearance: none;
  overflow: visible;
}

.boot-hotspot.boot-card.boot-mode-halo-hotspot::before {
  content: "";
  position: absolute;
  inset: -8% -10%;
  pointer-events: none;
  opacity: 0;
  border-radius: 18px;
  background: radial-gradient(
    ellipse at 50% 50%,
    rgba(var(--boot-accent-rgb), var(--mode-card-halo-glow)) 0%,
    rgba(var(--boot-accent-rgb), calc(var(--mode-card-halo-glow) * 0.52)) 46%,
    transparent 80%
  );
  -webkit-backdrop-filter: brightness(var(--mode-card-halo-brightness)) saturate(1.10);
  backdrop-filter: brightness(var(--mode-card-halo-brightness)) saturate(1.10);
  -webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 0 48%, rgba(0,0,0,.92) 61%, transparent 87%);
  mask-image: radial-gradient(ellipse at 50% 50%, #000 0 48%, rgba(0,0,0,.92) 61%, transparent 87%);
  mix-blend-mode: screen;
  filter: drop-shadow(0 0 18px rgba(var(--boot-accent-rgb), 0.22));
  transition: opacity 145ms ease-out;
  will-change: opacity, backdrop-filter;
}

.boot-hotspot.boot-card.boot-mode-halo-hotspot:hover::before,
.boot-hotspot.boot-card.boot-mode-halo-hotspot:focus-visible::before,
.boot-hotspot.boot-card.boot-mode-halo-hotspot.selected::before {
  opacity: 1;
}

.boot-hotspot.boot-card.boot-mode-halo-hotspot:active::before {
  opacity: 0.86;
}

/* Neutralise toutes les anciennes apparences de sélection/hover V79. */
.boot-hotspot.boot-card.boot-mode-halo-hotspot:hover,
.boot-hotspot.boot-card.boot-mode-halo-hotspot:focus,
.boot-hotspot.boot-card.boot-mode-halo-hotspot:focus-visible,
.boot-hotspot.boot-card.boot-mode-halo-hotspot:active,
.boot-hotspot.boot-card.boot-mode-halo-hotspot.selected,
.boot-hotspot.boot-card.boot-mode-halo-hotspot.selected:hover,
.boot-hotspot.boot-card.boot-mode-halo-hotspot.selected:focus-visible {
  border: 0 !important;
  outline: 0 !important;
  border-color: transparent !important;
  background: none !important;
  box-shadow: none !important;
  transform: none !important;
}

/* En mode debug, on garde volontairement l'aide visuelle de calibration. */
.boot-layer.debug .boot-hotspot.boot-card.boot-mode-halo-hotspot {
  border: 1px solid #00ff88 !important;
  background: rgba(0, 255, 136, 0.08) !important;
}


/* =========================================================
   V97 — halo navigation étendu aux actions + raccord synchronisé
   ========================================================= */
.boot-hotspot.boot-action.boot-action-halo-hotspot {
  position: absolute;
  z-index: 5;
  margin: 0;
  padding: 0;
  border: 0 !important;
  outline: 0 !important;
  border-radius: 0 !important;
  background: none !important;
  box-shadow: none !important;
  transform: none !important;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  appearance: none;
  overflow: visible;
}

.boot-hotspot.boot-action.boot-action-halo-hotspot::before {
  content: "";
  position: absolute;
  inset: -65% -12%;
  pointer-events: none;
  opacity: 0;
  border-radius: 999px;
  background: radial-gradient(
    ellipse at 50% 50%,
    rgba(var(--boot-accent-rgb), calc(var(--mode-card-halo-glow) * 0.92)) 0%,
    rgba(var(--boot-accent-rgb), calc(var(--mode-card-halo-glow) * 0.42)) 48%,
    transparent 82%
  );
  -webkit-backdrop-filter: brightness(var(--mode-card-halo-brightness)) saturate(1.10);
  backdrop-filter: brightness(var(--mode-card-halo-brightness)) saturate(1.10);
  -webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 0 50%, rgba(0,0,0,.90) 63%, transparent 88%);
  mask-image: radial-gradient(ellipse at 50% 50%, #000 0 50%, rgba(0,0,0,.90) 63%, transparent 88%);
  mix-blend-mode: screen;
  filter: drop-shadow(0 0 15px rgba(var(--boot-accent-rgb), 0.22));
  transition: opacity 145ms ease-out;
  will-change: opacity, backdrop-filter;
}

.boot-hotspot.boot-action.boot-action-halo-hotspot:hover::before,
.boot-hotspot.boot-action.boot-action-halo-hotspot:focus-visible::before,
.boot-hotspot.boot-action.boot-action-halo-hotspot:active::before {
  opacity: 1;
}

.boot-hotspot.boot-action.boot-action-halo-hotspot:hover,
.boot-hotspot.boot-action.boot-action-halo-hotspot:focus,
.boot-hotspot.boot-action.boot-action-halo-hotspot:focus-visible,
.boot-hotspot.boot-action.boot-action-halo-hotspot:active {
  border: 0 !important;
  outline: 0 !important;
  background: none !important;
  box-shadow: none !important;
  transform: none !important;
}

/* La vidéo reste totalement visible pendant l'accélération vers la frame de
   raccord. Le navigateur ne reçoit aucun fondu noir ou changement d'opacité. */
#bootMenuVideo.menu-sync-accelerating {
  opacity: 1 !important;
  visibility: visible !important;
}

.boot-layer.debug .boot-hotspot.boot-action.boot-action-halo-hotspot {
  border: 1px solid #00ff88 !important;
  background: rgba(0, 255, 136, 0.08) !important;
}


/* =========================================================
   V102 — continuité menu CRT → transition
   Trois ruptures visuelles supprimées au raccord :
   1. la géométrie (le menu était « contain » dans une boîte 16:9, les
      transitions « cover » plein écran : saut d'échelle sur tout écran
      dont le ratio n'est pas exactement 16:9),
   2. les scanlines, présentes sur le menu et absentes des transitions,
   3. la vignette, qui apparaissait d'un coup au lieu de monter en douceur.
   ========================================================= */

/* 1. Boîte strictement identique à .boot-video-stage. */
.transition-video-stage {
  position: relative;
  width: min(100vw, calc(100vh * 16 / 9));
  aspect-ratio: 16 / 9;
  overflow: hidden;
  contain: layout paint size;
  isolation: isolate;
}

.chaos-transition-video,
.mode-transition-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: transparent;
  pointer-events: none;
  transform: translateZ(0);
  backface-visibility: hidden;
  contain: paint;
}

/* Le fond de la couche reste transparent hors de la boîte 16:9 : le CRT
   continue d'occuper l'écran tant que la transition n'a pas peint. */
.chaos-transition-layer,
.mode-transition-layer {
  background-color: transparent;
  background-image: none;
}

/* Le poster anti-flash noir de la V89 est conservé, mais cadré dans la même
   boîte 16:9 que la vidéo au lieu d'être étiré en plein écran. */
.transition-video-stage {
  background-color: transparent;
  background-image: var(--transition-poster, none);
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

/* 2. Les scanlines du menu couvrent aussi la transition : même overlay,
      même opacité, aucune disparition au moment du raccord. */
.transition-video-stage .boot-video-scanlines {
  z-index: 1;
}

/* 3. Durée de fondu pilotée par le runtime selon l'écart mesuré entre
      l'image de menu affichée et la première image de la transition.
      0 ms = raccord image sur image, donc coupe franche. */
.chaos-transition-layer,
.mode-transition-layer {
  transition: opacity var(--handoff-fade-ms, 90ms) linear;
}

.chaos-transition-video,
.mode-transition-video {
  opacity: 0;
  transition: opacity var(--handoff-fade-ms, 70ms) linear;
}

.chaos-transition-video.transition-video-ready,
.mode-transition-video.transition-video-ready {
  opacity: 1;
}

.chaos-transition-vignette,
.mode-transition-vignette {
  opacity: 0;
  transition: opacity 700ms ease 120ms;
}

.chaos-transition-layer.transition-ready .chaos-transition-vignette,
.mode-transition-layer.transition-ready .mode-transition-vignette {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .chaos-transition-vignette,
  .mode-transition-vignette {
    transition: none;
    opacity: 1;
  }
}


/* =========================================================
   V103 — écran titre « vieille télé »
   Dalle noire plein écran, rendue par crt-title.js : courbure en tonneau,
   scanlines, masque d'ouverture, aberration chromatique, bloom, bruit VHS,
   scintillement, vignette et coins de tube arrondis. Le logo y rebondit
   comme un écran de veille DVD.
   ========================================================= */

#bootTitleScreen.boot-crt-title {
  background: #000;
  cursor: pointer;
  place-items: stretch;
}

/* Plus de photo floutée derrière le titre : la dalle est noire. */
#bootTitleScreen.boot-crt-title::before {
  content: none;
}

.crt-title-shell {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  background: #000;
  /* L'ancien écran titre portait une animation de respiration sur ce même
     identifiant (boot-title-breathe, scale 1 → 1,035). Sur une dalle CRT elle
     redimensionnait le canvas à chaque image — coûteux, et le bord du tube
     se mettait à respirer avec. On la neutralise explicitement. */
  animation: none !important;
  transform: none !important;
}

.crt-title-screen {
  position: absolute;
  inset: 0;
}

.crt-title-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: auto;
}

/* Repli sans WebGL : scanlines et vignette posées en CSS par-dessus le
   canvas de contenu, pour garder l'esprit du rendu. */
.crt-title-screen.crt-no-webgl::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom,
      rgba(255,255,255,0.05) 0, rgba(255,255,255,0.05) 1px,
      rgba(0,0,0,0.22) 1px, rgba(0,0,0,0.22) 3px),
    radial-gradient(ellipse at center, transparent 52%, rgba(0,0,0,0.75) 100%);
  mix-blend-mode: multiply;
}

.boot-crt-srtext {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- allumage du tube sur le menu CRT ---------- */
@keyframes crt-power-on {
  0%   { transform: scaleY(0.004); filter: brightness(7) saturate(0) contrast(1.6); }
  16%  { transform: scaleY(0.016); filter: brightness(5) saturate(0.25) contrast(1.4); }
  38%  { transform: scaleY(0.55);  filter: brightness(2.6) saturate(0.8) contrast(1.15); }
  62%  { transform: scaleY(1.055); filter: brightness(1.55) saturate(1.06); }
  80%  { transform: scaleY(0.982); filter: brightness(1.14) saturate(1.02); }
  100% { transform: none; filter: none; }
}

@keyframes crt-line-flash {
  0%   { opacity: 1;   transform: scaleY(0.05); }
  30%  { opacity: 0.9; transform: scaleY(0.35); }
  100% { opacity: 0;   transform: scaleY(1); }
}

#bootMenuScreen.crt-power-on #bootMenuStage {
  transform-origin: center center;
  animation: crt-power-on 640ms cubic-bezier(0.18, 0.86, 0.24, 1) forwards;
  will-change: transform, filter;
}

#bootMenuScreen.crt-power-on .boot-flash {
  opacity: 1;
  transform-origin: center center;
  background: linear-gradient(to bottom,
    transparent 46%,
    rgba(255,255,255,0.95) 49.4%,
    rgba(210,245,255,1) 50%,
    rgba(255,255,255,0.95) 50.6%,
    transparent 54%);
  animation: crt-line-flash 520ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}

@media (prefers-reduced-motion: reduce) {
  #bootMenuScreen.crt-power-on #bootMenuStage,
  #bootMenuScreen.crt-power-on .boot-flash {
    animation-duration: 160ms;
  }
}

/* =========================================================
   V105 — Vidéo d'ouverture (HorizonFall)
   Couche posée au-dessus de tout, avant l'écran titre CRT.
   Elle doit pouvoir échouer sans jamais bloquer le démarrage :
   toute la logique de repli est dans boot-screens.js.
   ========================================================= */

.boot-intro {
  /* L'écran titre est un .boot-layer de même z-index placé après dans le DOM :
     sans ce cran supplémentaire, il recouvrirait l'introduction. */
  z-index: 10001;
  background: #000;
  cursor: pointer;
  /* Le fondu de sortie est piloté par la classe .is-leaving. */
  opacity: 1;
  transition: opacity 520ms ease;
}

.boot-intro.is-leaving {
  opacity: 0;
}

.boot-intro-video {
  display: block;
  width: 100vw;
  height: 100vh;
  /* contain plutôt que cover : l'intro est cadrée, on ne la recadre pas. */
  object-fit: contain;
  background: #000;
}

/* Léger assombrissement des bords, pour raccorder avec le tube du menu. */
.boot-intro-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse at 50% 50%,
    rgba(0, 0, 0, 0) 58%,
    rgba(0, 0, 0, 0.42) 100%
  );
}

.boot-intro-skip {
  position: absolute;
  right: clamp(14px, 3vw, 42px);
  bottom: clamp(14px, 4vh, 44px);
  margin: 0;
  padding: 7px 13px;
  font-family: "Courier New", monospace;
  font-size: clamp(0.6rem, 1.1vw, 0.74rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(226, 236, 255, 0.72);
  border: 1px solid rgba(226, 236, 255, 0.24);
  border-radius: 4px;
  background: rgba(4, 3, 10, 0.42);
  pointer-events: none;
  /* Apparaît après une seconde : on ne propose pas de sauter avant
     que le joueur ait vu quoi que ce soit. */
  opacity: 0;
  animation: boot-intro-skip-in 600ms ease 1000ms forwards;
}

@keyframes boot-intro-skip-in {
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .boot-intro { transition-duration: 120ms; }
  .boot-intro-skip { animation-delay: 200ms; }
}

/* =========================================================
   V109 — Codex Domain.exe

   Le panneau se plaque sur la dalle du téléviseur dans sa perspective :
   la transformation matrix3d est calculée en JavaScript à partir des
   quatre coins réels de l'écran. Le CSS ne fait que le décorer.

   L'habillage suit le Terminal Hack équipé, via les mêmes variables
   --term-* définies dans styles.css. Un seul jeu de couleurs pour les
   deux surfaces : changer d'habillage change les deux.
   ========================================================= */

.boot-codex-hotspot {
  position: absolute;
  z-index: 6;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  /* Aucun indice visuel : c'est un secret. */
  opacity: 0;
}

.boot-codex-panel {
  /* --- V112 : allumage et effet cathodique. Tout est réglable, et poser
     une force à 0 éteint l'effet correspondant sans rien casser. --- */
  /* Multiplicateur de durée de l'allumage. 1 = 640 ms, exactement la
     durée de crt-power-on : les deux tubes s'allument sur le même temps. */
  --crt-ignite: 1;
  /* Dérive des stries : un tube ne tient jamais sa trame parfaitement. */
  --crt-scan-drift: 9s;
  --crt-scan-shift: 3px;
  /* Barre de retour vertical : la bande claire qui descend lentement. */
  --crt-roll: 1;
  --crt-roll-speed: 7.5s;
  /* Battement secteur : la respiration de luminosité du tube. */
  --crt-flicker: 1;
  --crt-flicker-speed: 2.6s;
  /* Grain de phosphore. */
  --crt-noise: 0.06;
  --crt-noise-speed: 0.9s;
  /* Aberration chromatique et halo du texte : la convergence n'est jamais
     parfaite, et le phosphore déborde toujours un peu. */
  --crt-fringe: 0.55;
  --crt-bloom: 0.7;
  /* Décrochage de synchro : une secousse brève, de loin en loin. */
  --crt-slip: 1;
  --crt-slip-period: 11s;

  /* Durées effectives, dérivées du multiplicateur ci-dessus. */
  --codex-ignite-dur: calc(640ms * var(--crt-ignite, 1));
  --codex-extinguish-dur: calc(400ms * var(--crt-ignite, 1));

  /* Réglages figés au calibrage — voir V110. */
  --codex-curve-x: 120%;
  --codex-curve-y: 560%;
  --codex-curve-origin: 100%;
  --codex-vignette: 1.95;
  --codex-vignette-start: 64%;
  --codex-lamp-y: 86%;
  --codex-lamp-w: 20%;
  --codex-sheen: 0.9;
  --codex-pad-x: 6.8%;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 8;
  transform-origin: 0 0;
  pointer-events: auto;
  /* La perspective vient de la matrice ; rien à interpoler ici. */
  will-change: transform;
}

.boot-codex-panel.hidden { display: none !important; }

.boot-codex-surface {
  position: absolute;
  /* La surface remplit tout le panneau : la marge réservée aux arêtes
     bombées en fait partie. C'est le clip-path qui donne la forme réelle,
     et le contenu se cale sur --codex-margin. */
  inset: 0;
  /* Galbe du tube : les coins sont largement arrondis, et pas tout à fait
     de la même façon, comme sur un vrai masque cathodique. Les rayons sont
     réglables un par un — voir l'outil de calibrage. */
  border-radius:
    var(--codex-r-tl-x, 4.2%) var(--codex-r-tr-x, 4.6%)
    var(--codex-r-br-x, 4.4%) var(--codex-r-bl-x, 4.0%) /
    var(--codex-r-tl-y, 6.2%) var(--codex-r-tr-y, 6.6%)
    var(--codex-r-br-y, 6.4%) var(--codex-r-bl-y, 6.0%);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Aucun bord dessiné : la forme vient uniquement du clip-path. Un bord
     ou un rayon droit ici recréerait le rectangle qu'on cherche à effacer. */
  border: 0;
  color: var(--term-ink, #a8ffc7);
  background:
    linear-gradient(var(--term-bg-top, rgba(12, 30, 19, 0.96)),
                    var(--term-bg-bottom, rgba(2, 10, 7, 0.985))),
    var(--term-base, #041009);
  /* Plus d'anneau de contour : seul le halo intérieur reste, et il est
     diffus donc il ne dessine aucune arête. */
  box-shadow: inset 0 0 40px var(--term-scan, rgba(32, 255, 108, 0.055));
  font-family: "Courier New", monospace;
}

/* Stries du tube, reprises de l'habillage du terminal. */
.boot-codex-surface::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--term-scan-opacity, 0.28);
  background: repeating-linear-gradient(
    180deg,
    transparent 0 3px,
    var(--term-scan, rgba(122, 255, 164, 0.055)) 3px 4px
  );
}

.boot-codex-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 1.6%;
  border-bottom: 1px dashed var(--term-border-soft, rgba(124, 255, 166, 0.27));
}

.boot-codex-kicker {
  font-size: clamp(7px, 0.95vw, 12px);
  letter-spacing: 0.14em;
  color: var(--term-ink-faint, rgba(159, 255, 190, 0.58));
}

.boot-codex-close {
  padding: 2px 7px;
  border: 1px solid var(--term-border, rgba(93, 255, 157, 0.6));
  border-radius: 4px;
  background: var(--term-halo, rgba(48, 255, 122, 0.1));
  color: var(--term-ink-strong, #caffd9);
  font: 700 clamp(7px, 0.9vw, 11px)/1 "Courier New", monospace;
  cursor: pointer;
}

.boot-codex-body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1.8% 0;
  display: grid;
  gap: 1.4%;
  align-content: start;
}

.boot-codex-entry {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 10px;
  align-items: start;
  padding: 1.2% 1.4%;
  border: 1px solid var(--term-border-soft, rgba(124, 255, 166, 0.22));
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.28);
}

/* Verrouillé : présent au registre, illisible. Savoir qu'une ligne
   existe fait partie du secret. */
.boot-codex-entry.locked {
  opacity: 0.5;
  border-style: dashed;
}

.boot-codex-entry-text strong {
  display: block;
  font-size: clamp(9px, 1.15vw, 15px);
  letter-spacing: 0.06em;
  color: var(--term-ink-strong, #caffd9);
}

.boot-codex-entry-text em {
  display: inline-block;
  margin: 2px 0 3px;
  font-style: normal;
  font-size: clamp(6px, 0.8vw, 10px);
  letter-spacing: 0.16em;
  color: var(--term-accent, #77ffa6);
}

.boot-codex-entry-text p {
  margin: 0;
  font-size: clamp(7px, 0.92vw, 12px);
  line-height: 1.45;
  color: var(--term-ink-soft, rgba(162, 255, 192, 0.72));
}

/* Case à cocher dessinée à la main : les contrôles natifs ne se
   thématisent pas et casseraient l'illusion du terminal. */
.boot-codex-toggle {
  position: relative;
  display: block;
  width: clamp(14px, 1.6vw, 22px);
  height: clamp(14px, 1.6vw, 22px);
  cursor: pointer;
}

.boot-codex-toggle input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.boot-codex-toggle span {
  position: absolute;
  inset: 0;
  border: 1px solid var(--term-border, rgba(93, 255, 157, 0.7));
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.4);
}

.boot-codex-toggle input:checked + span::after {
  content: "";
  position: absolute;
  inset: 22%;
  background: var(--term-accent, #77ffa6);
  box-shadow: 0 0 8px var(--term-glow, rgba(75, 255, 132, 0.5));
}

.boot-codex-toggle input:disabled + span {
  opacity: 0.4;
  cursor: not-allowed;
}

.boot-codex-foot {
  position: relative;
  padding-top: 1.4%;
  border-top: 1px dashed var(--term-border-soft, rgba(124, 255, 166, 0.2));
  font-size: clamp(7px, 0.85vw, 11px);
  letter-spacing: 0.08em;
  color: var(--term-ink-faint, rgba(159, 255, 190, 0.55));
}

/* =========================================================
   V109b — Intégration du codex à la dalle

   Trois ajouts pour que le panneau cesse d'être un rectangle posé
   par-dessus et devienne l'image affichée par le tube :

     · le galbe, suggéré par des stries courbes plutôt que droites —
       c'est le repère optique le plus fort d'un écran bombé ;
     · la vignette de tube, qui assombrit les coins comme le fait un
       masque cathodique ;
     · les reflets du salon sur le verre, dont celui du personnage
       lumineux posé en bas à droite de la pièce.

   Tout est en couche non interactive au-dessus du contenu, et rien
   n'anime en continu : ces surfaces sont statiques, donc gratuites.
   ========================================================= */

/* Les stries suivent une très large ellipse au lieu d'être droites :
   elles se courbent vers le bas aux extrémités, exactement comme sur un
   tube. C'est ce détail qui vend le galbe. */
.boot-codex-surface::before {
  background: repeating-radial-gradient(
    ellipse var(--codex-curve-x, 180%) var(--codex-curve-y, 260%)
      at 50% var(--codex-curve-origin, 50%),
    transparent 0 3px,
    var(--term-scan, rgba(122, 255, 164, 0.055)) 3px 4px
  );
}

.boot-codex-glass {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  z-index: 3;
  background:
    /* 1. Reflet du personnage lumineux, en bas à droite de la pièce.
       Blanc froid, large et très doux : c'est une lampe diffuse vue à
       travers un verre épais, pas une source ponctuelle. */
    radial-gradient(
      ellipse var(--codex-lamp-w, 46%) var(--codex-lamp-h, 38%)
        at var(--codex-lamp-x, 92%) var(--codex-lamp-y, 88%),
      rgba(214, 236, 255, calc(0.21 * var(--codex-lamp, 1))),
      rgba(190, 224, 255, calc(0.09 * var(--codex-lamp, 1))) 45%,
      transparent 72%
    ),
    /* 2. Son second lobe, plus resserré et plus vif, décalé vers le bord :
       le point chaud de la lampe. */
    radial-gradient(
      ellipse 16% 13% at calc(var(--codex-lamp-x, 92%) + 5%)
                         calc(var(--codex-lamp-y, 88%) + 3%),
      rgba(232, 244, 255, calc(0.27 * var(--codex-lamp, 1))),
      transparent 70%
    ),
    /* 3. Reflet de la fenêtre de la ville, en haut à gauche. */
    radial-gradient(
      ellipse 40% 30% at 6% 6%,
      rgba(126, 168, 224, calc(0.09 * var(--codex-window, 1))),
      transparent 68%
    ),
    /* 4. Voile diagonal : la pièce dans le verre. */
    linear-gradient(
      116deg,
      rgba(255, 255, 255, calc(0.055 * var(--codex-sheen, 1))) 0%,
      transparent 26%,
      transparent 62%,
      rgba(255, 255, 255, calc(0.035 * var(--codex-sheen, 1))) 84%,
      transparent 100%
    ),
    /* 5. Assombrissement haut et bas, en remplacement de l'ancien liseré
       rectangulaire : posé sur le verre, il suit le découpage. */
    linear-gradient(
      180deg,
      rgba(0, 0, 0, 0.34) 0%,
      transparent 9%,
      transparent 90%,
      rgba(0, 0, 0, 0.40) 100%
    ),
    /* 6. Vignette de tube : les coins d'un masque cathodique sont
       toujours plus sombres que le centre. */
    radial-gradient(
      ellipse 78% 74% at 50% 50%,
      transparent var(--codex-vignette-start, 52%),
      rgba(0, 0, 0, calc(0.20 * var(--codex-vignette, 1))) 88%,
      rgba(0, 0, 0, calc(0.38 * var(--codex-vignette, 1))) 100%
    );
}


/* Le contenu doit rester sous le verre. */
.boot-codex-head,
.boot-codex-body,
.boot-codex-foot { z-index: 2; }

/* Léger renfoncement du contenu : sur un tube bombé, le bord de l'image
   est plus éloigné de l'œil, donc jamais occupé par du texte. */
.boot-codex-head {
  margin: calc(var(--codex-margin, 0px) + var(--codex-pad-y, 3.2%))
          calc(var(--codex-margin, 0px) + var(--codex-pad-x, 4%)) 0;
}
.boot-codex-body {
  margin: 0 calc(var(--codex-margin, 0px) + var(--codex-pad-x, 4%));
}
.boot-codex-foot {
  margin: 0 calc(var(--codex-margin, 0px) + var(--codex-pad-x, 4%))
            calc(var(--codex-margin, 0px) + var(--codex-pad-y, 3.2%));
}

/* =========================================================
   V112 — Allumage du panneau et effet cathodique

   Deux choses distinctes :

     · l'ALLUMAGE, joué une seule fois à l'ouverture. Un tube ne s'allume
       pas en fondu : l'image s'ouvre depuis une ligne horizontale
       éblouissante, dépasse, revient, et la trame se cale en deux ou trois
       oscillations. À l'extinction, l'inverse — la ligne, puis le point.

     · l'EFFET PERMANENT, qui tourne tant que le panneau est ouvert :
       dérive des stries, barre de retour vertical, battement secteur,
       grain de phosphore, aberration chromatique et décrochage de synchro.

   RÈGLE DE PERFORMANCE — voir la V109 : rien ici n'anime autre chose que
   transform et opacity, et aucun mode de fusion n'est utilisé. Ce sont les
   deux seules propriétés que le compositeur sait traiter sans repeindre.
   Une seule de ces animations écrite en background-position ou en filter
   suffirait à faire repeindre le panneau soixante fois par seconde
   par-dessus une vidéo qui joue. Le panneau ne s'ouvre par ailleurs que
   dans le menu, où la boucle de jeu est arrêtée.
   ========================================================= */

/* Le tube porte la géométrie de l'allumage ; la surface porte l'image et
   ses secousses. Les séparer évite que les deux transformations se
   marchent dessus — et toutes deux se composent proprement sous la
   perspective du panneau. */
.boot-codex-tube {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
}

.boot-codex-panel.igniting .boot-codex-tube {
  animation: codex-ignite-tube var(--codex-ignite-dur) cubic-bezier(0.2, 0.8, 0.3, 1) 1 both;
}

.boot-codex-panel.extinguishing .boot-codex-tube {
  animation: codex-extinguish-tube var(--codex-extinguish-dur) cubic-bezier(0.6, 0, 0.9, 0.4) 1 both;
}

@keyframes codex-ignite-tube {
  0%   { transform: scale3d(1.06, 0.004, 1); }
  9%   { transform: scale3d(1.05, 0.008, 1); }
  26%  { transform: scale3d(1.012, 0.30, 1); }
  44%  { transform: scale3d(0.995, 1.055, 1); }
  60%  { transform: scale3d(1.004, 0.972, 1); }
  76%  { transform: scale3d(0.999, 1.012, 1); }
  88%  { transform: scale3d(1, 0.996, 1); }
  100% { transform: scale3d(1, 1, 1); }
}

/* L'extinction : l'image s'écrase sur une ligne, la ligne se resserre sur
   un point, le point s'éteint. */
@keyframes codex-extinguish-tube {
  0%   { transform: scale3d(1, 1, 1); }
  34%  { transform: scale3d(1.02, 0.055, 1); }
  62%  { transform: scale3d(1.05, 0.006, 1); }
  100% { transform: scale3d(0.07, 0.004, 1); }
}

/* Décrochage de synchro : la quasi-totalité de la durée est immobile, la
   secousse dure trois dixièmes de seconde. steps(1, end) fait sauter la
   valeur d'un palier à l'autre au lieu de l'interpoler — un décrochage est
   net, et une interpolation sous-pixel rendrait le texte flou. */
.boot-codex-surface {
  animation: codex-slip var(--crt-slip-period, 11s) steps(1, end) infinite;
}

@keyframes codex-slip {
  0%, 93.4% { transform: translate3d(0, 0, 0); }
  94%   { transform: translate3d(calc(-2px * var(--crt-slip, 1)), 0, 0); }
  94.9% { transform: translate3d(calc(3px * var(--crt-slip, 1)), 0, 0); }
  95.6% { transform: translate3d(calc(-1px * var(--crt-slip, 1)), 0, 0); }
  96.3%, 100% { transform: translate3d(0, 0, 0); }
}

/* Aberration chromatique et halo de phosphore. Hérités par tout le texte du
   panneau, et peints une seule fois : ils ne coûtent rien à l'animation. Le
   halo prend la couleur de l'habillage équipé. */
.boot-codex-surface {
  text-shadow:
    calc(0.62px * var(--crt-fringe, 0.55)) 0 0
      rgba(255, 62, 84, calc(0.5 * var(--crt-fringe, 0.55))),
    calc(-0.62px * var(--crt-fringe, 0.55)) 0 0
      rgba(62, 188, 255, calc(0.45 * var(--crt-fringe, 0.55))),
    0 0 calc(7px * var(--crt-bloom, 0.7)) var(--term-glow, rgba(75, 255, 132, 0.3));
}

/* Les stries dérivent : la trame d'un tube ne tient jamais parfaitement en
   place. Le va-et-vient évite d'avoir à connaître la période exacte du
   motif — qui dépend de l'ellipse du galbe — pour boucler sans saut. */
.boot-codex-surface::before {
  inset: -10px 0;
  animation: codex-scan-drift var(--crt-scan-drift, 9s) ease-in-out infinite alternate;
}

@keyframes codex-scan-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, var(--crt-scan-shift, 3px), 0); }
}

/* Couche des effets dynamiques, au-dessus du verre : ce que le tube émet
   passe devant ses reflets. */
.boot-codex-fx {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  border-radius: inherit;
}

/* Grain de phosphore. La texture est un bruit fractal produit par un filtre
   SVG en URI de données : rien à télécharger, et le navigateur ne la calcule
   qu'une fois. Elle déborde de tous les côtés pour que le déplacement ne
   découvre jamais un bord. */
.boot-codex-fx::before {
  content: "";
  position: absolute;
  inset: -56px;
  opacity: var(--crt-noise, 0.06);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
  animation: codex-grain var(--crt-noise-speed, 0.9s) steps(1, end) infinite;
}

/* Cinq champs de grain différents, sans interpolation : on ne fait pas
   glisser du bruit, on le remplace. */
@keyframes codex-grain {
  0%   { transform: translate3d(0, 0, 0); }
  20%  { transform: translate3d(-31px, 17px, 0); }
  40%  { transform: translate3d(19px, -23px, 0); }
  60%  { transform: translate3d(-11px, -37px, 0); }
  80%  { transform: translate3d(27px, 29px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* Battement secteur : le tube respire au rythme de l'alimentation. Lent et
   faible — la V109 avait déjà ramené le scintillement du menu de 0,18 s à
   2,4 s pour cette raison : un battement rapide fatigue et coûte cher. */
.boot-codex-fx::after {
  content: "";
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  animation: codex-flicker var(--crt-flicker-speed, 2.6s) ease-in-out infinite;
}

@keyframes codex-flicker {
  0%, 100% { opacity: calc(0.012 * var(--crt-flicker, 1)); }
  38%      { opacity: calc(0.055 * var(--crt-flicker, 1)); }
  54%      { opacity: calc(0.018 * var(--crt-flicker, 1)); }
  71%      { opacity: calc(0.042 * var(--crt-flicker, 1)); }
}

/* Barre de retour vertical : la bande claire qui descend sans fin. C'est le
   signe le plus reconnaissable d'un tube filmé — et l'un des plus discrets,
   parce qu'elle ne fait que passer. */
.boot-codex-roll {
  position: absolute;
  left: -4%;
  right: -4%;
  top: 0;
  height: 16%;
  z-index: 4;
  pointer-events: none;
  opacity: var(--crt-roll, 1);
  background: linear-gradient(
    180deg,
    transparent 0%,
    rgba(255, 255, 255, 0.028) 26%,
    rgba(255, 255, 255, 0.062) 50%,
    rgba(255, 255, 255, 0.022) 74%,
    transparent 100%
  );
  animation: codex-roll var(--crt-roll-speed, 7.5s) linear infinite;
}

@keyframes codex-roll {
  from { transform: translate3d(0, -120%, 0); }
  to   { transform: translate3d(0, 760%, 0); }
}

/* À l'allumage la barre passe plusieurs fois, vite et fort : la trame n'est
   pas encore verrouillée. */
.boot-codex-panel.igniting .boot-codex-roll {
  opacity: calc(var(--crt-roll, 1) * 2.4);
  animation: codex-roll calc(var(--codex-ignite-dur) * 0.4) linear infinite;
}

/* Plaque d'éclat. À l'instant où le tube est écrasé sur une ligne, c'est
   elle qu'on voit : la ligne éblouissante. Elle est DANS la surface, donc
   découpée à la forme de la dalle comme le reste. */
.boot-codex-flash {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    ellipse 130% 62% at 50% 50%,
    rgba(255, 255, 255, 0.94),
    rgba(246, 251, 255, 0.6) 58%,
    rgba(226, 240, 255, 0.28) 100%
  );
}

.boot-codex-panel.igniting .boot-codex-flash {
  animation: codex-ignite-flash var(--codex-ignite-dur) ease-out 1 both;
}

.boot-codex-panel.extinguishing .boot-codex-flash {
  animation: codex-extinguish-flash var(--codex-extinguish-dur) ease-in 1 both;
}

@keyframes codex-ignite-flash {
  0%   { opacity: 1; }
  10%  { opacity: 0.95; }
  26%  { opacity: 0.5; }
  44%  { opacity: 0.16; }
  62%  { opacity: 0.07; }
  100% { opacity: 0; }
}

@keyframes codex-extinguish-flash {
  0%   { opacity: 0; }
  30%  { opacity: 0.45; }
  62%  { opacity: 0.95; }
  100% { opacity: 0; }
}

/* Neige : à l'allumage le grain devient une averse, le temps que le signal
   se cale. Le grain garde sa propre animation — les deux ne touchent pas la
   même propriété, elles cohabitent. */
.boot-codex-panel.igniting .boot-codex-fx::before {
  animation:
    codex-grain calc(var(--crt-noise-speed, 0.9s) * 0.28) steps(1, end) infinite,
    codex-ignite-snow var(--codex-ignite-dur) ease-out 1 both;
}

@keyframes codex-ignite-snow {
  0%   { opacity: 0.6; }
  22%  { opacity: 0.42; }
  55%  { opacity: 0.16; }
  100% { opacity: var(--crt-noise, 0.06); }
}

/* Pendant l'allumage et l'extinction, le décrochage de synchro est
   suspendu : il se superposerait au geste et en brouillerait la lecture. */
.boot-codex-panel.igniting .boot-codex-surface,
.boot-codex-panel.extinguishing .boot-codex-surface {
  animation: none;
}

/* Mouvement réduit. Même convention que l'allumage du menu, plus haut :
   ce qui tourne en boucle s'arrête — c'est précisément ce que le réglage
   demande — et l'allumage se contente de raccourcir. Le geste reste lisible,
   il ne dure plus. */
@media (prefers-reduced-motion: reduce) {
  .boot-codex-surface,
  .boot-codex-surface::before,
  .boot-codex-fx::after,
  .boot-codex-roll,
  .boot-codex-panel.igniting .boot-codex-roll {
    animation: none !important;
  }
  .boot-codex-fx::before,
  .boot-codex-panel.igniting .boot-codex-fx::before {
    animation: none !important;
    opacity: var(--crt-noise, 0.06);
  }
  .boot-codex-panel.igniting .boot-codex-tube,
  .boot-codex-panel.igniting .boot-codex-flash,
  .boot-codex-panel.extinguishing .boot-codex-tube,
  .boot-codex-panel.extinguishing .boot-codex-flash {
    animation-duration: 160ms;
  }
}

/* =========================================================================
   OUTIL DE CALIBRAGE DU CODEX — TEMPORAIRE
   À supprimer avec calibration.js. Ces règles ne s'appliquent qu'aux
   éléments créés par l'outil, qui n'existent qu'avec ?calib=1.
   ========================================================================= */

#codexCalibLayer {
  position: absolute;
  inset: 0;
  z-index: 40;
  pointer-events: none;
}

#codexCalibOutline {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Grille de repérage, activée au bouton : tous les 5 %. */
#codexCalibLayer.show-grid::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, rgba(0,255,90,.22) 0 1px, transparent 1px 5%),
    repeating-linear-gradient(180deg, rgba(0,255,90,.22) 0 1px, transparent 1px 5%);
}

.cc-handle {
  position: absolute;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  padding: 0;
  z-index: 41;
  pointer-events: auto;
  cursor: grab;
  border: 2px solid #00ff5a;
  border-radius: 50%;
  background: rgba(0, 40, 15, 0.55);
  color: #00ff5a;
  font: 700 8px/1 "Courier New", monospace;
  touch-action: none;
}

.cc-handle::after {
  /* Croix centrale : la poignée doit désigner un point, pas une zone. */
  content: "";
  position: absolute;
  inset: 11px 0 11px 0;
  border-top: 1px solid #00ff5a;
}

.cc-handle.active {
  border-color: #ffe066;
  color: #ffe066;
  box-shadow: 0 0 12px rgba(255, 224, 102, 0.7);
}

.cc-handle:active { cursor: grabbing; }

#codexCalibration {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 20000;
  width: 286px;
  max-height: calc(100vh - 24px);
  display: flex;
  flex-direction: column;
  border: 1px solid #00ff5a;
  border-radius: 8px;
  background: rgba(3, 10, 6, 0.96);
  color: #b9ffd0;
  font: 11px/1.35 "Courier New", monospace;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.7);
}

#codexCalibration header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 9px;
  border-bottom: 1px solid rgba(0, 255, 90, 0.3);
  letter-spacing: 0.1em;
}

#codexCalibration header button {
  width: 22px;
  height: 20px;
  border: 1px solid rgba(0, 255, 90, 0.5);
  background: transparent;
  color: #00ff5a;
  cursor: pointer;
}

#codexCalibration .cc-body {
  overflow-y: auto;
  padding: 9px;
}

#codexCalibration.folded .cc-body { display: none; }

.cc-hint {
  margin: 0 0 8px;
  color: rgba(150, 230, 180, 0.75);
  font-size: 10px;
}

.cc-corner {
  display: grid;
  grid-template-columns: 1fr 62px 62px;
  align-items: center;
  gap: 4px;
  margin-bottom: 3px;
  padding: 2px 4px;
  border: 1px solid transparent;
  border-radius: 4px;
}

.cc-corner.active {
  border-color: #ffe066;
  background: rgba(255, 224, 102, 0.08);
}

.cc-corner input,
#codexCalibration .cc-out {
  border: 1px solid rgba(0, 255, 90, 0.4);
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.5);
  color: #d8ffe6;
  font: 11px "Courier New", monospace;
  padding: 2px 4px;
}

.cc-fields {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed rgba(0, 255, 90, 0.28);
  display: grid;
  gap: 4px;
}

.cc-field {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  font-size: 10px;
  color: rgba(160, 235, 190, 0.85);
}

.cc-field input[type="range"] { width: 100%; accent-color: #00ff5a; }
.cc-field output { color: #ffe066; font-size: 10px; }

.cc-actions {
  display: flex;
  gap: 5px;
  margin-top: 10px;
}

.cc-actions button {
  flex: 1;
  padding: 5px 4px;
  border: 1px solid rgba(0, 255, 90, 0.5);
  border-radius: 4px;
  background: rgba(0, 255, 90, 0.08);
  color: #b9ffd0;
  font: 10px "Courier New", monospace;
  cursor: pointer;
}

.cc-actions button:hover { background: rgba(0, 255, 90, 0.18); }

#codexCalibration .cc-out {
  width: 100%;
  margin-top: 8px;
  resize: vertical;
  font-size: 10px;
}

/* Titre de groupe dans l'outil de calibrage. */
#codexCalibration .cc-group {
  margin: 0 0 5px;
  font-size: 9px;
  letter-spacing: 0.14em;
  color: #ffe066;
}

#codexCalibration .cc-shape { border-top: none; margin-top: 8px; }
