// app.jsx — Mother's Day Trivia for Demetria
const { useState, useEffect, useRef, useCallback } = React;

// ───────────────────────────── Tweakable defaults ─────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "momName": "Demetria",
  "palette": "teal",
  "dark": false,
  "spinSpeed": 3000
}/*EDITMODE-END*/;

const PALETTES = {
  teal:   { primary: "#1F6F73", primaryDeep: "#16585C", primarySoft: "#7FB4B6", primaryTint: "#DDEDEC", accent: "#4F7A52", accentDeep: "#36603A", accentTint: "#E1ECDD" },
  rose:   { primary: "#C4456B", primaryDeep: "#A03555", primarySoft: "#E89AB1", primaryTint: "#F7DCE5", accent: "#7A8B5C", accentDeep: "#5C6A44", accentTint: "#E6EBD9" },
  amber:  { primary: "#C4762E", primaryDeep: "#A05E1F", primarySoft: "#E5B47A", primaryTint: "#F4E2C5", accent: "#2A6F70", accentDeep: "#1F5859", accentTint: "#D8E8E8" },
  indigo: { primary: "#3A5A8A", primaryDeep: "#2A4368", primarySoft: "#9AB0CC", primaryTint: "#DCE4F1", accent: "#7B5C8B", accentDeep: "#5E4569", accentTint: "#E5DDEC" },
};

// ───────────────────────────── Sound effects ─────────────────────────────
// Tiny Web Audio SFX synthesizer — no asset files needed.
const sfxCtx = (() => {
  let ctx = null;
  return () => {
    if (!ctx) {
      const Ctx = window.AudioContext || window.webkitAudioContext;
      if (Ctx) ctx = new Ctx();
    }
    if (ctx && ctx.state === "suspended") ctx.resume();
    return ctx;
  };
})();

function playSFX(kind, idx = 0) {
  const ctx = sfxCtx();
  if (!ctx) return;
  const now = ctx.currentTime;
  const master = ctx.createGain();
  master.gain.value = 0.18;
  master.connect(ctx.destination);

  const blip = (freq, when, dur, type = "sine", vol = 1) => {
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, when);
    g.gain.setValueAtTime(0, when);
    g.gain.linearRampToValueAtTime(vol, when + 0.005);
    g.gain.exponentialRampToValueAtTime(0.001, when + dur);
    o.connect(g).connect(master);
    o.start(when);
    o.stop(when + dur + 0.02);
  };

  if (kind === "correct") {
    // Bright triadic upward arpeggio (C-E-G-C)
    blip(523.25, now + 0.00, 0.18, "triangle", 0.9); // C5
    blip(659.25, now + 0.07, 0.18, "triangle", 0.9); // E5
    blip(783.99, now + 0.14, 0.20, "triangle", 0.9); // G5
    blip(1046.5, now + 0.22, 0.30, "triangle", 1.0); // C6
    // Subtle sine sparkle on top
    blip(2093.0, now + 0.22, 0.25, "sine", 0.35);
  } else if (kind === "incorrect") {
    // Soft minor descent — gentle, not punishing
    blip(392.00, now + 0.00, 0.18, "sine", 0.8); // G4
    blip(329.63, now + 0.10, 0.22, "sine", 0.8); // E4
    blip(293.66, now + 0.22, 0.30, "sine", 0.8); // D4
  } else if (kind === "coin") {
    // Classic two-pitch coin "ting"
    const base = 988 + (idx % 3) * 60; // varies slightly per coin
    blip(base, now + 0.00, 0.07, "square", 0.45);
    blip(base * 1.5, now + 0.06, 0.18, "square", 0.45);
    // metallic sparkle
    blip(base * 3, now + 0.06, 0.10, "sine", 0.18);
  } else if (kind === "needle-down") {
    // Vinyl pop + scratch when needle drops onto record
    // Sharp transient
    const click = ctx.createOscillator();
    const cg = ctx.createGain();
    click.type = "square";
    click.frequency.setValueAtTime(80, now);
    cg.gain.setValueAtTime(0.4, now);
    cg.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
    click.connect(cg).connect(master);
    click.start(now); click.stop(now + 0.05);
    // Short crackle burst (filtered noise)
    const buf = ctx.createBuffer(1, ctx.sampleRate * 0.5, ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < data.length; i++) {
      const t = i / data.length;
      data[i] = (Math.random() * 2 - 1) * (1 - t) * (Math.random() < 0.15 ? 1 : 0.25);
    }
    const noise = ctx.createBufferSource();
    noise.buffer = buf;
    const hp = ctx.createBiquadFilter();
    hp.type = "highpass"; hp.frequency.value = 1500;
    const ng = ctx.createGain();
    ng.gain.setValueAtTime(0.6, now);
    ng.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
    noise.connect(hp).connect(ng).connect(master);
    noise.start(now); noise.stop(now + 0.55);
  } else if (kind === "needle-up") {
    // Brief upward-pitched scratch as needle lifts off
    const buf = ctx.createBuffer(1, ctx.sampleRate * 0.25, ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < data.length; i++) {
      data[i] = (Math.random() * 2 - 1) * 0.6;
    }
    const noise = ctx.createBufferSource();
    noise.buffer = buf;
    const bp = ctx.createBiquadFilter();
    bp.type = "bandpass"; bp.frequency.setValueAtTime(800, now);
    bp.frequency.exponentialRampToValueAtTime(3500, now + 0.18);
    bp.Q.value = 6;
    const ng = ctx.createGain();
    ng.gain.setValueAtTime(0.5, now);
    ng.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
    noise.connect(bp).connect(ng).connect(master);
    noise.start(now); noise.stop(now + 0.25);
  } else if (kind === "cashin") {
    // Cash-register "kerching" — short low thunk, brassy bell, sparkle, two coin pings.
    blip(180,     now + 0.00, 0.05, "square",   0.45); // drawer thunk
    blip(1318.51, now + 0.04, 0.45, "triangle", 0.55); // E6 — bell body
    blip(1975.53, now + 0.05, 0.50, "sine",     0.40); // B6 — bell harmonic
    blip(2637.02, now + 0.10, 0.30, "sine",     0.30); // E7 — sparkle on top
    blip(987.77,  now + 0.22, 0.14, "square",   0.30); // B5 — coin clink 1
    blip(1567.98, now + 0.32, 0.16, "square",   0.28); // G6 — coin clink 2
  } else if (kind === "scrub") {
    // Tape-style FF/RW whoosh — a short pitched noise sweep
    const dir = idx >= 0 ? 1 : -1; // 1 = ff, -1 = rewind
    const buf = ctx.createBuffer(1, ctx.sampleRate * 0.18, ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < data.length; i++) {
      data[i] = (Math.random() * 2 - 1) * 0.7;
    }
    const noise = ctx.createBufferSource();
    noise.buffer = buf;
    const bp = ctx.createBiquadFilter();
    bp.type = "bandpass"; bp.Q.value = 4;
    const fStart = dir > 0 ? 1200 : 3000;
    const fEnd   = dir > 0 ? 3000 : 1200;
    bp.frequency.setValueAtTime(fStart, now);
    bp.frequency.exponentialRampToValueAtTime(fEnd, now + 0.14);
    const ng = ctx.createGain();
    ng.gain.setValueAtTime(0.0, now);
    ng.gain.linearRampToValueAtTime(0.45, now + 0.02);
    ng.gain.exponentialRampToValueAtTime(0.001, now + 0.16);
    noise.connect(bp).connect(ng).connect(master);
    noise.start(now); noise.stop(now + 0.18);
  }
}

