// app.jsx — Visual Timer main application
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ===== Default settings =====
const DEFAULTS = {
  title: "Playtime",
  durationSec: 5 * 60,           // 5 minutes
  imageData: null,                // base64 data url
  imageTransform: null,           // {bgScale, bgXPct, bgYPct, rotate}
  revealMode: "clockwise",        // 'clockwise' | 'sand' | 'pixel'
  diskColor: "peach",             // palette key
  endBehavior: "chime",           // 'chime' | 'pulse' | 'confetti' | 'silent'
  soundOn: true,
  showDigital: true,
};

const STORAGE_KEY = "visual-timer-v1";
const RUN_STATE_KEY = "visual-timer-run-v1";

// Palette colors (for disk cover)
const PALETTE = {
  peach: "oklch(0.78 0.11 40)",
  rose: "oklch(0.80 0.09 15)",
  sage: "oklch(0.82 0.09 145)",
  lavender: "oklch(0.82 0.09 290)",
  butter: "oklch(0.88 0.11 90)",
  sky: "oklch(0.82 0.08 230)",
};

function loadSettings() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return { ...DEFAULTS };
    return { ...DEFAULTS, ...JSON.parse(raw) };
  } catch (e) {
    return { ...DEFAULTS };
  }
}
function saveSettings(s) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch (e) {}
}
function loadRunState() {
  try {
    const raw = localStorage.getItem(RUN_STATE_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (e) { return null; }
}
function saveRunState(s) {
  try {
    if (s === null) localStorage.removeItem(RUN_STATE_KEY);
    else localStorage.setItem(RUN_STATE_KEY, JSON.stringify(s));
  } catch (e) {}
}

// ===== Sound (built-in chime via WebAudio) =====
let audioCtx = null;
function getCtx() {
  if (!audioCtx) {
    try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
  }
  return audioCtx;
}
function playChime() {
  const ctx = getCtx();
  if (!ctx) return;
  const now = ctx.currentTime;
  // Two-note gentle bell
  [880, 1320].forEach((freq, i) => {
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.frequency.value = freq;
    o.type = "sine";
    g.gain.setValueAtTime(0, now + i * 0.18);
    g.gain.linearRampToValueAtTime(0.18, now + i * 0.18 + 0.02);
    g.gain.exponentialRampToValueAtTime(0.0001, now + i * 0.18 + 1.4);
    o.connect(g).connect(ctx.destination);
    o.start(now + i * 0.18);
    o.stop(now + i * 0.18 + 1.5);
  });
}
function playTick() {
  const ctx = getCtx();
  if (!ctx) return;
  const o = ctx.createOscillator();
  const g = ctx.createGain();
  o.frequency.value = 600;
  o.type = "triangle";
  const now = ctx.currentTime;
  g.gain.setValueAtTime(0.0001, now);
  g.gain.linearRampToValueAtTime(0.06, now + 0.005);
  g.gain.exponentialRampToValueAtTime(0.0001, now + 0.08);
  o.connect(g).connect(ctx.destination);
  o.start(now);
  o.stop(now + 0.1);
}

// ===== Theme palettes (Tweaks) =====
const THEMES = {
  cream: {
    "--bg": "oklch(0.97 0.018 75)",
    "--bg-deeper": "oklch(0.94 0.025 70)",
    "--surface": "oklch(0.99 0.012 80)",
    "--ink": "oklch(0.32 0.04 50)",
    "--ink-soft": "oklch(0.52 0.035 55)",
    "--ink-faint": "oklch(0.72 0.02 60)",
    "--line": "oklch(0.88 0.02 70)",
  },
  mint: {
    "--bg": "oklch(0.97 0.022 165)",
    "--bg-deeper": "oklch(0.94 0.03 160)",
    "--surface": "oklch(0.99 0.014 170)",
    "--ink": "oklch(0.32 0.04 180)",
    "--ink-soft": "oklch(0.52 0.035 175)",
    "--ink-faint": "oklch(0.72 0.022 170)",
    "--line": "oklch(0.88 0.022 170)",
  },
  lavender: {
    "--bg": "oklch(0.97 0.018 300)",
    "--bg-deeper": "oklch(0.94 0.025 295)",
    "--surface": "oklch(0.99 0.012 305)",
    "--ink": "oklch(0.32 0.04 290)",
    "--ink-soft": "oklch(0.52 0.035 295)",
    "--ink-faint": "oklch(0.72 0.02 295)",
    "--line": "oklch(0.88 0.02 295)",
  },
  dusk: {
    "--bg": "oklch(0.24 0.02 270)",
    "--bg-deeper": "oklch(0.20 0.025 270)",
    "--surface": "oklch(0.28 0.025 270)",
    "--ink": "oklch(0.94 0.02 80)",
    "--ink-soft": "oklch(0.78 0.02 80)",
    "--ink-faint": "oklch(0.58 0.02 80)",
    "--line": "oklch(0.38 0.025 270)",
  },
};

const TITLE_FONTS = {
  caprasimo: "\"Caprasimo\", serif",
  quicksand: "\"Quicksand\", sans-serif",
};

// ===== App root =====
function App() {
  const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS);
  const [settings, setSettings] = useState(loadSettings);
  const [screen, setScreen] = useState("timer"); // 'timer' | 'settings'
  const pixelOrder = useMemo(
    () => makePixelOrder(t.pixelDensity * t.pixelDensity, 7),
    [t.pixelDensity]
  );

  // Run state — { startedAt (ms epoch), elapsedAtPauseMs, paused }
  const [run, setRun] = useState(() => loadRunState());
  const [tickNow, setTickNow] = useState(Date.now());
  const [doneState, setDoneState] = useState(false);
  const doneFiredRef = useRef(false);

  // Persist settings
  useEffect(() => { saveSettings(settings); }, [settings]);
  // Persist run state
  useEffect(() => { saveRunState(run); }, [run]);

  // Tick — setInterval (works when backgrounded; RAF would freeze).
  // We always tick so the display refreshes immediately on Start, even though we
  // only need it while running. Cheap (5Hz).
  useEffect(() => {
    const id = setInterval(() => setTickNow(Date.now()), 200);
    // Also refresh on visibility change (when tab/phone wakes up) so we resync
    // immediately instead of waiting for the next interval tick.
    const onVis = () => setTickNow(Date.now());
    document.addEventListener("visibilitychange", onVis);
    return () => { clearInterval(id); document.removeEventListener("visibilitychange", onVis); };
  }, []);

  // Compute remaining ms
  const totalMs = settings.durationSec * 1000;
  const rawElapsed = run
    ? (run.paused
        ? run.elapsedAtPauseMs
        : (tickNow - run.startedAt) + (run.elapsedAtPauseMs || 0))
    : 0;
  // Clamp to a sane range; defensive against clock drift or stale tickNow.
  const elapsedMs = Math.min(totalMs, Math.max(0, rawElapsed));
  const remainingMs = Math.max(0, totalMs - elapsedMs);
  const progress = totalMs > 0 ? Math.min(1, elapsedMs / totalMs) : 0;

  // Done detection
  useEffect(() => {
    if (run && remainingMs <= 0 && !doneFiredRef.current) {
      doneFiredRef.current = true;
      setDoneState(true);
      if (settings.soundOn && (settings.endBehavior === "chime" || settings.endBehavior === "pulse" || settings.endBehavior === "confetti")) {
        playChime();
      }
    }
  }, [remainingMs, run, settings.soundOn, settings.endBehavior]);

  // Reset done when run cleared/restarted
  useEffect(() => {
    if (!run || remainingMs > 0) {
      doneFiredRef.current = false;
      setDoneState(false);
    }
  }, [run]);

  // Actions
  const start = useCallback(() => {
    const ctx = getCtx();
    if (ctx && ctx.state === "suspended") ctx.resume();
    setRun({ startedAt: Date.now(), elapsedAtPauseMs: 0, paused: false });
  }, []);
  const pause = useCallback(() => {
    setRun((r) => {
      if (!r || r.paused) return r;
      const elapsed = (Date.now() - r.startedAt) + (r.elapsedAtPauseMs || 0);
      return { ...r, paused: true, elapsedAtPauseMs: elapsed };
    });
  }, []);
  const resume = useCallback(() => {
    setRun((r) => {
      if (!r || !r.paused) return r;
      return { ...r, paused: false, startedAt: Date.now() };
    });
  }, []);
  const reset = useCallback(() => {
    setRun(null);
    doneFiredRef.current = false;
    setDoneState(false);
  }, []);
  const addMinute = useCallback(() => {
    setSettings((s) => ({ ...s, durationSec: s.durationSec + 60 }));
  }, []);

  // Dismiss done overlay
  const dismissDone = () => {
    setDoneState(false);
    reset();
  };

  // Settings updaters
  const updateSettings = (patch) => setSettings((s) => ({ ...s, ...patch }));

  const diskColor = PALETTE[settings.diskColor] || PALETTE.peach;

  // Apply theme CSS variables + tweak-driven sizes
  const themeVars = THEMES[t.theme] || THEMES.cream;
  const appStyle = {
    ...themeVars,
    "--disk-size": `${t.diskSizePct}vw`,
    "--disk-size-max": `${Math.round(t.diskSizePct * 4.7)}px`,
    "--digital-size": `${t.digitalSize}px`,
    "--title-font": TITLE_FONTS[t.titleFont] || TITLE_FONTS.caprasimo,
  };

  return (
    <div className="app" style={appStyle}>
      {screen === "timer" ? (
        <TimerScreen
          settings={settings}
          run={run}
          progress={progress}
          remainingMs={remainingMs}
          diskColor={diskColor}
          pixelOrder={pixelOrder}
          doneState={doneState}
          dismissDone={dismissDone}
          onStart={start}
          onPause={pause}
          onResume={resume}
          onReset={reset}
          onAddMinute={addMinute}
          onOpenSettings={() => setScreen("settings")}
          tweaks={t}
        />
      ) : (
        <SettingsScreen
          settings={settings}
          onUpdate={updateSettings}
          onClose={() => setScreen("timer")}
          pixelOrder={pixelOrder}
        />
      )}

      <TweaksPanel>
        <TweakSection label="Theme" />
        <TweakRadio label="Palette" value={t.theme}
          options={["cream", "mint", "lavender", "dusk"]}
          onChange={(v) => setTweak("theme", v)} />
        <TweakRadio label="Title font" value={t.titleFont}
          options={["caprasimo", "quicksand"]}
          onChange={(v) => setTweak("titleFont", v)} />

        <TweakSection label="Timer disk" />
        <TweakSlider label="Disk size" value={t.diskSizePct}
          min={60} max={92} step={1} unit="vw"
          onChange={(v) => setTweak("diskSizePct", v)} />
        <TweakSlider label="Countdown size" value={t.digitalSize}
          min={22} max={72} step={1} unit="px"
          onChange={(v) => setTweak("digitalSize", v)} />
        <TweakRadio label="Edge effect" value={t.edgeStyle}
          options={["sand", "bubbles", "crisp"]}
          onChange={(v) => setTweak("edgeStyle", v)} />
        <TweakRadio label="Direction" value={t.clockwiseDirection}
          options={["clockwise", "counterclockwise"]}
          onChange={(v) => setTweak("clockwiseDirection", v)} />
        <TweakToggle label="Soft sand boundary" value={t.softBoundary}
          onChange={(v) => setTweak("softBoundary", v)} />
        <TweakSlider label="Pixel grid" value={t.pixelDensity}
          min={6} max={16} step={1} unit="²"
          onChange={(v) => setTweak("pixelDensity", v)} />
      </TweaksPanel>
    </div>
  );
}

// ===== Timer Screen =====
function TimerScreen({
  settings, run, progress, remainingMs, diskColor, pixelOrder,
  doneState, dismissDone,
  onStart, onPause, onResume, onReset, onAddMinute, onOpenSettings,
  tweaks
}) {
  const mm = Math.floor(remainingMs / 60000);
  const ss = Math.floor((remainingMs % 60000) / 1000);
  const hh = Math.floor(remainingMs / 3600000);
  const timeStr = hh > 0
    ? `${hh}:${String(mm % 60).padStart(2, "0")}:${String(ss).padStart(2, "0")}`
    : `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;

  const isRunning = run && !run.paused && remainingMs > 0;
  const isPaused = run && run.paused;
  const isIdle = !run;

  // Subtitle text
  let subtitle = `${formatDuration(settings.durationSec)} timer`;
  if (isRunning) subtitle = "Counting down…";
  else if (isPaused) subtitle = "Paused";
  else if (doneState) subtitle = "All done! ✨";

  const pulseEnd = doneState && settings.endBehavior === "pulse";

  return (
    <>
      <div className="topbar">
        <div className="brand">
          <span className="brand-dot" aria-hidden="true"></span>
          <span>visual timer</span>
        </div>
        <button className="icon-btn" aria-label="Settings" onClick={onOpenSettings}>
          <SettingsIcon/>
        </button>
      </div>

      <div className="timer-screen">
        <h1 className="timer-title">{settings.title || "Playtime"}</h1>
        <div className="timer-subtitle">{subtitle}</div>

        <div className="disk-wrap">
          <div className={`disk${pulseEnd ? " pulse" : ""}`}>
            <RevealDisk
              progress={progress}
              imageData={settings.imageData}
              imageTransform={settings.imageTransform}
              mode={settings.revealMode}
              color={diskColor}
              gridSize={tweaks?.pixelDensity || 10}
              pixelOrder={pixelOrder}
              direction={tweaks?.clockwiseDirection || "clockwise"}
              softBoundary={tweaks?.softBoundary !== false}
              edgeStyle={tweaks?.edgeStyle || "sand"}
            />
            {settings.showDigital && !doneState && (
              <div className="disk-digital">{timeStr}</div>
            )}
          </div>

          {doneState && (
            <div className="done-overlay">
              <div className="done-banner" role="alert">
                Time's up! Tap to clear ✨
                <div style={{ marginTop: 10, display: "flex", justifyContent: "center" }}>
                  <button className="btn btn-primary btn-small" onClick={dismissDone}>OK</button>
                </div>
              </div>
            </div>
          )}

          {doneState && settings.endBehavior === "confetti" && <Confetti />}
        </div>

        <div className="controls">
          {isIdle && !doneState && (
            <button className="btn btn-primary btn-round big" onClick={onStart} aria-label="Start">
              <PlayIcon/>
            </button>
          )}
          {isRunning && (
            <>
              <button className="btn btn-round" onClick={onAddMinute} aria-label="Add one minute" title="Add 1 minute">
                <PlusIcon/>
              </button>
              <button className="btn btn-primary btn-round big" onClick={onPause} aria-label="Pause">
                <PauseIcon/>
              </button>
              <button className="btn btn-round" onClick={onReset} aria-label="Reset">
                <ResetIcon/>
              </button>
            </>
          )}
          {isPaused && (
            <>
              <button className="btn btn-round" onClick={onAddMinute} aria-label="Add minute">
                <PlusIcon/>
              </button>
              <button className="btn btn-primary btn-round big" onClick={onResume} aria-label="Resume">
                <PlayIcon/>
              </button>
              <button className="btn btn-round" onClick={onReset} aria-label="Reset">
                <ResetIcon/>
              </button>
            </>
          )}
          {doneState && (
            <button className="btn btn-primary" onClick={dismissDone}>Restart</button>
          )}
        </div>

        <div className="timer-meta">
          {isIdle ? <>Ready · <strong>{formatDuration(settings.durationSec)}</strong></>
            : <>Total: <strong>{formatDuration(settings.durationSec)}</strong></>}
        </div>
      </div>
    </>
  );
}

// ===== Settings Screen =====
function SettingsScreen({ settings, onUpdate, onClose, pixelOrder }) {
  const [editing, setEditing] = useState(false);
  const fileInputRef = useRef(null);

  // Local HMS for the manual picker
  const [h, setH] = useState(Math.floor(settings.durationSec / 3600));
  const [m, setM] = useState(Math.floor((settings.durationSec % 3600) / 60));
  const [s, setS] = useState(settings.durationSec % 60);
  useEffect(() => {
    const dur = h * 3600 + m * 60 + s;
    if (dur !== settings.durationSec) onUpdate({ durationSec: dur });
  }, [h, m, s]);

  const presets = [
    { label: "1 min", sec: 60 },
    { label: "3 min", sec: 180 },
    { label: "5 min", sec: 300 },
    { label: "10 min", sec: 600 },
    { label: "20 min", sec: 1200 },
    { label: "30 min", sec: 1800 },
  ];
  const setPreset = (sec) => {
    setH(Math.floor(sec / 3600));
    setM(Math.floor((sec % 3600) / 60));
    setS(sec % 60);
  };

  const onPickFile = () => fileInputRef.current?.click();
  const onFileChange = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => {
      onUpdate({ imageData: reader.result, imageTransform: null });
      setEditing(true);
    };
    reader.readAsDataURL(f);
    e.target.value = "";
  };

  const removeImage = () => {
    onUpdate({ imageData: null, imageTransform: null });
  };

  return (
    <>
      <div className="topbar">
        <button className="icon-btn" aria-label="Back" onClick={onClose}>
          <BackIcon/>
        </button>
        <div className="brand" style={{ fontSize: 18 }}>timer settings</div>
        <div style={{ width: 44 }} />
      </div>

      <div className="settings-screen">
        {/* Title */}
        <div className="section">
          <div className="section-label">Label</div>
          <input
            type="text"
            className="text-input"
            placeholder="e.g. Brush teeth, Until bath, Quiet time"
            value={settings.title}
            onChange={(e) => onUpdate({ title: e.target.value })}
            maxLength={40}
          />
        </div>

        {/* Presets */}
        <div className="section">
          <div className="section-label">Quick presets</div>
          <div className="chips">
            {presets.map((p) => (
              <button
                key={p.sec}
                className={`chip${settings.durationSec === p.sec ? " active" : ""}`}
                onClick={() => setPreset(p.sec)}
              >
                {p.label}
              </button>
            ))}
          </div>
        </div>

        {/* Manual HMS */}
        <div className="section">
          <div className="section-label">Or set exact time</div>
          <div className="hms">
            <div className="hms-cell">
              <input
                className="hms-input"
                type="number"
                min="0" max="23"
                value={h}
                onChange={(e) => setH(clampNum(e.target.value, 0, 23))}
              />
              <label>hr</label>
            </div>
            <span className="hms-sep">:</span>
            <div className="hms-cell">
              <input
                className="hms-input"
                type="number"
                min="0" max="59"
                value={m}
                onChange={(e) => setM(clampNum(e.target.value, 0, 59))}
              />
              <label>min</label>
            </div>
            <span className="hms-sep">:</span>
            <div className="hms-cell">
              <input
                className="hms-input"
                type="number"
                min="0" max="59"
                value={s}
                onChange={(e) => setS(clampNum(e.target.value, 0, 59))}
              />
              <label>sec</label>
            </div>
          </div>
        </div>

        {/* Image upload */}
        <div className="section">
          <div className="section-label">Hidden image</div>
          <div className={`upload-card${settings.imageData ? " has-image" : ""}`}>
            <div
              className={`upload-thumb${settings.imageData ? "" : " empty"}`}
              style={settings.imageData ? {
                backgroundImage: `url(${settings.imageData})`,
                backgroundSize: settings.imageTransform ? `${100 * settings.imageTransform.bgScale}%` : "cover",
                backgroundPosition: settings.imageTransform
                  ? `${50 + settings.imageTransform.bgXPct}% ${50 + settings.imageTransform.bgYPct}%`
                  : "center"
              } : {}}
            >
              {!settings.imageData && <ImageIcon/>}
            </div>
            <div className="upload-meta">
              <div className="t">{settings.imageData ? "Image ready" : "No image yet"}</div>
              <div className="s">
                {settings.imageData ? "Tap below to reposition or replace." : "Picks something fun your kid will want to see appear."}
              </div>
              <div className="upload-actions">
                <button className="btn btn-small" onClick={onPickFile}>
                  {settings.imageData ? "Replace" : "Upload"}
                </button>
                {settings.imageData && (
                  <>
                    <button className="btn btn-small" onClick={() => setEditing(true)}>Reposition</button>
                    <button className="btn btn-small btn-ghost" onClick={removeImage}>Remove</button>
                  </>
                )}
              </div>
            </div>
          </div>
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            style={{ display: "none" }}
            onChange={onFileChange}
          />
        </div>

        {/* Reveal mode */}
        <div className="section">
          <div className="section-label">Reveal style</div>
          <div className="segmented cols-3">
            {[
              { id: "clockwise", label: "Clockwise" },
              { id: "sand", label: "Sand" },
              { id: "pixel", label: "Pixels" },
            ].map((opt) => (
              <button
                key={opt.id}
                className={`seg-card${settings.revealMode === opt.id ? " active" : ""}`}
                onClick={() => onUpdate({ revealMode: opt.id })}
              >
                <div className="seg-card-preview">
                  <PreviewMini mode={opt.id} pixelOrder={pixelOrder} />
                </div>
                <span className="seg-card-label">{opt.label}</span>
              </button>
            ))}
          </div>
          <div className="help">
            {settings.revealMode === "clockwise" && "A pie wedge shrinks clockwise as time passes."}
            {settings.revealMode === "sand" && "The cover drains downward, like sand in an hourglass."}
            {settings.revealMode === "pixel" && "Tiles disappear one by one to reveal the image."}
          </div>
        </div>

        {/* Disk color */}
        <div className="section">
          <div className="section-label">Cover color</div>
          <div className="chips">
            {Object.keys(PALETTE).map((k) => (
              <button
                key={k}
                onClick={() => onUpdate({ diskColor: k })}
                className="chip"
                style={{
                  background: PALETTE[k],
                  color: "oklch(0.32 0.04 50)",
                  borderColor: settings.diskColor === k ? "oklch(0.32 0.04 50)" : "transparent",
                  paddingLeft: 16, paddingRight: 16,
                }}
              >
                {k}
              </button>
            ))}
          </div>
        </div>

        {/* End behavior */}
        <div className="section">
          <div className="section-label">When time's up</div>
          <div className="segmented" style={{ gridTemplateColumns: "1fr 1fr" }}>
            {[
              { id: "chime", label: "Gentle chime" },
              { id: "pulse", label: "Pulsing glow" },
              { id: "confetti", label: "Confetti 🎉" },
              { id: "silent", label: "Silent" },
            ].map((opt) => (
              <button
                key={opt.id}
                className={`seg-card${settings.endBehavior === opt.id ? " active" : ""}`}
                onClick={() => onUpdate({ endBehavior: opt.id })}
                style={{ padding: "14px 12px" }}
              >
                <span className="seg-card-label" style={{ fontSize: 13 }}>{opt.label}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Toggles */}
        <div className="section">
          <div className="section-label">Options</div>
          <ToggleRow
            title="Sound"
            sub="Chime when timer ends"
            value={settings.soundOn}
            onChange={(v) => onUpdate({ soundOn: v })}
          />
          <ToggleRow
            title="Show digital countdown"
            sub="Display mm:ss inside the circle"
            value={settings.showDigital}
            onChange={(v) => onUpdate({ showDigital: v })}
          />
        </div>

        <div className="save-bar">
          <button className="btn btn-primary" onClick={onClose}>Save & close</button>
        </div>
      </div>

      {editing && settings.imageData && (
        <ImageEditor
          imageData={settings.imageData}
          initialTransform={settings.imageTransform ? {
            tx: settings.imageTransform.tx,
            ty: settings.imageTransform.ty,
            scale: settings.imageTransform.scale,
            rotate: settings.imageTransform.rotate,
          } : null}
          onSave={(t) => { onUpdate({ imageTransform: t }); setEditing(false); }}
          onCancel={() => setEditing(false)}
        />
      )}
    </>
  );
}

// ===== Helpers / small components =====
function clampNum(v, lo, hi) {
  const n = parseInt(v) || 0;
  return Math.min(hi, Math.max(lo, n));
}
function formatDuration(sec) {
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0 && s > 0) return `${m}m ${s}s`;
  if (m > 0) return `${m} min`;
  return `${s} sec`;
}

function ToggleRow({ title, sub, value, onChange }) {
  return (
    <div className="toggle-row" onClick={() => onChange(!value)} role="button">
      <div className="toggle-row-text">
        <div className="t">{title}</div>
        {sub && <div className="s">{sub}</div>}
      </div>
      <div className={`toggle${value ? " on" : ""}`}></div>
    </div>
  );
}

function PreviewMini({ mode, pixelOrder }) {
  // Static 60% progress preview
  const p = 0.4;
  const color = "oklch(0.78 0.11 40)";
  if (mode === "clockwise") {
    const theta = p * 2 * Math.PI;
    const endX = 50 + 50 * Math.sin(theta);
    const endY = 50 - 50 * Math.cos(theta);
    const largeArc = (1 - p) > 0.5 ? 1 : 0;
    const d = `M 50 50 L 50 0 A 50 50 0 ${largeArc} 0 ${endX.toFixed(2)} ${endY.toFixed(2)} Z`;
    return (
      <svg viewBox="0 0 100 100" width="100%" height="100%">
        <circle cx="50" cy="50" r="50" fill="oklch(0.88 0.04 65)" />
        <path d={d} fill={color} />
      </svg>
    );
  }
  if (mode === "sand") {
    const h = (1 - p) * 100;
    return (
      <svg viewBox="0 0 100 100" width="100%" height="100%">
        <circle cx="50" cy="50" r="50" fill="oklch(0.88 0.04 65)" />
        <defs><clipPath id="cp"><rect x="0" y="0" width="100" height={h} /></clipPath></defs>
        <circle cx="50" cy="50" r="50" fill={color} clipPath="url(#cp)" />
      </svg>
    );
  }
  // pixel
  const size = 6;
  const total = size * size;
  const gone = Math.floor(p * total);
  const order = makePixelOrder(total, 11);
  const goneSet = new Set(order.slice(0, gone));
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: `repeat(${size}, 1fr)`,
      gridTemplateRows: `repeat(${size}, 1fr)`,
      width: "100%", height: "100%",
      background: "oklch(0.88 0.04 65)",
      borderRadius: "50%", overflow: "hidden"
    }}>
      {Array.from({ length: total }).map((_, i) => (
        <div key={i} style={{
          background: goneSet.has(i) ? "transparent" : color,
          transition: "opacity 200ms"
        }} />
      ))}
    </div>
  );
}

function Confetti() {
  const colors = ["oklch(0.78 0.11 40)", "oklch(0.82 0.09 145)", "oklch(0.82 0.09 290)", "oklch(0.88 0.11 90)", "oklch(0.80 0.09 15)"];
  const pieces = useMemo(() =>
    Array.from({ length: 36 }).map((_, i) => ({
      left: Math.random() * 100,
      dx: (Math.random() - 0.5) * 200,
      delay: Math.random() * 0.6,
      color: colors[i % colors.length],
      rotate: Math.random() * 360,
    })), []);
  return (
    <div style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}>
      {pieces.map((p, i) => (
        <div key={i} className="confetti-piece" style={{
          left: `${p.left}%`,
          top: 0,
          background: p.color,
          animationDelay: `${p.delay}s`,
          "--dx": `${p.dx}px`,
          transform: `rotate(${p.rotate}deg)`,
        }} />
      ))}
    </div>
  );
}

// ===== Icons =====
function PlayIcon() { return <svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.73l-11-6.5A1 1 0 0 0 8 5.5z"/></svg>; }
function PauseIcon() { return <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1.5"/><rect x="14" y="5" width="4" height="14" rx="1.5"/></svg>; }
function ResetIcon() { return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><polyline points="3 4 3 9 8 9"/></svg>; }
function PlusIcon() { return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>; }
function SettingsIcon() { return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>; }
function BackIcon() { return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>; }
function ImageIcon() { return <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="M21 15l-5-5-9 9"/></svg>; }

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