/* Multiplicateur de durée pour toutes les transitions/animations CSS du jeu (glissé
   des habitants, disque de visibilité — non, celui-ci est un polygone Leaflet animé en
   JS, voir visibility.js —, fondus d'écran) — posé sur `<html>` par
   settings.js#applyAnimationSpeedToCSS, lu ici via `calc(<durée de base> *
   var(--anim-time-scale))`. `1` par défaut (vitesse normale) tant que ce script n'a
   pas encore tourné (premier paint), pour ne jamais laisser une règle sans valeur
   utilisable. Une **multiplication** par ce facteur, jamais une division par la
   vitesse choisie directement dans ces règles : une vitesse à 0 (animations
   désactivées) diviserait par zéro, alors que le facteur ici (l'inverse, déjà calculé
   côté JS) vaut simplement 0 dans ce cas — voir settings.js pour ce calcul.
   */
/* Palette (sombre — seul thème du projet, jeu et éditeur, depuis le retrait du thème
   clair). La plupart des panneaux/bulles du jeu partagent un petit nombre de teintes
   récurrentes (fond translucide, texte gris foncé/moyen/clair) : regroupées ici en
   variables plutôt que répétées en dur à chaque règle. Les couleurs propres au sens
   du jeu (bleu "vu"/liens, orange "armé", rouge danger, vert succès, etc.) restent
   des valeurs littérales dans leurs règles respectives. */
:root {
  --anim-time-scale: 1;
  color-scheme: dark; /* toute l'UI du jeu est un "moniteur" sombre — voir plus bas */

  /* --- Habillage "moniteur de vidéosurveillance" (voir js/surveillance-ui.js) ---
     Toute la chrome du jeu (fenêtres, frise, panneaux, bulles, boutons) est un
     panneau sombre à chasse fixe : c'est l'"appareil". */
  --surface-bg: rgba(16, 18, 21, 0.92);  /* icônes du haut, horloge, fenêtre de mission */
  --panel-bg: rgba(13, 15, 18, 0.97);    /* panneaux journal/rencontrés/options/crédits */
  --bubble-bg: #14171b;                   /* bulle de dialogue, bulle nom/métier, têtes de frise */
  --map-bg: #1a1c1e;                      /* fond de carte pendant le chargement des tuiles */
  --text-strong: #eef2f5;
  --text-base: #cdd3d9;
  --text-muted: #8b949e;
  --text-faint: #626b74;
  --border-color: rgba(255, 255, 255, 0.1);

  /* Rayon/police/lueur communs à toute la chrome. Coins peu arrondis (un
     appareil), rangées d'infos et libellés à chasse fixe. */
  --ui-radius: 4px;
  --ui-radius-lg: 6px;
  --ui-glow: 0 1px 6px rgba(0, 0, 0, 0.45);
  /* Bord haut des panneaux latéraux de droite (journal, rencontrés, options, …) —
     sous toute la barre du haut pour ne jamais couvrir l'horloge ni ses boutons
     (retouche demandée). Resserré sur mobile, où la barre du haut est plus haute
     mais la place verticale trop rare pour la gaspiller (voir @media). */
  --right-panel-top: 5.85rem;
  /* Bord bas des mêmes panneaux — s'arrêtent AU-DESSUS de la barre du bas
     (cible + frise) plutôt que de passer par-dessus (retouche demandée). Même
     hauteur réservée que l'attribution de la carte (voir .leaflet-bottom). */
  --right-panel-bottom: calc(5.5rem + env(safe-area-inset-bottom));
  /* Chrome du "moniteur" : chasse fixe partout (fenêtre de mission, frise,
     panneaux, bandeaux, boutons, bulle du Narrateur). `--font-speech` : la parole
     des habitants et les écrans de titre/narration restent en linéale lisible. */
  --font-readout: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
  --font-ui: var(--font-readout);
  --font-speech: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Couleurs des balises de dialogue par catégorie (voir dialogue-markup.js,
     `<pers>`/`<lieu>`/`<heure>`) — partagées pour que les mots-clés cliquables de
     l'énoncé de mission (`.mission-slot`) aient exactement la même couleur que le
     même mot dans une bulle. Calées pour rester lisibles sur la chrome sombre du
     "moniteur" (voir plus haut) comme sur une bulle. */
  --tag-personne: #5aa9e6;
  --tag-lieu: #dd9a4c;
  --tag-heure: #4cc0a6;

  /* Accents "sens du jeu", calés pour la chrome sombre. */
  --accent: #57a8ec;        /* liens, "déjà vu" */
  --accent-armed: #f0b23e;  /* mot-clé de mission armé */
  --ok: #5fc26f;            /* réussite */
  --danger: #ec6a5e;        /* erreur / blocage / danger */
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Entourage sombre du "moniteur" : ce qui se voit aux 4 coins arrondis de la
     zone image (voir #map `border-radius`). `#map` garde son propre `--map-bg`
     pour le flash de chargement des tuiles. */
  background: #06070a;
  /* Filet de sécurité (bug corrigé : plusieurs textes — dialogue, énoncé de mission,
     etc. — n'avaient jamais de `color` propre, donc restaient sur le noir par défaut
     du navigateur une fois leur fond assombri, illisibles). Une couleur de base
     héritée par tout le monde plutôt qu'un correctif au cas par cas sur chaque
     règle oubliée : les
     éléments qui déclarent déjà leur propre `color` (ex. `.mission-label`,
     `.habitant-dialogue .personne`) restent inchangés, plus spécifiques que cet
     héritage. */
  color: var(--text-base);
  /* Filet de sécurité plutôt qu'une nécessité identifiée : rien ne devrait dépasser
     du viewport une fois les ajustements mobiles ci-dessous en place, mais un
     défilement horizontal accidentel casserait complètement l'expérience sur
     téléphone (la carte glisserait avec la page plutôt que de rester fixe) — moins
     grave d'empêcher ce défilement dans l'absolu que de risquer ce résultat-là.
     Voir le bloc "Responsive / mobile" en fin de fichier pour les vraies causes
     traitées (plutôt que masquées) une par une. */
  overflow-x: hidden;
}

/* Même filet de sécurité que ci-dessus, pour les contrôles de formulaire (bouton,
   zone de texte...) : ils n'héritent PAS de la couleur de texte ni du fond de leur
   parent par défaut (styles natifs du navigateur, `buttontext`/`field` etc.), même
   maintenant que `body` déclare une couleur explicite — c'est ce qui laissait du
   texte noir sur fond sombre dans des éléments construits sur un `<button>` ou un
   `<textarea>` sans `color`/`background` propre (ex. `.encountered-item-main`,
   `.journal-textarea`). Posée tôt dans ce fichier : n'importe quelle règle plus
   spécifique plus bas (presque toutes les classes de boutons du jeu, voir
   `.mission-slot-armed` par ex.) l'emporte normalement dans la cascade. */
button, input, textarea, select {
  color: inherit;
  background-color: transparent;
}

/* Traitement commun de la chrome "moniteur" : un fin contour clair + une ombre
   portée, sur toutes les surfaces flottantes du jeu. Posé tôt : une règle plus
   spécifique plus bas (l'horloge se donne déjà son propre contour, par ex.)
   l'emporte. Les glyphes/contrôles internes gardent leurs propres styles. */
.mission-window,
.clock,
.top-bar-icon-strip,
.encountered-panel,
.journal-panel,
.achievements-panel,
.options-panel,
.credits-panel,
.feedback-panel,
.blocked-time-toast,
.achievement-toast,
.narrator-banner,
#ui-overlay > .bottom-bar {
  border: 1px solid var(--border-color);
  box-shadow: var(--ui-glow);
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: var(--map-bg); /* évite un flash blanc pendant le chargement des tuiles */
  /* Coins arrondis de la "zone image" — l'entourage sombre (body, #06070a)
     apparaît aux 4 coins. `overflow: hidden` : Leaflet pose les tuiles en
     `position: absolute` dedans, elles dépasseraient sinon la courbe. */
  border-radius: 10px;
  overflow: hidden;
}

/* Curseur en réticule sur la carte — « on manœuvre une caméra », pas « on fait
   glisser une carte » (voir l'ambiance surveillance). Leaflet pose `cursor: grab`
   / `grabbing` sur `.leaflet-grab` ; on l'écrase par un réticule, y compris
   pendant le glissé. Les contrôles d'UI (boutons, panneaux) gardent leur propre
   curseur, plus spécifiques. */
.leaflet-grab,
.leaflet-dragging .leaflet-grab {
  cursor: crosshair;
}
/* Marqueur d'habitant : réticule aussi (le crochet d'angle au survol, plus bas,
   signale qu'on peut le « verrouiller »). */
.habitant-icon {
  cursor: crosshair;
}

/* Couche d'UI par-dessus la carte (horloge, puis journal/carnet/mission aux étapes suivantes). */
#ui-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  pointer-events: none; /* laisse les clics traverser vers la carte, sauf sur les éléments d'UI eux-mêmes */
  z-index: 1000; /* au-dessus des panes Leaflet */
}

#ui-overlay > * {
  pointer-events: auto;
}

/* Bug corrigé (25/08/2026, passe mobile) : l'attribution Leaflet (obligatoire —
   licence OpenStreetMap/Protomaps) reste par défaut collée en bas à droite (`bottom:
   0; right: 0`), exactement là où `#ui-overlay > .bottom-bar` (cible + frise,
   z-index 1000, opaque) se pose en position fixe — elle se retrouvait donc
   systématiquement invisible derrière, sur toutes les tailles d'écran, pas
   seulement mobile (repéré en testant le mode paysage sur téléphone, où le fond
   rayé de la frise laissait deviner le texte masqué en transparence). Remontée
   d'une hauteur de barre basse (voir .bottom-bar plus bas, ~85px en pratique,
   marge incluse) pour rester lisible au-dessus — plus la même réserve pour la
   zone de geste Android (voir `env(safe-area-inset-bottom)` sur .bottom-bar),
   sans quoi l'attribution se retrouverait sous la marge de sécurité ajoutée là. */
.leaflet-bottom {
  margin-bottom: calc(5.5rem + env(safe-area-inset-bottom));
}

/* Attribution Protomaps/OpenStreetMap (obligatoire — licence, voir .leaflet-bottom).
   Par défaut Leaflet la pose sur un fond blanc translucide, qui tranche avec le
   moniteur sombre : on l'assortit au thème du jeu. Préfixée par `.leaflet-container`
   (bug corrigé le 2026-09-15) : leaflet.css pose sa propre règle sur
   `.leaflet-container .leaflet-control-attribution`, plus spécifique qu'un simple
   `.leaflet-control-attribution` — sans ce préfixe ici, l'ordre de chargement (cette
   feuille après leaflet.css) ne suffit pas, et le fond blanc d'origine l'emportait
   silencieusement. */
.leaflet-container .leaflet-control-attribution {
  background: rgba(8, 11, 16, 0.75);
  color: rgba(233, 242, 255, 0.6);
}
.leaflet-container .leaflet-control-attribution a {
  color: rgba(233, 242, 255, 0.85);
}

/* Rangée du haut : la fenêtre de mission à gauche (voir .mission-window plus bas),
   journal/rencontrés/options/horloge groupés à droite (`justify-content:
   space-between` — un seul groupe à droite désormais, les trois panneaux partagent le
   même emplacement, voir .journal-panel plus bas). */
.top-bar {
  display: flex;
  justify-content: space-between;
  /* `stretch` (desktop) : la fenêtre de mission, le bandeau d'icônes et l'horloge
     ont tous la MÊME HAUTEUR (retouche demandée) — une rangée de panneaux
     alignés haut et bas. La variante mobile (colonne) garde `stretch` aussi. */
  align-items: stretch;
  width: 100%;
  margin-top: 0.75rem;
  padding: 0 0.75rem;
  box-sizing: border-box;
}

.top-bar-right {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
}

/* Un seul bandeau sombre (façon console) derrière tous les boutons d'icône —
   voir main.js#iconStrip. Sur DESKTOP : deux rangées (groupe ponctuel au-dessus,
   groupe fréquent en dessous — retouche demandée), le bandeau prenant la même
   hauteur que l'horloge à côté (`align-items: stretch` du parent). Sur mobile :
   une seule rangée (voir @media), avec le séparateur entre les deux groupes. */
.top-bar-icon-strip {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 0.2rem;
  padding: 0.2rem 0.35rem;
  background: var(--surface-bg);
  border-radius: var(--ui-radius);
}

.top-bar-icon-group {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}

/* Séparateur entre les deux groupes — visible seulement quand ils sont côte à
   côte (mobile, une seule rangée). Les deux rangées desktop se séparent d'elles-
   mêmes. */
.top-bar-icon-divider {
  display: none;
  width: 1px;
  height: 1.5rem;
  margin: 0 0.25rem;
  background: var(--border-color);
  flex-shrink: 0;
}

.journal-toggle,
.encountered-toggle,
.achievements-toggle,
.options-toggle,
.feedback-toggle,
.mission-toggle,
.debug-follow-toggle,
.debug-showall-toggle {
  cursor: pointer;
  font-size: 1.15rem;
  line-height: 1;
  width: 2.15rem;
  height: 2.15rem;
  border: none;
  border-radius: var(--ui-radius);
  background: transparent;
  color: var(--text-muted);
}

.journal-toggle:hover,
.encountered-toggle:hover,
.achievements-toggle:hover,
.options-toggle:hover,
.feedback-toggle:hover,
.mission-toggle:hover,
.debug-follow-toggle:hover,
.debug-showall-toggle:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-strong);
}

/* Icônes propres au mode debug (`?debug`, voir map.js#DEBUG et main.js) — un
   simple rappel discret de couleur (au lieu de l'ancien bandeau "MODE DEBUG…"
   séparé, retiré) : parmi les autres icônes du groupe "ponctuel", jamais
   confondues avec elles pour autant. `.debug-showall-toggle-active` : posée en
   JS (main.js) tant que "afficher tout" est activé — même logique de bascule
   que `.editor-fullscreen-button-active` côté éditeur, teintée `--danger`
   plutôt que `--accent` pour rester dans le même rappel visuel "debug". */
.debug-follow-toggle,
.debug-showall-toggle {
  color: var(--danger);
}

.debug-showall-toggle-active {
  background: var(--danger);
  color: white;
}

.debug-showall-toggle-active:hover {
  background: var(--danger);
  color: white;
}

/* Bouton "afficher la mission" (voir main.js) — masqué par défaut : sur desktop la
   fenêtre de mission reste toujours affichée dans la barre du haut, ce bouton n'a de
   sens que sur mobile (voir @media plus bas où il redevient visible et où
   .mission-window se masque par défaut). */
.mission-toggle {
  display: none;
}

/* Horloge = panneau d'un magnétoscope de vidéosurveillance (voir js/ui.js,
   js/surveillance-ui.js) : bandeau `CAM NN · date · ● REC` puis rangée
   transport + horodatage. Fond sombre + police à chasse fixe quel que soit le
   thème (c'est un "appareil", pas une surface d'UI). */
.clock {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  width: fit-content;
  padding: 0.45rem 0.8rem;
  /* Sans ça, `flex: 1 0 100%` (voir la variante mobile plus bas, .top-bar-right
     .clock) ajoute son padding par-dessus les 100% de largeur du conteneur plutôt
     que dedans, débordant l'écran de la largeur de ce padding. */
  box-sizing: border-box;
  background: var(--surface-bg);
  border: 1px solid var(--border-color);
  border-radius: var(--ui-radius);
  color: var(--text-base);
  font-family: var(--font-readout);
  /* Contenu centré si le panneau est étiré plus haut que son contenu naturel
     (voir `.top-bar { align-items: stretch }`). */
  justify-content: center;
}

.clock-dvr-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-transform: uppercase;
}

.clock-dvr-cam { color: var(--text-base); }

/* Force du signal `●●●○○` — juste après l'étiquette caméra, discret. */
.clock-dvr-signal {
  color: var(--text-faint);
  letter-spacing: 0.05em;
  font-size: 0.9em;
}
.clock-dvr-signal:empty {
  display: none;
}

/* État de lecture (⏸ PAUSE / ▶ LECTURE) — poussé au centre du bandeau. */
.clock-dvr-mode {
  margin: 0 auto;
  color: var(--text-muted);
}
.clock-dvr-mode-playing {
  color: var(--ok);
}

.clock-dvr-rec {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  color: #ff6a5e;
}

/* Témoin d'enregistrement — pulse lent (via `--anim-time-scale` : 0 ⇒ figé). */
.clock-dvr-rec-dot {
  width: 0.55em;
  height: 0.55em;
  border-radius: 50%;
  background: #ff2d20;
  box-shadow: 0 0 5px rgba(255, 45, 32, 0.85);
  animation: dvr-rec-pulse calc(2.6s * var(--anim-time-scale)) ease-in-out infinite;
}

@keyframes dvr-rec-pulse {
  0%, 100% { opacity: 1; }
  55% { opacity: 0.28; }
}

.clock-dvr-body {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.clock-time {
  min-width: 9ch;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 1.55rem;
  letter-spacing: 0.05em;
  color: var(--text-strong);
}

/* Boîte de taille fixe (retouche demandée) — sans elle, «/»/▶ (ou ⏸ une fois la
   lecture lancée) n'ont pour largeur/hauteur que celles de leur propre glyphe +
   padding, qui diffèrent d'un caractère à l'autre : les trois boutons pouvaient
   alors avoir des empreintes de tailles différentes malgré le même `font-size`.
   `display: inline-flex` centre le glyphe dans cette boîte quel que soit son
   gabarit naturel. */
.clock-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.3rem;
  height: 2rem;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--ui-radius);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-base);
  font-size: 0.95rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}

.clock-nav:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.14);
  color: var(--text-strong);
}

.clock-nav:disabled {
  opacity: 0.28;
  cursor: default;
}

/* Les glyphes ⏮/⏭ rendent nettement plus petits que ▶/◀/⏸ à taille de police
   égale (retouche demandée) — on les agrandit sans toucher au gabarit du bouton
   (`display: inline-flex` recentre). */
.clock-nav-prev,
.clock-nav-next {
  font-size: 1.35rem;
}

/* Barre de progression dans la fenêtre du chapitre (voir js/ui.js#setProgress) —
   fine réglette + tête de lecture rouge, façon barre de défilement de
   magnétoscope. Transition douce calée sur le glissé de caméra (0,6 s). */
.clock-progress {
  position: relative;
  /* `width: 100%` explicite : sur mobile `.clock` passe en `align-items: center`
     (voir @media), sans quoi cette barre se réduirait à une largeur nulle centrée
     — la tête de lecture semblait alors figée au milieu (bug signalé). */
  width: 100%;
  height: 3px;
  margin-top: 0.15rem;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  overflow: visible;
}
.clock-progress-fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.34);
  transition: width calc(0.6s * var(--anim-time-scale)) cubic-bezier(0.5, 1, 0.89, 1);
}
.clock-progress-head {
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  width: 2px;
  margin-left: -1px;
  background: #ff3b30;
  box-shadow: 0 0 4px rgba(255, 59, 48, 0.8);
  transition: left calc(0.6s * var(--anim-time-scale)) cubic-bezier(0.5, 1, 0.89, 1);
}

