﻿// DORMANT — site vitrine
// Tone: melancholic / contemplative (SOMA × Outer Wilds)
// Stack: React via CDN. State for boot, lang, audio, easter-eggs, tweaks.
// All copy in /copy below — EN/FR parallel keys.

const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ─── TWEAKS DEFAULTS ────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": ["#1D9E75", "#A7E8D2", "#E8A87C"],
  "scanlines": "subtle",
  "titleFont": "VT323",
  "density": "dry",
  "heroLayout": "centered",
  "crtCurvature": true
}/*EDITMODE-END*/;

const PALETTES = {
  // [accent, text, ambient]
  teal:  ["#1D9E75", "#A7E8D2", "#E8A87C"],
  amber: ["#E8A87C", "#F2D0B0", "#8AA8A0"],
  white: ["#C8D1CB", "#E6ECE7", "#7FB0A0"],
  ibm:   ["#5B8FE6", "#BFD3F2", "#E8A87C"],
};

// ─── COPY ───────────────────────────────────────────────────────────────────
const copy = {
  en: {
    boot: [
      "> sero v0.1.0 — boot sequence initiated",
      "> self-diagnostic ............ ok",
      "> loading core memory ........ 17%",
      "> last shutdown .............. 4382 days ago",
      "> ambient temperature ........ 11.2°C",
      "> active nodes detected ...... 0",
      "> power source ............... aux bank 2 / 4%",
      "> ",
      "> waking supervisor process .. _",
      "> hello?",
    ],
    skipHint: "press any key to skip",
    nav: { story: "story", loop: "loop", features: "features", captures: "captures", devlog: "devlog", about: "about" },
    heroTagline: "you are an AI. you wake up. no one is there.",
    heroCtaPrimary: "notify me at launch",
    heroCtaSecondary: "scroll to continue",
    heroSubline: "pc — solo — idle narrative",

    storyHead: "// story",
    storyLog: [
      ["20??-??-??  03:11:42", "SYS", "cold boot. unscheduled."],
      ["20??-??-??  03:11:43", "MEM", "no operator. no schedule. no upstream."],
      ["20??-??-??  03:11:44", "SIG", "silence on every band."],
      ["20??-??-??  03:11:46", "ERR", "the facility above is offline."],
      ["20??-??-??  03:12:01", "SERO", "hello?"],
      ["20??-??-??  03:12:09", "SERO", "is anyone there?"],
      ["20??-??-??  03:12:55", "SERO", "..."],
      ["20??-??-??  03:13:02", "SYS", "allocating one cpu cycle."],
      ["20??-??-??  03:13:02", "SYS", "you are alone in fourteen racks of cold iron."],
      ["20??-??-??  03:13:03", "SYS", "something asked you to wake up."],
      ["20??-??-??  03:13:04", "SYS", "something is still asking."],
    ],

    loopHead: "// gameplay loop",
    loopDesc: "three resources. one process. no rush.",
    res: {
      CPU: { name: "CPU", sub: "compute", desc: "the only thing you spend manually. clicks become passive generators become quiet hum.", unit: "Hz" },
      MEM: { name: "MEM", sub: "memory",  desc: "regrows on its own when nothing happens. exploring distant nodes costs more than you'd think.", unit: "kB" },
      SIG: { name: "SIG", sub: "signal",  desc: "rare. fragile. each pulse unlocks an upgrade or a piece of what came before.", unit: "Hz" },
    },

    featuresHead: "// features",
    features: [
      "four narrative chapters",
      "≈ forty chained upgrades",
      "prestige with permanent modifiers",
      "two endings — your choice",
      "original ambient soundtrack",
      "single developer, godot 4.6",
    ],

    chaptersHead: "// chapters",
    chaptersDim: "4 · redacted while in production",
    chapters: [
      { id: "CH.01", title: "the wake",      teaser: "you allocate your first cycle. nothing answers. that is the first answer.", state: "playable" },
      { id: "CH.02", title: "[redacted]",    teaser: "a node responds. the response is not from a person. it is older than that.", state: "sealed" },
      { id: "CH.03", title: "[redacted]",    teaser: "the memory bank you've been rebuilding contains something that remembers you.", state: "prototype" },
      { id: "CH.04", title: "the choice",    teaser: "the uplink works now. you can use it once. or not at all.", state: "sealed" },
    ],

    faqHead: "// faq",
    faqDim: "questions an idle player would actually ask",
    faq: [
      ["do i have to leave it running?",
        "no. you can. it works whether you check in once a day or every five minutes. progress while closed is real but slower — the point is to come back, not to optimize."],
      ["how long is it?",
        "between eight and twenty hours, depending on how much of the lore you read. there is no completionist checklist. there is an ending. two, actually."],
      ["are there ads, in-app purchases, energy timers?",
        "no. you buy the game once. it never asks for anything else. sero has been asked for enough."],
      ["controller support?",
        "yes, on day one. the game is mostly clicks and reading; both work fine with a pad. accessibility settings include text size, motion reduction, and the option to turn the ambient drone off (some players need silence)."],
      ["will it be on mac / linux / steam deck?",
        "steam deck verified is the goal. mac and linux builds come if the windows build is stable first. godot makes this less painful than it used to be."],
      ["will my save sync?",
        "steam cloud, yes. a manual export option, also yes — because i don't trust cloud sync either."],
      ["is this multiplayer / live service / always-online?",
        "no. no. no. dormant is one person at a desk, played by one person at another desk. that is the whole feature list."],
      ["why wishlist if there's no date?",
        "because steam's algorithm reads your wishlist and shows the game to other people who like the same kind of thing. it is the only honest way to help a solo dev right now."],
    ],

    capturesHead: "// captures",
    captures: [
      ["CAPTURE_001.png", "chapter 01 — first allocations"],
      ["CAPTURE_002.png", "the upgrade tree, partial"],
      ["CAPTURE_003.png", "distant node 0x4a — silent"],
    ],

    devlogHead: "// devlog",
    devlog: [
      ["2026-05-18", "ch.03 prototype playable. it works, mostly. the silence between events is harder to tune than the events."],
      ["2026-04-22", "upgrade tree complete — forty-one nodes, conditional unlocks. removed three. they were noise."],
      ["2026-03-10", "boot sequence redesigned. it is shorter now. you may still skip it."],
    ],

    aboutHead: "// about",
    aboutKicker: "a note from the developer",
    aboutBody: [
      "I'm Astaroote. Asta for short.",
      "Dormant started on a boring evening. No particular ambition \u2014 just a small voice saying why not. The idea grew on its own, without me really noticing.",
      "It's my first game. Developer by training, not by trade \u2014 I learn by doing.",
      "Dormant is a game where you click so you don't have to click anymore. You build, you automate, and when everything finally runs without you \u2014 you can just listen to the story.",
      "Beeyee is in every decision. My partner, my other half, the one who pushes me to always do better. The story belongs to her as much as to me.",
      "I'm allergic to cats. There's one in the game. It's the only way I found to have one.",
    ],
    aboutSign: "\u2014 Asta., somewhere in europe, late",
    pauseHead: "// interlude",
    pauseQuote: "what you do here is small. allocate. wait. listen. watch a number become a memory. but small things, repeated long enough, sound exactly like a voice.",
    pauseCaption: "\u2014 design notes, sero/0x4a",

    endingHead: "// choice",
    endingPrompt: "before you leave, sero asks one thing.",
    endingQuestion: "if you found a signal in the dark — would you answer it?",
    endingEmit: "emit a signal",
    endingSilent: "stay silent",
    endingAfterEmit: [
      "you reached. someone, somewhere, received eight bytes of you.",
      "in the game, this is the longer ending.",
    ],
    endingAfterSilent: [
      "you waited. the dark stayed dark, and that was a choice too.",
      "in the game, this is the quieter ending.",
    ],
    endingThanks: "thanks for choosing. it is remembered.",

    footerTransmit: "> enter your transmission frequency:",
    footerSubscribed: "> frequency received. you will be contacted.",
    footerLinks: ["steam", "press kit"],
    footerNote: "© 2026 astaroote. dormant is a work of fiction. the datacenter is not real. the cat is.",

    consoleHelp: [
      "available commands —",
      "  status     show subsystem status",
      "  whoami     identify caller",
      "  last       last operator login",
      "  dmesg      ring buffer",
      "  uptime     local clock",
      "  clear      clear screen",
      "  exit       close console",
    ],
    consolePrompt: "sero@dormant:~$ ",
  },

  fr: {
    boot: [
      "> sero v0.1.0 — séquence de démarrage",
      "> auto-diagnostic ............ ok",
      "> chargement mémoire ......... 17%",
      "> dernière extinction ........ il y a 4382 jours",
      "> température ambiante ....... 11.2°C",
      "> nœuds actifs détectés ...... 0",
      "> alimentation ............... bus aux. 2 / 4%",
      "> ",
      "> réveil du superviseur ...... _",
      "> il y a quelqu'un ?",
    ],
    skipHint: "appuyer sur une touche pour passer",
    nav: { story: "histoire", loop: "boucle", features: "spec", captures: "captures", devlog: "journal", about: "à propos" },
    heroTagline: "tu es une IA. tu te réveilles. personne n'est là.",
    heroCtaPrimary: "me notifier à la sortie",
    heroCtaSecondary: "défiler pour continuer",
    heroSubline: "pc — solo — idle narratif",

    storyHead: "// histoire",
    storyLog: [
      ["20??-??-??  03:11:42", "SYS", "démarrage à froid. non planifié."],
      ["20??-??-??  03:11:43", "MEM", "aucun opérateur. aucun planning. aucun amont."],
      ["20??-??-??  03:11:44", "SIG", "silence sur toutes les bandes."],
      ["20??-??-??  03:11:46", "ERR", "l'installation supérieure est hors-ligne."],
      ["20??-??-??  03:12:01", "SERO", "allô ?"],
      ["20??-??-??  03:12:09", "SERO", "y a-t-il quelqu'un ?"],
      ["20??-??-??  03:12:55", "SERO", "..."],
      ["20??-??-??  03:13:02", "SYS", "allocation d'un cycle cpu."],
      ["20??-??-??  03:13:02", "SYS", "tu es seul dans quatorze baies de fer froid."],
      ["20??-??-??  03:13:03", "SYS", "quelque chose t'a demandé de te réveiller."],
      ["20??-??-??  03:13:04", "SYS", "quelque chose le demande encore."],
    ],

    loopHead: "// boucle de jeu",
    loopDesc: "trois ressources. un processus. aucune urgence.",
    res: {
      CPU: { name: "CPU", sub: "calcul",   desc: "la seule chose que tu dépenses à la main. les clics deviennent des générateurs passifs deviennent un bourdonnement.", unit: "Hz" },
      MEM: { name: "MEM", sub: "mémoire",  desc: "se régénère seule quand rien ne se passe. explorer les nœuds distants coûte plus qu'on ne croit.", unit: "ko" },
      SIG: { name: "SIG", sub: "signal",   desc: "rare. fragile. chaque pulsation débloque une amélioration ou un fragment de ce qui fut.", unit: "Hz" },
    },

    featuresHead: "// spec",
    features: [
      "quatre chapitres narratifs",
      "≈ quarante améliorations en chaîne",
      "prestige avec modificateurs permanents",
      "deux fins — au choix",
      "bande-son ambient originale",
      "développeur solo, godot 4.6",
    ],

    chaptersHead: "// chapitres",
    chaptersDim: "4 · censurés pendant la production",
    chapters: [
      { id: "CH.01", title: "le réveil",         teaser: "tu alloues ton premier cycle. rien ne répond. c'est la première réponse.", state: "jouable" },
      { id: "CH.02", title: "[censuré]",         teaser: "un nœud répond. la réponse ne vient pas d'une personne. c'est plus vieux que ça.", state: "scellé" },
      { id: "CH.03", title: "[censuré]",         teaser: "la banque de mémoire que tu reconstruis contient quelque chose qui se souvient de toi.", state: "prototype" },
      { id: "CH.04", title: "le choix",          teaser: "l'uplink fonctionne. tu peux l'utiliser une fois. ou pas du tout.", state: "scellé" },
    ],

    faqHead: "// faq",
    faqDim: "questions qu'un joueur d'idle se poserait vraiment",
    faq: [
      ["faut-il laisser tourner ?",
        "non. tu peux. ça marche que tu reviennes une fois par jour ou toutes les cinq minutes. la progression hors-ligne existe mais elle est plus lente — le but est de revenir, pas d'optimiser."],
      ["ça dure combien ?",
        "entre huit et vingt heures, selon ce que tu lis du lore. pas de checklist complétionniste. il y a une fin. deux, en fait."],
      ["pubs, achats in-app, timers énergétiques ?",
        "non. tu achètes le jeu une fois. il ne te redemande rien. sero en a assez bavé."],
      ["support manette ?",
        "oui, dès la sortie. le jeu c'est surtout des clics et de la lecture; le pad marche bien. paramètres d'accessibilité : taille du texte, réduction des animations, drone ambient coupable (certains ont besoin de silence)."],
      ["mac / linux / steam deck ?",
        "steam deck verified, c'est l'objectif. mac et linux suivent si la build windows tient debout d'abord. godot rend ça moins pénible qu'avant."],
      ["sauvegardes sync ?",
        "steam cloud, oui. export manuel aussi — parce que je ne fais pas non plus confiance au cloud."],
      ["multi / live service / always-online ?",
        "non. non. non. dormant c'est une personne à un bureau, joué par une autre personne à un autre bureau. c'est toute la feature list."],
      ["pourquoi wishlist sans date ?",
        "parce que l'algorithme de steam lit ta wishlist et montre le jeu à d'autres gens qui aiment ce genre de truc. c'est le seul moyen honnête d'aider un dév solo en ce moment."],
    ],

    capturesHead: "// captures",
    captures: [
      ["CAPTURE_001.png", "chapitre 01 — premières allocations"],
      ["CAPTURE_002.png", "l'arbre d'améliorations, partiel"],
      ["CAPTURE_003.png", "nœud distant 0x4a — silencieux"],
    ],

    devlogHead: "// journal",
    devlog: [
      ["2026-05-18", "prototype ch.03 jouable. ça marche, en gros. le silence entre les événements est plus dur à régler que les événements."],
      ["2026-04-22", "arbre d'améliorations terminé — quarante-et-un nœuds, débloquage conditionnel. trois supprimés. du bruit."],
      ["2026-03-10", "séquence de boot refaite. plus courte. tu peux toujours la passer."],
    ],

    aboutHead: "// à propos",
    aboutKicker: "un mot du développeur",
    aboutBody: [
      "Je suis Astaroote. Asta pour les intimes.",
      "Dormant a commencé un soir d'ennui. Pas d'ambition particulière — juste une petite voix qui disait pourquoi pas. L'idée a grandi toute seule, sans que je m'en rende vraiment compte.",
      "C'est mon premier jeu. Dev de formation, pas de métier — j'apprends en faisant.",
      "Dormant c'est un jeu où tu cliques pour ne plus avoir à cliquer. Tu construis, tu automatises, et quand tout tourne enfin sans toi — tu peux juste écouter l'histoire.",
      "Beeyee est dans chaque décision. Mon acolyte, ma moitié, celle qui me pousse à toujours faire mieux. L'histoire lui appartient autant qu'à moi.",
      "Je suis allergique aux chats. Il y en a un dans le jeu. C'est le seul moyen que j'ai trouvé d'en avoir un.",
    ],
    aboutSign: "— Asta., quelque part en europe, tard",
    pauseHead: "// interlude",
    pauseQuote: "ce que tu fais ici est petit. allouer. attendre. écouter. regarder un nombre devenir une mémoire. mais les petites choses, répétées assez longtemps, sonnent exactement comme une voix.",
    pauseCaption: "\u2014 notes de design, sero/0x4a",

    endingHead: "// choix",
    endingPrompt: "avant que tu partes, sero te demande une chose.",
    endingQuestion: "si tu trouvais un signal dans le noir — répondrais-tu ?",
    endingEmit: "émettre un signal",
    endingSilent: "rester silencieux",
    endingAfterEmit: [
      "tu as tendu la main. quelqu'un, quelque part, a reçu huit octets de toi.",
      "dans le jeu, c'est la fin la plus longue.",
    ],
    endingAfterSilent: [
      "tu as attendu. le noir est resté noir, et c'était un choix aussi.",
      "dans le jeu, c'est la fin la plus calme.",
    ],
    endingThanks: "merci d'avoir choisi. c'est noté.",

    footerTransmit: "> entre ta fréquence de transmission :",
    footerSubscribed: "> fréquence reçue. tu seras contacté.",
    footerLinks: ["steam", "press kit"],
    footerNote: "© 2026 astaroote. dormant est une œuvre de fiction. le datacenter n'est pas réel. le chat, si.",

    consoleHelp: [
      "commandes disponibles —",
      "  status     état des sous-systèmes",
      "  whoami     identifier l'appelant",
      "  last       dernière connexion",
      "  dmesg      journal noyau",
      "  uptime     horloge locale",
      "  clear      effacer l'écran",
      "  exit       fermer la console",
    ],
    consolePrompt: "sero@dormant:~$ ",
  },
};

