/* =============================================================================
   global.css — tout ce qui est commun à toutes les pages du site.
   =============================================================================

   Ce qu'on trouve ici :
   - la remise à zéro du navigateur
   - la « scène » (le cadre de format fixe avec bandes noires)
   - le bandeau de message en bas d'écran
   - l'inventaire
   - le zoom
   - les fondus de transition entre pages

   Ce qui est propre à une page va dans css/index0.css, css/index1.css, etc.
============================================================================= */

/* ---------------------------------------------------------------------------
   VARIABLES — les valeurs réutilisées partout. Les changer ici les change
   dans tout le site.
--------------------------------------------------------------------------- */
:root {
  /* Le format natif de la scène. La chambre fait 1440x800, soit 1.8.
     Chaque page peut le redéfinir : le bureau d'Eon est en 1920/1080. */
  --scene-ratio: 1.8;

  /* La lueur des objets à ramasser au sol (la lampe torche).
     Rien à voir avec les zones cliquables, qui n'ont plus aucun effet visuel. */
  --glow-color: rgba(255, 244, 214, .85);

  /* --- LE BANDEAU DE MESSAGE EN BAS -------------------------------------
     Utilisé uniquement par la vue plein écran du poster. Les zones de la
     chambre, elles, ont leurs propres étiquettes (voir juste en dessous).
       --hint-text-shadow : l'ombre portée des LETTRES
       --hint-box-shadow  : l'ombre portée du RECTANGLE gris
     Syntaxe : décalage-x décalage-y flou couleur. "none" désactive. */
  --hint-text-shadow: 0 1px 1px rgba(0, 0, 0, .45);
  --hint-box-shadow: 0 3px 10px rgba(0, 0, 0, .55);

  /* --- LES ÉTIQUETTES DES ZONES -----------------------------------------
     Le petit texte qui apparaît sous une zone quand le faisceau l'éclaire.
     Le TEXTE lui-même est dans js/lang.js — ici, uniquement l'apparence.

       --label-color   : la couleur des lettres
       --label-opacity : l'opacité au repos. Volontairement basse : l'étiquette
                         doit se deviner, pas s'imposer.
       --label-glow    : le halo autour des lettres. Trois valeurs superposées
                         dans la règle plus bas, celle-ci en est la couleur.
       --label-fade-in : la durée du fondu d'ENTRÉE. Lente : l'étiquette se
                         révèle, elle ne s'allume pas.
       --label-fade-out: la durée du fondu de SORTIE. Plus courte : quand on
                         quitte une zone, l'étiquette doit s'effacer sans
                         traîner derrière le curseur.
       --label-rise    : de combien l'étiquette monte en apparaissant. C'est ce
                         petit décalage qui donne l'impression qu'elle se
                         révèle plutôt qu'elle ne clignote.
       --label-size    : la taille du texte.

     Ces deux durées sont AUSSI lues par js/zone-label.js, qui s'en sert pour
     enchaîner proprement le changement de texte d'une zone à l'autre. Tu les
     modifies ici, et tout suit — le CSS comme le JavaScript. */
  --label-color: #f2ead8;
  --label-opacity: 1;
  --label-glow: rgba(255, 158, 158, 0.918);
  --label-fade-in: 480ms;
  --label-fade-out: 260ms;
  --label-rise: 7px;
  --label-size: clamp(12px, .82vw, 13px);
  /* Sa distance aux bords gauche et bas de la scène. */
  --label-inset-x: 3.2%;
  --label-inset-y: 4.5%;

  /* Le rectangle gris derrière le texte. Il détache l'étiquette du décor quand
     celui-ci est clair — sur le bureau d'Eon, le texte tombait sur la cigarette
     et devenait illisible.
       --label-bg        : la couleur du rectangle. Le quatrième nombre est son
                           opacité : 0.56 = franchement lisible sur n'importe
                           quel fond, tout en laissant deviner la scène au
                           travers. Baisse-le si tu le trouves trop présent,
                           monte-le jusqu'à 1 pour un gris plein.
       --label-bg-blur   : le flou appliqué à ce qui est DERRIÈRE le rectangle.
                           Mets 0px si tu préfères un fond net.
       --label-pad-x / y : la marge entre le texte et le bord du rectangle
       --label-radius    : l'arrondi des coins
       --label-border    : le liseré clair du haut, très faible, qui évite que
                           le rectangle ait l'air posé au hasard */
  --label-bg: rgba(122, 122, 128, 0);
  --label-bg-blur: 3px;
  --label-pad-x: 1.1em;
  --label-pad-y: .70em;
  --label-radius: none;
  --label-border: 1px solid rgba(65, 4, 4, 0.496);

  /* --- LA LOUPE DES POP-UP (touche C, quand une pop-up est ouverte) -------
     Un CARRÉ. Les deux valeurs sont volontairement séparées plutôt que
     regroupées en une seule : tu gardes la possibilité d'en faire un
     rectangle en changeant l'une des deux. Tant qu'elles sont égales, la
     loupe est carrée.

     Elles sont toutes les deux en vw — en pourcentage de la LARGEUR de la
     fenêtre — et pas l'une en vw et l'autre en vh. C'est ce qui garantit que
     le carré reste un carré : avec vh, il s'aplatirait ou s'étirerait dès
     qu'on redimensionne la fenêtre.

       --glass-w / --glass-h : la taille du carré
       --glass-zoom          : le grossissement. Lu AUSSI par js/glass.js :
                               tu le changes ici, le JavaScript suit.
       --glass-radius        : l'arrondi des coins
       --glass-fade          : la durée de son apparition

     Le liseré et le halo sont ceux de l'étiquette (--label-border,
     --label-glow) : la loupe appartient à la même famille d'objets que les
     textes du jeu, elle ne doit pas avoir l'air d'un outil de navigateur. */
  --glass-w: 24vw;
  --glass-h: 24vw;
  --glass-zoom: 1.5;
  --glass-radius: 2px;
  --glass-fade: 140ms;

  /* --- LA CARTE DES COMMANDES ET SON « i » --------------------------------
       --keys-w        : la largeur de la carte, au plus
       --keys-voile    : le noir posé derrière elle
       --keys-fade     : la durée de son apparition
       --keys-size     : la taille de la lettre « i »
       --keys-inset-x  : sa distance au bord DROIT de la fenêtre
       --keys-inset-y  : sa distance au bord HAUT

     Le « i » reprend exactement l'habillage de .zone-label — même police, même
     crème, même halo, même liseré. Ses deux distances se comptent depuis le
     coin haut-droit de l'IMAGE, pas de la fenêtre : voir --bande-x plus bas. */
  --keys-w: min(78vw, 1100px);
  --keys-voile: rgba(0, 0, 0, .88);
  --keys-fade: 260ms;
  --keys-size: clamp(13px, 1vw, 16px);
  --keys-inset-x: 1.6vw;
  --keys-inset-y: 1.6vw;

  /* --- LA LARGEUR DES BANDES NOIRES ---------------------------------------
     La scène garde son format (--scene-ratio) quoi qu'il arrive : dès que la
     fenêtre n'a pas exactement ce format, il reste du noir sur deux côtés.

     Ces deux formules retrouvent l'épaisseur de ce noir, et c'est ce qui
     permet de poser quelque chose dans le coin de l'IMAGE plutôt que dans le
     coin de la fenêtre — pour le « i », qui ne doit jamais flotter sur une
     bande noire.

     Comment elles se lisent. La scène fait
         largeur = min(100vw, 100vh x ratio)
     donc le noir qui reste de chaque côté vaut la moitié de ce qui dépasse :
         (100vw - 100vh x ratio) / 2
     Ce nombre devient négatif quand c'est la largeur qui limite — il n'y a
     alors pas de bande verticale du tout. Le max(0px, …) le ramène à zéro.
     Même raisonnement à la verticale, en divisant par le ratio au lieu de
     multiplier.

     Une seule des deux est non nulle à la fois : un écran plus large que
     16/9 a des bandes à gauche et à droite, un écran plus haut en a en haut
     et en bas. Jamais les deux. */
  --bande-x: max(0px, (100vw - 100vh * var(--scene-ratio)) / 2);
  --bande-y: max(0px, (100vh - 100vw / var(--scene-ratio)) / 2);

  /* --- LA PORTE ----------------------------------------------------------
     Le battement quand on clique dessus : elle bute, elle ne s'ouvre pas.
       --door-rattle      : l'amplitude du battement, en pixels
       --door-rattle-time : sa durée */
  --door-rattle: 5px;
  --door-rattle-time: 340ms;

  /* --- LE GRAIN ANIMÉ ----------------------------------------------------
     LA SCÈNE EST TOTALEMENT IMMOBILE. Le grain est la seule chose qui vit à
     l'écran : l'image ne se déplace pas, ne tourne pas, ne zoome pas.

     Le grain défile par sauts à la cadence d'une pellicule. Il est posé sur
     l'écran, indépendamment de l'image, et calibré sur ta vidéo de référence.

       --grain-opacity : l'intensité. 0 le supprime complètement.
       --grain-speed   : la cadence. 0.32s = 25 images par seconde.
                         Plus court = grain plus nerveux. */
  --grain-opacity: 0.3;
  --grain-speed: 0s;

  /* La durée de référence des transitions douces. */
  --ease: 220ms cubic-bezier(.22, .61, .36, 1);
}