/* ⏮ ⏭ ▶ ⏸ : glyphes de transport pleins (façon magnétoscope). Tous de même
   gabarit désormais — plus de réduction spécifique au bouton lecture comme quand
   ses voisins étaient les guillemets fins «/» (voir .clock-nav ci-dessus). */

/* Rangée du bas (Étape 9, retouche ultérieure : fondue en une seule barre) : barre de
   cible + frise côte à côte dans un seul bloc visuel (fond/ombre/coins portés par ce
   conteneur, pas par ses deux enfants séparément) plutôt que deux éléments qui
   flottent l'un à côté de l'autre — la frise démarre ainsi juste à droite de la bulle
   quelle que soit sa largeur (nom/profession de longueur variable), sans avoir à la
   mesurer en JS. Collée aux trois bords (bas, gauche, droite) : pas de `padding`, pas
   de marge — seuls les coins du haut restent arrondis, sans effet sur cet alignement
   puisqu'ils courbent vers l'intérieur à partir des bords, sans en décoller la barre.
   `position: fixed` : `#ui-overlay` n'a pas de hauteur propre (il s'ajuste à son
   contenu), un simple positionnement dans le flux ne l'amènerait pas en bas.
   `pointer-events: none` : rien n'est cliquable par défaut dans cette rangée, un
   marqueur de carte dessous resterait sinon incliquable (bug trouvé en testant une
   version antérieure) — seuls les segments de frise et les têtes le réactivent
   explicitement plus bas, hérité par leurs enfants. `#ui-overlay > .bottom-bar` (pas
   juste `.bottom-bar`) pour égaler la spécificité de `#ui-overlay > *` ci-dessus (un
   id bat une classe seule). `align-items: stretch` (plutôt que `flex-end` comme
   avant) : la barre de cible occupe désormais toute la hauteur de la frise, pour lire
   comme une seule pièce plutôt que deux éléments de hauteurs différentes posés côte à
   côte. `padding-bottom: env(safe-area-inset-bottom)` (nul sur la plupart des
   navigateurs, non nul en PWA/plein écran sur Android/iOS avec `viewport-fit=cover`,
   voir index.html) : réserve la hauteur de la zone de geste système (retour/app
   switcher) sous la cible et la frise plutôt que dessous — cible/frise gardent leur
   hauteur normale (le fond de la barre, lui, s'étend jusqu'au vrai bas d'écran grâce
   à ce padding plutôt qu'une marge, pas de bande transparente entre les deux). */
#ui-overlay > .bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: stretch;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--surface-bg);
  box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.2);
  border-radius: var(--ui-radius-lg) var(--ui-radius-lg) 0 0;
  /* Sans ça, le fond local de .target-bar-empty (voir plus bas) dépasserait en carré
     par-dessus le coin arrondi de la barre plutôt que de suivre sa courbe. */
  overflow: hidden;
  pointer-events: none;
  box-sizing: border-box;
}

.target-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: min(70vw, 320px);
  padding: 0.5rem 1rem;
  /* Trait fin plutôt qu'un fond/une ombre séparés (portés par .bottom-bar
     désormais) : sépare visuellement la cible de la frise sans rompre l'effet d'une
     seule barre continue. */
  border-right: 1px solid var(--border-color);
  font-family: var(--font-ui);
}

.target-bar-icon {
  font-size: 1.4rem;
  flex-shrink: 0;
}

/* Avatar généré ou portrait sprite (voir avatar.js#renderIcon) plutôt que l'emoji
   brut — `1em` plutôt qu'une taille fixe : suit `font-size` déjà posé sur
   .target-bar-icon ci-dessus, sans dupliquer la valeur. Même règle sur
   .crossed-face-avatar/.encountered-item-icon-avatar/.habitant-name-card-icon-avatar
   plus bas, chacune héritant du `font-size` de son propre contexte. Le sélecteur
   couvre à la fois `img` (avatar DiceBear) et `.habitant-icon-sprite-portrait`
   (sprite) : même boîte ronde, seul le contenu diffère — voir cette dernière classe
   plus bas pour le cadrage propre au portrait sprite. */
.target-bar-icon-avatar img,
.target-bar-icon-avatar .habitant-icon-sprite-portrait,
.crossed-face-avatar img,
.crossed-face-avatar .habitant-icon-sprite-portrait,
.encountered-item-icon-avatar img,
.encountered-item-icon-avatar .habitant-icon-sprite-portrait,
.habitant-name-card-icon-avatar img,
.habitant-name-card-icon-avatar .habitant-icon-sprite-portrait,
.narrator-banner-icon-avatar img,
.narrator-banner-icon-avatar .habitant-icon-sprite-portrait {
  width: 1em;
  height: 1em;
  border-radius: 50%;
  vertical-align: middle;
  /* `background-color`, jamais le raccourci `background` : ce dernier réinitialise
     implicitement tous les autres sous-réglages de fond (image/position/taille/
     répétition) à leur valeur initiale — sans effet pour l'avatar DiceBear (une
     balise <img>, aucun fond réellement affiché derrière), mais ça écrasait
     silencieusement le cadrage de .habitant-icon-sprite-portrait ci-dessous (bug
     corrigé le 25/08/2026 : cette règle groupée, plus spécifique via le sélecteur
     descendant à deux classes, l'emportait sur la règle dédiée malgré son ordre
     antérieur dans la feuille). */
  background-color: var(--surface-bg);
}

/* Cadrage du portrait recadré depuis le sprite `idle` (voir
   avatar.js#renderSpritePortrait) : la boîte ronde ci-dessus (taille/forme) est déjà
   posée par le sélecteur groupé — cette règle-ci ne s'occupe que du contenu.
   `background-image` posé en JS (URL dynamique par habitant). Recadrage en buste
   (tête + haut des épaules, pas le corps entier) par un simple zoom CSS : la
   spritesheet `idle` (voir sprite-engine.js#renderExportSheet côté générateur) a une
   frame de 64px de haut par direction sur 4 lignes (bas/gauche/droite/haut) — les
   8 premiers pixels de chaque frame sont une marge transparente (vérifié pixel par
   pixel sur plusieurs personnages, cohérent d'un habitant à l'autre puisque produit
   par le même export), pas la tête elle-même : sans en tenir compte, le portrait se
   retrouvait avec un vide en haut du cercle plutôt que rempli. On affiche donc une
   bande de 32px de la ligne "bas" démarrant 8px sous le haut de la frame (buste
   raisonnable, tête + haut des épaules), zoomée pour remplir toute la boîte : d'où
   un facteur ×(64/32) en largeur — 200 % (une seule colonne dans idle.png) — et
   ×(256/32) en hauteur — 800 % (÷32 sur les 4 lignes × 64px empilées). Le décalage
   vertical (`background-position-y`, formule standard CSS position/taille en
   pourcentage : décalage = position% × (taille affichée − boîte)) se déduit de ces
   8px sautés : 100 × 8 / (256 − 32) ≈ 3,57 %. */
.habitant-icon-sprite-portrait {
  display: inline-block;
  overflow: hidden;
  background-repeat: no-repeat;
  background-size: 200% 800%;
  background-position: 50% 3.57%;
  image-rendering: pixelated;
}

.target-bar-info {
  display: flex;
  flex-direction: column;
  min-width: 0; /* laisse text-overflow fonctionner dans un flex item */
}

/* Couleur personnelle (retouche demandée, voir js/habitant-color.js) — posée en
   `style` inline par carnet.js#createTargetBar, repli sur la couleur héritée
   (texte de base du bandeau) pour un habitant sans couleur choisie. */
.target-bar-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--pcolor-dark, inherit);
}

.target-bar-job {
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Aucune cible sélectionnée (voir carnet.js#showNone) — seul le tout début d'un
   chapitre sans ancrage de départ passe par cet état désormais (voir main.js), plus
   une simple absence de sélection qu'une anomalie : teinte neutre, pas d'alerte. Un
   fond local (la seule des deux moitiés de la barre à en avoir un — voir
   .target-bar, sinon transparent) plutôt qu'un fond posé sur toute .bottom-bar :
   seule la partie qui concerne la cible doit se teinter, pas la frise à côté. */
.target-bar-empty {
  background: var(--surface-bg);
}

.target-bar-empty .target-bar-name {
  color: var(--text-muted);
}

.target-bar-empty .target-bar-job {
  white-space: normal;
}

/* Message transitoire (voir carnet.js#showBlockedTimeToast) quand la navigation
   temporelle est refusée à cause de la cible suivie — même forme/position que
   .achievement-toast (achievements.js) pour rester dans le même langage visuel de
   bannière temporaire, teinte rouge sourd plutôt que la couleur neutre de succès :
   c'est un refus, pas une bonne nouvelle. */
.blocked-time-toast {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translate(-50%, -1rem);
  max-width: min(90vw, 420px);
  padding: 0.6rem 1.2rem;
  background: var(--surface-bg);
  border-radius: var(--ui-radius);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  font-family: var(--font-ui);
  font-weight: 600;
  color: var(--danger);
  text-align: center;
  opacity: 0;
  transition: opacity 0.3s ease, transform 0.3s ease;
  z-index: 2000;
  pointer-events: none;
}

.blocked-time-toast-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* Frise chronologique (Étape 9) — prend le reste de la largeur à droite de la barre
   de cible (voir .bottom-bar ci-dessus), trois rangées alignées colonne par colonne
   (une colonne par point de passage de l'arc) : têtes croisées / segments colorés /
   règle temporelle (voir carnet.js#createTimeline). Fond/ombre/coins arrondis portés
   par .bottom-bar (le parent commun avec .target-bar) plutôt que par ce bloc lui-même
   — les deux se lisent ainsi comme une seule barre plutôt que deux éléments distincts
   posés côte à côte. */
.timeline-block {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.3rem 0.5rem 0.4rem;
  font-family: var(--font-ui);
  box-sizing: border-box;
}

.timeline-faces-row,
.timeline-segments-row,
.timeline-ruler-row {
  display: flex;
  width: 100%;
}

.timeline-faces-row {
  align-items: flex-end;
  min-height: 1.6rem;
  margin-bottom: 0.2rem;
}

/* Têtes côte à côte (retouche demandée), le nombre réellement posé ici étant
   déjà borné par carnet.js#render (voir .timeline-face-column-badge plus bas :
   au-delà de ce qui tient sur une ligne, les dernières têtes sont retirées et
   remplacées par un badge "+N") — `flex-wrap` reste en garde-fou plutôt qu'une
   hauteur/`overflow: hidden` fixes (bug corrigé : ça coupait aussi la bulle
   nom/métier d'une tête normale, voir .habitant-name-card plus bas, qui déborde
   toujours de sa colonne au survol — plus d'infobulle du tout tant que ce
   `overflow: hidden` était là). */
.timeline-face-column {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-content: flex-end;
  gap: 0.15rem;
}

/* Badge "+N" (retouche demandée, voir carnet.js#render) — un habitant de plus
   dans une colonne trop chargée pour tenir sur une ligne. `position: relative`
   uniquement pour ancrer la liste au survol ci-dessous (voir
   .timeline-face-column-badge-list) — le badge lui-même ne bouge JAMAIS,
   contrairement à l'ancienne colonne repositionnée au survol (bug corrigé, voir
   commentaire sur .timeline-face-column ci-dessus) : aucun risque de
   clignotement, ce mécanisme est le même que .habitant-name-card plus bas,
   déjà éprouvé. */
.timeline-face-column-badge {
  position: relative;
  /* Comme `.crossed-face`/`.timeline-segment` : réactive les clics/le survol,
     sinon le `pointer-events: none` de `.bottom-bar` empêche le `:hover` qui
     révèle la liste des personnes du groupe (bug signalé). */
  pointer-events: auto;
  flex-shrink: 0;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background: #888;
  color: white;
  font-size: 0.6rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Badge "groupe" (👥, voir carnet.js#buildBadge) sur fond clair plutôt que le
   gris par défaut ci-dessus (retouche demandée : l'emoji, en couleurs propres,
   rendait mal/salement sur un fond gris uni). Même jeton que les têtes
   individuelles de la frise (`.crossed-face`, voir --bubble-bg) — blanc en
   thème clair, sombre en thème sombre, plutôt qu'un blanc fixe qui
   détonnerait la nuit — et même ombre portée qu'elles pour rester détouré sur
   un fond clair proche. */
.timeline-face-column-badge-group {
  background: var(--bubble-bg);
  color: var(--text-base);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

/* Fiches des habitants représentés par ce badge, révélées au survol — même
   formule de positionnement que .habitant-name-card (`top: 50%; left: 100%;
   transform: translateY(-50%)`), une carte par personne (icône + prénom +
   métier/âge, voir .timeline-face-column-badge-entry ci-dessous et
   avatar.js#createNameCardContent) plutôt que la liste de noms en texte brut
   d'avant cette retouche — même contenu que .habitant-name-card, pour rester
   cohérent avec l'infobulle d'une tête affichée seule. */
.timeline-face-column-badge-list {
  position: absolute;
  /* `top: 0`, jamais centrée verticalement sur le badge (retouche demandée,
     bug signalé : coupée en haut du volet) — la rangée de têtes croisées est
     la toute première rangée de la frise (`.timeline-faces-row`), elle-même
     dans un panneau à défilement propre (`overflow-y: auto` côté éditeur,
     `overflow: hidden` côté jeu — coins arrondis, voir #ui-overlay >
     .bottom-bar) : centrée, la moitié haute de cette liste (plusieurs fiches
     empilées, potentiellement assez haute) dépassait le bord haut de ce
     panneau et s'y faisait rogner. Alignée sur le haut du badge, elle ne
     s'étend plus que vers le bas, dans l'espace des rangées suivantes
     (segments/règle), toujours à l'intérieur du même panneau. */
  top: 0;
  left: 100%;
  margin-left: 0.4rem;
  /* Nettement au-dessus de tout le reste de l'interface (retouche demandée)
     — même valeur que .title-screen, la plus haute déjà en usage dans cette
     feuille de style. L'ancienne valeur (20) suffisait dans son propre
     contexte d'empilement mais laissait planer un risque de passage derrière
     un panneau tiers à z-index plus élevé selon le contexte (jeu ou éditeur,
     mode ouvert...). */
  z-index: 2000;
  background: var(--surface-bg);
  /* Sans ça, le texte hérite du `color: white` de .timeline-face-column-badge
     (le rond "+N" lui-même, pensé pour trancher sur son propre fond gris) — sur
     le fond clair de --surface-bg, ça donnait une bulle vide en apparence (texte
     blanc sur fond quasi blanc), bug signalé à l'usage. */
  color: var(--text-base);
  border-radius: var(--ui-radius);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  padding: 0.4rem 0.55rem;
  font-family: var(--font-ui);
  font-size: 0.7rem;
  white-space: nowrap;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease-out;
}

.timeline-face-column-badge:hover .timeline-face-column-badge-list {
  opacity: 1;
}

/* Une fiche par personne dans la liste ci-dessus — mêmes classes que
   .habitant-name-card (icône + prénom/métier-âge), juste sans son
   `position: absolute` : ici c'est .timeline-face-column-badge-list, en
   colonne (`gap` ci-dessus), qui empile les fiches les unes sous les autres. */
.timeline-face-column-badge-entry {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.crossed-face {
  position: relative; /* ancre .habitant-name-card, voir plus bas */
  pointer-events: auto; /* réactive les clics, voir .timeline-block plus haut */
  border: none;
  cursor: pointer;
  font-size: 1rem;
  width: 1.6rem;
  height: 1.6rem;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--bubble-bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  /* Centre aussi bien l'emoji que l'avatar généré (voir .crossed-face-avatar
     ci-dessus) — un `<button>` centre son texte par défaut, mais pas fiablement une
     image qu'il contiendrait. */
  display: flex;
  align-items: center;
  justify-content: center;
}

.timeline-segments-row {
  height: 1.1rem;
}

.timeline-segment {
  pointer-events: auto; /* réactive les clics, voir .timeline-block plus haut */
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  border: none;
  border-right: 1px solid rgba(0, 0, 0, 0.4);
  padding: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.13);
}

/* Instant inconnu (voir carnet.js#createTimeline) : `disabled` natif, jamais de saut
   à l'aveugle vers une position qu'on ne connaît pas. */
.timeline-segment:disabled {
  cursor: default;
}

.timeline-segment:last-child {
  border-right: none;
}

.timeline-segment-seen {
  background: var(--accent);
}

/* Point de passage courant au moment où avancer/reculer vient d'être refusé à cause
   de la cible suivie (voir main.js#setNavigationGuards, carnet.js#createTimeline) —
   couleur dédiée (ni le gris "jamais vu", ni le bleu "connu") pour repérer d'un coup
   d'œil où la piste s'arrête. Reste affiché tant que l'ancrage/l'heure ne changent
   pas, pas seulement le temps du message. */
.timeline-segment-blocked {
  background: var(--accent-armed);
}

/* Point de passage courant = tête de lecture d'un magnétoscope : un fin trait
   rouge vertical sur toute la hauteur de la frise + un petit triangle qui pointe
   dessus. Le segment lui-même garde un discret liseré clair. */
.timeline-segment-current {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}
.timeline-segment-current::after {
  content: "";
  position: absolute;
  left: 50%;
  top: -2px;
  bottom: -2px;
  width: 2px;
  transform: translateX(-50%);
  background: #ff3b30;
  box-shadow: 0 0 4px rgba(255, 59, 48, 0.85);
  pointer-events: none;
}
.timeline-segment-current::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -5px;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: #ff3b30;
  pointer-events: none;
}

/* Règle temporelle sous la frise (Étape 9, retouche demandée après coup) : un trait
   à chaque point de passage (1/4 h), une étiquette "07h00" etc. uniquement sur les
   heures pleines — les 13 points de passage de l'Arc 0 tombent tous sur un multiple
   de 15 min, seuls 4 d'entre eux sont aussi des heures rondes. */
.timeline-ruler-row {
  height: 1.1rem;
}

.timeline-tick {
  flex: 1 1 0;
  min-width: 0;
  border-left: 1px solid var(--text-faint);
  position: relative;
}

.timeline-tick-hour {
  border-left-color: var(--text-base);
}

.timeline-tick-label {
  position: absolute;
  top: 0.1rem;
  left: 0.15rem;
  font-size: 0.65rem;
  color: var(--text-base);
  white-space: nowrap;
}

/* Éclaircissement hors chapitre (retouche demandée, éditeur uniquement — voir
   js/carnet.js#setChapterWindow/setDimmed, editor/js/scenes-panel.js) — purement
   visuel, un créneau éclairci reste cliquable comme les autres. Le jeu réel ne
   pose jamais cette classe (pas de notion de chapitre affiché sur sa propre
   frise), sans effet dans ce cas. */
.timeline-dimmed {
  opacity: 0.3;
}

/* Panneau des habitants rencontrés (Étape 9, retouche 5) — panneau plein à droite,
   déjà un enfant direct de #ui-overlay donc `pointer-events: auto` par défaut (voir
   règle générale en haut du fichier) : contrairement à .bottom-bar/.top-bar, tout ici
   est interactif, pas besoin de le redéfinir finement. */
.encountered-panel {
  position: fixed;
  /* Sous la rangée d'icônes (voir .top-bar), pas à sa hauteur : les trois panneaux de
     droite (celui-ci, .journal-panel, .options-panel) partagent cet emplacement et
     doivent laisser leurs propres icônes de bascule visibles et cliquables au-dessus
     d'eux, plutôt que de les couvrir. */
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — voir le même commentaire sur .options-panel
     pour le pourquoi (bug corrigé, panneau à `auto` recouvert par le bandeau). */
  z-index: 1600;
}

/* `[hidden]` seul (spécificité égale à `.encountered-panel` ci-dessus) perdrait
   contre lui dans la cascade puisque `display: flex` est posé par une feuille de
   style auteur, qui l'emporte toujours sur le `display: none` par défaut du
   navigateur pour `[hidden]` — sans cette règle le panneau resterait visible (et
   cliquable par-dessus le reste de l'UI) même masqué via `panel.hidden = true`
   (bug trouvé en testant : la carte devenait incliquable sous le panneau "caché"). */
.encountered-panel[hidden] {
  display: none;
}

/* Panneau du journal — même principe que .encountered-panel, même côté (droite) : les
   trois panneaux (journal, rencontrés, options) partagent désormais cet emplacement,
   un seul ouvert à la fois (voir main.js#closeOtherPanels). `[hidden]` explicite tout
   de suite (voir plus haut pour le pourquoi), pas la peine de redécouvrir le même bug
   une troisième fois. */
.journal-panel {
  position: fixed;
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — un panneau ouvert par-dessus doit rester
     lisible/cliquable, pas recouvert par un bandeau qui continue de s'afficher
     tant que le Narrateur reste "sur scène" (voir narrator-banner.js). Bug
     corrigé : sans z-index explicite ici, ce panneau restait à `auto`, qu'un
     élément à z-index positif comme le bandeau passe devant dans le même
     contexte d'empilement, quel que soit l'ordre dans le DOM. */
  z-index: 1600;
}

.journal-panel[hidden] {
  display: none;
}

.journal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.journal-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

.journal-textarea {
  flex: 1 1 auto;
  min-height: 0;
  border: none;
  resize: none;
  padding: 1rem;
  font-size: 0.9rem;
  font-family: inherit;
  box-sizing: border-box;
}

.journal-textarea:focus {
  outline: none;
}

/* Panneau des succès — même principe que .journal-panel/.encountered-panel, même
   emplacement (droite), fait maintenant partie de la rotation "un seul ouvert à la
   fois" (voir main.js#closeOtherPanels). */
.achievements-panel {
  position: fixed;
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — un panneau ouvert par-dessus doit rester
     lisible/cliquable, pas recouvert par un bandeau qui continue de s'afficher
     tant que le Narrateur reste "sur scène" (voir narrator-banner.js). Bug
     corrigé : sans z-index explicite ici, ce panneau restait à `auto`, qu'un
     élément à z-index positif comme le bandeau passe devant dans le même
     contexte d'empilement, quel que soit l'ordre dans le DOM. */
  z-index: 1600;
}

.achievements-panel[hidden] {
  display: none;
}

.achievements-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.achievements-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

.achievements-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0.5rem 1rem;
}

.achievements-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border-color);
}

.achievements-item-icon {
  font-size: 1.4rem;
  line-height: 1;
}

/* Grisé/estompé tant que le succès n'a pas été obtenu (demande d'origine) — voir
   achievements.js#createAchievementsPanel, `render(earnedIds)`. */
.achievements-item-locked {
  opacity: 0.35;
  filter: grayscale(1);
}

/* Bannière d'obtention d'un succès, centrée en haut de l'écran (demande d'origine) —
   voir achievements.js#showAchievementToast. Fondu piloté par une classe ajoutée après
   coup (`-visible`) plutôt que directement à la création : sans ce décalage (double
   `setTimeout`, voir achievements.js), la transition ne jouerait pas, l'élément
   apparaissant déjà dans son état final dès le premier paint. */
.achievement-toast {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translate(-50%, -1rem);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 1.2rem;
  background: var(--surface-bg);
  border-radius: var(--ui-radius);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  font-family: var(--font-ui);
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.3s ease, transform 0.3s ease;
  z-index: 2000;
  pointer-events: none;
}

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

.achievement-toast-icon {
  font-size: 1.3rem;
  line-height: 1;
}

/* Bandeau du Narrateur (retouche demandée, voir js/narrator-banner.js) — même
   langage visuel que .achievement-toast ci-dessus (pilule, icône + texte), mais
   posé juste au-dessus de la barre du bas (retouche demandée : "en bas" plutôt
   qu'en haut, voir #ui-overlay > .bottom-bar plus haut pour les ~5,5rem de hauteur
   réservés + la zone de geste système) plutôt qu'à hauteur fixe depuis le haut —
   là où se déroule l'action (carte, cible suivie), pas à côté de la mission/des
   icônes. Persistant tant que la condition reste vraie (voir
   main.js#onStageNarrators), contrairement au toast de succès qui s'efface tout
   seul après quelques secondes. `pointer-events: none` : ne doit jamais intercepter
   un clic destiné à la carte en dessous. */
.narrator-banner-stack {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(5.5rem + env(safe-area-inset-bottom) + 0.6rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.75rem;
  pointer-events: none;
  z-index: 1500;
}

.narrator-banner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: min(90vw, 420px);
  padding: 0.6rem 1.1rem;
  background: var(--surface-bg);
  border-radius: var(--ui-radius);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  font-family: var(--font-ui);
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 0.3s ease, transform 0.3s ease;
  pointer-events: none;
}

.narrator-banner-visible {
  opacity: 1;
  transform: translateY(0);
}

.narrator-banner-icon {
  font-size: 1.3rem;
  line-height: 1;
  flex-shrink: 0;
}

.narrator-banner-text {
  font-size: 0.85rem;
  line-height: 1.5;
}

/* Mise en exergue d'un bouton de l'interface depuis un dialogue du Narrateur
   (retouche demandée, tutoriel — voir `<hl>` dans dialogue-markup.js et
   js/narrator-banner.js). `outline` plutôt que `box-shadow` : ne remplace pas
   l'ombre propre du bouton visé (`.journal-toggle`/`.clock-nav`/... en ont déjà
   une) — les deux se superposent sans conflit. `outline-offset` laisse un peu d'air
   entre le bouton et son liseret plutôt qu'un trait collé au bord. Vitesse fixe,
   volontairement pas indexée sur `--anim-time-scale` (réglage "vitesse des
   animations", voir settings.js) : un réglage à 0 figerait le clignotement sur son
   image de fin (transparent), rendant le repère invisible pile quand il doit rester
   le plus lisible. */
.narrator-highlight {
  outline: 3px solid transparent;
  outline-offset: 2px;
  border-radius: inherit;
  animation: narrator-highlight-blink 1s ease-in-out infinite;
}

@keyframes narrator-highlight-blink {
  0%, 100% { outline-color: transparent; }
  50% { outline-color: var(--accent-armed); }
}

/* Panneau d'options — même principe que .journal-panel/.encountered-panel, même
   emplacement (droite), un seul des trois ouvert à la fois. */
.options-panel {
  position: fixed;
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — un panneau ouvert par-dessus doit rester
     lisible/cliquable, pas recouvert par un bandeau qui continue de s'afficher
     tant que le Narrateur reste "sur scène" (voir narrator-banner.js). Bug
     corrigé : sans z-index explicite ici, ce panneau restait à `auto`, qu'un
     élément à z-index positif comme le bandeau passe devant dans le même
     contexte d'empilement, quel que soit l'ordre dans le DOM. */
  z-index: 1600;
}

/* Même piège que .encountered-panel[hidden] plus haut dans ce fichier — voir son
   commentaire pour le pourquoi. */
.options-panel[hidden] {
  display: none;
}

.options-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.options-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

/* Corps défilant du panneau (comme .encountered-list/.credits-body/etc.) : le
   panneau est un flex column à hauteur bornée (top/bottom fixes), l'en-tête ne
   bouge pas, ce bloc prend le reste et défile si le contenu déborde. `min-height: 0`
   est indispensable — sans lui un item flex refuse de passer sous la taille de son
   contenu et le `overflow-y` ne s'active jamais (cas signalé : menu Options plus
   haut que l'écran, bas inaccessible). */
.options-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
}

/* Section "Affichage" (plein écran, rayon de visibilité, vitesse des animations —
   voir options.js/settings.js) — même style de titre que .options-credits-title plus
   bas, réutilisé tel quel plutôt qu'une nouvelle règle redondante. */
.options-section-title {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-base);
}

.options-fullscreen-button {
  width: 100%;
  border: 1px solid var(--text-faint);
  background: transparent;
  color: var(--text-base);
  border-radius: var(--ui-radius);
  padding: 0.6rem 0.8rem;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 0.6rem;
}

/* Mode sombre — réglage binaire, une case à cocher plutôt qu'un curseur (voir
   createSliderRow dans options.js, réservé aux réglages à plusieurs paliers). */
.options-checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0;
  font-size: 0.85rem;
  color: var(--text-base);
  cursor: pointer;
}