// ───────────────────────────── Main App ─────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Apply palette to CSS vars
  useEffect(() => {
    const p = PALETTES[t.palette] || PALETTES.teal;
    const r = document.documentElement.style;
    r.setProperty("--primary", p.primary);
    r.setProperty("--rose", p.primary);
    r.setProperty("--rose-deep", p.primaryDeep);
    r.setProperty("--rose-soft", p.primarySoft);
    r.setProperty("--rose-tint", p.primaryTint);
    r.setProperty("--accent", p.accent);
    r.setProperty("--sage", p.accent);
    r.setProperty("--sage-deep", p.accentDeep);
    r.setProperty("--sage-tint", p.accentTint);
    r.setProperty("--dur-spin", `${t.spinSpeed}ms`);
  }, [t.palette, t.spinSpeed]);

  // ── Game state ──
  const [screen, setScreen] = useState("welcome"); // welcome, crate, playing, question, shop
  const [currentId, setCurrentId] = useState(null);
  const [played, setPlayed] = useState({});        // { id: {correct: bool} }
  const [picked, setPicked] = useState(null);
  const [audioPlaying, setAudioPlaying] = useState(false);
  const [notes, setNotes] = useState(0);
  const [purchased, setPurchased] = useState({});  // { itemId: true }
  const [redeemed, setRedeemed] = useState({});    // { itemId: true }
  const [redeemingItem, setRedeemingItem] = useState(null);
  const [pillBump, setPillBump] = useState(false);
  const [confirmItem, setConfirmItem] = useState(null);
  const [pendingPurchase, setPendingPurchase] = useState(null);
  const [flyingNotes, setFlyingNotes] = useState([]);
  const [crateSide, setCrateSide] = useState("A");
  // Bumped each time the user clicks the side toggle, so the Crate's
  // scroll effect re-fires even if crateSide hasn't actually changed.
  const [crateScrollTick, setCrateScrollTick] = useState(0);
  // Tracks whether the user has completed each turntable tutorial step at
  // least once. Used to disable the pulsing instruction after first success.
  const [tutorialDone, setTutorialDone] = useState({ sleeve: false, loaded: false });
  // True once the user has dismissed the first-visit greeting popup.
  const [firstVisitGreeted, setFirstVisitGreeted] = useState(false);
  // Whether the user is allowed to advance from the welcome screen. Closes
  // briefly while the greeting plays — opens at 1/8 of duration (or when the
  // audio ends/errors). Defaults open so non-first visits never block.
  const [greetingGateOpen, setGreetingGateOpen] = useState(true);
  // Holds the greeting Audio element so we can stop it when the user clicks
  // "Drop the needle" or otherwise leaves the welcome screen.
  const greetingAudioRef = useRef(null);

  const current = QUESTIONS.find(q => q.id === currentId);
  const total = QUESTIONS.length;
  const playedCount = Object.keys(played).length;

  // Persist to localStorage so refresh doesn't reset
  useEffect(() => {
    const saved = localStorage.getItem("mdt_state");
    if (saved) {
      try {
        const s = JSON.parse(saved);
        if (s.played) setPlayed(s.played);
        if (typeof s.notes === "number") setNotes(s.notes);
        if (s.purchased) setPurchased(s.purchased);
        if (s.redeemed) setRedeemed(s.redeemed);
        if (s.tutorialDone) setTutorialDone(td => ({ ...td, ...s.tutorialDone }));
        if (typeof s.firstVisitGreeted === "boolean") setFirstVisitGreeted(s.firstVisitGreeted);
      } catch {}
    }
  }, []);
  useEffect(() => {
    localStorage.setItem("mdt_state", JSON.stringify({ played, notes, purchased, redeemed, tutorialDone, firstVisitGreeted }));
  }, [played, notes, purchased, redeemed, tutorialDone, firstVisitGreeted]);

  // (Audio "finishing" is now driven inside <Playing> via the tonearm interaction.)

  // ── Actions ──
  const pickRecord = useCallback((id) => {
    const q = QUESTIONS.find(qq => qq.id === id);
    if (!q) return;
    // Block locked bonus tracks
    if (q.bonus) {
      const unlocked = QUESTIONS.filter(qq => !qq.bonus).every(qq => {
        const p = played[qq.id];
        return p && p.correct;
      });
      if (!unlocked) return;
    }
    setCurrentId(id);
    setPicked(null);
    setScreen("playing");
    setAudioPlaying(true);
  }, [played]);

  const answer = useCallback((i) => {
    setPicked(i);
    const wasCorrect = !!(played[current.id] && played[current.id].correct);
    if (i === current.correct) {
      playSFX("correct");
      // Only award notes / fly the celebration the first time they get it right.
      if (!wasCorrect) {
        setNotes(n => n + 5);
        setPillBump(true);
        setTimeout(() => setPillBump(false), 600);
        launchFlyingNotes();
      }
      setPlayed(p => ({ ...p, [current.id]: { correct: true } }));
    } else {
      playSFX("incorrect");
      // Don't downgrade a previously-correct answer if they replay and miss.
      if (!wasCorrect) {
        setPlayed(p => ({ ...p, [current.id]: { correct: false } }));
      }
    }
  }, [current, played]);

  const launchFlyingNotes = () => {
    // Find pill location relative to viewport, and origin near center
    const pill = document.querySelector(".notes-pill");
    if (!pill) return;
    const pr = pill.getBoundingClientRect();
    const ox = window.innerWidth / 2;
    const oy = window.innerHeight / 2;
    const flies = [];
    for (let k = 0; k < 5; k++) {
      flies.push({
        id: Date.now() + k,
        x: ox + (Math.random() - 0.5) * 60,
        y: oy + (Math.random() - 0.5) * 40,
        dx: (pr.left + pr.width / 2) - ox + (Math.random() - 0.5) * 30,
        dy: (pr.top + pr.height / 2) - oy + (Math.random() - 0.5) * 16,
        delay: k * 70,
      });
      // Schedule a coin "ting" for each note's arrival
      setTimeout(() => playSFX("coin", k), 700 + k * 70);
    }
    setFlyingNotes(flies);
    setTimeout(() => setFlyingNotes([]), 1400);
  };

  const continueAfterReveal = () => {
    const triviaTotal = QUESTIONS.filter(q => !q.bonus).length;
    const triviaPlayed = QUESTIONS.filter(q => !q.bonus && played[q.id]).length;
    if (triviaPlayed >= triviaTotal) {
      setScreen("shop");
    } else {
      setScreen("crate");
    }
    setPicked(null);
  };

  const buy = (item) => {
    if (purchased[item.id] || notes < item.cost) return;
    setPendingPurchase(item);
  };

  const finalizePurchase = () => {
    const item = pendingPurchase;
    if (!item) return;
    if (purchased[item.id] || notes < item.cost) {
      setPendingPurchase(null);
      return;
    }
    setNotes(n => n - item.cost);
    setPurchased(p => ({ ...p, [item.id]: true }));
    setPendingPurchase(null);
    setConfirmItem(item);
    playSFX("cashin");
  };

  const redeem = (item) => {
    if (!purchased[item.id] || redeemed[item.id]) return;
    setRedeemed(r => ({ ...r, [item.id]: true }));
  };

  const restart = () => {
    setPlayed({}); setNotes(0); setPurchased({}); setRedeemed({}); setPicked(null);
    setCurrentId(null); setScreen("welcome"); setCrateSide("A");
    setTutorialDone({ sleeve: false, loaded: false });
    setFirstVisitGreeted(false);
    setGreetingGateOpen(true);
    stopGreetingAudio();
    localStorage.removeItem("mdt_state");
  };

  const stopGreetingAudio = () => {
    const a = greetingAudioRef.current;
    if (!a) return;
    try { a.pause(); a.currentTime = 0; a.src = ""; } catch (e) {}
    greetingAudioRef.current = null;
  };

  const dismissGreeting = () => {
    setFirstVisitGreeted(true);
    // Close the "Drop the needle" gate while the greeting plays. Opens again
    // at 1/8 of the duration (or on natural end / playback error / audio
    // failure) so the user can't bail before they've heard a beat or two.
    setGreetingGateOpen(false);
    // Auto-play the greeting. The user click that fired this handler counts
    // as the user gesture browsers require for autoplay, so .play() should resolve.
    try {
      const a = new Audio("assets/audio/greeting.mp3");
      a.volume = 0.95;
      greetingAudioRef.current = a;
      const openGate = () => setGreetingGateOpen(true);
      a.addEventListener("timeupdate", () => {
        const d = a.duration;
        // Known + finite duration → wait until 1/8 elapsed.
        // Unknown / VBR mp3s without header duration → fall back to 3 s of
        // playback so we never strand the user.
        if (isFinite(d) && d > 0) {
          if (a.currentTime >= d / 8) openGate();
        } else if (a.currentTime >= 3) {
          openGate();
        }
      });
      a.addEventListener("ended", () => {
        openGate();
        if (greetingAudioRef.current === a) greetingAudioRef.current = null;
      });
      a.addEventListener("error", openGate);
      a.play().catch((e) => {
        console.warn("Greeting audio could not auto-play:", e);
        openGate();
      });
    } catch (e) {
      console.warn("Greeting audio failed:", e);
      setGreetingGateOpen(true);
    }
  };

  const markTutorial = (step) => {
    setTutorialDone(td => td[step] ? td : { ...td, [step]: true });
  };

  const toggleCrateSide = () => {
    setCrateSide(s => (s === "A" ? "B" : "A"));
    setCrateScrollTick(n => n + 1);
  };

  const triviaTotal = QUESTIONS.filter(q => !q.bonus).length;
  const triviaPlayedCount = QUESTIONS.filter(q => !q.bonus && played[q.id]).length;
  const allPlayed = triviaPlayedCount >= triviaTotal;
  // Bonus tracks unlock only when EVERY regular track has been answered correctly
  const bonusUnlocked = QUESTIONS.filter(q => !q.bonus).every(q => played[q.id] && played[q.id].correct);
  const isDark = t.dark;

  return (
    <div className={`app ${isDark ? "dark" : ""}`}>
      {/* Topbar — never on welcome */}
      {screen !== "welcome" && (
        <TopBar
          left={
            screen === "crate" ? (
              <Brand
                side={crateSide}
                onClick={allPlayed ? toggleCrateSide : undefined}
              />
            ) :
            screen === "shop"  ? <Brand /> :
            screen === "redeem" ? <BackBtn label="Back" onClick={() => setScreen("shop")} /> :
            screen === "playing" ? <BackBtn label="Back" onClick={() => setScreen("crate")} /> :
            screen === "question" ? <BackBtn label="Back" onClick={() => setScreen("crate")} /> :
            null
          }
          right={
            <div className="topbar-right">
              <SettingsCog onClick={() => setScreen("welcome")} />
              <NotesPill
                notes={notes}
                bumped={pillBump}
                onClick={screen === "shop" ? undefined : () => setScreen("shop")}
              />
            </div>
          }
        />
      )}

      {/* Screen body */}
      {screen === "welcome" && (
        <Welcome
          momName={t.momName}
          onStart={() => { stopGreetingAudio(); setScreen("crate"); }}
          t={t}
          setTweak={setTweak}
          canStart={greetingGateOpen}
        />
      )}

      {screen === "welcome" && !firstVisitGreeted && (
        <GreetingPopup onReady={dismissGreeting} />
      )}

      {screen === "crate" && (
        <Crate
          played={played}
          allPlayed={allPlayed}
          bonusUnlocked={bonusUnlocked}
          onPick={pickRecord}
          onShop={() => setScreen("shop")}
          notes={notes}
          crateSide={crateSide}
          scrollTick={crateScrollTick}
        />
      )}

      {screen === "playing" && current && (
        <Playing
          q={current}
          tutorialDone={tutorialDone}
          markTutorial={markTutorial}
          onSkip={() => {
            setAudioPlaying(false);
            if (current && current.bonus) { setScreen("crate"); }
            else { setScreen("question"); }
          }}
          onAdvance={() => {
            setAudioPlaying(false);
            if (current && current.bonus) {
              // Bonus track: mark as "played" (no correctness), go back to crate.
              setPlayed(p => ({ ...p, [current.id]: { bonus: true } }));
              setScreen("crate");
            } else {
              setScreen("question");
            }
          }}
        />
      )}

      {screen === "question" && current && (
        <QuestionScreen
          q={current}
          picked={picked}
          onAnswer={answer}
          onContinue={continueAfterReveal}
          isLast={QUESTIONS.filter(q => !q.bonus && played[q.id]).length >= QUESTIONS.filter(q => !q.bonus).length}
        />
      )}

      {screen === "shop" && (
        <Shop
          notes={notes}
          purchased={purchased}
          redeemed={redeemed}
          allPlayed={allPlayed}
          bonusUnlocked={bonusUnlocked}
          onBuy={buy}
          onBack={() => setScreen("crate")}
          onPlayAgain={restart}
          onRedeem={() => setScreen("redeem")}
          onBonus={() => {
            if (!bonusUnlocked) return;
            setCrateSide("B");
            setCrateScrollTick(n => n + 1);
            setScreen("crate");
          }}
          momName={t.momName}
        />
      )}

      {screen === "redeem" && (
        <RedeemList
          purchased={purchased}
          redeemed={redeemed}
          onPick={(item) => setRedeemingItem(item)}
          onBack={() => setScreen("shop")}
        />
      )}

      {/* Flying notes */}
      {flyingNotes.map(f => (
        <span
          key={f.id}
          className="note-fly"
          style={{
            left: f.x, top: f.y,
            "--dx": `${f.dx}px`,
            "--dy": `${f.dy}px`,
            animationDelay: `${f.delay}ms`,
          }}
        >♪</span>
      ))}

      {/* Buy confirmation — appears BEFORE notes are deducted */}
      {pendingPurchase && (
        <BuyConfirm
          item={pendingPurchase}
          notes={notes}
          momName={t.momName}
          onCancel={() => setPendingPurchase(null)}
          onConfirm={finalizePurchase}
        />
      )}

      {/* Press celebration — appears AFTER purchase is finalized */}
      {confirmItem && (
        <ConfirmPurchase item={confirmItem} onClose={() => setConfirmItem(null)} momName={t.momName} />
      )}

      {/* Redeem certificate modal */}
      {redeemingItem && (
        <RedeemConfirm
          item={redeemingItem}
          redeemed={!!redeemed[redeemingItem.id]}
          momName={t.momName}
          onClose={() => setRedeemingItem(null)}
          onRedeem={() => redeem(redeemingItem)}
        />
      )}

      {/* Tweaks panel */}
      <TweaksPanel>
        <TweakSection label="Personalize" />
        <TweakText label="Mom's name" value={t.momName} onChange={v => setTweak("momName", v)} />
        <TweakSection label="Palette" />
        <TweakColor
          label="Accent"
          value={PALETTES[t.palette]?.primary}
          options={Object.values(PALETTES).map(p => p.primary)}
          onChange={(hex) => {
            const key = Object.keys(PALETTES).find(k => PALETTES[k].primary === hex) || "teal";
            setTweak("palette", key);
          }}
        />
        <TweakToggle label="Dark mode" value={t.dark} onChange={v => setTweak("dark", v)} />
        <TweakSection label="Motion" />
        <TweakSlider label="Spin speed" value={t.spinSpeed} min={900} max={3600} step={100} unit="ms"
                     onChange={v => setTweak("spinSpeed", v)} />
        <TweakSection label="Game" />
        <TweakButton label="Restart game" onClick={restart} />
      </TweaksPanel>
    </div>
  );
}