/* ---------------------------------------------------------------------------
   REMISE À ZÉRO
--------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;   /* les bordures comptent dans la largeur : plus simple */
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

body {
  background: #000;         /* le noir des bandes, et le fond de tout le site */
  color: #ddd;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  overflow: hidden;         /* aucune barre de défilement nulle part */

  /* La scène est centrée dans la fenêtre. */
  display: flex;
  align-items: center;
  justify-content: center;

  /* On empêche la sélection de texte : un clic-glissé sur une zone ne doit pas
     surligner quoi que ce soit, ça casserait l'illusion. */
  user-select: none;
  -webkit-user-select: none;
}

/* Le curseur par défaut du site. Les zones cliquables le remplacent. */
body {
  cursor: default;
}

/* ---------------------------------------------------------------------------
   LA SCÈNE
   Un cadre au format natif de l'image, aussi grand que possible, centré, avec
   des bandes noires si l'écran n'a pas le même format.

   La formule : on prend la plus petite des deux contraintes. Soit la largeur
   de la fenêtre limite, soit sa hauteur. Le résultat est toujours le format
   voulu, jamais déformé, jamais rogné.
--------------------------------------------------------------------------- */
.stage {
  position: relative;
  width: min(100vw, calc(100vh * var(--scene-ratio)));
  height: min(100vh, calc(100vw / var(--scene-ratio)));
  overflow: hidden;         /* rien ne dépasse quand une zone grossit ou qu'on zoome */
  background: #000;

  /* isolation: isolate crée un contexte de fusion fermé. Sans ça, les calques
     en Produit et en Incrustation déborderaient sur le fond noir de la page et
     le rendu serait faux. C'est la même précaution que dans ton test. */
  isolation: isolate;
}

