// games.jsx — three phonics/reading games for the hub:
//   ReadFindGame  (Atrodi attēlu)  — decodable reading: read word → pick picture
//   FirstLetterGame (Pirmais burts) — phonemic awareness: pick the starting letter
//   BlendGame     (Skaņas)         — sound blending: hear sounds → pick the word
// Each runs a 6-word round over the child's unlocked words, then calls onDone(rating).
// Reuses window globals: WORDS, HUES, BLEND_WORDS, shuffle, pickWordDistractors,
// pickLetterDistractors, SparkleField, playWord, playSound, playSfx.
const { useState: useStateP, useEffect: useEffectP, useRef: useRefP } = React;
const SharedGameFrame = window.GameFrame;
const SharedWordPictureCard = window.WordPictureCard;

const ROUND_SIZE = 6;

// build a shuffled round of up to `size` words. Starts from `pool` (the words
// the child has unlocked) and, if that's shorter than `size`, pads with the
// easiest unused words from `fallback` (default: all WORDS, easy → hard order)
// so a round is always full even for a brand-new player with one unlocked word.
function buildRound(pool, size, fallback) {
  const fb = (fallback && fallback.length) ? fallback : Object.keys(WORDS);
  const base = (pool && pool.length) ? pool.slice() : fb.slice();
  for (const w of fb) {
    if (base.length >= size) break;
    if (!base.includes(w)) base.push(w);
  }
  const list = shuffle(base).slice(0, Math.min(size, base.length));
  return list.length ? list : [fb[0] || Object.keys(WORDS)[0]];
}

// build a round that deliberately MIXES short (≤2 syllable) and long (≥3
// syllable) words, drawn from the whole word set — so the child practises easy
// and harder words together instead of grinding the journey's all-easy-then-
// all-hard order. Takes ~half from each bucket, padding from the other if one
// runs short, then shuffles so the order is random but the mix is balanced.
function buildMixedRound(size) {
  const all = Object.keys(WORDS);
  const short = shuffle(all.filter(w => (WORDS[w].syll || []).length <= 2));
  const long = shuffle(all.filter(w => (WORDS[w].syll || []).length >= 3));
  const want = { long: Math.floor(size / 2), short: Math.ceil(size / 2) };
  const out = [...short.slice(0, want.short), ...long.slice(0, want.long)];
  // top up from whichever bucket has leftovers if one was too small
  const rest = shuffle([...short.slice(want.short), ...long.slice(want.long)]);
  for (const w of rest) { if (out.length >= size) break; out.push(w); }
  const list = shuffle(out).slice(0, Math.min(size, out.length));
  return list.length ? list : [all[0]];
}