.options-checkbox-row input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--accent);
  cursor: pointer;
}

.options-setting-row {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.5rem 0;
  font-size: 0.85rem;
  color: var(--text-base);
}

/* Même piège que .encountered-panel[hidden] plus haut : `display: flex`
   ci-dessus l'emporterait sur le `display: none` par défaut de `[hidden]` (à
   spécificité égale, la règle auteur gagne) — sans ça, une ligne de réglage
   masquée en JS (`row.hidden = true`, ex. le rayon de visibilité hors debug)
   resterait visible. */
.options-setting-row[hidden],
.options-checkbox-row[hidden] {
  display: none;
}

.options-setting-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
}

.options-setting-value {
  font-weight: 600;
  color: var(--accent);
}

/* Curseur pleine largeur plutôt que la taille par défaut du navigateur (souvent
   étroite) — `accent-color` recolore la piste remplie/le pouce sans avoir à
   redéfinir les pseudo-éléments spécifiques à chaque moteur (`::-webkit-slider-*`,
   `::-moz-range-*`), cohérent avec le bleu déjà utilisé pour .options-setting-value
   et les liens de crédits. */
.options-setting-slider {
  width: 100%;
  accent-color: var(--accent);
}

.options-reset-button {
  width: 100%;
  border: 1px solid var(--danger);
  background: transparent;
  color: var(--danger);
  border-radius: var(--ui-radius);
  padding: 0.6rem 0.8rem;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
  margin-bottom: 0.5rem;
}

.options-reset-button:last-of-type {
  margin-bottom: 0;
}

/* Bloc de confirmation générique (retouche demandée, voir options.js#createConfirmBlock)
   — partagé par la réinitialisation et l'import de sauvegarde, deux actions
   destructrices pour la progression en cours. Anciennement `.options-reset-confirm*`,
   renommé pour refléter cet usage partagé plutôt que de dupliquer des règles
   quasi identiques pour un second bloc. */
.options-confirm {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin-bottom: 0.5rem;
}

/* Même piège que .encountered-panel[hidden] plus haut dans ce fichier : `display:
   flex` ci-dessus l'emporterait sur le `display: none` par défaut de `[hidden]`. */
.options-confirm[hidden] {
  display: none;
}

.options-confirm-text {
  margin: 0;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.options-confirm-actions {
  display: flex;
  gap: 0.5rem;
}

.options-confirm-yes,
.options-confirm-no {
  flex: 1 1 0;
  border-radius: var(--ui-radius);
  padding: 0.5rem 0.6rem;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
}

.options-confirm-yes {
  border: 1px solid var(--danger);
  background: var(--danger);
  color: white;
}

.options-confirm-no {
  border: 1px solid var(--text-faint);
  background: transparent;
  color: var(--text-base);
}

/* Section "Chapitre" (retouche demandée, voir options.js) — un bouton par chapitre
   atteint, même langage visuel neutre que .options-fullscreen-button. */
.options-chapter-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

.options-chapter-status {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.options-chapter-button {
  width: 100%;
  border: 1px solid var(--text-faint);
  background: transparent;
  color: var(--text-base);
  border-radius: var(--ui-radius);
  padding: 0.5rem 0.7rem;
  font-size: 0.8rem;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

/* Chapitre en cours — affiché mais désactivé (voir options.js, pas la peine de
   "sélectionner" celui déjà actif) : bordure/texte pleins plutôt qu'en pointillés,
   pour distinguer d'un simple survol au clavier. */
.options-chapter-button-current {
  border-color: var(--accent);
  color: var(--accent);
  cursor: default;
}

.options-import-error {
  margin: -0.2rem 0 0.5rem;
  font-size: 0.8rem;
  color: var(--danger);
}

.options-import-error[hidden] {
  display: none;
}

.options-divider {
  margin: 1.1rem 0;
  border: none;
  border-top: 1px solid var(--border-color);
}

.options-credits-button {
  width: 100%;
  border: 1px solid var(--text-faint);
  background: transparent;
  color: var(--text-base);
  border-radius: var(--ui-radius);
  padding: 0.6rem 0.8rem;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
}

.options-version {
  margin-top: 0.9rem;
  font-size: 0.7rem;
  color: var(--text-faint);
  text-align: right;
}

/* Panneau des crédits (voir credits.js) — même principe que
   .journal-panel/.encountered-panel/.options-panel, même emplacement (droite), un
   seul des quatre ouvert à la fois (voir main.js#closeOtherPanels). Pas d'icône
   dédiée dans .top-bar : ouvert depuis un bouton du panneau Options
   (.options-credits-button), qui se ferme lui-même avant. */
.credits-panel {
  position: fixed;
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — un panneau ouvert par-dessus doit rester
     lisible/cliquable, pas recouvert par un bandeau qui continue de s'afficher
     tant que le Narrateur reste "sur scène" (voir narrator-banner.js). Bug
     corrigé : sans z-index explicite ici, ce panneau restait à `auto`, qu'un
     élément à z-index positif comme le bandeau passe devant dans le même
     contexte d'empilement, quel que soit l'ordre dans le DOM. */
  z-index: 1600;
}

/* Même piège que .encountered-panel[hidden] plus haut dans ce fichier — voir son
   commentaire pour le pourquoi. */
.credits-panel[hidden] {
  display: none;
}

.credits-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.credits-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

.credits-body {
  /* Voir .options-body pour le pourquoi de `flex`/`min-height: 0`. */
  flex: 1 1 auto;
  min-height: 0;
  padding: 1rem;
  overflow-y: auto;
}

.credits-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.credits-item {
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-muted);
}

.credits-item a {
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
}

.credits-item a:hover {
  text-decoration: underline;
}

.credits-desc {
  color: var(--text-muted);
}

/* Panneau de feedback — même principe que .journal-panel/.options-panel/etc., même
   emplacement (droite), rejoint la rotation "un seul ouvert à la fois". */
.feedback-panel {
  position: fixed;
  top: var(--right-panel-top);
  right: 0;
  bottom: var(--right-panel-bottom);
  width: min(85vw, 300px);
  background: var(--panel-bg);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.25);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  /* Doit passer au-dessus du bandeau du Narrateur (`.narrator-banner-stack`,
     z-index: 1500, voir plus bas) — un panneau ouvert par-dessus doit rester
     lisible/cliquable, pas recouvert par un bandeau qui continue de s'afficher
     tant que le Narrateur reste "sur scène" (voir narrator-banner.js). Bug
     corrigé : sans z-index explicite ici, ce panneau restait à `auto`, qu'un
     élément à z-index positif comme le bandeau passe devant dans le même
     contexte d'empilement, quel que soit l'ordre dans le DOM. */
  z-index: 1600;
}

.feedback-panel[hidden] {
  display: none;
}

.feedback-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.feedback-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

.feedback-body {
  /* Voir .options-body pour le pourquoi de `flex`/`min-height: 0`. */
  flex: 1 1 auto;
  min-height: 0;
  padding: 1rem;
  overflow-y: auto;
}

.feedback-section-title {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-base);
}

.feedback-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0.7rem;
  font-size: 0.85rem;
  color: var(--text-base);
}

.feedback-type-select {
  font: inherit;
  padding: 0.4rem;
  border-radius: 6px;
  border: 1px solid var(--text-faint);
  background: var(--surface-bg);
  color: var(--text-base);
}

.feedback-stars {
  display: flex;
  gap: 0.3rem;
}

.feedback-star {
  border: none;
  background: transparent;
  font-size: 1.4rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  color: var(--text-faint);
}

.feedback-star.selected {
  color: var(--accent-armed);
}

.feedback-textarea {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--text-faint);
  background: var(--surface-bg);
  color: var(--text-base);
  resize: vertical;
  min-height: 4.5rem;
  box-sizing: border-box;
}

.feedback-send-button,
.feedback-survey-button {
  width: 100%;
  border: 1px solid var(--text-faint);
  background: transparent;
  color: var(--text-base);
  border-radius: var(--ui-radius);
  padding: 0.6rem 0.8rem;
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
}

.feedback-send-button:disabled,
.feedback-survey-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.feedback-status {
  min-height: 1.2em;
  margin: 0.4rem 0 0;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.feedback-divider {
  margin: 1.1rem 0;
  border: none;
  border-top: 1px solid var(--border-color);
}

.feedback-survey-intro {
  margin: 0 0 0.6rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* Fenêtre de mission (Étape 11, revue pour des missions à plusieurs zones cliquables
   intégrées à l'énoncé, puis pour l'enchaînement de plusieurs chapitres via `?chapitre=`
   dans l'URL, voir main.js) — dans la rangée du haut, alignée avec les boutons de
   droite (voir .top-bar) plutôt qu'en position fixe : un simple enfant flex, aligné à
   gauche. Pas de rappel du point de départ (voir GDD 4.2/Étape 7) : seulement le
   titre, l'énoncé (avec ses mots-clés à armer) et un message une fois une réponse
   tentée. */
/* Une seule carte visuelle (fond/ombre/radius portés ici) regroupant le label et
   l'énoncé — auparavant deux bulles flottantes séparées (une pilule + une bulle),
   fusionnées en un seul élément graphique cohérent. */
.mission-window {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.3rem;
  /* Desktop : la fenêtre s'ÉLARGIT quand son contenu le demande (message de
     réussite long, notamment) plutôt que de grandir en hauteur (retouche
     demandée) — d'où une largeur max généreuse. La variante mobile la ramène à
     100 % (voir @media). */
  max-width: min(52vw, 640px);
  font-family: var(--font-ui);
  padding: 0.6rem 0.9rem;
  background: var(--surface-bg);
  border-radius: var(--ui-radius-lg);
}

/* « Mission : <titre> » — repère de quelle mission est en cours, utile maintenant
   qu'on peut en enchaîner plusieurs (voir main.js#missionUrl). Simple en-tête dans
   la carte (voir .mission-window ci-dessus), plus de fond/pilule propre. */
.mission-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--accent);
}

/* Rangée du repère de chapitre + bouton "Revenir au chapitre" (retouche "monde
   partagé sur 24h", voir mission.js) — le bouton reste discret (petit texte souligné
   plutôt qu'une vraie pilule) : un raccourci de confort, pas une action mise en
   avant. */
.mission-label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
}