/* Tous les calques de la scène sont superposés au même endroit. */
.stage__layer {
  position: absolute;
  inset: 0;                 /* raccourci pour top/right/bottom/left: 0 */
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;     /* les clics traversent : c'est le JS qui décide */
}

/* ---------------------------------------------------------------------------
   LES DEUX COUCHES DE LA SCÈNE

       .stage__zoom      le zoom de la touche C, et lui seul
       .stage__content   les calques de la scène

   LA SCÈNE NE BOUGE PAS. Aucune animation de déplacement, de rotation ou de
   zoom automatique n'existe dans ce fichier. L'image est strictement fixe tant
   que l'utilisateur n'appuie pas sur C.
--------------------------------------------------------------------------- */
.stage__zoom {
  position: absolute;
  inset: 0;
  transition: transform var(--ease);
  will-change: transform;   /* prévient le navigateur : il prépare la carte graphique */
}

.stage__content {
  position: absolute;
  inset: 0;
}

/* -----------------------------------------------------------------------------
   LE GRAIN ANIMÉ
   Posé par-dessus la scène et en dehors du conteneur de zoom : le grain est
   un effet de pellicule, il appartient à l'écran et non à l'image. Il ne
   grossit donc pas quand on zoome avec C.

   Le défilement se fait par sauts (steps) et non en continu : un grain de
   pellicule change d'un coup à chaque image, il ne glisse pas.

   Le mode Incrustation avec une image centrée sur le gris neutre ne change
   rien là où le grain vaut 128, et éclaircit ou assombrit ailleurs. C'est le
   comportement d'un vrai grain, qui respecte les valeurs de l'image.
----------------------------------------------------------------------------- */
.room-grain {
  position: absolute;
  inset: 0;
  z-index: 30;
  pointer-events: none;

  background-image: url("../assets-general/grain.png");
  background-repeat: repeat;
  mix-blend-mode: overlay;
  opacity: var(--grain-opacity);

  animation: grain-shift var(--grain-speed) steps(1, end) infinite;
  will-change: background-position;
}

/* Huit positions tirées au hasard, sans multiple commun : le motif ne se
   remet jamais en phase avec lui-même, donc on ne voit pas de répétition. */
@keyframes grain-shift {
  0%    { background-position:    0px    0px; }
  12.5% { background-position:  -47px   31px; }
  25%   { background-position:   83px  -62px; }
  37.5% { background-position: -119px  -17px; }
  50%   { background-position:   29px   97px; }
  62.5% { background-position:  -71px  -88px; }
  75%   { background-position:  111px   43px; }
  87.5% { background-position:  -23px -105px; }
}

/* ---------------------------------------------------------------------------
   LES SPRITES DE SURVOL
   Un morceau de la scène, découpé et reposé par-dessus, qui grossit et
   s'illumine quand on le survole.
--------------------------------------------------------------------------- */
/* SUPPRIMÉ : l'aurore et l'agrandissement des zones.
   Les sprites superposés passaient forcément au-dessus du faisceau de la lampe
   torche : dès que le curseur entrait dans une zone, le sprite recouvrait la
   lumière et la torche semblait ne plus fonctionner. Il n'y a plus aucun calque
   par-dessus la scène.
   Le survol d'une zone se signale uniquement par le bandeau de texte en bas et
   par le curseur qui devient une main. */

