/* App.jsx — Nonogram Lab orchestrator (v2: sound, boot, daily, colored, share). */
const { useState: useS, useEffect: useE, useMemo: useM, useRef: useR, useCallback: useC } = React;

const ACCENTS = { Lime: "#bef264", Cyan: "#56d6e2", Magenta: "#ff5fb0", Coral: "#ff7a59" };
const MAKE_PALETTE = ["#e0402e", "#f2b807", "#5bbf54", "#2d8fe0", "#efecf7", "#23201c"];

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "Lime",
  "theme": "Dusk",
  "zen": false,
  "guide": true,
  "scan": true,
  "hardcore": false
}/*EDITMODE-END*/;

const HINTS_PER = 3;
const LS_SOLVED = "nl.solved.v1";
const LS_CURRENT = "nl.current.v1";
const LS_STATS = "nl.stats.v1";

function loadJSON(k, fb) { try { return JSON.parse(localStorage.getItem(k)) || fb; } catch (e) { return fb; } }
function saveJSON(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }

function Confetti({ palette }) {
  const bits = useM(() => {
    const cols = palette && palette.length ? palette.concat(["var(--accent)"]) : ["var(--accent)"];
    return Array.from({ length: 64 }, (_, i) => ({
      left: Math.random() * 100, delay: Math.random() * 0.5, dur: 1.6 + Math.random() * 1.6,
      size: 7 + Math.random() * 9, rot: Math.random() * 360, color: cols[i % cols.length], drift: (Math.random() - 0.5) * 130,
    }));
  }, [palette]);
  return (
    <div className="confetti">
      {bits.map((b, i) => (
        <span key={i} style={{ left: b.left + "%", width: b.size, height: b.size, background: b.color, animationDelay: b.delay + "s", animationDuration: b.dur + "s", ["--rot"]: b.rot + "deg", ["--drift"]: b.drift + "px" }} />
      ))}
    </div>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const zen = !!t.zen;
  const hardcore = !!t.hardcore;

  const [booted, setBooted] = useS(false);
  const [muted, setMutedState] = useS(() => (window.Sfx ? Sfx.isMuted() : false));
  const setMute = useC((m) => { if (window.Sfx) Sfx.setMuted(m); setMutedState(m); }, []);

  const [mode, setMode] = useS("Play");
  const [drawerOpen, setDrawerOpen] = useS(false);
  const [statsOpen, setStatsOpen] = useS(false);
  const [libOpen, setLibOpen] = useS(false);

  // ---------- PLAY ----------
  const startId = (() => { const s = localStorage.getItem(LS_CURRENT); return (s && PUZZLES.some((p) => p.id === s)) ? s : PUZZLES[0].id; })();
  const [currentId, setCurrentId] = useS(startId);
  const [active, setActive] = useS(() => PUZZLES.find((p) => p.id === startId) || PUZZLES[0]);
  const [board, setBoard] = useS(() => Engine.boardWithZeroClueMarks(active.rows, active.cols, active.rowClues, active.colClues));
  const boardRef = useR(board);
  useE(() => { boardRef.current = board; }, [board]);

  const [lives, setLives] = useS(3);
  const [hintsLeft, setHintsLeft] = useS(HINTS_PER);
  const [won, setWon] = useS(false);
  const [dead, setDead] = useS(false);
  const [reveal, setReveal] = useS(null);
  const [wrongKey, setWrongKey] = useS(null);
  const [animMarks, setAnimMarks] = useS({});
  const [solvedMap, setSolvedMap] = useS(() => loadJSON(LS_SOLVED, {}));
  const [stats, setStats] = useS(() => loadJSON(LS_STATS, { plays: 0, totalMs: 0, fastestMs: null, streak: 0, bestStreak: 0, lastDailyKey: null }));
  const [showWin, setShowWin] = useS(false);
  const [shake, setShake] = useS(false);
  const [playColor, setPlayColor] = useS(1);
  const [tapMode, setTapMode] = useS("fill");
  const mistakesRef = useR(0);
  const hintsUsedRef = useR(0);
  const [dailyActive, setDailyActive] = useS(false);

  const [timeMs, setTimeMs] = useS(0);
  const startRef = useR(Date.now());
  const playing = booted && mode === "Play" && !won && !dead;
  useE(() => {
    if (!playing) return;
    const id = setInterval(() => setTimeMs(Date.now() - startRef.current), 250);
    return () => clearInterval(id);
  }, [playing, currentId]);

  // daily
  const todayK = Engine.todayKey();
  const dailyPuz = PUZZLES[Engine.hashStr(todayK) % PUZZLES.length];
  const dailyDone = stats.lastDailyKey === todayK;

  // ---------- MAKE ----------
  const [mrows, setMrows] = useS(10);
  const [mcols, setMcols] = useS(10);
  const [solution, setSolution] = useS(() => Engine.emptyGrid(10, 10, 0));
  const [makerColor, setMakerColor] = useS(null);
  const [showColorPreview, setShowColorPreview] = useS(false);
  const [importStatus, setImportStatus] = useS(null);
  const [importSize, setImportSize] = useS(20);
  const [threshold, setThreshold] = useS(128);
  const [invert, setInvert] = useS(false);
  const [makeColored, setMakeColored] = useS(false);
  const [makeColor, setMakeColor] = useS(1);
  const makerClues = useM(() => (
    makeColored ? Engine.computeColoredClues(solution)
      : Engine.computeCluesFromSolution(solution.map((r) => r.map((v) => (v > 0 ? 1 : 0))))
  ), [solution, makeColored]);
  const makerFilled = useM(() => solution.reduce((a, r) => a + r.reduce((x, v) => x + (v > 0 ? 1 : 0), 0), 0), [solution]);

  // ---------- load ----------
  const loadActive = useC((p, idForLs, daily) => {
    setActive(p);
    setBoard(Engine.boardWithZeroClueMarks(p.rows, p.cols, p.rowClues, p.colClues));
    setLives(3); setHintsLeft(HINTS_PER); setWon(false); setDead(false);
    setReveal(null); setWrongKey(null); setAnimMarks({}); setShowWin(false);
    setPlayColor(1); mistakesRef.current = 0; hintsUsedRef.current = 0;
    setDailyActive(!!daily);
    setTimeMs(0); startRef.current = Date.now();
    if (idForLs) { try { localStorage.setItem(LS_CURRENT, idForLs); } catch (e) {} }
  }, []);

  const selectBuiltin = useC((id, daily) => {
    const p = PUZZLES.find((x) => x.id === id);
    if (!p) return;
    if (window.Sfx) Sfx.ui();
    setCurrentId(id); setMode("Play"); setDrawerOpen(false);
    loadActive(p, id, daily);
  }, [loadActive]);

  const playDaily = useC(() => { selectBuiltin(dailyPuz.id, true); }, [dailyPuz, selectBuiltin]);

  const resetBoard = useC(() => {
    if (window.Sfx) Sfx.ui();
    setBoard(Engine.boardWithZeroClueMarks(active.rows, active.cols, active.rowClues, active.colClues));
    setLives(3); setHintsLeft(HINTS_PER); setWon(false); setDead(false);
    setReveal(null); setAnimMarks({}); setWrongKey(null); setShowWin(false);
    setPlayColor(1); mistakesRef.current = 0; hintsUsedRef.current = 0;
    setTimeMs(0); startRef.current = Date.now();
  }, [active]);

  // ---------- win ----------
  const winNow = useC((grid) => {
    const final = Date.now() - startRef.current;
    const perfect = mistakesRef.current === 0;
    const noHints = hintsUsedRef.current === 0;
    setWon(true); setTimeMs(final);
    setReveal(active.colorSolution || Engine.gridToColorGrid(grid, active.palette || ["var(--accent)"]));
    setShake(true); setTimeout(() => setShake(false), 420);
    if (window.Sfx) { Sfx.win(perfect); for (let i = 0; i < 8; i++) setTimeout(() => Sfx.sweep(i), 120 + i * 70); }
    setTimeout(() => setShowWin(true), 780);

    // collection unlock + best time
    if (active.id && active.id !== "custom") {
      setSolvedMap((m) => {
        const prev = m[active.id];
        if (prev == null || (!zen && final < prev)) { const nm = Object.assign({}, m, { [active.id]: final }); saveJSON(LS_SOLVED, nm); return nm; }
        return m;
      });
    }
    // stats
    setStats((s) => {
      const ns = Object.assign({}, s);
      ns.plays = (s.plays || 0) + 1;
      ns.totalMs = (s.totalMs || 0) + final;
      ns.fastestMs = s.fastestMs == null ? final : Math.min(s.fastestMs, final);
      if (dailyActive && s.lastDailyKey !== todayK) {
        const gap = s.lastDailyKey ? Engine.daysBetween(s.lastDailyKey, todayK) : null;
        ns.streak = gap === 1 ? (s.streak || 0) + 1 : 1;
        ns.bestStreak = Math.max(s.bestStreak || 0, ns.streak);
        ns.lastDailyKey = todayK;
      }
      saveJSON(LS_STATS, ns);
      return ns;
    });
  }, [active, zen, dailyActive, todayK]);

  // ---------- play move ----------
  const playMove = useC((r, c, v) => {
    if (won || dead) return;
    const prev = boardRef.current;
    if (prev[r][c] === v) return;
    const art = active.art;
    // Lock cells that are already set correctly: a right answer is permanent.
    // Any attempt to change/remove it is a silent no-op — never costs a life.
    if (!hardcore) {
      const cur = prev[r][c];
      const locked = active.colored
        ? (cur > 0 ? cur === art[r][c] : cur === -1 && !(art[r][c] > 0))
        : (cur === 1 ? art[r][c] > 0 : cur === -1 && !(art[r][c] > 0));
      if (locked) return;
    }
    let correct = true;
    if (!hardcore) {
      if (active.colored) correct = (v === 0) || (v === -1 ? art[r][c] === 0 : art[r][c] === v);
      else correct = (v === 1 && art[r][c] > 0) || (v === -1 && !(art[r][c] > 0)) || v === 0;
    }
    if (!correct) {
      const key = r + "-" + c;
      mistakesRef.current++;
      if (window.Sfx) Sfx.error();
      setWrongKey(key);
      setTimeout(() => setWrongKey((k) => (k === key ? null : k)), 360);
      if (!zen) setLives((L) => { const n = Math.max(0, L - 1); if (n === 0) { setDead(true); setTimeMs(Date.now() - startRef.current); } return n; });
      return;
    }
    const next = prev.map((row) => row.slice());
    next[r][c] = v;
    if (window.Sfx) { if (v > 0) Sfx.fill(); else if (v === -1) Sfx.mark(); else Sfx.erase(); }

    // auto-✕ completed lines
    const anim = {}; let idx = 0;
    const rowSat = active.colored ? Engine.coloredLineSatisfied(next[r], active.rowClues[r]) : Engine.lineSatisfied(next[r], active.rowClues[r]);
    if (rowSat) for (let cc = 0; cc < next[0].length; cc++) if (next[r][cc] === 0) { next[r][cc] = -1; anim[r + "-" + cc] = idx++; }
    const colLine = next.map((row) => row[c]);
    const colSat = active.colored ? Engine.coloredLineSatisfied(colLine, active.colClues[c]) : Engine.lineSatisfied(colLine, active.colClues[c]);
    if (colSat) for (let rr = 0; rr < next.length; rr++) if (next[rr][c] === 0) { next[rr][c] = -1; anim[rr + "-" + c] = idx++; }

    boardRef.current = next;
    setBoard(next);
    if (idx > 0) { if (window.Sfx) Sfx.line(); setAnimMarks(anim); setTimeout(() => setAnimMarks((m) => (m === anim ? {} : m)), Math.min(900, idx * 45 + 250)); }

    const solved = active.colored ? Engine.checkSolvedColored(next, active.art) : Engine.checkSolved(next, active);
    if (solved) winNow(next);
  }, [won, dead, active, zen, hardcore, winNow]);

  const useHint = useC(() => {
    if (won || dead || hintsLeft <= 0) return;
    const art = active.art, b = boardRef.current;
    for (let r = 0; r < art.length; r++) for (let c = 0; c < art[0].length; c++) {
      const want = art[r][c] > 0 ? (active.colored ? art[r][c] : 1) : 0;
      if (want > 0 && b[r][c] !== want) { hintsUsedRef.current++; setHintsLeft((h) => h - 1); if (window.Sfx) Sfx.hint(); playMove(r, c, want); return; }
    }
  }, [won, dead, hintsLeft, active, playMove]);

  // ---------- make ----------
  const makeMove = useC((r, c, v) => {
    if (window.Sfx) { if (v > 0) Sfx.fill(); else Sfx.erase(); }
    setSolution((prev) => { const n = prev.map((x) => x.slice()); n[r][c] = v === -1 ? 0 : v; return n; });
  }, []);
  const setRowsSafe = useC((v) => { setMrows(v); setSolution((g) => { const out = Engine.emptyGrid(v, mcols, 0); for (let r = 0; r < Math.min(v, g.length); r++) for (let c = 0; c < Math.min(mcols, g[0].length); c++) out[r][c] = g[r][c]; return out; }); setMakerColor(null); }, [mcols]);
  const setColsSafe = useC((v) => { setMcols(v); setSolution((g) => { const out = Engine.emptyGrid(mrows, v, 0); for (let r = 0; r < Math.min(mrows, g.length); r++) for (let c = 0; c < Math.min(v, g[0].length); c++) out[r][c] = g[r][c]; return out; }); setMakerColor(null); }, [mrows]);

  const importImage = useC((file) => {
    setMakeColored(false);
    setImportStatus("Reading…");
    Engine.extractBothGrids(file, undefined, undefined, { mode: "luma", threshold, invert, maxCells: importSize, aspect: "square" })
      .then(({ grid, colorGrid }) => { setMrows(grid.length); setMcols(grid[0].length); setSolution(grid); setMakerColor(colorGrid); setImportStatus("Traced " + grid.length + "×" + grid[0].length + " — color kept"); })
      .catch((e) => setImportStatus("Failed: " + e.message));
  }, [threshold, invert, importSize]);

  const loadInPlayer = useC(() => {
    if (makerFilled === 0) { setImportStatus("Ink at least one cell first."); return; }
    let p;
    if (makeColored) {
      const cc = Engine.computeColoredClues(solution);
      const colorSolution = solution.map((row) => row.map((v) => (v > 0 ? MAKE_PALETTE[v - 1] : "transparent")));
      p = { id: "custom", name: "Your Puzzle", tag: "Your drawing", level: "Custom", cat: "Color", colored: true, rows: mrows, cols: mcols, palette: MAKE_PALETTE, art: solution, solution: solution.map((r) => r.map((v) => (v > 0 ? 1 : 0))), colorSolution, rowClues: cc.rowClues, colClues: cc.colClues };
    } else {
      const bin = solution.map((r) => r.map((v) => (v > 0 ? 1 : 0)));
      const cc = Engine.computeCluesFromSolution(bin);
      const colorSolution = (makerColor && makerColor.length === bin.length && makerColor[0].length === bin[0].length) ? makerColor : Engine.gridToColorGrid(bin, [ACCENTS[t.accent] || "#bef264", "#23202c"]);
      p = { id: "custom", name: "Your Puzzle", tag: "Your drawing", level: "Custom", cat: "Pictures", colored: false, rows: mrows, cols: mcols, palette: [ACCENTS[t.accent] || "#bef264"], art: bin, solution: bin, colorSolution, rowClues: cc.rowClues, colClues: cc.colClues };
    }
    setCurrentId("custom"); setMode("Play"); setDrawerOpen(false); loadActive(p);
  }, [solution, makerColor, mrows, mcols, makerFilled, makeColored, loadActive, t.accent]);

  const exportJSON = useC(() => { const cc = Engine.computeCluesFromSolution(solution.map((r) => r.map((v) => (v > 0 ? 1 : 0)))); Engine.downloadJSON("nonogram-" + mrows + "x" + mcols + ".json", { rows: mrows, cols: mcols, rowClues: cc.rowClues, colClues: cc.colClues, solution: solution.map((r) => r.map((v) => (v > 0 ? 1 : 0))) }); }, [solution, mrows, mcols]);
  const exportPNG = useC(() => {
    const colorSol = makeColored ? solution.map((row) => row.map((v) => (v > 0 ? MAKE_PALETTE[v - 1] : "transparent"))) : makerColor;
    const url = Engine.postcardPNG({ tag: "Your puzzle", name: "Your puzzle", rows: mrows, cols: mcols, colorSolution: colorSol || solution.map((r) => r.map((v) => (v > 0 ? (ACCENTS[t.accent] || "#bef264") : "transparent"))) }, { accent: ACCENTS[t.accent], dark: t.theme !== "Day" });
    const a = document.createElement("a"); a.href = url; a.download = "nonogram-" + mrows + "x" + mcols + ".png"; a.click();
  }, [solution, makerColor, makeColored, mrows, mcols, t.accent, t.theme]);

  const importJSON = useC((file) => {
    const fr = new FileReader();
    fr.onload = () => {
      try {
        const p = JSON.parse(String(fr.result));
        let sol = p.solution;
        if (!sol) throw new Error("This file has no solution grid to play.");
        let rowClues = p.rowClues, colClues = p.colClues;
        if (!rowClues || !colClues) { const cc = Engine.computeCluesFromSolution(sol); rowClues = cc.rowClues; colClues = cc.colClues; }
        const rows = sol.length, cols = sol[0].length;
        const palette = p.palette || [ACCENTS[t.accent] || "#bef264"];
        const colorSolution = p.colorSolution || Engine.gridToColorGrid(sol, palette);
        const np = { id: "custom", name: p.name || "Imported", tag: "Imported", level: "Custom", cat: "Pictures", colored: false, rows, cols, palette, art: sol, solution: sol, colorSolution, rowClues, colClues };
        setCurrentId("custom"); setMode("Play"); setDrawerOpen(false); loadActive(np);
      } catch (e) { setImportStatus("JSON error: " + e.message); alert("Could not load puzzle: " + e.message); }
    };
    fr.readAsText(file);
  }, [loadActive, t.accent]);

  const sharePostcard = useC(() => {
    const url = Engine.postcardPNG(active, { time: Engine.fmtTime(timeMs), perfect: mistakesRef.current === 0, accent: ACCENTS[t.accent], dark: t.theme !== "Day" });
    const a = document.createElement("a"); a.href = url; a.download = "nonogram-" + (active.id || "puzzle") + ".png"; a.click();
    if (window.Sfx) Sfx.ui();
  }, [active, timeMs, t.accent, t.theme]);

  const accentHex = ACCENTS[t.accent] || ACCENTS.Lime;
  const rootStyle = { ["--accent"]: accentHex };

  const nextPuzzle = useC(() => { const idx = PUZZLES.findIndex((p) => p.id === currentId); selectBuiltin(PUZZLES[(idx + 1) % PUZZLES.length].id); }, [currentId, selectBuiltin]);

  const startGame = useC(() => { if (window.Sfx) { Sfx.unlock(); Sfx.coin(); } setBooted(true); }, []);

  // colored palette strip shown above board
  const showPalette = (mode === "Play" && active.colored && !won && !dead) || (mode === "Make" && makeColored);

  const sidebarPlay = (closeDrawer) => (
    <PlayPanel
      puzzles={PUZZLES} currentId={currentId} solvedMap={solvedMap} onSelect={(id) => selectBuiltin(id)}
      lives={lives} zen={zen} time={timeMs} won={won} dead={dead}
      onReset={() => { resetBoard(); if (closeDrawer) setDrawerOpen(false); }} onHint={useHint} hintsLeft={hintsLeft} onImportJSON={importJSON}
      dailyLabel={Engine.dateLabel()} dailyDone={dailyDone} streak={stats.streak || 0} onPlayDaily={playDaily} onOpenStats={() => { setStatsOpen(true); if (window.Sfx) Sfx.ui(); }} dailyActive={dailyActive} onOpenLibrary={() => { setLibOpen(true); setDrawerOpen(false); if (window.Sfx) Sfx.ui(); }}
    />
  );
  const sidebarMake = () => (
    <MakePanel
      rows={mrows} cols={mcols} setRows={setRowsSafe} setCols={setColsSafe} filled={makerFilled}
      onClear={() => { if (window.Sfx) Sfx.ui(); setSolution(Engine.emptyGrid(mrows, mcols, 0)); setMakerColor(null); }}
      onLoadInPlayer={loadInPlayer} onImportImage={importImage} importStatus={importStatus}
      importSize={importSize} setImportSize={setImportSize} threshold={threshold} setThreshold={setThreshold} invert={invert} setInvert={setInvert}
      showColorPreview={showColorPreview} toggleColorPreview={() => setShowColorPreview((s) => !s)} hasColor={!!makerColor}
      onExportJSON={exportJSON} onExportPNG={exportPNG} makeColored={makeColored} setMakeColored={setMakeColored}
    />
  );

  return (
    <div className={"app " + (t.theme === "Day" ? "day" : "dusk") + (t.scan ? " scan" : "") + (t.guide ? "" : " noguide")} style={rootStyle}>
      {!booted && <BootScreen onStart={startGame} />}

      <header className="masthead">
        <button className="hamburger" onClick={() => setDrawerOpen(true)} aria-label="Menu">≡</button>
        <div className="brand">
          <span className="logo-mark" aria-hidden="true"><PixelLogo size={30} /></span>
          <div className="brand-text">
            <h1 className="wordmark">NONOGRAM<span className="ac"> LAB</span></h1>
            <div className="dek">picture-logic puzzles</div>
          </div>
        </div>
        <button className={"sound-btn" + (muted ? " off" : "")} onClick={() => { setMute(!muted); }} title={muted ? "Sound off" : "Sound on"} aria-label="Toggle sound">♪</button>
        <div className="mode-switch" role="tablist">
          <button className={"ms-btn" + (mode === "Play" ? " on" : "")} onClick={() => { setShowWin(false); setMode("Play"); if (window.Sfx) Sfx.ui(); }}>Solve</button>
          <button className={"ms-btn" + (mode === "Make" ? " on" : "")} onClick={() => { setShowWin(false); setMode("Make"); if (window.Sfx) Sfx.ui(); }}>Make</button>
        </div>
      </header>

      <main className="layout">
        <aside className="sidebar">{mode === "Play" ? sidebarPlay(false) : sidebarMake()}</aside>

        <section className="board-area">
          <div className="board-caption">
            {mode === "Play" ? (
              dead ? <span className="cap-bad">Out of lives — Reset to try again</span>
                : won ? <span className="cap-good">Solved in {Engine.fmtTime(timeMs)}{mistakesRef.current === 0 ? " · perfect!" : ""}</span>
                : hardcore ? <span>Hardcore — mistakes stick. Left fills · right marks ✕ · drag to paint</span>
                : <span>Left-click fills · right-click marks ✕ · drag to paint · shift-click a line</span>
            ) : (<span>Draw your picture — clues compute live. Right-click erases.</span>)}
          </div>

          {showPalette && (
            <PalettePicker
              palette={mode === "Make" ? MAKE_PALETTE : active.palette}
              value={mode === "Make" ? makeColor : playColor}
              onChange={mode === "Make" ? setMakeColor : setPlayColor}
              withErase={mode === "Make"}
              label={mode === "Make" ? "Paint" : "Place"}
            />
          )}

          {mode === "Play" && !won && !dead && (
            <div className="tap-mode">
              <span className="tap-mode-label">Tap</span>
              <div className="tap-switch">
                <button className={"tm-btn" + (tapMode === "fill" ? " on" : "")} onClick={() => { setTapMode("fill"); if (window.Sfx) Sfx.ui(); }}>
                  <span className="tm-ic fill" />Fill
                </button>
                <button className={"tm-btn" + (tapMode === "mark" ? " on" : "")} onClick={() => { setTapMode("mark"); if (window.Sfx) Sfx.ui(); }}>
                  <span className="tm-ic mark">✕</span>Mark
                </button>
              </div>
            </div>
          )}

          <div className={"board-wrap" + (shake ? " shaking" : "")}>
            {mode === "Play" ? (
              <Board key={"play-" + active.id + "-" + active.rows + "x" + active.cols}
                cells={board} rowClues={active.rowClues} colClues={active.colClues}
                onChange={playMove} readOnly={won || dead}
                animMarks={animMarks} wrongKey={wrongKey} colorGrid={reveal} sweep={true}
                colored={active.colored} palette={active.palette} activeColor={playColor} tapMode={tapMode} />
            ) : (
              <Board key={"make-" + mrows + "x" + mcols}
                cells={solution} rowClues={makerClues.rowClues} colClues={makerClues.colClues}
                onChange={makeMove} readOnly={false} animMarks={{}} wrongKey={null}
                colorGrid={showColorPreview ? makerColor : null}
                colored={makeColored} palette={MAKE_PALETTE} activeColor={makeColor} />
            )}
          </div>
        </section>
      </main>

      {showWin && (
        <div className="win-scrim" onClick={() => setShowWin(false)}>
          <Confetti palette={active.palette} />
          <div className="win-card" onClick={(e) => e.stopPropagation()}>
            <div className="win-banner">SOLVED!</div>
            <div className="win-pic"><Thumb puzzle={active} solved={true} size={132} /></div>
            <div className="win-reveal-label">It was</div>
            <div className="win-name">{active.tag}</div>
            <div className="win-badges">
              {mistakesRef.current === 0 && <span className="badge perfect">★ PERFECT</span>}
              {hintsUsedRef.current === 0 && <span className="badge">NO HINTS</span>}
              {dailyActive && <span className="badge daily">▲ STREAK {stats.streak}</span>}
            </div>
            <div className="win-stats">
              <div><span className="ws-num">{Engine.fmtTime(timeMs)}</span><span className="ws-lab">your time</span></div>
              {active.id && active.id !== "custom" && solvedMap[active.id] != null && (
                <div><span className="ws-num">{Engine.fmtTime(solvedMap[active.id])}</span><span className="ws-lab">best</span></div>
              )}
            </div>
            <div className="win-actions">
              <button className="btn ghost" onClick={sharePostcard}>Share</button>
              <button className="btn" onClick={() => { setShowWin(false); resetBoard(); }}>Replay</button>
              <button className="btn primary" onClick={nextPuzzle}>Next →</button>
            </div>
          </div>
        </div>
      )}

      {statsOpen && <StatsModal stats={stats} puzzles={PUZZLES} solvedMap={solvedMap} onClose={() => setStatsOpen(false)} />}

      {libOpen && <LibraryModal puzzles={PUZZLES} currentId={currentId} solvedMap={solvedMap} onSelect={(id) => { selectBuiltin(id); setLibOpen(false); }} onClose={() => setLibOpen(false)} />}

      {drawerOpen && (
        <div className="drawer-scrim" onClick={() => setDrawerOpen(false)}>
          <div className="drawer" onClick={(e) => e.stopPropagation()}>
            <div className="drawer-head">
              <div className="mode-switch">
                <button className={"ms-btn" + (mode === "Play" ? " on" : "")} onClick={() => setMode("Play")}>Solve</button>
                <button className={"ms-btn" + (mode === "Make" ? " on" : "")} onClick={() => setMode("Make")}>Make</button>
              </div>
              <button className="drawer-close" onClick={() => setDrawerOpen(false)}>✕</button>
            </div>
            {mode === "Play" ? sidebarPlay(true) : sidebarMake()}
          </div>
        </div>
      )}

      <TweaksPanel>
        <TweakSection label="Cabinet" />
        <TweakColor label="Accent" value={ACCENTS[t.accent] || ACCENTS.Lime} options={Object.values(ACCENTS)}
          onChange={(hex) => { const name = Object.keys(ACCENTS).find((k) => ACCENTS[k] === hex) || "Lime"; setTweak("accent", name); }} />
        <TweakRadio label="Theme" value={t.theme} options={["Dusk", "Day"]} onChange={(v) => setTweak("theme", v)} />
        <TweakToggle label="CRT scanlines" value={t.scan} onChange={(v) => setTweak("scan", v)} />
        <TweakToggle label="Sound" value={!muted} onChange={(v) => setMute(!v)} />
        <TweakSection label="Play" />
        <TweakToggle label="Zen mode (no lives)" value={t.zen} onChange={(v) => setTweak("zen", v)} />
        <TweakToggle label="Hardcore (mistakes stick)" value={t.hardcore} onChange={(v) => setTweak("hardcore", v)} />
        <TweakToggle label="Row / column guide" value={t.guide} onChange={(v) => setTweak("guide", v)} />
      </TweaksPanel>
    </div>
  );
}

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