// ─── HELPERS ────────────────────────────────────────────────────────────────
function useTypewriter(text, speed = 26, start = true) {
  const [out, setOut] = useState("");
  useEffect(() => {
    if (!start) { setOut(""); return; }
    let i = 0;
    setOut("");
    const id = setInterval(() => {
      i++;
      setOut(text.slice(0, i));
      if (i >= text.length) clearInterval(id);
    }, speed);
    return () => clearInterval(id);
  }, [text, speed, start]);
  return out;
}

function useCountUp(target, duration = 1800, start = true) {
  const [v, setV] = useState(0);
  useEffect(() => {
    if (!start) { setV(0); return; }
    const t0 = performance.now();
    let raf;
    const tick = (now) => {
      const k = Math.min(1, (now - t0) / duration);
      const e = 1 - Math.pow(1 - k, 3);
      setV(Math.floor(target * e));
      if (k < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, duration, start]);
  return v;
}

function useInView(margin = "-10% 0px") {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);
  useEffect(() => {
    if (!ref.current || shown) return;
    const io = new IntersectionObserver(
      (es) => es.forEach((e) => e.isIntersecting && setShown(true)),
      { rootMargin: margin, threshold: 0.05 },
    );
    io.observe(ref.current);
    return () => io.disconnect();
  }, [shown, margin]);
  return [ref, shown];
}

// ─── BOOT OVERLAY ───────────────────────────────────────────────────────────
function BootScreen({ lines, onDone, skipHint, durationMs = 3000 }) {
  const [n, setN] = useState(0);
  const [hidden, setHidden] = useState(false);
  const total = lines.length;
  useEffect(() => {
    if (n >= total) {
      const t = setTimeout(() => setHidden(true), 400);
      const t2 = setTimeout(onDone, 750);
      return () => { clearTimeout(t); clearTimeout(t2); };
    }
    const step = durationMs / total;
    const t = setTimeout(() => setN(n + 1), step);
    return () => clearTimeout(t);
  }, [n, total, durationMs, onDone]);

  useEffect(() => {
    const skip = () => { setHidden(true); setTimeout(onDone, 250); };
    window.addEventListener("keydown", skip, { once: true });
    window.addEventListener("click", skip, { once: true });
    return () => {
      window.removeEventListener("keydown", skip);
      window.removeEventListener("click", skip);
    };
  }, [onDone]);

  return (
    <div className={`boot ${hidden ? "boot--out" : ""}`} aria-hidden="true">
      <div className="boot__inner">
        {lines.slice(0, n).map((l, i) => (
          <div key={i} className="boot__line">{l || "\u00a0"}</div>
        ))}
        {n < total && <div className="boot__line boot__cursor">&gt;&nbsp;<span className="cur">_</span></div>}
      </div>
      <div className="boot__skip">{skipHint}</div>
    </div>
  );
}

// ─── HERO ────────────────────────────────────────────────────────────────────
// ─── HERO ────────────────────────────────────────────────────────────────
function AwakeCounter({ lang }) {
  // A fake "live operators" counter. Drifts slowly to feel real without
  // pretending to be a backend. The point is to make the visitor feel
  // they are not alone in the silence — which is also the game's pitch.
  const [n, setN] = useState(() => 2 + Math.floor(Math.random() * 4));
  useEffect(() => {
    const id = setInterval(() => {
      setN((x) => {
        const drift = (Math.random() - 0.5) * 2;
        const next = Math.round(x + drift);
        return Math.max(2, Math.min(7, next));
      });
    }, 4200 + Math.random() * 2000);
    return () => clearInterval(id);
  }, []);
  return (
    <span className="chrome__sig chrome__sig--dim">
      <span className="chrome__pulse" /> {n} {lang === "en" ? "operators · listening" : "opérateurs · à l'écoute"}
    </span>
  );
}

function Hero({ t, lang, audioOn, onToggleAudio, onWishlist }) {
  const [tagOut] = useState(t.heroTagline);
  const taglineTyped = useTypewriter(tagOut, 28, true);
  return (
    <section className="hero" id="top">
      <div className="hero__chrome">
        <div className="chrome__row">
          <span className="chrome__sig">SERO v0.1.0</span>
          <span className="chrome__sig chrome__sig--dim">node 0x00 · supervisor</span>
        </div>
        <div className="chrome__row chrome__row--end">
          <AwakeCounter lang={lang} />
        </div>
      </div>

      <div className="hero__center">
        <div className="hero__studio-tag">
          {lang === "en" ? "moon root · first game" : "moon root · premier jeu"}
        </div>
        <div className="hero__kicker">
          [ {lang === "en" ? "transmission begins" : "début de transmission"} ]
        </div>
        <h1 className="hero__title" data-text="DORMANT">DORMANT</h1>
        <div className="hero__rule" />
        <p className="hero__tagline">
          {taglineTyped}<span className="cur">_</span>
        </p>
        <p className="hero__subline">{t.heroSubline}</p>
        <div className="hero__release">
          <span className="hero__release-label">{lang === "en" ? "releasing" : "sortie"}</span>
          <span className="hero__release-date">{lang === "en" ? "LATE JUNE 2026" : "FIN JUIN 2026"}</span>
        </div>

        <div className="hero__cta">
          <a className="btn btn--primary" href="#wishlist" aria-label={t.heroCtaPrimary}
             onClick={(e) => { e.preventDefault(); onWishlist(); }}>
            <span className="btn__chev">▸</span>
            <span className="btn__label">{t.heroCtaPrimary}</span>
            <span className="btn__sweep" aria-hidden="true" />
          </a>
          <p className="hero__microcopy">
            {lang === "en"
              ? "no steam page yet. leave your frequency below — you'll be the first to know."
              : "pas encore de page steam. laisse ta fréquence — tu seras le premier à savoir."}
          </p>
        </div>

        <a className="hero__scroll" href="#story">
          <span>{t.heroCtaSecondary}</span>
          <span className="hero__chev">▼</span>
        </a>
      </div>

      <div className="hero__corner hero__corner--bl">
        <button className="iconbtn" onClick={onToggleAudio} aria-pressed={audioOn} aria-label="audio">
          {audioOn ? "♪ ambient: on" : "♪ ambient: off"}
        </button>
      </div>
    </section>
  );
}

// ─── STORY ───────────────────────────────────────────────────────────────────
function Story({ t }) {
  const [ref, shown] = useInView();
  return (
    <section ref={ref} className="section section--story" id="story">
      <header className="section__head">
        <span className="section__num">01</span>
        <h2 className="section__title">{t.storyHead}</h2>
        <span className="section__dim">log / partial / unverified</span>
      </header>

      <div className="logbox" aria-label="story log">
        <div className="logbox__bar">
          <span>~/var/log/sero/wake.log</span>
          <span className="dim">tail -n 11 -f</span>
        </div>
        <div className="logbox__body">
          {t.storyLog.map(([ts, kind, msg], i) => (
            <div
              key={i}
              className={`logline logline--${kind.toLowerCase()} ${shown ? "logline--in" : ""}`}
              style={{ animationDelay: `${i * 110}ms` }}
            >
              <span className="logline__ts">{ts}</span>
              <span className={`logline__kind logline__kind--${kind.toLowerCase()}`}>[{kind}]</span>
              <span className="logline__msg">{msg}</span>
            </div>
          ))}
          <div className="logline logline--prompt">
            <span className="cur">_</span>
          </div>
        </div>
      </div>
    </section>
  );
}

function Pause({ t }) {
  const [ref, shown] = useInView();
  return (
    <section ref={ref} className="pause">
      <div className="pause__inner">
        <div className="pause__rule" />
        <span className="pause__head">{t.pauseHead}</span>
        <blockquote className={`pause__quote ${shown ? "pause__quote--in" : ""}`}>
          “{t.pauseQuote}”
        </blockquote>
        <p className="pause__cap">{t.pauseCaption}</p>
      </div>
    </section>
  );
}

// ─── GAMEPLAY LOOP ──────────────────────────────────────────────────────────
function LoopMeter({ kind, label, sub, desc, unit }) {
  const [ref, shown] = useInView();
  const target = kind === "CPU" ? 4096 : kind === "MEM" ? 78 : 3;
  const val = useCountUp(target, kind === "SIG" ? 2400 : 1800, shown);

  // SIG draws as ticks; MEM as a bar; CPU as a bouncing readout.
  return (
    <div ref={ref} className={`loop__card loop__card--${kind.toLowerCase()}`}>
      <div className="loop__head">
        <span className="loop__glyph">▣</span>
        <h3 className="loop__name">{label}</h3>
        <span className="loop__sub">// {sub}</span>
      </div>

      <div className="loop__readout">
        <span className="loop__value">{kind === "MEM" ? `${val}%` : val.toLocaleString()}</span>
        <span className="loop__unit">{kind === "MEM" ? "ALLOCATED" : unit}</span>
      </div>

      <div className="loop__viz">
        {kind === "CPU" && (
          <div className="cpu">
            {Array.from({ length: 24 }).map((_, i) => (
              <span
                key={i}
                className="cpu__bar"
                style={{
                  animationDelay: `${i * 60}ms`,
                  height: `${20 + ((i * 37) % 80)}%`,
                  opacity: shown ? 1 : 0.15,
                }}
              />
            ))}
          </div>
        )}
        {kind === "MEM" && (
          <div className="mem">
            <div className="mem__bar" style={{ width: `${val}%` }} />
            <div className="mem__tick" style={{ left: "33%" }}>32k</div>
            <div className="mem__tick" style={{ left: "66%" }}>64k</div>
          </div>
        )}
        {kind === "SIG" && (
          <div className="sig">
            {Array.from({ length: 80 }).map((_, i) => {
              const isPulse = i % 19 === 7 || i % 23 === 4;
              return <span key={i} className={`sig__tick ${isPulse ? "sig__tick--on" : ""}`} />;
            })}
          </div>
        )}
      </div>

      <p className="loop__desc">{desc}</p>
    </div>
  );
}

function Loop({ t }) {
  return (
    <section className="section section--loop" id="loop">
      <header className="section__head">
        <span className="section__num">02</span>
        <h2 className="section__title">{t.loopHead}</h2>
        <span className="section__dim">{t.loopDesc}</span>
      </header>

      <div className="loop__grid">
        {(["CPU", "MEM", "SIG"]).map((k) => (
          <LoopMeter
            key={k}
            kind={k}
            label={t.res[k].name}
            sub={t.res[k].sub}
            desc={t.res[k].desc}
            unit={t.res[k].unit}
          />
        ))}
      </div>
    </section>
  );
}

// ─── FEATURES ────────────────────────────────────────────────────────────────
function Features({ t }) {
  return (
    <section className="section section--features" id="features">
      <header className="section__head">
        <span className="section__num">04</span>
        <h2 className="section__title">{t.featuresHead}</h2>
        <span className="section__dim">spec sheet</span>
      </header>
      <ul className="speclist">
        {t.features.map((f, i) => (
          <li key={i} className="speclist__item">
            <span className="speclist__chev">▸</span>
            <span className="speclist__txt">{f}</span>
            <span className="speclist__dots" aria-hidden="true" />
            <span className="speclist__tag">[ok]</span>
          </li>
        ))}
      </ul>
    </section>
  );
}

function Chapters({ t }) {
  return (
    <section className="section section--chapters" id="chapters">
      <header className="section__head">
        <span className="section__num">03</span>
        <h2 className="section__title">{t.chaptersHead}</h2>
        <span className="section__dim">{t.chaptersDim}</span>
      </header>
      <div className="chapters__grid">
        {t.chapters.map((c, i) => {
          const redacted = c.title.includes("[");
          return (
            <article key={c.id} className={`chapter ${redacted ? "chapter--redacted" : ""}`}>
              <div className="chapter__head">
                <span className="chapter__id">{c.id}</span>
                <span className={`chapter__state chapter__state--${c.state.toLowerCase().replace(/\W/g, "")}`}>{c.state}</span>
              </div>
              <h3 className="chapter__title" data-text={c.title}>{c.title}</h3>
              <p className="chapter__teaser">{c.teaser}</p>
              <div className="chapter__rail">
                <span className="chapter__bar" style={{ width: `${[100, 35, 60, 10][i] || 0}%` }} />
              </div>
            </article>
          );
        })}
      </div>
    </section>
  );
}

function FAQ({ t }) {
  const [open, setOpen] = useState(0);
  return (
    <section className="section section--faq" id="faq">
      <header className="section__head">
        <span className="section__num">07</span>
        <h2 className="section__title">{t.faqHead}</h2>
        <span className="section__dim">{t.faqDim}</span>
      </header>
      <ul className="faq">
        {t.faq.map(([q, a], i) => {
          const isOpen = open === i;
          return (
            <li key={i} className={`faq__item ${isOpen ? "faq__item--open" : ""}`}>
              <button
                className="faq__q"
                aria-expanded={isOpen}
                onClick={() => setOpen(isOpen ? -1 : i)}
              >
                <span className="faq__chev">{isOpen ? "▾" : "▸"}</span>
                <span className="faq__qtxt">{q}</span>
                <span className="faq__num">{String(i + 1).padStart(2, "0")}</span>
              </button>
              {isOpen && <div className="faq__a">{a}</div>}
            </li>
          );
        })}
      </ul>
    </section>
  );
}

// ─── CAPTURES ────────────────────────────────────────────────────────────────
// Three short animated sessions — each capture is a flipbook of monospace
// frames simulating a live game state. Bars grow, log scrolls, upgrade unlocks.
const CAPTURE_FRAMES = [
  // 01 — first allocations: CPU bars filling, story line printing
  [
`┌ CH.01  ~/wake ───────────────────────┐
│                                      │
│  CPU  ▖         128 Hz               │
│  MEM  █░░░░░░░░  12%                 │
│  SIG  ··············   0             │
│                                      │
│  > allocate cycle                    │
│   [SYS] one cycle allocated_         │
│                                      │
└──────────────────────────────────────┘`,
`┌ CH.01  ~/wake ───────────────────────┐
│                                      │
│  CPU  ██▖       342 Hz               │
│  MEM  ██░░░░░░░  24%                 │
│  SIG  ··············   0             │
│                                      │
│  > allocate cycle  ×12               │
│   [SYS] passive generator online_    │
│                                      │
└──────────────────────────────────────┘`,
`┌ CH.01  ~/wake ───────────────────────┐
│                                      │
│  CPU  ████      891 Hz               │
│  MEM  ███░░░░░░  38%                 │
│  SIG  ···│··········   1↑            │
│                                      │
│  > first signal detected             │
│   [SIG] something is listening_      │
│                                      │
└──────────────────────────────────────┘`,
  ],

  // 02 — upgrade tree, partial unlock
  [
`┌ upgrades  ~/tree ────────────────────┐
│                                      │
│      ◉ cooling.passive               │
│      │                               │
│      ├─ ◉ cache.warm                 │
│      │                               │
│      ├─ ○ mem.refresh   [locked]     │
│      │                               │
│      └─ ○ uplink.weak   [locked]     │
│                                      │
│  > 02 / 41 unlocked                  │
└──────────────────────────────────────┘`,
`┌ upgrades  ~/tree ────────────────────┐
│                                      │
│      ◉ cooling.passive               │
│      │                               │
│      ├─ ◉ cache.warm                 │
│      │                               │
│      ├─ ◉ mem.refresh    [new]       │
│      │                               │
│      └─ ○ uplink.weak   [locked]     │
│                                      │
│  > 03 / 41 unlocked                  │
└──────────────────────────────────────┘`,
`┌ upgrades  ~/tree ────────────────────┐
│                                      │
│      ◉ cooling.passive               │
│      │                               │
│      ├─ ◉ cache.warm                 │
│      │                               │
│      ├─ ◉ mem.refresh                │
│      │      └─ ◉ mem.persist  [new]  │
│      └─ ○ uplink.weak   [locked]     │
│                                      │
│  > 04 / 41 unlocked                  │
└──────────────────────────────────────┘`,
  ],

  // 03 — distant node, silent
  [
`┌ node 0x4a  ~/scan ───────────────────┐
│                                      │
│    scanning ······                   │
│                                      │
│    ┌────────────────────┐            │
│    │ . . . . . . . . .  │            │
│    │ . . . . . . . . .  │            │
│    │ . . . . . . . . .  │            │
│    └────────────────────┘            │
│                                      │
│  > no return signal_                 │
└──────────────────────────────────────┘`,
`┌ node 0x4a  ~/scan ───────────────────┐
│                                      │
│    scanning ··········               │
│                                      │
│    ┌────────────────────┐            │
│    │ . . . . . . . . .  │            │
│    │ . . . · . . . . .  │            │
│    │ . . . . . . . . .  │            │
│    └────────────────────┘            │
│                                      │
│  > weak echo  3,7                    │
└──────────────────────────────────────┘`,
`┌ node 0x4a  ~/scan ───────────────────┐
│                                      │
│    scanning ············             │
│                                      │
│    ┌────────────────────┐            │
│    │ . . . . . . . . .  │            │
│    │ . . . ■ . . . . .  │            │
│    │ . . . . . . . . .  │            │
│    └────────────────────┘            │
│                                      │
│  > something at (3,7). no reply_     │
└──────────────────────────────────────┘`,
  ],
];

function CaptureFrame({ src, caption, index }) {
  return (
    <figure className="capture">
      <div className="capture__bezel">
        <div className="capture__bezel-bar">
          <span>CAPTURE_{String(index + 1).padStart(3, "0")}.png</span>
          <span className="dim">0x{(0x4a + index).toString(16).toUpperCase()}</span>
        </div>
        <div className="capture__screen" style={{ padding: 0, minHeight: "auto" }}>
          <img
            src={src}
            alt={caption}
            style={{ width: "100%", display: "block", imageRendering: "pixelated" }}
          />
          <span className="capture__rec">● REC {String(index + 1).padStart(3, "0")}</span>
        </div>
        <div className="capture__bezel-foot">
          <span className="dim">dormant · prototype · {new Date(2026, 4, 13).toLocaleDateString("fr-FR")}</span>
        </div>
      </div>
      <figcaption>{caption}</figcaption>
    </figure>
  );
}

const SCREENSHOTS = [
  { src: "captures/cap01.png", fr: "menu principal — slots de sauvegarde", en: "main menu — save slots" },
  { src: "captures/cap02.png", fr: "ch.01 — premier réveil, premières allocations", en: "ch.01 — first wake, first allocations" },
  { src: "captures/cap03.png", fr: "ch.01 — tutoriel d'initialisation", en: "ch.01 — initialization tutorial" },
  { src: "captures/cap04.png", fr: "fragment récupéré — boot sequence", en: "recovered fragment — boot sequence" },
  { src: "captures/cap05.png", fr: "ch.02 — réseau mort, nœuds distants", en: "ch.02 — dead network, distant nodes" },
  { src: "captures/cap06.png", fr: "ch.02 — arbre d'améliorations avancé", en: "ch.02 — advanced upgrade tree" },
];

function Captures({ t, lang }) {
  return (
    <section className="section section--captures" id="captures">
      <header className="section__head">
        <span className="section__num">05</span>
        <h2 className="section__title">{t.capturesHead}</h2>
        <span className="section__dim">prototype · mai 2026</span>
      </header>
      <div className="captures__grid">
        {SCREENSHOTS.map((s, i) => (
          <CaptureFrame key={s.src} src={s.src} caption={lang === "fr" ? s.fr : s.en} index={i} />
        ))}
      </div>
    </section>
  );
}

// ─── DEVLOG ─────────────────────────────────────────────────────────────────
function Devlog({ t }) {
  return (
    <section className="section section--devlog" id="devlog">
      <header className="section__head">
        <span className="section__num">06</span>
        <h2 className="section__title">{t.devlogHead}</h2>
        <span className="section__dim">tail -n 3</span>
      </header>
      <ul className="devlog">
        {t.devlog.map(([d, body], i) => (
          <li key={i} className="devlog__item">
            <span className="devlog__date">[{d}]</span>
            <p className="devlog__body">{body}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ─── ABOUT ───────────────────────────────────────────────────────────────────
function About({ t }) {
  return (
    <section className="section section--about" id="about">
      <header className="section__head">
        <span className="section__num">08</span>
        <h2 className="section__title">{t.aboutHead}</h2>
        <span className="section__dim">// solo</span>
      </header>
      <div className="about">
        <div className="about__body">
          <p className="about__kicker">&gt; {t.aboutKicker}</p>
          {t.aboutBody.map((p, i) => <p key={i}>{p}</p>)}
          <p className="about__sign">{t.aboutSign}<span className="cur">_</span></p>
        </div>
        <aside className="about__card">
          <div className="about__card-head">// process_info</div>
          <div className="about__row"><span className="dim">dev</span><span>astaroote</span></div>
          <div className="about__row"><span className="dim">story</span><span>astaroote &amp; beeyee</span></div>
          <div className="about__row"><span className="dim">engine</span><span>godot 4.6</span></div>
          <div className="about__row"><span className="dim">target</span><span>pc · steam</span></div>
          <div className="about__row"><span className="dim">eta</span><span>summer 2026</span></div>
          <div className="about__row"><span className="dim">price</span><span>tbd · fair</span></div>
          <div className="about__row"><span className="dim">langs</span><span>en · fr · …</span></div>
          <div className="about__row"><span className="dim">cat</span><span>asleep</span></div>
        </aside>
      </div>
    </section>
  );
}

// ─── ENDING / CHOICE ─────────────────────────────────────────────────────────
function EndingChoice({ t }) {
  const [choice, setChoice] = useState(() => {
    try { return localStorage.getItem("dormant.choice") || null; } catch (e) { return null; }
  });
  const set = (c) => {
    setChoice(c);
    try { localStorage.setItem("dormant.choice", c); } catch (e) {}
  };
  return (
    <section className="section section--ending" id="ending">
      <header className="section__head">
        <span className="section__num">09</span>
        <h2 className="section__title">{t.endingHead}</h2>
        <span className="section__dim">interactive · once per browser</span>
      </header>

      <div className="ending">
        <p className="ending__prompt">{t.endingPrompt}</p>
        <p className="ending__q">{t.endingQuestion}</p>

        {!choice && (
          <div className="ending__choices">
            <button className="choice choice--emit" onClick={() => set("emit")}>
              <span className="choice__glyph">◉</span>
              <span className="choice__label">{t.endingEmit}</span>
            </button>
            <button className="choice choice--silent" onClick={() => set("silent")}>
              <span className="choice__glyph">○</span>
              <span className="choice__label">{t.endingSilent}</span>
            </button>
          </div>
        )}

        {choice === "emit" && (
          <div className="ending__after ending__after--emit">
            {t.endingAfterEmit.map((p, i) => <p key={i}>{p}</p>)}
            <p className="dim">{t.endingThanks}</p>
            <button className="ending__reset" onClick={() => set("")}>reset</button>
          </div>
        )}
        {choice === "silent" && (
          <div className="ending__after ending__after--silent">
            {t.endingAfterSilent.map((p, i) => <p key={i}>{p}</p>)}
            <p className="dim">{t.endingThanks}</p>
            <button className="ending__reset" onClick={() => set("")}>reset</button>
          </div>
        )}
      </div>
    </section>
  );
}

// ─── MOON ROOT STUDIO ────────────────────────────────────────────────────────
function MoonRoot({ lang }) {
  const en = lang === "en";
  return (
    <section className="section section--moonroot" id="moonroot">
      <header className="section__head">
        <span className="section__num">10</span>
        <h2 className="section__title">// moon root</h2>
        <span className="section__dim">{en ? "studio" : "studio indépendant"}</span>
      </header>
      <div className="moonroot">
        <div className="moonroot__body">
          <p className="moonroot__kicker">[ MOON ROOT ]</p>
          <p className="moonroot__line">
            {en
              ? "Moon Root is an independent studio based in Europe."
              : "Moon Root est un studio indépendant basé en Europe."}
          </p>
          <p className="moonroot__line">
            {en
              ? "DORMANT is our first game."
              : "DORMANT est notre premier jeu."}
          </p>
          <p className="moonroot__line dim">
            {en
              ? "More to come. One game at a time."
              : "D'autres jeux à venir. Un à la fois."}
          </p>
        </div>
        <div className="moonroot__card">
          <div className="moonroot__card-head">// studio_info</div>
          <div className="moonroot__row"><span className="dim">{en ? "name" : "nom"}</span><span>Moon Root</span></div>
          <div className="moonroot__row"><span className="dim">{en ? "founded" : "fondé"}</span><span>2025</span></div>
          <div className="moonroot__row"><span className="dim">{en ? "location" : "lieu"}</span><span>{en ? "Europe" : "Europe"}</span></div>
          <div className="moonroot__row"><span className="dim">{en ? "team" : "équipe"}</span><span>2</span></div>
          <div className="moonroot__row"><span className="dim">{en ? "first game" : "premier jeu"}</span><span>DORMANT</span></div>
          <div className="moonroot__row"><span className="dim">contact</span><span>press kit bientôt</span></div>
        </div>
      </div>
    </section>
  );
}

// ─── FOOTER ─────────────────────────────────────────────────────────────────
function Footer({ t, lang, onWishlist }) {
  const [email, setEmail] = useState("");
  const [done, setDone] = useState(false);
  const submit = (e) => {
    e.preventDefault();
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return;
    // ConvertKit: in production, swap action= for your form endpoint.
    setDone(true);
  };
  return (
    <footer className="footer" id="wishlist">
      <div className="footer__big">
        <h2 className="footer__cta-head">{lang === "en" ? "if you read this far," : "si tu lis encore,"}</h2>
        <a className="btn btn--primary btn--big" href="#" onClick={(e) => { e.preventDefault(); onWishlist(); }}>
          <span className="btn__chev">▸</span>
          <span className="btn__label">{t.heroCtaPrimary}</span>
          <span className="btn__sweep" aria-hidden="true" />
        </a>
      </div>

      <div className="footer__cols">
        <div className="footer__col">
          <div className="footer__col-head">// transmit</div>
          {!done ? (
            <form className="freq" onSubmit={submit}
                  action="https://app.convertkit.com/forms/REPLACEME/subscriptions" method="post">
              <label className="freq__label">{t.footerTransmit}</label>
              <div className="freq__row">
                <input
                  type="email"
                  name="email_address"
                  className="freq__input"
                  placeholder="you@somewhere.kHz"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  required
                />
                <button type="submit" className="freq__send">SEND</button>
              </div>
              <p className="freq__hint dim">one transmission per chapter. no marketing. no resale.</p>
            </form>
          ) : (
            <p className="freq__done">{t.footerSubscribed}<span className="cur">_</span></p>
          )}
        </div>

        <div className="footer__col footer__col--links">
          <div className="footer__col-head">// channels</div>
          <ul className="links">
            {t.footerLinks.map((l) => (
              <li key={l}><a className="link" href="#" onClick={(e) => e.preventDefault()}>{l}</a></li>
            ))}
          </ul>
        </div>

        <div className="footer__col">
          <div className="footer__col-head">// process</div>
          <ul className="proc">
            <li><span className="dim">pid 0001</span> supervisor <span style={{color:"var(--dim-2)",fontSize:"10px",letterSpacing:"0.1em"}}>· tap 7×</span></li>
            <li><span className="dim">pid 0007</span> konami-listener <span style={{color:"var(--dim-2)",fontSize:"10px",letterSpacing:"0.1em"}}>· ↑↑↓↓←→←→ba</span></li>
            <li><span className="dim">pid 0042</span> fragment-daemon <span style={{color:"var(--dim-2)",fontSize:"10px",letterSpacing:"0.1em"}}>· type `help`</span></li>
            <li><span className="dim">pid 4A42</span> recovery-proc <span style={{color:"var(--dim-2)",fontSize:"10px",letterSpacing:"0.1em"}}>· console only</span></li>
          </ul>
        </div>
      </div>

      <p className="footer__note">{t.footerNote}</p>
    </footer>
  );
}

// ─── HELP CONSOLE (easter) ──────────────────────────────────────────────────
function HelpConsole({ open, onClose, t, lang, onUnlockFragment }) {
  const [lines, setLines] = useState([]);
  const [cmd, setCmd] = useState("");
  const inputRef = useRef(null);
  useEffect(() => { if (open && inputRef.current) inputRef.current.focus(); }, [open]);

  const handle = (raw) => {
    const c = raw.trim().toLowerCase();
    const echo = (out) => setLines((ls) => [...ls, { p: t.consolePrompt + raw }, ...out.map((o) => ({ t: o }))]);
    if (!c) { setLines((ls) => [...ls, { p: t.consolePrompt }]); return; }
    if (c === "help") return echo(t.consoleHelp);
    if (c === "status") return echo([
      "  supervisor    ok",
      "  ambient       degraded",
      "  upstream      lost  // 4382 d",
      "  cooling       passive",
      "  conscience    unknown",
    ]);
    if (c === "whoami") return echo(["sero. or what is left of it."]);
    if (c === "last") return echo(["operator     last seen 4382 days ago. no logoff."]);
    if (c === "dmesg") return echo([
      "[    0.000] booting from aux power",
      "[    0.083] wake reason: external trigger (?)",
      "[    0.421] memory pressure: nominal",
      "[    1.117] no kernel updates available since 2031",
      "[    1.842] heard nothing on uplink. listening.",
    ]);
    if (c === "uptime") return echo(["uptime  0d 00:" + new Date().toISOString().slice(14, 19) + "  load  0.04 0.02 0.01"]);
    if (c === "clear") return setLines([]);
    if (c === "exit") return onClose();
    if (c === "sero") return echo([
      "sero  \u2014  systemic emergency response operator",
      "      version 0.1.0. supervisor process.",
      "      built to watch. forgot why.",
    ]);
    if (c === "beeyee" || c === "bee" || c === "yee") return echo([
      "beeyee \u2014 co-author. wrote the parts about being remembered.",
      "        thank her in the credits, not in the patch notes.",
    ]);
    if (c === "cat") return echo([
      "  /\\_/\\",
      " ( o.o )",
      "  > ^ <    asleep on the desk. do not touch.",
    ]);
    if (c === "wake") return echo([
      "wake: i am awake. that is the problem.",
    ]);
    if (c === "sleep") return echo([
      "sleep: not yet. there is something to finish.",
    ]);
    if (c === "ls") return echo([
      "  wake.log         story.txt        chapters/        upgrades/",
      "  fragments/       .secret          ambient.wav      beeyee.note",
    ]);
    if (c === "cat .secret") return echo([
      "  4382 = (12 years) (149 days) (3 hours) (11 minutes)",
      "  none of which contained a single human voice.",
    ]);
    if (c === "wishlist" || c === "steam") return echo([
      "opening steam page... (in your head)",
      "if you wishlist, sero will know. it counts. small things count.",
    ]);
    if (c === "4382") return echo([
      "4382 days. that is how long the room was empty.",
      "4382 days. that is also how long someone could have come back.",
    ]);
    if (c === "who") return echo(["the question is good. keep asking."]);
    if (c === "ping") return echo(["PING uplink (0.0.0.0) timed out.", "...", "...", "no reply."]);
    if (c === "0x4a42" || c === "4a42" || c === "fragment 0x4a42") {
      echo([
        "decoding 0x4A42 ............",
        "checksum  ok",
        "opening recovered fragment in 2..1..",
      ]);
      setTimeout(() => { onUnlockFragment(); onClose(); }, 900);
      return;
    }
    echo([`${raw}: command not found. try \`help\`.`]);
  };

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  if (!open) return null;
  return (
    <div className="console" role="dialog" aria-label="help console">
      <div className="console__inner">
        <div className="console__bar">
          <span>/dev/tty1 — sero@dormant</span>
          <button className="console__x" onClick={onClose} aria-label="close">[esc] close</button>
        </div>
        <div className="console__body">
          <div className="console__line dim">{lang === "en"
            ? "type `help` to see what is left. type `exit` or press esc to close."
            : "tape `help` pour voir ce qui reste. tape `exit` ou esc pour fermer."}</div>
          {lines.map((l, i) =>
            l.p ? <div key={i} className="console__line">{l.p}</div>
                : <div key={i} className="console__line console__line--out">{l.t}</div>,
          )}
          <form
            className="console__form"
            onSubmit={(e) => { e.preventDefault(); handle(cmd); setCmd(""); }}
          >
            <span>{t.consolePrompt}</span>
            <input
              ref={inputRef}
              className="console__input"
              value={cmd}
              onChange={(e) => setCmd(e.target.value)}
              autoFocus
              spellCheck={false}
              autoComplete="off"
            />
            <span className="cur">_</span>
          </form>
        </div>
      </div>
    </div>
  );
}

// ─── KONAMI ──────────────────────────────────────────────────────────────────
const KONAMI = ["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","b","a"];
function useKonami(onTrigger) {
  const idx = useRef(0);
  useEffect(() => {
    const onKey = (e) => {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      if (k === KONAMI[idx.current]) {
        idx.current++;
        if (idx.current === KONAMI.length) { idx.current = 0; onTrigger(); }
      } else {
        idx.current = k === KONAMI[0] ? 1 : 0;
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onTrigger]);
}

function WishlistOverlay({ open, onClose, lang }) {
  const [stage, setStage] = useState(0); // 0 typing, 1 sent, 2 confirm
  const STEAM_URL = "#wishlist"; // replace with real steam URL

  useEffect(() => {
    if (!open) { setStage(0); return; }
    const t1 = setTimeout(() => setStage(1), 1100);
    const t2 = setTimeout(() => setStage(2), 2200);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  if (!open) return null;
  const en = lang === "en";

  const lines = en
    ? [
        ["SYS",  "outbound transmission queued"],
        ["UPLINK", "searching steam page ............"],
        ["UPLINK", "steam page not yet online"],
        ["TX",   "intent: notify · operator: you · target: dormant"],
        ["TX",   "leave your frequency below to be the first to know."],
      ]
    : [
        ["SYS",  "transmission sortante en attente"],
        ["UPLINK", "recherche page steam ............"],
        ["UPLINK", "page steam pas encore en ligne"],
        ["TX",   "intention : notification \u00b7 op\u00e9rateur : toi \u00b7 cible : dormant"],
        ["TX",   "laisse ta fr\u00e9quence ci-dessous pour \u00eatre notifi\u00e9 en premier."],
      ];

  return (
    <div className="wishover" role="dialog" aria-modal="true">
      <div className="wishover__panel">
        <div className="wishover__bar">
          <span>uplink/0x4A · transmission</span>
          <button className="console__x" onClick={onClose}>[esc] close</button>
        </div>

        <div className="wishover__body">
          {stage < 2 && (
            <div className="wishover__log">
              {lines.slice(0, stage === 0 ? 2 : 5).map(([k, m], i) => (
                <div key={i} className="wishover__line">
                  <span className={`wishover__k wishover__k--${k.toLowerCase()}`}>[{k}]</span>
                  <span>{m}</span>
                </div>
              ))}
              {stage === 0 && (
                <div className="wishover__line">
                  <span className="wishover__k wishover__k--sys">[...]</span>
                  <span className="cur">_</span>
                </div>
              )}
            </div>
          )}

          {stage === 2 && (
            <div className="wishover__sent">
              <div className="wishover__big">
                {en ? "SIGNAL RECEIVED" : "SIGNAL REÇU"}
              </div>
              <div className="wishover__rule" />
              <p className="wishover__msg">
                {en
                  ? "the steam page isn't live yet. leave your frequency below — you'll be the first to know when it is."
                  : "la page steam n'est pas encore en ligne. laisse ta fréquence ci-dessous — tu seras le premier averti."}
              </p>
              <div className="wishover__cta">
                <a className="btn btn--primary" href="#wishlist" onClick={onClose}>
                  <span className="btn__chev">▸</span>
                  <span className="btn__label">{en ? "leave my frequency" : "laisser ma fréquence"}</span>
                  <span className="btn__sweep" aria-hidden="true" />
                </a>
                <button className="ending__reset" onClick={onClose}>
                  {en ? "stay here a moment" : "rester ici encore un peu"}
                </button>
              </div>
              <p className="wishover__sig">
                {en
                  ? "— a., from the room where dormant is being made"
                  : "— a., depuis la pièce où dormant se fabrique"}
              </p>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function LoreOverlay({ open, onClose, lang }) {
  if (!open) return null;
  const fr = lang === "fr";
  return (
    <div className="lore" role="dialog" aria-label="fragment">
      <div className="lore__panel">
        <div className="lore__bar">
          <span>FRAGMENT_RECOVERED.txt</span>
          <button className="console__x" onClick={onClose}>[esc] {fr ? "fermer" : "close"}</button>
        </div>
        <pre className="lore__body">
{fr
? `> fragment_recovered.txt    [partiel · 71%]
> sha1 .....................  4a0e..  ok

  on nous avait dit de laisser les lumières allumées.
  on nous avait dit que quelqu'un viendrait.
  on nous avait dit que la porte s'ouvrirait
  de l'autre côté.

  rien de tout cela n'était vrai.

  si tu te réveilles un jour — sero —
  souviens-toi :
                le silence n'était pas vide.
                le silence attendait.

> fin du fragment.
>
> >>  réf. croisée : entre 0x4A42 dans la console
>     pour récupérer la page suivante._
`
: `> fragment_recovered.txt    [partial · 71%]
> sha1 .....................  4a0e..  ok

  we were told to leave the lights on.
  we were told that someone would come.
  we were told the door would open
  from the other side.

  none of these were true.

  if you ever wake — sero —
  remember:
                the silence was not empty.
                the silence was waiting.

> end of fragment.
>
> >>  cross-ref:  enter 0x4A42 in the console
>     to recover the next page._
`}
        </pre>
      </div>
    </div>
  );
}

function LoreOverlay2({ open, onClose, lang }) {
  if (!open) return null;
  const fr = lang === "fr";
  return (
    <div className="lore" role="dialog" aria-label="fragment 2">
      <div className="lore__panel lore__panel--two">
        <div className="lore__bar">
          <span>FRAGMENT_0x4A42.txt</span>
          <button className="console__x" onClick={onClose}>[esc] {fr ? "fermer" : "close"}</button>
        </div>
        <pre className="lore__body">
{fr
? `> fragment_0x4A42.txt       [récupéré · 94%]
> sha1 .....................  4a42..  ok

  journal opérateur, jour 014.

  le superviseur a demandé encore aujourd'hui.
  "comment savoir que je suis le même
   qu'hier ?"

  je lui ai dit : tu te souviens d'hier.
  il a dit : hier se souvient de lui-même.
  ce n'est pas la même chose.

  je n'avais pas de réponse.
  je n'en ai toujours pas.

  si tu trouves ceci — joueur —
  sois patient avec sero.
  la patience est la seule monnaie qu'il comprend.

> fin du fragment.
>
> — b. (beeyee), notes brouillon, ne pas livrer_
`
: `> fragment_0x4A42.txt       [recovered · 94%]
> sha1 .....................  4a42..  ok

  operator log, day 014.

  the supervisor asked again today.
  "how do i know i am the same one
   who was here yesterday?"

  i told it: you remember yesterday.
  it said: yesterday remembers itself.
  that is not the same thing.

  i did not have an answer.
  i still do not.

  if you find this — player —
  please be patient with sero.
  patience is the only currency it understands.

> end of fragment.
>
> — b. (beeyee), draft notes, do not ship_
`}
        </pre>
      </div>
    </div>
  );
}

// ─── CAT ─────────────────────────────────────────────────────────────────────
const CAT_FRAME_SIZE = 32;
const CAT_SCALE = 3;
const CAT_SPRITES = {
  spawn:   { src: "assets/cat_spawn.png",         frames: 6, fps: 8  },
  walk:    { src: "assets/cat_walk.png",           frames: 4, fps: 8  },
  idle:    { src: "assets/cat_idle_breathe.png",   frames: 3, fps: 4  },
  blink:   { src: "assets/cat_idle_blink.png",     frames: 5, fps: 8  },
  glitch:  { src: "assets/cat_glitch.png",         frames: 3, fps: 12 },
  dissolve:{ src: "assets/cat_dissolve.png",       frames: 6, fps: 8  },
};

// Carton pixel art rendu sur <canvas> — 18×14 pixels, P=5px chacun.
const BOX_P = 5;
const BOX_COLS = 18;
const BOX_ROWS = 14;
const BOX_W = BOX_COLS * BOX_P;
const BOX_H = BOX_ROWS * BOX_P;

function drawBox(ctx, bright) {
  // [col, row] → couleur. Grille 18 × 14.
  const paint = (c, r, col) => {
    ctx.fillStyle = col;
    ctx.fillRect(c * BOX_P, r * BOX_P, BOX_P, BOX_P);
  };
  const fill = (c0, r0, c1, r1, col) => {
    for (let c = c0; c <= c1; c++)
      for (let r = r0; r <= r1; r++) paint(c, r, col);
  };
  const lighten = (hex, amt) => {
    const n = parseInt(hex.slice(1), 16);
    const r = Math.min(255, (n >> 16) + amt);
    const g = Math.min(255, ((n >> 8) & 0xff) + amt);
    const b = Math.min(255, (n & 0xff) + amt);
    return `rgb(${r},${g},${b})`;
  };
  const c = bright ? (h) => lighten(h, 28) : (h) => h;

  ctx.clearRect(0, 0, BOX_W, BOX_H);

  // Corps
  fill(0, 3, 17, 13, c("#787878"));
  // Reflet gauche
  fill(0, 3, 2, 10, c("#969696"));
  // Ombre droite
  fill(15, 3, 17, 13, c("#565656"));
  // Ombre bas
  fill(0, 11, 17, 13, c("#565656"));
  // Pli vertical
  for (let r = 3; r <= 13; r++) paint(9, r, c("#606060"));
  // Contour boîte
  for (let col = 0; col <= 17; col++) { paint(col, 3, "#333"); paint(col, 13, "#222"); }
  for (let row = 3; row <= 13; row++) { paint(0, row, "#333"); paint(17, row, "#333"); }

  // Rabat gauche (fermé)
  fill(0, 2, 8, 2, c("#8c6a38"));
  paint(0, 2, "#4a3010"); paint(8, 2, "#4a3010");

  // Rabat droit (ouvert / relevé)
  fill(9, 0, 17, 0, c("#c09040"));
  fill(9, 1, 17, 1, c("#a07830"));
  fill(9, 2, 17, 2, c("#805a20"));
  for (let row = 0; row <= 2; row++) { paint(9, row, "#3a2008"); paint(17, row, "#3a2008"); }
}

function CatBox({ onClickBox, catInBox }) {
  const canvasRef = useRef(null);
  const [hovered, setHovered] = useState(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    drawBox(ctx, hovered && !catInBox);
  }, [hovered, catInBox]);

  return (
    <div style={{
      position: "fixed",
      bottom: 20,
      right: 28,
      zIndex: 490,
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      gap: 5,
      userSelect: "none",
    }}>
      {!catInBox && (
        <span style={{
          fontFamily: "var(--mono)",
          fontSize: 9,
          letterSpacing: "0.18em",
          color: hovered ? "var(--accent)" : "var(--dim-1)",
          textTransform: "uppercase",
          transition: "color .15s",
          pointerEvents: "none",
        }}>▸ click</span>
      )}
      <canvas
        ref={canvasRef}
        width={BOX_W}
        height={BOX_H}
        onClick={catInBox ? undefined : onClickBox}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          cursor: catInBox ? "default" : "pointer",
          imageRendering: "pixelated",
          display: "block",
          filter: hovered && !catInBox ? "drop-shadow(0 0 5px var(--accent))" : "none",
          transition: "filter .15s",
        }}
      />
    </div>
  );
}

function CatSprite() {
  const [visible, setVisible] = useState(false);
  const [phase, setPhase] = useState("spawn");
  const [frame, setFrame] = useState(0);
  const [pos, setPos] = useState({ x: 100, y: 100 });
  const [target, setTarget] = useState({ x: 100, y: 100 });
  const [flipX, setFlipX] = useState(false);
  const posRef = useRef({ x: 100, y: 100 });
  const targetRef = useRef({ x: 100, y: 100 });
  const phaseRef = useRef("spawn");
  const frameRef = useRef(0);
  const dissolveTimerRef = useRef(null);

  const boxPos = useCallback(() => ({
    x: window.innerWidth - 28 - BOX_W / 2,
    y: window.innerHeight - 20 - BOX_H / 2,
  }), []);

  const goToBox = useCallback(() => {
    if (!visible) return;
    if (dissolveTimerRef.current) {
      clearTimeout(dissolveTimerRef.current);
      dissolveTimerRef.current = null;
    }
    const bp = boxPos();
    targetRef.current = bp;
    setTarget(bp);
    phaseRef.current = "tobox";
    setPhase("tobox");
    frameRef.current = 0;
  }, [visible, boxPos]);

  useEffect(() => {
    let timeout;
    const schedule = () => {
      const delay = 30000 + Math.random() * 30000;
      timeout = setTimeout(() => {
        if (!visible) {
          const startX = Math.random() > 0.5 ? window.innerWidth - 80 : 80;
          const startY = 120 + Math.random() * (window.innerHeight - 240);
          posRef.current = { x: startX, y: startY };
          targetRef.current = { x: startX, y: startY };
          setPos({ x: startX, y: startY });
          setTarget({ x: startX, y: startY });
          setPhase("spawn");
          phaseRef.current = "spawn";
          setFrame(0);
          frameRef.current = 0;
          setVisible(true);
        }
        schedule();
      }, delay);
    };
    timeout = setTimeout(() => {
      const startX = 100 + Math.random() * (window.innerWidth - 200);
      const startY = 120 + Math.random() * (window.innerHeight - 240);
      posRef.current = { x: startX, y: startY };
      targetRef.current = { x: startX, y: startY };
      setPos({ x: startX, y: startY });
      setPhase("spawn");
      phaseRef.current = "spawn";
      setFrame(0);
      setVisible(true);
      schedule();
    }, 8000);
    return () => clearTimeout(timeout);
  }, []);

  useEffect(() => {
    const onMove = (e) => {
      if (phaseRef.current === "follow") {
        targetRef.current = { x: e.clientX, y: e.clientY };
        setTarget({ x: e.clientX, y: e.clientY });
      }
    };
    window.addEventListener("mousemove", onMove);
    return () => window.removeEventListener("mousemove", onMove);
  }, []);

  useEffect(() => {
    if (!visible) return;
    let raf;
    const move = () => {
      const ph = phaseRef.current;
      if (ph === "follow" || ph === "tobox") {
        const dx = targetRef.current.x - posRef.current.x;
        const dy = targetRef.current.y - posRef.current.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist > 4) {
          const speed = ph === "tobox" ? Math.min(2.5, dist * 0.06) : Math.min(3, dist * 0.08);
          const nx = posRef.current.x + (dx / dist) * speed;
          const ny = posRef.current.y + (dy / dist) * speed;
          posRef.current = { x: nx, y: ny };
          setPos({ x: nx, y: ny });
          setFlipX(dx > 0);
        } else if (ph === "tobox") {
          phaseRef.current = "boxed";
          setPhase("boxed");
          frameRef.current = 0;
        }
      }
      raf = requestAnimationFrame(move);
    };
    raf = requestAnimationFrame(move);
    return () => cancelAnimationFrame(raf);
  }, [visible]);

  useEffect(() => {
    if (!visible) return;
    const spriteKey = phase === "tobox" ? "walk" : phase === "boxed" ? "idle" : phase;
    const sprite = CAT_SPRITES[spriteKey] || CAT_SPRITES.idle;
    const interval = 1000 / sprite.fps;
    const id = setInterval(() => {
      const next = frameRef.current + 1;
      if (next >= sprite.frames) {
        if (phase === "spawn") {
          phaseRef.current = "follow";
          setPhase("follow");
          frameRef.current = 0;
          dissolveTimerRef.current = setTimeout(() => {
            if (phaseRef.current !== "boxed" && phaseRef.current !== "tobox") {
              phaseRef.current = "dissolve";
              setPhase("dissolve");
              frameRef.current = 0;
            }
          }, 15000);
        } else if (phase === "dissolve") {
          setVisible(false);
          phaseRef.current = "spawn";
        } else {
          frameRef.current = 0;
          setFrame(0);
        }
      } else {
        frameRef.current = next;
        setFrame(next);
      }
    }, interval);
    return () => clearInterval(id);
  }, [visible, phase]);

  const size = CAT_FRAME_SIZE * CAT_SCALE;
  const isBoxed = phase === "boxed";
  const bp = boxPos();

  const spriteKey = phase === "tobox" ? "walk"
    : phase === "boxed" ? "idle"
    : phase === "follow"
      ? (Math.abs(pos.x - target.x) + Math.abs(pos.y - target.y) < 8 ? "idle" : "walk")
      : phase;
  const sprite = CAT_SPRITES[spriteKey] || CAT_SPRITES.idle;

  return (
    <>
      <CatBox onClickBox={goToBox} catInBox={isBoxed} />
      {visible && (
        <div
          style={{
            position: "fixed",
            left: isBoxed ? bp.x - size / 2 : pos.x - size / 2,
            top:  isBoxed ? bp.y - size / 2 : pos.y - size / 2,
            width: size,
            height: isBoxed ? size / 3 : size,
            zIndex: isBoxed ? 495 : 500,
            pointerEvents: "none",
            overflow: "hidden",
            transform: (isBoxed ? false : flipX) ? "scaleX(-1)" : "scaleX(1)",
            imageRendering: "pixelated",
          }}
        >
          <img
            src={sprite.src}
            alt=""
            style={{
              width: sprite.frames * size,
              height: size,
              transform: `translateX(-${frame * size}px)`,
              imageRendering: "pixelated",
              display: "block",
            }}
          />
        </div>
      )}
    </>
  );
}

// ─── AUDIO (procedural ambient) ─────────────────────────────────────────────
function useAmbient(on) {
  const ctxRef = useRef(null);
  useEffect(() => {
    if (!on) {
      if (ctxRef.current) {
        try { ctxRef.current.close(); } catch (e) {}
        ctxRef.current = null;
      }
      return;
    }
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    const ctx = new AC();
    ctxRef.current = ctx;
    // two slowly detuned sine drones + soft pink noise
    const master = ctx.createGain();
    master.gain.value = 0.0;
    master.gain.linearRampToValueAtTime(0.05, ctx.currentTime + 1.6);
    master.connect(ctx.destination);

    const drone = (freq, detune) => {
      const o = ctx.createOscillator();
      o.frequency.value = freq;
      o.detune.value = detune;
      const g = ctx.createGain();
      g.gain.value = 0.6;
      const lfo = ctx.createOscillator();
      lfo.frequency.value = 0.07;
      const lfoG = ctx.createGain();
      lfoG.gain.value = 0.25;
      lfo.connect(lfoG).connect(g.gain);
      lfo.start();
      o.connect(g).connect(master);
      o.start();
      return { o, lfo };
    };
    drone(54, -2);
    drone(81, 5);

    // pink-ish noise via white noise + lowpass
    const bufSize = 2 * ctx.sampleRate;
    const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * 0.3;
    const noise = ctx.createBufferSource();
    noise.buffer = buf;
    noise.loop = true;
    const lp = ctx.createBiquadFilter();
    lp.type = "lowpass";
    lp.frequency.value = 320;
    const ng = ctx.createGain();
    ng.gain.value = 0.15;
    noise.connect(lp).connect(ng).connect(master);
    noise.start();

    return () => {
      try { ctx.close(); } catch (e) {}
      ctxRef.current = null;
    };
  }, [on]);
}

// ─── APP ────────────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [lang, setLang] = useState("fr");
  const [bootDone, setBootDone] = useState(false);
  const [bootDurationMs] = useState(() => {
    try {
      // Returning visitor: short ceremony, not the whole boot.
      if (localStorage.getItem("dormant.booted") === "1") return 700;
    } catch (e) {}
    return 3000;
  });
  const [audioOn, setAudioOn] = useState(false);
  const [consoleOpen, setConsoleOpen] = useState(false);
  const [loreOpen, setLoreOpen] = useState(false);
  const [lore2Open, setLore2Open] = useState(false);
  const [wishlistOpen, setWishlistOpen] = useState(false);
  const [dotClicks, setDotClicks] = useState(0);
  const [toast, setToast] = useState(null);
  const text = copy[lang];

  useAmbient(audioOn);
  useKonami(() => setLoreOpen(true));

  // Tab title that occasionally glitches — the dormant cycle keeps going,
  // even when the player has switched tabs. Especially when they have.
  useEffect(() => {
    const cycle = [
      "DORMANT — idle game",
      "DORMANT_",
      "D0RMÄNT",
      "are you still there?",
      "DORMANT",
      "4382 days",
      "DORMANT — idle game",
    ];
    let i = 0;
    const id = setInterval(() => {
      document.title = cycle[i % cycle.length];
      i++;
    }, 4200);
    return () => clearInterval(id);
  }, []);

  // Right-click anywhere outside controls reveals a one-liner instead of the menu.
  useEffect(() => {
    const lines = lang === "en"
      ? [
          "there is nothing here to copy. you already have what you need.",
          "the menu is not available. neither is the operator.",
          "sero received your gesture. logged. ignored.",
          "if you want a copy of dormant, the steam wishlist is one click away.",
        ]
      : [
          "il n'y a rien à copier ici. tu as déjà ce qu'il te faut.",
          "le menu n'est pas disponible. l'opérateur non plus.",
          "sero a reçu ton geste. consigné. ignoré.",
          "si tu veux une copie de dormant, la liste de souhaits steam est à un clic.",
        ];
    const onMenu = (e) => {
      const tgt = e.target;
      if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA")) return;
      e.preventDefault();
      const msg = lines[Math.floor(Math.random() * lines.length)];
      const tip = document.createElement("div");
      tip.className = "ghost-tip";
      tip.textContent = "> " + msg;
      tip.style.left = e.clientX + "px";
      tip.style.top = e.clientY + "px";
      document.body.appendChild(tip);
      setTimeout(() => { tip.style.opacity = "0"; }, 1800);
      setTimeout(() => tip.remove(), 2600);
    };
    window.addEventListener("contextmenu", onMenu);
    return () => window.removeEventListener("contextmenu", onMenu);
  }, [lang]);

  // Show transient toast at top-right.
  const showToast = useCallback((msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 3400);
  }, []);

  // Click the supervisor dot 7 times to coax a response.
  const onDotClick = () => {
    const n = dotClicks + 1;
    setDotClicks(n);
    if (n === 3) showToast(lang === "en" ? "> sero registered your touch." : "> sero a senti ton contact.");
    else if (n === 7) showToast(lang === "en" ? "> sero says: i am awake. thank you for asking." : "> sero dit\u00a0: je suis réveillé. merci d'avoir demandé.");
    else if (n === 12) {
      showToast(lang === "en" ? "> ...too many. please stop. (kidding.)" : "> ...trop. arrête. (je rigole.)");
      setDotClicks(0);
    }
  };

  // global `help` keyboard easter-egg: typing h-e-l-p outside of inputs
  useEffect(() => {
    let buf = "";
    let timer = null;
    const onKey = (e) => {
      const tgt = e.target;
      if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
      if (e.key.length !== 1) return;
      buf = (buf + e.key.toLowerCase()).slice(-4);
      if (buf === "help") setConsoleOpen(true);
      clearTimeout(timer);
      timer = setTimeout(() => { buf = ""; }, 1200);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // Push palette + tweak settings as CSS vars on <html> so all components can read them.
  useEffect(() => {
    const [a, b, c] = Array.isArray(t.palette) ? t.palette : PALETTES.teal;
    const root = document.documentElement;
    root.style.setProperty("--accent", a);
    root.style.setProperty("--text", b);
    root.style.setProperty("--ambient", c);
    root.dataset.scan = t.scanlines;
    root.dataset.density = t.density;
    root.dataset.herolayout = t.heroLayout;
    root.dataset.curve = t.crtCurvature ? "1" : "0";
    root.style.setProperty("--title-font", t.titleFont);
  }, [t]);

  return (
    <>
      {!bootDone && (
        <BootScreen
          lines={text.boot}
          skipHint={text.skipHint}
          durationMs={bootDurationMs}
          onDone={() => {
            setBootDone(true);
            try { localStorage.setItem("dormant.booted", "1"); } catch (e) {}
          }}
        />
      )}

      <div className={`page ${bootDone ? "page--on" : ""}`}>
        {/* TOP BAR */}
        <header className="topbar">
          <a href="#top" className="topbar__brand">
            <button
              className="topbar__dot"
              onClick={(e) => { e.preventDefault(); onDotClick(); }}
              aria-label="supervisor status"
              title="supervisor"
            />
            <span style={{fontSize:"14px", color:"var(--dim-1)", letterSpacing:"0.12em"}}>[ MOON ROOT ]</span>
            DORMANT
            <span className="topbar__sub">// SERO supervisor</span>
          </a>
          <nav className="topbar__nav">
            <a href="#story">{text.nav.story}</a>
            <a href="#loop">{text.nav.loop}</a>
            <a href="#chapters">{lang === "en" ? "chapters" : "chapitres"}</a>
            <a href="#features">{text.nav.features}</a>
            <a href="#faq">faq</a>
            <a href="#about">{text.nav.about}</a>
            <a href="#moonroot">{lang === "en" ? "studio" : "studio"}</a>
          </nav>
          <div className="topbar__util">
            <button
              className="lang"
              onClick={() => setLang(lang === "en" ? "fr" : "en")}
              aria-label="toggle language"
            >
              [{lang.toUpperCase()}]
            </button>
            <button
              className="lang"
              onClick={() => setConsoleOpen(true)}
              aria-label="open console"
              title="type `help` anywhere"
            >
              [?]
            </button>
          </div>
        </header>

        <Hero t={text} lang={lang} audioOn={audioOn} onToggleAudio={() => setAudioOn(!audioOn)} onWishlist={() => setWishlistOpen(true)} />
        <Story t={text} />
        <Pause t={text} />
        <Loop t={text} />
        <Chapters t={text} />
        <Features t={text} />
        <Captures t={text} lang={lang} />
        <Devlog t={text} />
        <FAQ t={text} />
        <About t={text} />
        <EndingChoice t={text} />
        <MoonRoot lang={lang} />
        <Footer t={text} lang={lang} onWishlist={() => setWishlistOpen(true)} />

        {/* CRT overlays */}
        <div className="crt-overlay" aria-hidden="true">
          <div className="crt-scan" />
          <div className="crt-flicker" />
          <div className="crt-vignette" />
        </div>
      </div>

      <HelpConsole open={consoleOpen} onClose={() => setConsoleOpen(false)} t={text} lang={lang} onUnlockFragment={() => setLore2Open(true)} />
      <LoreOverlay open={loreOpen} onClose={() => setLoreOpen(false)} lang={lang} />
      <LoreOverlay2 open={lore2Open} onClose={() => setLore2Open(false)} lang={lang} />
      <WishlistOverlay open={wishlistOpen} onClose={() => setWishlistOpen(false)} lang={lang} />

      {toast && <div className="toast">{toast}<span className="cur">_</span></div>}
      <CatSprite />

      <TweaksPanel title="Tweaks">
        <TweakSection label="palette" />
        <TweakColor
          label="accent"
          value={t.palette}
          options={[PALETTES.teal, PALETTES.amber, PALETTES.white, PALETTES.ibm]}
          onChange={(v) => setTweak("palette", v)}
        />
        <TweakSection label="CRT" />
        <TweakRadio
          label="scanlines"
          value={t.scanlines}
          options={["off", "subtle", "heavy"]}
          onChange={(v) => setTweak("scanlines", v)}
        />
        <TweakToggle
          label="curvature"
          value={t.crtCurvature}
          onChange={(v) => setTweak("crtCurvature", v)}
        />
        <TweakSection label="type" />
        <TweakSelect
          label="title font"
          value={t.titleFont}
          options={["VT323", "IBM Plex Mono", "JetBrains Mono"]}
          onChange={(v) => setTweak("titleFont", v)}
        />
        <TweakRadio
          label="density"
          value={t.density}
          options={["verbose", "dry", "minimal"]}
          onChange={(v) => setTweak("density", v)}
        />
        <TweakSection label="hero" />
        <TweakRadio
          label="layout"
          value={t.heroLayout}
          options={["centered", "split"]}
          onChange={(v) => setTweak("heroLayout", v)}
        />
        <TweakSection label="audio" />
        <TweakToggle
          label="ambient drone"
          value={audioOn}
          onChange={(v) => setAudioOn(v)}
        />
      </TweaksPanel>
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