/* ---------------------------------------------------------------------------
   L'ÉTIQUETTE DES ZONES

   Une seule étiquette pour toute la page, ancrée dans le COIN BAS-GAUCHE de la
   scène. Son texte change selon la zone éclairée.

   Elle n'apparaît que lorsque le faisceau de la lampe éclaire une zone. Le
   curseur seul ne la révèle pas, et la zone reste cliquable.

   Le fondu porte sur l'opacité ET sur un très léger glissement vertical :
   l'étiquette monte de quelques pixels en apparaissant. C'est ce petit décalage
   qui donne l'impression qu'elle se révèle plutôt qu'elle ne s'allume.

   Position réglable par --label-inset-x et --label-inset-y, en haut du fichier.
--------------------------------------------------------------------------- */
.zone-label {
  position: absolute;
  left: var(--label-inset-x);
  bottom: var(--label-inset-y);

  /* z-index 200 : AU-DESSUS de tout ce qui vit dans la scène, y compris un
     objet du bureau survolé (qui monte à 100). Sans ça, la cigarette d'Eon —
     qui occupe précisément ce coin — passait par-dessus son propre texte.
     On reste très en dessous des pop-ups (7000) et du voile de fondu (9500). */
  z-index: 200;

  /* L'état AU REPOS : invisible, et légèrement plus bas que sa place réelle.
     C'est de là qu'elle part en apparaissant, et c'est là qu'elle retombe en
     disparaissant. */
  transform: translateY(var(--label-rise));

  /* Le rectangle gris. Réglages tout en haut du fichier.
     La marge négative annule exactement la marge intérieure : le TEXTE reste
     pile là où il était avant que le rectangle existe, et c'est le rectangle
     qui déborde vers l'extérieur. Tes réglages --label-inset-x / -y gardent
     donc le sens qu'ils avaient. */
  padding: var(--label-pad-y) var(--label-pad-x);
  margin: 0 0 calc(var(--label-pad-y) * -1) calc(var(--label-pad-x) * -1);
  background: var(--label-bg);
  border-radius: var(--label-radius);
  border: var(--label-border);
  backdrop-filter: blur(var(--label-bg-blur));
  -webkit-backdrop-filter: blur(var(--label-bg-blur));

  color: var(--label-color);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 300;
  font-size: var(--label-size);
  letter-spacing: .18em;
  white-space: nowrap;

  /* Le halo : trois ombres de plus en plus larges, sans décalage. Superposées,
     elles font une lueur qui s'étale doucement au lieu d'un contour net. */
  text-shadow:
    0 0 4px var(--label-glow),
    0 0 10px var(--label-glow),
    0 0 22px var(--label-glow);

  opacity: 0;
  pointer-events: none;

  /* LE FONDU DE SORTIE.
     Une transition écrite sur la règle de base est celle qui joue quand on
     REVIENT à cet état — donc quand l'étiquette s'efface. Courte et sèche :
     on a quitté la zone, l'étiquette ne doit pas traîner.
     La courbe (.4, 0, .7, .3) part vite et finit doucement. */
  transition:
    opacity   var(--label-fade-out) cubic-bezier(.4, 0, .7, .3),
    transform var(--label-fade-out) cubic-bezier(.4, 0, .7, .3);
}

/* LE FONDU D'ENTRÉE.
   Même logique à l'envers : la transition écrite ici est celle qui joue quand
   on ARRIVE dans cet état. Plus lente, et la courbe (.16, .8, .3, 1) démarre
   franchement puis se pose — l'étiquette monte et se stabilise.
   Le rectangle gris, le halo et le texte montent ensemble : ils sont sur le
   même élément, donc une seule opacité les porte tous les trois. */
.zone-label.is-visible {
  opacity: var(--label-opacity);
  transform: translateY(0);
  transition:
    opacity   var(--label-fade-in) cubic-bezier(.16, .8, .3, 1),
    transform var(--label-fade-in) cubic-bezier(.16, .8, .3, 1);
}

/* ---------------------------------------------------------------------------
   LA PORTE QUI REFUSE DE S'OUVRIR

   Un clic la fait battre une fois dans son cadre, sans s'ouvrir. Le mouvement
   part du gond (bord gauche) et ne dépasse jamais quelques pixels : c'est un
   battement sec, pas une oscillation.

   Volontairement très court — 340 ms — et déclenché uniquement par un clic de
   l'utilisateur. Rien ne bouge tant qu'on ne touche à rien.
--------------------------------------------------------------------------- */
.door-sprite {
  position: absolute;
  display: block;
  cursor: pointer;
  pointer-events: auto;
  transform-origin: 0% 50%;   /* le gond, sur le bord gauche */

  /* z-index 3 : AU-DESSUS des deux états de la scène (1 et 2), mais EN DESSOUS
     du faisceau de la lampe (5) et de son halo (6).
     C'est important : si la porte passait au-dessus du faisceau, elle resterait
     sombre quand on l'éclaire — la lumière passerait derrière elle.
     Les objets ramassables, eux, restent au-dessus (8), parce que l'image
     révélée par le faisceau ne les contient pas : ils y disparaîtraient. */
  z-index: 3;
}

