  /* ---- Mode VN (roman visuel) v1 — décors & sprites en PLACEHOLDER (CSS), à remplacer par des assets ---- */
  #vn { position:fixed; inset:38px 0 0 0; z-index:80; display:none; overflow:hidden; user-select:none; }
  body.vn-open #vn { display:block; }
  body.vn-open #log, body.vn-open #inbar, body.vn-open .sidebar { visibility:hidden; }
  #vn-bg { position:absolute; inset:0; background:linear-gradient(160deg,#243049,#0e1320 58%,#1a1030); }
  /* VN6 : CROSSFADE entre deux calques d'image superposes (background-image n'est pas animable :
     on fond l'OPACITE des calques). Le degrade placeholder du conteneur transparait quand le
     calque visible n'a pas d'image. L'overlay ::after (lisibilite) reste au-dessus des calques. */
  .vn-bg-layer { position:absolute; inset:0; background-size:cover; background-position:center;
    opacity:0; transition:opacity .8s ease; }
  .vn-bg-layer.on { opacity:1; }
  #vn-bg::after { content:""; position:absolute; inset:0;
    background:radial-gradient(120% 80% at 50% -10%, rgba(130,160,230,.16), transparent 60%); }
  /* Decor IMAGE (VN1, resolu par ambiance.py) : le halo placeholder laisse place a un leger
     assombrissement bas -> la boite de dialogue et le tag de lieu restent lisibles. */
  #vn-bg.img::after { background:linear-gradient(180deg, rgba(6,8,14,.30) 0%, transparent 26%,
    transparent 55%, rgba(6,8,14,.42) 100%); }
  #vn-loc { position:absolute; top:14px; left:0; right:0; padding:0 150px; text-align:center;
    color:#cdd6e6; font-size:12px; letter-spacing:.18em; text-transform:uppercase; opacity:.6;
    text-shadow:0 1px 4px #000; pointer-events:none; transition:opacity .35s; }
  #vn-loc.fade { opacity:0; }
  /* v21 : EFFETS D'ÉCRAN des scènes scriptées (overlay #vn-fx + classes sur la scène) */
  #vn-fx { position:absolute; inset:0; z-index:6; pointer-events:none; opacity:0; }
  /* Studio v27 : IMAGE PLEIN ÉCRAN (CG) — devant le décor et les sprites, derrière la bulle ; fondu */
  #vn-cg { position:absolute; inset:0; z-index:3; background-position:center; background-size:cover;
    background-repeat:no-repeat; opacity:0; pointer-events:none; transition:opacity .6s ease; }
  #vn.cg-on #vn-cg { opacity:1; }
  #vn.cg-on #vn-stage { opacity:0; transition:opacity .4s ease; }
  #vn-fx.fx-flash { background:#fff; opacity:0; animation:fxFlash var(--fxd,500ms) ease-out; }
  #vn-fx.fx-fondu_noir { background:#000; opacity:0; animation:fxFade var(--fxd,600ms) ease-in-out; }
  #vn-fx.fx-fondu_blanc { background:#fff; opacity:0; animation:fxFade var(--fxd,600ms) ease-in-out; }
  #vn-fx.fx-teinte { background:var(--fxc,#c0392b); opacity:0; animation:fxTint var(--fxd,600ms) ease-in-out; }
  #vn-fx.fx-vignette { background:radial-gradient(ellipse at center, transparent 42%, #000 122%);
    opacity:0; animation:fxTint var(--fxd,600ms) ease-in-out; }
  #vn-fx.fx-onde { background:radial-gradient(circle at center, rgba(230,240,255,.55) 0%, transparent 42%);
    opacity:0; animation:fxWave var(--fxd,600ms) ease-out; }
  /* v65 — LES BANDES CINÉ NE S'AFFICHAIENT NULLE PART. `#vn-fx` est à `opacity:0` par défaut, et
     chacun des dix autres effets la relève par son animation ; letterbox, lui, n'animait QUE ses
     pseudo-éléments — donc tout son sous-arbre restait invisible. Et `fxBar` finissait à `height:0`
     sans `forwards` : même visibles, les bandes se seraient refermées seules. Elles sont opaques
     par elles-mêmes (`--fxo` n'a pas de sens ici) et TIENNENT jusqu'à la fin de la scène, qui les
     retire — c'est ce qu'on attend d'un cadrage cinéma. */
  #vn-fx.fx-letterbox { opacity:1; }
  #vn-fx.fx-letterbox::before, #vn-fx.fx-letterbox::after { content:""; position:absolute; left:0; right:0;
    height:0; background:#000; animation:fxBar var(--fxd,600ms) ease-in-out forwards; }
  #vn-fx.fx-letterbox::before { top:0; } #vn-fx.fx-letterbox::after { bottom:0; }
  @keyframes fxFlash { 0%{opacity:0} 12%{opacity:var(--fxo,.7)} 100%{opacity:0} }
  @keyframes fxFade { 0%{opacity:0} 42%{opacity:1} 60%{opacity:1} 100%{opacity:0} }
  @keyframes fxTint { 0%{opacity:0} 25%{opacity:var(--fxo,.5)} 75%{opacity:var(--fxo,.5)} 100%{opacity:0} }
  @keyframes fxWave { 0%{opacity:var(--fxo,.6); transform:scale(.2)} 100%{opacity:0; transform:scale(1.7)} }
  @keyframes fxBar { 0%{height:0} 100%{height:12%} }   /* v65 : elles s'ouvrent et RESTENT */
  #vn.fx-shake { animation:fxShake var(--fxd,500ms) linear; }
  @keyframes fxShake { 0%,100%{transform:translate(0,0)} 20%{transform:translate(calc(var(--fxa,6px)*-1),var(--fxa,6px))}
    40%{transform:translate(var(--fxa,6px),calc(var(--fxa,6px)*-1))} 60%{transform:translate(calc(var(--fxa,6px)*-.6),0)}
    80%{transform:translate(var(--fxa,6px),var(--fxa,6px))} }
  #vn.fx-glitch { animation:fxGlitch var(--fxd,500ms) steps(3); }
  @keyframes fxGlitch { 0%{filter:none;transform:translate(0)} 20%{filter:hue-rotate(90deg) saturate(2);transform:translate(-5px,2px)}
    45%{filter:invert(.12);transform:translate(5px,-3px)} 70%{filter:hue-rotate(-80deg);transform:translate(-3px,1px)}
    100%{filter:none;transform:translate(0)} }
  #vn-bg.fx-zoom { animation:fxZoom var(--fxd,600ms) ease-out forwards; }
  @keyframes fxZoom { from{transform:scale(1)} to{transform:scale(1.12)} }
  /* v21 : boutons de CHOIX (au-dessus de la bulle ; le prompt de réponse est masqué) */
  #vn-choices { position:absolute; left:0; right:0; bottom:150px; z-index:6; display:none;
    flex-direction:column; align-items:center; gap:10px; padding:0 20px; }
  .vn-choice { min-width:min(440px,82%); max-width:82%; padding:12px 18px; border-radius:10px;
    border:1px solid rgba(200,215,240,.34); background:rgba(12,16,26,.82); color:#e6ecf7; font:inherit;
    font-size:15px; cursor:pointer; text-align:center; transition:transform .14s, border-color .14s, background .14s; }
  .vn-choice:hover:not([disabled]) { border-color:#c9a24a; background:rgba(32,28,16,.9); transform:translateY(-1px); }
  .vn-choice[disabled] { opacity:.5; cursor:default; }
  @media (prefers-reduced-motion:reduce) {
    #vn-fx, #vn.fx-shake, #vn.fx-glitch, #vn-bg.fx-zoom { animation-duration:1ms !important; }
  }
  /* v20.9 : horloge du monde (sous le lieu, discrète) */
  #vn-clock { position:absolute; top:33px; left:0; right:0; padding:0 150px; text-align:center;
    color:#e0c069; font-size:11px; letter-spacing:.08em; opacity:.72; text-shadow:0 1px 4px #000;
    pointer-events:none; }
  /* VN6 : controles de lecture (auto / vitesse / fin) — coin oppose au bouton Quitter */
  #vn-ctl { position:absolute; top:12px; left:14px; z-index:2; display:flex; gap:6px; }
  #vn-ctl button { background:rgba(0,0,0,.45); color:#cdd6e6; border:1px solid var(--line);
    border-radius:8px; padding:5px 10px; cursor:pointer; font:inherit; font-size:13px; }
  #vn-ctl button.on { border-color:#8ab8e8; color:#fff; box-shadow:0 0 10px rgba(138,184,232,.35); }
  /* vitesse : 3 chevrons, 1/2/3 allumes selon le niveau */
  #vn-speed .ar { opacity:.22; transition:opacity .15s; letter-spacing:1px; }
  #vn-speed .ar.lit { opacity:1; color:#8ab8e8; text-shadow:0 0 6px rgba(138,184,232,.6); }
  /* 📜 backlog : les 20 dernieres bulles lues, par-dessus la scene, sous les controles */
  #vn-log { position:absolute; inset:52px 16px 180px; z-index:3; display:none;
    justify-content:center; align-items:flex-start; }
  #vn-log-list { width:min(760px, 100%); max-height:100%; overflow-y:auto;
    background:rgba(10,12,18,.93); border:1px solid var(--line); border-radius:12px;
    padding:14px 16px; display:flex; flex-direction:column; gap:12px; }
  .vn-log-item .w { display:inline-block; font-size:11px; font-weight:600; padding:2px 8px;
    border-radius:6px; margin-bottom:3px; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.5); }
  .vn-log-item .tx { font-size:13px; line-height:1.5; white-space:pre-wrap; color:#dde3ee; }
  .vn-log-item.dice .tx { color:#e8d78a; }
  .vn-log-item.user .tx { color:#c3d6c3; font-style:italic; }
  .vn-log-item .vn-rb { margin-top:5px; }
  .vn-log-ch { border-top:1px solid var(--line); margin-top:6px; padding-top:8px; }
  .vn-log-ch .tt { font-size:12px; font-weight:600; color:#e8d78a; margin-bottom:4px; }
  /* v54 — LE PLATEAU EST UN ESPACE POSITIONNÉ, plus une rangée flex. Chaque sprite est posé à SA
     place : `left` en % de la scène, `bottom` au ras du sol. Bouger = changer un nombre, et la
     transition l'y emmène — le FLIP (mesurer, recalculer, animer l'écart) a disparu avec le flex.
     ANCRAGE BAS-CENTRE : le transform ramène le sprite d'une demi-largeur vers la gauche et
     `transform-origin` est au sol, si bien qu'un personnage agrandi grandit DEPUIS le sol au lieu
     de s'enfoncer sous le cadre, et que deux échelles différentes partagent la ligne de sol.
     Les variables d'AUTEUR (--px --py --pe --pm) et celles du MOTEUR (--fx --fy --fsc, l'entrée,
     la sortie, le relief de l'actif) sont SÉPARÉES et se composent : une animation ne peut donc
     pas écraser une place, ni l'inverse. */
  /* v68 — LE PLATEAU DESCEND JUSQU'EN BAS, ET LES SPRITES PASSENT DERRIÈRE LA BULLE. C'est la
     convention de tous les moteurs de VN, et ce n'était pas un détail d'esthétique : le plateau
     s'arrêtant AU-DESSUS de la bulle, une image plein pied devait tenir tout entière dans ce qui
     restait — donc elle était réduite, et la tête tombait à 83 px là où un buste en faisait 190.
     C'est la cause mesurée du « un plein pied fait bizarre ». Le personnage est maintenant coupé
     par la bulle à mi-jambe : il gagne un tiers de stature et il est ENFIN dans la scène.
     Deux défauts d'empilement tombent avec :
       • la bulle réservait 198 px (176 de haut + 22 de marge) et le plateau n'en cédait que 176 :
         tout sprite mordait 22 px sur elle, sur tous les mondes ;
       • les sprites étaient en z-index 100+ SANS contexte d'empilement, donc ils se
         peignaient par-dessus la bulle (z 4), par-dessus l'image plein écran (z 3) ET par-dessus
         les effets d'écran (z 6) — le commentaire de la bulle affirmait pourtant l'inverse.
         `isolation` enferme les profondeurs d'auteur DANS le plateau : `--pz` continue de trier
         les personnages entre eux, et ne peut plus franchir les couches de l'interface. */
  #vn-stage { position:absolute; inset:0 0 22px 0; z-index:1; isolation:isolate; overflow:hidden;
    pointer-events:none; }   /* 22px = la marge sous la bulle : les pieds finissent DERRIÈRE elle,
                                jamais dans la bande qui la sépare du bas de l'écran. */
  .vn-sprite { position:absolute; left:var(--px,50%); bottom:var(--py,0%);
    /* v68 : 95 % du plateau — qui va maintenant jusqu'en bas. Le personnage touche le sol de
       l'écran, la bulle le coupe vers le genou, et il lui reste un doigt de ciel au-dessus de la
       tête. Mesuré en 1400x880 : 829 px de stature et 111 px de tête, contre 623 et 83 avant. */
    width:min(340px,30vw); height:95%; border-radius:130px 130px 16px 16px;
    background:linear-gradient(180deg, var(--sc,#888) 0%, rgba(10,12,20,.4) 74%, rgba(10,12,20,.7) 100%);
    box-shadow:inset 0 0 40px rgba(0,0,0,.5); opacity:.4; filter:grayscale(.55) brightness(.7);
    /* transform COMPOSÉ par variables : --fx/--fy = glissement (entrée + FLIP de recentrage), --fsc = échelle
       (actif / sortie). Anime le recentrage SANS écraser le lift de l'actif — aucun mouvement ne « saute ». */
    transform-origin:bottom center;
    transform:translateX(-50%) translateX(var(--fx,0)) translateY(var(--fy,0))
              translate(var(--ax,0), var(--ay,0)) rotate(var(--ar,0deg))
              scale(calc(var(--pe,1) * var(--fsc,1) * var(--az,1)));
    /* le miroir est une réflexion de l'IMAGE, pas du gabarit : appliqué plus bas sur .art img */
    transition:opacity .8s ease, transform .8s ease, left .55s ease, bottom .55s ease,
               filter .35s, box-shadow .35s; }
  .vn-sprite.art img { transform:scaleX(var(--pm,1)); }
  /* VN7.2 : scene dynamique — 2 emplacements RAPPROCHES du centre (petite marge de securite,
     jamais de chevauchement en taille normale). PLANS : le locuteur ACTIF passe au plan 2
     (devant), l'autre au plan 1 — un tres gros sprite peut deborder, l'actif reste devant. */
  /* v54 : la PROFONDEUR est explicite (`--pz` posé par l'auteur via z-index en ligne, base 10) ;
     l'ordre d'écriture ne décide plus de qui passe devant. L'actif gagne un cran, pas la première
     place : un personnage au fond (z négatif) reste au fond même quand il parle. */
  /* v80 : DANS une profondeur d'auteur, les personnages sont feuilletés par la RÉCENCE DE PAROLE
     (`--pr`, posé par `vnProfondeurs`) : celui qui parle devant, celui d'avant derrière lui, etc.
     Avant, seul le locuteur du moment gagnait un cran et tous les autres étaient à égalité — donc
     départagés par l'ordre du DOM, c'est-à-dire par leur ordre d'ARRIVÉE en scène.
     `--pr` reste sous 10 : il ne franchit jamais le pas qui sépare deux profondeurs d'auteur, donc
     un personnage que l'auteur a mis au fond y reste, même quand il parle. */
  .vn-sprite { z-index:calc(100 + var(--pz, 0) * 10 + var(--pr, 0)); }
  /* entrée : glissement COURT (220px) + fondu — jamais de grand écart vers les bords de l'écran */
  .vn-sprite.enter.l { --fx:-220px; opacity:0; }
  .vn-sprite.enter.r { --fx:220px; opacity:0; }
  /* ——— v54 : LE VOCABULAIRE D'ANIMATION. Toutes travaillent en DÉCALAGE (--ax/--ay/--ar/--az)
     par-dessus la place, jamais sur la place : le point de repos reste exact et deux animations
     successives ne dérivent pas. Durées courtes par défaut (--pd) : au-delà d'une demi-seconde,
     une animation devient un frein au rythme de lecture. */
  .vn-sprite.an-glisse  { transition:transform var(--pd,450ms) cubic-bezier(.22,.61,.36,1),
                                     left var(--pd,450ms) cubic-bezier(.22,.61,.36,1),
                                     bottom var(--pd,450ms) cubic-bezier(.22,.61,.36,1), opacity .35s; }
  .vn-sprite.an-fondu   { animation:vn-an-fondu var(--pd,350ms) ease both; }
  .vn-sprite.an-saut    { animation:vn-an-saut var(--pd,420ms) cubic-bezier(.3,.8,.4,1) both; }
  .vn-sprite.an-secousse{ animation:vn-an-secousse var(--pd,380ms) linear both; }
  .vn-sprite.an-tremble { animation:vn-an-tremble var(--pd,900ms) linear infinite; }
  .vn-sprite.an-respire { animation:vn-an-respire var(--pd,3600ms) ease-in-out infinite; }
  .vn-sprite.an-bascule { animation:vn-an-bascule var(--pd,500ms) ease-in-out both; }
  .vn-sprite.an-zoom    { animation:vn-an-zoom var(--pd,450ms) ease-out both; }
  @keyframes vn-an-fondu { from { opacity:0 } to { opacity:1 } }
  @keyframes vn-an-saut { 0%,100% { --ay:0px } 40% { --ay:-26px } 70% { --ay:-6px } }
  @keyframes vn-an-secousse { 0%,100% { --ax:0px } 15% { --ax:-9px } 35% { --ax:8px } 55% { --ax:-6px } 78% { --ax:4px } }
  @keyframes vn-an-tremble { 0%,100% { --ax:0px } 25% { --ax:-2px } 50% { --ax:2px } 75% { --ax:-1px } }
  /* l'idle est SOUS-DOSÉ à dessein : trop d'amplitude et le personnage a l'air de flotter */
  @keyframes vn-an-respire { 0%,100% { --az:1 } 50% { --az:1.004 } }
  @keyframes vn-an-bascule { 0%,100% { --ar:0deg } 30% { --ar:-2.4deg } 65% { --ar:1.6deg } }
  @keyframes vn-an-zoom { from { --az:.86 } to { --az:1 } }
  /* Les variables animées doivent être DÉCLARÉES typées, sinon Chrome ne les interpole pas. */
  @property --ax { syntax:"<length>"; initial-value:0px; inherits:false }
  @property --ay { syntax:"<length>"; initial-value:0px; inherits:false }
  @property --ar { syntax:"<angle>"; initial-value:0deg; inherits:false }
  @property --az { syntax:"<number>"; initial-value:1; inherits:false }
  /* ACCESSIBILITÉ — « mouvement réduit » n'est PAS une suppression : une secousse effacée ferait
     disparaître l'information dramatique. On garde un signal, en version dégradée et immobile. */
  @media (prefers-reduced-motion: reduce) {
    .vn-sprite { transition:opacity .2s ease, filter .2s; }
    .vn-sprite.an-saut, .vn-sprite.an-secousse, .vn-sprite.an-tremble,
    .vn-sprite.an-bascule, .vn-sprite.an-zoom, .vn-sprite.an-glisse { animation:none; }
    .vn-sprite.an-saut, .vn-sprite.an-secousse, .vn-sprite.an-bascule {
      animation:vn-an-signal .28s steps(2, jump-none) 2 both; }
    @keyframes vn-an-signal { from { filter:brightness(1.35) } to { filter:none } }
  }
  .vn-sprite.out { opacity:0 !important; z-index:1; --fy:6%; --fsc:.98; }
  .vn-sprite.active { opacity:1; filter:none; --fy:-2%; --fsc:1.04;
    box-shadow:inset 0 0 40px rgba(0,0,0,.4), 0 0 34px var(--sc); }
  .vn-sprite .head { position:absolute; top:7%; left:50%; transform:translateX(-50%); width:46%; aspect-ratio:1;
    border-radius:50%; background:rgba(255,255,255,.28); box-shadow:inset 0 6px 20px rgba(0,0,0,.35); }
  .vn-sprite .nm { position:absolute; bottom:12px; left:0; right:0; text-align:center; font-weight:bold;
    color:#fff; text-shadow:0 2px 6px #000; font-size:15px; }
  /* VN7 : VRAI sprite (art) — l'image remplace la silhouette, meme gabarit de scene.
     Inactif : attenue mais bien PRESENT (moins fantomatique que la silhouette). */
  .vn-sprite.art { background:none; box-shadow:none; border-radius:0; width:auto;
    opacity:.62; filter:grayscale(.3) brightness(.78); }
  .vn-sprite.art img { height:100%; width:auto; max-width:min(600px,42vw); object-fit:contain;   /* PC : sprites ~+50% (mobile réduit plus bas) */
    object-position:bottom; display:block; filter:drop-shadow(0 6px 18px rgba(0,0,0,.55)); }
  /* v80 : un cadrage SERRÉ (buste / visage, cf. le suffixe du fichier) s'accroche par le HAUT —
     sa tête retombe là où elle serait sur le corps entier. Sans ça, un buste carré, peint au bas
     d'une boîte plus haute que lui, avait la tête 253 px sous celle de son voisin en plein pied.
     Un buste en 2:3 remplit sa boîte : la règle ne lui change rien. */
  .vn-sprite.art.pl-serre img { object-position:top; }
  .vn-sprite.art.active { opacity:1; box-shadow:none; filter:drop-shadow(0 0 22px var(--sc)); }
  /* transition IRIS (asset fade converti en voile alpha) : cligne au CHANGEMENT DE LIEU */
  #vn-veil { position:absolute; inset:-55%; z-index:1; background-size:100% 100%;
    opacity:0; pointer-events:none; }
  #vn-veil.blink { animation:vn-blink var(--fxd,1.05s) ease-in-out; }   /* v65 : durée du beat */
  @keyframes vn-blink { 0%{opacity:0} 40%{opacity:var(--fxo,.97)} 60%{opacity:var(--fxo,.97)} 100%{opacity:0} }
  /* ---- GABARIT bulle VN : taille FIXE, prête à recevoir une IMAGE (via --vn-bubble). Narration et dialogue
         partagent la MÊME bulle ; seul l'encart du nom au-dessus change. Le texte est PAGINÉ pour toujours tenir. ---- */
  /* PAS d'overflow:hidden ici : l'encart du nom depasse AU-DESSUS de la bulle par design
     (top negatif) — c'est #vn-text qui clippe le texte pagine. */
  #vn-box { position:absolute; left:50%; bottom:22px; transform:translateX(-50%); width:min(880px,94%);
    height:176px; box-sizing:border-box; z-index:4; display:flex; flex-direction:column;   /* DEVANT les sprites (z≤2) */
    /* v68 : OPAQUE. Elle était « nettement transparente, façon VN » — ce qui n'a de sens que si
       rien ne passe derrière. Maintenant que le personnage y descend, une bulle translucide
       laisserait voir ses jambes sous le texte. `--vn-bubble` reste le point d'entrée d'un monde
       qui voudrait sa propre image de bulle. */
    background:var(--vn-bubble, linear-gradient(180deg, #101420, #090c14));
    background-size:cover; background-position:center;
    border:1px solid rgba(140,180,240,.5); border-radius:16px; padding:18px 28px 14px;
    box-shadow:0 8px 40px rgba(0,0,0,.55), inset 0 0 30px rgba(60,100,180,.12);
    cursor:pointer; }        /* v68 : plus de backdrop-filter — il ne floutait plus rien sous un fond opaque */
  #vn-name { position:absolute; top:-16px; left:22px; padding:3px 16px; border-radius:8px; font-weight:bold;
    font-size:15px; color:#fff; background:#3a4a66; box-shadow:0 2px 10px rgba(0,0,0,.5);
    border:1px solid rgba(255,255,255,.28); text-shadow:0 1px 3px rgba(0,0,0,.5); }
  #vn-name.hidden { display:none; }
  #vn-text { flex:1; min-height:0; overflow:hidden; font-size:calc(19px * var(--vn-scale, 1)); line-height:1.72; letter-spacing:.03em;
    color:#eef2f8; white-space:pre-wrap; font-family:var(--vn-font, inherit); }   /* AÉRÉ : interligne + interlettrage (toutes polices).
    Taille pilotée par --vn-scale (réglage « Taille du texte » du compte) — la bulle est fixe, la pagination re-mesure.
    Police VN choisie (compte) — la pagination re-mesure après chargement, donc rien ne déborde. */
  #vn-text.pop { animation:vnPop .18s ease; }   /* VN6 : apparition douce de chaque page */
  @keyframes vnPop { from { opacity:0; transform:translateY(3px); } to { opacity:1; } }
  #vn-text.narr { font-style:italic; color:#dce3ee; }
  #vn-text.user { color:#dbe7d5; }                              /* replique du joueur (relecture) */
  #vn-text.dice { font-family:ui-monospace,monospace; font-size:calc(16px * var(--vn-scale, 1)); color:#e8d78a; }   /* ligne de des */
  /* v66 — LA RÉGIE de « Simuler ». Elle ne doit JAMAIS pouvoir se confondre avec du récit : autre
     famille de caractères, autre couleur, et un liseré à gauche. Elle n'existe que dans l'apercu. */
  #vn-text.regie { font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
    font-size:calc(15.5px * var(--vn-scale, 1)); line-height:1.6; letter-spacing:0;
    color:#cfc2f2; border-left:3px solid #7a63c9; padding-left:12px; font-style:normal; }
  #vn-hint.vn-hint-say { color:#ffd98a; opacity:1; }          /* v66 : un mot du lecteur (⏭ refusé) */
  #vn-end.off { opacity:.35; }                                /* v66 : scène imposée -> ⏭ inopérant */
  #vn-hint { align-self:flex-end; margin-top:4px; font-size:12px; color:#8fa0bd; opacity:.85; pointer-events:none; }
  #vn-exit { position:absolute; top:12px; right:14px; z-index:2; background:rgba(0,0,0,.45); color:#cdd6e6;
    border:1px solid var(--line); border-radius:8px; padding:5px 12px; cursor:pointer; font:inherit; }
  #vn-reply { position:absolute; left:50%; bottom:22px; transform:translateX(-50%); width:min(880px,94%);
    display:none; gap:8px; z-index:4; }
  #vn.await #vn-reply { display:flex; }
  #vn.await #vn-box { display:none; }
  #vn-reply input { flex:1; background:rgba(12,16,26,.92); color:#eef2f8; border:1px solid rgba(140,180,240,.5);
    border-radius:10px; padding:12px 14px; font:inherit; font-size:16px; }
  #vn-reply button { background:#3a4a66; color:#fff; border:none; border-radius:10px; padding:0 18px; cursor:pointer; font:inherit; }
  /* ---- PC HAUTE RÉSOLUTION : la scène s'ADAPTE — grand écran = bulle + police + sprites plus
     grands (sinon une bulle de 880px et 19px de texte semblent minuscules sur du 1440p/4K). Le
     gabarit de base garde sa hauteur FIXE (contrat image) ; on ne fait que le monter par paliers. ---- */
  @media (min-width:1500px){
    #vn-box { width:min(1120px,82%); height:224px; padding:22px 36px 16px; }
    #vn-text { font-size:calc(23px * var(--vn-scale, 1)); line-height:1.72; }
    #vn-name { font-size:17px; top:-18px; }
    #vn-hint { font-size:13px; }
    #vn-loc { font-size:13px; letter-spacing:.2em; }
    #vn-clock { font-size:12px; }
    #vn-reply { width:min(1120px,82%); }
    #vn-reply input { font-size:18px; padding:14px 18px; }
    .vn-sprite.art img { max-width:min(700px,40vw); }
  }
  @media (min-width:2100px){
    #vn-box { width:min(1400px,72%); height:272px; padding:28px 44px 20px; }
    #vn-text { font-size:calc(28px * var(--vn-scale, 1)); line-height:1.75; }
    #vn-name { font-size:20px; top:-21px; padding:4px 20px; }
    #vn-hint { font-size:15px; }
    #vn-loc { font-size:15px; }
    #vn-clock { font-size:13px; }
    #vn-reply { width:min(1400px,72%); }
    #vn-reply input { font-size:21px; padding:16px 20px; }
    .vn-sprite.art img { max-width:min(840px,38vw); }
  }
  @media (min-width:3200px){   /* 4K (3840px+) : sinon la bulle de 1400px et 28px de texte redeviennent petits */
    #vn-box { width:min(2050px,62%); height:348px; padding:36px 56px 26px; }
    #vn-text { font-size:calc(37px * var(--vn-scale, 1)); line-height:1.75; }
    #vn-name { font-size:27px; top:-27px; padding:6px 28px; }
    #vn-hint { font-size:20px; }
    #vn-loc { font-size:20px; }
    #vn-clock { font-size:17px; }
    #vn-reply { width:min(2050px,62%); }
    #vn-reply input { font-size:27px; padding:20px 26px; }
    .vn-sprite.art img { max-width:min(1180px,34vw); }
  }
  /* ---- TELEPHONE (<=760px) : le VN prend TOUT l'ecran (il couvre le topbar, ✕ pour sortir),
     UN seul sprite (gere en JS, vnStageMax), plus GRAND et centre ; controles compacts qui ne
     chevauchent plus Quitter ; bulle adaptee ; decor en cover centre (crop lateral assume). ---- */
  @media (max-width:760px){
    #vn { inset:0; }
    #vn-ctl { gap:4px; left:8px; top:8px; max-width:calc(100% - 70px); flex-wrap:wrap; }
    #vn-ctl button { padding:4px 8px; font-size:12px; }
    #vn-exit { top:8px; right:8px; font-size:0; padding:6px 10px; }
    #vn-exit::before { content:"✕"; font-size:15px; }
    #vn-loc { top:44px; padding:0 56px; font-size:10.5px; letter-spacing:.12em; }
    .vn-sprite { width:min(260px,58vw); height:88%; }
    .vn-sprite.art img { max-width:80vw; }
    .vn-sprite.enter.l { --fx:-70vw; }
    .vn-sprite.enter.r { --fx:70vw; }
    #vn-box { height:184px; padding:14px 17px 12px; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); }
    #vn-stage { inset:0 0 calc(8px + env(safe-area-inset-bottom, 0px)) 0; }   /* v68 : le bas de la bulle */
    #vn-text { font-size:calc(16px * var(--vn-scale, 1)); line-height:1.62; }
    #vn-text.dice { font-size:calc(13.5px * var(--vn-scale, 1)); }
    #vn-name { left:12px; top:-14px; font-size:13.5px; padding:2px 12px; }
    #vn-log { inset:44px 8px 175px; }
    #vn-log-list { padding:10px 12px; }
    #vn-reply { bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:calc(100% - 16px); }
  }
  /* VN6 : accessibilite — pas d'animations si l'OS demande la reduction de mouvement */
  @media (prefers-reduced-motion: reduce){
    .vn-bg-layer, #vn-loc { transition:none; }
    #vn-text.pop { animation:none; }
    #vn-veil.blink { animation:none; }
    .vn-sprite { transition:filter .25s, box-shadow .25s; }
  }

  /* ===== SKIN « Cristal » — bulle de dialogue VN (cadran façon JRPG cyan). Le FOND reste piloté par
     --vn-bubble (déjà personnalisable : image / monde plus tard) ; on n'ajoute QUE le cadre orné + la
     plaque de nom, theme-aware (prend var(--accent)). Toujours 100 % CSS, aucun asset. ===== */
  [data-skin="cristal"] #vn-box {
    border:1px solid color-mix(in srgb, var(--accent) 70%, rgba(140,180,240,.5)); border-radius:13px;
    box-shadow:0 8px 40px rgba(0,0,0,.55), 0 0 24px -5px var(--accent),
      inset 0 0 34px -8px color-mix(in srgb, var(--accent) 34%, transparent); }
  [data-skin="cristal"] #vn-box::before {   /* double liseré d'accent, en retrait du bord */
    content:""; position:absolute; inset:5px; pointer-events:none; border-radius:9px;
    border:1px solid color-mix(in srgb, var(--accent) 62%, transparent);
    box-shadow:inset 0 0 26px -13px var(--accent); }
  [data-skin="cristal"] #vn-box::after {    /* équerres de coin (à droite) */
    content:""; position:absolute; inset:4px; pointer-events:none;
    background:
      linear-gradient(var(--accent),var(--accent)) 100% 0 / 26px 2px no-repeat,
      linear-gradient(var(--accent),var(--accent)) 100% 0 / 2px 26px no-repeat,
      linear-gradient(var(--accent),var(--accent)) 100% 100% / 26px 2px no-repeat,
      linear-gradient(var(--accent),var(--accent)) 100% 100% / 2px 26px no-repeat;
    filter:drop-shadow(0 0 3px var(--accent)); }
  [data-skin="cristal"] #vn-name {          /* PLAQUE de nom biseautée + glow (élément signature) */
    background:linear-gradient(120deg, color-mix(in srgb, var(--accent) 52%, var(--panel)), color-mix(in srgb, var(--accent) 22%, var(--panel)));
    border:1px solid color-mix(in srgb, var(--accent) 78%, transparent); border-radius:0; padding-right:26px;
    clip-path:polygon(0 0, 100% 0, calc(100% - 13px) 100%, 0 100%);
    box-shadow:0 2px 10px rgba(0,0,0,.5), 0 0 13px -3px var(--accent); }

  /* ═══ v55 — LE TEXTE RICHE D'AUTEUR ══════════════════════════════════════════════════════
     Rendu à partir de NŒUDS construits par le lecteur (jamais d'innerHTML sur du texte d'auteur).
     Toutes les tailles sont RELATIVES (em) : elles survivent au changement de police, au zoom et
     au DPI, contrairement à un décalage en pixels. Les couleurs sont des JETONS du thème : un bleu
     à 4,6:1 sur fond clair tombe à 2,3:1 sur fond sombre — une couleur écrite en dur dans une
     scène ne survivrait ni au thème ni au mode fort contraste. */
  .rt { white-space: inherit; }
  .rt-g { font-weight: 700; }
  .rt-i { font-style: italic; }
  .rt-nb { white-space: nowrap; }
  /* BOUTEN — l'italique du japonais : on n'y incline pas les caractères, on les pointille.
     `text-emphasis` n'est pas partout : le repli en soulignement pointillé garde l'intention. */
  .rt-em { text-emphasis: filled sesame; -webkit-text-emphasis: filled sesame;
    text-emphasis-position: over right; -webkit-text-emphasis-position: over right; }
  @supports not (text-emphasis: filled sesame) {
    .rt-em { text-decoration: underline dotted; text-underline-offset: .25em; }
  }
  /* JETONS de couleur — chacun a un SENS, pas une teinte ; le thème les résout. */
  /* v64 — LES HUIT JETONS SONT ÉCRITS POUR LA BULLE, PAS POUR LE SITE. Quatre d'entre eux suivaient
     le thème du site (`--accent`, `--no`, `--dim`, `--ok`) alors que la bulle, elle, reste TOUJOURS
     sombre (`--vn-bubble`, plus haut). En thème « clair » ils s'assombrissaient donc sur un fond qui
     ne s'éclaircissait pas : mesuré 3,45 pour {couleur=accent}, 3,46 pour {couleur=danger} et 2,71
     pour {couleur=murmure} — sous le seuil AA de 4,5. C'est exactement le mal que la doctrine du
     module dit vouloir empêcher, à l'envers : le moteur garantit la LISIBILITÉ, l'auteur choisit
     l'INTENTION. La bulle VN est sa propre surface ; ses couleurs sont fixes, comme les quatre
     autres l'étaient déjà, et toutes tiennent au-dessus de 5,5 même sur un décor clair qui
     transparaît. */
  .rt-c-accent  { color: #8dbcff; }
  .rt-c-danger  { color: #ff9aa2; }
  .rt-c-calme   { color: #8fb8d8; }
  .rt-c-murmure { color: #a7aeba; }
  .rt-c-systeme { color: #cfd6e4; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .94em; }
  .rt-c-joie    { color: #6ee0ad; }
  .rt-c-froid   { color: #a8c6e8; }
  .rt-c-chaud   { color: #e8b57a; }
  /* RUBY — le lecteur RÉSERVE la hauteur de ligne tout seul : un auteur ne doit pas avoir à y
     penser, et il oublierait. Sans cette réserve, la lecture chevauche la ligne du dessus. */
  #vn-text ruby, .vn-log-item ruby { ruby-position: over; }
  #vn-text ruby rt, .vn-log-item ruby rt { font-size: .5em; opacity: .85; line-height: 1; }
  #vn-text:has(ruby), .vn-log-item .tx:has(ruby) { line-height: 2.05; }
  /* MOUVEMENT — intensités NOMMÉES, jamais de paramètres libres : amp/freq invitent aux valeurs
     nauséeuses. Chacune a son repli STATIQUE, utilisé au « mouvement réduit » et dans le backlog. */
  .rt-secoue { display: inline-block; animation: rt-secoue var(--rtd, .32s) linear infinite; }
  .rt-onde   { display: inline-block; animation: rt-onde var(--rtd, 1.6s) ease-in-out infinite; }
  .rt-secoue.rt-i-faible { --rta: .6px; --rtd: .42s; }
  .rt-secoue.rt-i-moyen  { --rta: 1.3px; --rtd: .32s; }
  .rt-secoue.rt-i-fort   { --rta: 2.4px; --rtd: .22s; }
  .rt-onde.rt-i-faible { --rta: .12em; --rtd: 2.1s; }
  .rt-onde.rt-i-moyen  { --rta: .2em; --rtd: 1.6s; }
  .rt-onde.rt-i-fort   { --rta: .32em; --rtd: 1.1s; }
  @keyframes rt-secoue { 0%,100%{transform:translate(0,0)} 25%{transform:translate(var(--rta,1px),calc(var(--rta,1px) * -1))}
    50%{transform:translate(calc(var(--rta,1px) * -1),var(--rta,1px))} 75%{transform:translate(var(--rta,1px),var(--rta,1px))} }
  @keyframes rt-onde { 0%,100%{transform:translateY(0)} 50%{transform:translateY(calc(var(--rta,.2em) * -1))} }
  /* dans le BACKLOG et au « mouvement réduit », le repli statique : l'intention reste lisible
     (le texte garde son relief) mais plus rien ne bouge. */
  .rt-fige, .vn-log-item .rt-secoue, .vn-log-item .rt-onde { animation: none !important; font-style: italic; }
  @media (prefers-reduced-motion: reduce) {
    .rt-secoue, .rt-onde { animation: none !important; font-style: italic; }
  }
  /* MACHINE À ÉCRIRE — le texte est posé EN ENTIER (donc mesuré et paginé une fois pour toutes),
     puis la partie non révélée est masquée. La mise en page ne bouge jamais pendant la frappe. */
  .rt-cache { visibility: hidden; }
  /* v60 — LE RYTHME. Un marqueur {attend} / {pause} / {vite} est une ANCRE sans contenu : elle tient
     sa place dans l'ordre du texte pour la machine à écrire, et ne pèse pas un pixel dans la bulle. */
  .rt-m { display: inline; font-size: 0; line-height: 0; }
  /* v60 — le beat ⏸ Pause : la bulle s'efface (personne ne parle), seul l'indice « suite » reste.
     Le cadre garde sa place pour que la scène ne saute pas sous les yeux du joueur. */
  #vn-box.vn-wait { background: transparent; border-color: transparent; box-shadow: none; }
  #vn-box.vn-wait #vn-text { min-height: 0; }
  /* frappe SUSPENDUE au milieu d'une bulle : l'indice « suite » bat, pour que le joueur comprenne
     que c'est à lui de reprendre — sans ce signal, un {attend} ressemble à un plantage. */
  #vn.rt-hold #vn-hint { animation: vn-hold-bat 1s ease-in-out infinite; }
  @keyframes vn-hold-bat { 0%,100%{ opacity:.35; transform:translateY(0) } 50%{ opacity:1; transform:translateY(2px) } }
  @media (prefers-reduced-motion: reduce) { #vn.rt-hold #vn-hint { animation: none; opacity: 1; } }
  /* lu par la synthèse vocale, pas montré */
  .rt-lu { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* v62 — SIMULATION : une option SOUS CONDITION se voit. En jeu elle n'apparaît que si sa condition
   est vraie ; dans « Simuler » on les montre toutes (l'auteur doit pouvoir marcher chaque branche),
   alors il faut dire lesquelles sont conditionnelles — sans quoi il valide un menu que le jeu ne
   montrera pas tel quel. Liseré pointillé + un losange discret, jamais un texte qui déborde. */
  .vn-choice.vn-choice-cond { border-style: dashed; }
  .vn-choice.vn-choice-cond::after { content: "◈"; margin-left: .5em; opacity: .55; font-size: .85em; }