.mission-return-button {
  flex-shrink: 0;
  border: none;
  background: transparent;
  padding: 0;
  font: inherit;
  font-size: 0.7rem;
  color: var(--text-muted);
  text-decoration: underline;
  text-decoration-style: dotted;
  cursor: pointer;
}

/* Énoncé de la mission (voir mission.js#splitTemplate) : du texte normal, sauf les
   mots-clés d'un slot qui sont de vrais `<button>` insérés au fil du texte — un
   `<button>` reste de facto `display: inline-block`, donc s'intègre dans le flux
   d'une phrase sans règle spéciale ici. Corps de la carte (voir .mission-window
   ci-dessus), plus de fond/radius/ombre propres. */
.mission-statement {
  margin: 0;
  text-align: left;
  font-size: 0.85rem;
  line-height: 1.6;
}

/* Chapitre sans `stages` défini (aucun aujourd'hui, mais le cas reste géré — voir
   mission.js#createEmptyMissionWindow) : même bulle, texte atténué plutôt qu'un
   énoncé à trous. */
.mission-statement-empty {
  color: var(--text-faint);
  font-style: italic;
}

/* Mot-clé cliquable d'un slot (ex. "Où") — souligné plutôt qu'en pilule séparée,
   pour rester lisible comme un mot du texte qu'on peut cliquer, pas un bouton à part.
   `color` ci-dessous n'est qu'un repli (type de slot non reconnu) : voir les règles
   `.mission-slot.personne/.lieu/.heure` juste en dessous, qui colorent chaque mot-clé
   selon `slot.type` (retouche demandée, voir mission.js) — mêmes variables que les
   mots-clés d'une bulle de dialogue (`--tag-*`, voir :root et .habitant-dialogue
   .personne/.lieu/.heure plus haut), pour que « Où » se lise avec la même couleur que
   « lieu » ailleurs dans le jeu. */
.mission-slot {
  border: none;
  background: transparent;
  padding: 0 0.15em;
  margin: 0 -0.15em;
  border-radius: 4px;
  font: inherit;
  font-weight: 600;
  color: var(--tag-personne);
  text-decoration: underline;
  text-decoration-style: dotted;
  /* Épaisseur/décalage explicites (retouche demandée) — laissés à "auto" par défaut,
     le pointillé se rendait visiblement plus fin/discret sous certaines couleurs
     (`lieu`, orange) que d'autres (`personne`, bleu), au point de sembler absent par
     endroits alors que la propriété est bien la même pour tous les types de slot. */
  text-decoration-thickness: 1.5px;
  text-underline-offset: 2px;
  cursor: pointer;
}

.mission-slot.personne,
.mission-slot-preview.personne {
  color: var(--tag-personne);
}

.mission-slot.lieu,
.mission-slot-preview.lieu {
  color: var(--tag-lieu);
}

.mission-slot.heure,
.mission-slot-preview.heure {
  color: var(--tag-heure);
  font-variant-numeric: tabular-nums;
}

/* Aperçu en lecture seule d'un critère (mode Bible de l'éditeur, voir
   js/mission.js#formatMissionTemplate) — même couleur par type que le mot-clé
   cliquable ci-dessus, mais ni soulignement ni curseur pointeur : rien à cliquer
   ici, juste une prévisualisation de la mise en forme réelle du jeu. */
.mission-slot-preview {
  font-weight: 600;
  color: var(--tag-personne);
}

/* Armé = en attente du clic qui vaudra réponse (voir main.js#attemptAnswer) —
   distinct visuellement pour ne jamais laisser le joueur se demander s'il a
   effectivement activé le mot avant de cliquer sur la carte. */
/* `.mission-slot.mission-slot-armed` (deux classes) et pas `.mission-slot-armed`
   seul : la couleur par type (`.mission-slot.personne` etc.) a deux classes, elle
   l'emporterait sinon sur cet état. Texte sombre sur le fond ambré (contraste). */
.mission-slot.mission-slot-armed {
  background: var(--accent-armed);
  color: #1a1205;
  text-decoration: none;
}

.mission-slot.mission-slot-solved {
  color: var(--ok);
  text-decoration: none;
  cursor: default;
}

.mission-banner {
  /* Remplit la largeur de la fenêtre de mission (qui s'élargit avec ce contenu,
     voir .mission-window) — le texte occupe l'horizontale disponible avant de
     revenir à la ligne, au lieu d'empiler les lignes dans une bulle étroite. */
  max-width: 100%;
  text-align: center;
  padding: 0.5rem 0.9rem;
  border-radius: var(--ui-radius);
  font-size: 0.85rem;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}

/* Bannières calées sur la chrome sombre du "moniteur" (l'ancien fond vert/rouge
   pâle donnait du vert sur vert, illisible — signalé). Fond translucide teinté +
   liseré de la couleur du sens, texte clair. */
.mission-banner-success {
  background: rgba(95, 194, 111, 0.14);
  border: 1px solid rgba(95, 194, 111, 0.4);
  color: #86dc93;
}

.mission-banner-error {
  background: rgba(236, 106, 94, 0.14);
  border: 1px solid rgba(236, 106, 94, 0.4);
  color: #f2988f;
}

/* Lien vers la mission suivante (voir mission.js#render, `feedback.nextMissionUrl`) —
   même couleur que le texte du succès, souligné pour rester repérable comme un lien
   au milieu d'une phrase. */
.mission-banner-link {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
}

.encountered-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.encountered-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
}

/* Filtre par prénom/métier (Étape 9, retouche 6). */
.encountered-filter {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--border-color);
}

.encountered-filter-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  font-size: 0.85rem;
  font-family: inherit;
}

.encountered-filter-clear {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
  color: var(--text-muted);
  flex-shrink: 0;
}

.encountered-list {
  /* Voir .options-body pour le pourquoi de `flex`/`min-height: 0`. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0.4rem;
}

/* Une "carte" par habitant : rangée principale (icône/nom/profession + bouton note),
   sa mini-frise, sa note dépliable — plus une simple rangée depuis la retouche 6.
   `.encountered-item` elle-même n'est plus un <button> (elle contient d'autres
   boutons ; imbriquer des <button> est invalide en HTML, voir carnet.js). */
.encountered-item {
  padding: 0.4rem 0.5rem;
  border-radius: var(--ui-radius);
}

.encountered-item:hover {
  background: rgba(255, 255, 255, 0.06);
}

.encountered-item-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.encountered-item-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0.1rem;
  text-align: left;
}

.encountered-item-icon {
  font-size: 1.3rem;
  flex-shrink: 0;
}

.encountered-item-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Couleur personnelle (retouche demandée, voir js/habitant-color.js) — posée en
   `style` inline par carnet.js#createItem, repli sur la couleur héritée pour un
   habitant sans couleur choisie. */
.encountered-item-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--pcolor-dark, inherit);
}

.encountered-item-job {
  font-size: 0.75rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

.encountered-item-note-toggle {
  flex-shrink: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0.2rem;
  border-radius: 50%;
  opacity: 0.4; /* discret tant qu'il n'y a pas de note, voir .has-note ci-dessous */
}

/* Une note existe pour cet habitant — reste bien visible sans avoir à l'ouvrir pour
   le savoir. */
.encountered-item-note-toggle.has-note {
  opacity: 1;
  background: rgba(224, 160, 48, 0.25);
}

.encountered-item-note {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin-top: 0.3rem;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  padding: 0.4rem;
  font-size: 0.8rem;
  font-family: inherit;
  resize: vertical;
  min-height: 3.5rem;
}

/* Même piège que .encountered-panel[hidden] plus haut dans ce fichier : `display:
   block` ci-dessus est une règle auteur, elle l'emporterait sur le `display: none`
   par défaut de `[hidden]` à spécificité égale — sans cette règle, toutes les notes
   s'afficheraient dépliées en permanence (repéré en testant : c'était bien le cas). */
.encountered-item-note[hidden] {
  display: none;
}

/* Mini-frise par habitant (Étape 9, retouche 6) — même principe que
   .timeline-segments-row en beaucoup plus petit, avec la même couleur dédiée pour le
   point de passage bloqué (voir .timeline-segment-blocked plus haut). */
.mini-timeline {
  display: flex;
  height: 0.5rem;
  margin: 0.35rem 0.1rem;
  border-radius: var(--ui-radius);
  overflow: hidden; /* pas de chevrons ici, contrairement à .timeline-block : rien ne dépasse, on peut arrondir simplement */
}

.mini-timeline {
  /* `visible` (et non `hidden`) : laisse dépasser la tête de lecture rouge d'un
     pixel ou deux — le très léger arrondi des coins se voit à peine à cette
     taille. */
  overflow: visible;
}

.mini-timeline-segment {
  position: relative; /* ancre la tête de lecture ::after ci-dessous */
  flex: 1 1 0;
  min-width: 0;
  border: none;
  padding: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.13);
}

/* Même règle que .timeline-segment:disabled — jamais de saut à l'aveugle. */
.mini-timeline-segment:disabled {
  cursor: default;
}

.mini-timeline-segment-seen {
  background: var(--accent);
}

.mini-timeline-segment-blocked {
  background: var(--accent-armed);
}

/* Point de passage courant = même tête de lecture rouge que la frise principale
   (voir .timeline-segment-current), en plus petit — un fin trait vertical qui
   dépasse un peu de la mini-frise. */
.mini-timeline-segment-current::after {
  content: "";
  position: absolute;
  left: 50%;
  top: -2px;
  bottom: -2px;
  width: 2px;
  transform: translateX(-50%);
  background: #ff3b30;
  box-shadow: 0 0 3px rgba(255, 59, 48, 0.85);
  pointer-events: none;
}

/* Icônes des habitants (Étape 5) — placeholder texte/emoji le temps d'une vraie
   direction artistique (voir GDD 5.1). */
.habitant-icon {
  /* PAS `position: relative` ici (bug corrigé — voir git blame/RELEASE_NOTES) :
     cette classe est posée directement sur l'élément `.leaflet-marker-icon` créé
     par `L.divIcon({className: 'habitant-icon', ...})` (voir population.js), or
     Leaflet positionne CET élément en `position: absolute` via son propre CSS
     (leaflet.css, chargé avant celui-ci) — un `position: relative` ici, même
     spécificité CSS, gagnait sur la règle de Leaflet simplement parce que chargé
     après, et faisait retomber chaque marqueur suivant dans le flux normal du
     document (empilés les uns sous les autres, un habitant de plus = ~16px de
     décalage vertical en plus, invisible avec 2-3 habitants à l'écran mais très
     visible avec une dizaine). Un élément `position: absolute` est déjà lui-même
     un bloc de positionnement valide pour ses descendants absolus (voir
     .habitant-name-card plus bas) — ce `relative` était non seulement inutile
     mais activement cassant. */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
  /* Glissé de l'icône quand un habitant change de position (changement d'heure ou
     d'ancrage) : les marqueurs sont réutilisés d'un rendu à l'autre (voir
     population.js#createPopulationLayer), setLatLng() change juste ce transform,
     que le navigateur anime tout seul — pas de boucle JS à écrire. Même durée que le
     glissé de caméra (0,6 s, voir map.js#PAN_ANIMATION_SECONDS) — retouche demandée :
     plus courte (0,35 s) à l'origine, la personne suivie arrivait avant que la caméra
     n'ait fini de la rattraper, un effet de décalage perturbant. Cinq choses à
     garder synchronisées avec cette valeur si elle change encore : `PAN_ANIMATION_SECONDS`
     (map.js), `MASK_ANIMATION_MS` (visibility.js, le disque de visibilité suit ce
     même rythme), le délai de la bulle de dialogue juste plus bas, la courbe
     `cubic-bezier` du `transform` ci-dessous, et `MARKER_GLIDE_SECONDS`
     (population.js, retouche demandée "points de passage intermédiaires" — durée
     totale d'un glissé à travers plusieurs segments, répartie entre eux
     proportionnellement à leur longueur via une `transition-duration` posée en
     ligne pour l'occasion, cette classe CSS reprenant la main une fois le
     trajet terminé).
     `cubic-bezier(0.5, 1, 0.89, 1)` plutôt que le mot-clé `ease-out` (bug signalé et
     corrigé le 25/08/2026) : approximation standard (easings.net, "easeOutQuad") de
     la courbe quadratique `1 - (1-t)²`, la même que `map.js#PAN_EASE_LINEARITY`
     (option `easeLinearity` de Leaflet) et `visibility.js#easeOut` — le mot-clé
     `ease-out` correspond à une autre courbe (`cubic-bezier(0, 0, 0.58, 1)`), plus
     lente à démarrer, qui faisait visiblement traîner l'habitant derrière la caméra
     et le disque de visibilité pendant un simple changement d'heure.
     `opacity` dans la même liste : sert au fondu d'apparition/disparition d'un
     habitant sans entrée de planning à l'instant courant (voir .habitant-hidden et
     population.js) — sans risque de composition avec `transform` comme `scale`
     (voir .habitant-pulse plus bas), l'opacité ne touche pas à la géométrie, sa
     propre courbe n'a donc pas besoin de suivre ce même alignement. */
  transition:
    transform calc(0.6s * var(--anim-time-scale)) cubic-bezier(0.5, 1, 0.89, 1),
    opacity calc(0.3s * var(--anim-time-scale)) ease-out;
}

/* Crochets d'angle au survol d'un habitant — deux équerres opposées (haut-gauche
   / bas-droite) qui « cadrent » la personne, façon viseur de caméra. Purement
   décoratif (`pointer-events: none`), révélé en CSS via `:hover`, aucun JS. Se
   posent en absolu sur `.habitant-icon` (élément `position: absolute` de Leaflet,
   donc bloc de positionnement valide — même mécanisme que `.habitant-name-card`).
   Discret : fin liseré clair, petite mise à l'échelle à l'apparition. */
.habitant-icon::before,
.habitant-icon::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1.5px solid rgba(233, 242, 255, 0.85);
  opacity: 0;
  transform: scale(1.6);
  transition:
    opacity calc(0.12s * var(--anim-time-scale)) ease-out,
    transform calc(0.12s * var(--anim-time-scale)) ease-out;
  pointer-events: none;
}
.habitant-icon::before {
  top: -5px;
  left: -5px;
  border-right: 0;
  border-bottom: 0;
}
.habitant-icon::after {
  bottom: -5px;
  right: -5px;
  border-left: 0;
  border-top: 0;
}
.habitant-icon:hover::before,
.habitant-icon:hover::after {
  opacity: 1;
  transform: scale(1);
}

/* Posée un seul rendu à la fois (voir population.js#render, option `instant`,
   main.js) quand le glissé de caméra de ce même rendu a lui-même sauté à sa
   position finale plutôt que de glisser (voir map.js#updatePanBounds : une
   précédente translation n'était pas encore terminée) — jamais la caméra figée
   pendant que les habitants continuent de glisser dessus. Ne redéclare que
   `opacity` (fondu d'apparition/disparition, sans lien avec ce problème de
   synchronisation) : `transform` n'a ainsi plus aucune transition tant que cette
   classe est posée, `opacity` continue d'animer normalement. */
.habitant-icon-instant {
  transition: opacity calc(0.3s * var(--anim-time-scale)) ease-out;
}

/* Habitant sans entrée de planning à l'instant courant ("dans un bâtiment", voir
   population.js#entryAt) — fondu géré en JS (ajout/retrait de cette classe), pas en
   CSS pur : il faut distinguer une première apparition/un retour (fondu) d'une
   sortie du rayon de visibilité (disparition instantanée, inchangée). */
.habitant-hidden {
  opacity: 0;
}

/* Bulle de dialogue placée à côté / dans un coin de la tête (direction choisie
   par l'auteur parmi 8, voir js/bubble-placement.js/population.js — la boucle des
   bulles flottantes ; `top` reste la tooltip du marqueur). Le conteneur
   `.habitant-floating-bubble` EST un
   `.leaflet-marker-icon` ancré pile sur la tête (`iconSize: [0, 0]`) — même piège
   que `.habitant-icon` plus haut : PAS de `transform`/`position` propre dessus,
   Leaflet le positionne lui-même. Tout le décalage directionnel est porté par
   l'enfant `.habitant-floating-bubble-inner`, jamais touché par Leaflet
   (translations en `%` relatives à la bulle elle-même : indépendantes du zoom).
   Non déplaçable, purement décoratif (l'ancien glissé pixel par zoom a été
   retiré — il dépendait du zoom et se réglait mal d'un zoom fixe à l'autre). */
.habitant-floating-bubble {
  pointer-events: none;
}

.habitant-floating-bubble-inner {
  position: absolute;
  top: 0;
  left: 0;
  /* Se dimensionne sur son contenu (`.habitant-dialogue` porte déjà une largeur
     fixe `min(80vw, 220px)`, le retour à la ligne est donc déterministe dès le
     premier calcul) — les translations en `-50%`/`-100%` ci-dessous tombent
     juste sans largeur imposée, et une bulle réduite à une emoticon reste
     accolée à la tête plutôt que centrée dans une boîte de 220px. */
  width: max-content;
}

/* `translate` en `%` = relatif à la boîte de `.habitant-floating-bubble-inner`
   elle-même : `-50%` la centre sur la tête (axe traversé), `0` / `-100%` la
   pousse d'un côté ou de l'autre. L'écart en px (24) reproduit
   MARKER_TO_BUBBLE_GAP de js/dialogue-layout.js — compté depuis le centre de la
   tête, il laisse ~8 px entre la bulle et le bord de l'icône ; doit rester
   synchronisé avec `naturalRect` là-bas. Cardinales : centrées sur l'axe
   traversé. Diagonales : un coin près de la tête. */
.habitant-floating-bubble-right {
  transform: translate(24px, -50%);
}
.habitant-floating-bubble-left {
  transform: translate(calc(-100% - 24px), -50%);
}
.habitant-floating-bubble-bottom {
  transform: translate(-50%, 24px);
}
.habitant-floating-bubble-top-right {
  transform: translate(24px, calc(-100% - 24px));
}
.habitant-floating-bubble-top-left {
  transform: translate(calc(-100% - 24px), calc(-100% - 24px));
}
.habitant-floating-bubble-bottom-right {
  transform: translate(24px, 24px);
}
.habitant-floating-bubble-bottom-left {
  transform: translate(calc(-100% - 24px), 24px);
}