.door-sprite.is-locked {
  animation: door-rattle var(--door-rattle-time) cubic-bezier(.36, .07, .19, .97);
}

@keyframes door-rattle {
  0%, 100% { transform: translateX(0)                    scale(1); }
  18%      { transform: translateX(var(--door-rattle))   scale(1.004); }
  42%      { transform: translateX(calc(var(--door-rattle) * -0.6)); }
  64%      { transform: translateX(calc(var(--door-rattle) * 0.45)); }
  84%      { transform: translateX(calc(var(--door-rattle) * -0.2)); }
}

/* ---------------------------------------------------------------------------
   LE BANDEAU DE MESSAGE, EN BAS AU MILIEU
   Ton cahier des charges : rectangle gris, contour noir très fin, angles vifs,
   police très fine noire.
--------------------------------------------------------------------------- */
.hint {
  /* fixed et non absolute : le bandeau doit aussi s'afficher par-dessus la vue
     plein écran du poster, qui est un calque séparé de la scène. */
  position: fixed;
  left: 50%;
  bottom: 5.5%;
  transform: translate(-50%, 6px);   /* légèrement bas : il remonte en apparaissant */

  padding: .55em 1.5em;
  background: #b9b9b9;
  border: 1px solid #000;
  border-radius: 0;                  /* angles vifs, explicitement */

  color: #000;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 200;                  /* la police la plus fine disponible */
  font-size: clamp(11px, 1.05vw, 15px);
  letter-spacing: .06em;
  white-space: nowrap;

  /* Les deux ombres, réglables en haut de ce fichier. */
  text-shadow: var(--hint-text-shadow);
  box-shadow: var(--hint-box-shadow);

  opacity: 0;
  pointer-events: none;
  transition: opacity var(--ease), transform var(--ease);
  /* Au-dessus de la vue du poster (6000), en dessous des pop-ups (7000). */
  z-index: 6500;
}

.hint.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---------------------------------------------------------------------------
   L'INVENTAIRE (touche E)
   Rien n'indique son existence nulle part : ce panneau n'apparaît que si
   l'utilisateur trouve la touche tout seul.
--------------------------------------------------------------------------- */
.inventory {
  position: fixed;
  inset: 0;
  z-index: 8000;

  display: flex;
  align-items: center;
  justify-content: center;

  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(5px);

  opacity: 0;
  visibility: hidden;                /* retire vraiment le panneau du parcours clavier */
  transition: opacity 180ms ease, visibility 180ms;
}

.inventory.is-open {
  opacity: 1;
  visibility: visible;
}

.inventory__panel {
  position: relative;
  width: min(74vw, calc(74vh * (1920 / 1080)));
  aspect-ratio: 1920 / 1080;
  transform: scale(.97);
  transition: transform 180ms cubic-bezier(.2, .8, .3, 1);
}

.inventory.is-open .inventory__panel {
  transform: scale(1);
}

.inventory__texture {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* La texture est volontairement pixelisée et sale : on demande au navigateur
     de ne surtout pas la lisser en l'agrandissant. */
  image-rendering: pixelated;
}

.inventory__slot {
  position: absolute;                /* placé en pourcentage depuis inventory.js */
  display: flex;
  align-items: center;
  justify-content: center;
  transition: box-shadow 140ms ease, background 140ms ease;
}

/* La case sélectionnée de la barre d'action : c'est l'objet qu'on a EN MAIN.
   Un liseré clair, sans fond, pour ne pas masquer la texture. */
.inventory__slot--hotbar.is-selected {
  box-shadow:
    inset 0 0 0 2px rgba(255, 248, 230, .95),
    0 0 10px rgba(255, 244, 214, .45);
}

/* La case survolée pendant un glisser-déposer. */
.inventory__slot.is-drop-target {
  background: rgba(255, 255, 255, .14);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5);
}

/* La case d'origine pendant qu'on déplace son contenu : on l'atténue plutôt
   que de la vider, pour qu'on voie d'où l'objet vient. */
.inventory__slot.is-dragging .inventory__icon {
  opacity: .28;
}

.inventory__icon {
  width: 78%;
  height: 78%;
  object-fit: contain;
  image-rendering: pixelated;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .6));
  pointer-events: none;              /* les clics vont à la case, pas à l'image */
}