// ───────────────────────────── Welcome ─────────────────────────────
function Welcome({ momName, onStart, t, setTweak, canStart = true }) {
  return (
    <div className="welcome">
      <div className="welcome-meta">A Mother's Day Pressing · 2026</div>
      <div className="hero-stack">
        <div className="sleeve-bg"><div className="rings" /></div>
        <div className="vinyl-hero"><div className="label" /></div>
      </div>
      <div>
        <h1 className="h-display">Hi {momName}.<br/><em>We made a few records for you.</em></h1>
        <p className="body-lead" style={{ marginTop: 14, maxWidth: 320, marginLeft: "auto", marginRight: "auto" }}>
          5 tracks. Each one is a question you should know. Get them right and earn music notes. These notes are currency in the shop on Side B.
        </p>
      </div>
      <button className="btn btn-primary" onClick={onStart} disabled={!canStart}>
        {canStart ? "Drop the needle" : "Listening…"}
      </button>

      {/* Inline tweak strip — palette / dark / spin speed */}
      <div className="welcome-tweaks">
        <div className="wt-row">
          <span className="wt-label">Palette</span>
          <div className="wt-swatches">
            {Object.keys(PALETTES).map(key => (
              <button
                key={key}
                type="button"
                className={`wt-swatch ${t.palette === key ? "active" : ""}`}
                style={{ background: PALETTES[key].primary }}
                aria-label={key}
                onClick={() => setTweak("palette", key)}
              />
            ))}
          </div>
        </div>
        <div className="wt-row">
          <span className="wt-label">Dark mode</span>
          <button
            type="button"
            className={`wt-toggle ${t.dark ? "on" : ""}`}
            onClick={() => setTweak("dark", !t.dark)}
            aria-pressed={t.dark}
          >
            <span className="wt-knob" />
          </button>
        </div>
        <div className="wt-row">
          <span className="wt-label">Spin speed</span>
          <div className="wt-slider">
            <input
              type="range"
              min={900}
              max={3600}
              step={100}
              /* slider is inverted: dragging right reduces the spin duration so the record spins faster */
              value={4500 - t.spinSpeed}
              onChange={e => setTweak("spinSpeed", 4500 - Number(e.target.value))}
            />
            <span className="wt-val">{Math.round(60000 / t.spinSpeed)} rpm</span>
          </div>
        </div>
      </div>

      <div className="welcome-credits">
        <div className="lbl">Voices on the records</div>
        <div>Elijah · Lorelei · Jackson · Brandon</div>
      </div>
    </div>
  );
}