/* Contenu réel de l'icône (l'emoji, ou un avatar généré ci-dessous) — élément séparé
   du conteneur ci-dessus, jamais positionné par Leaflet, pour pouvoir y animer
   .habitant-pulse sans toucher au `transform` que Leaflet applique sur .habitant-icon
   (voir plus bas). */
.habitant-icon-inner {
  display: inline-block;
}

/* Avatar généré (DiceBear, voir avatar.js) plutôt que l'emoji brut — même empreinte
   visuelle que les autres icônes (24×24, voir .habitant-icon-inner ci-dessus) mais
   recadré en cercle, fond clair pour ne jamais laisser transparaître la carte dans les
   coins du SVG (son cadre est carré). */
.habitant-icon-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-bg);
}

.habitant-icon-avatar img {
  width: 100%;
  height: 100%;
  display: block;
}

/* Sprite animé LPC (sous-système B, voir doc/roadmap/sprites-lpc-personnages.md et
   js/sprite-renderer.js) — remplace l'avatar/emoji quand `habitant.sprite` est
   renseigné. 32px : SPRITE_DISPLAY_SIZE dans sprite-renderer.js, à garder en phase
   si l'une des deux valeurs change. `background-image` posé en JS (URL dynamique
   par habitant, voir applySpriteState) ; tout le reste — cadrage d'une frame, cycle
   de marche — est ici, en CSS pur. */
.habitant-sprite {
  width: 32px;
  height: 32px;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  background-position-x: 0;
}

/* walk.png/idle.png/run.png/sit.png/emote.png exportés par le compositeur de
   sprites de l'éditeur n'ont pas tous la même largeur (voir
   editor/js/sprite-engine.js#ANIMATIONS — 9/1/8/3/3 colonnes) —
   `background-size` doit donc suivre l'animation active, pas juste la
   direction. Largeurs à l'échelle d'affichage (32px/frame) ; hauteur commune à
   toutes = 4 directions × 32. */
.habitant-sprite-anim-idle {
  background-size: 32px 128px;
}

/* `--anim-time-scale` (voir :root plus haut) déjà utilisé par tout le reste des
   animations du jeu pour le réglage "vitesse des animations" (settings.js) — à 0
   ("Désactivées"), la durée devient nulle et le cycle cesse de tourner (reste figé
   sur une frame) : répond à la demande roadmap §4.3 de pouvoir couper l'animation
   des sprites (perf/accessibilité), sans inventer un réglage séparé. Reprise pour
   les 3 animations cycliques ci-dessous (walk/run/sit/emote), même principe. */
.habitant-sprite-anim-walk {
  background-size: 288px 128px; /* 9 frames × 32px */
  animation: habitant-sprite-walk-cycle calc(0.9s * var(--anim-time-scale)) steps(9) infinite;
}

.habitant-sprite-anim-run {
  background-size: 256px 128px; /* 8 frames × 32px */
  animation: habitant-sprite-run-cycle calc(0.7s * var(--anim-time-scale)) steps(8) infinite;
}

/* 2 frames, pas 3 (bug corrigé 31/08/2026) — voir sprite-engine.js#
   EXPORT_FRAME_COUNTS pour le détail : le corps/la tête, toujours présents,
   n'ont que 2 colonnes assises réellement utilisables dans leur feuille source. */
.habitant-sprite-anim-sit {
  background-size: 64px 128px; /* 2 frames × 32px */
  animation: habitant-sprite-sit-cycle calc(1.2s * var(--anim-time-scale)) steps(2) infinite;
}

.habitant-sprite-anim-emote {
  background-size: 96px 128px; /* 3 frames × 32px */
  animation: habitant-sprite-emote-cycle calc(1.8s * var(--anim-time-scale)) steps(3) infinite;
}

/* Lecture à reculons du cycle walk/run (roadmap §4.2 point 3) — même ligne de
   direction, frames jouées à l'envers, pour donner l'impression de marcher/
   courir en arrière plutôt que de se retourner. Sans effet sur idle/sit/emote
   (pas des cycles de déplacement, voir js/sprite-renderer.js#applySpriteState). */
.habitant-sprite-reverse {
  animation-direction: reverse;
}

@keyframes habitant-sprite-walk-cycle {
  from { background-position-x: 0; }
  to { background-position-x: -288px; } /* -(9 frames × 32px) */
}

@keyframes habitant-sprite-run-cycle {
  from { background-position-x: 0; }
  to { background-position-x: -256px; } /* -(8 frames × 32px) */
}

@keyframes habitant-sprite-sit-cycle {
  from { background-position-x: 0; }
  to { background-position-x: -64px; } /* -(2 frames × 32px) */
}

@keyframes habitant-sprite-emote-cycle {
  from { background-position-x: 0; }
  to { background-position-x: -96px; } /* -(3 frames × 32px) */
}

/* Ligne de la spritesheet à afficher — ordre fixé par l'export (voir
   editor/js/sprite-engine.js#DIRECTIONS et js/sprite-renderer.js#DIRECTIONS,
   qui doivent rester synchronisés avec ces 4 classes). */
.habitant-sprite-dir-down { background-position-y: 0; }
.habitant-sprite-dir-left { background-position-y: -32px; }
.habitant-sprite-dir-right { background-position-y: -64px; }
.habitant-sprite-dir-up { background-position-y: -96px; }

/* Bulle de dialogue (Étape 7) — .habitant-tooltip est le conteneur Leaflet : on n'y
   touche pas (ni transform, ni animation), pour ne pas interférer avec le calcul
   de position/centrage que Leaflet lui applique lui-même — c'est ce qui donnait
   l'impression que la bulle "venait de la gauche". L'apparition (fondu + zoom sur
   elle-même) est sur .habitant-dialogue, le contenu qu'on contrôle entièrement,
   complètement découplé du positionnement du conteneur. */
.habitant-tooltip {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  text-align: center;
}

.habitant-tooltip::before {
  display: none; /* pas de petite flèche Leaflet par défaut, le contenu s'en charge */
}

.habitant-dialogue {
  /* `animation-delay` : durée du glissé de l'icône (0,6 s, voir .habitant-icon
     plus haut) — l'habitant arrive, puis la bulle apparaît, pas pendant qu'il
     bouge — PLUS `(--beat - 1) × 0,45 s` pour les paliers d'apparition différée
     (transcription d'un échange : « Iris dit… », « Noé répond… », voir
     js/dialogue-beat.js `BEAT_GAP_SECONDS` et js/population.js#buildBubbleHtml).
     `--beat` (défaut 1, jamais émis pour une bulle de palier 1 ni en lecture
     automatique) est un entier dans [1, MAX_BEAT=5]. Le tout est multiplié par
     `--anim-time-scale` : la séquence suit la vitesse d'animation choisie et
     tombe à 0 quand les animations sont désactivées. `fill-mode: backwards`
     garde la bulle invisible pendant tout ce délai. */
  animation: habitant-dialogue-appear calc(0.25s * var(--anim-time-scale)) ease-out;
  animation-delay: calc((0.6s + (var(--beat, 1) - 1) * 0.45s) * var(--anim-time-scale));
  animation-fill-mode: backwards;
  font-family: var(--font-speech);
  font-size: 0.75rem;
  /* Élargie (160px à l'origine) : un dialogue un peu long se retrouvait sur une
     dizaine de lignes, bien plus haut que large. `width`, pas `max-width` : ce
     conteneur vit dans le pane de tooltip de Leaflet (`position: absolute`, sans
     largeur propre) — un `max-width` seul s'y réduit au mot le plus long plutôt que
     de remplir l'espace disponible jusqu'au plafond, l'algorithme de
     "shrink-to-fit" du navigateur n'a rien à mesurer pour aller plus loin (bug
     constaté en testant : une valeur de 260px ici produisait une bulle encore plus
     étroite qu'avant). Une largeur fixe force le rendu voulu ; `min(80vw, 220px)`
     reste dans l'écran sur un téléphone étroit (voir la section "Responsive /
     mobile" en fin de fichier) tout en profitant de plus de place sur un écran
     large. */
  width: min(80vw, 220px);
  background: var(--bubble-bg);
  border-radius: var(--ui-radius-lg);
  /* Un peu plus généreux qu'avant (4px 8px, retouche demandée) — plus d'air autour
     du texte, cohérent avec BUBBLE_HEIGHT relevé en écho dans dialogue-layout.js. */
  padding: 7px 11px;
  /* Bulle sombre (façon sous-titrage d'images de surveillance) posée aussi bien
     sur une carte claire que sombre — une ombre portée franche lui donne son
     décollement dans les deux cas. */
  box-shadow: 0 2px 9px rgba(0, 0, 0, 0.5);
  white-space: normal;
  /* Liseré de couleur personnelle (retouche demandée, voir js/habitant-color.js) —
     `--pcolor`/`--pcolor-dark` posées en `style` inline par habitant (chaîne vide
     un habitant sans couleur, voir colorStyleAttr) : `var(--pcolor, transparent)`
     retombe alors sur une bordure invisible plutôt que sur rien, pour ne jamais
     faire varier la largeur totale de la bulle (`box-sizing: border-box`
     ci-dessous) selon qu'une couleur est posée ou non. */
  box-sizing: border-box;
  border: 2px solid var(--pcolor-dark, transparent);
  /* Explicite plutôt qu'hérité (bug corrigé, mode sombre) : Leaflet fixe lui-même
     `color: #222` sur `.leaflet-tooltip` (le parent direct, voir sa propre feuille de
     style) — un ancêtre plus proche que `body`, qui l'emporte donc dans la cascade
     malgré le filet de sécurité posé dessus plus haut dans ce fichier. Texte noir sur
     bulle sombre sinon. */
  color: var(--text-base);
}

/* Déplacement instantané (voir population.js#updateTooltip, `habitant-tooltip-instant`
   posée par main.js/map.js quand le rendu précédent date de moins d'une seconde) :
   pas de glissé de caméra ni de marqueur cette fois (voir .habitant-icon-instant plus
   haut), donc plus de raison d'attendre 0,6 s avant d'afficher la bulle — ce délai
   n'existait que pour laisser l'habitant arriver visuellement avant de parler. */
.habitant-tooltip-instant .habitant-dialogue {
  animation-delay: 0s;
}

/* Empile TOUT ce qu'un habitant affiche au-dessus de sa tête à ce créneau
   (retouche demandée "plusieurs bulles/emoji par créneau") — emoticons puis
   répliques, chacune dans l'ordre de sa propre liste (voir
   population.js#buildTooltipContent) : un simple flux flex vertical avec
   `gap`, aucune position individuelle calculée en JS. `gap` en écho à
   `STACK_GAP` dans dialogue-layout.js (calcul anti-chevauchement) — les deux
   doivent rester synchronisés. */
.habitant-bubble-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

/* Emoticon OpenMoji (retouche demandée, voir js/openmoji.js/population.js#
   buildEmojiBadges) — badge indépendant du dialogue, plusieurs possibles par
   créneau (empilées entre elles comme le reste, voir .habitant-bubble-stack
   ci-dessus). Même apparition/délai que .habitant-dialogue ci-dessous (attend
   le glissé de l'icône). */
.habitant-emoji-badge {
  display: flex;
  justify-content: center;
  animation: habitant-dialogue-appear calc(0.25s * var(--anim-time-scale)) ease-out;
  /* Même délai (glissé de l'icône + palier `--beat`) que .habitant-dialogue
     ci-dessus — voir le commentaire là-bas. Une emoticon peut elle aussi porter
     un palier (« Noé lève les yeux au ciel » après la réplique d'Iris). */
  animation-delay: calc((0.6s + (var(--beat, 1) - 1) * 0.45s) * var(--anim-time-scale));
  animation-fill-mode: backwards;
}

.habitant-tooltip-instant .habitant-emoji-badge {
  animation-delay: 0s;
}

.habitant-emoji-badge img {
  width: 28px;
  height: 28px;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
}

/* Types de bulle (voir entry.dialogueType, population.js#buildTooltipContent) — juste
   un habillage supplémentaire par-dessus .habitant-dialogue ci-dessus (bordure, icône
   via ::before, style de texte), jamais un changement de mise en page : le contenu et
   son positionnement restent identiques à une bulle normale. Couleur personnelle
   (retouche demandée) : remplace la couleur fixe par catégorie (`#7a8fa6`/`#2f9e58`,
   conservées comme repli `var(--pcolor, …)` pour un habitant sans couleur choisie),
   mais garde le style de trait propre au type (`border-style` seul redéfini ici,
   pas `border` en entier — sinon on écraserait la couleur posée par
   `.habitant-dialogue` juste au-dessus). */