/* L'objet qui suit le curseur pendant un déplacement. */
.inventory__drag {
  position: fixed;
  z-index: 9200;
  pointer-events: none;
  object-fit: contain;
  image-rendering: pixelated;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 4px 8px rgba(0, 0, 0, .7));
}

/* Le clone animé pendant le ramassage d'un objet. */
.collect-ghost {
  image-rendering: pixelated;
  filter: drop-shadow(0 0 10px var(--glow-color));
}

/* ---------------------------------------------------------------------------
   LES FONDUS DE TRANSITION
   Un voile noir plein écran, utilisé au chargement et au changement de page.

   DEUX DURÉES, ET C'EST VOULU
   Partir et arriver ne se ressemblent pas. Le départ doit être net : on a
   cliqué, quelque chose se passe. L'arrivée doit être lente : la pièce se
   révèle. D'où --fade-out-ms (on s'assombrit) et --fade-in-ms (on s'éclaircit).

   COMMENT LES DEUX DURÉES SONT CHOISIES
   En CSS, la transition utilisée est toujours celle de l'état d'ARRIVÉE.
     - on ajoute .is-black  → l'état d'arrivée est « .fade.is-black », donc
                              c'est --fade-out-ms qui s'applique : on part.
     - on retire .is-black  → l'état d'arrivée est « .fade » tout court, donc
                              c'est --fade-in-ms : on arrive.
   Pas de JavaScript pour arbitrer, le navigateur choisit tout seul.

   IMPORTANT — le voile est NOIR DÈS LA PREMIÈRE IMAGE.
   Chaque page écrit <div class="fade is-black"> directement dans son HTML.
   Avant, la classe était posée par le JavaScript, qui ne s'exécute qu'après
   l'analyse du document : pendant ce court instant la page était visible, et
   on apercevait une image de la fumée du cendrier avant que le noir tombe.
--------------------------------------------------------------------------- */
:root {
  --fade-out-ms: 600ms;   /* on quitte une page : le décor s'éteint lentement */
  --fade-in-ms: 600ms;   /* on arrive : plus lent encore, la pièce se révèle */
}

.fade {
  position: fixed;
  inset: 0;
  z-index: 9500;
  background: #000;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--fade-in-ms) cubic-bezier(.35, 0, .3, 1);
}

.fade.is-black {
  opacity: 1;
  transition: opacity var(--fade-out-ms) cubic-bezier(.4, 0, .7, .3);
}

/* ---------------------------------------------------------------------------
   ACCESSIBILITÉ
   Si l'utilisateur a demandé à son système de limiter les animations, on
   raccourcit les transitions. Le glitch de lumière, lui, reste : c'est le sujet
   de la page, pas une décoration.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
  }

  /* =========================================================================
     EXCEPTION : UN FONDU N'EST PAS DU MOUVEMENT.
     =========================================================================

     LE BUG QUE ÇA CORRIGE, ET IL ÉTAIT SÉRIEUX

     La règle ci-dessus vise le MOUVEMENT : une image qui glisse, qui tourne,
     qui tremble. C'est ce qui donne mal au coeur, et c'est ce que le réglage
     « Effets d'animation » de Windows demande de supprimer.

     Mais l'étoile `*` frappe TOUT, y compris les fondus en opacité — qui, eux,
     ne bougent pas d'un pixel. Conséquence : chez quelqu'un qui a coupé les
     animations dans Windows (Paramètres → Accessibilité → Effets visuels),
     le voile noir passait de transparent à opaque en UNE SEULE IMAGE. Plus
     aucun fondu entre la chambre et le bureau : l'écran devenait noir d'un
     coup, restait noir le temps prévu, puis la page suivante apparaissait
     d'un coup. Et comme le JavaScript, lui, attendait toujours la durée
     annoncée, régler --fade-out-ms ne changeait plus que la LONGUEUR du noir,
     jamais sa douceur.

     On rend donc leur durée aux fondus en opacité. Ils restent confortables :
     rien ne se déplace, c'est de la lumière qui monte et qui descend.
     Ce qui BOUGE, en revanche, reste supprimé — voir juste en dessous.
  ========================================================================= */
  .fade                  { transition-duration: var(--fade-in-ms)     !important; }
  .fade.is-black         { transition-duration: var(--fade-out-ms)    !important; }
  .zone-label            { transition-duration: var(--label-fade-out) !important; }
  .zone-label.is-visible { transition-duration: var(--label-fade-in)  !important; }

}

