/*
 * game-shared.jsx — components, helpers, and tweak/palette plumbing
 * shared by both games. Exposes everything on `window` because each
 * <script type="text/babel"> file gets its own scope.
 */

// ── Palette / font / shape options ───────────────────────────────────────
const PALETTES = {
  cream_teal:  { label: "קרם · טורקיז",  colors: ["#18605a", "#f6efe2", "#fffaf1"], extra: { accentInk: "#ffffff", bgDeep: "#ecdfc6", sun: "#f3b04a", good: "#2f9d6e", bad: "#d6452e", ink: "#2b2118", inkSoft: "#6b5b48" } },
  peach_plum:  { label: "אפרסק · שזיף",  colors: ["#7a3158", "#fde8d7", "#fff5ec"], extra: { accentInk: "#ffffff", bgDeep: "#f6d0b5", sun: "#e88a4a", good: "#3aa17a", bad: "#c63a45", ink: "#2e1a26", inkSoft: "#7a5b69" } },
  mint_indigo: { label: "מנטה · אינדיגו", colors: ["#3d3a8a", "#e6f1e6", "#f3faf3"], extra: { accentInk: "#ffffff", bgDeep: "#cfe3d0", sun: "#f0b94a", good: "#2f9d6e", bad: "#c8442c", ink: "#1f1d3d", inkSoft: "#5e5c7d" } },
  sky_navy:    { label: "שמיים · נייבי",  colors: ["#1d3a6e", "#e9f1fa", "#f5f9fd"], extra: { accentInk: "#ffffff", bgDeep: "#cddef0", sun: "#f4b94a", good: "#2f9d6e", bad: "#d6452e", ink: "#15233f", inkSoft: "#536685" } },
};

const FONT_OPTIONS = [
  { value: "Fredoka",   label: "Fredoka" },
  { value: "Rubik",     label: "Rubik" },
  { value: "Heebo",     label: "Heebo" },
  { value: "Assistant", label: "Assistant" },
];

// ── Helpers ──────────────────────────────────────────────────────────────
function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function pickDistractors(pool, correct, n) {
  return shuffle(pool.filter((x) => x !== correct)).slice(0, n);
}

// ── Hook: apply tweak state to CSS custom properties ─────────────────────
function useApplyTweaks(t) {
  React.useEffect(() => {
    const root = document.documentElement.style;
    const found = Object.values(PALETTES).find((p) =>
      JSON.stringify(p.colors) === JSON.stringify(t.palette)
    ) || PALETTES.cream_teal;
    root.setProperty("--accent",     found.colors[0]);
    root.setProperty("--bg",         found.colors[1]);
    root.setProperty("--card",       found.colors[2]);
    root.setProperty("--accent-ink", found.extra.accentInk);
    root.setProperty("--bg-deep",    found.extra.bgDeep);
    root.setProperty("--sun",        found.extra.sun);
    root.setProperty("--good",       found.extra.good);
    root.setProperty("--bad",        found.extra.bad);
    root.setProperty("--ink",        found.extra.ink);
    root.setProperty("--ink-soft",   found.extra.inkSoft);
  }, [t.palette]);

  React.useEffect(() => {
    document.documentElement.style.setProperty(
      "--letter-font",
      `"${t.letterFont}", "Heebo", system-ui, sans-serif`
    );
  }, [t.letterFont]);

  React.useEffect(() => {
    const r = { squircle: "22%", circle: "50%", cushion: "34%", square: "12%" }[t.buttonShape] || "22%";
    document.documentElement.style.setProperty("--btn-radius", r);
  }, [t.buttonShape]);
}