.habitant-dialogue-pensee {
  border-style: dotted;
  border-color: var(--pcolor-dark, #7a8fa6);
  border-radius: var(--ui-radius-lg);
  font-style: italic;
}

.habitant-dialogue-pensee::before {
  content: '💭 ';
  font-style: normal;
}

.habitant-dialogue-telephone {
  border-color: var(--pcolor-dark, #2f9e58);
}

.habitant-dialogue-telephone::before {
  content: '📞 ';
}

/* Bulle stylisée (tête + nom + métier) au survol d'un avatar — carte (population.js)
   comme frise (carnet.js#createFace) — voir avatar.js#createNameCard. Cachée par
   défaut, révélée en pur CSS via `:hover` sur le conteneur ancré (`.habitant-icon`/
   `.crossed-face`, tous deux `position: relative` pour ça) : ni JS de positionnement,
   ni conflit avec la bulle de dialogue permanente ci-dessus, posée à droite plutôt
   qu'au-dessus (déjà pris par elle sur la carte). Survol souris uniquement — sur
   tactile, sans état `:hover` fiable, elle ne s'affiche simplement jamais ; le clic
   de sélection, lui, continue de fonctionner comme avant, rien n'est perdu. */
.habitant-name-card {
  position: absolute;
  top: 50%;
  left: 100%;
  transform: translateY(-50%);
  margin-left: 0.4rem;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.5rem;
  background: var(--bubble-bg);
  border-radius: var(--ui-radius);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  font-family: var(--font-ui);
  font-size: 0.7rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease-out;
}

/* Têtes d'habitants présents dans une scène (retouche demandée, éditeur — voir
   editor/js/scenes-list-mode.js) ajoutée à cette même règle plutôt que
   déclarée dans editor/css/style.css : elle centralise déjà tous les
   conteneurs qui révèlent .habitant-name-card, jeu et éditeur confondus (les
   deux pages chargent cette feuille de style, voir editor/index.html). */
.habitant-icon:hover .habitant-name-card,
.crossed-face:hover .habitant-name-card,
.editor-scenes-list-habitant:hover .habitant-name-card {
  opacity: 1;
}

/* `<pers pers="Nom">texte</pers>` (retouche demandée, voir dialogue-markup.js) — un
   mot-clé de dialogue qui révèle au survol la bulle nom/métier/tête d'un habitant
   précis, pas forcément celui que le texte affiché nomme ("ce type" peut viser
   Noé). Même principe que .habitant-icon/.crossed-face ci-dessus (`position:
   relative` pour ancrer la bulle, révélée via `:hover`) — un `<span>` inline dans le
   texte plutôt qu'une icône ronde, la bulle se positionne donc pareillement (`left:
   100%` de .habitant-name-card) mais part du coin du mot plutôt que du centre d'une
   icône. `pointer-events: auto` est indispensable ici : ce mot-clé vit soit dans une
   bulle de dialogue Leaflet (`.leaflet-tooltip-pane`, non interactive par défaut
   dans Leaflet — les clics doivent traverser jusqu'à la carte), soit dans le bandeau
   du Narrateur (`.narrator-banner`, `pointer-events: none` pour la même raison) —
   sans ce réglage ciblé, ce `<span>` précis resterait lui aussi non survolable comme
   le reste de son conteneur, `:hover` ne se déclenche jamais sur un élément dont
   `pointer-events` vaut `none`, même hérité d'un ancêtre plusieurs niveaux plus
   haut. */
.dialogue-person-hover {
  position: relative;
  pointer-events: auto;
}

.dialogue-person-hover:hover .habitant-name-card {
  opacity: 1;
}

/* Étiquettes forcées visibles (retouche demandée) — soit temporairement en maintenant
   Alt (`.show-name-cards-alt`, voir main.js), soit en permanence via le réglage du
   panneau Options (`.show-name-cards-always`, voir settings.js#getShowNameCards) :
   les deux classes sont posées sur `<html>`, un ancêtre commun à toute la carte, pas
   sur chaque `.habitant-icon` individuellement. Toujours sous les bulles de dialogue
   (`.habitant-tooltip`) : celles-ci vivent dans le pane de tooltip de Leaflet,
   au-dessus du pane des marqueurs par défaut, sans rien à faire de plus ici — voir
   `.habitant-name-card` plus haut, dont le `z-index: 10` ne joue que dans son propre
   contexte d'empilement local (`.habitant-icon`, position: relative), jamais contre le
   pane des tooltips. Restreint à `.habitant-icon` (icônes sur la carte) : la frise
   chronologique (`.crossed-face`, voir carnet.js#createFace) a ses propres têtes très
   rapprochées, où forcer toutes les étiquettes en même temps les fait se chevaucher —
   la frise garde donc son comportement `:hover` normal ci-dessus, non affecté par Maj. */
.show-name-cards-always .habitant-icon .habitant-name-card,
.show-name-cards-alt .habitant-icon .habitant-name-card {
  opacity: 1;
}

.habitant-name-card-icon {
  display: flex;
  font-size: 1.1rem;
  line-height: 1;
  flex-shrink: 0;
}

.habitant-name-card-info {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}

/* Ligne du haut de la fiche : prénom à gauche, âge estimé poussé à droite (voir
   avatar.js#createNameCardContent). `gap` minimal garanti même quand la bulle se
   réduit à la largeur de son contenu. */
.habitant-name-card-namerow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.1rem;
}

.habitant-name-card-age {
  color: var(--text-faint);
  font-size: 0.82em;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Couleur personnelle (retouche demandée, voir js/habitant-color.js) — posée en
   `style` inline par avatar.js#createNameCardContent, repli sur `--text-strong`
   habituel pour un habitant sans couleur choisie. */
.habitant-name-card-name {
  font-weight: 600;
  color: var(--pcolor-dark, var(--text-strong));
}

.habitant-name-card-job {
  color: var(--text-muted);
  font-size: 0.9em;
}

/* Habillage "fiche d'identification de surveillance" de la bulle nom/métier —
   uniquement dans les contextes de JEU (icône sur la carte, mot-clé `<pers>` de
   dialogue, étiquettes forcées visibles), jamais dans la liste de l'éditeur
   (`.editor-scenes-list-habitant`) qui partage pourtant le même composant. Petit
   cartouche "SUJET" au-dessus du nom + 4 crochets d'angle. */
.habitant-icon .habitant-name-card-info::before,
.dialogue-person-hover .habitant-name-card-info::before,
.show-name-cards-always .habitant-icon .habitant-name-card-info::before,
.show-name-cards-alt .habitant-icon .habitant-name-card-info::before {
  content: "SUJET";
  font-size: 0.68em;
  letter-spacing: 0.16em;
  color: var(--text-faint);
  line-height: 1;
  margin-bottom: 0.1rem;
}

.habitant-icon .habitant-name-card,
.dialogue-person-hover .habitant-name-card {
  padding: 0.35rem 0.55rem;
}
.habitant-icon .habitant-name-card::before,
.habitant-icon .habitant-name-card::after,
.dialogue-person-hover .habitant-name-card::before,
.dialogue-person-hover .habitant-name-card::after {
  content: "";
  position: absolute;
  width: 6px;
  height: 6px;
  border: 1px solid rgba(233, 242, 255, 0.5);
  pointer-events: none;
}
.habitant-icon .habitant-name-card::before,
.dialogue-person-hover .habitant-name-card::before {
  top: 3px;
  left: 3px;
  border-right: 0;
  border-bottom: 0;
}
.habitant-icon .habitant-name-card::after,
.dialogue-person-hover .habitant-name-card::after {
  bottom: 3px;
  right: 3px;
  border-left: 0;
  border-top: 0;
}

/* Fondu + zoom sur elle-même, rien d'autre (pas de déplacement). `opacity`/`scale`
   plutôt que `transform: scale(...)` par cohérence avec le reste du fichier — ici
   ça n'a pas d'incidence sur le positionnement (.habitant-dialogue n'est pas
   déplacé par Leaflet, contrairement à .habitant-tooltip), mais autant garder
   la même convention que .habitant-pulse. */
@keyframes habitant-dialogue-appear {
  from { opacity: 0; scale: 0.85; }
  to { opacity: 1; scale: 1; }
}

/* Mots-clés mis en valeur dans un dialogue — `<span class="personne|lieu|heure">` en
   toutes lettres dans les données (ex. chapitre2-chien-echappe.json), ou les balises
   courtes équivalentes `<pers>`/`<lieu>`/`<heure>` (voir js/dialogue-markup.js,
   résolues en `<span class="…">` avant affichage — les deux formes produisent
   exactement ce même HTML). Rendus tels quels par la bulle (déjà de l'innerHTML, voir
   population.js#buildTooltipContent), un style constant par catégorie plutôt qu'une
   couleur libre par arc. Couleurs posées en variables (`--tag-personne`/`-lieu`/
   `-heure`, voir :root plus haut) plutôt qu'en dur ici — réutilisées telles quelles par
   les mots-clés cliquables de l'énoncé de mission (retouche demandée, voir
   .mission-slot plus bas), pour que « Où » dans l'énoncé porte la même couleur que
   « lieu » dans une bulle de dialogue. `heure` en chiffres tabulaires pour rester
   lisible à cette taille de police. */
/* `.narrator-banner-text` (voir narrator-banner.js) ajouté à chaque sélecteur
   ci-dessous (bug corrigé) — son texte passe par le même
   `expandShortDialogueTags` que `.habitant-dialogue` et produit donc les mêmes
   `<span class="personne|lieu|heure|em1|em2">`, mais n'existait pas encore quand
   ces règles ont été écrites : sans lui, les mots-clés du bandeau du Narrateur
   s'affichaient en texte brut, sans couleur ni gras. */
/* Couleur personnelle (retouche demandée, voir js/habitant-color.js) — un
   `<pers pers="Nom">` dont l'attribut résout un habitant avec une couleur choisie
   porte cette couleur (`--pcolor` posée sur ce `<span>` précis, voir
   dialogue-markup.js) plutôt que la couleur générique `--tag-personne` (repli
   ci-dessous, inchangé pour un `<pers>` sans cible reconnue — "qui", "quelqu'un
   d'autre"…). */
.habitant-dialogue .personne,
.narrator-banner-text .personne {
  color: var(--pcolor-dark, var(--tag-personne));
  font-weight: 600;
}

.habitant-dialogue .lieu,
.narrator-banner-text .lieu {
  color: var(--tag-lieu);
  font-weight: 600;
}

.habitant-dialogue .heure,
.narrator-banner-text .heure {
  color: var(--tag-heure);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Deux surlignages génériques supplémentaires (`<em1>`/`<em2>`, voir
   js/dialogue-markup.js), sans catégorie fixe contrairement aux trois ci-dessus — à
   l'autrice de décider au cas par cas ce qu'ils mettent en valeur (un détail à
   retenir, un indice...). Deux teintes distinctes de la palette déjà en place
   ailleurs dans le jeu, pour rester cohérentes sans en inventer de nouvelles. */
.habitant-dialogue .em1,
.narrator-banner-text .em1 {
  color: #9b5de5;
  font-weight: 600;
}

.habitant-dialogue .em2,
.narrator-banner-text .em2 {
  color: var(--danger);
  font-weight: 600;
}

/* Texte alternatif selon la largeur d'écran (voir js/dialogue-markup.js#SHORT_TAGS,
   balises courtes `<mob>`/`<desk>`) — pas propre à `.habitant-dialogue` : réutilisé
   tel quel par le bandeau du Narrateur (narrator-banner.js), qui passe aussi son
   texte par `expandShortDialogueTags`. Seuil de bascule identique à celui du reste
   de l'UI responsive (voir @media plus bas), pour ne jamais désynchroniser "ce que
   le joueur voit" et "ce que le dialogue lui dit de regarder".
   Desktop par défaut, le mobile masque/révèle l'inverse dans le bloc "Responsive /
   mobile" en fin de fichier plutôt qu'ici, pour garder tous les ajustements de seuil
   groupés au même endroit. */
.dialogue-mobile-only {
  display: none;
}

/* Retour visuel au clic sur une entité non-sélectionnable (Étape 6) — confirme que
   le clic a été pris en compte sans changer d'ancrage (ex. le chien, Pataud). */
.habitant-pulse {
  animation: habitant-pulse calc(0.4s * var(--anim-time-scale)) ease-out;
}

/* Posée sur .habitant-icon-inner, jamais sur .habitant-icon (voir plus haut) : même
   `scale` en propriété indépendante plutôt que `transform: scale(...)`, ça ne suffit
   pas à éviter le conflit si c'est posé sur l'élément que Leaflet positionne — les
   propriétés indépendantes (translate/rotate/scale) se combinent *après* la valeur
   de `transform` dans la matrice effective (le point est d'abord déplacé par le
   `translate3d` de Leaflet, *puis* mis à l'échelle), donc `scale` multiplierait le
   déplacement de Leaflet lui-même : l'icône partait hors écran puis revenait au pic
   du pulse (40 %, scale 1.6). Sur un élément séparé et non positionné, plus de
   translate3d à multiplier — `scale` reste purement local. */
@keyframes habitant-pulse {
  0% { scale: 1; }
  40% { scale: 1.6; }
  100% { scale: 1; }
}

/* Overlay plein écran de la chorégraphie de caméra entre deux chapitres (voir
   mission.js#showIntroOverlay et map.js#playArrivalIntro/playDepartureZoomOut) —
   au-dessus de tout le reste de l'UI, la carte est encore en train de se
   recentrer/zoomer dessous. Un simple assombrissement uniforme plutôt qu'un cercle de
   visibilité agrandi (essayé puis abandonné, voir le commentaire dans mission.js).
   Opacité pilotée en JS (classe -visible ajoutée puis retirée) plutôt qu'une animation
   @keyframes : un simple `transition` sur la classe suffit et reste plus simple à
   déclencher au bon moment (fin de la chorégraphie de caméra) qu'une animation nommée
   à durée fixe. `fixed`, pas `absolute` : comme `.bottom-bar` plus haut, `#ui-overlay`
   n'a pas de hauteur propre (il s'ajuste à son contenu, ici juste .top-bar) — un
   positionnement relatif à lui ne couvrirait que cette bande du haut, pas tout
   l'écran. */
.mission-intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Voile plein écran couvrant toute la chorégraphie de caméra d'un changement de
     chapitre (dézoom de départ comme arrivée — voir main.js). Noir profond
     translucide, dans la continuité du reste de l'habillage « signal » (brouillage,
     extinction du moniteur, tous déjà en noir) : pas de cercle de visibilité
     cohérent à redessiner pendant un zoom/dézoom en cours (essayé puis abandonné —
     voir showIntroOverlay plus bas). */
  background: rgba(4, 6, 9, 0.62);
  pointer-events: none; /* décor seul, ne doit intercepter aucun clic sur la carte en dessous */
  opacity: 0;
  transition: opacity calc(0.5s * var(--anim-time-scale)) ease;
}

.mission-intro-overlay-visible {
  opacity: 1;
}

/* Conteneur du carton d'arrivée (voir mission.js#showChapterTitle) — juste le
   centrage : `.mission-intro-overlay` centre déjà son enfant, ceci ne fait que
   borner la largeur et donner un peu d'air sur les bords d'un petit écran. */
.mission-intro-title {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 1rem;
  box-sizing: border-box;
}

/* Carte d'identification du flux — OSD façon moniteur de vidéosurveillance qui
   « acquiert » un canal (voir mission.js#showChapterTitle). Le cadre physique du
   moniteur + les crochets plein écran sont déjà posés par surveillance-ui.js ;
   cette carte est l'incrustation d'identification, un panneau sombre translucide
   (lisible sur le voile de .mission-intro-overlay comme sur la carte en
   dessous) à chasse fixe et texte clair. Pas de transition de sortie : retirée
   d'un coup au bout de TITLE_DISPLAY_SECONDS, l'overlay restant assombri jusqu'au
   rezoom. */
.mission-intro-card {
  position: relative;
  box-sizing: border-box;
  width: min(90vw, 600px);
  padding: 1.5rem 1.8rem 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  overflow: hidden;
  background: linear-gradient(rgba(9, 13, 19, 0.82), rgba(6, 9, 14, 0.86));
  border: 1px solid rgba(150, 190, 235, 0.28);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.4),
    0 18px 50px rgba(0, 0, 0, 0.5),
    inset 0 0 40px rgba(90, 140, 200, 0.06);
  color: #e6eefb;
  font-family: var(--font-readout);
  animation: mission-intro-card-in calc(0.42s * var(--anim-time-scale)) ease-out both;
}

@keyframes mission-intro-card-in {
  from { opacity: 0; transform: translateY(6px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

/* Crochets d'angle « viseur » — s'écartent légèrement en s'installant (même idée
   que les crochets de survol de .habitant-icon). */
.mission-intro-bracket {
  position: absolute;
  width: 14px;
  height: 14px;
  border: 1.5px solid rgba(180, 214, 255, 0.9);
  pointer-events: none;
  animation: mission-intro-bracket-in calc(0.3s * var(--anim-time-scale)) calc(0.12s * var(--anim-time-scale)) ease-out both;
}
.mission-intro-bracket-tl { top: 6px; left: 6px; border-right: 0; border-bottom: 0; }
.mission-intro-bracket-tr { top: 6px; right: 6px; border-left: 0; border-bottom: 0; }
.mission-intro-bracket-bl { bottom: 6px; left: 6px; border-right: 0; border-top: 0; }
.mission-intro-bracket-br { bottom: 6px; right: 6px; border-left: 0; border-top: 0; }

@keyframes mission-intro-bracket-in {
  from { opacity: 0; transform: scale(2.2); }
  to   { opacity: 1; transform: scale(1); }
}

/* Ligne de balayage — descend une fois sur toute la carte pendant l'acquisition
   (façon CRT qui accroche un signal), puis disparaît. */
.mission-intro-scan {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 34%;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(150, 200, 255, 0) 0%,
    rgba(150, 200, 255, 0.08) 62%,
    rgba(200, 226, 255, 0.26) 92%,
    rgba(210, 232, 255, 0.5) 100%
  );
  border-bottom: 1px solid rgba(210, 232, 255, 0.5);
  transform: translateY(-110%);
  animation: mission-intro-scan calc(1s * var(--anim-time-scale)) calc(0.14s * var(--anim-time-scale)) ease-in both;
}

@keyframes mission-intro-scan {
  from { transform: translateY(-110%); opacity: 1; }
  to   { transform: translateY(400%); opacity: 0; }
}

/* Entête du cartouche : statut « acquisition » (point rouge clignotant) à gauche,
   repère de canal à droite. */
.mission-intro-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: clamp(0.6rem, 1.7vw, 0.72rem);
  letter-spacing: 0.18em;
  color: rgba(200, 216, 236, 0.7);
}

.mission-intro-acq {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: #ff6a5e;
  /* Sans ça, un flex-item par défaut (`flex-shrink: 1`) se laisse rétrécir sous la
     largeur de son texte plutôt que de le laisser déborder — "ACQUISITION DU FLUX"
     repassait alors à la ligne (repéré à 375px) au lieu de rester sur la même ligne
     que .mission-intro-cam à droite. Tronqué plutôt que débordant si jamais un jour
     trop juste. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mission-intro-acq-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #ff2d20;
  box-shadow: 0 0 5px rgba(255, 45, 32, 0.85);
  animation: mission-intro-blink calc(1.1s * var(--anim-time-scale)) ease-in-out infinite;
}

@keyframes mission-intro-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

.mission-intro-cam {
  font-variant-numeric: tabular-nums;
  /* Ne rétrécit jamais (voir .mission-intro-acq juste au-dessus) : c'est le repère
     de canal, toujours prioritaire sur le statut d'acquisition à sa gauche. */
  flex-shrink: 0;
  white-space: nowrap;
}

/* Corps du cartouche — apparaît juste après le cadre, glissé bref vers le haut. */
.mission-intro-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  animation: mission-intro-rise calc(0.4s * var(--anim-time-scale)) calc(0.2s * var(--anim-time-scale)) ease-out both;
}

@keyframes mission-intro-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.mission-intro-designation {
  font-size: clamp(0.66rem, 1.8vw, 0.8rem);
  letter-spacing: 0.3em;
  color: rgba(150, 190, 235, 0.85);
}

.mission-intro-title-name {
  margin: 0;
  /* Plancher abaissé (1.6rem → 1.15rem, passe mobile 2026-09-15) : à 375px de large,
     `5.5vw` (~20px) restait sous l'ancien plancher, qui l'écrasait et forçait les
     titres de chapitre à deux lignes (ex. "RENDEZ-VOUS SECRET") malgré la place
     disponible. Le plafond desktop (2.9rem) est inchangé. */
  font-size: clamp(1.15rem, 5.5vw, 2.9rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #f2f7ff;
  text-shadow: 0 0 18px rgba(120, 170, 235, 0.35);
  /* « Verrouillage du signal » : le titre accroche par à-coups avant de se fixer. */
  animation: mission-intro-lock calc(0.5s * var(--anim-time-scale)) calc(0.24s * var(--anim-time-scale)) steps(1, end) both;
}

@keyframes mission-intro-lock {
  0%   { opacity: 0; }
  15%  { opacity: 0.9; }
  25%  { opacity: 0.12; }
  40%  { opacity: 1; }
  55%  { opacity: 0.45; }
  70%, 100% { opacity: 1; }
}

.mission-intro-subtitle {
  font-family: var(--font-speech);
  font-size: clamp(0.85rem, 2.4vw, 1.05rem);
  font-style: italic;
  line-height: 1.35;
  color: rgba(216, 226, 240, 0.82);
}

/* Pied de cartouche : lieu (majuscules, puce ▸) + fenêtre horaire, façon relevé
   incrusté ; passe en colonne sur un écran étroit. */
.mission-intro-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 0.7rem;
  border-top: 1px solid rgba(150, 190, 235, 0.2);
  font-size: clamp(0.66rem, 1.9vw, 0.8rem);
  letter-spacing: 0.14em;
  color: rgba(200, 216, 236, 0.72);
  animation: mission-intro-rise calc(0.4s * var(--anim-time-scale)) calc(0.32s * var(--anim-time-scale)) ease-out both;
}

.mission-intro-foot-place {
  text-transform: uppercase;
  /* Rétrécit et tronque plutôt que de forcer .mission-intro-foot à passer en
     colonne (voir plus bas) : garde le lieu et l'horaire sur la même ligne même à
     375px, quitte à couper un `lieu` inhabituellement long — l'horaire, plus court
     et plus utile d'un coup d'œil, ne doit lui jamais être coupé. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mission-intro-foot-place::before {
  content: "▸ ";
  color: rgba(150, 190, 235, 0.9);
}

.mission-intro-foot-time {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #e6eefb;
}

/* Écran étroit (téléphone, ~480px et moins) : la carte gagne le padding latéral
   perdu par le texte (retouche demandée 2026-09-15 — lieu/horaire et
   acquisition/canal restent chacun sur une seule ligne, voir .mission-intro-foot-place
   / .mission-intro-acq / .mission-intro-cam ci-dessus, plutôt que de les faire
   passer en colonne comme avant). */
@media (max-width: 480px) {
  .mission-intro-card {
    padding: 1.2rem 1.1rem 1rem;
  }
  .mission-intro-foot {
    gap: 0.6rem;
  }
}

/* Ligne de statut "surveillance" — utilisée seule et centrée à la sortie de
   chapitre (main.js#playChapterExit). N'altère pas le travelling de caméra. */
.mission-intro-status {
  font-family: var(--font-readout);
  font-size: clamp(0.7rem, 2vw, 0.95rem);
  font-weight: 600;
  letter-spacing: 0.16em;
  color: #cfd9e8;
  opacity: 0.72;
}

/* Animations désactivées / mouvement réduit : la carte et son contenu s'affichent
   d'emblée dans leur état final, plus de balayage ni de clignotement. */
@media (prefers-reduced-motion: reduce) {
  .mission-intro-card,
  .mission-intro-bracket,
  .mission-intro-body,
  .mission-intro-title-name,
  .mission-intro-foot {
    animation: none;
  }
  .mission-intro-scan { display: none; }
  .mission-intro-acq-dot { animation: none; }
}

/* Invite d'attente en pied de carte d'identification (voir mission.js#showChapterTitle) :
   c'est cette carte qui sert de sas de démarrage désormais (l'ancien écran noir plein
   écran « appuyez pour démarrer » a été retiré) — le premier geste lève la carte ET
   débloque l'audio. Légère pulsation pour signaler l'attente ; figée sous
   prefers-reduced-motion. */
.mission-intro-prompt {
  margin-top: 0.15rem;
  padding-top: 0.7rem;
  border-top: 1px solid rgba(150, 190, 235, 0.2);
  text-align: center;
  font-family: var(--font-readout);
  font-size: clamp(0.68rem, 1.9vw, 0.82rem);
  font-weight: 600;
  letter-spacing: 0.18em;
  line-height: 1.4;
  color: #d7dde3;
  animation: mission-intro-prompt-pulse 1.8s ease-in-out infinite;
}

@keyframes mission-intro-prompt-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.95; }
}