/* =============================================================================
   LA LOUPE DES POP-UP
   =============================================================================

   Le rectangle que la touche C fait apparaître quand une pop-up est ouverte.
   Toute la mécanique est dans js/glass.js ; ici il n'y a que son allure.

   Ce qu'il faut savoir en le lisant :

   - Il n'y a AUCUNE image dans cet élément. Son fond EST l'image de la pop-up,
     posée plus grande et décalée par le JavaScript. C'est pour ça qu'on ne
     trouve ici ni background-image ni background-position : ces deux-là sont
     écrits en direct, à chaque mouvement de souris.

   - left et top sont eux aussi écrits par le JavaScript : ce sont les
     coordonnées du curseur. Le translate(-50%, -50%) ci-dessous recentre le
     rectangle dessus, pour que le point visé soit au milieu de la loupe et non
     dans son coin.

   - pointer-events: none — la loupe est posée SOUS le curseur en permanence.
     Sans cette ligne, elle intercepterait les clics destinés aux flèches de la
     pop-up, qu'elle recouvre forcément par moments.

   z-index 7100 : au-dessus des pop-up (7000), en dessous de l'inventaire
   (8000). Ouvrir l'inventaire passe donc par-dessus la loupe, ce qui est la
   bonne priorité.
============================================================================= */
.glass {
  position: fixed;
  width: var(--glass-w);
  height: var(--glass-h);
  transform: translate(-50%, -50%);
  z-index: 7100;

  background-repeat: no-repeat;
  /* image-rendering: auto — on veut que le navigateur lisse l'agrandissement.
     À 1,5x, du pixel dur donnerait un rendu d'émulateur, pas de loupe. */
  image-rendering: auto;

  border: var(--label-border);
  border-radius: var(--glass-radius);

  /* Le halo : le même que celui du texte des étiquettes, mais porté par la
     boîte au lieu des lettres. Trois ombres de plus en plus larges, sans
     décalage, plus une ombre noire pour détacher la loupe du fond sombre. */
  box-shadow:
    0 0 4px var(--label-glow),
    0 0 12px var(--label-glow),
    0 0 26px var(--label-glow),
    0 10px 34px rgba(0, 0, 0, .7);

  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--glass-fade) ease, visibility var(--glass-fade);
}

.glass.is-on {
  opacity: 1;
  visibility: visible;
}

/* =============================================================================
   LA CARTE DES COMMANDES, ET LE « i » QUI LA RAPPELLE
   =============================================================================

   La carte s'affiche seule à l'arrivée dans la chambre, une fois par session,
   et le « i » en haut à droite la rappelle à la demande. Voir js/keys-card.js.

   z-index 9300 pour la carte : au-dessus de TOUT — pop-up, inventaire — parce
   qu'elle explique justement l'inventaire et les pop-up. Elle reste en dessous
   du voile noir des changements de page (9500), pour ne pas rester affichée
   par-dessus une transition.

   z-index 7300 pour le « i » : au-dessus des pop-up (7000) et de la loupe
   (7100), comme tu l'as demandé, mais en dessous de l'inventaire (8000) — on
   ne veut pas d'un bouton qui flotte par-dessus les cases quand on les manipule.
============================================================================= */
.keys-card {
  position: fixed;
  inset: 0;
  z-index: 9300;

  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--keys-voile);

  opacity: 0;
  visibility: hidden;
  /* cursor: pointer — un clic n'importe où la referme, autant que ça se voie. */
  cursor: pointer;
  transition: opacity var(--keys-fade) ease, visibility var(--keys-fade);
}

.keys-card.is-open {
  opacity: 1;
  visibility: visible;
}

.keys-card__image {
  max-width: var(--keys-w);
  max-height: 86vh;
  object-fit: contain;
  display: block;
  /* L'image est un fond noir avec du texte blanc : elle se fond dans le voile.
     Cette lueur très large lui redonne un bord, sans dessiner de cadre. */
  filter: drop-shadow(0 0 40px rgba(0, 0, 0, .9));
}

/* LE « i ».

   Tout ce bloc, à partir de la couleur, est repris à l'identique de
   .zone-label : même police à chasse fixe, même crème, même graisse, même
   interlettrage, même halo à trois ombres. C'est ce qui fait que le bouton
   appartient au décor plutôt qu'à l'interface du navigateur. */