// ───────────────────────────── Crate ─────────────────────────────
function Crate({ played, allPlayed, bonusUnlocked, onPick, onShop, notes, crateSide = "A", scrollTick = 0 }) {
  const sideA = QUESTIONS.filter(q => q.side === "A");
  const sideB = QUESTIONS.filter(q => q.side === "B");
  const triviaTotal = QUESTIONS.filter(q => !q.bonus).length;
  const playedCount = QUESTIONS.filter(q => !q.bonus && played[q.id]).length;
  const screenRef = useRef(null);
  const sideADividerRef = useRef(null);
  const sideBDividerRef = useRef(null);

  // When the topbar side toggle fires, scroll the appropriate divider into view.
  useEffect(() => {
    if (scrollTick === 0) return; // skip on initial mount
    const target = crateSide === "B" ? sideBDividerRef.current : sideADividerRef.current;
    if (target && target.scrollIntoView) {
      target.scrollIntoView({ behavior: "smooth", block: "start" });
    }
  }, [crateSide, scrollTick]);

  return (
    <div className="screen" ref={screenRef}>
      <div className="crate-head">
        <div>
          <div className="eyebrow">The Crate</div>
          <h2 className="h-display" style={{ fontSize: 30, marginTop: 4 }}>Pick a track.</h2>
        </div>
      </div>
      <p className="body-lead" style={{ fontSize: 15 }}>
        {playedCount === 0 && "FIVE tracks waiting. Each correct answer is worth five notes. Answer them all correctly and unlock the hidden tracks on Side B."}
        {playedCount > 0 && playedCount < triviaTotal && `${playedCount} of ${triviaTotal} played. ${notes} notes in the bank.`}
        {allPlayed && "That's all of the tracks. Head to the shop on Side B."}
      </p>
      <div className="progress-track">
        <div className="fill" style={{ width: `${(playedCount/triviaTotal)*100}%` }} />
      </div>

      <div className="crate-list">
        <div className="side-divider" ref={sideADividerRef}><span>Side A</span></div>
        {sideA.map(q => (
          <Sleeve key={q.id} q={q} played={!!played[q.id]} onPick={() => onPick(q.id)} />
        ))}

        <div className="side-divider" ref={sideBDividerRef}><span>Side B</span></div>
        {sideB.map(q => (
          <Sleeve
            key={q.id}
            q={q}
            played={!!played[q.id]}
            locked={q.bonus && !bonusUnlocked}
            onPick={() => onPick(q.id)}
          />
        ))}

        {allPlayed && (
          <div className="endside-card">
            <div className="ribbon">End of the game</div>
            <h3>The shop is open.</h3>
            <p>You earned {notes} notes. Spend them on certificates.</p>
            <div className="actions">
              <button className="btn btn-primary" onClick={onShop}>Go to the shop</button>
            </div>
          </div>
        )}

        {!allPlayed && playedCount > 0 && (
          <div className="endside-card">
            <div className="ribbon">Skip ahead</div>
            <h3>Want to peek at the shop?</h3>
            <p>You can spend notes anytime. The shop stays open between tracks.</p>
            <div className="actions">
              <button className="btn btn-ghost" onClick={onShop}>Visit the shop</button>
            </div>
          </div>
        )}

        <div className="spacer-3" />
      </div>
    </div>
  );
}