// a single picture-choice tile
function PicTile({ wordKey, accent, wrong, dim, onPick }) {
  const data = WORDS[wordKey] || {};
  return (
    <div className="tile" onClick={onPick} style={{
      aspectRatio: '1 / 1', borderRadius: 28, background: 'var(--surface)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      boxShadow: `0 8px 0 ${accent[1]}, 0 12px 20px rgba(140,90,130,.16)`,
      animation: wrong ? 'shake .5s' : 'pop-in .35s',
      opacity: dim ? 0.4 : 1, transition: 'opacity .2s',
    }}>
      <span style={{ fontSize: 64, lineHeight: 1, filter: 'drop-shadow(0 3px 4px rgba(140,90,130,.22))' }}>{data.pic}</span>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// shared round controller — handles word progression + star tally
// ─────────────────────────────────────────────────────────────
// drive a fixed word list: tracks position + per-word stars, then reports the
// rounded average via onDone when the last word is answered.
function useRoundList(list, onDone) {
  const [idx, setIdx] = useStateP(0);
  const ratings = useRefP([]);
  const advance = (stars) => {
    ratings.current = [...ratings.current, stars];
    if (idx < list.length - 1) setIdx(idx + 1);
    else {
      const rs = ratings.current;
      onDone(Math.max(1, Math.round(rs.reduce((a, b) => a + b, 0) / rs.length)));
    }
  };
  return { word: list[idx], idx, total: list.length, advance };
}

// build a round from the unlocked-word pool once, then drive it.
function useRound(pool, onDone, fallback) {
  const round = useRefP(null);
  if (!round.current) round.current = buildRound(pool, ROUND_SIZE, fallback);
  return useRoundList(round.current, onDone);
}

// ─────────────────────────────────────────────────────────────
// ATRODI ATTĒLU — read the written word, then tap the matching picture.
// Picture options revealed only as choices; audio plays AFTER the pick.
// ─────────────────────────────────────────────────────────────
function ReadFindGame({ words, accent, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  const { word, idx, total, advance } = useRound(words, onDone);
  const { setSafeTimeout, clearTimers } = useTimeoutBag();
  const wordStartRef = useRefP(null);

  const opts = useRefP(null);
  if (!opts.current || opts.current._w !== word) {
    const o = shuffle([word, ...pickWordDistractors(word, 3)]);
    o._w = word;
    opts.current = o;
  }

  const [won, setWon] = useStateP(false);
  const [mistakes, setMistakes] = useStateP(0);
  const [wrongKey, setWrongKey] = useStateP(null);
  useEffectP(() => { clearTimers(); setWon(false); setMistakes(0); setWrongKey(null); wordStartRef.current = Date.now(); }, [word]);

  function pick(key) {
    if (won) return;
    if (key === word) {
      setWon(true);
      playSfx('win');
      playWord(word);
      const m = mistakes;
      const ms = wordStartRef.current ? Date.now() - wordStartRef.current : 0;
      if (onWordDone) onWordDone(m === 0);
      if (onWordRecord) onWordRecord({ word, game: 'readfind', stars: starsForMistakes(m), ms });
      setSafeTimeout(() => advance(starsForMistakes(m)), 1150);
    } else {
      setMistakes(m => m + 1);
      setWrongKey(key);
      setSafeTimeout(() => setWrongKey(null), 600);
    }
  }

  return (
    <SharedGameFrame onExit={onExit} index={idx} total={total} won={won} musicOn={musicOn} onToggleMusic={onToggleMusic} onShowCards={onShowCards} companion={companion}>
      <div style={{ textAlign: 'center', padding: '18px 28px 0' }}>
        <span className="display" style={{ fontSize: 18, fontWeight: 500, color: 'var(--ink)' }}>
          {won ? 'Lieliski! 🎉' : 'Izlasi vārdu un atrodi attēlu!'}
        </span>
      </div>

      {/* the written word — read first, no picture, no pre-audio */}
      <div style={{ display: 'flex', justifyContent: 'center', padding: '18px 22px 0' }}>
        <div style={{
          background: 'var(--surface)', borderRadius: 28, padding: '18px 34px',
          boxShadow: '0 12px 26px rgba(140,90,130,.16)',
        }}>
          <span className="display" style={{ fontSize: 48, fontWeight: 600, letterSpacing: 2, color: 'var(--primary)' }}>{word}</span>
        </div>
      </div>

      <div style={{ flex: 1 }} />

      {/* picture choices */}
      <div className="pic-grid">
        {opts.current.map((k, i) => (
          <PicTile key={i} wordKey={k} accent={accent}
            wrong={wrongKey === k} dim={won && k !== word} onPick={() => pick(k)} />
        ))}
      </div>
    </SharedGameFrame>
  );
}

// ─────────────────────────────────────────────────────────────
// PIRMAIS BURTS — picture shown (and spoken); pick the starting letter.
// ─────────────────────────────────────────────────────────────
function FirstLetterGame({ words, accent, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  const { word, idx, total, advance } = useRound(words, onDone);
  const data = WORDS[word] || {};
  const first = Array.from(word)[0];
  const { setSafeTimeout, clearTimers } = useTimeoutBag();
  const wordStartRef = useRefP(null);

  const opts = useRefP(null);
  if (!opts.current || opts.current._w !== word) {
    const o = shuffle([first, ...pickLetterDistractors(first, 2)]);
    o._w = word;
    opts.current = o;
  }

  const [won, setWon] = useStateP(false);
  const [mistakes, setMistakes] = useStateP(0);
  const [wrong, setWrong] = useStateP(null);
  useEffectP(() => { clearTimers(); setWon(false); setMistakes(0); setWrong(null); wordStartRef.current = Date.now(); playWord(word); }, [word]);

  function pick(letter) {
    if (won) return;
    if (letter === first) {
      setWon(true);
      playSfx('win');
      setSafeTimeout(() => playWord(word), 350);
      const m = mistakes;
      const ms = wordStartRef.current ? Date.now() - wordStartRef.current : 0;
      if (onWordDone) onWordDone(m === 0);
      if (onWordRecord) onWordRecord({ word, game: 'firstletter', stars: starsForMistakes(m), ms });
      setSafeTimeout(() => advance(starsForMistakes(m)), 3000);
    } else {
      setMistakes(m => m + 1);
      setWrong(letter);
      setSafeTimeout(() => setWrong(null), 600);
    }
  }

  return (
    <SharedGameFrame onExit={onExit} index={idx} total={total} won={won} musicOn={musicOn} onToggleMusic={onToggleMusic} onShowCards={onShowCards} companion={companion}>
      {/* picture card (tap to hear) */}
      <SharedWordPictureCard wordKey={word} data={data} accent={accent} won={won} paddingTop={18} />

      <div style={{ textAlign: 'center', padding: '16px 28px 0' }}>
        <span className="display" style={{ fontSize: 19, fontWeight: 500, color: 'var(--ink)' }}>
          {won ? 'Lieliski! 🎉' : 'Ar kuru burtu sākas vārds?'}
        </span>
      </div>

      <div style={{ flex: 1 }} />

      {/* letter choices */}
      <div style={{ display: 'flex', gap: 16, justifyContent: 'center', padding: '0 22px calc(40px + var(--safe-bottom, 0px))' }}>
        {opts.current.map((l, i) => (
          <div key={i} className="tile" onClick={() => pick(l)} style={{
            width: 'clamp(72px, 20vw, 100px)', aspectRatio: '1 / 1', borderRadius: 26, display: 'flex', alignItems: 'center', justifyContent: 'center',
            background: (won && l === first) ? accent[0] : 'var(--surface)',
            boxShadow: (won && l === first) ? `0 6px 0 ${accent[1]}` : '0 6px 0 rgba(150,110,150,.22), 0 9px 16px rgba(140,90,130,.14)',
            animation: wrong === l ? 'shake .5s' : 'none',
            opacity: (won && l !== first) ? 0.4 : 1, transition: 'opacity .2s, background .2s',
          }}>
            <span className="display" style={{ fontSize: 46, fontWeight: 600, color: (won && l === first) ? '#fff' : 'var(--primary)' }}>{l}</span>
          </div>
        ))}
      </div>
    </SharedGameFrame>
  );
}

// ─────────────────────────────────────────────────────────────
// SKAŅAS — hear each sound (tap to replay), then pick the matching picture.
// Uses the curated BLEND_WORDS subset so blending is clean.
// ─────────────────────────────────────────────────────────────
function BlendGame({ words, accent, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  // restrict the pool to blend-friendly words the child has unlocked
  const pool = useRefP(null);
  if (!pool.current) {
    const unlocked = (words || []).filter(w => BLEND_WORDS[w]);
    pool.current = unlocked.length ? unlocked : Object.keys(BLEND_WORDS);
  }
  const { word, idx, total, advance } = useRound(pool.current, onDone, Object.keys(BLEND_WORDS));
  const sounds = BLEND_WORDS[word] || Array.from(word);
  const { setSafeTimeout, clearTimers } = useTimeoutBag();
  const wordStartRef = useRefP(null);

  const opts = useRefP(null);
  if (!opts.current || opts.current._w !== word) {
    const o = shuffle([word, ...pickWordDistractors(word, 2)]);
    o._w = word;
    opts.current = o;
  }

  const [won, setWon] = useStateP(false);
  const [mistakes, setMistakes] = useStateP(0);
  const [wrongKey, setWrongKey] = useStateP(null);
  const [lit, setLit] = useStateP(-1); // which sound button is highlighted during playback

  // auto-play the sound sequence whenever the word changes
  useEffectP(() => {
    clearTimers();
    setWon(false); setMistakes(0); setWrongKey(null); setLit(-1);
    wordStartRef.current = Date.now();
    sounds.forEach((s, i) => {
      setSafeTimeout(() => { setLit(i); playSound(s); }, 350 + i * 700);
    });
    setSafeTimeout(() => setLit(-1), 350 + sounds.length * 700);
    return clearTimers;
  }, [word]);

  function pick(key) {
    if (won) return;
    if (key === word) {
      setWon(true);
      playSfx('win');
      playWord(word);
      const m = mistakes;
      const ms = wordStartRef.current ? Date.now() - wordStartRef.current : 0;
      if (onWordDone) onWordDone(m === 0);
      if (onWordRecord) onWordRecord({ word, game: 'blend', stars: starsForMistakes(m), ms });
      setSafeTimeout(() => advance(starsForMistakes(m)), 1150);
    } else {
      setMistakes(m => m + 1);
      setWrongKey(key);
      setSafeTimeout(() => setWrongKey(null), 600);
    }
  }

  function replayAll() {
    if (won) return;
    sounds.forEach((s, i) => setSafeTimeout(() => { setLit(i); playSound(s); }, i * 700));
    setSafeTimeout(() => setLit(-1), sounds.length * 700);
  }

  return (
    <SharedGameFrame onExit={onExit} index={idx} total={total} won={won} musicOn={musicOn} onToggleMusic={onToggleMusic} onShowCards={onShowCards} companion={companion}>
      <div style={{ textAlign: 'center', padding: '18px 28px 0' }}>
        <span className="display" style={{ fontSize: 19, fontWeight: 500, color: 'var(--ink)' }}>
          {won ? 'Lieliski! 🎉' : 'Klausies skaņas — kāds vārds sanāk?'}
        </span>
      </div>

      {/* sound buttons (tap any to replay that sound) */}
      <div style={{ display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap', padding: '22px 22px 0' }}>
        {sounds.map((s, i) => (
          <div key={i} className="tile" onClick={() => { setLit(i); playSound(s); setSafeTimeout(() => setLit(-1), 450); }} style={{
            minWidth: 60, height: 72, padding: '0 6px', borderRadius: 20, display: 'flex', alignItems: 'center', justifyContent: 'center',
            background: lit === i ? accent[0] : 'var(--surface)',
            boxShadow: lit === i ? `0 6px 0 ${accent[1]}` : '0 6px 0 rgba(150,110,150,.22), 0 9px 16px rgba(140,90,130,.14)',
            transform: lit === i ? 'translateY(-3px)' : 'none', transition: 'all .15s',
          }}>
            <span className="display" style={{ fontSize: 34, fontWeight: 600, color: lit === i ? '#fff' : 'var(--primary)' }}>{s}</span>
          </div>
        ))}
      </div>

      {/* replay-all button */}
      <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 16 }}>
        <button onClick={replayAll} className="kid-btn ghost" style={{ padding: '10px 22px', fontSize: 17, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
          🔊 Vēlreiz
        </button>
      </div>

      <div style={{ flex: 1 }} />

      {/* picture choices */}
      <div style={{
        display: 'flex', gap: 14, justifyContent: 'center',
        width: '100%', maxWidth: 480, margin: '0 auto',
        padding: '0 22px calc(38px + var(--safe-bottom, 0px))',
      }}>
        {opts.current.map((k, i) => (
          <div key={i} style={{ flex: '1 1 0', maxWidth: 130 }}>
            <PicTile wordKey={k} accent={accent} wrong={wrongKey === k} dim={won && k !== word} onPick={() => pick(k)} />
          </div>
        ))}
      </div>
    </SharedGameFrame>
  );
}

// ─────────────────────────────────────────────────────────────
// KLAUSIES! — hear the spoken word (recorded clip), pick the matching
// picture. The reverse of Atrodi attēlu: trains listening → meaning.
// Uses only assets already present (audio/<slug>.mp3 + emoji pictures).
// ─────────────────────────────────────────────────────────────
function ListenFindGame({ words, accent, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  const { word, idx, total, advance } = useRound(words, onDone);
  const { setSafeTimeout, clearTimers } = useTimeoutBag();
  const wordStartRef = useRefP(null);

  const opts = useRefP(null);
  if (!opts.current || opts.current._w !== word) {
    const o = shuffle([word, ...pickWordDistractors(word, 3)]);
    o._w = word;
    opts.current = o;
  }

  const [won, setWon] = useStateP(false);
  const [mistakes, setMistakes] = useStateP(0);
  const [wrongKey, setWrongKey] = useStateP(null);
  const [speaking, setSpeaking] = useStateP(false);

  const say = () => {
    playWord(word);
    setSpeaking(true);
    setSafeTimeout(() => setSpeaking(false), 900);
  };

  // auto-speak each new word (the child is mid-session, so audio is unlocked)
  useEffectP(() => {
    clearTimers(); setWon(false); setMistakes(0); setWrongKey(null);
    wordStartRef.current = Date.now();
    setSafeTimeout(say, 450);
  }, [word]);

  function pick(key) {
    if (won) return;
    if (key === word) {
      setWon(true);
      playSfx('win');
      playWord(word);
      const m = mistakes;
      const ms = wordStartRef.current ? Date.now() - wordStartRef.current : 0;
      if (onWordDone) onWordDone(m === 0);
      if (onWordRecord) onWordRecord({ word, game: 'listen', stars: starsForMistakes(m), ms });
      setSafeTimeout(() => advance(starsForMistakes(m)), 1150);
    } else {
      setMistakes(m => m + 1);
      setWrongKey(key);
      setSafeTimeout(() => setWrongKey(null), 600);
    }
  }

  return (
    <SharedGameFrame onExit={onExit} index={idx} total={total} won={won} musicOn={musicOn} onToggleMusic={onToggleMusic} onShowCards={onShowCards} companion={companion}>
      <div style={{ textAlign: 'center', padding: '18px 28px 0' }}>
        <span className="display" style={{ fontSize: 19, fontWeight: 500, color: 'var(--ink)' }}>
          {won ? 'Lieliski! 🎉' : 'Klausies vārdu un atrodi attēlu!'}
        </span>
      </div>

      {/* big speaker card — tap to hear the word again */}
      <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 18 }}>
        <div onClick={say} className="tile" style={{
          '--spk': 'min(140px, calc(var(--app-h, 100dvh) * 0.2))',
          width: 'var(--spk)', height: 'var(--spk)', borderRadius: 36,
          background: speaking ? accent[0] : 'var(--surface)',
          boxShadow: speaking ? `0 8px 0 ${accent[1]}` : '0 8px 0 rgba(150,110,150,.22), 0 14px 30px rgba(140,90,130,.18)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          animation: won ? 'bob .6s ease-in-out infinite' : 'floaty-slow 4s ease-in-out infinite',
          transition: 'background .2s, box-shadow .2s',
        }}>
          <span style={{ fontSize: 'calc(var(--spk) * 0.5)', lineHeight: 1, filter: 'drop-shadow(0 4px 6px rgba(140,90,130,.25))' }}>🔊</span>
        </div>
      </div>

      <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 14 }}>
        <button onClick={say} className="kid-btn ghost" style={{ padding: '10px 22px', fontSize: 17, fontWeight: 600 }}>
          🔊 Vēlreiz
        </button>
      </div>

      <div style={{ flex: 1 }} />

      {/* picture choices */}
      <div className="pic-grid">
        {opts.current.map((k, i) => (
          <PicTile key={i} wordKey={k} accent={accent}
            wrong={wrongKey === k} dim={won && k !== word} onPick={() => pick(k)} />
        ))}
      </div>
    </SharedGameFrame>
  );
}

// ─────────────────────────────────────────────────────────────
// ATRODI PĀRI — classic memory pairs over the picture emoji: 6 words,
// 12 face-down cards. Matching a pair flips it for good and speaks the
// word (recorded clip), so vocabulary sneaks into the fun. Stars per pair
// drop with the mismatches it took to find it.
// ─────────────────────────────────────────────────────────────
const PAIRS_COUNT = 6;
function PairsGame({ words, accent, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  const { setSafeTimeout } = useTimeoutBag();
  const deck = useRefP(null);
  if (!deck.current) {
    const pool = buildRound(words, PAIRS_COUNT);
    deck.current = shuffle(pool.flatMap(w => [{ w, id: w + '-a' }, { w, id: w + '-b' }]));
  }
  const cards = deck.current;

  const [flipped, setFlipped] = useStateP([]);       // indices face-up this turn (max 2)
  const [matched, setMatched] = useStateP([]);       // word keys already paired
  const [won, setWon] = useStateP(false);
  const [wrongPair, setWrongPair] = useStateP(false);
  const missSince = useRefP(0);                      // mismatches since the last match
  const ratings = useRefP([]);
  const pairStart = useRefP(Date.now());

  function flip(i) {
    if (won || wrongPair) return;
    if (flipped.length === 2 || flipped.includes(i) || matched.includes(cards[i].w)) return;
    const next = [...flipped, i];
    setFlipped(next);
    if (next.length < 2) return;
    const [a, b] = next.map(x => cards[x].w);
    if (a === b) {
      playSfx('win', 0.45);
      playWord(a);
      const m = missSince.current;
      missSince.current = 0;
      const stars = m === 0 ? 3 : m <= 2 ? 2 : 1;
      const ms = Date.now() - pairStart.current;
      pairStart.current = Date.now();
      ratings.current = [...ratings.current, stars];
      if (onWordDone) onWordDone(m === 0);
      if (onWordRecord) onWordRecord({ word: a, game: 'pairs', stars, ms });
      setSafeTimeout(() => {
        setMatched(mw => {
          const done = [...mw, a];
          if (done.length === PAIRS_COUNT) {
            setWon(true);
            const rs = ratings.current;
            setSafeTimeout(() => onDone(Math.max(1, Math.round(rs.reduce((x, y) => x + y, 0) / rs.length))), 1300);
          }
          return done;
        });
        setFlipped([]);
      }, 700);
    } else {
      missSince.current += 1;
      setWrongPair(true);
      setSafeTimeout(() => { setFlipped([]); setWrongPair(false); }, 900);
    }
  }

  return (
    <SharedGameFrame onExit={onExit} index={matched.length} total={PAIRS_COUNT} won={won} musicOn={musicOn} onToggleMusic={onToggleMusic} onShowCards={onShowCards} companion={companion}>
      <div style={{ textAlign: 'center', padding: '16px 28px 0' }}>
        <span className="display" style={{ fontSize: 19, fontWeight: 500, color: 'var(--ink)' }}>
          {won ? 'Visi pāri atrasti! 🎉' : 'Atver kartītes un atrodi pārus!'}
        </span>
      </div>

      <div style={{
        flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: '12px 20px calc(20px + var(--safe-bottom, 0px))',
      }}>
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10,
          width: '100%', maxWidth: 'min(400px, calc((var(--app-h, 100dvh) - 230px) * 0.72))',
        }}>
          {cards.map((c, i) => {
            const isMatched = matched.includes(c.w);
            const isUp = isMatched || flipped.includes(i);
            const isWrong = wrongPair && flipped.includes(i);
            return (
              <div key={c.id} className="tile" onClick={() => flip(i)} style={{
                aspectRatio: '1 / 1', borderRadius: 18,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                background: isUp ? 'var(--surface)' : accent[0],
                boxShadow: isMatched
                  ? `0 4px 0 ${GOLD_DARK}, 0 0 0 3px ${GOLD}`
                  : isUp
                    ? '0 5px 0 rgba(150,110,150,.22), 0 8px 14px rgba(140,90,130,.14)'
                    : `0 5px 0 ${accent[1]}, 0 8px 14px rgba(140,90,130,.16)`,
                animation: isWrong ? 'shake .5s' : (isUp ? 'pop-in .3s' : 'none'),
                opacity: isMatched ? 0.85 : 1,
                cursor: isUp ? 'default' : 'pointer', transition: 'background .15s, opacity .3s',
              }}>
                {isUp
                  ? <span style={{ fontSize: 'min(44px, 9vw)', lineHeight: 1, filter: 'drop-shadow(0 2px 3px rgba(140,90,130,.22))' }}>{(WORDS[c.w] || {}).pic}</span>
                  : <span className="display" style={{ fontSize: 'min(30px, 7vw)', fontWeight: 700, color: '#fff', opacity: .9 }}>?</span>}
              </div>
            );
          })}
        </div>
      </div>
    </SharedGameFrame>
  );
}

// ─────────────────────────────────────────────────────────────
// JAUKTI VĀRDI — the journey's build-from-syllables game, but over a
// shuffled round that mixes short and long words (buildMixedRound). Wraps the
// shared SyllableGame; earns stars like the other hub games (onDone) without
// touching journey progress.
// ─────────────────────────────────────────────────────────────
function MixedWordsGame({ mode, onDone, onExit, onWordDone, onWordRecord, musicOn, onToggleMusic, onShowCards, companion }) {
  const SharedSyllableGame = window.SyllableGame;
  const round = useRefP(null);
  if (!round.current) round.current = buildMixedRound(ROUND_SIZE);
  const { word, idx, total, advance } = useRoundList(round.current, onDone);
  const hueVals = Object.values(HUES);
  const accent = hueVals[idx % hueVals.length];

  return (
    <SharedSyllableGame
      key={word + '-' + idx}
      wordKey={word} mode={mode} accent={accent}
      progress={{ index: idx, total }}
      onWin={advance} onWordDone={onWordDone} onWordRecord={onWordRecord} gameType="mixed"
      onExit={onExit} onShowCards={onShowCards} companion={companion}
      musicOn={musicOn} onToggleMusic={onToggleMusic} />
  );
}

Object.assign(window, { ReadFindGame, FirstLetterGame, BlendGame, MixedWordsGame, ListenFindGame, PairsGame });