@media (prefers-reduced-motion: reduce) {
  .mission-intro-prompt {
    animation: none;
    opacity: 0.9;
  }
}

/* Écran titre du jeu (voir js/titlescreen.js) — affiché une seule fois, avant la
   toute première partie d'un joueur, jamais aux visites suivantes. Fond opaque (pas
   translucide comme .mission-intro-overlay) : la carte est déjà montée en dessous
   (avec son voile d'intro), l'écran titre doit la masquer complètement. La
   `transition` ne sert qu'au fondu d'ENTRÉE (classe -visible) : à la sortie,
   titlescreen.js retire l'élément d'un coup pendant que l'allumage du moniteur
   (`.surveillance-powerup`, z-index plus élevé) recouvre déjà l'écran de noir. */
.title-screen {
  position: fixed;
  inset: 0;
  z-index: 2000; /* au-dessus de tout, y compris .mission-intro-overlay (1000) */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 1rem 2rem;
  text-align: center;
  font-family: var(--font-speech);
  color: var(--text-strong);
  background: #06070a;
  opacity: 0;
  transition: opacity 0.6s ease;
}

.title-screen-visible {
  opacity: 1;
}

/* Mire de barres (façon SMPTE) en fond — sept barres verticales + une bande
   sombre en bas. Pur `linear-gradient`, aucun asset. */
.title-screen-bars {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom, transparent 0 74%, #10131a 74% 82%, #05060a 82% 100%),
    linear-gradient(90deg,
      #c0c0c0 0 14.28%, #c9c90c 14.28% 28.57%, #0cc9c9 28.57% 42.85%,
      #0cc90c 42.85% 57.14%, #c90cc9 57.14% 71.42%, #c90c0c 71.42% 85.71%,
      #1414c9 85.71% 100%);
  opacity: 0.5;
}

.title-screen-scan {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(to bottom,
    rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.12) 1px, transparent 1px, transparent 3px);
  mix-blend-mode: multiply;
}

/* Carte sombre lisible par-dessus la mire. */
.title-screen-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  padding: 2rem 2.6rem;
  background: rgba(9, 11, 14, 0.9);
  border: 1px solid var(--border-color);
  border-radius: var(--ui-radius-lg);
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}

.title-screen-status {
  position: relative;
  font-family: var(--font-readout);
  font-size: clamp(0.7rem, 2vw, 0.9rem);
  letter-spacing: 0.14em;
  color: #ff6a5e;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

/* Brouillage plein écran (voir js/camera-glitch.js) — joué le temps qu'une
   tentative de navigation refusée "hors caméra" s'explique (voir
   js/population.js#previewShadowExcursion). `pointer-events: none` : ne bloque
   jamais l'interaction, contrairement à .title-screen/.mission-intro-overlay, un
   effet purement visuel et temporaire. Sous .title-screen (2000) — les deux ne
   se produisent jamais en même temps, mais autant garder le même ordre logique
   ("recouvrement le plus définitif = le plus haut"). Dégradé répété + un
   `hue-rotate` animé en boucle plutôt qu'une image de bruit statique : aucun
   fichier supplémentaire à charger, cohérent avec "aucune dépendance en plus de
   ce qui l'est déjà" (voir CLAUDE.md). */
/* `#ui-overlay > .camera-*` / `.blocked-time-toast` / `.achievement-toast` /
   `.narrator-banner-stack` : mêmes précautions que `#ui-overlay > .surveillance-*`
   plus bas — la règle `#ui-overlay > *` (pointer-events: auto, spécificité 1,0,0)
   l'emporterait sinon sur un simple sélecteur de classe, et ces recouvrements
   (créés une fois puis jamais retirés, gardés à opacité 0 entre deux passages)
   captureraient TOUS les clics du jeu même invisibles. Bug signalé une première
   fois pour l'aperçu « signal coupé » (plus rien de cliquable après), puis à
   nouveau sur mobile : la pilule « la personne n'est plus visible »
   (`.blocked-time-toast`, `position: fixed; top: 0.75rem; left: 50%`) se superpose
   à l'horloge — donc aux boutons ◀/▶ — de la barre du haut et, restée dans le DOM
   à opacité 0 après sa première apparition, bloquait durablement ces boutons.
   Idem `.achievement-toast` (même position) et `.narrator-banner-stack` (bande
   fixe pleine largeur au-dessus de la barre du bas). */
#ui-overlay > .camera-glitch-overlay,
#ui-overlay > .camera-signal-blip,
#ui-overlay > .camera-signal-dropout,
#ui-overlay > .blocked-time-toast,
#ui-overlay > .achievement-toast,
#ui-overlay > .narrator-banner-stack {
  pointer-events: none;
}

/* « Signal coupé » : l'écran plonge dans le NOIR (pas un voile pâle — retouche
   demandée : l'ancienne version, un voile clair en `mix-blend-mode: difference`,
   ressortait beaucoup trop claire sur le thème sombre), avec de fines lignes de
   balayage et un léger scintillement d'opacité. */
.camera-glitch-overlay {
  position: fixed;
  inset: 0;
  z-index: 1500;
  pointer-events: none;
  opacity: 0;
  background:
    repeating-linear-gradient(
      0deg,
      rgba(255, 255, 255, 0.035) 0px,
      rgba(255, 255, 255, 0.035) 1px,
      transparent 1px,
      transparent 3px
    ),
    #04060a;
  transition: opacity calc(0.12s * var(--anim-time-scale)) ease;
}

.camera-glitch-overlay-visible {
  opacity: 0.94;
  animation: camera-glitch-flicker calc(0.14s * var(--anim-time-scale)) steps(2, jump-none) infinite;
}

@keyframes camera-glitch-flicker {
  0%   { opacity: 0.94; }
  50%  { opacity: 0.8; }
  100% { opacity: 0.97; }
}

/* Micro-artefact "la bande saute" (voir js/camera-glitch.js#playSignalBlip) —
   déclenché rarement à un changement d'heure : une barre de désynchro qui roule
   verticalement en ~0,15 s. Durée via `--anim-time-scale` ⇒ 0 s si animations
   désactivées / `prefers-reduced-motion` sans choix explicite. */
.camera-signal-blip {
  position: fixed;
  inset: 0;
  z-index: 1490;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
}

.camera-signal-blip::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 20%;
  transform: translateY(-40%);
  background: linear-gradient(
    rgba(255, 255, 255, 0),
    rgba(255, 255, 255, 0.16),
    rgba(0, 0, 0, 0.12),
    rgba(255, 255, 255, 0)
  );
}

.camera-signal-blip-active {
  animation: camera-signal-blip calc(0.15s * var(--anim-time-scale)) linear;
}

.camera-signal-blip-active::before {
  animation: camera-signal-blip-roll calc(0.15s * var(--anim-time-scale)) linear;
}

@keyframes camera-signal-blip {
  0%, 100% { opacity: 0; }
  25%, 75% { opacity: 1; }
}

@keyframes camera-signal-blip-roll {
  from { transform: translateY(-40%); }
  to { transform: translateY(520%); }
}

/* Perte de signal franche (voir js/camera-glitch.js#playSignalDropout) — rare :
   neige plein écran + bandeau « PERTE DE SIGNAL » central. `pointer-events: none`
   (effet purement visuel). Sous .title-screen (2000), au-dessus du brouillage
   ordinaire. Neige : dégradés répétés fins + balayage vertical animé, aucun
   fichier. Durée via `--anim-time-scale` ⇒ 0 s si animations désactivées. */
.camera-signal-dropout {
  position: fixed;
  inset: 0;
  z-index: 1495;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  background:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.10) 0 1px, transparent 1px 2px),
    repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, rgba(0,0,0,0.10) 1px 3px),
    #05070a;
}
.camera-signal-dropout-label {
  font-family: var(--font-readout);
  font-size: clamp(0.9rem, 3.4vw, 1.5rem);
  font-weight: 700;
  letter-spacing: 0.22em;
  color: #ff5a4e;
  text-shadow: 0 0 8px rgba(0, 0, 0, 0.9), 0 2px 4px rgba(0, 0, 0, 0.9);
}
.camera-signal-dropout-active {
  animation: camera-signal-dropout calc(0.62s * var(--anim-time-scale)) steps(1, end);
}
.camera-signal-dropout-active .camera-signal-dropout-label {
  animation: camera-signal-dropout-jitter calc(0.1s * var(--anim-time-scale)) steps(2) infinite;
}
@keyframes camera-signal-dropout {
  0%   { opacity: 0; }
  10%  { opacity: 0.94; background-position: 0 0, 0 0, 0 0; }
  45%  { opacity: 0.94; background-position: 0 -7px, 3px 0, 0 0; }
  80%  { opacity: 0.94; background-position: 0 5px, -2px 0, 0 0; }
  100% { opacity: 0; }
}
@keyframes camera-signal-dropout-jitter {
  0%   { transform: translate(0, 0); }
  100% { transform: translate(-1px, 1px); }
}

/* Habillage "images de surveillance" (voir js/surveillance-ui.js) — trois couches
   décoratives plein écran, entièrement passives (`pointer-events: none`, override
   de la règle `#ui-overlay > *`). Volontairement TRÈS discret : le jeu doit
   rester parfaitement lisible, c'est une ambiance, pas un filtre. */
/* `#ui-overlay > .surveillance-*` et pas `.surveillance-*` seul : la règle
   `#ui-overlay > *` plus haut (pointer-events: auto) l'emporterait sinon sur un
   simple sélecteur de classe, et ces couches capteraient TOUS les clics du jeu.
   `pointer-events` étant hérité, les sous-couches suivent.
   `.surveillance-overlay` est peinte DERRIÈRE l'UI (état de l'image),
   `.surveillance-frame` DEVANT (cadre physique du moniteur — coins, burn-in). */
#ui-overlay > .surveillance-overlay,
#ui-overlay > .surveillance-frame {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

#ui-overlay > .surveillance-frame {
  z-index: 1450; /* au-dessus de l'UI de base, sous les panneaux (1600) et modales */
}

.surveillance-grain,
.surveillance-scanlines,
.surveillance-vignette,
.surveillance-bezel {
  position: absolute;
  inset: 0;
}

/* Coins d'écran arrondis + fond sombre derrière : la "zone image" (#map) est
   rognée aux coins, révélant l'entourage sombre — voir aussi `.leaflet-container`.
   Appliqué au bezel/scanlines/vignette pour qu'ils suivent la même courbe. */
.surveillance-grain,
.surveillance-scanlines,
.surveillance-vignette {
  border-radius: 9px;
}

/* Grain animé — tuile de bruit (générée à la volée, voir
   surveillance-ui.js#noiseTileUri) répétée et décalée en boucle. Très discret.
   Durée via `--anim-time-scale` : 0 s ⇒ grain figé (ne "crawle" plus). */
.surveillance-grain {
  background-repeat: repeat;
  opacity: 0.035;
  mix-blend-mode: overlay;
  animation: surveillance-grain calc(0.6s * var(--anim-time-scale)) steps(5) infinite;
}
@keyframes surveillance-grain {
  0%   { background-position: 0 0; }
  100% { background-position: -83px 57px; }
}

/* Fines lignes de balayage horizontales. `mix-blend-mode: multiply` : assombrit
   très légèrement une ligne sur trois, sans "laver" les couleurs. */
.surveillance-scanlines {
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.035) 0,
    rgba(0, 0, 0, 0.035) 1px,
    transparent 1px,
    transparent 3px
  );
  mix-blend-mode: multiply;
  opacity: 0.4;
}

/* Assombrissement progressif des bords — "on regarde un écran". */
.surveillance-vignette {
  background: radial-gradient(
    ellipse 82% 82% at 50% 47%,
    transparent 44%,
    rgba(0, 0, 0, 0.5) 100%
  );
}

/* Assombrissement interne feutré du bord de l'image (couche arrière). */
.surveillance-bezel {
  box-shadow: inset 0 0 44px rgba(0, 0, 0, 0.32);
}

/* Cadre physique du moniteur (couche AVANT, `.surveillance-frame`) — un vrai
   liseré épais avec biseau (lumière en haut-gauche, ombre en bas-droite) et
   coins extérieurs arrondis. `pointer-events: none` hérité : les clics passent
   au travers (la frise reste cliquable même si ses derniers pixels passent sous
   le cadre). Réduit sur mobile (voir @media). */
.surveillance-bezel-frame {
  position: absolute;
  inset: 0;
  border: 8px solid #0b0c0f;
  border-radius: 15px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    inset 3px 3px 6px rgba(255, 255, 255, 0.035),
    inset -3px -3px 7px rgba(0, 0, 0, 0.55);
}


/* Coins en équerre (façon viseur) — chaque élément ne dessine que 2 côtés. */
.surveillance-corner {
  position: absolute;
  width: 22px;
  height: 22px;
  border: 0 solid rgba(255, 255, 255, 0.68);
}
.surveillance-corner-tl { top: 15px; left: 15px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 3px; }
.surveillance-corner-tr { top: 15px; right: 15px; border-top-width: 2px; border-right-width: 2px; border-top-right-radius: 3px; }
.surveillance-corner-bl { bottom: 15px; left: 15px; border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: 3px; }
.surveillance-corner-br { bottom: 15px; right: 15px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 3px; }

/* Incrustations "gravées dans l'image" (burn-in) — voir surveillance-ui.js.
   Blanc, chasse fixe, ombre portée dure (comme un vrai timecode incrusté), en
   dehors des zones occupées par l'UI (fenêtre de mission en haut à gauche,
   horloge en haut à droite, barre du bas). */
.surveillance-burn {
  position: absolute;
  font-family: var(--font-readout);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 0 1px #000, 0 1px 2px rgba(0, 0, 0, 0.9);
  white-space: nowrap;
}
/* Étiquette caméra (bas-gauche) et horodatage (bas-droite) — juste au-dessus de
   la barre du bas, comme le burn-in d'un vrai enregistreur. Les zones hautes
   sont prises par la fenêtre de mission et l'horloge. */
.surveillance-burn-cam {
  left: 1rem;
  bottom: calc(6.7rem + env(safe-area-inset-bottom));
}
.surveillance-burn-stamp {
  right: 1rem;
  bottom: calc(6.7rem + env(safe-area-inset-bottom)); /* au-dessus de l'attribution de la carte */
}

/* Flash de l'incrustation `CAM NN` au changement de cible
   (surveillance-ui.js#flashCameraSwitch) — la coupure au noir plein écran a été
   retirée (trop brutale). */
.surveillance-burn-cam.surveillance-switching {
  animation: surveillance-cam-flash calc(0.6s * var(--anim-time-scale)) ease-out;
}
@keyframes surveillance-cam-flash {
  0%, 100% { color: rgba(255, 255, 255, 0.8); }
  20%, 55% { color: #fff; text-shadow: 0 0 6px rgba(255, 255, 255, 0.8), 0 1px 2px #000; }
}

/* Réticule de verrouillage de cible (surveillance-ui.js#lockOn) — 4 équerres
   rouges qui convergent sur la personne sélectionnée puis s'effacent. Une seule
   keyframe : `scale` sur l'assemblage entier (les équerres gardent leur écart
   fixe). Durée via `--anim-time-scale`. */
.surveillance-lock {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
}
.surveillance-lock-b {
  position: absolute;
  width: 15px;
  height: 15px;
  border: 2px solid #ff3b30;
  box-shadow: 0 0 3px rgba(255, 59, 48, 0.7);
}
.surveillance-lock-b-tl { top: -21px; left: -21px; border-right: 0; border-bottom: 0; }
.surveillance-lock-b-tr { top: -21px; left: 6px;   border-left: 0;  border-bottom: 0; }
.surveillance-lock-b-bl { top: 6px;   left: -21px; border-right: 0; border-top: 0; }
.surveillance-lock-b-br { top: 6px;   left: 6px;   border-left: 0;  border-top: 0; }
.surveillance-lock-active {
  animation: surveillance-lock calc(0.55s * var(--anim-time-scale)) ease-out;
}
@keyframes surveillance-lock {
  0%   { opacity: 0; transform: scale(2.6); }
  22%  { opacity: 1; }
  65%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1); }
}

/* Extinction du moniteur (fin de chapitre — voir js/surveillance-ui.js#powerDown,
   js/main.js#onContinue) : coupure au noir, flash blanc plein cadre qui se
   comprime en un trait horizontal puis en un point avant de s'éteindre. Joué
   APRÈS le dézoom de départ (inchangé). Durée via `--anim-time-scale` : 0 ⇒
   l'appelant saute entièrement l'effet. Au-dessus des panneaux (1600). */