.keys-info {
  position: fixed;

  /* LE COIN DE L'IMAGE, PAS LE COIN DE LA FENÊTRE.

     On ajoute l'épaisseur de la bande noire à la distance voulue : le « i »
     se pose donc juste à l'intérieur de l'image, quel que soit le format de
     l'écran, et ne flotte jamais sur le noir. Voir --bande-x en haut du
     fichier.

     Il reste en position: fixed et dans <body>, et non dans .stage, pour une
     raison précise : .stage porte « isolation: isolate », un contexte
     d'empilement fermé dont rien ne peut sortir. Un bouton placé dedans
     passerait sous les pop-up quel que soit son z-index. On le laisse donc
     dehors, et on va chercher le coin de l'image par le calcul. */
  top:   calc(var(--bande-y) + var(--keys-inset-y));
  right: calc(var(--bande-x) + var(--keys-inset-x));
  z-index: 7300;

  /* appearance: none efface le relief que le système d'exploitation dessine
     sur les boutons. Sans ça, on aurait un bouton gris de Windows au milieu de
     la chambre d'Eon. */
  appearance: none;
  background: var(--label-bg);
  border: var(--label-border);
  border-radius: var(--label-radius);
  backdrop-filter: blur(var(--label-bg-blur));
  -webkit-backdrop-filter: blur(var(--label-bg-blur));
  padding: var(--label-pad-y) calc(var(--label-pad-x) * 1.05);

  color: var(--label-color);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 300;
  font-size: var(--keys-size);
  line-height: 1;
  letter-spacing: .18em;
  /* L'interlettrage ajoute de l'espace APRÈS la lettre, jamais avant : un « i »
     seul se retrouverait collé au bord gauche de son rectangle. On rend cet
     espace à gauche pour qu'il soit réellement centré. */
  text-indent: .18em;

  text-shadow:
    0 0 4px var(--label-glow),
    0 0 10px var(--label-glow),
    0 0 22px var(--label-glow);

  cursor: pointer;
  user-select: none;
  opacity: .55;
  transition: opacity var(--label-fade-in) ease;
}

.keys-info:hover,
.keys-info:focus-visible {
  opacity: 1;
}

/* =============================================================================
   LE BLOCAGE MOBILE — la partie artistique se joue au clavier et à la souris.
   =============================================================================

   Sur index0, index1, eon-desktop, calling et virtual-desktop, tout repose sur
   des gestes qu'un téléphone n'a pas : le survol pour éclairer une zone, la
   molette pour régler le faisceau, le clic droit pour ramasser, les touches C,
   E, F et les chiffres. Il n'y a pas de version tactile de ça — ce serait un
   autre jeu à concevoir. On affiche donc ta carte, et rien d'autre.

   LA PARTIE COMMERCIALE N'EST PAS CONCERNÉE. nuances, polas et order
   s'ouvrent normalement sur un téléphone : elles ne portent pas ce blocage.

   COMMENT ON RECONNAÎT UN TÉLÉPHONE

   Pas par la largeur de la fenêtre : tu réduis la tienne en travaillant, et le
   site n'aurait aucune raison de te bloquer pour autant. Pas par le nom du
   navigateur non plus — ces listes sont fausses le jour où on les écrit.

   On interroge le MATÉRIEL, par les deux seules questions qui comptent ici :
     pointer: coarse  → le pointeur principal est imprécis (un doigt)
     hover: none      → il ne peut pas survoler sans appuyer

   Les deux ensemble, c'est un écran tactile sans souris : téléphone ou
   tablette. Un portable à écran tactile répond « fine » et « hover » grâce à
   son pavé tactile, et passe donc sans problème.

   POURQUOI IL N'Y A PAS UNE LIGNE DE JAVASCRIPT ICI

   Un module se charge après le HTML. Il y aurait eu un instant — court mais
   bien visible — où la chambre d'Eon serait apparue avant d'être recouverte.
   En CSS, le blocage s'applique au tout premier affichage, avant même que la
   première image ne soit peinte.

   À SAVOIR QUAND MÊME : cacher n'est pas empêcher de télécharger. Les images
   de la scène continuent d'être demandées derrière la carte. Si tu veux aussi
   épargner le forfait de tes visiteurs, c'est un garde à ajouter en tête de
   chaque fichier JS de page — dis-le moi, ce n'est pas une affaire de CSS.
============================================================================= */
.mobile-block { display: none; }

@media (pointer: coarse) and (hover: none) {

  /* TOUT LE RESTE DISPARAÎT.
     Pas seulement recouvert : retiré. Un calque posé par-dessus laisserait le
     décor vivre derrière — la lumière continuerait de grésiller, le grain de
     défiler — et le visiteur sentirait qu'il y a quelque chose sous la carte.
     Ici il n'y a plus rien d'autre.

     Le sélecteur porte sur les enfants DIRECTS de body, ce qui emporte aussi
     les éléments que le JavaScript y ajoute après coup : la loupe, le bouton
     « i », la carte des commandes. */
  body > *:not(.mobile-block) { display: none !important; }

  .mobile-block {
    position: fixed;
    inset: 0;
    z-index: 9900;

    display: flex;
    align-items: center;
    justify-content: center;

    background: #000;
  }

  /* contain et non cover : ta carte est une composition, elle ne doit jamais
     être rognée. Le noir qui reste autour est exactement celui de l'image, on
     ne voit donc aucun bord. */
  .mobile-block__image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    display: block;
  }
}