// ── Accelerated, REPEATING idle-hint cycle (docs/game-baseline.md "Hint") ──
// Returns the current hint level (0..stepTimesMs.length) while `active`,
// escalating at each step time, then RE-RUNNING the whole escalation every
// `cycleMs` so a stuck kid keeps getting nudged. One shared timeline for
// every game; each game maps level → which elements fade / glitter.
//   few-choice : steps [5s,10s,15s]  → fade wrong1, fade wrong2, glitter
//   multi      : steps [5s,10s]       → fade all-but-correct, glitter
// `resetKey` (optional): when it changes the cycle restarts from 0 — pass a
// per-tap nonce in games that accumulate taps (find-letter) so the hint only
// fires after the kid actually pauses, not mid-scan.
function useHintCycle(active, stepTimesMs, cycleMs, resetKey) {
  const [level, setLevel] = React.useState(0);
  const key = stepTimesMs.join(",");
  React.useEffect(() => {
    if (!active) { setLevel(0); return; }
    let timers = [];
    const run = () => {
      setLevel(0);
      stepTimesMs.forEach((ms, i) => timers.push(window.setTimeout(() => setLevel(i + 1), ms)));
    };
    run();
    const iv = cycleMs ? window.setInterval(() => {
      timers.forEach(clearTimeout); timers = []; run();
    }, cycleMs) : null;
    return () => { timers.forEach(clearTimeout); if (iv) clearInterval(iv); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active, key, cycleMs, resetKey]);
  return level;
}

// ── The one idle-cue pattern, owned here (ADR 0028, game-baseline.md) ──────
// Timing (override via ?hint1/2/3 + ?hintCycle for tests):
//   t+5s / t+10s / t+15s beats, repeating every 20s.
const _hintQs = (typeof window !== "undefined")
  ? new URLSearchParams(window.location.search) : new URLSearchParams();
const _hintMs = (k, d) => { const v = _hintQs.get(k); return v == null || v === "" ? d : Math.max(0, +v) || d; };
const HINT_STEPS    = [_hintMs("hint1", 5000), _hintMs("hint2", 10000), _hintMs("hint3", 15000)];
const HINT_CYCLE_MS = _hintMs("hintCycle", 20000);

// Given the current hint level (0..3 from useHintCycle) and one option's
// role, return its cue: "wrong" (fade 2s) | "correct" (glitter) | null.
//   level 1 (t+5):  fade the first wrong only when there are 3 options;
//                   otherwise fade all wrong.
//   level 2 (t+10): fade ALL wrong.
//   level 3 (t+15): glitter the correct option(s). Correct is never faded.
function hintCueFor(level, { correct, firstWrong = false, fewOptions = false }) {
  if (correct) return level >= 3 ? "correct" : null;
  if (level >= 2) return "wrong";
  if (level >= 1 && (!fewOptions || firstWrong)) return "wrong";
  return null;
}

// ── Shared components ────────────────────────────────────────────────────
//
// v1.42: ThemeSwitcher removed from the in-game Header (themes are picked
//   from the landing tiles).
// v1.44: THEME_LABELS + the theme-badge in the crown removed — parent-only
//   noise the kid couldn't read; the landing tile they tapped already told
//   them which theme is active.

function MuteToggle() {
  const [muted, setMuted] = React.useState(() =>
    typeof window !== "undefined" && window.SOUNDS ? window.SOUNDS.isMuted() : false
  );
  React.useEffect(() => {
    if (!window.SOUNDS) return;
    return window.SOUNDS.onChange((v) => setMuted(v));
  }, []);
  if (typeof window === "undefined" || !window.SOUNDS) return null;
  const onClick = (e) => { e.preventDefault(); window.SOUNDS.toggleMuted(); };
  return (
    <button
      type="button"
      className="mute-btn"
      aria-label={muted ? "הפעלת קולות" : "השתקה"}
      aria-pressed={muted ? "true" : "false"}
      onClick={onClick}
    >
      {muted ? (
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M11 5L6 9H3v6h3l5 4z"/>
          <path d="M16 9l5 6m0-6l-5 6"/>
        </svg>
      ) : (
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M11 5L6 9H3v6h3l5 4z"/>
          <path d="M16 8c1.5 1.2 2.5 2.7 2.5 4s-1 2.8-2.5 4"/>
          <path d="M19 5c2.6 1.8 4 4.3 4 7s-1.4 5.2-4 7"/>
        </svg>
      )}
    </button>
  );
}

// ── The brand "home" crown — the single source of truth for the אותיות
// mark as a home link. Used by the Header AND the in-game pickers
// (theme/level), so the brand symbol is identical everywhere and lives in
// ONE place (R31 brand / R35 consolidation). The graphic itself is the
// single artifact assets/brand/mark.svg. (The config/requests *tools*
// don't load this module, so they reference the same mark.svg directly.)
function Crown() {
  return (
    <a className="crown" href="index.html" aria-label="חזרה לעמוד הבית">
      <img className="crown-mark" src="assets/brand/mark.svg" alt="" aria-hidden="true" />
    </a>
  );
}

function Header({ index, total, progressSlot }) {
  // A soft "start" chime on first game mount (once per page load). Where
  // the browser blocks audio before a gesture it's simply skipped; the
  // first tap unlocks and the in-game cues take over. Also resets the
  // correct-streak so each game/session starts its combo climb fresh.
  React.useEffect(() => {
    if (typeof window === "undefined" || window.__alefStartChimed) return;
    window.__alefStartChimed = true;
    if (window.SOUNDS) window.SOUNDS.play("start");
  }, []);
  return (
    <div className="header">
      <Crown />
      <div className="header-tools">
        <MuteToggle />
      </div>
      {/* A game may pass a custom `progressSlot` (e.g. letter-or-not's
          per-alphabet tracker) — it still lives inside `.progress` and
          renders `.progress-dot`s, so the shared-chrome contract (CI-2)
          holds. Default: the one-dot-per-unit row. */}
      <div className="progress" aria-label={`סיבוב ${index + 1} מתוך ${total}`}>
        {progressSlot != null ? progressSlot : Array.from({ length: total }).map((_, i) => (
          <span
            key={i}
            className="progress-dot"
            data-state={i < index ? "done" : i === index ? "current" : "todo"}
          />
        ))}
      </div>
    </div>
  );
}

function WordHint({ word, mode }) {
  if (mode === "none") return <div style={{ height: 8 }} />;
  if (mode === "full") return <div className="word-row" aria-hidden="true">{word}</div>;
  const tail = word.slice(1);
  return (
    <div className="word-row" aria-hidden="true">
      <span className="blank">_</span>
      <span>{tail}</span>
    </div>
  );
}

function PhotoCard({ round, shake }) {
  return (
    <div className="stage">
      <div className="photo-card" data-shake={shake ? "1" : "0"}>
        <div className="slot">
          <image-slot
            id={`slot-${round.slotId}`}
            shape="rounded"
            radius="18"
            placeholder={`גרור תמונה של ${round.word}`}
            src={round.image}
            alt={round.alt}
          />
        </div>
      </div>
    </div>
  );
}

function CorrectOverlay({ on }) {
  return (
    <div className="overlay" data-on={on ? "1" : "0"} aria-hidden={!on}>
      <div className="veil" />
      <div className="check-burst">
        {on && <div className="ring" />}
        <svg viewBox="0 0 200 200">
          <circle cx="100" cy="100" r="78" fill="rgba(255,255,255,.18)" />
          {on && (
            <path
              className="check-path"
              d="M62 104 L92 132 L142 74"
              fill="none"
              stroke="#fff"
              strokeWidth="14"
              strokeLinecap="round"
              strokeLinejoin="round"
              pathLength="100"
            />
          )}
        </svg>
      </div>
    </div>
  );
}

function Confetti() {
  const PIECES = 36;
  const COLORS = ["#f3b04a", "#18605a", "#d6452e", "#2f9d6e", "#7a3158", "#1d3a6e"];
  const pieces = React.useMemo(() => {
    return Array.from({ length: PIECES }).map((_, i) => ({
      left:  Math.random() * 100,
      delay: Math.random() * 2,
      dur:   2.4 + Math.random() * 2.4,
      color: COLORS[i % COLORS.length],
      tilt:  Math.random() * 360,
      shape: Math.random() < .3 ? "circle" : "rect",
    }));
  }, []);
  return (
    <div className="confetti" aria-hidden="true">
      {pieces.map((p, i) => (
        <i
          key={i}
          style={{
            left: `${p.left}%`,
            background: p.color,
            borderRadius: p.shape === "circle" ? "50%" : "2px",
            transform: `rotate(${p.tilt}deg)`,
            animationDuration: `${p.dur}s`,
            animationDelay: `${p.delay}s`,
          }}
        />
      ))}
    </div>
  );
}

// ── Icon buttons for break + end screens ────────────────────────────────
// Large, graphic-only, no text inside — the target audience is 4-5yo and
// can't read yet. Hebrew labels live on aria-label for accessibility /
// screen-reader; the SVG icon carries the meaning visually.
//
// IconPlay points LEFT — in RTL Hebrew "forward / next" is the leftward
// direction, the natural follow-on to the just-completed letter row that
// reads right-to-left. A right-pointing arrow reads as "backward" here.
function IconPlay()    { return <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18 4.5v15a1 1 0 0 1-1.55.83l-11-7.5a1 1 0 0 1 0-1.66l11-7.5A1 1 0 0 1 18 4.5z"/></svg>; }
function IconX()       { return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M6 6L18 18M18 6L6 18"/></svg>; }
function IconRestart() { return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66"/><path d="M20 4v5h-5"/></svg>; }
function IconHome()    { return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 11.5L12 4l9 7.5"/><path d="M5.5 10v10h13V10"/><path d="M10 20v-5h4v5"/></svg>; }

// v1.45: dropped the MasteryGrid component. Was a 22-letter star grid
// rendered on the EndScreen until v1.35 replaced it with the card
// gallery; sat dead in the file since. window.MASTERY itself still
// records progress (shared/mastery.js) for potential parent-facing
// surfaces later.

// v1.63: EndScreen always offers BOTH actions — small red home (stop +
// back to landing) and large green restart — matching the BreakScreen
// pattern. The kid had only "שוב" (restart) before, with no graphic
// path off the end screen back to the home tile picker.
function EndScreen({
  onRestart,
  onStop,
  title = "כל הכבוד! 🎉",
  restartLabel = "שוב",
  stopLabel   = "חזרה לעמוד הבית",
  playedRounds = [],
  playedFirstTries = [],
  children,   // optional custom gallery (docs/game-baseline.md summary divergence)
}) {
  const handleStop = onStop || (() => {
    if (typeof window !== "undefined") {
      // Home = the app root. `document.baseURI` already resolves the page's
      // <base href="../../"> to /aleph-bet/ on Pages (and / on localhost), so
      // navigate straight there. The old "../index.html" double-counted the
      // <base> and climbed one level PAST /aleph-bet/ to the domain root →
      // https://<user>.github.io/index.html → 404. That only reproduces on
      // the Pages sub-path (localhost clamps "../" at root, hiding it), which
      // is why it survived the v1.32 BreakScreen-only fix. One root fix here
      // covers all seven games' EndScreens. See docs/agentic-workflow-gaps.md §6.
      window.location.href = document.baseURI;
    }
  });
  // v1.35: dropped the mastery grid (felt like a report card). Now uses
  // the BreakScreen style — every card the kid went through, each with a
  // gold star (going through IS a success). First-try clean cards still
  // get the green ring border for a subtle distinction. Single big
  // restart button preserved.
  // v1.42: dropped the subtitle <p> — the kid can't read it, and the
  // sparkle + "כל הכבוד!" title + confetti + card gallery already carry
  // the celebration.
  return (
    <div className="break" role="dialog" aria-label="סיום המשחק">
      <Confetti />
      <div className="break-card">
        <div className="break-sparkle" aria-hidden="true">✨</div>
        <h1>{title}</h1>
        {/* Baseline chrome (confetti + title + buttons) is shared; the GALLERY
            is the sanctioned divergence point — a game may pass its own via
            children (e.g. letter-or-not's per-script summary). Default = the
            illustrated letter-card gallery. */}
        {children ? children : playedRounds.length > 0 && (
          <div className="break-gallery break-gallery--end">
            {playedRounds.map((r, i) => (
              <div
                key={r.letter}
                className="break-letter-card"
                data-first-try={playedFirstTries[i] ? "1" : "0"}
                style={{ animationDelay: `${Math.min(i, 12) * 60}ms` }}
              >
                <div className="break-img">
                  <img src={r.image} alt={r.alt} />
                </div>
                <div className="break-letter">{r.letter}</div>
                <span className="break-card-star" aria-hidden="true"><IconStarFilled /></span>
              </div>
            ))}
          </div>
        )}
        {/* RTL order: in the markup stop comes first, but with the parent
            `direction: rtl` flex-row this puts the small red home button
            on the right (the "back" / secondary direction) and the big
            green restart on the left (the RTL "forward" direction),
            mirroring the BreakScreen layout. */}
        <div className="end-actions end-actions--end">
          <button type="button" className="icon-btn icon-btn--bad icon-btn--sm" aria-label={stopLabel} onClick={handleStop}><IconHome /></button>
          <button type="button" className="icon-btn icon-btn--good icon-btn--lg" aria-label={restartLabel} onClick={onRestart}><IconRestart /></button>
        </div>
      </div>
    </div>
  );
}

function IconStarFilled() { return <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l3 6.5 7 1-5 4.8 1.4 7L12 18.4 5.6 21.8 7 14.8 2 10l7-1z"/></svg>; }
function IconStarOutline() { return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" aria-hidden="true"><path d="M12 2.5l3 6.5 7 1-5 4.8 1.4 7L12 18.4 5.6 21.8 7 14.8 2 10l7-1z"/></svg>; }

// ── Break screen: every 5 letters, celebrate + let kid choose to go on ──
// batchFirstTries: boolean[] aligned with batchRounds. true = solved
// without any hint help; false = needed at least one wrong tap or hint.
// Drawn as a gold-filled star (true) or a faint cream outline (false)
// in the top corner of each letter card, so the kid sees their score
// at a glance.
//
// v1.38: the green continue button is a countdown timer. After
// BREAK_TIMER_MS the game auto-continues — the kid doesn't have to
// tap if they're just enjoying the celebration. Tapping the green
// button fires continue immediately (skip the wait). Tapping the
// red X cancels the timer + goes home. Override via ?breakTimer=ms.
function getBreakTimerMs() {
  if (typeof window === "undefined") return 6000;
  const v = parseInt(new URLSearchParams(window.location.search).get("breakTimer"), 10);
  if (Number.isFinite(v) && v > 0) return v;
  return 6000;
}

function BreakScreen({ batchRounds, batchFirstTries = [], completed, total, onContinue, onStop }) {
  const timerMs = React.useMemo(() => getBreakTimerMs(), []);
  const [timerActive, setTimerActive] = React.useState(true);

  // Auto-continue when the countdown elapses, unless the kid taps
  // either button first (which flips timerActive false and clears
  // the timeout).
  React.useEffect(() => {
    if (!timerActive) return;
    const id = window.setTimeout(() => {
      setTimerActive(false);
      onContinue();
    }, timerMs);
    return () => window.clearTimeout(id);
  }, [timerActive, timerMs, onContinue]);

  const handleContinue = () => { setTimerActive(false); onContinue(); };
  const handleStop = () => { setTimerActive(false); onStop(); };
  // v1.42: dropped the "למדת X מתוך Y אותיות" <p> subtitle (the kid
  // can't read it — the card gallery + per-card stars already speak the
  // progress visually) and the 5-star .break-score row (redundant with
  // the per-card gold stars on each break-letter-card).
  return (
    <div className="break" role="dialog" aria-label="הפסקה">
      <Confetti />
      <div className="break-card">
        <div className="break-sparkle" aria-hidden="true">✨</div>
        <h1>כל הכבוד!</h1>
        <div className="break-gallery">
          {batchRounds.map((r, i) => (
            <div
              key={r.letter}
              className="break-letter-card"
              data-first-try={batchFirstTries[i] ? "1" : "0"}
              style={{ animationDelay: `${i * 90}ms` }}
            >
              <div className="break-img">
                <img src={r.image} alt={r.alt} />
              </div>
              <div className="break-letter">{r.letter}</div>
              {/* v1.35: every completed card gets a star — going through
                  is a success. First-try-clean still gets the green ring
                  border on the card itself for distinction. */}
              <span className="break-card-star" aria-hidden="true"><IconStarFilled /></span>
            </div>
          ))}
        </div>
        {/* Stop comes FIRST in DOM. The page is dir="rtl", so flex-row
            children render right-to-left → stop on the right, continue
            on the left (the RTL "forward" direction). Continue is the
            primary action: bigger, full-color green, with a draining
            countdown ring that auto-fires the continue at zero. Stop is
            the secondary: smaller, red, cancels the timer + goes home. */}
        <div className="end-actions end-actions--break">
          <button type="button" className="icon-btn icon-btn--bad icon-btn--sm" aria-label="מספיק לעכשיו" onClick={handleStop}><IconX /></button>
          <button
            type="button"
            className="icon-btn icon-btn--good icon-btn--lg icon-btn--timer"
            aria-label="עוד אותיות"
            onClick={handleContinue}
            style={{ "--timer-duration": `${timerMs}ms` }}
          >
            {timerActive && (
              <svg className="timer-ring" viewBox="0 0 100 100" aria-hidden="true">
                <circle cx="50" cy="50" r="46" pathLength="100" />
              </svg>
            )}
            <IconPlay />
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Shared TweaksPanel content (palette/font/shape) ──────────────────────
function SharedTweakControls({ t, setTweak, children }) {
  return (
    <>
      <TweakSection label="לוח צבעים" />
      <TweakColor
        label="ערכת צבעים"
        value={t.palette}
        options={Object.values(PALETTES).map((p) => p.colors)}
        onChange={(v) => setTweak("palette", v)}
      />

      <TweakSection label="טיפוגרפיה" />
      <TweakSelect
        label="גופן אותיות"
        value={t.letterFont}
        options={FONT_OPTIONS}
        onChange={(v) => setTweak("letterFont", v)}
      />

      {children}
    </>
  );
}

// ── Flying letter (baseline COMMIT — docs/game-baseline.md) ───────────────
// The chosen glyph peels off the touched control and flies to where the
// answer lands (slot, house door). `hue` tints the tile to the destination
// colour; `fontClass` lets a non-Hebrew script use its own font.
function FlyingLetter({ letter, fromRect, toRect, hue, fontClass = "" }) {
  if (!fromRect || !toRect) return null;
  const fromCx = fromRect.left + fromRect.width / 2;
  const fromCy = fromRect.top + fromRect.height / 2;
  const toCx   = toRect.left + toRect.width / 2;
  const toCy   = toRect.top + toRect.height / 2;
  const style = {
    left: fromRect.left,
    top: fromRect.top,
    width: fromRect.width,
    height: fromRect.height,
    fontSize: `${fromRect.width * 0.55}px`,
    "--dx": `${toCx - fromCx}px`,
    "--dy": `${toCy - fromCy}px`,
  };
  if (hue) style.background = hue;
  return (
    <div className={`flying-letter ${fontClass}`} style={style} aria-hidden="true">
      {letter}
    </div>
  );
}

// ── Canonical correct-burst + wrong-mark (docs/animation-language.md) ─────
// One implementation of the "right!" sparks and the "wrong" X for every
// game to consume, so the gesture can't drift. Both anchor to the nearest
// positioned ancestor (the tile / card / house) via inset:0.
const BURST_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];
function CorrectBurst({ on = true, hue }) {
  if (!on) return null;
  return (
    <span className="burst" aria-hidden="true"
          style={hue ? { "--burst-hue": hue } : undefined}>
      {BURST_ANGLES.map((a) => (
        <span key={a} className="spark" style={{ "--angle": `${a}deg` }} />
      ))}
    </span>
  );
}
function WrongMark({ on = true }) {
  if (!on) return null;
  return (
    <span className="wrong-mark" aria-hidden="true">
      <svg viewBox="0 0 60 60" width="40" height="40">
        <path d="M16 16 L44 44 M44 16 L16 44" stroke="#fff" strokeWidth="8"
              strokeLinecap="round" fill="none" />
      </svg>
    </span>
  );
}

// ── Expose to other Babel scripts ────────────────────────────────────────
Object.assign(window, {
  PALETTES, FONT_OPTIONS,
  shuffle, pickDistractors,
  useApplyTweaks, useHintCycle, hintCueFor, HINT_STEPS, HINT_CYCLE_MS,
  Header, Crown, WordHint, PhotoCard, CorrectOverlay, Confetti, EndScreen, BreakScreen,
  CorrectBurst, WrongMark, FlyingLetter,
  SharedTweakControls,
});