#ui-overlay > .surveillance-powerdown {
  position: fixed;
  inset: 0;
  z-index: 1700;
  pointer-events: none;
  background: #000;
  opacity: 0;
  overflow: hidden;
}
.surveillance-powerdown-on {
  animation: surveillance-powerdown-bg calc(0.62s * var(--anim-time-scale)) linear forwards;
}
.surveillance-powerdown::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 46vh;
  transform: translateY(-50%);
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0) 0%,
    rgba(233, 242, 255, 0.95) 45%,
    rgba(255, 255, 255, 1) 50%,
    rgba(233, 242, 255, 0.95) 55%,
    rgba(255, 255, 255, 0) 100%
  );
  opacity: 0;
}
.surveillance-powerdown-on::after {
  animation: surveillance-powerdown-line calc(0.62s * var(--anim-time-scale)) ease-in forwards;
}
@keyframes surveillance-powerdown-bg {
  0%  { opacity: 0; }
  6%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes surveillance-powerdown-line {
  0%   { opacity: 0; transform: translateY(-50%) scale(1, 1); }
  8%   { opacity: 1; transform: translateY(-50%) scale(1, 1); }
  38%  { opacity: 1; transform: translateY(-50%) scale(1, 0.012); }
  72%  { opacity: 1; transform: translateY(-50%) scale(0.0016, 0.012); }
  90%  { opacity: 0.85; transform: translateY(-50%) scale(0.0016, 0.012); }
  100% { opacity: 0; transform: translateY(-50%) scale(0.0016, 0.012); }
}

/* Allumage du moniteur (début de chapitre — voir js/surveillance-ui.js#powerUp,
   js/main.js#startChapterIntro) : l'INVERSE de l'extinction — un point puis un
   trait qui s'ouvre pour révéler l'image déjà rendue en dessous. Joué APRÈS le
   travelling d'arrivée (inchangé). Au-dessus de l'écran titre (`.title-screen`,
   2000) : à la toute première partie, l'allumage enchaîne directement sur
   l'écran titre en le recouvrant de noir avant qu'on ait pu voir la carte —
   sans ce z-index, l'écran titre disparaissait en fondu et révélait un instant
   la carte à peine voilée (retouche demandée). */
#ui-overlay > .surveillance-powerup {
  position: fixed;
  inset: 0;
  z-index: 2100;
  pointer-events: none;
  background: #000;
  opacity: 0;
  overflow: hidden;
}
.surveillance-powerup-on {
  animation: surveillance-powerup-bg calc(0.62s * var(--anim-time-scale)) linear forwards;
}
.surveillance-powerup::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 46vh;
  transform: translateY(-50%);
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0) 0%,
    rgba(233, 242, 255, 0.95) 45%,
    rgba(255, 255, 255, 1) 50%,
    rgba(233, 242, 255, 0.95) 55%,
    rgba(255, 255, 255, 0) 100%
  );
  opacity: 0;
}
.surveillance-powerup-on::after {
  animation: surveillance-powerup-line calc(0.62s * var(--anim-time-scale)) ease-out forwards;
}
@keyframes surveillance-powerup-bg {
  0%   { opacity: 1; }
  62%  { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes surveillance-powerup-line {
  0%   { opacity: 0; transform: translateY(-50%) scale(0.0016, 0.012); }
  14%  { opacity: 1; transform: translateY(-50%) scale(0.0016, 0.012); }
  40%  { opacity: 1; transform: translateY(-50%) scale(1, 0.012); }
  70%  { opacity: 0.9; transform: translateY(-50%) scale(1, 1); }
  100% { opacity: 0; transform: translateY(-50%) scale(1, 1); }
}

/* Icône de caméra sur la carte (voir js/camera-layer.js, js/camera-icon.js) — un
   `L.divIcon`, pas un marqueur d'habitant : pas de glissé/transition, la caméra ne
   bouge jamais en jeu (contrairement à .habitant-icon). Le "cône" n'est plus
   dessiné ici : c'est le masque de visibilité qui l'exprime en négatif (voir
   js/visibility.js). */
.camera-marker-icon {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}

/* `<div>` intermédiaire qui porte la rotation vers le centre du cône (cap de la
   caméra) — voir camera-icon.js#cameraIconHtml. transform-origin au centre : la
   caméra pivote autour de sa position sur la carte (iconAnchor par défaut = centre). */
.camera-marker-body {
  display: block;
  width: 22px;
  height: 22px;
  transform-origin: 50% 50%;
}

/* Silhouette de la caméra en un seul tracé : `paint-order: stroke` fait passer le
   contour derrière le remplissage, il ne reste qu'un fin liseré clair tout autour
   — lisible aussi bien sur fond de carte clair que sombre, sans jeu de palette par
   thème. */
.camera-marker-shape {
  fill: #2b303a;
  stroke: rgba(255, 255, 255, 0.92);
  stroke-width: 1.4;
  stroke-linejoin: round;
  paint-order: stroke;
}

.camera-marker-lens {
  fill: #0f1116;
}

/* Témoin d'enregistrement — point rouge, fixe au repos. "Pompe" 3 fois (point
   qui grossit + halo rouge sur toute l'icône) quand l'ancrage suivi vient
   d'entrer dans le champ de cette caméra ("elle t'a repéré", voir
   js/camera-layer.js#blinkTally, déclenché par js/main.js). Durée via
   `--anim-time-scale` : animations désactivées (ou `prefers-reduced-motion` sans
   choix explicite) ⇒ 0 s, rien ne se passe. */
.camera-marker-tally {
  fill: #ff3b30;
  transform-box: fill-box;
  transform-origin: center;
}

.camera-marker-icon.tally-blink {
  animation: camera-tally-glow calc(3s * var(--anim-time-scale)) ease-in-out;
}

.camera-marker-icon.tally-blink .camera-marker-tally {
  animation: camera-tally-blink calc(3s * var(--anim-time-scale)) ease-in-out;
}

/* 3 pulsations lentes (pic ~16/50/83 %, retour au repos entre) — un halo qui
   "respire" plutôt qu'un flash. */
@keyframes camera-tally-glow {
  0%, 33%, 66%, 100% { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)); }
  16%, 50%, 83% {
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55))
            drop-shadow(0 0 4px #ff2d20)
            drop-shadow(0 0 9px #ff2d20);
  }
}

@keyframes camera-tally-blink {
  0%, 33%, 66%, 100% { fill: #ff3b30; transform: scale(1); }
  16%, 50%, 83%      { fill: #ff9086; transform: scale(1.75); }
}

/* Masque de visibilité (voir js/visibility.js#createVisibilityMask) — trois
   `L.polygon` sans contour, empilés : `-dim` voile TOUT le disque uniformément,
   `-glow` (par-dessus) dévoile/éclaircit l'union des cônes ∩ disque, `-outer`
   voile tout le reste du monde. Résultat : cône = net, disque hors cône = voilé,
   hors disque = sombre — et deux cônes qui se recouvrent rendent exactement
   comme un seul (plus de "trous" pair/impair, voir l'en-tête du module). On ne
   peut pas "assombrir plus" une carte déjà noire — le voile `-dim` reste discret
   et c'est surtout la LUEUR `-glow` (bleutée, plus claire que ses abords) qui
   matérialise le faisceau. */
.visibility-mask-dim   { fill: #05070a; fill-opacity: 0.36; }
.visibility-mask-glow  { fill: #e4eeff; fill-opacity: 0.2; }
/* `-outer` porte aussi le CONTOUR du disque (son "trou") : le cercle de vision
   est ainsi tracé, pas seulement suggéré par le contraste de voile — mais très
   discrètement (un liseré à peine perceptible, pas un trait franc). L'écart de
   `fill-opacity` avec `-dim` fait le gros du travail : c'est lui qui sépare le
   disque net du voile extérieur. */
.visibility-mask-outer {
  fill: #05070a; fill-opacity: 0.75;
  stroke: #bcd2f5; stroke-width: 1; stroke-opacity: 0.10;
}

/* `.visibility-mask-blur` (posée/retirée par le module) adoucit les frontières ;
   retirée pendant le glissé du disque, où re-rasteriser un flou plein écran à
   chaque image coûterait trop cher. Le voile "hors disque" est peu flouté (bord
   du cercle juste adouci, sans perdre en opacité), les deux couches internes
   davantage (bord des faisceaux bien fondu). */
.visibility-mask-blur.visibility-mask-outer { filter: blur(2px); }
.visibility-mask-blur.visibility-mask-dim,
.visibility-mask-blur.visibility-mask-glow { filter: blur(6px); }

/* Logo (voir titlescreen.js) — PNG à fond transparent, titre « angle mort »
   inclus : posé tel quel sur la carte sombre de l'écran titre, sans plaque ni
   liseré, et sans titre texte répété en dessous. */
.title-screen-logo {
  width: clamp(240px, 62vw, 440px);
  max-width: 100%;
  height: auto;
  display: block;
}

.title-screen-tagline {
  max-width: 34rem;
  font-size: clamp(0.95rem, 2.4vw, 1.2rem);
  font-style: italic;
  color: var(--text-muted);
}

.title-screen-credit {
  margin-top: 0.4rem;
  font-size: clamp(0.85rem, 2vw, 1rem);
  color: var(--text-faint);
}

/* --- Responsive / mobile --------------------------------------------------------
   Le reste de cette feuille a été écrit desktop-first (rem/vw/min() déjà partout,
   mais aucun media query) — testé et affiné ici sur un iPhone SE (375px) et un
   Android moyen (360px) en portrait, plus une hauteur réduite en paysage. Trois
   groupes de règles : largeur de la barre du haut, équilibre cible/frise dans la
   barre du bas, taille des cibles tactiles — regroupées ici plutôt qu'éclatées à
   côté de chaque règle d'origine, pour garder une seule source de vérité des
   ajustements mobiles à relire d'un bloc. */

/* Barre du haut : `.mission-window` (à gauche) et `.top-bar-right` (icônes +
   horloge, à droite) sont deux enfants flex côte à côte sans possibilité de
   rétrécir suffisamment sur un téléphone — au lieu de laisser flexbox les écraser
   tous les deux jusqu'à rendre l'énoncé illisible (un mot par ligne, constaté en
   testant), ils passent l'un sous l'autre en dessous de cette largeur. */
@media (max-width: 700px) {
  /* Bascule du texte alternatif de dialogue (voir .dialogue-mobile-only plus haut,
     js/dialogue-markup.js#SHORT_TAGS) — inverse desktop/mobile en une seule fois. */
  .dialogue-mobile-only {
    display: inline;
  }

  .dialogue-desktop-only {
    display: none;
  }

  .top-bar {
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
  }

  /* Barre du haut plus haute ici (éléments empilés) mais place verticale rare —
     les panneaux repartent plus haut qu'en desktop. */
  :root {
    --right-panel-top: 3.4rem;
  }

  /* Masquée par défaut (voir .mission-toggle plus bas, seul moyen de la rouvrir sur
     mobile) : en flux normal une fois rouverte, pas de position fixe/overlay — elle
     pousse simplement le reste de .top-bar (colonne, voir ci-dessus) vers le bas
     tant qu'elle reste ouverte. */
  .mission-window {
    display: none;
    max-width: 100%;
  }

  .mission-window.mission-window-open {
    display: flex;
  }

  .mission-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Horloge au-dessus, icônes toutes ensemble sur une seule ligne juste en dessous
     (retouche demandée) — plutôt que de les cacher derrière un menu ☰ (essayé puis
     abandonné) ou de les répartir sur plusieurs lignes. `order: -1` sur l'horloge
     (voir plus bas) la fait passer en premier visuellement bien qu'elle soit
     dernière dans le DOM (inchangé, desktop l'affiche après les icônes) — combiné à
     son propre `flex: 1 0 100%`, elle occupe sa propre ligne pleine largeur, et les
     trois groupes d'icônes (occasionnelles, séparateur, fréquentes) suivent tous sur
     la ligne suivante. Bug corrigé une première fois (25/08/2026, passe mobile) :
     icônes + horloge ensemble sur une seule ligne dépassaient un écran de téléphone
     (~375px et moins, `overflow-x: hidden` sur `body` masquait silencieusement le
     débordement) — les icônes seules, sans l'horloge à côté, tiennent largement en
     dessous de cette largeur. */
  .top-bar-right {
    justify-content: space-between;
    align-items: center; /* annule le `stretch` desktop : ici horloge et icônes sont sur deux lignes */
    flex-wrap: wrap;
    row-gap: 0.4rem;
  }

  /* Retour à UNE seule rangée d'icônes sur mobile (le desktop les met sur deux) :
     la place verticale y est trop précieuse, et l'horloge occupe déjà sa propre
     ligne pleine largeur juste au-dessus. */
  .top-bar-icon-strip {
    flex-direction: row;
  }
  .top-bar-icon-divider {
    display: block;
  }
  .top-bar-right .clock-dvr-body {
    gap: 0.25rem; /* 5 boutons désormais (lecture arrière ajoutée) — resserré */
  }

  /* Horloge resserrée (retouche mobile demandée) — mêmes boutons/lecture, juste
     moins de marge autour : sur un écran étroit, la version desktop (padding
     généreux, gap large) occupait à elle seule une bonne partie de la largeur pour
     un simple repère d'heure + 3 boutons. */
  .top-bar-right .clock {
    order: -1;
    flex: 1 0 100%;
    align-items: center;
    gap: 0.2rem;
    padding: 0.3rem 0.7rem;
  }

  .top-bar-right .clock-dvr-head {
    width: 100%;
  }

  .top-bar-right .clock-time {
    min-width: 8ch;
    font-size: 1rem;
  }

  .top-bar-right .clock-nav {
    width: 1.7rem;
    height: 1.6rem;
    font-size: 0.8rem;
  }

  /* La barre de cible (icône + nom/métier) et la frise chronologique se partagent
     la largeur de l'écran en bas — `max-width: min(70vw, 320px)` (voir plus haut)
     est pensé pour un écran large ; sur un téléphone, ça laisse à peine assez de
     place à la frise pour que ses segments restent cliquables (repéré en testant :
     des colonnes de moins de 25 px de large sur un Android à 360px). Resserré ici
     plutôt que là-bas, pour ne garder qu'un seul endroit qui connaît la largeur
     "normale" (desktop/tablette large).
     Le métier disparaît (le nom seul suffit à identifier la cible sur cet espace
     réduit, voir .target-bar-job plus haut pour l'ellipsis déjà en place sur le nom
     si besoin) — la place gagnée va à la frise, nettement plus utile à agrandir. */
  .target-bar {
    max-width: 38vw;
    padding: 0.4rem 0.6rem;
    gap: 0.4rem;
  }

  .target-bar-job {
    display: none;
  }

  /* Rangées de segments/règle un peu plus hautes : la largeur de chaque segment
     reste contrainte par le nombre de points de passage (hors de contrôle ici),
     mais une cible plus haute reste plus facile à toucher précisément même
     étroite. */
  .timeline-segments-row,
  .timeline-ruler-row {
    height: 1.4rem;
  }

  /* Têtes croisées empilées avec chevauchement (retouche demandée) plutôt que
     passer à la ligne (`flex-wrap: wrap`, comportement desktop, voir plus haut) —
     une colonne de frise est étroite sur téléphone, plusieurs têtes qui passent à
     la ligne poussaient la rangée sur une hauteur grandissante et écrasante.
     `margin-left` négatif (annulé sur la première tête de chaque colonne) plutôt
     que `gap` : les têtes se recouvrent légèrement, à la manière d'une pile de
     cartes, une seule ligne quel que soit le nombre de personnes présentes.
     Bordure de la couleur du fond (voir .bottom-bar) pour détacher visuellement
     chaque tête de celle qu'elle recouvre partiellement — sans elle, deux avatars
     de couleurs proches se fondraient l'un dans l'autre au niveau du chevauchement. */
  .timeline-face-column {
    flex-wrap: nowrap;
    gap: 0;
  }

  .crossed-face {
    box-sizing: border-box;
    margin-left: -0.7rem;
    border: 2px solid var(--surface-bg);
  }

  .crossed-face:first-child {
    margin-left: 0;
  }
}

/* Cibles tactiles — `(pointer: coarse)` cible les écrans tactiles quelle que soit
   leur largeur (un iPad en usage tactile compte autant qu'un téléphone), plutôt
   qu'une largeur d'écran qui ne dit rien du mode de pointage réel. Les icônes de la
   barre du haut (2,2rem ≈ 35px) et le rayon de tolérance des mots-clés de mission
   restent sous la recommandation usuelle de 44px de cible tactile — agrandis ici
   sans toucher à leur taille "souris/trackpad", déjà confortable telle quelle. */
@media (pointer: coarse) {
  .journal-toggle,
  .encountered-toggle,
  .achievements-toggle,
  .options-toggle,
  .feedback-toggle,
  .mission-toggle,
  .debug-follow-toggle,
  .debug-showall-toggle {
    width: 2.75rem;
    height: 2.75rem;
    font-size: 1.3rem;
  }

  /* Mot-clé de mission (ex. "Où") : un `<button>` intégré au fil du texte, jamais
     agrandi jusqu'à 44px sans casser la lecture de la phrase qui le contient — une
     marge tactile modeste plutôt qu'une vraie cible carrée, seul compromis possible
     pour un bouton qui doit rester un mot parmi d'autres. */
  .mission-slot {
    padding: 0.3em 0.25em;
    margin: 0 -0.25em;
  }

  .crossed-face {
    width: 2rem;
    height: 2rem;
  }
}

/* Téléphone (≤ 480px, tactile ou non — retouche demandée 2026-09-15) : la cible de
   44px ci-dessus est confortable sur une tablette, mais les 6 icônes prennent alors
   une part disproportionnée de la largeur d'un téléphone, à côté de l'horloge sur la
   même ligne (voir .top-bar-right dans le bloc "Responsive" plus haut). Resserré
   d'un cran, toujours au-dessus de la taille "souris" d'origine (2.15rem/1.15rem). */
@media (max-width: 480px) {
  .journal-toggle,
  .encountered-toggle,
  .achievements-toggle,
  .options-toggle,
  .feedback-toggle,
  .mission-toggle,
  .debug-follow-toggle,
  .debug-showall-toggle {
    width: 2.3rem;
    height: 2.3rem;
    font-size: 1.1rem;
  }
}

/* Téléphone en paysage : peu de hauteur disponible, la barre du haut (fenêtre de
   mission + icônes) et la barre du bas (cible + frise) peuvent à elles deux manger
   toute la vue si rien ne rétrécit — resserré plutôt que laissé tel quel, pour
   garder un peu de carte visible entre les deux. `orientation: landscape` seul
   suffirait, mais exclurait à tort une tablette large en paysage (qui a largement
   la hauteur pour l'affichage normal) — la hauteur réduite est le vrai critère. */
@media (max-height: 480px) and (orientation: landscape) {
  .top-bar {
    margin-top: 0.4rem;
  }

  .mission-window {
    padding: 0.4rem 0.7rem;
    max-width: min(55vw, 320px);
  }

  .mission-statement {
    font-size: 0.78rem;
    line-height: 1.4;
  }

  .target-bar-icon {
    font-size: 1.1rem;
  }

  .target-bar {
    padding: 0.3rem 0.5rem;
  }
}

/* Rappel « tenez l'appareil en portrait » (voir js/orientation-hint.js) —
   recouvre tout tant qu'on est sur un petit écran tenu en paysage. Même critère
   que le @media de resserrement juste au-dessus (petite hauteur + paysage +
   pointeur grossier), appliqué côté JS via matchMedia. Au-dessus des panneaux
   (1600) mais sous l'écran titre (2000) : si la toute première partie démarre en
   paysage, le titre passe d'abord, puis ce rappel prend le relais. Affichage
   piloté par l'attribut `hidden` (jamais de règle `display` inconditionnelle qui
   l'emporterait sur lui). */
.orientation-hint {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1900;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.1rem;
  padding: 1.2rem 2rem;
  text-align: center;
  font-family: var(--font-speech);
  color: var(--text-strong);
  background: #06070a;
}

.orientation-hint:not([hidden]) {
  display: flex;
}

/* Téléphone qui bascule de la verticale au paysage puis revient — montre le geste
   à faire à l'envers (on part de "ce que tu tiens" vers "ce qu'il faut"). Coupée
   sous prefers-reduced-motion (voir plus bas). */
.orientation-hint-icon {
  font-size: 2.6rem;
  line-height: 1;
  animation: orientation-hint-tip 2.6s ease-in-out infinite;
}

@keyframes orientation-hint-tip {
  0%, 45% { transform: rotate(90deg); }
  75%, 100% { transform: rotate(0deg); }
}

.orientation-hint-message {
  margin: 0;
  max-width: 22rem;
  font-size: 1rem;
  line-height: 1.5;
}

.orientation-hint-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
}

.orientation-hint-button {
  padding: 0.55rem 1rem;
  font-family: var(--font-ui);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--text-strong);
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid var(--border-color);
  border-radius: var(--ui-radius);
  cursor: pointer;
}

.orientation-hint-button:hover {
  background: rgba(255, 255, 255, 0.16);
}

.orientation-hint-button-ghost {
  background: transparent;
  color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
  .orientation-hint-icon {
    animation: none;
    transform: rotate(90deg);
  }
}