// ───────────────────────────── Now Playing (tactile) ─────────────────────────────
// Stages:
//   1. "sleeve"  — vinyl peeking out of its sleeve, drag it to the platter
//   2. "loaded"  — vinyl on platter, idle (paused). Drag the tonearm to play.
//   3. "playing" — tonearm dropped, record spinning, audio "playing"
function Playing({ q, onSkip, onAdvance, tutorialDone = { sleeve: false, loaded: false }, markTutorial = () => {} }) {
  const [stage, setStage] = useState("sleeve");      // sleeve | loaded | playing

  // First-time tutorial: mark each step done as the user completes it.
  // sleeve→loaded ⇒ they figured out how to drop the record.
  // loaded→playing ⇒ they figured out how to drop the needle.
  useEffect(() => {
    if (stage === "loaded" && !tutorialDone.sleeve) markTutorial("sleeve");
    if (stage === "playing" && !tutorialDone.loaded) markTutorial("loaded");
  }, [stage, tutorialDone.sleeve, tutorialDone.loaded, markTutorial]);

  const pulseInstruction =
    (stage === "sleeve" && !tutorialDone.sleeve) ||
    (stage === "loaded" && !tutorialDone.loaded);
  const [vinylPos, setVinylPos] = useState(null);    // {x,y} in stage coords (pointer position) while dragging
  const [armAngle, setArmAngle] = useState(-26);     // -26 = parked, -6 = playing
  const [armDragging, setArmDragging] = useState(false);
  const [progress, setProgress] = useState(0);       // 0..1, audio progress
  const [durationSec, setDurationSec] = useState(0); // seconds — populated once metadata is read
  const [coverOpen, setCoverOpen] = useState(false); // sleeve-art lightbox

  const stageAreaRef = useRef(null);
  const stageRef = useRef(null);
  const platterRef = useRef(null);
  const tonearmRef = useRef(null);
  const sleeveOnStageRef = useRef(null);
  const animRef = useRef(null);

  // Drag state via refs (avoids stale-closure issues on window listeners).
  // `source` records where the drag started so we can route the drop:
  //   "sleeve"  → vinyl is being put on the platter
  //   "platter" → vinyl is being removed back into the sleeve
  const dragRef = useRef({ active: false, source: null });
  const [dragSource, setDragSource] = useState(null);
  const armDragRef = useRef(false);

  // Audio progress is driven by the HTMLAudioElement timeupdate (see useEffect below).
  // Auto-advance is handled by the audio "ended" event — no separate RAF timer needed.

  const dragging = vinylPos !== null;

  // ── Vinyl drag — sleeve→platter and platter→sleeve ──
  const startVinylDrag = (e, source) => {
    e.preventDefault();
    e.stopPropagation();
    if (!stageAreaRef.current) return;
    const r = stageAreaRef.current.getBoundingClientRect();
    dragRef.current = { active: true, source };
    setDragSource(source);
    setVinylPos({ x: e.clientX - r.left, y: e.clientY - r.top });
  };
  // Pull the record OUT of the sleeve onto the platter.
  const onVinylPointerDown = (e) => {
    if (stage !== "sleeve") return;
    startVinylDrag(e, "sleeve");
  };
  // Pull the record OFF the platter back into the sleeve. Only allowed when
  // loaded and the tonearm is parked (stage !== "playing").
  const onDiscPointerDown = (e) => {
    if (stage !== "loaded") return;
    startVinylDrag(e, "platter");
  };

  // Window-level listeners while dragging vinyl
  useEffect(() => {
    if (!dragging) return;
    const onMove = (e) => {
      if (!dragRef.current.active || !stageAreaRef.current) return;
      const r = stageAreaRef.current.getBoundingClientRect();
      setVinylPos({ x: e.clientX - r.left, y: e.clientY - r.top });
    };
    const onUp = (e) => {
      if (!dragRef.current.active) return;
      const source = dragRef.current.source;
      dragRef.current = { active: false, source: null };
      const platter = platterRef.current?.getBoundingClientRect();
      const sleeveRect = sleeveOnStageRef.current?.getBoundingClientRect();
      setVinylPos(null);
      setDragSource(null);

      // Drop near platter → vinyl ends up on the platter (loaded).
      if (platter) {
        const cx = platter.left + platter.width / 2;
        const cy = platter.top + platter.height / 2;
        const dist = Math.hypot(e.clientX - cx, e.clientY - cy);
        if (dist < platter.width * 0.6) {
          setStage("loaded");
          return;
        }
      }
      // Drop over the sleeve → vinyl returns to the sleeve. Only meaningful
      // when the source was the platter — sleeve→sleeve is a no-op anyway.
      if (sleeveRect && source === "platter") {
        const sx = sleeveRect.left + sleeveRect.width / 2;
        const sy = sleeveRect.top + sleeveRect.height / 2;
        const inside =
          e.clientX >= sleeveRect.left && e.clientX <= sleeveRect.right &&
          e.clientY >= sleeveRect.top  && e.clientY <= sleeveRect.bottom;
        const near = Math.hypot(e.clientX - sx, e.clientY - sy) < sleeveRect.width * 0.9;
        if (inside || near) {
          setStage("sleeve");
          return;
        }
      }
      // Otherwise: no state change — the vinyl visually springs back to wherever it came from.
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    window.addEventListener("pointercancel", onUp);
    return () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
      window.removeEventListener("pointercancel", onUp);
    };
  }, [dragging]);

  // ── Tonearm drag (window-level) ──
  // We capture the offset between (current arm angle) and (pointer angle relative to pivot)
  // at pointerdown, so the head follows the pointer 1:1 instead of snapping to it.
  const armOffsetRef = useRef(0);
  const computePivot = () => {
    const p = platterRef.current?.getBoundingClientRect();
    if (!p) return null;
    const tonearmH = p.width * 0.46 / 0.7; // aspect-ratio: 0.7
    // Tonearm CSS: top: -4%; right: 4%; transform-origin: 100% 14%
    return {
      x: p.right - p.width * 0.04,
      y: p.top - p.height * 0.04 + tonearmH * 0.14,
    };
  };
  const onArmPointerDown = (e) => {
    if (stage === "sleeve") return;
    e.preventDefault();
    e.stopPropagation();
    const piv = computePivot();
    if (piv) {
      const pointerDeg = Math.atan2(e.clientY - piv.y, e.clientX - piv.x) * 180 / Math.PI - 90;
      armOffsetRef.current = armAngle - pointerDeg;
    } else {
      armOffsetRef.current = 0;
    }
    armDragRef.current = true;
    setArmDragging(true);
  };
  useEffect(() => {
    if (!armDragging) return;
    const onMove = (e) => {
      if (!armDragRef.current) return;
      e.preventDefault();
      const piv = computePivot();
      if (!piv) return;
      const dx = e.clientX - piv.x;
      const dy = e.clientY - piv.y;
      if (Math.abs(dx) < 2 && Math.abs(dy) < 2) return;
      let deg = Math.atan2(dy, dx) * 180 / Math.PI - 90 + armOffsetRef.current;
      if (!Number.isFinite(deg)) return;
      deg = Math.max(-30, Math.min(4, deg));
      setArmAngle(deg);
    };
    const onUp = () => {
      armDragRef.current = false;
      setArmDragging(false);
      // Snap based on final angle
      setArmAngle(prev => {
        if (prev > -16) {
          // dropped on the record
          setStage(s => {
            if (s === "sleeve") return s;
            if (s !== "playing") playSFX("needle-down");
            return "playing";
          });
          return -6;
        } else {
          setStage(s => {
            if (s === "playing") { playSFX("needle-up"); return "loaded"; }
            return s;
          });
          return -26;
        }
      });
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    window.addEventListener("pointercancel", onUp);
    return () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
      window.removeEventListener("pointercancel", onUp);
    };
  }, [armDragging]);

  const showSleeve  = stage === "sleeve";
  const showLoaded  = stage !== "sleeve";
  const isPlaying   = stage === "playing";

  // Synthesized warm vinyl tone via Web Audio — plays softly while needle is on the record.
  // Real audio playback via HTMLAudioElement using assets/audio/track-{id}.mp3.
  // The audio element lives for as long as the track does — pulling the
  // tonearm off pauses rather than tearing it down, so position is preserved.
  const audioRef = useRef(null);
  const realDurationRef = useRef(0);

  // Create / dispose audio when the track changes
  useEffect(() => {
    if (!q) return;
    const audioSrc = `assets/audio/track-${q.id}.mp3`;
    const a = new Audio(audioSrc);
    a.volume = 0;
    audioRef.current = a;
    realDurationRef.current = 0;

    const updateProgress = () => {
      const d = realDurationRef.current || (isFinite(a.duration) && a.duration > 0 ? a.duration : 0);
      if (d > 0) {
        setProgress(Math.min(1, a.currentTime / d));
        setDurationSec(prev => (prev !== d ? d : prev));
      }
    };

    // VBR scan on a side channel — doesn't interfere with playback element
    const scan = new Audio();
    scan.preload = "metadata";
    const tryReadScan = () => {
      const d = scan.duration;
      if (isFinite(d) && d > 0) {
        realDurationRef.current = d;
        setDurationSec(prev => (prev !== d ? d : prev));
        return true;
      }
      return false;
    };
    scan.addEventListener("loadedmetadata", () => {
      if (!tryReadScan()) {
        try {
          scan.currentTime = 1e101;
          scan.addEventListener("timeupdate", function once() {
            scan.removeEventListener("timeupdate", once);
            tryReadScan();
          });
        } catch (e) {}
      }
    });
    scan.addEventListener("durationchange", tryReadScan);
    scan.src = audioSrc;

    const onEnded = () => { onAdvance && onAdvance(); };
    a.addEventListener("ended", onEnded);
    a.addEventListener("timeupdate", updateProgress);
    a.addEventListener("durationchange", updateProgress);

    return () => {
      a.removeEventListener("ended", onEnded);
      a.removeEventListener("timeupdate", updateProgress);
      a.removeEventListener("durationchange", updateProgress);
      try { a.pause(); } catch (e) {}
      try { a.src = ""; } catch (e) {}
      try { scan.src = ""; } catch (e) {}
      audioRef.current = null;
    };
  }, [q && q.id]);

  // Play / pause based on isPlaying — preserves currentTime so lifting the arm pauses.
  useEffect(() => {
    const a = audioRef.current;
    if (!a) return;
    let cancelled = false;
    let fadeTimer;
    const fade = (toVol, dur = 250) => {
      if (fadeTimer) clearInterval(fadeTimer);
      const from = a.volume;
      const steps = 10;
      const dv = (toVol - from) / steps;
      let i = 0;
      fadeTimer = setInterval(() => {
        i++;
        a.volume = Math.max(0, Math.min(1, from + dv * i));
        if (i >= steps) { clearInterval(fadeTimer); fadeTimer = null; }
      }, dur / steps);
    };
    if (isPlaying) {
      a.play().then(() => { if (!cancelled) fade(1.0); }).catch((e) => {
        console.warn("Audio play blocked or failed:", e);
      });
    } else {
      fade(0, 200);
      // pause after fade completes
      const t = setTimeout(() => { try { a.pause(); } catch (e) {} }, 220);
      return () => { cancelled = true; clearTimeout(t); if (fadeTimer) clearInterval(fadeTimer); };
    }
    return () => { cancelled = true; if (fadeTimer) clearInterval(fadeTimer); };
  }, [isPlaying]);

  // Reset progress whenever the track changes
  useEffect(() => { setProgress(0); setDurationSec(0); }, [q && q.id]);

  // Close the cover lightbox on Escape
  useEffect(() => {
    if (!coverOpen) return;
    const onKey = (e) => { if (e.key === "Escape") setCoverOpen(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [coverOpen]);

  // Progress is driven by the real audio's timeupdate event (see audio useEffect above).
  // No synthetic timer needed.

  return (
    <div
      className="screen turntable-screen tactile-stage"
      ref={stageRef}
    >
      <div className="turntable-wrap">
        <div className="track-meta" style={{ marginBottom: 4 }}>
          <div className="num">Track {q.n} · Side {q.side}</div>
        </div>

        <div className={`caption tt-status ${pulseInstruction ? "pulse" : ""}`}>
          {stage === "sleeve"   && <>Slide the record out and onto the turntable.</>}
          {stage === "loaded"   && <>Now drop the needle to start playing.</>}
          {stage === "playing"  && <>Playing {q.title}</>}
        </div>

        {/* Stage area — sleeve on the left, turntable on the right */}
        <div className="stage-area" ref={stageAreaRef}>
          {/* Sleeve */}
          <div className="sleeve-on-stage" ref={sleeveOnStageRef}>
            <div className="sleeve-frame">
              {/* Vinyl peeks BEHIND the sleeve-art */}
              {showSleeve && !dragging && (
                <div
                  className="vinyl-grab in-sleeve ready"
                  onPointerDown={onVinylPointerDown}
                >
                  <div className="mini-vinyl">
                    <div className="mv-label" />
                  </div>
                </div>
              )}
              <div
                className="sleeve-art clickable"
                style={{ background: q.art.bg, color: q.art.fg }}
                onClick={() => setCoverOpen(true)}
                role="button"
                tabIndex={0}
                aria-label={`Enlarge cover for ${q.title}`}
                onKeyDown={(e) => {
                  if (e.key === "Enter" || e.key === " ") {
                    e.preventDefault();
                    setCoverOpen(true);
                  }
                }}
              >
                <img
                  className="sleeve-art-img"
                  src={`assets/covers/track-${q.id}.jpg`}
                  alt=""
                  onError={(e) => { e.currentTarget.style.display = 'none'; }}
                />
              </div>
            </div>
            <div className="sleeve-spine">
              <div className="spine-side">SIDE {q.side}</div>
              <div className="spine-title">{q.title}</div>
            </div>
          </div>

          {/* Turntable */}
          <div className="turntable" ref={platterRef}>
            <div className="platter">
              <div className="platter-empty">
                <div className="spindle" />
                {!showLoaded && <div className="drop-hint">Drop the<br/><em>record</em></div>}
              </div>
              {showLoaded && !(dragging && dragSource === "platter") && (
                <div
                  className={`disc ${isPlaying ? "" : "paused"} ${stage === "loaded" ? "removable" : ""}`}
                  onPointerDown={stage === "loaded" ? onDiscPointerDown : undefined}
                >
                  <div className="disc-label">
                    <img
                      className="disc-label-img"
                      src={`assets/covers/track-${q.id}.jpg`}
                      alt=""
                      onError={(e) => { e.currentTarget.style.display = 'none'; }}
                    />
                    <div className="disc-label-shade" />
                    <div className="disc-label-text">
                      <div className="dl-track">Track {q.n} · Side {q.side}</div>
                      <div className="dl-title">{q.title}</div>
                    </div>
                    <div className="disc-label-hole" />
                  </div>
                </div>
              )}
            </div>
            {/* Tonearm — draggable */}
            <div
              className={`tonearm ${isPlaying ? "playing" : ""} ${armDragging ? "dragging" : ""} ${stage === "loaded" && !armDragging ? "ready" : ""}`}
              ref={tonearmRef}
              style={{
                transform: `rotate(${armAngle}deg)`,
                transition: armDragging ? "none" : "transform 600ms var(--ease-arm)",
              }}
            >
              <div className="pivot" />
              <div className="arm"><div className="head" onPointerDown={onArmPointerDown} /></div>
            </div>
          </div>

          {/* Floating vinyl while dragging — absolutely positioned in stage */}
          {dragging && vinylPos && (
            <div
              className="vinyl-grab floating"
              style={{
                left: vinylPos.x,
                top: vinylPos.y,
              }}
              onPointerDown={onVinylPointerDown}
            >
              <div className={`mini-vinyl ${q.side === "B" ? "sage" : ""}`}>
                <div className="mv-label" />
              </div>
            </div>
          )}
        </div>

        {/* Track meta + status */}
        <div className="track-meta">
          <div className="ttl">{q.title}</div>
          {q.voiced && (
            <div className="voiced">{q.bonus ? q.voiced : `Voiced by ${q.voiced}`}</div>
          )}
        </div>

        <Waveform active={isPlaying} />

        {/* Progress bar — always visible; thumb + live times appear when loaded */}
        {(() => {
          const fmt = (sec) => {
            if (!isFinite(sec) || sec < 0) return "—:—";
            const m = Math.floor(sec / 60);
            const s = Math.floor(sec % 60).toString().padStart(2, "0");
            return `${m}:${s}`;
          };
          const elapsedSec = durationSec > 0 ? progress * durationSec : 0;
          const remainingSec = durationSec > 0 ? Math.max(0, durationSec - elapsedSec) : 0;
          const showTimes = showLoaded && durationSec > 0;
          return (
            <div className={`audio-progress-wrap ${showLoaded ? "is-active" : "is-empty"}`}>
              <span className="ap-time ap-elapsed">
                {showTimes ? fmt(elapsedSec) : "—:—"}
              </span>
              <div
                className="audio-progress"
                onPointerDown={!showLoaded ? undefined : (e) => {
                  const a = audioRef.current;
                  const d = realDurationRef.current || (a && isFinite(a.duration) && a.duration > 0 ? a.duration : 0);
                  if (!a || d <= 0) return;
                  const bar = e.currentTarget;
                  bar.setPointerCapture(e.pointerId);
                  const seek = (clientX) => {
                    const r = bar.getBoundingClientRect();
                    const ratio = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
                    const prev = a.currentTime;
                    const next = ratio * d;
                    try { a.currentTime = next; } catch (err) {}
                    setProgress(ratio);
                    if (Math.abs(next - prev) > 0.4) {
                      playSFX("scrub", next > prev ? 1 : -1);
                    }
                  };
                  seek(e.clientX);
                  const onMove = (ev) => seek(ev.clientX);
                  const onUp = (ev) => {
                    bar.removeEventListener("pointermove", onMove);
                    bar.removeEventListener("pointerup", onUp);
                    bar.removeEventListener("pointercancel", onUp);
                    try { bar.releasePointerCapture(ev.pointerId); } catch (err) {}
                  };
                  bar.addEventListener("pointermove", onMove);
                  bar.addEventListener("pointerup", onUp);
                  bar.addEventListener("pointercancel", onUp);
                }}
              >
                <div className="ap-fill" style={{ width: `${progress * 100}%` }} />
                {showLoaded && (
                  <div className="ap-thumb" style={{ left: `${progress * 100}%` }} />
                )}
              </div>
              <span className="ap-time ap-remaining">
                {showTimes ? `-${fmt(remainingSec)}` : "—:—"}
              </span>
            </div>
          );
        })()}

        <div className="tt-cta">
          {!q.bonus && <button className="btn btn-ghost" onClick={onSkip}>Skip to question</button>}
        </div>
      </div>

      {/* Cover-art lightbox */}
      {coverOpen && (
        <div className="confirm cover-lightbox-backdrop" onClick={() => setCoverOpen(false)}>
          <div
            className="cover-lightbox"
            style={{ background: q.art.bg, color: q.art.fg }}
            onClick={(e) => e.stopPropagation()}
            role="dialog"
            aria-label={`Cover for ${q.title}`}
          >
            <img
              className="cover-lightbox-img"
              src={`assets/covers/track-${q.id}.jpg`}
              alt={q.title}
              onError={(e) => { e.currentTarget.style.display = 'none'; }}
            />
            <div className="cover-lightbox-caption">
              <div className="eyebrow">Track {q.n} · Side {q.side}</div>
              <div className="ttl">{q.title}</div>
            </div>
            <button
              type="button"
              className="cover-lightbox-close"
              onClick={() => setCoverOpen(false)}
              aria-label="Close cover"
            >
              ✕
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────── Question screen ─────────────────────────────
function QuestionScreen({ q, picked, onAnswer, onContinue, isLast }) {
  const correct = picked === q.correct;
  return (
    <div className="screen">
      <div className="qscreen-head">
        <div className="eyebrow">Your answer</div>
        <h2 className="h-display">What do you remember?</h2>
      </div>
      <QuestionCard q={q} picked={picked} onAnswer={onAnswer} />

      {picked != null && (
        <div className="reveal">
          <span className={`tag ${correct ? "right" : "wrong"}`}>
            {correct ? "Of course you knew." : "Close. Not quite."}
          </span>
          <div className="note">{correct ? q.rightNote : q.wrongNote}</div>
          {correct && (
            <div className="award">
              <span className="glyph">♪</span>
              <span>+5 music notes added to your stash.</span>
            </div>
          )}
        </div>
      )}

      {picked != null && (
        <div className="qscreen-cta">
          <button className="btn btn-primary" onClick={onContinue}>
            {isLast ? "Open the shop" : "Back to the crate"}
          </button>
        </div>
      )}
      <div className="spacer-4" />
    </div>
  );
}

// ───────────────────────────── Shop ─────────────────────────────
function Shop({ notes, purchased, redeemed = {}, allPlayed, bonusUnlocked, onBuy, onBack, onPlayAgain, onRedeem, onBonus, momName }) {
  const ownedCount = Object.keys(purchased).length;
  return (
    <div className="screen">
      <div className="shop-head">
        <div className="eyebrow">Side B · The Shop</div>
        <h2 className="h-display">Cash in, <em>{momName}</em></h2>
        <p className="body-lead" style={{ marginTop: 8, fontSize: 15 }}>
          Each certificate is a real, redeemable promise from me.
        </p>
      </div>

      <div className="shop-balance">
        <div>
          <div className="bal-num">♪ {notes}</div>
          <div className="bal-lbl">Notes in the bank</div>
        </div>
        <div className="right">
          {allPlayed
            ? <>You played all 5 records.</>
            : <>Earn 5 more by playing the next track or play again to try again.</>}
        </div>
      </div>

      <div className="shop-list">
        {SHOP_ITEMS.map(item => (
          <Certificate
            key={item.id}
            item={item}
            owned={!!purchased[item.id]}
            affordable={notes >= item.cost}
            onBuy={() => onBuy(item)}
          />
        ))}
      </div>

      <div className="endside-card">
        <div className="ribbon">{bonusUnlocked ? "Bonus tracks · unlocked" : "Bonus tracks · locked"}</div>
        <h3>{bonusUnlocked ? "There are some hidden tracks on Side B." : "Get every question right to unlock the bonus tracks."}</h3>
        <p>{bonusUnlocked ? "Pulled from the cutting-room floor just for you." : "Replay to try again. The bonus side opens when the records are perfect."}</p>
        <div className="actions">
          {bonusUnlocked
            ? <button className="btn btn-primary" onClick={onBonus}>Play the bonus tracks</button>
            : <button className="btn btn-ghost" onClick={onBack}>Back to the crate</button>}
        </div>
      </div>

      <div className="endside-card">
        <div className="ribbon">Mom Certs:</div>
        <h3>"For the greatest mom to walk planet Earth."</h3>
        <p>You've pressed {ownedCount} certificate{ownedCount === 1 ? "" : "s"}. I will honor any certificate presented to me.</p>
        <div className="actions">
          {ownedCount > 0 && (
            <button className="btn btn-primary" onClick={onRedeem}>Redeem certs</button>
          )}
          {!allPlayed && <button className="btn btn-ghost" onClick={onBack}>Back to the crate</button>}
          {allPlayed && <button className="btn btn-ghost" onClick={onPlayAgain}>Play again</button>}
        </div>
      </div>

      <div className="shop-foot">
        <div className="lbl">Pressed with love · May 2026</div>
        <div>Elijah · Lorelei · Jackson · Brandon</div>
      </div>
    </div>
  );
}

// ───────────────────────────── First-visit greeting popup ─────────────────────────────
// Gates the welcome screen on the very first visit. Dismissing it auto-plays
// assets/audio/greeting.mp3. Backdrop clicks are intentionally ignored — the
// user has to acknowledge with the "I'm Ready!" button.
function GreetingPopup({ onReady }) {
  return (
    <div className="confirm">
      <div className="confirm-card greeting-card" onClick={(e) => e.stopPropagation()}>
        <div className="eyebrow">Before we start</div>
        <h3 className="h-display" style={{ fontSize: 26, marginTop: 10 }}>
          Turn <em>up</em> your volume.
        </h3>
        <p className="body-lead" style={{ marginTop: 12, fontSize: 15 }}>
          For the best experience, grab some headphones.
        </p>
        <p className="body-lead serif-accent" style={{ marginTop: 14, fontSize: 18 }}>
          Are you ready?
        </p>
        <div style={{ marginTop: 20 }}>
          <button className="btn btn-primary" onClick={onReady}>I'm Ready!</button>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────── Buy Confirmation Modal ─────────────────────────────
// Shown BEFORE a purchase is finalized — gives the user a chance to back out
// before notes are deducted and the kerching SFX fires.
function BuyConfirm({ item, notes, onConfirm, onCancel, momName }) {
  const remaining = Math.max(0, notes - item.cost);
  return (
    <div className="confirm" onClick={onCancel}>
      <div className="confirm-card" onClick={e => e.stopPropagation()}>
        <div className="eyebrow">Press this certificate?</div>
        <div className={`stamp-big`} style={{ marginTop: 14 }}>{item.glyph}</div>
        <h3 className="h-display" style={{ fontSize: 28, marginTop: 10 }}>
          <em>{item.title}</em>
        </h3>
        <div className="caption" style={{ marginTop: 4, fontStyle: "italic", fontFamily: "var(--font-display)", fontSize: 16 }}>
          {item.serif}
        </div>
        <p className="body-lead" style={{ marginTop: 14, fontSize: 15 }}>
          {item.blurb}
        </p>
        <div className="buy-confirm-cost">
          <div className="bcc-row">
            <span className="bcc-lbl">Cost</span>
            <span className="bcc-val">♪ {item.cost} note{item.cost === 1 ? "" : "s"}</span>
          </div>
          <div className="bcc-row">
            <span className="bcc-lbl">After</span>
            <span className="bcc-val">♪ {remaining} note{remaining === 1 ? "" : "s"}</span>
          </div>
        </div>
        <div style={{ marginTop: 18, display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" }}>
          <button className="btn btn-ghost" onClick={onCancel}>Not yet</button>
          <button className="btn btn-primary" onClick={onConfirm}>Press it</button>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────── Confirm Purchase Modal ─────────────────────────────
function ConfirmPurchase({ item, onClose, momName }) {
  return (
    <div className="confirm" onClick={onClose}>
      <div className="confirm-card" onClick={e => e.stopPropagation()}>
        <div className="eyebrow">Certificate pressed</div>
        <div className={`stamp-big`} style={{ marginTop: 14 }}>{item.glyph}</div>
        <h3 className="h-display" style={{ fontSize: 28, marginTop: 10 }}>
          <em>{item.title}</em>
        </h3>
        <div className="caption" style={{ marginTop: 4, fontStyle: "italic", fontFamily: "var(--font-display)", fontSize: 16 }}>
          {item.serif}
        </div>
        <p className="body-lead" style={{ marginTop: 14, fontSize: 15 }}>
          Redeemable by {momName}, anytime. Signed: Brandon
        </p>
        <div style={{ marginTop: 18 }}>
          <button className="btn btn-primary" onClick={onClose}>Keep shopping</button>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────── Redeem List Screen ─────────────────────────────
function RedeemList({ purchased, redeemed, onPick, onBack }) {
  const owned = SHOP_ITEMS.filter(item => purchased[item.id]);
  const readyCount = owned.filter(item => !redeemed[item.id]).length;
  return (
    <div className="screen">
      <div className="shop-head">
        <div className="eyebrow">Cash in · Redeem</div>
        <h2 className="h-display">Your <em>certificates.</em></h2>
        <p className="body-lead" style={{ marginTop: 8, fontSize: 15 }}>
          {owned.length === 0
            ? "Nothing pressed yet. Earn notes and buy a certificate first."
            : readyCount > 0
              ? `${readyCount} ready to redeem. Tap one to call it in.`
              : "Every certificate has been redeemed. Thanks, Mom."}
        </p>
      </div>

      {owned.length === 0 ? (
        <div className="endside-card">
          <h3>The shelf is empty.</h3>
          <p>Head back to the shop, spend some notes, then come back here.</p>
          <div className="actions">
            <button className="btn btn-primary" onClick={onBack}>Back to the shop</button>
          </div>
        </div>
      ) : (
        <div className="shop-list">
          {owned.map(item => {
            const isRedeemed = !!redeemed[item.id];
            return (
              <button
                key={item.id}
                type="button"
                className={`cert ${isRedeemed ? "redeemed" : ""}`}
                onClick={() => onPick(item)}
              >
                <div className={`stamp ${item.accent}`}>{item.glyph}</div>
                <div className="body">
                  <div className="sm-eyebrow">{isRedeemed ? "Redeemed" : "Ready to redeem"}</div>
                  <div className="ttl">{item.title}</div>
                  <div className="blurb">{item.blurb}</div>
                </div>
                <div className="price">
                  {isRedeemed
                    ? <span>✓ Done</span>
                    : <span>Tap →</span>}
                </div>
              </button>
            );
          })}
        </div>
      )}

      {owned.length > 0 && (
        <div className="shop-foot">
          <div className="lbl">Tap a card · I'll handle the rest</div>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────── Redeem Confirm Modal ─────────────────────────────
function RedeemConfirm({ item, redeemed, onRedeem, onClose, momName }) {
  return (
    <div className="confirm" onClick={onClose}>
      <div className="confirm-card" onClick={e => e.stopPropagation()}>
        <div className="eyebrow">{redeemed ? "Already redeemed" : "Ready to redeem?"}</div>
        <div className={`stamp-big`} style={{ marginTop: 14 }}>{item.glyph}</div>
        <h3 className="h-display" style={{ fontSize: 28, marginTop: 10 }}>
          <em>{item.title}</em>
        </h3>
        <div className="caption" style={{ marginTop: 4, fontStyle: "italic", fontFamily: "var(--font-display)", fontSize: 16 }}>
          {item.serif}
        </div>
        <p className="body-lead" style={{ marginTop: 14, fontSize: 15 }}>
          {item.blurb}
        </p>
        <p className="caption" style={{ marginTop: 10 }}>
          Redeemable by {momName}. Signed: Brandon
        </p>
        <div style={{ marginTop: 18, display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" }}>
          {redeemed ? (
            <button className="btn btn-primary" onClick={onClose}>Close</button>
          ) : (
            <>
              <button className="btn btn-ghost" onClick={onClose}>Not yet</button>
              <button className="btn btn-primary" onClick={onRedeem}>Redeem now</button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

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