// Integrated Seamenese Learning Environment + Silly Dopamine Mode & Cosmetic Shop
const {
  useState: useStateL,
  useEffect: useEffectL,
  useMemo: useMemoL,
  useCallback: useCallbackL,
  useRef: useRefL,
} = React;

// ============================================================================
// 1. UNBIASED FISHER-YATES SHUFFLE (Global + Stable Option IDs)
// ============================================================================
function fisherYatesShuffle(arr) {
  const copy = Array.isArray(arr) ? [...arr] : [];
  for (let i = copy.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const tmp = copy[i];
    copy[i] = copy[j];
    copy[j] = tmp;
  }
  return copy;
}
window.fisherYatesShuffle = fisherYatesShuffle;

const OPT_LETTERS = ['A', 'B', 'C', 'D'];

function createShuffledMcOptions(rawOptions) {
  const shuffled = window.fisherYatesShuffle(rawOptions);
  return shuffled.map((opt, idx) => ({
    ...opt,
    optId: `opt-${idx}`,
    letter: OPT_LETTERS[idx] || String(idx + 1),
  }));
}

// ============================================================================
// 2. SILLY DOPAMINE MODE & COSMETIC SHOP ENGINE (seamenese_dopamine_v2)
// ============================================================================
const STORAGE_KEY_DOPAMINE = 'seamenese_dopamine_v2';
const STORAGE_KEY_DOPAMINE_LEGACY = 'seamenese_dopamine_v1';
const STORAGE_KEY_SRS_V2 = 'seamenese_srs_v2';
const STORAGE_KEY_SCENES_V3 = 'seamenese_solved_scenes_v3';
const STORAGE_KEY_UNLOCKED_GEN = 'seamenese_unlocked_gen_scenes_v1';
const STORAGE_KEY_TIER = 'seamenese_selected_tier_v1';
const STORAGE_KEY_PERSISTED_SCENES = 'seamenese_persisted_scenes_v1';
const STORAGE_KEY_SCENES_REV = 'seamenese_scenes_rev_v3';
const STORAGE_KEY_AUTO_ROTATE = 'seamenese_auto_rotate_v1';
const STORAGE_KEY_DAILY_GEN = 'seamenese_daily_gen_v1';
const NON_ADMIN_DAILY_GEN_LIMIT = 2;

function getTodayDateKey() {
  return new Date().toISOString().slice(0, 10);
}

const DIFFICULTY_TIERS = [
  {
    id: 1,
    icon: '🌱',
    short: '🌱 Tier 1 · Beginner',
    label: 'Tier 1: Tadpole (Beginner)',
    basePts: 20,
    desc: 'Foundational greetings, singular pronouns (Ëi, Te, La), copula (xa vs xaai), and everyday nouns & colors.',
  },
  {
    id: 2,
    icon: '🐟',
    short: '🐟 Tier 2 · Intermediate',
    label: 'Tier 2: Blobber (Intermediate)',
    basePts: 30,
    desc: 'Plural -on rule (sobon, buanon), possessives (Ta, Tëi, Nai), negation (Ni), amplifier (Wo), and plural verb endings (-u, -ami, -ai).',
  },
  {
    id: 3,
    icon: '👑',
    short: '👑 Tier 3 · Graduate',
    label: 'Tier 3: Grandmaster (Graduate)',
    basePts: 45,
    desc: 'Multi-verb chaining (only 1st verb conjugates!), vowel-preserving stems (Sanai → sanaami), noun derivations (kiniwako), and corpus traps.',
  },
  {
    id: 'all',
    icon: '🌊',
    short: '🌊 All Tiers (1→3)',
    label: 'All Tiers (Progressive 1→3)',
    basePts: 25,
    desc: 'All difficulty pools combined and ordered progressively from Tier 1 → Tier 2 → Tier 3.',
  },
];

function getTierMeta(tierNum) {
  const n = Number(tierNum);
  if (n === 3) return DIFFICULTY_TIERS[2];
  if (n === 2) return DIFFICULTY_TIERS[1];
  return DIFFICULTY_TIERS[0];
}

const SHOP_RARITIES = {
  common:       { id: 'common',       label: 'Common',       icon: '⚪', color: '#94a3b8' },
  rare:         { id: 'rare',         label: 'Rare',         icon: '🔷', color: '#38bdf8' },
  epic:         { id: 'epic',         label: 'Epic',         icon: '🔮', color: '#c084fc' },
  legendary:    { id: 'legendary',    label: 'Legendary',    icon: '🌟', color: '#fbbf24' },
  mythic:       { id: 'mythic',       label: 'Mythic',       icon: '🔥', color: '#f43f5e' },
  transcendent: { id: 'transcendent', label: 'Transcendent', icon: '🪐', color: '#22d3ee' },
};

function getShopRarityMeta(rarity) {
  return SHOP_RARITIES[rarity] || SHOP_RARITIES.common;
}

const DOPAMINE_SHOP_ITEMS = {
  themes: [
    { id: 'theme_default',      name: 'Default Archival Ink',             cost: 0,     rarity: 'common',       previewColors: ['#07090f', '#111a2c', '#38bdf8', '#e8e4d6'], colors: ['#07090f', '#111a2c', '#38bdf8', '#e8e4d6'], desc: 'Classic editorial midnight slate & warm parchment.' },
    { id: 'theme_lagoon',       name: '🌊 Deep Lagoon Neon',              cost: 100,   rarity: 'common',       previewColors: ['#021619', '#093840', '#00f5d4', '#fbbf24'], colors: ['#021619', '#093840', '#00f5d4', '#fbbf24'], desc: 'Lush tropical turquoise-teal ocean & electric seafoam mint.' },
    { id: 'theme_solar',        name: '☀️ Golden Pan Solar',              cost: 150,   rarity: 'rare',         previewColors: ['#1a0b05', '#3d1b0d', '#ff7b00', '#fff7ed'], colors: ['#1a0b05', '#3d1b0d', '#ff7b00', '#fff7ed'], desc: 'Warm desert terracotta, blazing tangerine sun & solar ember.' },
    { id: 'theme_matrix',       name: '🥗 Liron Blubur Matrix',           cost: 200,   rarity: 'rare',         previewColors: ['#010d04', '#062910', '#39ff14', '#dcfce7'], colors: ['#010d04', '#062910', '#39ff14', '#dcfce7'], desc: 'Phosphor CRT hacker terminal with radioactive neon-lime glow.' },
    { id: 'theme_romance',      name: '💘 Skantar Romance',               cost: 250,   rarity: 'rare',         previewColors: ['#1c0718', '#421339', '#ff6ec7', '#c084fc'], colors: ['#1c0718', '#421339', '#ff6ec7', '#c084fc'], desc: 'Velvet plum wine, sakura rose pink & soft lilac.' },
    { id: 'theme_overdrive',    name: '🌈 Maximum Overdrive',             cost: 400,   rarity: 'epic',         previewColors: ['#120428', '#2e1060', '#ff007f', '#facc15'], colors: ['#120428', '#2e1060', '#ff007f', '#facc15'], desc: '80s outrun synthwave grid with laser hot-pink & cyber yellow.' },
    { id: 'theme_gold',         name: '👑 Imperial Gold & Obsidian',      cost: 850,   rarity: 'epic',         previewColors: ['#080808', '#1c1912', '#eab308', '#fefce8'], colors: ['#080808', '#1c1912', '#eab308', '#fefce8'], desc: 'Pure carbon-black obsidian & burnished 24K champagne gold.' },
    { id: 'theme_abyssal',      name: '🌌 Abyssal Bioluminescence',       cost: 1500,  rarity: 'legendary',    previewColors: ['#04031c', '#121054', '#818cf8', '#2dd4bf'], colors: ['#04031c', '#121054', '#818cf8', '#2dd4bf'], desc: 'Deep ultraviolet jellyfish trench with electric periwinkle & spring aqua.' },
    { id: 'theme_crimson',      name: '🩸 Blood Moon Eclipse',            cost: 2500,  rarity: 'legendary',    previewColors: ['#140202', '#380709', '#ef4444', '#f97316'], colors: ['#140202', '#380709', '#ef4444', '#f97316'], desc: 'Gothic vampire wine, arterial scarlet & molten eclipse corona.' },
    { id: 'theme_celestial',    name: '💎 Celestial Diamond Archive',     cost: 5000,  rarity: 'mythic',       previewColors: ['#eef4fc', '#ffffff', '#0284c7', '#7c3aed'], colors: ['#eef4fc', '#ffffff', '#0284c7', '#7c3aed'], desc: 'Luminous daytime alabaster, frosted diamond pearl & royal cobalt ink!' },
    { id: 'theme_transcendent', name: '✨ Void Singularity (Chromatic)',  cost: 10000, rarity: 'transcendent', previewColors: ['#020106', '#120826', '#e879f9', '#22d3ee'], colors: ['#020106', '#120826', '#e879f9', '#22d3ee'], desc: 'Endgame pitch-black singularity with shifting prismatic aurora borders.' },
  ],
  titles: [
    { id: 'title_blober',      name: '🐟 Tiny Blober',                  cost: 0,     rarity: 'common',       desc: 'Just a humble little fish in the lagoon.' },
    { id: 'title_buana',       name: '🤝 Certified Buana',              cost: 75,    rarity: 'common',       desc: 'A trusted friend of the Seamenese channel.' },
    { id: 'title_ezuki',       name: '🗣️ Ezuki Vocalizer',              cost: 150,   rarity: 'rare',         desc: 'Master of "Obisëi ezuki" and multi-verb chains.' },
    { id: 'title_skantar',     name: '💘 Supreme Skantar',              cost: 300,   rarity: 'rare',         desc: 'Romantic legend of Corpus #37 and #39.' },
    { id: 'title_grandmaster', name: '👑 Grandmaster Jai-Semen',        cost: 500,   rarity: 'epic',         desc: 'Supreme authority on all 20 verb tables & -on plurals.' },
    { id: 'title_sage',        name: '🧙‍♂️ Seamenese Sage',               cost: 750,   rarity: 'epic',         desc: 'Keeper of the sacred diaeresis Ë and shibilant X.' },
    { id: 'title_megalodon',   name: '🦈 Apex Megalodon',               cost: 1250,  rarity: 'epic',         desc: 'Top predator of the Seamenese Dojo leaderboards.' },
    { id: 'title_archon',      name: '⚡ Archon of the 45 Corpus',      cost: 2000,  rarity: 'legendary',    desc: 'Flawless scholar of every canonical sentence in SeameneseDictionary.txt.' },
    { id: 'title_oracle',      name: '🏛️ Living Lexicon Oracle',        cost: 3500,  rarity: 'legendary',    desc: 'Omniscient translator who speaks fluent post-midnight Seamenese.' },
    { id: 'title_leviathan',   name: '🐉 Primordial Trench Leviathan',  cost: 6000,  rarity: 'mythic',       desc: 'Ancient mythic titan of the deep Bonk ocean.' },
    { id: 'title_emperor',     name: '🌌 God-Emperor of Seamenese',     cost: 10000, rarity: 'transcendent', desc: 'Ultimate transcendent ruler of the Seamenese & Bonk Lexicon.' },
  ],
  nameplates: [
    { id: 'np_default',      name: 'Standard Nameplate',           cost: 0,    rarity: 'common',       cssClass: 'np-style-default',      desc: 'Clean archival typography with no aura.' },
    { id: 'np_cyan_glow',    name: '🌊 Neon Trench Glow',          cost: 350,  rarity: 'rare',         cssClass: 'np-style-cyan-glow',    desc: 'Bioluminescent aqua sonar badge with pulsing cyan neon border.' },
    { id: 'np_gold_shimmer', name: '✨ Midas Gold Shimmer',        cost: 900,  rarity: 'epic',         cssClass: 'np-style-gold-shimmer', desc: 'Embossed 24K gold foil plaque with an animated sweeping specular sheen.' },
    { id: 'np_hellfire',     name: '🔥 Abyssal Hellfire Pulse',    cost: 1800, rarity: 'legendary',    cssClass: 'np-style-hellfire',     desc: 'Roaring volcanic magma badge flanked by flickering 🔥 flames & molten heat glow.' },
    { id: 'np_prismatic',    name: '🌈 Prismatic Mythic Hologram', cost: 4000, rarity: 'mythic',       cssClass: 'np-style-prismatic',    desc: 'Full-spectrum animated cyber-prism foil with chromatic neon text & 💎 shard ornaments.' },
    { id: 'np_celestial',    name: '🪐 Sovereign Starlight Crown', cost: 8500, rarity: 'transcendent', cssClass: 'np-style-celestial',    desc: 'Over-the-top endgame regalia: levitating 👑✦ starlight crown, cosmic nebula & triple-ring golden halo!' },
  ],
  frames: [
    {
      id: 'frame_default',
      name: 'Standard Archive Row',
      cost: 0,
      rarity: 'common',
      cssClass: 'row-frame-default',
      shaderMode: 0,
      decorIcons: [],
      desc: 'Clean editorial leaderboard row with standard borders.',
    },
    {
      id: 'frame_kelp',
      name: '🌿 Bioluminescent Reef Trim',
      cost: 5000,
      rarity: 'legendary',
      cssClass: 'row-frame-kelp',
      shaderMode: 1,
      decorIcons: ['🫧', '🌊', '🐟'],
      desc: 'Reactive WebGL water caustics & bioluminescent plankton shader! Ripples and parts around your cursor or touch.',
    },
    {
      id: 'frame_runes',
      name: '🏛️ Gilded Lexicon Filigree',
      cost: 8500,
      rarity: 'legendary',
      cssClass: 'row-frame-runes',
      shaderMode: 2,
      decorIcons: ['✦', '📜', '⚜️'],
      desc: 'Reactive 24K liquid-gold foil & royal filigree shader! Tilts its metallic specular sheen toward your cursor or finger.',
    },
    {
      id: 'frame_synth',
      name: '🌆 Outrun Cyber-Grid Frame',
      cost: 13000,
      rarity: 'mythic',
      cssClass: 'row-frame-synth',
      shaderMode: 3,
      decorIcons: ['⚡', '🌆', '💠'],
      desc: 'Reactive 3D ray-projected synthwave cyber-grid shader! Bends the neon grid like a gravity well under your touch & fires EMP waves on tap.',
    },
    {
      id: 'frame_inferno',
      name: '🌋 Molten Caldera Border',
      cost: 19000,
      rarity: 'mythic',
      cssClass: 'row-frame-inferno',
      shaderMode: 4,
      decorIcons: ['🔥', '🌋', '☄️'],
      desc: 'Reactive FBM volcanic magma shader! Your cursor or finger superheats and cracks the obsidian crust into roaring white-hot lava.',
    },
    {
      id: 'frame_aurora',
      name: '💎 Prismatic Leviathan Frame',
      cost: 27500,
      rarity: 'transcendent',
      cssClass: 'row-frame-aurora',
      shaderMode: 5,
      decorIcons: ['💎', '🌈', '✨'],
      desc: 'Reactive holographic aurora borealis & crystal prism shader! Refracts into full-spectrum RGB waves that bend around your pointer.',
    },
    {
      id: 'frame_singularity',
      name: '🌌 Cosmic Event Horizon Frame',
      cost: 40000,
      rarity: 'transcendent',
      cssClass: 'row-frame-singularity',
      shaderMode: 6,
      decorIcons: ['🪐', '☄️', '🌟', '🌌'],
      desc: 'Ultimate reactive Black Hole & gravitational-lensing shader! Warps nearby stars & swirls relativistic accretion plasma around your touch!',
    },
  ],
  effects: [
    { id: 'effect_sparkles', name: '✨ Classic Sparkles',                cost: 0,    rarity: 'common',    emojis: ['✨', '🎉', '⭐', '💎', '🔥', '💫'], desc: 'Crisp gold & diamond celebration burst.' },
    { id: 'effect_blobbers', name: '🐟 Flying Blobbers (Fish Rain)',     cost: 120,  rarity: 'common',    emojis: ['🐟', '🐠', '🐡', '🌊', '🫧', '🎣'], desc: 'Unleash a school of flying blobberon!' },
    { id: 'effect_spicy',    name: '🌶️ Spicy Corpus Chaos (🍆🍑🔥)',    cost: 200,  rarity: 'rare',      emojis: ['🍆', '🍑', '🔥', '💋', '🌶️', '🥵'], desc: 'Pure unhinged Corpus #33 & #42 energy.' },
    { id: 'effect_cosmic',   name: '🌞 Cosmic Pan & Wubu (☀️🌝🌌)',     cost: 250,  rarity: 'rare',      emojis: ['☀️', '🌝', '🌌', '✨', '🪐', '⚡'], desc: 'Celestial sun, moon, and sky explosion.' },
    { id: 'fx_gold',         name: '💰 Golden Doubloon Shower',          cost: 600,  rarity: 'rare',      emojis: ['🪙', '💰', '💎', '👑', '🏆', '✨'], desc: 'Rains glittering pirate doubloons, trophies & treasure.' },
    { id: 'fx_royal',        name: '👑 Royal Crown & Gem Storm',         cost: 1200, rarity: 'epic',      emojis: ['👑', '💎', '💍', '⚜️', '🌟', '🥂'], desc: 'Imperial crowns, sapphires & champagne celebration.' },
    { id: 'fx_dragon',       name: '🐉 Mythic Dragon & Lightning',       cost: 2500, rarity: 'legendary', emojis: ['🐉', '⚡', '🔥', '🌋', '☄️', '🗡️'], desc: 'Draconic hellfire, thunderbolts & meteor strikes.' },
    { id: 'fx_supernova',    name: '🌌 Cosmic Supernova Singularity',    cost: 5500, rarity: 'mythic',    emojis: ['🌌', '🪐', '☄️', '🌟', '✨', '🔮', '💫', '☀️'], particleCount: 42, dualWave: true, desc: 'Massive dual-wave 42-particle cosmic singularity explosion!' },
  ],
  sounds: [
    { id: 'snd_default', name: '🎹 Classic Major Arpeggio',   cost: 0,    rarity: 'common',    desc: 'Crisp triangle-wave C-major chord arpeggio (C5–E5–G5–C6).' },
    { id: 'snd_arcade',  name: '👾 8-Bit Chiptune Victory',   cost: 300,  rarity: 'rare',      desc: 'Rapid square-wave 8-bit coin & level-up chiptune fanfare.' },
    { id: 'snd_royal',   name: '🎺 Royal Brass Fanfare',      cost: 800,  rarity: 'epic',      desc: 'Warm sawtooth/triangle regal trumpet triad fanfare.' },
    { id: 'snd_cyber',   name: '⚡ Cyberpunk Synthwave Chord', cost: 1600, rarity: 'legendary', desc: 'Detuned dual-oscillator lush neon synthwave power chord.' },
    { id: 'snd_angelic', name: '✨ Celestial Choir Harmonic', cost: 3200, rarity: 'mythic',    desc: 'Shimmering multi-octave harmonic cascade with bell overtones.' },
  ],
  reactions: [
    { id: 'react_super_wave',    emoji: '🌊', name: '🌊 Tidal Surge Super Reaction',       cost: 450,  rarity: 'rare',         animClass: 'super-anim-wave',    particles: ['🌊', '🫧', '💦'], desc: 'Permanently unlocks the 🌊 Tidal Surge Super Reaction in Global Chat — spawns with a cresting aqua wave splash!' },
    { id: 'react_super_fish',    emoji: '🐟', name: '🐟 Golden Bonk Fish Super Reaction',  cost: 500,  rarity: 'rare',         animClass: 'super-anim-fish',    particles: ['🐟', '✨', '🫧'], desc: 'Permanently unlocks the 🐟 Golden Bonk Fish Super Reaction in Global Chat — leaps out of the water in a 24K gold arc!' },
    { id: 'react_super_party',   emoji: '🍻', name: '🍻 Daka Tavern Toast Super Reaction', cost: 550,  rarity: 'rare',         animClass: 'super-anim-party',   particles: ['🍻', '🎉', '✨'], desc: 'Permanently unlocks the 🍻 Daka Tavern Toast Super Reaction in Global Chat — clinks tankards with festive foam!' },
    { id: 'react_super_heart',   emoji: '💘', name: '💘 Skantar Heartbeat Super Reaction', cost: 600,  rarity: 'rare',         animClass: 'super-anim-heart',   particles: ['💘', '💖', '✨'], desc: 'Permanently unlocks the 💘 Skantar Heartbeat Super Reaction in Global Chat — double-pulses with rose-gold hearts!' },
    { id: 'react_super_fire',    emoji: '🔥', name: '🔥 Abyssal Hellfire Super Reaction',  cost: 650,  rarity: 'epic',         animClass: 'super-anim-fire',    particles: ['🔥', '☄️', '✨'], desc: 'Permanently unlocks the 🔥 Abyssal Hellfire Super Reaction in Global Chat — erupts upward in molten volcanic sparks!' },
    { id: 'react_super_crown',   emoji: '👑', name: '👑 Sovereign Crown Super Reaction',   cost: 800,  rarity: 'epic',         animClass: 'super-anim-crown',   particles: ['👑', '✨', '🪙'], desc: 'Permanently unlocks the 👑 Sovereign Crown Super Reaction in Global Chat — descends from above with a royal coronation bounce!' },
    { id: 'react_super_gem',     emoji: '💎', name: '💎 Trench Diamond Super Reaction',    cost: 850,  rarity: 'epic',         animClass: 'super-anim-gem',     particles: ['💎', '💠', '✨'], desc: 'Permanently unlocks the 💎 Trench Diamond Super Reaction in Global Chat — refracts in a crystalline prism spin!' },
    { id: 'react_super_bolt',    emoji: '⚡', name: '⚡ Overdrive Bolt Super Reaction',    cost: 950,  rarity: 'legendary',    animClass: 'super-anim-bolt',    particles: ['⚡', '💥', '✨'], desc: 'Permanently unlocks the ⚡ Overdrive Bolt Super Reaction in Global Chat — strikes with an electric synthwave jitter!' },
    { id: 'react_super_octopus', emoji: '🐙', name: '🐙 Kraken Awakening Super Reaction',  cost: 1100, rarity: 'legendary',    animClass: 'super-anim-octopus', particles: ['🐙', '🌊', '🫧'], desc: 'Permanently unlocks the 🐙 Kraken Awakening Super Reaction in Global Chat — rises from the trench with swirling tentacles!' },
    { id: 'react_super_galaxy',  emoji: '🌌', name: '🌌 Cosmic Singularity Super Reaction', cost: 1200, rarity: 'mythic',       animClass: 'super-anim-galaxy',  particles: ['🌌', '🪐', '✦'], desc: 'Permanently unlocks the 🌌 Cosmic Singularity Super Reaction in Global Chat — implodes and expands in a 360° nebula vortex!' },
    { id: 'react_super_rocket',  emoji: '🚀', name: '🚀 Starlight Rocket Super Reaction',  cost: 1350, rarity: 'mythic',       animClass: 'super-anim-rocket',  particles: ['🚀', '☄️', '🌟'], desc: 'Permanently unlocks the 🚀 Starlight Rocket Super Reaction in Global Chat — blasts diagonally into orbit with starlight exhaust!' },
    { id: 'react_super_dragon',  emoji: '🐉', name: '🐉 Leviathan Roar Super Reaction',    cost: 1600, rarity: 'transcendent', animClass: 'super-anim-dragon',  particles: ['🐉', '🔥', '⚡'], desc: 'Permanently unlocks the 🐉 Leviathan Roar Super Reaction in Global Chat — swoops in with draconic shockwaves!' },
  ],
  perks: [
    {
      id: 'perk_streak_shield',
      name: '🛡️ Blobber Streak Shield',
      cost: 400,
      rarity: 'rare',
      repeatable: true,
      perkKey: 'streak_shields',
      grantAmount: 1,
      maxStacks: 5,
      desc: 'Consumable shield. Automatically absorbs a wrong answer when you have an active streak, preserving your streak AND preventing the -15 pt penalty!',
    },
    {
      id: 'perk_double_xp',
      name: '⚡ Overdrive Booster (Next 5 Answers 2x Pts)',
      cost: 750,
      rarity: 'epic',
      repeatable: true,
      perkKey: 'double_xp_charges',
      grantAmount: 5,
      chargesPerPurchase: 5,
      desc: 'Adds +5 Overdrive charges. Each eligible mastery award consumes 1 charge and doubles its points. Capped practice and diminished verb drills do not consume charges.',
    },
    {
      id: 'perk_scholar_multiplier',
      name: '📜 Scholar Talisman (+25% Permanent Points)',
      cost: 3000,
      rarity: 'legendary',
      repeatable: false,
      multiplierBonus: 0.25,
      desc: 'One-time permanent relic that permanently boosts all Dojo point gains by +25% forever!',
    },
    {
      id: 'perk_grandmaster_multiplier',
      name: '👑 Omniscient Crown (+50% Permanent Points)',
      cost: 7500,
      rarity: 'transcendent',
      repeatable: false,
      multiplierBonus: 0.5,
      desc: 'Ultimate permanent relic! Stacks with Scholar Talisman to boost all Dojo point gains by +50% (up to 1.75x base, or 3.5x with Overdrive!).',
    },
  ],
};

const DISAPPOINTED_JORN_ROASTS = [
  '🤬 KIXIX, TEXIK! Did you even read SeameneseDictionary.txt?! (-15 pts)',
  '🐟 Tëi kiniwako xaai NI TUK! Even a Blobber knows this conjugation! (-15 pts)',
  '😔 Wo panke for your brain cells. Jorn is shaking his head. (-15 pts)',
  '🤏 Tëi seamenese xaai TI! Study the plural rule, buana! (-15 pts)',
  '😴 Te xa sarpo zo fasma?! Wake up (`xa lipo`) and try again! (-15 pts)',
  '🌧️ Eke mikeai on your streak! Streak reset to 0! (-15 pts)',
  '🥗 Go eat some liron blubur and rethink that answer, ta buana! (-15 pts)',
];

function generatePlayerId() {
  return 'p-' + Math.random().toString(36).slice(2, 10) + '-' + Date.now().toString(36);
}

const DEFAULT_DOPAMINE_STATE = {
  playerId: '',
  username: '',
  enabled: true,
  points: 0,
  streak: 0,
  bestStreak: 0,
  solvedCount: 0,
  unlockedItems: ['theme_default', 'title_blober', 'effect_sparkles', 'np_default', 'frame_default', 'snd_default'],
  unlocked_perks: [],
  equippedTheme: 'theme_default',
  equippedTitle: 'title_blober',
  equippedEffect: 'effect_sparkles',
  equippedNameplate: 'np_default',
  equippedFrame: 'frame_default',
  equippedSound: 'snd_default',
  perks: {
    streak_shields: 0,
    double_xp_charges: 0,
  },
  soundEnabled: true,
};

function sanitizeAndValidateDopamineState(parsed) {
  const validItemIds = new Set();
  Object.values(DOPAMINE_SHOP_ITEMS).forEach(list => {
    (list || []).forEach(it => validItemIds.add(it.id));
  });

  const rawUnlocked = [
    ...DEFAULT_DOPAMINE_STATE.unlockedItems,
    ...(Array.isArray(parsed?.unlockedItems) ? parsed.unlockedItems : []),
    ...(Array.isArray(parsed?.unlocked_perks) ? parsed.unlocked_perks : []),
  ].filter(id => validItemIds.has(id));

  const unlockedSet = new Set(rawUnlocked);

  const validEquipped = (catKey, candidateId, fallbackId) => {
    const existsInCat = (DOPAMINE_SHOP_ITEMS[catKey] || []).some(it => it.id === candidateId);
    if (existsInCat && unlockedSet.has(candidateId)) return candidateId;
    return fallbackId;
  };

  const equippedTheme = validEquipped('themes', parsed?.equippedTheme, 'theme_default');
  const equippedTitle = validEquipped('titles', parsed?.equippedTitle, 'title_blober');
  const equippedEffect = validEquipped('effects', parsed?.equippedEffect, 'effect_sparkles');
  const equippedNameplate = validEquipped('nameplates', parsed?.equippedNameplate, 'np_default');
  const equippedFrame = validEquipped('frames', parsed?.equippedFrame, 'frame_default');
  const equippedSound = validEquipped('sounds', parsed?.equippedSound, 'snd_default');

  const unlockedPerks = (DOPAMINE_SHOP_ITEMS.perks || [])
    .filter(p => !p.repeatable && unlockedSet.has(p.id))
    .map(p => p.id);

  const rawShields = parsed?.perks?.streak_shields ?? parsed?.streak_shields ?? 0;
  const rawDoubleXp = parsed?.perks?.double_xp_charges ?? parsed?.double_xp_charges ?? 0;

  return {
    ...DEFAULT_DOPAMINE_STATE,
    ...parsed,
    playerId: parsed?.playerId || generatePlayerId(),
    username: String(parsed?.username || '').trim().slice(0, 28),
    enabled: typeof parsed?.enabled === 'boolean' ? parsed.enabled : true,
    points: Math.max(0, Math.floor(Number(parsed?.points) || 0)),
    streak: Math.max(0, Math.floor(Number(parsed?.streak) || 0)),
    bestStreak: Math.max(0, Math.floor(Number(parsed?.bestStreak) || 0)),
    solvedCount: Math.max(0, Math.floor(Number(parsed?.solvedCount) || 0)),
    unlockedItems: Array.from(unlockedSet),
    unlocked_perks: unlockedPerks,
    equippedTheme,
    equippedTitle,
    equippedEffect,
    equippedNameplate,
    equippedFrame,
    equippedSound,
    perks: {
      streak_shields: Math.max(0, Math.floor(Number(rawShields) || 0)),
      double_xp_charges: Math.max(0, Math.floor(Number(rawDoubleXp) || 0)),
    },
    soundEnabled: typeof parsed?.soundEnabled === 'boolean' ? parsed.soundEnabled : true,
  };
}

function loadDopamineState() {
  try {
    const rawV2 = localStorage.getItem(STORAGE_KEY_DOPAMINE);
    if (rawV2) {
      const parsed = JSON.parse(rawV2);
      const st = sanitizeAndValidateDopamineState(parsed);
      localStorage.setItem(STORAGE_KEY_DOPAMINE, JSON.stringify(st));
      return st;
    }
    const rawV1 = localStorage.getItem(STORAGE_KEY_DOPAMINE_LEGACY);
    if (rawV1) {
      const parsedV1 = JSON.parse(rawV1);
      const migrated = sanitizeAndValidateDopamineState({ ...parsedV1, enabled: true });
      localStorage.setItem(STORAGE_KEY_DOPAMINE, JSON.stringify(migrated));
      return migrated;
    }
    const fresh = sanitizeAndValidateDopamineState({});
    localStorage.setItem(STORAGE_KEY_DOPAMINE, JSON.stringify(fresh));
    return fresh;
  } catch {
    return sanitizeAndValidateDopamineState({});
  }
}

function saveDopamineState(state) {
  const { activityRewardEvents, ...persisted } = state || {};
  try {
    localStorage.setItem(STORAGE_KEY_DOPAMINE, JSON.stringify(persisted));
  } catch {}
}

function applyDopamineThemeToDom(themeId) {
  if (typeof document === 'undefined') return;
  const isValidTheme = DOPAMINE_SHOP_ITEMS.themes.some(t => t.id === themeId);
  const t = isValidTheme ? themeId : 'theme_default';
  document.documentElement.setAttribute('data-seamenese-theme', t);
}

function getComboMultiplier() {
  return 1;
}

function getComboLabel(streak) {
  return `${Math.max(0, Number(streak) || 0)} in a row`;
}

function getPermanentPointMultiplier(st) {
  if (!st) return 1;
  const unlocked = new Set([...(st.unlockedItems || []), ...(st.unlocked_perks || [])]);
  let mult = 1;
  (DOPAMINE_SHOP_ITEMS.perks || []).forEach(p => {
    if (!p.repeatable && p.multiplierBonus && unlocked.has(p.id)) {
      mult += p.multiplierBonus;
    }
  });
  return Number(mult.toFixed(2));
}

function getOverdriveCharges(state) {
  return Math.max(
    0,
    Math.floor(Number(state?.perks?.double_xp_charges ?? state?.double_xp_charges ?? 0) || 0)
  );
}

function getTotalActivePointMultiplier(state) {
  return Number((getPermanentPointMultiplier(state) * (getOverdriveCharges(state) > 0 ? 2 : 1)).toFixed(2));
}

function getEquippedThemeObj(themeId) {
  return DOPAMINE_SHOP_ITEMS.themes.find(t => t.id === themeId) || DOPAMINE_SHOP_ITEMS.themes[0];
}

function getEquippedTitleObj(titleId) {
  return DOPAMINE_SHOP_ITEMS.titles.find(t => t.id === titleId) || DOPAMINE_SHOP_ITEMS.titles[0];
}

function getEquippedEffectObj(effectId) {
  return DOPAMINE_SHOP_ITEMS.effects.find(e => e.id === effectId) || DOPAMINE_SHOP_ITEMS.effects[0];
}

function getEquippedNameplateObj(nameplateId) {
  return DOPAMINE_SHOP_ITEMS.nameplates.find(n => n.id === nameplateId) || DOPAMINE_SHOP_ITEMS.nameplates[0];
}

function getEquippedFrameObj(frameId) {
  return (DOPAMINE_SHOP_ITEMS.frames || []).find(f => f.id === frameId) || (DOPAMINE_SHOP_ITEMS.frames || [])[0] || { id: 'frame_default', name: 'Standard Archive Row', cssClass: 'row-frame-default', decorIcons: [] };
}

function getEquippedSoundObj(soundId) {
  return DOPAMINE_SHOP_ITEMS.sounds.find(s => s.id === soundId) || DOPAMINE_SHOP_ITEMS.sounds[0];
}

// WebAudio synth fanfares (5 unlockable packs) & sad trombone
function playDopamineSynth(kind, soundEnabled = true, soundPackId = null) {
  if (!soundEnabled) return;
  try {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) return;
    const ctx = new AudioCtx();
    const now = ctx.currentTime;

    if (kind === 'win' || kind === 'correct') {
      let pack = soundPackId;
      if (!pack) {
        try {
          const saved = JSON.parse(localStorage.getItem(STORAGE_KEY_DOPAMINE) || '{}');
          pack = saved.equippedSound || 'snd_default';
        } catch {
          pack = 'snd_default';
        }
      }

      if (pack === 'snd_arcade') {
        // 8-Bit Chiptune Victory (rapid square wave coin + level up)
        const notes = [
          { freq: 587.33, t: 0.0, d: 0.06 },
          { freq: 783.99, t: 0.055, d: 0.06 },
          { freq: 987.77, t: 0.11, d: 0.06 },
          { freq: 1174.66, t: 0.165, d: 0.08 },
          { freq: 1567.98, t: 0.24, d: 0.22 },
        ];
        notes.forEach(n => {
          const osc = ctx.createOscillator();
          const gain = ctx.createGain();
          osc.type = 'square';
          osc.frequency.setValueAtTime(n.freq, now + n.t);
          gain.gain.setValueAtTime(0.001, now + n.t);
          gain.gain.exponentialRampToValueAtTime(0.11, now + n.t + 0.01);
          gain.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
          osc.connect(gain);
          gain.connect(ctx.destination);
          osc.start(now + n.t);
          osc.stop(now + n.t + n.d + 0.01);
        });
      } else if (pack === 'snd_royal') {
        // Royal Brass Fanfare (warm sawtooth + triangle triad)
        const seq = [
          { freq: 392.0, t: 0.0, d: 0.09 },
          { freq: 392.0, t: 0.095, d: 0.09 },
          { freq: 523.25, t: 0.19, d: 0.13 },
          { freq: 659.25, t: 0.32, d: 0.14 },
          { freq: 783.99, t: 0.46, d: 0.32 },
        ];
        seq.forEach(n => {
          ['sawtooth', 'triangle'].forEach((wave, wIdx) => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = wave;
            osc.frequency.setValueAtTime(n.freq * (wIdx === 1 ? 0.5 : 1), now + n.t);
            gain.gain.setValueAtTime(0.001, now + n.t);
            gain.gain.exponentialRampToValueAtTime(wIdx === 0 ? 0.11 : 0.14, now + n.t + 0.02);
            gain.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
            osc.connect(gain);
            gain.connect(ctx.destination);
            osc.start(now + n.t);
            osc.stop(now + n.t + n.d + 0.02);
          });
        });
      } else if (pack === 'snd_cyber') {
        // Cyberpunk Synthwave Chord (detuned dual-oscillator lush synth chord)
        const chord = [261.63, 329.63, 392.0, 493.88, 659.25, 1046.5];
        chord.forEach((freq, idx) => {
          [-6, 6].forEach(detuneCents => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            const start = now + idx * 0.035;
            osc.frequency.setValueAtTime(freq, start);
            osc.detune.setValueAtTime(detuneCents, start);
            gain.gain.setValueAtTime(0.001, start);
            gain.gain.exponentialRampToValueAtTime(0.065, start + 0.03);
            gain.gain.exponentialRampToValueAtTime(0.001, start + 0.42);
            osc.connect(gain);
            gain.connect(ctx.destination);
            osc.start(start);
            osc.stop(start + 0.44);
          });
        });
      } else if (pack === 'snd_angelic') {
        // Celestial Choir Harmonic (shimmering high-octave cascade)
        const notes = [523.25, 659.25, 783.99, 987.77, 1318.51, 1567.98, 2093.0];
        notes.forEach((freq, idx) => {
          const osc = ctx.createOscillator();
          const gain = ctx.createGain();
          osc.type = 'sine';
          const start = now + idx * 0.05;
          osc.frequency.setValueAtTime(freq, start);
          gain.gain.setValueAtTime(0.001, start);
          gain.gain.exponentialRampToValueAtTime(0.14, start + 0.025);
          gain.gain.exponentialRampToValueAtTime(0.001, start + 0.45);
          osc.connect(gain);
          gain.connect(ctx.destination);
          osc.start(start);
          osc.stop(start + 0.47);
        });
      } else {
        // Classic Major Arpeggio (snd_default)
        const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
        notes.forEach((freq, idx) => {
          const osc = ctx.createOscillator();
          const gain = ctx.createGain();
          osc.type = 'triangle';
          osc.frequency.setValueAtTime(freq, now + idx * 0.065);
          gain.gain.setValueAtTime(0.001, now + idx * 0.065);
          gain.gain.exponentialRampToValueAtTime(0.18, now + idx * 0.065 + 0.02);
          gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.065 + 0.24);
          osc.connect(gain);
          gain.connect(ctx.destination);
          osc.start(now + idx * 0.065);
          osc.stop(now + idx * 0.065 + 0.25);
        });
      }
    } else if (kind === 'fail') {
      // Sad trombone / buzzer: Bb3 -> A3 -> Ab3 -> G3
      const notes = [
        { freq: 233.08, start: 0.0, dur: 0.16 },
        { freq: 220.00, start: 0.17, dur: 0.16 },
        { freq: 207.65, start: 0.34, dur: 0.16 },
        { freq: 196.00, start: 0.51, dur: 0.42 },
      ];
      notes.forEach((n, idx) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(n.freq, now + n.start);
        if (idx === 3) {
          osc.frequency.linearRampToValueAtTime(178.0, now + n.start + n.dur);
        }
        gain.gain.setValueAtTime(0.001, now + n.start);
        gain.gain.exponentialRampToValueAtTime(0.14, now + n.start + 0.02);
        gain.gain.exponentialRampToValueAtTime(0.001, now + n.start + n.dur);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start(now + n.start);
        osc.stop(now + n.start + n.dur + 0.02);
      });
    } else if (kind === 'unlock') {
      const notes = [659.25, 880, 1318.5];
      notes.forEach((freq, idx) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(freq, now + idx * 0.06);
        gain.gain.setValueAtTime(0.001, now + idx * 0.06);
        gain.gain.exponentialRampToValueAtTime(0.16, now + idx * 0.06 + 0.015);
        gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.06 + 0.2);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start(now + idx * 0.06);
        osc.stop(now + idx * 0.06 + 0.22);
      });
    }
  } catch {}
}

// ============================================================================
// 3. BASELINE GROUND-TRUTH LEXICON, CONJUGATIONS, SCENES, CORPUS & COMPOUNDS
// ============================================================================
const BASELINE_LEXICON = [
  { id: 'ap', term: 'Ap', pos: 'particle', status: 'confirmed', emoji: '✋', meaning: 'But', hint: "Contrastive particle linking clauses or preceding a command ('Ap bet!' = 'But wait!')", example: 'Ëi xa sarpo, ap ëi skan obisëi seamenese.', notes: "Followed by a conjugated verb stem (e.g., 'ap bet')." },
  { id: 'beti', term: 'Beti', pos: 'verb', status: 'confirmed', emoji: '⏳', meaning: 'To wait', hint: "Pausing or holding on ('Ap bet, beto!')", example: 'Ap bet, ta buana! Ëi kin zorpëi te.', notes: 'Conjugates: Ëi/Te=Bet, Ton=Betu, Na=Betami, La/Lon=Betai.' },
  { id: 'beto', term: 'Beto', pos: 'noun', status: 'confirmed', emoji: '🙏', meaning: 'Please', hint: 'Polite marker placed at the end of a request or used standalone', example: 'Ob ëi tumu, beto!', notes: "Listed as a noun in the dictionary; used like 'please'." },
  { id: 'blobber', term: 'Blobber', pos: 'noun', status: 'confirmed', emoji: '🐟', meaning: 'Fish', hint: 'Aquatic animal that swims in water; eaten as tumu', example: 'Ta silhbi wo skanai zetëi blobber.', notes: 'Variant spelling: Blober. Plural: Blobberon.' },
  { id: 'blober', term: 'Blober', pos: 'noun', status: 'confirmed', emoji: '🐟', meaning: 'Fish', hint: 'Alternate spelling of Blobber (fish)', example: 'La zeteai ti blober.', notes: 'Variant spelling of Blobber.' },
  { id: 'blubur', term: 'Blubur', pos: 'noun', status: 'confirmed', emoji: '🥗', meaning: 'Salad', hint: 'Liron (green) vegetable dish, a type of tumu', example: 'Etpanke ëi zete liron blubur u blobber.', notes: 'Noun for salad.' },
  { id: 'brux', term: 'Brux', pos: 'adjective', status: 'confirmed', emoji: '🟥', meaning: 'Red', hint: 'Warm primary color of fire and rubies', example: 'Ta skantar skanai brux tumu.', notes: 'Color adjective in the official dictionary.' },
  { id: 'buana', term: 'Buana', pos: 'noun', status: 'confirmed', emoji: '🤝', meaning: 'Friend', hint: "Someone you trust and like ('1 buana, 2 buanon')", example: 'Tamako ta buanon! Wu xaai tëion panke?', notes: "Plural drops final vowel '-a' and adds '-on': Buanon." },
  { id: 'daka', term: 'Daka', pos: 'noun', status: 'confirmed', emoji: '🍻', meaning: 'Celebratory word said when drinking. Similar to cheers.', hint: 'Exclamation said when raising drinks together', example: 'Daka, ta buanon! Etwuke na xaami mubu!', notes: "Used like 'Cheers!'." },
  { id: 'ei', term: 'Ëi', pos: 'pronoun', status: 'confirmed', emoji: '🙋', meaning: 'I (1st person singular pronoun)', hint: 'Refers to the speaker (subject/object; never possessive)', example: 'Ëi skan seamenese u ëi pon lulu semenon.', notes: "E followed by I is always written Ë and pronounced 'ay'. For 'my', use Ta." },
  { id: 'eke', term: 'Eke', pos: 'noun', status: 'confirmed', emoji: '🌌', meaning: 'Sky', hint: 'The peke (blue) expanse above; combines with pan or wubu', example: 'Eke xaai peke etpanke.', notes: 'Forms compounds: pan + eke = panke (day), wubu + eke = wuke (night).' },
  { id: 'et', term: 'Et', pos: 'pronoun', status: 'confirmed', emoji: '👇', meaning: 'This', hint: 'Demonstrative pronoun pointing to something near or current', example: 'Wa xaai et? Et xaai lulu semen.', notes: 'Combines with panke -> etpanke (today) and wuke -> etwuke (tonight).' },
  { id: 'etpanke', term: 'Etpanke', pos: 'noun', status: 'confirmed', emoji: '📅', meaning: 'Today', hint: 'Compound of et (this) + panke (day)', example: 'Etpanke xaai wo tuk panke!', notes: "Takes 3sg copula 'xaai' when used as a subject." },
  { id: 'etwuke', term: 'Etwuke', pos: 'noun', status: 'confirmed', emoji: '🌃', meaning: 'Tonight / This night', hint: 'Compound of et (this) + wuke (night time)', example: 'Etwuke na sarpami zo na obiseami ezuki?', notes: 'Compound of et + wuke.' },
  { id: 'ezuki', term: 'Ezuki', pos: 'noun', status: 'confirmed', emoji: '🎵', meaning: 'Music / Song', hint: "Melodic sound; 'Obisëi ezuki' means 'to sing'", example: 'Ëi skan obisëi tuk ezuki!', notes: "To express 'singing', Seamenese says 'Obisëi ezuki'." },
  { id: 'faki', term: 'Faki', pos: 'verb', status: 'confirmed', emoji: '🔞', meaning: 'To fuck', hint: 'Intimate/vulgar action verb', example: 'Ëi skan itxi tëi texik u ëi skan san faki tëi wo ka.', notes: 'Conjugates: Ëi/Te=Fak, Ton=Faku, Na=Fakami, La/Lon=Fakai.' },
  { id: 'fasma', term: 'fasma', pos: 'adjective', status: 'confirmed', emoji: '⚡', meaning: 'Tired', hint: 'Adjective describing fatigue', example: 'Ëi xa fasma etwuke.', notes: 'Listed in the dictionary as adjective: Tired.' },
  { id: 'ha', term: 'Ha', pos: 'particle', status: 'confirmed', emoji: '✅', meaning: 'Yes', hint: 'Affirmative particle (opposite of Ni)', example: 'Ha, ëi skan!', notes: "Combines with 'ni' to form 'hani' (maybe)." },
  { id: 'hani', term: 'Hani', pos: 'particle', status: 'confirmed', emoji: '🤔', meaning: 'Maybe', hint: 'Compound of ha (yes) + ni (no)', example: 'Hani xaai ha + ni.', notes: "Attested in corpus example #26: 'hani xaai ha + ni'." },
  { id: 'itxi', term: 'Itxi', pos: 'verb', status: 'confirmed', emoji: '💋', meaning: 'To kiss', hint: "Affectionate action with the lips ('X' is pronounced 'sh')", example: 'Ton itxu texikon.', notes: 'Conjugates: Ëi/Te=Itx, Ton=Itxu, Na=Itxami, La/Lon=Itxai.' },
  { id: 'jai', term: 'Jai', pos: 'verb', status: 'confirmed', emoji: '🛠️', meaning: 'To do / To make', hint: 'Core action verb (also tagged as pronoun in dictionary header)', example: 'Wa te ja etpanke?', notes: 'Conjugates: Ëi/Te=Ja, Ton=Jau, Na=Jaami, La/Lon=Jaai.' },
  { id: 'jai-semen', term: 'Jai-semen', pos: 'noun', status: 'confirmed', emoji: '🏃', meaning: 'An action-word / a verb / a word that does or acts', hint: 'Compound of jai (to do) + semen (word)', example: "'Obisëi' u 'zetëi' xaai jai-semen.", notes: "Literally 'do-word'." },
  { id: 'ka', term: 'Ka', pos: 'noun', status: 'confirmed', emoji: '🍆', meaning: 'Penis', hint: "Male anatomical noun ('ti ka' vs 'wo ka')", example: 'Ëi ni skan wo ka.', notes: 'Attested across corpus examples #10, #11, #16, #17, #32, #34, #42.' },
  { id: 'kani', term: 'Kani', pos: 'noun', status: 'confirmed', emoji: '🌸', meaning: 'Vagina', hint: 'Female anatomical noun', example: 'Kani xaai semen.', notes: 'Anatomical noun.' },
  { id: 'kini', term: 'Kini', pos: 'verb', status: 'confirmed', emoji: '🎯', meaning: 'To need', hint: 'Expresses necessity; if followed by another verb, only Kini conjugates', example: 'Ëi kin zorpëi te! Na kinami tumu.', notes: 'Conjugates: Ëi/Te=Kin, Ton=Kinu, Na=Kinami, La/Lon=Kinai.' },
  { id: 'kiniwai', term: 'Kiniwai', pos: 'verb', status: 'confirmed', emoji: '💡', meaning: 'To explain', hint: 'Making a concept or word clear to someone', example: 'Beto, kiniwa et lulu semen!', notes: 'Conjugates: Ëi/Te=Kiniwa, Ton=Kiniwau, Na=Kiniwaami, La/Lon=Kiniwaai.' },
  { id: 'kiniwako', term: 'Kiniwako', pos: 'noun', status: 'confirmed', emoji: '📖', meaning: 'Explanation', hint: 'Noun form of kiniwai (to explain)', example: 'Tëi kiniwako xaai wo tuk!', notes: 'Derived from kiniwai + -ko.' },
  { id: 'kixixi', term: 'Kixixi', pos: 'verb', status: 'confirmed', emoji: '🤫', meaning: 'To shut up', hint: "Telling someone to stop talking ('X' pronounced 'sh')", example: 'Texik, kixix!', notes: 'Conjugates: Ëi/Te=Kixix, Ton=Kixixu, Na=Kixixami, La/Lon=Kixixai.' },
  { id: 'la', term: 'La', pos: 'pronoun', status: 'confirmed', emoji: '👤', meaning: 'He / She / It (3rd person SINGULAR pronoun)', hint: 'Refers to any single third person or thing', example: 'La obiseai ezuki u la xaai mubu.', notes: 'Takes -ai verb ending. Plural is Lon.' },
  { id: 'lai', term: 'Lai', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'His / Her / Its (3rd person SINGULAR possessive pronoun)', hint: 'Shows ownership by a single third person (La)', example: 'Lai silhbi xaai ti u tuk.', notes: 'Singular possessive; plural is Laion.' },
  { id: 'laion', term: 'Laion', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'Their', hint: '3rd person plural possessive pronoun (belongs to Lon)', example: 'Lon skanai laion buanon.', notes: 'Formed from Lai + -on.' },
  { id: 'lipo', term: 'Lipo', pos: 'adjective', status: 'confirmed', emoji: '🤩', meaning: 'Awake / Energetic / Excited / Not tired', hint: 'Full of energy; opposite of sarpo', example: 'Ëi ni xa sarpo, ëi xa lipo!', notes: 'Antonym of sarpo.' },
  { id: 'liron', term: 'Liron', pos: 'adjective', status: 'confirmed', emoji: '🟩', meaning: 'Green', hint: 'The color of blubur (salad) and grass', example: 'Blubur xaai liron tumu.', notes: 'Color adjective in the official dictionary.' },
  { id: 'lon', term: 'Lon', pos: 'pronoun', status: 'confirmed', emoji: '👥', meaning: 'They / Them', hint: '3rd person plural pronoun (takes -ai verb ending)', example: 'Lon zeteai tumu u lon obiseai.', notes: 'Conjugates with -ai, same as La.' },
  { id: 'lulu', term: 'Lulu', pos: 'noun', status: 'confirmed', emoji: '✨', meaning: 'New', hint: "Fresh or novel (listed under noun in dictionary, modifies nouns like 'lulu semen')", example: 'Etpanke ëi pon lulu semen!', notes: "Listed as 'noun: New' in SeameneseDictionary.txt." },
  { id: 'mibi', term: 'Mibi', pos: 'emotion', status: 'confirmed', emoji: '😠', meaning: 'Angry', hint: 'Feeling mad or upset (opposite of mubu)', example: 'We te xa mibi, ta buana?', notes: 'Emotion word used with copula Xai.' },
  { id: 'mikei', term: 'Mikëi', pos: 'verb', status: 'confirmed', emoji: '🌧️', meaning: 'To rain', hint: 'Water falling from the eke (sky); note the Ë before I', example: 'Etpanke eke mikeai.', notes: 'Conjugates: Ëi/Te=Mike, Ton=Mikeu, Na=Mikeami, La/Lon=Mikeai.' },
  { id: 'mubu', term: 'Mubu', pos: 'emotion', status: 'confirmed', emoji: '🙂', meaning: 'Happy', hint: 'Feeling joyful and glad', example: 'Ëi xa wo mubu etpanke!', notes: 'Emotion word used with Xai.' },
  { id: 'na', term: 'Na', pos: 'pronoun', status: 'confirmed', emoji: '🫂', meaning: 'We / Us', hint: '1st person plural pronoun (takes -ami verb ending)', example: 'Na skanami ezuki!', notes: 'Possessive form is Nai.' },
  { id: 'nai', term: 'Nai', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'Our / Ours', hint: '1st person plural possessive pronoun', example: 'Seamenese xaai nai tuk semen!', notes: 'Belonging to Na (we).' },
  { id: 'ni', term: 'Ni', pos: 'particle', status: 'confirmed', emoji: '❌', meaning: 'No / Not', hint: "Negates a verb or adjective ('ni tuk ezuki' = bad music)", example: 'Ëi ni skan wo ka. Ni tuk ezuki.', notes: 'Placed directly before the negated word.' },
  { id: 'nimi', term: 'Nimi', pos: 'verb', status: 'confirmed', emoji: '🙌', meaning: 'To thank', hint: "Expressing gratitude ('Ëi nim te' = I thank you)", example: 'Ëi nim te, ta buana!', notes: 'Conjugates: Ëi/Te=Nim, Ton=Nimu, Na=Nimami, La/Lon=Nimai.' },
  { id: 'obi', term: 'Obi', pos: 'verb', status: 'confirmed', emoji: '🎁', meaning: 'To give', hint: 'Handing or offering something to someone', example: 'La obai tumu ta silhbi.', notes: 'Conjugates: Ëi/Te=Ob, Ton=Obu, Na=Obami, La/Lon=Obai.' },
  { id: 'obisei', term: 'Obisëi', pos: 'verb', status: 'confirmed', emoji: '🗣️', meaning: 'To speak / To vocalize', hint: "Talking aloud; 'Obisëi ezuki' = To sing!", example: 'Ëi skan obisëi ezuki.', notes: 'Conjugates: Ëi/Te=Obise, Ton=Obiseu, Na=Obiseami, La/Lon=Obiseai.' },
  { id: 'pan', term: 'Pan', pos: 'noun', status: 'confirmed', emoji: '☀️', meaning: 'Sun', hint: 'Bright daytime star; pan + eke = panke (day)', example: 'Ëi skan wubu u ëi skan pan.', notes: 'Root of panke.' },
  { id: 'panke', term: 'Panke', pos: 'noun', status: 'confirmed', emoji: '🌤️', meaning: 'Day', hint: "Compound of pan (sun) + eke (sky); 'wo panke' = long/tough day", example: 'Jorn, wu xaai tëi panke? Ta panke xaai tuk.', notes: 'Attested in examples #02, #13, #21, #23, #24, #25.' },
  { id: 'pansai', term: 'Pansai', pos: 'verb', status: 'confirmed', emoji: '💭', meaning: 'To think / To ponder', hint: 'Mental reflection (from the official verb conjugation table)', example: 'Ëi pansa seamenese xaai wo tuk.', notes: 'Conjugates: Ëi/Te=Pansa, Ton=Pansau, Na=Pansaami, La/Lon=Pansaai.' },
  { id: 'peke', term: 'Peke', pos: 'adjective', status: 'confirmed', emoji: '🟦', meaning: 'Blue', hint: 'The color of a clear daytime eke (sky) and deep ocean', example: 'Eke xaai peke u blubur xaai liron.', notes: 'Color adjective in the official dictionary.' },
  { id: 'poni', term: 'Poni', pos: 'verb', status: 'confirmed', emoji: '🧠', meaning: 'To understand', hint: 'Comprehending meaning or explanations', example: 'Ha, ëi pon tëi kiniwako!', notes: 'Conjugates: Ëi/Te=Pon, Ton=Ponu, Na=Ponami, La/Lon=Ponai.' },
  { id: 'ragoi', term: 'Ragoi', pos: 'verb', status: 'confirmed', emoji: '🤜🤛', meaning: 'To agree / to be on the same wavelength', hint: 'Sharing the same view or vibe', example: 'Rago! Na ragoami!', notes: 'Conjugates: Ëi/Te=Rago, Ton=Ragou, Na=Ragoami, La/Lon=Ragoai.' },
  { id: 'san', term: 'San', pos: 'particle', status: 'confirmed', emoji: '➕', meaning: 'Also / Too / Same (adverb)', hint: "Indicates addition ('Ëi san' = Me too)", example: 'Blui skanai tëi ezuki - ëi san skan tëi ezuki.', notes: 'Can appear before the verb, after the verb, or standalone after a pronoun.' },
  { id: 'sanai', term: 'Sanai', pos: 'verb', status: 'confirmed', emoji: '🔁', meaning: 'To cycle / to loop / to repeat / to do a question-answer pattern', hint: 'Repeating a drill or Q&A loop', example: 'Na sanaami lulu semenon!', notes: 'Related to san (same/also).' },
  { id: 'sarpi', term: 'Sarpi', pos: 'verb', status: 'confirmed', emoji: '😴', meaning: 'To sleep / To be asleep', hint: 'Resting at wuke (night)', example: 'Etwuke ëi sarp.', notes: 'Conjugates: Ëi/Te=Sarp, Ton=Sarpu, Na=Sarpami, La/Lon=Sarpai.' },
  { id: 'sarpo', term: 'sarpo', pos: 'adjective', status: 'confirmed', emoji: '🥱', meaning: 'Tired / Sleepy', hint: 'Needing sleep (opposite of lipo)', example: 'Wuke xaai, ëi xa sarpo.', notes: 'Used with copula Xai.' },
  { id: 'seamenese', term: 'seamenese', pos: 'noun', status: 'confirmed', emoji: '🌊', meaning: 'The conlang itself', hint: 'The official name of this language', example: 'Ëi wo skan obisëi seamenese!', notes: 'Derived from semen (word).' },
  { id: 'semen', term: 'Semen', pos: 'noun', status: 'confirmed', emoji: '🔤', meaning: 'Word', hint: 'A unit of vocabulary (plural: semenon)', example: 'Te pon et lulu semen?', notes: 'Forms compounds jai-semen (verb) and zorpe-semen (question-word).' },
  { id: 'silhbi', term: 'Silhbi', pos: 'noun', status: 'confirmed', emoji: '🐱', meaning: 'Cat', hint: 'Feline pet that loves zetëi blobber (plural: silhbon)', example: 'Ta ti silhbi sarpai.', notes: "Plural drops '-i' and adds '-on': Silhbon." },
  { id: 'skani', term: 'Skani', pos: 'verb', status: 'confirmed', emoji: '❤️', meaning: 'To like', hint: "Feeling affection; 'wo skan' = to love!", example: 'Ëi skan ezuki. Te wo skan ezuki?', notes: 'Conjugates: Ëi/Te=Skan, Ton=Skanu, Na=Skanami, La/Lon=Skanai.' },
  { id: 'skantar', term: 'Skantar', pos: 'noun', status: 'confirmed', emoji: '💘', meaning: 'Crush / lover', hint: "Romantic partner or crush ('1 skantar, 2 skantaron')", example: 'Luke, ëi ni xa tëi buana. Ëi xa tëi skantar.', notes: 'Plural attested as skantaron (in Example #39).' },
  { id: 'sobo', term: 'Sobo', pos: 'noun', status: 'confirmed', emoji: '🦶', meaning: 'Foot', hint: "Body part for standing; plural 'Feet' is Sobon (drop -o + on)", example: '1 sobo, 2 sobon! Ta sobon xaai wo.', notes: 'Canonical plural rule example: 1 sobo, 2 sobon.' },
  { id: 'ta', term: 'Ta', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'My', hint: '1st person singular possessive pronoun', example: 'Tamako ta buanon! Ta panke xaai tuk.', notes: "Always use Ta (never Ëi) before a noun for 'my'." },
  { id: 'taki', term: 'Taki', pos: 'noun', status: 'confirmed', emoji: '👋', meaning: 'Bye', hint: "Parting farewell ('Taki! Tuk wuke')", example: 'Taki! Tuk wuke, ta buana.', notes: 'Opposite of Tamako.' },
  { id: 'tamako', term: 'Tamako', pos: 'noun', status: 'confirmed', emoji: '🖐️', meaning: 'Hello (greeting)', hint: 'Friendly greeting when meeting someone', example: 'Tamako, wu xa te?', notes: 'Standard greeting.' },
  { id: 'te', term: 'Te', pos: 'pronoun', status: 'confirmed', emoji: '👉', meaning: 'You (2nd person singular pronoun — singular only)', hint: 'Addressing one person (takes bare verb stem)', example: 'Te xa ta buana, Jorn.', notes: "Singular only; for plural 'you all', use Ton." },
  { id: 'tei', term: 'Tëi', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'Your (2nd person possessive)', hint: 'Belonging to one person (Te); note the Ë!', example: 'Jorn, wu xaai tëi panke?', notes: 'Singular possessive; plural is Tëion.' },
  { id: 'teion', term: 'Tëion', pos: 'pronoun', status: 'confirmed', emoji: '🏷️', meaning: 'Your (2nd person plural possessive)', hint: 'Belonging to multiple people (Ton)', example: 'Tamako ta buanon. Wu xaai tëion panke?', notes: 'Attested in corpus example #13.' },
  { id: 'texik', term: 'Texik', pos: 'noun', status: 'confirmed', emoji: '🤬', meaning: 'Asshole', hint: 'Pejorative or anatomical noun (plural: texikon)', example: 'Ton itxu texikon.', notes: "X is pronounced 'sh'." },
  { id: 'ti', term: 'Ti', pos: 'adjective', status: 'confirmed', emoji: '🤏', meaning: 'Small', hint: 'Little in size (opposite of wo)', example: 'Ti ka. Ta ti silhbi zeteai blobber.', notes: 'Antonym of wo.' },
  { id: 'ton', term: 'Ton', pos: 'pronoun', status: 'confirmed', emoji: '🫵', meaning: 'You (plural)', hint: 'Addressing two or more people (takes -u verb ending)', example: 'Ëi skan ton! Ton itxu texikon.', notes: '2nd person plural pronoun.' },
  { id: 'tuk', term: 'Tuk', pos: 'adjective', status: 'confirmed', emoji: '👍', meaning: 'Good', hint: "Positive quality ('ni tuk' = bad, 'wo tuk' = very good)", example: 'Ëi skan tuk ezuki. Tuk wuke!', notes: 'Core positive adjective.' },
  { id: 'tumu', term: 'Tumu', pos: 'noun', status: 'confirmed', emoji: '🍽️', meaning: 'Food', hint: 'Anything edible (like blobber or blubur)', example: 'Lon kinai zetëi tumu.', notes: 'General noun for food.' },
  { id: 'u', term: 'U', pos: 'particle', status: 'confirmed', emoji: '🔗', meaning: 'And', hint: 'Coordinating conjunction linking words or clauses', example: 'Ëi skan wubu u ëi skan pan.', notes: "Standard 'and' particle." },
  { id: 'wa', term: 'Wa', pos: 'particle', status: 'confirmed', emoji: '❓', meaning: 'What', hint: "Question word triggering object fronting ('Wa ezuki te skan?')", example: 'Wa ezuki te skan?', notes: 'Attested in example #09.' },
  { id: 'we', term: 'We', pos: 'particle', status: 'confirmed', emoji: '🤔', meaning: 'Why', hint: "Question word asking reason ('We te skan ezuki?')", example: 'We te skan ezuki?', notes: 'Attested in examples #07 and #08.' },
  { id: 'wo', term: 'Wo', pos: 'adjective', status: 'confirmed', emoji: '🐘', meaning: 'Big / Amplifier (intensifies verbs, adverbs & adjectives)', hint: "Means 'big' on nouns ('wo ka', 'wo panke') and amplifies verbs ('wo skan' = love)", example: 'Te wo skan ezuki? Wo panke.', notes: 'Amplifier and size adjective.' },
  { id: 'wu', term: 'Wu', pos: 'particle', status: 'confirmed', emoji: '🧭', meaning: 'How', hint: "Question word used with copula Xai ('Wu xa te?')", example: 'Tamako, wu xa te? Wu xaai tëi panke?', notes: 'Attested in examples #13, #23, #44, #45.' },
  { id: 'wubu', term: 'Wubu', pos: 'noun', status: 'confirmed', emoji: '🌝', meaning: 'Moon', hint: 'Shines at night; wubu + eke = wuke', example: 'Wubu + eke = wuke.', notes: 'Root of wuke (night).' },
  { id: 'wuke', term: 'Wuke', pos: 'noun', status: 'confirmed', emoji: '🌙', meaning: 'Night time', hint: 'Compound of wubu (moon) + eke (sky)', example: 'Taki! Tuk wuke!', notes: 'Attested in examples #12, #20, #22, #44.' },
  { id: 'xai', term: 'Xai', pos: 'particle', status: 'confirmed', emoji: '🟰', meaning: 'To be', hint: 'Copula linking subject to state (Ëi/Te=xa, Ton=xau, Na=xaami, La/Lon=xaai)', example: 'Ta panke xaai tuk. Ëi xa tëi skantar.', notes: 'Tagged as particle in Dictionary list and conjugated in Verb Tables.' },
  { id: 'zetei', term: 'Zetëi', pos: 'verb', status: 'confirmed', emoji: '🍴', meaning: 'To eat', hint: 'Consuming tumu; infinitive has Ë, stem removes -i and changes ë to e (zete)', example: 'Na zeteami blubur u blobber.', notes: 'Conjugates: Ëi/Te=zete, Ton=zeteu, Na=zeteami, La/Lon=zeteai.' },
  { id: 'zo', term: 'Zo', pos: 'particle', status: 'confirmed', emoji: '⚖️', meaning: 'Or', hint: 'Choice particle between alternatives', example: 'Te skan peke zo liron?', notes: "Disjunctive particle 'or'." },
  { id: 'zorpe-semen', term: 'Zorpe-semen', pos: 'noun', status: 'confirmed', emoji: '❔', meaning: 'A question-word / a word that asks', hint: 'Compound of zorpe (ask) + semen (word): Wa, We, Wu', example: "'Wa', 'We', u 'Wu' xaai zorpe-semen.", notes: "Literally 'asking-word'." },
  { id: 'zorpei', term: 'Zorpëi', pos: 'verb', status: 'confirmed', emoji: '🙋‍♂️', meaning: 'To ask / To question', hint: 'Inquiring or posing a question', example: 'Ëi kin zorpëi te: te pon seamenese?', notes: 'Conjugates: Ëi/Te=Zorpe, Ton=Zorpeu, Na=Zorpeami, La/Lon=Zorpeai.' },
];

const BASELINE_CONJUGATIONS = [
  { infinitive: 'Beti', meaning: 'To wait', type: 'regular', ei: 'Bet', te: 'Bet', ton: 'Betu', na: 'Betami', la: 'Betai', lon: 'Betai', note: "Conjugated stem 'bet' follows particles like 'ap' ('ap bet')." },
  { infinitive: 'Itxi', meaning: 'To kiss', type: 'regular', ei: 'Itx', te: 'Itx', ton: 'Itxu', na: 'Itxami', la: 'Itxai', lon: 'Itxai', note: "Attested in 'Ton itxu texikon' and 'Ëi wo skan itxi tëi texik'." },
  { infinitive: 'Faki', meaning: 'To fuck', type: 'regular', ei: 'Fak', te: 'Fak', ton: 'Faku', na: 'Fakami', la: 'Fakai', lon: 'Fakai', note: 'Regular conjugation; La and Lon both take -ai.' },
  { infinitive: 'Skani', meaning: "To like ('wo skan' = love)", type: 'regular', ei: 'Skan', te: 'Skan', ton: 'Skanu', na: 'Skanami', la: 'Skanai', lon: 'Skanai', note: "Amplified with 'wo': 'Ëi wo skan' = I love." },
  { infinitive: 'Kixixi', meaning: 'To shut up', type: 'regular', ei: 'Kixix', te: 'Kixix', ton: 'Kixixu', na: 'Kixixami', la: 'Kixixai', lon: 'Kixixai', note: "All X's are pronounced 'sh'." },
  { infinitive: 'Nimi', meaning: 'To thank', type: 'regular', ei: 'Nim', te: 'Nim', ton: 'Nimu', na: 'Nimami', la: 'Nimai', lon: 'Nimai', note: 'Regular conjugation.' },
  { infinitive: 'Xai', meaning: 'To be (copula)', type: 'copula', ei: 'Xa', te: 'Xa', ton: 'Xau', na: 'Xaami', la: 'Xaai', lon: 'Xaai', note: "3rd person singular/plural and noun subjects take 'xaai'." },
  { infinitive: 'Obi', meaning: 'To give', type: 'regular', ei: 'Ob', te: 'Ob', ton: 'Obu', na: 'Obami', la: 'Obai', lon: 'Obai', note: 'Regular conjugation.' },
  { infinitive: 'Jai', meaning: 'To do / To make', type: 'regular', ei: 'Ja', te: 'Ja', ton: 'Jau', na: 'Jaami', la: 'Jaai', lon: 'Jaai', note: "Root of 'jai-semen' (verb / action-word)." },
  { infinitive: 'Sarpi', meaning: 'To sleep', type: 'regular', ei: 'Sarp', te: 'Sarp', ton: 'Sarpu', na: 'Sarpami', la: 'Sarpai', lon: 'Sarpai', note: "Distinct from adjective 'sarpo' (tired/sleepy)." },
  { infinitive: 'Ragoi', meaning: 'To agree / vibe', type: 'regular', ei: 'Rago', te: 'Rago', ton: 'Ragou', na: 'Ragoami', la: 'Ragoai', lon: 'Ragoai', note: "Used standalone as 'Rago!' (Agreed!)." },
  { infinitive: 'Kini', meaning: 'To need', type: 'regular', ei: 'Kin', te: 'Kin', ton: 'Kinu', na: 'Kinami', la: 'Kinai', lon: 'Kinai', note: 'When followed by a second verb, only Kini conjugates.' },
  { infinitive: 'Kiniwai', meaning: 'To explain', type: 'irregular', ei: 'Kiniwa', te: 'Kiniwa', ton: 'Kiniwau', na: 'Kiniwaami', la: 'Kiniwaai', lon: 'Kiniwaai', note: "Retains stem vowel '-a' across all persons." },
  { infinitive: 'Poni', meaning: 'To understand', type: 'regular', ei: 'Pon', te: 'Pon', ton: 'Ponu', na: 'Ponami', la: 'Ponai', lon: 'Ponai', note: 'Means to understand (whereas Pansai = to think).' },
  { infinitive: 'Mikëi', meaning: 'To rain', type: 'weather', ei: 'Mike', te: 'Mike', ton: 'Mikeu', na: 'Mikeami', la: 'Mikeai', lon: 'Mikeai', note: "Infinitive has Ë ('Mikëi'); conjugated stems use plain 'e'." },
  { infinitive: 'Obisëi', meaning: 'To speak / vocalize', type: 'regular', ei: 'Obise', te: 'Obise', ton: 'Obiseu', na: 'Obiseami', la: 'Obiseai', lon: 'Obiseai', note: "'Obisëi ezuki' = To sing." },
  { infinitive: 'Zetëi', meaning: 'To eat', type: 'regular', ei: 'zete', te: 'zete', ton: 'zeteu', na: 'zeteami', la: 'zeteai', lon: 'zeteai', note: "Stem 'zete-' removes final '-i' and changes 'ë' to 'e'." },
  { infinitive: 'Pansai', meaning: 'To think / ponder', type: 'regular', ei: 'Pansa', te: 'Pansa', ton: 'Pansau', na: 'Pansaami', la: 'Pansaai', lon: 'Pansaai', note: "Retains stem '-a' before suffixes." },
  { infinitive: 'Zorpëi', meaning: 'To ask / question', type: 'regular', ei: 'Zorpe', te: 'Zorpe', ton: 'Zorpeu', na: 'Zorpeami', la: 'Zorpeai', lon: 'Zorpeai', note: "Root of 'zorpe-semen' (question-word)." },
  { infinitive: 'Sanai', meaning: 'To cycle / repeat / loop', type: 'irregular', ei: 'Sana', te: 'Sana', ton: 'Sanau', na: 'Sanaami', la: 'Sanaai', lon: 'Sanaai', note: "Retains stem vowel '-a' across all persons (sana, sanau, sanaami, sanaai)." },
];

const BASELINE_SCENES = [
  {
    id: 'scene_1',
    title: 'Scene 1: Greeting Multiple Friends (Plurals & Possessives)',
    scene: "☀️ Daytime (Panke). You walk up to two of your friends (buanon) and want to say: 'Hello my friends! How is your (plural) day?'",
    speaker: 'Narrator',
    seamenese: 'Ton xau ta buanon! Ëi kin obisëi ton...',
    gloss: 'You all are my friends! I need to speak to you all...',
    options: [
      { reply: 'Tamako ta buanon. Wu xaai tëion panke?', correct: true, feedback: "Wo tuk! 'Buana' drops '-a' + '-on' → 'buanon' (friends), 'tëion' is 2nd-person plural possessive ('your all's'), and 'panke' (3rd-person singular noun) takes 'xaai' (Corpus #13)." },
      { reply: 'Tamako ta buanaon. Wu xaai tëion panke?', correct: false, feedback: "Spelling trap! The plural rule is <noun> minus the last vowel + 'on'. Drop the final '-a' in 'buana' to get 'buanon', not 'buanaon'." },
      { reply: 'Tamako ëi buanon. Wu xaai tëi panke?', correct: false, feedback: "Grammar trap! 'Ëi' is never possessive ('Ta' = my), and since you are addressing multiple friends, you must use plural 'tëion' instead of singular 'tëi'." },
      { reply: 'Tamako ta buanon. Wu xau tëion panke?', correct: false, feedback: "Conjugation trap! Even though 'tëion' is plural ('your'), the subject of the verb is 'panke' (day, 3rd person singular), so the copula must be 'xaai', not 'xau'." },
    ],
  },
  {
    id: 'scene_2',
    title: 'Scene 2: Singing Good Music (Multi-Verb Chaining Rule)',
    scene: "🎵 Your friend asks what you like to do. You want to say: 'I like to sing good music!'",
    speaker: 'Buana',
    seamenese: 'Tamako! Wa te skan jai etpanke?',
    gloss: 'Hello! What do you like to do today?',
    options: [
      { reply: 'Ëi skan obisëi tuk ezuki!', correct: true, feedback: "Wo tuk! In Seamenese, 'to sing' is 'obisëi ezuki', and when two verbs refer to the same subject ('Ëi'), ONLY the first verb conjugates ('skan') while the second stays in the infinitive ('obisëi') with the diaeresis 'ë'!" },
      { reply: 'Ëi skan obise tuk ezuki!', correct: false, feedback: "Multi-verb grammar trap! When two verbs share one subject, only the first verb ('skan') conjugates. The second verb must remain in the infinitive ('obisëi'), not conjugated ('obise')." },
      { reply: 'Ëi skani obisëi tuk ezuki!', correct: false, feedback: "Conjugation trap! The first verb must be conjugated for 'Ëi' ('skan'), not left in the infinitive ('skani')." },
      { reply: 'Ëi skan obisei tuk ezuki!', correct: false, feedback: "Spelling trap! In Seamenese, E followed by I always takes a diaeresis: 'obisëi', never 'obisei'." },
    ],
  },
  {
    id: 'scene_3',
    title: 'Scene 3: The Three Colors (Brux, Liron, Peke)',
    scene: '🎨 Your friend points to the blue sky (eke), a green salad (blubur), and red food (tumu) and asks you to describe their colors.',
    speaker: 'Buana',
    seamenese: 'Beto kiniwa ëi: wu xaai eke, blubur, u et tumu?',
    gloss: 'Please explain to me: how are the sky, the salad, and this food?',
    options: [
      { reply: 'Eke xaai peke, blubur xaai liron, u et tumu xaai brux.', correct: true, feedback: "Wo tuk! 'Peke' = Blue (sky), 'Liron' = Green (salad), 'Brux' = Red, and 3rd-person singular nouns take the copula 'xaai'." },
      { reply: 'Eke xaai liron, blubur xaai brux, u et tumu xaai peke.', correct: false, feedback: "Vocabulary mix-up! 'Peke' is blue (for 'eke' sky) and 'liron' is green (for 'blubur' salad)." },
      { reply: 'Eke xa peke, blubur xa liron, u et tumu xa brux.', correct: false, feedback: "Grammar trap! 'Xa' is only for 'Ëi' (I) and 'Te' (You sg.). Third-person nouns like 'eke', 'blubur', and 'tumu' require 'xaai'." },
      { reply: 'Eke xaai peke, blubur xaai liron, u etpanke tumu xaai brux.', correct: false, feedback: "Word choice trap! 'Et' means 'this' ('et tumu' = this food), whereas 'etpanke' means 'today'." },
    ],
  },
  {
    id: 'scene_4',
    title: 'Scene 4: Counting Feet & Cats (Applying the Plural Rule)',
    scene: "🦶🐱 Your friend is practicing the Seamenese plural rule (<noun> -last vowel + on) and asks how to say '1 foot, 2 feet, and 2 small cats'.",
    speaker: 'Buana',
    seamenese: 'Te pon nai lulu semenon? Kiniwa: 1 sobo, 2 ___ u 2 ti ___!',
    gloss: 'Do you understand our new words? Explain: 1 foot, 2 [feet] and 2 small [cats]!',
    options: [
      { reply: '1 sobo, 2 sobon, u 2 ti silhbon!', correct: true, feedback: "Wo tuk! Both 'sobo' (drop -o + on → sobon) and 'silhbi' (drop -i + on → silhbon) follow the exact plural rule." },
      { reply: '1 sobo, 2 soboon, u 2 ti silhbion!', correct: false, feedback: "Plural spelling trap! You must remove the final vowel ('-o' and '-i') before adding '-on': 'sobon' and 'silhbon'." },
      { reply: '1 sobo, 2 sobon, u 2 tion silhbon!', correct: false, feedback: "Grammar trap! Adjectives like 'ti' (small) do not take the noun plural suffix '-on'; only the noun ('silhbon') pluralizes." },
      { reply: '1 sobo, 2 sobos, u 2 ti silhbis!', correct: false, feedback: "Seamenese never uses '-s' for plurals! Drop the last vowel and add '-on' ('sobon', 'silhbon')." },
    ],
  },
  {
    id: 'scene_5',
    title: 'Scene 5: Clarifying Relationship Status (Buana vs. Skantar)',
    scene: "💘 Luke thinks you are just his friend (buana), but you want to tell him: 'Luke, I am not your friend. I am your crush/lover.'",
    speaker: 'Luke',
    seamenese: 'Tamako! Te xa ta wo tuk buana, ha?',
    gloss: 'Hello! You are my very good friend, yes?',
    options: [
      { reply: 'Luke, ëi ni xa tëi buana. Ëi xa tëi skantar.', correct: true, feedback: "Wo tuk! Exact match for Corpus #37: 'ni' precedes the conjugated copula 'xa', and 'tëi' (with diaeresis ë) means 'your' (singular)." },
      { reply: 'Luke, ëi ni xau tëi buana. Ëi xa tëi skantar.', correct: false, feedback: "Conjugation trap! 'Xau' is only for 'Ton' (you plural); with 'Ëi' (I), use 'xa'." },
      { reply: 'Luke, ëi ni xa te buana. Ëi xa te skantar.', correct: false, feedback: "Pronoun trap! 'Te' is the subject/object pronoun ('you'). Before a noun, you must use the possessive pronoun 'tëi' ('your')." },
      { reply: 'Luke, ëi ni xaai tëi buana. Ëi xaai tëi skantar.', correct: false, feedback: "Conjugation trap! With 'Ëi' (I), the copula Xai conjugates to 'xa', never 'xaai'." },
    ],
  },
  {
    id: 'scene_6',
    title: 'Scene 6: What Music Do You Like? (Wa Object Fronting vs. We SVO)',
    scene: "❓ You want to ask your friend two questions: 'What music do you like?' and 'Why do you like music?'",
    speaker: 'Buana',
    seamenese: 'Ëi wo skan ezuki! Te kin zorpëi ëi?',
    gloss: 'I really love music! Do you need to ask me?',
    options: [
      { reply: 'Wa ezuki te skan? U we te skan ezuki?', correct: true, feedback: "Wo tuk! Matches Corpus #08 & #09: 'Wa' fronts the questioned object ('Wa ezuki te skan?'), while 'We' (why) keeps standard Subject-Verb-Object order ('We te skan ezuki?')." },
      { reply: 'Wa te skan ezuki? U we ezuki te skan?', correct: false, feedback: "Interrogative syntax trap! 'Wa ezuki' (what music) fronts the noun phrase before 'te skan', whereas 'We' (why) is followed directly by 'te skan ezuki'." },
      { reply: 'Wu ezuki te skan? U wa te skan ezuki?', correct: false, feedback: "Wrong question words! 'Wu' means 'How', not 'What' ('Wa') or 'Why' ('We')." },
      { reply: 'Wa ezuki te skani? U we te skani ezuki?', correct: false, feedback: "Conjugation trap! For 'Te' (you sg.), conjugate 'skani' to its bare stem 'skan'." },
    ],
  },
  {
    id: 'scene_7',
    title: 'Scene 7: A Long, Tough Day & Rainy Sky (Wo Panke & Mikëi)',
    scene: "🌧️ It has been a big/tough day ('wo panke'), the sky is raining, and you are sleepy. Express this accurately.",
    speaker: 'Buana',
    seamenese: 'Jorn, wu xaai tëi panke? Te xa lipo?',
    gloss: 'Jorn, how is your day? Are you energetic?',
    options: [
      { reply: 'Ni, etpanke xaai wo panke! Eke mikeai u ëi xa sarpo.', correct: true, feedback: "Wo tuk! 'Wo panke' = big/tough day (Corpus #02), 'Eke mikeai' drops the diaeresis in the 3sg conjugated form of 'Mikëi', and 'ëi xa sarpo' uses the adjective 'sarpo'." },
      { reply: 'Ni, etpanke xaai wo panke! Eke mikëai u ëi xa sarpo.', correct: false, feedback: "Orthography trap! 'Ë' only occurs when E is directly followed by I ('Mikëi'). In 'mikeai', E is followed by A, so it is spelled 'mikeai', not 'mikëai'!" },
      { reply: 'Ni, etpanke xaai wo panke! Eke mikeai u ëi xa sarpi.', correct: false, feedback: "POS trap! 'Sarpi' is the verb 'to sleep'. After the copula 'ëi xa' (I am), use the adjective 'sarpo' (sleepy/tired)." },
      { reply: 'Ni, etpanke xa wo panke! Eke mike u ëi xaai sarpo.', correct: false, feedback: "Conjugation trap! 'Etpanke' and 'Eke' are 3rd-person singular nouns requiring '-ai' ('xaai', 'mikeai'), while 'Ëi' takes 'xa'." },
    ],
  },
  {
    id: 'scene_8',
    title: "Scene 8: Agreeing with 'San' (Me Too / Also)",
    scene: "➕ Blui likes your friend's music, and you want to say: 'Blui likes your music — I also like your music!'",
    speaker: 'Buana',
    seamenese: "Te skan ta ezuki 'semen demen'?",
    gloss: "Do you like my music 'semen demen'?",
    options: [
      { reply: 'Ha! Blui skanai tëi ezuki - ëi san skan tëi ezuki!', correct: true, feedback: "Wo tuk! Matches Corpus #28 word-for-word: 3rd-person 'Blui skanai', 1st-person 'ëi san skan', and singular possessive 'tëi ezuki'." },
      { reply: 'Ha! Blui skan tëi ezuki - ëi san skanai tëi ezuki!', correct: false, feedback: "Inverted conjugations! 'Blui' (La, 3rd person) takes 'skanai', while 'Ëi' (1st person) takes bare stem 'skan'." },
      { reply: 'Ha! Blui skanai tei ezuki - ei san skan tei ezuki!', correct: false, feedback: "Spelling trap! Remember the pronunciation rule: E followed by I always makes Ë ('tëi', 'ëi')." },
      { reply: 'Ha! Blui skanai tëi ezuki - ta san skan tëi ezuki!', correct: false, feedback: "Pronoun trap! 'Ta' means 'my' (possessive). The subject pronoun 'I' is 'Ëi' ('ëi san skan')." },
    ],
  },
  {
    id: 'scene_9',
    title: 'Scene 9: Feeding the Cats Together (1st Pl -ami & 3rd Pl -ai)',
    scene: "🍽️ You and your friend ('Na') are eating green salad, while the cats ('Lon') need to eat fish.",
    speaker: 'Buana',
    seamenese: 'Wa na zeteami etpanke? U wa silhbon kinai?',
    gloss: 'What do we eat today? And what do the cats need?',
    options: [
      { reply: 'Na zeteami liron blubur, u lon kinai zetëi blobber.', correct: true, feedback: "Wo tuk! 'Na' takes '-ami' ('zeteami'), 'lon' takes '-ai' on the first verb ('kinai'), and the second verb in the chain stays in the infinitive ('zetëi')." },
      { reply: 'Na zeteami liron blubur, u lon kinai zeteai blobber.', correct: false, feedback: "Multi-verb trap! In 'lon kinai zetëi blobber', only the first verb ('kinai') conjugates; the second verb must stay infinitive ('zetëi')." },
      { reply: 'Na zeteu liron blubur, u lon kinu zetëi blobber.', correct: false, feedback: "Conjugation trap! '-u' is for 'Ton' (you plural). 'Na' (we) takes '-ami' and 'Lon' (they) takes '-ai'." },
      { reply: 'Nai zeteami liron blubur, u laion kinai zetëi blobber.', correct: false, feedback: "Pronoun trap! 'Nai' (our) and 'Laion' (their) are possessives; use subject pronouns 'Na' (we) and 'Lon' (they)." },
    ],
  },
  {
    id: 'scene_10',
    title: 'Scene 10: Explaining Hani & Compound Words (Kiniwai Irregular Stem)',
    scene: "💡 A learner asks you to explain the word 'hani'. Reply: 'I explain! Hani is ha + ni. Do you all understand my explanation?'",
    speaker: 'Buana',
    seamenese: "Ap bet! Ëi ni pon 'hani'. Te kiniwa?",
    gloss: "But wait! I don't understand 'hani'. Do you explain?",
    options: [
      { reply: 'Ëi kiniwa! Hani xaai ha + ni. Ton ponu ta kiniwako?', correct: true, feedback: "Wo tuk! 'Kiniwai' retains its '-a' stem ('Ëi kiniwa'), 'Hani xaai ha + ni' is Corpus #26, and 'Ton' (you plural) takes '-u' ('Ton ponu')." },
      { reply: 'Ëi kiniw! Hani xaai ha + ni. Ton ponu ta kiniwako?', correct: false, feedback: "Irregular verb trap! 'Kiniwai' keeps its '-a' vowel in the stem: 'Ëi kiniwa', never 'Ëi kiniw'." },
      { reply: 'Ëi kiniwa! Hani xa ha + ni. Ton pon ta kiniwako?', correct: false, feedback: "Conjugation trap! 'Hani' (3sg noun/concept) requires 'xaai', and 'Ton' (you pl.) requires 'ponu' with '-u'." },
      { reply: 'Ëi kiniwa! Hani xaai ha + ni. Ton ponu ëi kiniwako?', correct: false, feedback: "Possessive trap! Never use 'Ëi' for 'my'; use 'Ta kiniwako' (my explanation)." },
    ],
  },
  {
    id: 'scene_11',
    title: 'Scene 11: Thinking vs. Understanding (Pansai vs. Poni)',
    scene: "💭 You want to say: 'She thinks Seamenese is very good, and we agree!'",
    speaker: 'Buana',
    seamenese: 'Wa la pansaai? U ton ragou?',
    gloss: 'What does she think? And do you all agree?',
    options: [
      { reply: 'La pansaai seamenese xaai wo tuk, u na ragoami!', correct: true, feedback: "Wo tuk! 'Pansai' retains its stem '-a' + '-ai' → 'La pansaai', 'seamenese' takes 'xaai', and 'Na' takes 'ragoami'." },
      { reply: 'La pansai seamenese xaai wo tuk, u na ragoami!', correct: false, feedback: "Stem trap! The infinitive is 'Pansai' (stem 'Pansa-'), so adding 3rd-person '-ai' produces double-a: 'La pansaai', not 'pansai'!" },
      { reply: 'La ponai seamenese xaai wo tuk, u na ragoami!', correct: false, feedback: "Lexical trap! 'Poni' means 'to understand'. For 'to think/ponder', use 'Pansai' ('La pansaai')." },
      { reply: 'Lai pansaai seamenese xaai wo tuk, u na ragoami!', correct: false, feedback: "Pronoun trap! 'Lai' means 'his/her' (possessive). Use subject pronoun 'La' (he/she)." },
    ],
  },
  {
    id: 'scene_12',
    title: 'Scene 12: Two Lovers Celebrating at Night (Plural Nouns & Daka)',
    scene: "🍻 Vgaggia and Small are crushes/lovers ('skantaron'). Raise a glass and say: 'Cheers! Vgaggia and Small are lovers, and tonight they kiss!'",
    speaker: 'Buana',
    seamenese: 'Daka! Wa Vgaggia u Small jaai etwuke?',
    gloss: 'Cheers! What are Vgaggia and Small doing tonight?',
    options: [
      { reply: 'Daka! Vgaggia u Small xaai skantaron, u etwuke lon itxai!', correct: true, feedback: "Wo tuk! Matches Corpus #39 ('Vgaggia u Small xaai skantaron'), and 3rd-person plural 'lon' takes '-ai' ('lon itxai')." },
      { reply: 'Daka! Vgaggia u Small xau skantaron, u etwuke lon itxu!', correct: false, feedback: "Conjugation trap! '-u' ('xau', 'itxu') is only for 'Ton' (you plural). 3rd-person plural ('Vgaggia u Small' / 'lon') takes '-ai' ('xaai', 'itxai')." },
      { reply: 'Daka! Vgaggia u Small xaai skantar, u etwuke la itxai!', correct: false, feedback: "Number trap! Two people are plural: use plural noun 'skantaron' and plural pronoun 'lon' (they), not 'la' (he/she)." },
      { reply: 'Daka! Vgaggia u Small xaai skantaron, u etpanke lon itxai!', correct: false, feedback: "Time trap! The scene is tonight ('etwuke'), not today ('etpanke')." },
    ],
  },
  {
    id: 'scene_13',
    title: 'Scene 13: Dealing with a Loud Texik (Imperatives & Insults)',
    scene: "🤬 Someone is being rude while your friends are singing. Tell them: 'You are an asshole! Shut up! We need to sing our music!'",
    speaker: 'Texik',
    seamenese: 'Tëion ezuki xaai ni tuk! Ëi ni skan!',
    gloss: "Your (plural) music is bad! I don't like it!",
    options: [
      { reply: 'Te xa texik! Kixix! Na kinami obisëi nai ezuki!', correct: true, feedback: "Wo tuk! 'Te xa texik' (You are an asshole), bare stem imperative 'Kixix!' (Shut up!), 'Na kinami obisëi' (first verb conjugated, second infinitive), and 'nai' (our)." },
      { reply: 'Te xa texik! Kixixi! Na kinami obiseami nai ezuki!', correct: false, feedback: "Double trap! Commands for 'Te' use the bare stem ('Kixix!', not 'Kixixi!'), and in 'kinami obisëi', the second verb must stay infinitive ('obisëi')." },
      { reply: 'Te xaai texik! Kixix! Na kinami obisëi na ezuki!', correct: false, feedback: "Grammar trap! 'Te' takes 'xa' (not 'xaai'), and 'our music' requires possessive 'nai ezuki' (not 'na ezuki')." },
      { reply: 'Tëi xa texik! Kixix! Na kinami obisëi nai ezuki!', correct: false, feedback: "Pronoun trap! 'Tëi' is possessive ('your'). 'You are' is 'Te xa'." },
    ],
  },
  {
    id: 'scene_14',
    title: 'Scene 14: Sun, Moon, and Compound Time Words',
    scene: "☀️🌝 Explain how 'day' and 'night' are formed in Seamenese: 'I like the moon and I like the sun. Moon + sky = night, and sun + sky = day!'",
    speaker: 'Buana',
    seamenese: "Kiniwa 'panke' u 'wuke', beto!",
    gloss: "Explain 'panke' and 'wuke', please!",
    options: [
      { reply: 'Ëi skan wubu u ëi skan pan. Wubu + eke = wuke, u pan + eke = panke!', correct: true, feedback: 'Wo tuk! Matches Corpus #20, #21, and #40: Wubu (moon) + eke (sky) = wuke (night), Pan (sun) + eke (sky) = panke (day).' },
      { reply: 'Ëi skan wubu u ëi skan pan. Pan + eke = wuke, u wubu + eke = panke!', correct: false, feedback: "Root mix-up! 'Pan' is sun (panke = day) and 'Wubu' is moon (wuke = night), not the other way around." },
      { reply: 'Ëi skanai wubu u ëi skanai pan. Wubu + eke = wuke, u pan + eke = panke!', correct: false, feedback: "Conjugation trap! With 'Ëi', use the bare stem 'skan', not 3rd-person 'skanai'." },
      { reply: 'Ëi skan wubu u ëi skan pan. Wubu + peke = wuke, u pan + peke = panke!', correct: false, feedback: "Word trap! 'Eke' means sky; 'peke' is the adjective blue." },
    ],
  },
  {
    id: 'scene_15',
    title: 'Scene 15: Spice & Bold Corpus Expressions (Amplifier Wo + Chaining)',
    scene: "🔥 Your partner is testing your mastery of the spicy Corpus #33 & #42 grammar structures: 'I really love kissing your asshole, and I don't like big penises.'",
    speaker: 'Skantar',
    seamenese: 'Wa te wo skan jai etwuke, ta skantar?',
    gloss: 'What do you really love to do tonight, my lover?',
    options: [
      { reply: 'Ëi wo skan itxi tëi texik, u ëi ni skan wo ka!', correct: true, feedback: "Wo tuk! Combines Corpus #33 ('Ëi wo skan itxi tëi texik' — amplifier 'wo' + conjugated 'skan' + infinitive 'itxi') and Corpus #34 ('Ëi ni skan wo ka')." },
      { reply: 'Ëi wo skan itx tëi texik, u ëi ni skan wo ka!', correct: false, feedback: "Multi-verb trap! Since 'skan' is already conjugated for 'Ëi', the second verb 'itxi' (to kiss) must stay in the infinitive ('itxi', not 'itx')." },
      { reply: 'Ëi skan wo itxi tëi texik, u ëi skan ni wo ka!', correct: false, feedback: "Modifier placement trap! Both the amplifier 'wo' ('wo skan' = love) and negation 'ni' ('ni skan' = don't like) must go BEFORE 'skan'." },
      { reply: 'Ëi wo skan itxi te texik, u ëi ni skan ti ka!', correct: false, feedback: "Trap! Use possessive 'tëi texik' (not 'te'), and 'big' is 'wo' ('ti' means small)." },
    ],
  },
  {
    id: 'scene_16',
    title: 'Scene 16: Saying Goodnight & Thanking Your Friends',
    scene: "🌙 The night ('wuke') is ending. Thank all your friends ('ton') for their gifts, wish them a good night, and say goodbye!",
    speaker: 'Buanon',
    seamenese: 'Na obami te brux u peke tumu! Wu xaai tëi wuke?',
    gloss: 'We give you red and blue food! How is your night?',
    options: [
      { reply: 'Ëi nim ton, ta buanon! Ta wuke xaai wo tuk. Taki! Tuk wuke!', correct: true, feedback: "Wo tuk! 'Ëi nim ton' (I thank you all), 'ta buanon' (my friends), 'ta wuke xaai wo tuk' (my night is very good), and 'Taki! Tuk wuke!' (Bye! Good night! — Corpus #12)." },
      { reply: 'Ëi nimu ton, ta buanon! Ta wuke xaai wo tuk. Taki! Tuk wuke!', correct: false, feedback: "Subject-verb agreement trap! The verb agrees with the SUBJECT ('Ëi' → 'nim'), not the object ('ton')." },
      { reply: 'Ëi nim ton, ta buanon! Ta wuke xa wo tuk. Tamako! Tuk wuke!', correct: false, feedback: "Double trap! 'Ta wuke' (3sg noun) takes 'xaai' (not 'xa'), and 'Tamako' means Hello ('Taki' = Bye)." },
      { reply: 'Ëi nimi ton, ta buana! Ta wuke xaai wo tuk. Taki! Tuk panke!', correct: false, feedback: "Trap! Conjugate 'nimi' to 'nim', pluralize 'buanon', and 'good night' is 'tuk wuke' ('panke' = day)." },
    ],
  },
];

const BASELINE_CORPUS_45 = [
  { num: '01', sea: 'Tamako ta buanon', eng: 'Hello my friends' },
  { num: '02', sea: 'Wo panke', eng: 'Big day (😔 — long/tough day)' },
  { num: '03', sea: 'Ëi skan tuk ezuki', eng: 'I like good music' },
  { num: '04', sea: 'Tuk ezuki', eng: 'Good music (✅🎵)' },
  { num: '05', sea: 'Ni tuk ezuki', eng: 'Not good music / bad music (❌🎵)' },
  { num: '06', sea: 'Te skan ezuki?', eng: 'Do you like music?' },
  { num: '07', sea: 'we te skan?', eng: 'Why do you like [it]?' },
  { num: '08', sea: 'we te skan ezuki?', eng: 'Why do you like music?' },
  { num: '09', sea: 'wa ezuki te skan?', eng: 'What music do you like?' },
  { num: '10', sea: 'ti ka', eng: 'Small penis (🤏🍆)' },
  { num: '11', sea: 'wo ka', eng: 'Big penis (not small 🍆)' },
  { num: '12', sea: 'Taki! Tuk wuke', eng: 'Bye! Good night' },
  { num: '13', sea: 'Tamako ta buanon. Wu xaai tëion panke?', eng: 'Hello my friends. How is your (plural) day?' },
  { num: '14', sea: 'Ha, ëi skan!', eng: 'Yes, I like [it]!' },
  { num: '15', sea: 'Ëi skan ezuki', eng: 'I like music' },
  { num: '16', sea: 'Ëi skan ka', eng: 'I like penis' },
  { num: '17', sea: 'Te wo skan ka?', eng: 'Do you really like penis? / Do you love penis?' },
  { num: '18', sea: 'te wo skan ezuki?', eng: 'Do you really like music? / Do you love music?' },
  { num: '19', sea: 'Te skan ta ezuki "semen demen"', eng: "You like my music '[track called] semen demen'" },
  { num: '20', sea: 'Wubu + eke = wuke', eng: 'Moon + sky = night (compound formation)' },
  { num: '21', sea: 'Pan + eke = panke', eng: 'Sun + sky = day (compound formation)' },
  { num: '22', sea: 'Tuk wuke', eng: 'Good night' },
  { num: '23', sea: 'Jorn, wu xaai tëi panke?', eng: 'Jorn, how is your day?' },
  { num: '24', sea: 'ta panke xaai tuk', eng: 'My day is good' },
  { num: '25', sea: 'tëi panke xaai tuk?', eng: 'Is your day good?' },
  { num: '26', sea: 'hani xaai ha + ni', eng: 'Hani is ha + ni (maybe = yes + no)' },
  { num: '27', sea: 'Jorn skanai blui - ëi san', eng: 'Jorn likes Blui — I also (do) / me too' },
  { num: '28', sea: 'Blui skanai tëi ezuki - ëi san skan tëi ezuki', eng: 'Blui likes your music — I also like your music' },
  { num: '29', sea: 'Luke san skanai Jorn ezuki?', eng: "Does Luke also like Jorn's music?" },
  { num: '30', sea: 'Ëi skan ezuki. Ëi san skan tëi texik', eng: 'I like music. I also like your asshole.' },
  { num: '31', sea: 'ëi skan tëi texik', eng: 'I like your asshole' },
  { num: '32', sea: 'ëi skan san tëi ka', eng: 'I also like your penis' },
  { num: '33', sea: 'Ëi wo skan itxi tëi texik', eng: 'I really love kissing your asshole' },
  { num: '34', sea: 'Ëi ni skan wo ka', eng: "I don't like big penises" },
  { num: '35', sea: 'ton itxu texikon', eng: 'You (plural) kiss assholes' },
  { num: '36', sea: 'Ëi skan ton', eng: 'I like you (plural / you all)' },
  { num: '37', sea: 'Luke, ëi ni xa tëi buana. Ëi xa tëi skantar', eng: 'Luke, I am not your friend. I am your crush/lover.' },
  { num: '38', sea: 'Vgaggia xaai Small skantar', eng: "Vgaggia is Small's crush/lover" },
  { num: '39', sea: 'Vgaggia u Small xaai skantaron', eng: 'Vgaggia and Small are crushes/lovers' },
  { num: '40', sea: 'Ëi skan wubu u ëi skan pan', eng: 'I like the moon and I like the sun' },
  { num: '41', sea: 'Te xa ta buana, Jorn', eng: 'You are my friend, Jorn' },
  { num: '42', sea: 'Ëi skan itxi tëi texik u ëi skan san faki tëi wo ka', eng: 'I like to kiss your asshole and I also like to fuck your big penis' },
  { num: '43', sea: 'Na skanami ezuki', eng: 'We like music' },
  { num: '44', sea: 'wu xaai tëi wuke?', eng: 'How is your night?' },
  { num: '45', sea: 'Tamako, wu xa te?', eng: 'Hello, how are you?' },
];

const CANONICAL_COMPOUNDS = [
  { part1: 'Pan (Sun ☀️)', part2: 'Eke (Sky 🌌)', result: 'Panke', meaning: "Day ('sun-sky')" },
  { part1: 'Wubu (Moon 🌝)', part2: 'Eke (Sky 🌌)', result: 'Wuke', meaning: "Night time ('moon-sky')" },
  { part1: 'Et (This 👇)', part2: 'Panke (Day 🌤️)', result: 'Etpanke', meaning: "Today ('this-day')" },
  { part1: 'Et (This 👇)', part2: 'Wuke (Night 🌙)', result: 'Etwuke', meaning: "Tonight ('this-night')" },
  { part1: 'Ha (Yes ✅)', part2: 'Ni (No ❌)', result: 'Hani', meaning: "Maybe ('yes-no')" },
  { part1: 'Jai (To do 🛠️)', part2: 'Semen (Word 🔤)', result: 'Jai-semen', meaning: "Verb / Action-word ('do-word')" },
  { part1: 'Zorpe (To ask ❓)', part2: 'Semen (Word 🔤)', result: 'Zorpe-semen', meaning: "Question-word ('asking-word': Wa, We, Wu)" },
  { part1: 'Obisëi (To vocalize 🗣️)', part2: 'Ezuki (Music 🎵)', result: 'Obisëi ezuki', meaning: "To sing ('vocalize music')" },
  { part1: 'Wo (Amplifier 🐘)', part2: 'Skani (To like ❤️)', result: 'Wo skani', meaning: "To love ('Ëi wo skan' = I love)" },
  { part1: '<Noun> minus last vowel', part2: '-on (Plural suffix)', result: 'Sobon / Buanon / Skantaron', meaning: 'Plural nouns (Feet, Friends, Lovers)' },
];

const PRIORITY_TRAINER_CARDS = [
  // ---- TIER 1: Beginner (Foundational Words, Greetings & Copula xa/xaai) ----
  {
    id: 'type_foot',
    tier: 1,
    qType: 'typing',
    prompt: "Write 'Foot' (singular) in Seamenese",
    subHint: '🌱 Tier 1 · Anatomical noun (🦶)',
    accepted: ['sobo'],
    displayAnswer: 'Sobo',
    explanation: "'Sobo' means Foot (and 'Sobon' means Feet).",
  },
  {
    id: 'type_red',
    tier: 1,
    qType: 'typing',
    prompt: "Write 'Red' in Seamenese",
    subHint: '🌱 Tier 1 · Color adjective in the dictionary (🟥)',
    accepted: ['brux'],
    displayAnswer: 'Brux',
    explanation: "'Brux' is the Seamenese adjective for Red (pronounced 'brush').",
  },
  {
    id: 'mc_liron',
    tier: 1,
    qType: 'mc',
    prompt: "What is 'Liron'?",
    subHint: '🌱 Tier 1 · Think of the color of Blubur (salad) 🥗:',
    correctChoice: 'Green',
    choices: ['Green', 'Blue', 'Red', 'Tired'],
    displayAnswer: 'Green',
    explanation: "'Liron' means Green (Brux = Red, Peke = Blue).",
  },
  {
    id: 'type_blue',
    tier: 1,
    qType: 'typing',
    prompt: "Write 'Blue' in Seamenese",
    subHint: '🌱 Tier 1 · The color of the daytime Eke (sky) 🟦',
    accepted: ['peke'],
    displayAnswer: 'Peke',
    explanation: "'Peke' means Blue.",
  },
  {
    id: 'mc_copula_ei',
    tier: 1,
    qType: 'mc',
    prompt: 'Complete: "Ëi ___ mubu!" (I am happy!)',
    subHint: '🌱 Tier 1 · Which form of Xai (to be/have) goes with Ëi (I) and Te (You)?',
    correctChoice: 'xa',
    choices: ['xa', 'xaai', 'xau', 'xai'],
    displayAnswer: 'xa',
    explanation: "'Ëi' (I) and 'Te' (You sg.) take the bare stem 'xa' ('Ëi xa mubu').",
  },
  {
    id: 'mc_copula_la',
    tier: 1,
    qType: 'mc',
    prompt: 'Complete: "La ___ tuk!" (He/She/It is good!)',
    subHint: '🌱 Tier 1 · Which form of Xai goes with 3rd-person La (He/She/It)?',
    correctChoice: 'xaai',
    choices: ['xaai', 'xa', 'xaami', 'xau'],
    displayAnswer: 'xaai',
    explanation: "3rd-person 'La' and 'Lon' (plus singular/plural nouns) take '-ai' → 'xaai'.",
  },
  // ---- TIER 2: Intermediate (Plurals -on, Possessives, Negation & Plural Pronouns) ----
  {
    id: 'plural_feet',
    tier: 2,
    qType: 'typing',
    prompt: "Write 'Feet' (plural of Foot) in Seamenese",
    subHint: "🐟 Tier 2 · 'Foot' is Sobo. Apply the plural rule: <noun> minus last vowel + on!",
    accepted: ['sobon'],
    displayAnswer: 'Sobon',
    explanation: "'Sobo' = Foot. Drop the final vowel '-o' and add '-on' → 'Sobon' (1 sobo, 2 sobon).",
  },
  {
    id: 'type_friends',
    tier: 2,
    qType: 'typing',
    prompt: "Write 'Friends' (plural of Friend) in Seamenese",
    subHint: "🐟 Tier 2 · 'Friend' is Buana. Drop the last vowel and add -on!",
    accepted: ['buanon'],
    displayAnswer: 'Buanon',
    explanation: "1 buana (friend) → 2 buanon (friends), as in 'Tamako ta buanon'.",
  },
  {
    id: 'mc_skantar',
    tier: 2,
    qType: 'mc',
    prompt: "What is 'Skantar'?",
    subHint: '🐟 Tier 2 · Select the correct English meaning from the dictionary:',
    correctChoice: 'Crush / lover',
    choices: ['Crush / lover', 'Friend', 'Asshole', 'Explanation'],
    displayAnswer: 'Crush / lover',
    explanation: "'Skantar' means 'Crush / lover' (derived from skani = to like). Plural: Skantaron.",
  },
  {
    id: 'type_lovers_pl',
    tier: 2,
    qType: 'typing',
    prompt: "Write 'Crushes / Lovers' (plural of Skantar) in Seamenese",
    subHint: '🐟 Tier 2 · Plural rule: 1 skantar, 2 ___',
    accepted: ['skantaron'],
    displayAnswer: 'Skantaron',
    explanation: 'Skantaron is the plural of Skantar. Remember the plural rule: <noun> -last vowel +on',
  },
  {
    id: 'mc_et',
    tier: 2,
    qType: 'mc',
    prompt: "What is 'Et'?",
    subHint: '🐟 Tier 2 · Found at the start of Etpanke (today) and Etwuke (tonight):',
    correctChoice: 'This',
    choices: ['This', 'Sky', 'And', 'Our / Ours'],
    displayAnswer: 'This',
    explanation: "'Et' is the pronoun meaning 'This' (Et + panke = Today, Et + wuke = Tonight).",
  },
  {
    id: 'type_my_friend',
    tier: 2,
    qType: 'typing',
    prompt: "Write 'My friend' in Seamenese (2 words)",
    subHint: "🐟 Tier 2 · Possessive 'My' is Ta (never Ëi!), and 'Friend' is Buana.",
    accepted: ['ta buana'],
    displayAnswer: 'Ta buana',
    explanation: "'Ta' is the 1st-person possessive ('my'). 'My friend' is always 'Ta buana'.",
  },
  {
    id: 'mc_negation_order',
    tier: 2,
    qType: 'mc',
    prompt: 'How do you say "I do not understand" in Seamenese?',
    subHint: "🐟 Tier 2 · Conjugate 'Poni' for 'Ëi' with the negation particle 'ni':",
    correctChoice: 'Ëi ni pon',
    choices: ['Ëi ni pon', 'Ëi ni poni', 'Ëi ni ponai', 'Ta ni pon'],
    displayAnswer: 'Ëi ni pon',
    explanation: "Negation 'ni' precedes the conjugated stem 'pon' ('Ëi ni pon', not infinitive 'poni' or 3rd-person 'ponai').",
  },
  // ---- TIER 3: Graduate (Multi-Verb Chaining, Irregular Stems, Derivations & Compounds) ----
  {
    id: 'type_sing',
    tier: 3,
    qType: 'typing',
    prompt: "Write 'To sing' in Seamenese (2 words)",
    subHint: "👑 Tier 3 · Literally 'To speak/vocalize music'",
    accepted: ['obisëi ezuki', 'obisei ezuki'],
    displayAnswer: 'Obisëi ezuki',
    explanation: "There is no single verb for singing; Seamenese uses 'Obisëi ezuki' (to vocalize music).",
  },
  {
    id: 'mc_multiverb_chain',
    tier: 3,
    qType: 'mc',
    prompt: 'How do you say "I like to sing music" in Seamenese?',
    subHint: '👑 Tier 3 · Multi-verb chaining rule: when two verbs share a subject, which verb conjugates?',
    correctChoice: 'Ëi skan obisëi ezuki',
    choices: ['Ëi skan obisëi ezuki', 'Ëi skan obise ezuki', 'Ëi skanai obisëi ezuki', 'Ëi skani obisëi ezuki'],
    displayAnswer: 'Ëi skan obisëi ezuki',
    explanation: "Only the FIRST verb conjugates ('skan'); the second verb stays in the infinitive ('obisëi')!",
  },
  {
    id: 'type_sanaami',
    tier: 3,
    qType: 'typing',
    prompt: 'Conjugate "Sanai" (to repeat/cycle) for "Na" (We): "Na ___"',
    subHint: "👑 Tier 3 · Stem 'sana-' keeps its '-a' before adding '-ami'!",
    accepted: ['sanaami', 'na sanaami'],
    displayAnswer: 'sanaami',
    explanation: "'Sanai' has stem 'sana-', so adding '-ami' for 'Na' produces 'na sanaami' (double-a).",
  },
  {
    id: 'mc_kiniwako',
    tier: 3,
    qType: 'mc',
    prompt: 'What is the difference between "Kiniwai" and "Kiniwako"?',
    subHint: '👑 Tier 3 · Compare the -ai verb ending vs the -ko noun suffix:',
    correctChoice: 'Kiniwai = to explain (verb), Kiniwako = explanation (noun)',
    choices: [
      'Kiniwai = to explain (verb), Kiniwako = explanation (noun)',
      'Kiniwai = explanation (noun), Kiniwako = to explain (verb)',
      'They are singular and plural of the same noun',
      'Kiniwako is the 3rd-person conjugation of Kiniwai',
    ],
    displayAnswer: 'Kiniwai = to explain (verb), Kiniwako = explanation (noun)',
    explanation: "'Kiniwai' is the verb 'to explain' (stem kiniwa-), while 'Kiniwako' is the noun 'explanation' ('-ko' derives the noun from the verb).",
  },
];

// ============================================================================
// 3B. DIFFICULTY TIER CLASSIFIERS FOR ALL POOLS (Tiers 1, 2, 3)
// ============================================================================
const TIER_1_WORDS = new Set([
  'tamako', 'taki', 'daka', 'ha', 'ni', 'ëi', 'ei', 'te', 'la', 'na',
  'xai', 'tuk', 'panke', 'mubu', 'mibi', 'sarpo', 'lipo', 'wo', 'ti',
  'silhbi', 'blobber', 'buana', 'tumu', 'ezuki', 'pan', 'wubu', 'eke',
  'sobo', 'semen', 'peke', 'liron', 'brux',
]);

const TIER_2_WORDS = new Set([
  'ton', 'lon', 'ta', 'tëi', 'tei', 'tëion', 'teion', 'nai', 'lai', 'laion',
  'wa', 'wu', 'we', 'zo', 'u', 'et', 'beto', 'hani', 'san',
  'etpanke', 'etwuke', 'wuke', 'skani', 'poni', 'kini', 'beti',
  'sarpi', 'zetëi', 'zetei', 'obisëi', 'obisei', 'obi', 'pansai',
  'skantar', 'blubur',
]);

function inferLexiconTier(entry) {
  if (!entry) return 1;
  if ([1, 2, 3].includes(Number(entry.tier))) return Number(entry.tier);
  const key = String(entry.term || entry.id || '').toLowerCase().trim();
  const keyAscii = key.replace(/ë/g, 'e');
  if (TIER_1_WORDS.has(key) || TIER_1_WORDS.has(keyAscii)) return 1;
  if (TIER_2_WORDS.has(key) || TIER_2_WORDS.has(keyAscii)) return 2;
  return 3;
}

const TIER_1_VERBS = new Set(['xai', 'skani', 'poni', 'kini', 'beti', 'sarpi']);
const TIER_2_VERBS = new Set(['zetëi', 'zetei', 'obisëi', 'obisei', 'zorpëi', 'zorpei', 'obi', 'pansai', 'nimi']);

function inferVerbTier(verb) {
  if (!verb) return 1;
  if ([1, 2, 3].includes(Number(verb.tier))) return Number(verb.tier);
  const inf = String(verb.infinitive || '').toLowerCase().trim();
  const infAscii = inf.replace(/ë/g, 'e');
  if (TIER_1_VERBS.has(inf) || TIER_1_VERBS.has(infAscii)) return 1;
  if (TIER_2_VERBS.has(inf) || TIER_2_VERBS.has(infAscii)) return 2;
  return 3;
}

const TIER_1_SCENE_IDS_CLIENT = new Set([
  's1', 's2', 's3', 's5', 's6',
  'scene_3', 'scene_5', 'scene_6', 'scene_7', 'scene_8', 'scene_14',
  'det_scene_1', 'det_scene_6', 'det_scene_7', 'det_scene_8', 'det_scene_13',
  'det_scene_16', 'det_scene_18', 'det_scene_21', 'det_scene_44',
]);
const TIER_2_SCENE_IDS_CLIENT = new Set([
  's4', 's8', 's9', 's11', 's12', 's13',
  'scene_1', 'scene_4', 'scene_11', 'scene_12', 'scene_16',
  'det_scene_2', 'det_scene_5', 'det_scene_9', 'det_scene_10', 'det_scene_11',
  'det_scene_12', 'det_scene_15', 'det_scene_19', 'det_scene_20', 'det_scene_22',
]);
const TIER_3_SCENE_IDS_CLIENT = new Set([
  's7', 's10', 's14', 's15', 's16',
  'scene_2', 'scene_9', 'scene_10', 'scene_13', 'scene_15',
  'det_scene_27', 'det_scene_32',
]);

function inferSceneTierClient(scene) {
  if (!scene || typeof scene !== 'object') return 1;
  const id = String(scene.id || '').toLowerCase();
  if (TIER_1_SCENE_IDS_CLIENT.has(id)) return 1;
  if (TIER_2_SCENE_IDS_CLIENT.has(id)) return 2;
  if (TIER_3_SCENE_IDS_CLIENT.has(id)) return 3;
  if ([1, 2, 3].includes(Number(scene.tier))) return Number(scene.tier);

  const blob = [
    scene.title || '',
    scene.scene || '',
    scene.seamenese || '',
    ...(Array.isArray(scene.options) ? scene.options.map(o => `${o?.reply || ''} ${o?.feedback || ''}`) : []),
  ].join(' ').toLowerCase();

  if (
    blob.includes('multi-verb') ||
    blob.includes('chained verb') ||
    blob.includes('second verb') ||
    blob.includes('stays infinitive') ||
    blob.includes('sanaami') ||
    blob.includes('kiniwako') ||
    blob.includes('orthography') ||
    blob.includes('ap bet')
  ) {
    return 3;
  }
  if (
    blob.includes('plural') ||
    blob.includes('-on') ||
    blob.includes('possessive') ||
    blob.includes('negation') ||
    blob.includes('-ami') ||
    blob.includes('ton ') ||
    blob.includes('laion') ||
    blob.includes('nai ')
  ) {
    return 2;
  }
  return 1;
}

// Strip parenthetical or semicolon explanations that leak the Seamenese word or clutter quiz prompts
function cleanQuizMeaning(rawMeaning, term = '') {
  if (!rawMeaning) return '';
  let s = String(rawMeaning).trim();

  // 1. Strip square-bracketed annotations like [moon] or [3sg subject] first
  for (let i = 0; i < 3; i++) {
    s = s.replace(/\s*\[[^[\]]*\]/g, '');
  }

  // 2. Strip verbose parenthetical notes BEFORE splitting on ';' (since notes often contain ';' inside parens!)
  const cleanT = String(term || '').trim().toLowerCase();
  const asciiT = cleanT.replace(/ë/g, 'e');
  s = s.replace(/\s*\(([^()]*)\)/g, (match, inner) => {
    const low = inner.toLowerCase().trim();
    if (
      cleanT.length >= 2 &&
      (low.includes(cleanT) || low.includes(asciiT))
    ) {
      return '';
    }
    // Keep only short grammatical disambiguations like "(singular)", "(plural)", "(copula)", "(greeting)"
    if (
      /^(?:1st|2nd|3rd|singular|plural|copula|greeting|adverb)/i.test(low)
    ) {
      const concise = inner.split(/[;—,—-]/)[0].trim();
      return concise ? ` (${concise})` : '';
    }
    return '';
  });

  // 3. Strip any dangling unclosed '(' or '['
  s = s.replace(/\s*\([^()]*$/, '').replace(/\s*\[[^[\]]*$/, '').trim();

  // 4. Now safely split on top-level ';'
  if (s.includes(';')) {
    s = s.split(';')[0].trim();
  }

  // 5. Final cleanup of any trailing unclosed '('
  s = s.replace(/\s*\([^()]*$/, '').trim();
  return s || String(rawMeaning).replace(/\s*\([^()]*\)?/g, '').split(';')[0].trim();
}

function normalizeMeaningKey(str) {
  return String(str || '')
    .toLowerCase()
    .replace(/\([^()]*\)/g, '')
    .replace(/\([^()]*$/g, '')
    .replace(/[^a-z0-9\s/]/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}

const LEXICON_VARIANT_GROUPS = {
  blobber:       { accepted: ['blobber', 'blober'], displayAnswer: 'Blobber / Blober', synonymIds: ['blobber', 'blober'] },
  blober:        { accepted: ['blobber', 'blober'], displayAnswer: 'Blobber / Blober', synonymIds: ['blobber', 'blober'], skipDuplicateCard: true },
  blobberon:     { accepted: ['blobberon', 'bloberon'], displayAnswer: 'Blobberon / Bloberon' },
  bloberon:      { accepted: ['blobberon', 'bloberon'], displayAnswer: 'Blobberon / Bloberon', skipDuplicateCard: true },
  fasma:         { accepted: ['fasma', 'sarpo'], displayAnswer: 'Fasma / Sarpo', synonymIds: ['fasma', 'sarpo'] },
  sarpo:         { accepted: ['sarpo', 'fasma'], displayAnswer: 'Sarpo / Fasma', synonymIds: ['fasma', 'sarpo'] },
  skani:         { accepted: ['skani', 'skan'], displayAnswer: 'Skani' },
  'jai-semen':   { accepted: ['jai-semen', 'jai semen', 'jaisemen'], displayAnswer: 'Jai-semen' },
  'zorpe-semen': { accepted: ['zorpe-semen', 'zorpe semen', 'zorpesemen'], displayAnswer: 'Zorpe-semen' },
  etpanke:       { accepted: ['etpanke', 'et-panke', 'et panke'], displayAnswer: 'Etpanke' },
  etwuke:        { accepted: ['etwuke', 'et-wuke', 'et wuke'], displayAnswer: 'Etwuke' },
  hani:          { accepted: ['hani', 'ha-ni', 'ha ni'], displayAnswer: 'Hani' },
  zetei:         { accepted: ['zetëi', 'zetei', 'zete'], displayAnswer: 'Zetëi' },
  'zetëi':       { accepted: ['zetëi', 'zetei', 'zete'], displayAnswer: 'Zetëi' },
  obisei:        { accepted: ['obisëi', 'obisei', 'obise'], displayAnswer: 'Obisëi' },
  'obisëi':      { accepted: ['obisëi', 'obisei', 'obise'], displayAnswer: 'Obisëi' },
  zorpei:        { accepted: ['zorpëi', 'zorpei', 'zorpe'], displayAnswer: 'Zorpëi' },
  'zorpëi':      { accepted: ['zorpëi', 'zorpei', 'zorpe'], displayAnswer: 'Zorpëi' },
  mikei:         { accepted: ['mikëi', 'mikei', 'mike'], displayAnswer: 'Mikëi' },
  'mikëi':       { accepted: ['mikëi', 'mikei', 'mike'], displayAnswer: 'Mikëi' },
};

const VERB_BARE_STEMS = {
  beti: 'bet',
  itxi: 'itx',
  faki: 'fak',
  skani: 'skan',
  kixixi: 'kixix',
  nimi: 'nim',
  xai: 'xa',
  obi: 'ob',
  jai: 'ja',
  sarpi: 'sarp',
  ragoi: 'rago',
  kini: 'kin',
  kiniwai: 'kiniwa',
  poni: 'pon',
  mikei: 'mike',
  'mikëi': 'mike',
  obisei: 'obise',
  'obisëi': 'obise',
  zetei: 'zete',
  'zetëi': 'zete',
  pansai: 'pansa',
  zorpei: 'zorpe',
  'zorpëi': 'zorpe',
  sanai: 'sana',
};

function getAcceptedAnswersForEntry(entry, lexList = []) {
  const cleanTerm = String(entry?.term || '').trim().toLowerCase();
  const plainAscii = cleanTerm.replace(/ë/g, 'e');
  const idLow = String(entry?.id || '').trim().toLowerCase();
  const group = LEXICON_VARIANT_GROUPS[cleanTerm] || LEXICON_VARIANT_GROUPS[plainAscii] || LEXICON_VARIANT_GROUPS[idLow];

  const acceptedSet = new Set([cleanTerm, plainAscii]);
  if (group && Array.isArray(group.accepted)) {
    group.accepted.forEach(a => {
      acceptedSet.add(a.toLowerCase());
      acceptedSet.add(a.toLowerCase().replace(/ë/g, 'e'));
    });
  }

  // Compound hyphen/space flexibility for any hyphenated word
  if (cleanTerm.includes('-')) {
    acceptedSet.add(cleanTerm.replace(/-/g, ' '));
    acceptedSet.add(cleanTerm.replace(/-/g, ''));
    acceptedSet.add(plainAscii.replace(/-/g, ' '));
    acceptedSet.add(plainAscii.replace(/-/g, ''));
  }

  // Verb bare stem support
  const bareStem = VERB_BARE_STEMS[cleanTerm] || VERB_BARE_STEMS[plainAscii] || VERB_BARE_STEMS[idLow] || null;
  if (bareStem) {
    acceptedSet.add(bareStem.toLowerCase());
  }

  // Dynamic synonym matching across lexList (same normalized core meaning)
  const myNormMeaning = normalizeMeaningKey(entry?.meaning);
  const synonymMeanings = new Set([entry?.meaning]);
  if (group?.synonymIds) {
    (lexList || []).forEach(other => {
      const oTerm = String(other?.term || '').toLowerCase();
      const oId = String(other?.id || '').toLowerCase();
      if (group.synonymIds.includes(oTerm) || group.synonymIds.includes(oId)) {
        if (other.meaning) synonymMeanings.add(other.meaning);
        if (other.term) {
          acceptedSet.add(oTerm);
          acceptedSet.add(oTerm.replace(/ë/g, 'e'));
        }
      }
    });
  }
  if (myNormMeaning.length >= 3) {
    (lexList || []).forEach(other => {
      if (!other || other.id === entry?.id) return;
      const otherNorm = normalizeMeaningKey(other.meaning);
      if (
        otherNorm === myNormMeaning ||
        (myNormMeaning === 'tired' && otherNorm.startsWith('tired')) ||
        (otherNorm === 'tired' && myNormMeaning.startsWith('tired'))
      ) {
        const oTerm = String(other.term || '').trim().toLowerCase();
        if (oTerm) {
          acceptedSet.add(oTerm);
          acceptedSet.add(oTerm.replace(/ë/g, 'e'));
        }
        if (other.meaning) synonymMeanings.add(other.meaning);
      }
    });
  }

  return {
    accepted: Array.from(acceptedSet),
    displayAnswer: group?.displayAnswer || entry?.term || '',
    bareStem,
    skipDuplicateCard: Boolean(group?.skipDuplicateCard),
    synonymMeanings,
  };
}

// Helper to merge live `db.entries` with `BASELINE_LEXICON` metadata (emoji, cue, example)
function buildLiveLexicon(db) {
  const baseByWord = {};
  BASELINE_LEXICON.forEach(b => {
    baseByWord[b.term.toLowerCase()] = b;
    baseByWord[b.id.toLowerCase()] = b;
  });

  const rejectedSet = new Set(
    (Array.isArray(db?.meta?.rejected_words) ? db.meta.rejected_words : [])
      .map(w => String(w || '').trim().toLowerCase())
      .filter(Boolean)
  );

  const liveEntries = (db?.entries || []).filter(e => {
    const low = String(e?.word || '').trim().toLowerCase();
    if (!low || e.status === 'superseded' || e.category === 'slang') return false;
    if (rejectedSet.has(low) && !baseByWord[low]) return false;
    return e.confidence === 'confirmed' || e.confidence === 'high' || Boolean(baseByWord[low]);
  });

  if (liveEntries.length === 0) {
    return BASELINE_LEXICON.map(b => ({ ...b, tier: inferLexiconTier(b) }));
  }

  const merged = [];
  const seen = new Set();

  // First include all baseline lexicon items enriched with live db fields
  BASELINE_LEXICON.forEach(base => {
    const live = liveEntries.find(e => (e.word || '').toLowerCase() === base.term.toLowerCase());
    seen.add(base.term.toLowerCase());
    seen.add(base.id.toLowerCase());
    const cleanedLiveMeaning = live?.meaning ? cleanQuizMeaning(live.meaning, base.term) : '';
    const cleanedBaseMeaning = cleanQuizMeaning(base.meaning, base.term) || base.meaning;
    const item = {
      ...base,
      meaning:
        cleanedLiveMeaning && cleanedLiveMeaning.length <= 48
          ? cleanedLiveMeaning
          : cleanedBaseMeaning,
      fullMeaning: live?.meaning || base.meaning,
      pos: live?.category || base.pos,
      notes: live?.notes || base.notes,
      plural: live?.plural || null,
      pronunciation: live?.pronunciation || null,
    };
    item.tier = inferLexiconTier(item);
    merged.push(item);
  });

  // Then add any additional confirmed entries from live db
  liveEntries.forEach(e => {
    const low = (e.word || '').toLowerCase();
    const ascii = low.replace(/ë/g, 'e');
    if (!low || seen.has(low) || seen.has(ascii) || rejectedSet.has(low)) return;
    seen.add(low);
    seen.add(ascii);
    const cleanedMeaning = cleanQuizMeaning(e.meaning || '', e.word);
    const item = {
      id: e.id || ascii,
      term: e.word,
      pos: e.category || 'noun',
      status: e.confidence || 'confirmed',
      emoji: '🌊',
      meaning: cleanedMeaning,
      fullMeaning: e.meaning || cleanedMeaning,
      hint: e.notes ? cleanQuizMeaning(e.notes, e.word).slice(0, 110) : `Active ${e.category || 'word'} in the Seamenese lexicon`,
      example: `${e.word} xaai tuk semen.`,
      notes: e.notes || '',
      plural: e.plural || null,
      pronunciation: e.pronunciation || null,
    };
    item.tier = inferLexiconTier(item);
    merged.push(item);
  });

  return merged;
}

// Helper to merge live `db.conjugations` with `BASELINE_CONJUGATIONS`
function buildLiveConjugations(db) {
  const baseMap = {};
  BASELINE_CONJUGATIONS.forEach(c => {
    baseMap[c.infinitive.toLowerCase()] = c;
    baseMap[c.infinitive.toLowerCase().replace(/ë/g, 'e')] = c;
  });

  const liveConjObj = db?.conjugations || {};
  const liveValues = Object.values(liveConjObj).filter(c => c && c.status !== 'superseded');
  if (liveValues.length === 0) {
    return BASELINE_CONJUGATIONS.map(c => ({ ...c, tier: inferVerbTier(c) }));
  }

  const result = [];
  const seen = new Set();

  BASELINE_CONJUGATIONS.forEach(base => {
    const keyLow = base.infinitive.toLowerCase();
    const keyAscii = keyLow.replace(/ë/g, 'e');
    const live = liveValues.find(
      c => (c.infinitive || '').toLowerCase() === keyLow || (c.infinitive || '').toLowerCase().replace(/ë/g, 'e') === keyAscii
    );
    seen.add(keyLow);
    seen.add(keyAscii);
    if (live && live.forms) {
      const mergedVerb = {
        ...base,
        ei: live.forms['Ëi'] || live.forms['Ei'] || base.ei,
        te: live.forms['Te'] || base.te,
        ton: live.forms['Ton'] || base.ton,
        na: live.forms['Na'] || base.na,
        la: live.forms['La'] || base.la,
        lon: live.forms['Lon'] || live.forms['La'] || base.lon,
        note: live.notes || base.note,
      };
      mergedVerb.tier = inferVerbTier(mergedVerb);
      result.push(mergedVerb);
    } else {
      result.push({ ...base, tier: inferVerbTier(base) });
    }
  });

  liveValues.forEach(c => {
    if (!c || !c.infinitive || !c.forms) return;
    const low = c.infinitive.toLowerCase();
    const ascii = low.replace(/ë/g, 'e');
    if (seen.has(low) || seen.has(ascii)) return;
    seen.add(low);
    const customVerb = {
      infinitive: c.infinitive,
      meaning: c.meaning || 'Verb',
      type: c.notes?.toLowerCase().includes('irregular') ? 'irregular' : 'regular',
      ei: c.forms['Ëi'] || c.forms['Te'] || c.infinitive,
      te: c.forms['Te'] || c.forms['Ëi'] || c.infinitive,
      ton: c.forms['Ton'] || `${c.forms['Ëi'] || c.infinitive}u`,
      na: c.forms['Na'] || `${c.forms['Ëi'] || c.infinitive}ami`,
      la: c.forms['La'] || `${c.forms['Ëi'] || c.infinitive}ai`,
      lon: c.forms['Lon'] || c.forms['La'] || `${c.forms['Ëi'] || c.infinitive}ai`,
      note: c.notes || 'Conjugated verb.',
    };
    customVerb.tier = inferVerbTier(customVerb);
    result.push(customVerb);
  });

  return result;
}

// Build token lookup map for clickable Seamenese words
function buildTokenMap(lexicon, conjugations) {
  const map = {};
  lexicon.forEach(entry => {
    map[entry.term.toLowerCase()] = entry;
    map[entry.id.toLowerCase()] = entry;
    map[entry.term.toLowerCase().replace(/ë/g, 'e')] = entry;
    if (entry.pos === 'noun') {
      const base = entry.term.toLowerCase();
      const pluralVowelDrop = base.replace(/[aeiouë]$/, '') + 'on';
      const pluralDirect = base + 'on';
      map[pluralVowelDrop] = entry;
      map[pluralDirect] = entry;
    }
  });
  const specialPlurals = {
    skantaron: 'skantar',
    sobon: 'sobo',
    buanon: 'buana',
    texikon: 'texik',
    silhbon: 'silhbi',
    semenon: 'semen',
  };
  Object.entries(specialPlurals).forEach(([pl, id]) => {
    const found = lexicon.find(e => e.id === id);
    if (found) map[pl] = found;
  });

  conjugations.forEach(c => {
    const baseEntry = lexicon.find(
      e =>
        e.term.toLowerCase() === c.infinitive.toLowerCase() ||
        e.id === c.infinitive.toLowerCase().replace(/ë/g, 'e')
    );
    if (baseEntry) {
      [c.infinitive, c.ei, c.te, c.ton, c.na, c.la, c.lon].forEach(form => {
        if (form) {
          map[form.toLowerCase()] = baseEntry;
          map[form.toLowerCase().replace(/ë/g, 'e')] = baseEntry;
        }
      });
    }
  });

  return map;
}

// Fallback procedural generator for 10 fresh scenes if offline / API unavailable
function generateClientFallbackScenes(existingCount, lexicon, conjugations) {
  const templates = [
    {
      title: 'Greeting & Plural Feet',
      scene: '🦶 Your friend asks how many feet you have. Answer using the canonical -on plural rule!',
      speaker: 'Buana',
      seamenese: 'Tamako ta buana! Te xa 1 sobo zo 2 ___?',
      gloss: 'Hello my friend! Do you have 1 foot or 2 [feet]?',
      options: [
        { reply: '1 sobo, 2 sobon! Ta sobon xaai tuk!', correct: true, feedback: "Wo tuk! 'Sobo' drops final '-o' and adds '-on' → 'sobon'." },
        { reply: '1 sobo, 2 soboon! Ta soboon xaai tuk!', correct: false, feedback: "Plural trap! Drop the final vowel '-o' before adding '-on' ('sobon')." },
        { reply: '1 sobo, 2 sobon! Ëi sobon xaai tuk!', correct: false, feedback: "Possessive trap! Use 'Ta sobon' (my feet), never 'Ëi sobon'." },
        { reply: '1 sobo, 2 sobon! Ta sobon xa tuk!', correct: false, feedback: "Copula trap! Third-person nouns ('ta sobon') take 'xaai', not 'xa'." },
      ],
    },
    {
      title: 'Singing Under the Blue Sky',
      scene: '☀️🎶 The daytime sky is blue and you want to sing with your friends ("Na").',
      speaker: 'Buanon',
      seamenese: 'Etpanke eke xaai peke! Wa na jaami?',
      gloss: 'Today the sky is blue! What do we do?',
      options: [
        { reply: 'Etpanke na skanami obisëi tuk ezuki!', correct: true, feedback: "Wo tuk! 'Na' takes '-ami' on the first verb ('skanami'), and the chained verb stays infinitive ('obisëi')." },
        { reply: 'Etpanke na skanami obiseami tuk ezuki!', correct: false, feedback: "Multi-verb trap! Only the first verb ('skanami') conjugates; 'obisëi' stays infinitive." },
        { reply: 'Etpanke na skanu obisëi tuk ezuki!', correct: false, feedback: "Pronoun trap! '-u' is for 'Ton' (you all). 'Na' (we) requires '-ami'." },
        { reply: 'Etpanke nai skanami obisëi tuk ezuki!', correct: false, feedback: "Pronoun trap! 'Nai' is possessive ('our'). Use subject pronoun 'Na' (we)." },
      ],
    },
    {
      title: 'Green Salad & Red Fish',
      scene: '🥗🐟 You are ordering food ("tumu") for yourself and your cat ("ta silhbi").',
      speaker: 'Buana',
      seamenese: 'Wa tumu te zete, u wa tumu tëi silhbi zeteai?',
      gloss: 'What food do you eat, and what food does your cat eat?',
      options: [
        { reply: 'Ëi zete liron blubur, u ta silhbi zeteai brux blobber!', correct: true, feedback: "Wo tuk! 'Ëi zete' (1sg bare stem), 'liron blubur' (green salad), and 'ta silhbi zeteai' (3sg -ai)." },
        { reply: 'Ëi zeteai liron blubur, u ta silhbi zete brux blobber!', correct: false, feedback: "Inverted conjugations! 'Ëi' takes 'zete' and 'ta silhbi' (3sg) takes 'zeteai'." },
        { reply: 'Ëi zetëi liron blubur, u ta silhbi zeteai brux blobber!', correct: false, feedback: "Infinitive trap! Conjugate 'Zetëi' to 'zete' for 'Ëi'." },
        { reply: 'Ëi zete peke blubur, u ta silhbi zeteai liron blobber!', correct: false, feedback: "Color mix-up! 'Liron' is green (for blubur) and 'brux' is red." },
      ],
    },
    {
      title: 'Need to Ask a Question',
      scene: '🙋‍♂️ You need to ask your friends a question about a new word.',
      speaker: 'Buanon',
      seamenese: 'Ap bet! Te kin obisëi na?',
      gloss: 'But wait! Do you need to speak to us?',
      options: [
        { reply: 'Ha! Ëi kin zorpëi ton: ton ponu et lulu semen?', correct: true, feedback: "Wo tuk! 'Ëi kin zorpëi' (first verb conjugated, second infinitive) and 'ton ponu' (2nd plural -u)." },
        { reply: 'Ha! Ëi kin zorpe ton: ton ponu et lulu semen?', correct: false, feedback: "Multi-verb trap! After 'Ëi kin', the second verb must stay infinitive ('zorpëi')." },
        { reply: 'Ha! Ëi kin zorpëi ton: ton pon et lulu semen?', correct: false, feedback: "Conjugation trap! 'Ton' (you plural) requires '-u' ('ton ponu')." },
        { reply: 'Ha! Ëi kini zorpëi ton: ton ponu et lulu semen?', correct: false, feedback: "Conjugation trap! Conjugate 'Kini' to 'kin' for 'Ëi'." },
      ],
    },
    {
      title: 'Awake Tonight, Not Sleepy!',
      scene: '🤩🌃 It is tonight ("etwuke"), and your friend asks if you are sleepy ("sarpo"). Say no, you are energetic ("lipo")!',
      speaker: 'Buana',
      seamenese: 'Etwuke te xa sarpo zo te xa lipo?',
      gloss: 'Tonight are you sleepy or are you energetic?',
      options: [
        { reply: 'Ëi ni xa sarpo etwuke — ëi xa wo lipo!', correct: true, feedback: "Wo tuk! 'Ni' precedes the copula 'xa', and 'wo lipo' amplifies 'energetic'!" },
        { reply: 'Ëi ni xau sarpo etwuke — ëi xa wo lipo!', correct: false, feedback: "Conjugation trap! 'Xau' is only for 'Ton' (you plural); 'Ëi' takes 'xa'." },
        { reply: 'Ëi ni xa sarpi etwuke — ëi xa wo lipo!', correct: false, feedback: "POS trap! 'Sarpi' is the verb 'to sleep'; use the adjective 'sarpo' after 'xa'." },
        { reply: 'Ëi ni xaai sarpo etwuke — ëi xaai wo lipo!', correct: false, feedback: "Copula trap! 'Ëi' takes 'xa', not 'xaai'." },
      ],
    },
    {
      title: 'Whose Music Do They Love?',
      scene: '🎵👥 Ask why they ("lon") really love our ("nai") music!',
      speaker: 'Buana',
      seamenese: 'Lon obiseai nai ezuki! Te pon we lon obiseai nai ezuki?',
      gloss: 'They sing our music! Do you understand why they sing our music?',
      options: [
        { reply: 'We lon wo skanai nai ezuki? Nai ezuki xaai wo tuk!', correct: true, feedback: "Wo tuk! 'We' keeps SVO order, 'lon wo skanai' uses amplifier 'wo' + 3pl '-ai', and 'nai' means 'our'." },
        { reply: 'We lon wo skanu nai ezuki? Nai ezuki xaai wo tuk!', correct: false, feedback: "Conjugation trap! 'Lon' (they) takes '-ai' ('skanai'), not '-u' ('skanu')." },
        { reply: 'We laion wo skanai na ezuki? Na ezuki xaai wo tuk!', correct: false, feedback: "Pronoun trap! 'Lon' = they (subject), and 'Nai' = our (possessive)." },
        { reply: 'Wa lon wo skanai nai ezuki? Nai ezuki xaai wo tuk!', correct: false, feedback: "Question word trap! 'Why' is 'We', whereas 'Wa' means 'What'." },
      ],
    },
    {
      title: 'Waiting for an Explanation',
      scene: '⏳📖 Tell your friend: "But wait, please! Give me your explanation!"',
      speaker: 'Buana',
      seamenese: 'Ëi pansa ëi pon et jai-semen...',
      gloss: 'I think I understand this verb...',
      options: [
        { reply: 'Ap bet, beto! Ob ëi tëi kiniwako!', correct: true, feedback: "Wo tuk! 'Ap bet' uses conjugated stem 'bet', 'beto' = please, 'ob' = give, and 'tëi kiniwako' = your explanation." },
        { reply: 'Ap beti, beto! Obi ëi tëi kiniwako!', correct: false, feedback: "Conjugation trap! After 'ap' and in imperatives for 'te', use the bare stems 'bet' and 'ob'." },
        { reply: 'Ap bet, beto! Ob ëi tëi kiniwai!', correct: false, feedback: "POS trap! 'Kiniwai' is the verb 'to explain'; the noun 'explanation' is 'kiniwako'." },
        { reply: 'Ap beto, bet! Ob ëi te kiniwako!', correct: false, feedback: "Trap! 'Bet' is wait and 'beto' is please; and 'your' is 'tëi', not 'te'." },
      ],
    },
    {
      title: 'Repeating the Drill Loop (Sanai)',
      scene: '🔁🫂 Tell your study group that we cycle/repeat new words so we understand Seamenese!',
      speaker: 'Buana',
      seamenese: 'Wu na ponami lulu semenon?',
      gloss: 'How do we understand new words?',
      options: [
        { reply: 'Na sanaami lulu semenon u na ponami seamenese!', correct: true, feedback: "Wo tuk! 'Sanai' (stem 'sana-') + '-ami' → 'na sanaami', and plural of 'semen' is 'semenon'." },
        { reply: 'Na sanami lulu semenon u na ponami seamenese!', correct: false, feedback: "Stem trap! 'Sanai' retains its '-a' stem vowel before '-ami' → 'sanaami'." },
        { reply: 'Na sanaami lulu semen u na ponu seamenese!', correct: false, feedback: "Agreement trap! 'Na' takes 'ponami' (-ami), not 'ponu' (-u)." },
        { reply: 'Na sanaami lulu semens u na ponami seamenese!', correct: false, feedback: "Plural trap! Plural of 'semen' is 'semenon', never 'semens'." },
      ],
    },
    {
      title: 'Why Are You Angry, My Friend?',
      scene: '😠➡️🙂 Your friend looks angry ("mibi"). Ask why they are angry and say that we are happy today!',
      speaker: 'Buana',
      seamenese: 'Etpanke xaai wo panke...',
      gloss: 'Today is a tough day...',
      options: [
        { reply: 'We te xa mibi, ta buana? Na xaami mubu etpanke!', correct: true, feedback: "Wo tuk! 'We te xa mibi' (Why are you angry) and 'Na xaami mubu etpanke' (We are happy today)." },
        { reply: 'We te xa mubu, ta buana? Na xaami mibi etpanke!', correct: false, feedback: "Emotion mix-up! 'Mibi' means angry and 'mubu' means happy." },
        { reply: 'We te xaai mibi, ëi buana? Na xaami mubu etpanke!', correct: false, feedback: "Grammar trap! 'Te' takes 'xa' (not 'xaai'), and 'my friend' is 'ta buana' (not 'ëi buana')." },
        { reply: 'Wu te xa mibi, ta buana? Na xaami mubu etpanke!', correct: false, feedback: "Question word trap! 'Why' is 'We' ('Wu' means 'How')." },
      ],
    },
    {
      title: 'Cheers to Our Crushes!',
      scene: '🍻💘 Raise a toast ("Daka!") and say: "Their crushes sing good music tonight!"',
      speaker: 'Buana',
      seamenese: 'Daka! Wa laion skantaron jaai etwuke?',
      gloss: 'Cheers! What are their crushes doing tonight?',
      options: [
        { reply: 'Daka! Laion skantaron obiseai tuk ezuki etwuke!', correct: true, feedback: "Wo tuk! 'Laion' (their), 'skantaron' (crushes/lovers), and 'obiseai' (3pl -ai)." },
        { reply: 'Daka! Lai skantar obiseu tuk ezuki etwuke!', correct: false, feedback: "Trap! 'Their' is 'laion' (not 'lai'), and 3rd-person plural takes '-ai' ('obiseai'), not '-u'." },
        { reply: 'Daka! Lon skantaron obiseai tuk ezuki etwuke!', correct: false, feedback: "Pronoun trap! 'Lon' is 'they'; for possessive 'their', use 'laion'." },
        { reply: 'Daka! Laion skantaron obisëai tuk ezuki etwuke!', correct: false, feedback: "Orthography trap! 'Ë' only appears before 'I' ('Obisëi'). Before '-ai', it is spelled 'obiseai'." },
      ],
    },
  ];

  return templates.map((t, i) => {
    const num = existingCount + i + 1;
    return {
      id: `client_fallback_${i + 1}`,
      title: `Scene ${num}: ${t.title}`,
      scene: t.scene,
      speaker: t.speaker,
      seamenese: t.seamenese,
      gloss: t.gloss,
      options: t.options,
    };
  });
}

// ============================================================================
// 4. INTERACTIVE CLICKABLE SEAMENESE TEXT COMPONENT
// ============================================================================
function InteractiveSeameneseText({ text, tokenMap, onInspectWord }) {
  if (!text) return null;
  const parts = String(text).split(/(\s+)/);
  return (
    <>
      {parts.map((part, idx) => {
        const clean = part.replace(/[^a-zA-ZëË\-]/g, '').toLowerCase();
        const ascii = clean.replace(/ë/g, 'e');
        const matched = clean ? (tokenMap[clean] || tokenMap[ascii]) : null;
        if (matched) {
          return (
            <span
              key={idx}
              className="learn-clickable-word"
              title={`Inspect "${matched.term}"`}
              onClick={e => {
                e.stopPropagation();
                onInspectWord && onInspectWord(matched);
              }}
            >
              {part}
            </span>
          );
        }
        return <React.Fragment key={idx}>{part}</React.Fragment>;
      })}
    </>
  );
}

// ============================================================================
// 5. WORD INSPECTOR MODAL (with Web Speech Phonetic Synthesis + Strict Immersion)
// ============================================================================
function speakSeamenesePhonetic(term, example) {
  if (!('speechSynthesis' in window)) return;
  try {
    window.speechSynthesis.cancel();
    const phoneticize = str =>
      String(str || '')
        .replace(/ëi/gi, 'ay')
        .replace(/ë/gi, 'ay')
        .replace(/x/gi, 'sh');
    const u = new SpeechSynthesisUtterance(`${phoneticize(term)}. ${phoneticize(example || '')}`);
    u.rate = 0.92;
    window.speechSynthesis.speak(u);
  } catch {}
}

function WordInspectorModal({ entry, conjugations, immersionMode, onClose, onPeekScheduleSrs }) {
  const [peeked, setPeeked] = useStateL(false);

  useEffectL(() => {
    setPeeked(false);
  }, [entry?.id, entry?.term]);

  if (!entry) return null;

  const showGloss = !immersionMode || peeked;
  const conj = (conjugations || []).find(
    c =>
      c.infinitive.toLowerCase() === entry.term.toLowerCase() ||
      c.infinitive.toLowerCase().replace(/ë/g, 'e') === entry.id.toLowerCase()
  );

  function handlePeek() {
    setPeeked(true);
    onPeekScheduleSrs && onPeekScheduleSrs(entry);
  }

  return (
    <div className="learn-modal-backdrop" onClick={onClose}>
      <div className="learn-modal-sheet" onClick={e => e.stopPropagation()}>
        <div className="learn-modal-head">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 28 }}>{entry.emoji || '🌊'}</span>
            <span className="serif" style={{ fontSize: 28, fontWeight: 600, color: 'var(--accent)' }}>{entry.term}</span>
            <span className="learn-badge">{entry.pos}</span>
            {entry.plural && <span className="learn-badge learn-badge-amber">pl: {entry.plural}</span>}
          </div>
          <button
            type="button"
            className="btn"
            onClick={() => speakSeamenesePhonetic(entry.term, entry.example)}
          >
            🔊 Listen
          </button>
        </div>

        <p style={{ fontSize: 15, margin: '14px 0 8px', lineHeight: 1.55, color: 'var(--paper)' }}>
          <strong style={{ color: 'var(--fg-mute)', fontFamily: 'var(--mono)', fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Immersion Cue: </strong>
          <span>{immersionMode ? cleanImmersionCue(entry.hint) : entry.hint}</span>
        </p>

        {entry.example && (
          <div className="learn-dialogue-line" style={{ margin: '10px 0 16px' }}>
            "{entry.example}"
          </div>
        )}

        {conj && (
          <div style={{ marginBottom: 16 }}>
            <div className="eyebrow" style={{ marginBottom: 6 }}><span className="rule"></span>Verb Conjugation ({conj.infinitive})</div>
            <table className="learn-conj-table">
              <thead>
                <tr>
                  <th>Ëi / Te</th>
                  <th>Ton (-u)</th>
                  <th>Na (-ami)</th>
                  <th>La / Lon (-ai)</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td className="mono">{conj.ei}</td>
                  <td className="mono">{conj.ton}</td>
                  <td className="mono">{conj.na}</td>
                  <td className="mono">{conj.la}</td>
                </tr>
              </tbody>
            </table>
          </div>
        )}

        {showGloss ? (
          <div className="learn-gloss-box">
            <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--conf-confirmed)', marginBottom: 4 }}>
              {entry.meaning}
            </div>
            {entry.notes && (
              <div style={{ fontSize: 13, color: 'var(--paper-dim)', lineHeight: 1.5 }}>
                {entry.notes}
              </div>
            )}
          </div>
        ) : (
          <div className="learn-gloss-hidden-notice mono">
            🔒 Strict Immersion is active — try inferring the meaning from the cue &amp; sentence first!
          </div>
        )}

        <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 16 }}>
          {!showGloss && (
            <button type="button" className="btn btn-warm" style={{ flex: 1, justifyContent: 'center', padding: '12px 16px' }} onClick={handlePeek}>
              👁️ Reveal English Gloss (Schedules SRS)
            </button>
          )}
          <button type="button" className="btn btn-accent" style={{ flex: 1, justifyContent: 'center', padding: '12px 16px' }} onClick={onClose}>
            Got It ✓
          </button>
        </div>
      </div>
    </div>
  );
}

// ============================================================================
// 5B. ULTRA-LIGHTWEIGHT SHARED WEBGL REACTIVE ROW SHADER ENGINE
// ============================================================================
const ROW_SHADER_MODES = {
  frame_default: 0,
  frame_kelp: 1,
  frame_runes: 2,
  frame_synth: 3,
  frame_inferno: 4,
  frame_aurora: 5,
  frame_singularity: 6,
};

const ROW_VERTEX_SHADER_SRC = `
attribute vec2 a_pos;
varying vec2 v_uv;
void main() {
  v_uv = a_pos * 0.5 + 0.5;
  gl_Position = vec4(a_pos, 0.0, 1.0);
}
`;

const ROW_FRAGMENT_SHADER_SRC = `
precision mediump float;
varying vec2 v_uv;
uniform vec2 u_res;
uniform float u_time;
uniform vec2 u_ptr;
uniform vec2 u_vel;
uniform float u_hover;
uniform vec3 u_pulse;
uniform float u_mode;

float hash21(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

float noise2D(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  float a = hash21(i);
  float b = hash21(i + vec2(1.0, 0.0));
  float c = hash21(i + vec2(0.0, 1.0));
  float d = hash21(i + vec2(1.0, 1.0));
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

float fbm3(vec2 p) {
  float v = 0.0;
  float amp = 0.5;
  mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
  for (int i = 0; i < 3; i++) {
    v += amp * noise2D(p);
    p = rot * p * 2.02 + vec2(1.7, 9.2);
    amp *= 0.5;
  }
  return v;
}

void main() {
  vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y);
  float aspect = clamp(u_res.x / max(u_res.y, 1.0), 3.0, 16.0);
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 ptr = vec2(u_ptr.x * aspect, u_ptr.y);

  vec2 dPtrVec = vec2((uv.x - u_ptr.x) * min(aspect * 0.45, 3.6), uv.y - u_ptr.y);
  float dPtr = length(dPtrVec);
  float ptrGlow = exp(-dPtr * dPtr * 4.0) * (0.32 + 0.68 * u_hover);
  float ptrCore = exp(-dPtr * dPtr * 18.0) * (0.25 + 0.75 * u_hover);

  float shockRing = 0.0;
  vec2 shockDisp = vec2(0.0);
  if (u_pulse.z >= 0.0 && u_pulse.z <= 1.0) {
    vec2 pVec = vec2((uv.x - u_pulse.x) * min(aspect * 0.45, 3.6), uv.y - u_pulse.y);
    float dP = length(pVec);
    float rad = u_pulse.z * 3.4;
    float dr = dP - rad;
    float fade = (1.0 - u_pulse.z) * (1.0 - u_pulse.z);
    shockRing = exp(-dr * dr * 22.0) * fade;
    shockDisp = (pVec / (dP + 0.01)) * shockRing * 0.1;
  }

  vec2 ptrPush = (dPtrVec / (dPtr + 0.06)) * ptrGlow * 0.09;
  vec2 wp = p - ptrPush * vec2(aspect * 0.25, 1.0) + shockDisp * vec2(aspect * 0.3, 1.0) - u_vel * exp(-dPtr * 2.5) * 0.35;

  float bx = min(uv.x, 1.0 - uv.x) * aspect;
  float by = min(uv.y, 1.0 - uv.y);
  float bDist = min(bx, by);
  float edgeGlow = exp(-bDist * 7.0);
  float edgeRim = exp(-bDist * 22.0);
  float reactiveEdge = edgeGlow * (0.55 + 0.55 * u_hover + 0.95 * ptrGlow + 1.2 * shockRing);

  vec3 col = vec3(0.04, 0.06, 0.1);

  if (u_mode < 1.5) {
    // MODE 1: Bioluminescent Reef Trim (Caustics + Interactive Ripples)
    float ripple = sin(dPtr * 20.0 - u_time * 5.0) * exp(-dPtr * 2.8) * (0.25 + 0.75 * u_hover);
    vec2 cp = wp * vec2(1.4, 2.2) + vec2(u_time * 0.22, -u_time * 0.15) + ripple * 0.22;
    float c1 = sin(cp.x * 3.2 + sin(cp.y * 4.0 + u_time)) * cos(cp.y * 3.5 - cos(cp.x * 2.8 - u_time * 0.8));
    float c2 = sin((cp.x + cp.y) * 4.5 - u_time * 1.3);
    float caustic = smoothstep(0.15, 0.92, abs(c1 + c2 * 0.5));

    vec3 deepSea = mix(vec3(0.015, 0.11, 0.14), vec3(0.03, 0.24, 0.28), uv.x * 0.7 + uv.y * 0.3);
    vec3 aquaGlow = vec3(0.08, 0.78, 0.68) * caustic * (0.45 + 0.55 * ptrGlow);
    vec3 cyanSpark = vec3(0.22, 0.88, 0.98) * (ptrCore * 0.85 + shockRing * 1.1);
    vec3 rimCol = mix(vec3(0.08, 0.72, 0.65), vec3(0.45, 0.98, 0.92), ptrGlow + shockRing) * (reactiveEdge * 0.75 + edgeRim * 0.65);
    col = deepSea + aquaGlow + cyanSpark + rimCol;
  } else if (u_mode < 2.5) {
    // MODE 2: Gilded Lexicon Filigree (24K Liquid Gold Foil + Interactive Specular Sheen)
    float waves = fbm3(wp * vec2(1.1, 2.4) + vec2(u_time * 0.14, 0.0));
    float tiltAngle = (uv.x - u_ptr.x) * 1.8 + (uv.y - u_ptr.y) * 0.7 + sin(u_time * 0.6) * 0.25;
    float foilSheen = exp(-tiltAngle * tiltAngle * 5.5) * (0.45 + 0.65 * u_hover);
    float filigree = smoothstep(0.55, 0.92, abs(sin(wp.x * 13.0) * cos(wp.y * 9.5 + waves * 2.0))) * edgeGlow;

    vec3 obsidianGold = mix(vec3(0.11, 0.06, 0.015), vec3(0.28, 0.16, 0.03), waves);
    vec3 goldFoil = vec3(0.95, 0.75, 0.15) * (waves * 0.35 + foilSheen * 0.65 + filigree * 0.75);
    vec3 whiteGold = vec3(1.0, 0.95, 0.72) * (ptrCore * 0.8 + shockRing * 1.15 + edgeRim * 0.6);
    col = obsidianGold + goldFoil + whiteGold + vec3(0.92, 0.68, 0.1) * reactiveEdge * 0.55;
  } else if (u_mode < 3.5) {
    // MODE 3: Outrun 3D Cyber-Grid (Perspective Grid + Gravity Well Bending + EMP)
    float horizon = 0.28 + (u_ptr.y - 0.5) * 0.14 * u_hover;
    vec3 sky = mix(vec3(0.08, 0.02, 0.18), vec3(0.32, 0.05, 0.38), clamp(uv.y / max(horizon, 0.01), 0.0, 1.0));
    float fy = max(uv.y - horizon, 0.02);
    float z = 0.35 / fy;
    float wellPull = ptrGlow * (uv.x - u_ptr.x) * 2.2;
    float gx = (uv.x - 0.5 - (u_ptr.x - 0.5) * 0.25 * u_hover - wellPull * 0.35) * z * aspect * 0.45;
    float gz = z * 1.4 - u_time * 1.8 - shockRing * 2.0;

    float lineX = smoothstep(0.86, 0.98, abs(cos(gx * 3.14159)));
    float lineZ = smoothstep(0.84, 0.98, abs(cos(gz * 3.14159)));
    float grid = max(lineX, lineZ) * smoothstep(horizon - 0.02, horizon + 0.12, uv.y);

    float sunDist = length(vec2((uv.x - mix(0.75, u_ptr.x, 0.35 * u_hover)) * aspect * 0.4, uv.y - 0.42));
    float sun = smoothstep(0.55, 0.12, sunDist) * (0.55 + 0.45 * sin(uv.y * 45.0 - u_time * 3.0));

    vec3 gridCol = mix(vec3(0.96, 0.15, 0.55), vec3(0.12, 0.85, 0.98), uv.x + ptrGlow * 0.5) * grid * (0.55 + 0.65 * ptrGlow);
    vec3 sunCol = mix(vec3(0.98, 0.2, 0.45), vec3(0.99, 0.82, 0.2), 1.0 - uv.y) * sun * 0.55;
    vec3 neonEdge = mix(vec3(0.96, 0.18, 0.52), vec3(0.15, 0.88, 0.98), sin(uv.x * 4.0 + u_time) * 0.5 + 0.5) * (reactiveEdge * 0.85 + shockRing * 1.2 + edgeRim * 0.65);
    col = sky + gridCol + sunCol + neonEdge + vec3(0.2, 0.92, 1.0) * ptrCore * 0.7;
  } else if (u_mode < 4.5) {
    // MODE 4: Molten Caldera Border (FBM Magma + Interactive Blowtorch Heat Source)
    vec2 q = vec2(fbm3(wp * vec2(1.4, 2.2) - vec2(0.0, u_time * 0.55)), fbm3(wp * vec2(1.4, 2.2) + vec2(4.2, -u_time * 0.45)));
    float lava = fbm3(wp * vec2(1.6, 2.5) + q * 1.8);
    float heat = clamp(lava * 0.85 + (uv.y * 0.35) + ptrGlow * 0.65 + shockRing * 0.8 + edgeGlow * 0.45, 0.0, 1.35);

    vec3 crust = vec3(0.09, 0.015, 0.015);
    vec3 ember = vec3(0.65, 0.08, 0.02);
    vec3 magma = vec3(0.98, 0.42, 0.04);
    vec3 whiteHot = vec3(1.0, 0.92, 0.45);

    col = mix(crust, ember, smoothstep(0.2, 0.55, heat));
    col = mix(col, magma, smoothstep(0.52, 0.85, heat));
    col = mix(col, whiteHot, smoothstep(0.82, 1.22, heat));
    col += vec3(1.0, 0.65, 0.15) * (edgeRim * 0.7 + ptrCore * 0.75 + shockRing * 0.9);
  } else if (u_mode < 5.5) {
    // MODE 5: Prismatic Leviathan Frame (Holographic Aurora + Crystal RGB Dispersion)
    float w1 = sin(wp.x * 1.6 + u_time * 0.9 + sin(wp.y * 3.5 - u_time * 0.6));
    float w2 = cos(wp.x * 2.4 - u_time * 0.7 + fbm3(wp * 1.4));
    float aurora = smoothstep(-0.3, 0.85, (w1 + w2) * 0.5);

    float phase = uv.x * 0.85 - u_time * 0.18 + aurora * 0.38 + ptrGlow * 0.45 + shockRing * 0.5;
    vec3 rainbow = 0.52 + 0.48 * cos(6.28318 * (phase + vec3(0.0, 0.33, 0.67)));

    vec2 cell = floor(wp * vec2(3.5, 4.0));
    float crystal = hash21(cell);
    float sparkle = smoothstep(0.82, 0.99, sin(u_time * 2.5 + crystal * 18.0)) * (0.3 + 0.7 * ptrGlow);

    vec3 baseVoid = vec3(0.05, 0.04, 0.14);
    col = mix(baseVoid, rainbow * 0.72, aurora * 0.68 + ptrGlow * 0.42);
    col += rainbow * (reactiveEdge * 0.85 + shockRing * 1.15);
    col += vec3(0.95, 0.98, 1.0) * (sparkle * 0.45 + ptrCore * 0.75 + edgeRim * 0.55);
  } else {
    // MODE 6: Cosmic Event Horizon (Interactive Black Hole Lensing + Accretion Spiral)
    vec2 bhCenter = mix(vec2(aspect * 0.74, 0.5), ptr, 0.42 + 0.38 * u_hover);
    vec2 toBh = vec2((p.x - bhCenter.x) * 0.55, p.y - bhCenter.y);
    float r = length(toBh);
    float ang = atan(toBh.y, toBh.x);

    // Gravitational lensing coordinate warp
    vec2 lensedP = wp - (toBh / (r * r + 0.14)) * (0.09 + 0.06 * u_hover);
    float spiral = sin(ang * 3.0 - 4.2 / (r + 0.18) - u_time * 2.1);
    float disk = exp(-abs(r - 0.34) * 4.2) * (0.65 + 0.35 * spiral);
    float photonRing = exp(-abs(r - 0.18) * 18.0) * (0.9 + 0.5 * u_hover);
    float eventHorizon = smoothstep(0.08, 0.18, r);

    // Starfield
    vec2 starGrid = floor(lensedP * vec2(7.0, 9.0));
    float starHash = hash21(starGrid);
    float star = smoothstep(0.93, 0.995, starHash) * (0.55 + 0.45 * sin(u_time * 3.0 + starHash * 40.0));

    float neb = fbm3(lensedP * vec2(1.1, 1.8) + vec2(u_time * 0.12, -u_time * 0.08));
    vec3 spaceBg = mix(vec3(0.03, 0.015, 0.09), vec3(0.18, 0.05, 0.34), neb);
    vec3 diskCol = mix(vec3(0.75, 0.32, 0.98), vec3(0.99, 0.84, 0.25), clamp(spiral * 0.5 + 0.5 + ptrGlow * 0.4, 0.0, 1.0));
    vec3 jetCyan = vec3(0.22, 0.78, 0.99) * exp(-r * 2.2) * (0.4 + 0.6 * ptrGlow);

    col = (spaceBg + diskCol * disk * 1.15 + jetCyan + vec3(1.0, 0.96, 0.8) * star * 0.7) * eventHorizon;
    col += vec3(1.0, 0.9, 0.45) * photonRing * 1.1;
    vec3 cosmicRim = mix(vec3(0.99, 0.82, 0.22), vec3(0.85, 0.42, 0.98), sin(uv.x * 3.5 - u_time * 1.4) * 0.5 + 0.5);
    col += cosmicRim * (reactiveEdge * 0.9 + shockRing * 1.3 + edgeRim * 0.75);
  }

  // Keep interior text crystal-clear while allowing edges & pointer interactions to pop
  float textContrastGuard = mix(0.78, 1.0, clamp(edgeGlow * 0.85 + ptrGlow * 0.65 + shockRing * 0.85, 0.0, 1.0));
  col *= textContrastGuard;

  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}
`;

const SHADER_INTERNAL_W = 280;
const SHADER_INTERNAL_H = 48;
let _sharedShaderEngine = null;

function getSharedRowShaderEngine() {
  if (_sharedShaderEngine) return _sharedShaderEngine;

  const instances = new Set();
  let gl = null;
  let glCanvas = null;
  let program = null;
  let uniforms = null;
  let rafId = null;
  let lastFrameTime = 0;
  const startTime = performance.now();

  try {
    glCanvas = document.createElement('canvas');
    glCanvas.width = SHADER_INTERNAL_W;
    glCanvas.height = SHADER_INTERNAL_H;
    gl = glCanvas.getContext('webgl', {
      alpha: false,
      depth: false,
      stencil: false,
      antialias: false,
      preserveDrawingBuffer: true,
      powerPreference: 'low-power',
    });

    if (gl) {
      const compile = (type, src) => {
        const s = gl.createShader(type);
        gl.shaderSource(s, src);
        gl.compileShader(s);
        if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
          gl.deleteShader(s);
          return null;
        }
        return s;
      };
      const vs = compile(gl.VERTEX_SHADER, ROW_VERTEX_SHADER_SRC);
      const fs = compile(gl.FRAGMENT_SHADER, ROW_FRAGMENT_SHADER_SRC);
      if (vs && fs) {
        program = gl.createProgram();
        gl.attachShader(program, vs);
        gl.attachShader(program, fs);
        gl.linkProgram(program);
        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
          gl.useProgram(program);
          const buf = gl.createBuffer();
          gl.bindBuffer(gl.ARRAY_BUFFER, buf);
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
            gl.STATIC_DRAW
          );
          const aPos = gl.getAttribLocation(program, 'a_pos');
          gl.enableVertexAttribArray(aPos);
          gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
          gl.viewport(0, 0, SHADER_INTERNAL_W, SHADER_INTERNAL_H);

          uniforms = {
            u_res: gl.getUniformLocation(program, 'u_res'),
            u_time: gl.getUniformLocation(program, 'u_time'),
            u_ptr: gl.getUniformLocation(program, 'u_ptr'),
            u_vel: gl.getUniformLocation(program, 'u_vel'),
            u_hover: gl.getUniformLocation(program, 'u_hover'),
            u_pulse: gl.getUniformLocation(program, 'u_pulse'),
            u_mode: gl.getUniformLocation(program, 'u_mode'),
          };
        } else {
          gl = null;
        }
      } else {
        gl = null;
      }
    }
  } catch (e) {
    gl = null;
  }

  function renderFallback2D(ctx, w, h, inst, t) {
    const palettes = {
      1: ['#042f2e', '#0d9488', '#2dd4bf'],
      2: ['#1c1004', '#a16207', '#facc15'],
      3: ['#1e0a3c', '#db2777', '#22d3ee'],
      4: ['#2a0606', '#dc2626', '#facc15'],
      5: ['#180b30', '#c026d3', '#38bdf8'],
      6: ['#0d0622', '#7e22ce', '#fde047'],
    };
    const pal = palettes[inst.mode] || palettes[1];
    const grad = ctx.createLinearGradient(0, 0, w, h);
    const shift = (Math.sin(t * 1.5) + 1) * 0.25;
    grad.addColorStop(0, pal[0]);
    grad.addColorStop(0.35 + shift, pal[1]);
    grad.addColorStop(1, pal[0]);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w, h);

    const px = inst.ptrX * w;
    const py = inst.ptrY * h;
    const rad = ctx.createRadialGradient(px, py, 2, px, py, w * (0.28 + inst.hover * 0.2));
    rad.addColorStop(0, pal[2] + 'aa');
    rad.addColorStop(1, 'transparent');
    ctx.fillStyle = rad;
    ctx.fillRect(0, 0, w, h);
  }

  function tick(now) {
    rafId = null;
    if (document.hidden || instances.size === 0) return;

    let anyVisible = false;
    let anyActiveInteraction = false;
    instances.forEach(inst => {
      if (inst.isVisible && inst.mode > 0) {
        anyVisible = true;
        if (inst.isPointerOver || inst.hover > 0.04 || inst.pulseStart > 0) {
          anyActiveInteraction = true;
        }
      }
    });

    if (!anyVisible) return;

    // 60 FPS when interacting/hovering/pulsing; 30 FPS gentle power-saving mode when idle
    const minDelta = anyActiveInteraction ? 15 : 32;
    if (now - lastFrameTime < minDelta) {
      rafId = requestAnimationFrame(tick);
      return;
    }
    lastFrameTime = now;
    const elapsed = (now - startTime) * 0.001;

    instances.forEach(inst => {
      if (!inst.isVisible || inst.mode <= 0 || !inst.ctx2d) return;

      const prevX = inst.ptrX;
      const prevY = inst.ptrY;
      if (!inst.isPointerOver) {
        // Gentle autonomous drift when idle so the shader feels alive
        inst.targetX = 0.5 + Math.sin(elapsed * 0.65 + inst.seed) * 0.32;
        inst.targetY = 0.5 + Math.cos(elapsed * 0.9 + inst.seed) * 0.22;
      }

      inst.ptrX += (inst.targetX - inst.ptrX) * 0.18;
      inst.ptrY += (inst.targetY - inst.ptrY) * 0.18;
      inst.velX = inst.velX * 0.82 + (inst.ptrX - prevX) * 2.8;
      inst.velY = inst.velY * 0.82 + (inst.ptrY - prevY) * 2.8;

      const targetHover = inst.isPointerOver ? 1.0 : 0.0;
      inst.hover += (targetHover - inst.hover) * (inst.isPointerOver ? 0.18 : 0.07);
      if (inst.hover < 0.002) inst.hover = 0;

      let pulseAge = -1.0;
      if (inst.pulseStart > 0) {
        pulseAge = (now - inst.pulseStart) / 850.0;
        if (pulseAge > 1.0) {
          inst.pulseStart = 0;
          pulseAge = -1.0;
        }
      }

      if (gl && uniforms) {
        gl.uniform2f(uniforms.u_res, inst.rectW || 680, inst.rectH || 54);
        gl.uniform1f(uniforms.u_time, elapsed + inst.seed);
        gl.uniform2f(uniforms.u_ptr, inst.ptrX, inst.ptrY);
        gl.uniform2f(uniforms.u_vel, inst.velX, inst.velY);
        gl.uniform1f(uniforms.u_hover, inst.hover);
        gl.uniform3f(uniforms.u_pulse, inst.pulseX, inst.pulseY, pulseAge);
        gl.uniform1f(uniforms.u_mode, inst.mode);
        gl.drawArrays(gl.TRIANGLES, 0, 6);

        inst.ctx2d.drawImage(glCanvas, 0, 0);
      } else {
        renderFallback2D(inst.ctx2d, SHADER_INTERNAL_W, SHADER_INTERNAL_H, inst, elapsed + inst.seed);
      }
    });

    rafId = requestAnimationFrame(tick);
  }

  function wake() {
    if (!rafId && !document.hidden && instances.size > 0) {
      rafId = requestAnimationFrame(tick);
    }
  }

  let io = null;
  if (typeof IntersectionObserver !== 'undefined') {
    io = new IntersectionObserver(
      entries => {
        entries.forEach(entry => {
          const inst = entry.target.__rowShaderInst;
          if (inst) {
            inst.isVisible = entry.isIntersecting;
            if (entry.isIntersecting) {
              const r = entry.boundingClientRect;
              if (r && r.width > 0) {
                inst.rectW = r.width;
                inst.rectH = r.height;
              }
            }
          }
        });
        wake();
      },
      { rootMargin: '80px' }
    );
  }

  document.addEventListener('visibilitychange', () => {
    if (!document.hidden) wake();
  });

  // Gentle scroll reactivity on mobile & desktop
  window.addEventListener(
    'scroll',
    () => {
      instances.forEach(inst => {
        if (inst.isVisible && !inst.isPointerOver) {
          inst.hover = Math.min(1.0, inst.hover + 0.16);
          inst.velY = clampNum((inst.velY || 0) + 0.08, -0.4, 0.4);
        }
      });
      wake();
    },
    { passive: true }
  );

  function clampNum(v, lo, hi) {
    return Math.max(lo, Math.min(hi, v));
  }

  _sharedShaderEngine = {
    register(inst) {
      instances.add(inst);
      if (io && inst.canvas) {
        inst.canvas.__rowShaderInst = inst;
        io.observe(inst.canvas);
      }
      wake();
    },
    unregister(inst) {
      instances.delete(inst);
      if (io && inst.canvas) {
        io.unobserve(inst.canvas);
        delete inst.canvas.__rowShaderInst;
      }
    },
    wake,
  };

  return _sharedShaderEngine;
}

function RowShaderCanvas({ frameId }) {
  const mode = ROW_SHADER_MODES[frameId] || 0;
  const canvasRef = useRefL(null);

  useEffectL(() => {
    const canvas = canvasRef.current;
    if (!canvas || mode <= 0) return;

    const parent = canvas.parentElement;
    if (!parent) return;

    const ctx2d = canvas.getContext('2d', { alpha: false });
    const rect = parent.getBoundingClientRect();
    const inst = {
      canvas,
      ctx2d,
      mode,
      isVisible: true,
      rectW: rect.width || 640,
      rectH: rect.height || 52,
      seed: Math.random() * 12.0,
      ptrX: 0.5,
      ptrY: 0.5,
      targetX: 0.5,
      targetY: 0.5,
      velX: 0,
      velY: 0,
      hover: 0,
      isPointerOver: false,
      pulseX: 0.5,
      pulseY: 0.5,
      pulseStart: 0,
    };

    const engine = getSharedRowShaderEngine();
    engine.register(inst);

    const updateCoords = (clientX, clientY) => {
      const r = parent.getBoundingClientRect();
      if (r.width > 0 && r.height > 0) {
        inst.rectW = r.width;
        inst.rectH = r.height;
        inst.targetX = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
        inst.targetY = Math.max(0, Math.min(1, (clientY - r.top) / r.height));
      }
    };

    let downX = 0;
    let downY = 0;

    const triggerTapPulse = (clientX, clientY) => {
      updateCoords(clientX, clientY);
      inst.ptrX = inst.targetX;
      inst.ptrY = inst.targetY;
      inst.pulseX = inst.targetX;
      inst.pulseY = inst.targetY;
      inst.pulseStart = performance.now();
      inst.hover = 1.0;
      engine.wake();
    };

    const onPointerEnter = e => {
      if (e.pointerType !== 'mouse') return;
      inst.isPointerOver = true;
      updateCoords(e.clientX, e.clientY);
      engine.wake();
    };

    const onPointerMove = e => {
      if (e.pointerType !== 'mouse') return;
      inst.isPointerOver = true;
      updateCoords(e.clientX, e.clientY);
      engine.wake();
    };

    const onPointerDown = e => {
      downX = e.clientX;
      downY = e.clientY;
      if (e.pointerType === 'mouse') {
        inst.isPointerOver = true;
        triggerTapPulse(e.clientX, e.clientY);
      }
    };

    const onPointerUp = e => {
      if (e.pointerType !== 'mouse') {
        // Mobile / touch: only fire the reactive shader shockwave on a deliberate tap (not a scroll drag)
        const dist = Math.hypot(e.clientX - downX, e.clientY - downY);
        if (dist < 14) {
          inst.isPointerOver = false;
          triggerTapPulse(e.clientX, e.clientY);
        }
      }
    };

    const onPointerLeave = e => {
      if (e.pointerType && e.pointerType !== 'mouse') return;
      inst.isPointerOver = false;
      engine.wake();
    };

    parent.addEventListener('pointerenter', onPointerEnter, { passive: true });
    parent.addEventListener('pointermove', onPointerMove, { passive: true });
    parent.addEventListener('pointerdown', onPointerDown, { passive: true });
    parent.addEventListener('pointerup', onPointerUp, { passive: true });
    parent.addEventListener('pointerleave', onPointerLeave, { passive: true });

    return () => {
      parent.removeEventListener('pointerenter', onPointerEnter);
      parent.removeEventListener('pointermove', onPointerMove);
      parent.removeEventListener('pointerdown', onPointerDown);
      parent.removeEventListener('pointerup', onPointerUp);
      parent.removeEventListener('pointerleave', onPointerLeave);
      engine.unregister(inst);
    };
  }, [mode]);

  if (mode <= 0) return null;

  return (
    <canvas
      ref={canvasRef}
      width={SHADER_INTERNAL_W}
      height={SHADER_INTERNAL_H}
      className="lb-row-shader-canvas"
      aria-hidden="true"
    />
  );
}

// ============================================================================
// 6. COSMETIC SHOP & SITE CUSTOMIZER MODAL + LIVE LEADERBOARD PANEL
// ============================================================================
const SHOP_CATEGORY_TABS = [
  { id: 'themes',     icon: '🎨', label: 'Themes',          sub: 'Site Palettes' },
  { id: 'titles',     icon: '👑', label: 'Titles',          sub: 'Rank Badges' },
  { id: 'nameplates', icon: '✨', label: 'Nameplates',      sub: 'Username Auras' },
  { id: 'frames',     icon: '🖼️', label: 'Row Frames',      sub: 'Leaderboard Borders' },
  { id: 'effects',    icon: '🎆', label: 'Confetti FX',     sub: 'Win Bursts' },
  { id: 'sounds',     icon: '🔊', label: 'Synth Packs',     sub: 'Audio Fanfares' },
  { id: 'reactions',  icon: '🤩', label: 'Super Reactions', sub: 'Animated Chat FX' },
  { id: 'perks',      icon: '⚡', label: 'Perks & Relics',  sub: 'Boosts & Shields' },
  { id: 'all',        icon: '🌐', label: 'All Cosmetics',   sub: 'Full Catalog' },
];

function DopamineShopModal({
  open,
  onClose,
  dopamine,
  isAdmin,
  onToggleEnabled,
  onToggleSound,
  onAddDemoPoints,
  onUnlockOrEquip,
  onPreviewWin,
  onPreviewRoast,
  onPreviewEffect,
  onSaveUsername,
}) {
  const [activeCategory, setActiveCategory] = useStateL('themes');
  const [shopFilter, setShopFilter] = useStateL('all'); // 'all' | 'unlocked' | 'affordable' | 'high_roller'
  const [previewingThemeId, setPreviewingThemeId] = useStateL(null);
  const [previewingReactionId, setPreviewingReactionId] = useStateL(null);
  const previewReactionTimerRef = useRefL(null);
  const previewThemeTimerRef = useRefL(null);
  const currentThemeRef = useRefL(dopamine.equippedTheme);
  currentThemeRef.current = dopamine.equippedTheme;

  useEffectL(() => {
    if (!open) return;
    const previous = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = previous; };
  }, [open]);

  useEffectL(() => {
    if (previewThemeTimerRef.current) clearTimeout(previewThemeTimerRef.current);
    previewThemeTimerRef.current = null;
    setPreviewingThemeId(null);
    applyDopamineThemeToDom(currentThemeRef.current);
  }, [dopamine.equippedTheme, open]);

  useEffectL(() => {
    return () => {
      if (previewThemeTimerRef.current) {
        clearTimeout(previewThemeTimerRef.current);
      }
      if (previewReactionTimerRef.current) {
        clearTimeout(previewReactionTimerRef.current);
      }
      applyDopamineThemeToDom(currentThemeRef.current);
    };
  }, []);

  if (!open) return null;

  // STRICT ADMIN CHECK: Only show admin controls when explicitly in Admin Mode!
  const adminAuthed = Boolean(isAdmin);
  const unlockedSet = new Set(dopamine.unlockedItems || []);
  const equippedThemeObj = getEquippedThemeObj(dopamine.equippedTheme);
  const equippedTitleObj = getEquippedTitleObj(dopamine.equippedTitle);
  const equippedNameplateObj = getEquippedNameplateObj(dopamine.equippedNameplate);
  const equippedFrameObj = getEquippedFrameObj(dopamine.equippedFrame);
  const equippedEffectObj = getEquippedEffectObj(dopamine.equippedEffect);
  const equippedSoundObj = getEquippedSoundObj(dopamine.equippedSound);
  const permMult = getPermanentPointMultiplier(dopamine);
  const activeTotalMult = getTotalActivePointMultiplier(dopamine);
  const shieldsCount = Number(dopamine.perks?.streak_shields || 0);
  const doubleXpCount = Number(dopamine.perks?.double_xp_charges || 0);

  // Calculate collector progress and next milestone across all non-repeatable shop items
  const allNonRepeatableItems = [
    ...DOPAMINE_SHOP_ITEMS.themes,
    ...DOPAMINE_SHOP_ITEMS.titles,
    ...DOPAMINE_SHOP_ITEMS.nameplates,
    ...(DOPAMINE_SHOP_ITEMS.frames || []),
    ...DOPAMINE_SHOP_ITEMS.effects,
    ...DOPAMINE_SHOP_ITEMS.sounds,
    ...(DOPAMINE_SHOP_ITEMS.reactions || []),
    ...DOPAMINE_SHOP_ITEMS.perks.filter(p => !p.repeatable),
  ];
  const unlockedCount = allNonRepeatableItems.filter(it => it.cost === 0 || unlockedSet.has(it.id)).length;
  const totalCollectibles = allNonRepeatableItems.length;
  const collectorPct = totalCollectibles > 0 ? Math.round((unlockedCount / totalCollectibles) * 100) : 100;

  const lockedItemsByCost = allNonRepeatableItems
    .filter(it => it.cost > 0 && !unlockedSet.has(it.id))
    .sort((a, b) => a.cost - b.cost);
  const nextMilestoneItem = lockedItemsByCost[0] || null;
  const milestonePct = nextMilestoneItem
    ? Math.min(100, Math.round(((dopamine.points || 0) / nextMilestoneItem.cost) * 100))
    : 100;

  function handleReactionSpawnPreview(reactionId) {
    if (previewReactionTimerRef.current) clearTimeout(previewReactionTimerRef.current);
    setPreviewingReactionId(null);
    requestAnimationFrame(() => {
      setPreviewingReactionId(reactionId);
      previewReactionTimerRef.current = setTimeout(() => {
        setPreviewingReactionId(null);
        previewReactionTimerRef.current = null;
      }, 1250);
    });
  }

  function handleTemporaryThemePreview(themeId) {
    if (previewThemeTimerRef.current) {
      clearTimeout(previewThemeTimerRef.current);
    }
    setPreviewingThemeId(themeId);
    applyDopamineThemeToDom(themeId, true);
    previewThemeTimerRef.current = setTimeout(() => {
      setPreviewingThemeId(null);
      previewThemeTimerRef.current = null;
      applyDopamineThemeToDom(currentThemeRef.current);
    }, 4000);
  }

  function filterCategoryItems(items) {
    return (items || []).filter(item => {
      const isUnlocked = item.cost === 0 || unlockedSet.has(item.id);
      const canAfford = (dopamine.points || 0) >= item.cost;
      if (shopFilter === 'unlocked') return isUnlocked;
      if (shopFilter === 'affordable') return !isUnlocked && canAfford;
      if (shopFilter === 'high_roller') return item.cost >= 1000;
      return true;
    });
  }

  function renderShopSection(categoryKey, title, subtitle, items, equippedId) {
    const filteredItems = filterCategoryItems(items);
    if (filteredItems.length === 0) {
      if (activeCategory === 'all') return null;
      return (
        <div className="shop-section">
          <div className="shop-section-head">
            <h4 className="serif shop-section-title">{title}</h4>
            <span className="shop-section-sub">{subtitle}</span>
          </div>
          <div className="learn-card" style={{ textAlign: 'center', padding: 28, color: 'var(--fg-mute)' }}>
            No items match the current filter in this category. Try switching to <strong>All Items</strong>!
          </div>
        </div>
      );
    }

    return (
      <div className="shop-section" key={categoryKey}>
        <div className="shop-section-head">
          <h4 className="serif shop-section-title">{title}</h4>
          <span className="shop-section-sub">{subtitle}</span>
        </div>
        <div className="shop-items-grid">
          {filteredItems.map(item => {
            const isRepeatable = Boolean(item.repeatable);
            const isUnlocked = !isRepeatable && (item.cost === 0 || unlockedSet.has(item.id));
            const isEquipped = !isRepeatable && equippedId === item.id;
            const canAfford = (dopamine.points || 0) >= item.cost;
            const rarityMeta = getShopRarityMeta(item.rarity);
            const ownedPerkCount = item.perkKey ? Number(dopamine.perks?.[item.perkKey] || 0) : 0;

            return (
              <div
                key={item.id}
                className={`shop-item-card rarity-${item.rarity || 'common'} ${isEquipped ? 'is-equipped' : ''} ${isUnlocked ? 'is-unlocked' : 'is-locked'}`}
              >
                <div className="shop-item-rarity-row">
                  <span
                    className="shop-rarity-pill mono"
                    style={{ color: rarityMeta.color, borderColor: `${rarityMeta.color}55` }}
                  >
                    {rarityMeta.icon} {rarityMeta.label}
                  </span>
                  {isRepeatable ? (
                    <span className="shop-badge cost mono">🪙 {item.cost.toLocaleString()} pts</span>
                  ) : isEquipped ? (
                    <span className="shop-badge equipped mono">EQUIPPED</span>
                  ) : isUnlocked ? (
                    <span className="shop-badge unlocked mono">
                      {categoryKey === 'perks' ? 'ACTIVE RELIC' : 'UNLOCKED'}
                    </span>
                  ) : (
                    <span className="shop-badge cost mono">🪙 {item.cost.toLocaleString()} pts</span>
                  )}
                </div>

                <div className="shop-item-top">
                  <span className="shop-item-name">{item.name}</span>
                </div>

                <p className="shop-item-desc">{item.desc}</p>

                {/* Category-Specific Visual Previews */}
                {categoryKey === 'themes' && Array.isArray(item.previewColors) && (
                  <div className="shop-theme-preview-row">
                    <div className="shop-theme-swatches">
                      {item.previewColors.map((hex, i) => (
                        <span
                          key={i}
                          className="shop-theme-swatch"
                          style={{ background: hex }}
                          title={`Palette swatch ${hex}`}
                        />
                      ))}
                    </div>
                    {!isEquipped && (
                      <button
                        type="button"
                        className="btn shop-mini-preview-btn mono"
                        onClick={() => handleTemporaryThemePreview(item.id)}
                      >
                        {previewingThemeId === item.id ? '✨ Previewing…' : '👁️ Preview 4s'}
                      </button>
                    )}
                  </div>
                )}

                {categoryKey === 'nameplates' && (
                  <div className="shop-nameplate-preview shop-nameplate-preview-box">
                    <span className="mono" style={{ fontSize: 10, color: 'var(--fg-mute)', marginRight: 8 }}>LIVE PREVIEW:</span>
                    <span className={`seamenese-nameplate leaderboard-player-name ${item.cssClass || 'np-style-default'}`}>
                      {dopamine.username || 'Jorn'}
                    </span>
                  </div>
                )}

                {categoryKey === 'frames' && (
                  <div
                    className={`shop-frame-preview-row leaderboard-row ${item.cssClass || 'row-frame-default'}`}
                    title="Move cursor, drag finger, or tap to interact with the live WebGL shader!"
                  >
                    <RowShaderCanvas frameId={item.id} />
                    <span className="mono lb-cell-content" style={{ fontSize: 11, fontWeight: 700, color: '#fbbf24' }}>
                      🥇 #1
                    </span>
                    <span
                      className={`seamenese-nameplate lb-cell-content ${equippedNameplateObj.cssClass}`}
                      style={{ fontSize: 12.5 }}
                    >
                      {dopamine.username || 'Jorn'}
                    </span>
                    <span className="mono lb-cell-content" style={{ fontSize: 9.5, color: 'rgba(255,255,255,0.72)', marginLeft: 'auto' }}>
                      {item.shaderMode > 0 ? '✨ Touch / Hover' : 'Standard'}
                    </span>
                    <span className="mono lb-cell-content" style={{ fontSize: 11, color: '#fbbf24', fontWeight: 700 }}>
                      🪙 {(dopamine.points || 13200).toLocaleString()}p
                    </span>
                  </div>
                )}

                {categoryKey === 'titles' && (
                  <div className="shop-nameplate-preview">
                    <span className="learn-title-badge" style={{ fontSize: 12 }}>{item.name}</span>
                  </div>
                )}

                {categoryKey === 'effects' && item.emojis && (
                  <div className="shop-effect-preview-row">
                    <div className="shop-item-emojis">{item.emojis.join(' ')}</div>
                    <button
                      type="button"
                      className="btn shop-mini-preview-btn mono"
                      onClick={() => onPreviewEffect && onPreviewEffect(item.id)}
                    >
                      🎉 Preview Burst
                    </button>
                  </div>
                )}

                {categoryKey === 'sounds' && (
                  <div className="shop-effect-preview-row">
                    <span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>
                      🎵 WebAudio Synth
                    </span>
                    <button
                      type="button"
                      className="btn shop-mini-preview-btn mono"
                      onClick={() => playDopamineSynth('win', true, item.id)}
                    >
                      🔊 Test Sound
                    </button>
                  </div>
                )}

                {categoryKey === 'reactions' && (
                  <div className="shop-effect-preview-row" style={{ alignItems: 'center', gap: 10 }}>
                    <span
                      className={`chat-reaction-pill is-super ${item.animClass || 'super-anim-wave'} ${previewingReactionId === item.id ? 'is-spawning' : ''}`}
                      style={{ pointerEvents: 'none', position: 'relative' }}
                    >
                      {previewingReactionId === item.id && (
                        <span className="chat-super-spawn-particles" aria-hidden="true">
                          {(item.particles || ['✨', '✦', item.emoji]).map((p, idx) => (
                            <span key={idx} className={`chat-super-particle p-${idx}`}>{p}</span>
                          ))}
                        </span>
                      )}
                      <span className="chat-reaction-emoji">{item.emoji}</span>
                      <span className="chat-reaction-count mono">1</span>
                      <span className="chat-super-sparkle" aria-hidden="true">✦</span>
                    </span>
                    <button
                      type="button"
                      className="btn shop-mini-preview-btn mono"
                      onClick={() => handleReactionSpawnPreview(item.id)}
                    >
                      ▶ Preview Spawn
                    </button>
                  </div>
                )}

                {categoryKey === 'perks' && (
                  <div className="shop-perk-meta mono">
                    {isRepeatable ? (
                      <span style={{ color: '#38bdf8' }}>
                        📦 Currently Owned: <strong>{ownedPerkCount}</strong>
                      </span>
                    ) : (
                      <span style={{ color: '#fbbf24' }}>
                        ✨ Permanent Bonus: <strong>+{Math.round((item.multiplierBonus || 0) * 100)}% Points</strong>
                      </span>
                    )}
                  </div>
                )}

                <div className="shop-item-actions">
                  {isRepeatable ? (
                    <button
                      type="button"
                      className={`btn ${canAfford ? 'btn-warm' : ''}`}
                      disabled={!canAfford}
                      style={{ width: '100%', justifyContent: 'center' }}
                      onClick={() => onUnlockOrEquip('perks', item)}
                    >
                      {canAfford
                        ? `⚡ Buy +${item.grantAmount || 1} (${item.cost.toLocaleString()} pts)`
                        : `Need ${(item.cost - (dopamine.points || 0)).toLocaleString()} more pts`}
                    </button>
                  ) : categoryKey === 'perks' && isUnlocked ? (
                    <button type="button" className="btn" disabled style={{ width: '100%', justifyContent: 'center' }}>
                      ✓ Permanent Relic Active
                    </button>
                  ) : categoryKey === 'reactions' && isUnlocked ? (
                    <button type="button" className="btn" disabled style={{ width: '100%', justifyContent: 'center' }}>
                      ✓ Permanently Unlocked (Use in Chat)
                    </button>
                  ) : isEquipped ? (
                    <button type="button" className="btn" disabled style={{ width: '100%', justifyContent: 'center' }}>
                      ✓ Equipped
                    </button>
                  ) : isUnlocked ? (
                    <button
                      type="button"
                      className="btn btn-accent"
                      style={{ width: '100%', justifyContent: 'center' }}
                      onClick={() => onUnlockOrEquip(categoryKey, item)}
                    >
                      Equip Now
                    </button>
                  ) : (
                    <button
                      type="button"
                      className={`btn ${canAfford ? 'btn-warm' : ''}`}
                      disabled={!canAfford}
                      style={{ width: '100%', justifyContent: 'center' }}
                      onClick={() => onUnlockOrEquip(categoryKey, item)}
                    >
                      {canAfford
                        ? `🔓 Unlock (${item.cost.toLocaleString()} pts)`
                        : `Need ${(item.cost - (dopamine.points || 0)).toLocaleString()} more pts`}
                    </button>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  return (
    <div className="learn-modal-backdrop" onClick={onClose}>
      <div className="learn-modal-sheet shop-modal-sheet" onClick={e => e.stopPropagation()}>
        <header className="shop-modal-header">
          <div>
            <div className="eyebrow"><span className="rule"></span>Silly Dopamine Vault · High-Roller Cosmetics &amp; Boosters</div>
            <h3 className="serif" style={{ fontSize: 28, margin: '4px 0 0', color: 'var(--paper)' }}>
              🛒 Seamenese Grand Customization Emporium
            </h3>
          </div>
          <button type="button" className="gear-btn" onClick={onClose}>Close ✕</button>
        </header>

        {/* Top Wallet + Active Multipliers + Strict Admin Gating */}
        <div className="shop-wallet-bar">
          <div className="shop-wallet-stats">
            <div className="shop-wallet-pill mono">
              <span>🪙 Balance:</span>
              <strong style={{ color: '#fbbf24', fontSize: 16 }}>
                {Number(dopamine.points || 0).toLocaleString()} pts
              </strong>
            </div>
            <div className="shop-wallet-pill mono">
              <span>🔥 Streak:</span>
              <strong>{dopamine.streak} ({getComboLabel(dopamine.streak)})</strong>
            </div>
            <div className="shop-wallet-pill mono" title="Total active point multiplier (Combo × Permanent Relics × 2x Overdrive)">
              <span>🚀 Total Multiplier:</span>
              <strong style={{ color: '#34d399' }}>{activeTotalMult}x</strong>
              {permMult > 1 && (
                <span style={{ fontSize: 10, color: '#fbbf24' }}> (Relics {permMult.toFixed(2).replace(/\.00$/, '')}x)</span>
              )}
            </div>
            <div className="shop-wallet-pill mono">
              <span>🛡️ Shields: <strong>{shieldsCount}</strong></span>
              <span style={{ opacity: 0.4 }}>|</span>
              <span>⚡ 2x Overdrive: <strong>{doubleXpCount}</strong></span>
            </div>
          </div>

          <div className="shop-wallet-controls">
            <button
              type="button"
              className={`chip ${dopamine.enabled ? 'on' : ''}`}
              onClick={onToggleEnabled}
            >
              ⚡ Dopamine Mode: {dopamine.enabled ? 'ON' : 'OFF'}
            </button>
            <button
              type="button"
              className={`chip ${dopamine.soundEnabled ? 'on' : ''}`}
              onClick={onToggleSound}
            >
              {dopamine.soundEnabled ? '🔊 Synth SFX: ON' : '🔇 Synth SFX: OFF'}
            </button>
            {adminAuthed && (
              <button
                type="button"
                className="btn btn-warm"
                style={{ padding: '6px 12px', fontSize: 11 }}
                onClick={onAddDemoPoints}
                title="Admin Mode only: Add 100 points to test unlocking cosmetics"
              >
                🎁 +100 Demo Points (Admin)
              </button>
            )}
          </div>
        </div>

        {/* Active Player Loadout (Boxed Categories) & Collector Milestone Banner */}
        <div className="shop-loadout-banner">
          <div className="shop-loadout-header-row">
            <div className="shop-loadout-identity-box">
              <span className="shop-loadout-box-label mono">Active Identity</span>
              <div className="shop-loadout-identity-main">
                <span className={`seamenese-nameplate leaderboard-player-name ${equippedNameplateObj.cssClass}`} style={{ fontSize: 16 }}>
                  {dopamine.username || 'Unnamed Tadpole'}
                </span>
                <span className="learn-title-badge">{equippedTitleObj.name}</span>
              </div>
            </div>

            <div className="shop-loadout-progress-col">
              <div className="shop-milestone-row mono">
                <span>🏆 Collector Vault: <strong>{unlockedCount}/{totalCollectibles}</strong> ({collectorPct}%)</span>
                {nextMilestoneItem ? (
                  <span style={{ color: '#fbbf24' }}>
                    🎯 Next Unlock: <strong>{nextMilestoneItem.name}</strong> ({nextMilestoneItem.cost.toLocaleString()}p)
                  </span>
                ) : (
                  <span style={{ color: '#34d399' }}>👑 100% Vault Complete!</span>
                )}
              </div>
              <div className="shop-milestone-bar-track">
                <div
                  className="shop-milestone-bar-fill"
                  style={{ width: `${nextMilestoneItem ? milestonePct : 100}%` }}
                />
              </div>
            </div>
          </div>

          <div className="shop-loadout-grid" role="list" aria-label="Equipped cosmetic categories">
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">🎨 Theme</span>
              <strong className="shop-loadout-box-value">{equippedThemeObj.name}</strong>
            </div>
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">👑 Title</span>
              <strong className="shop-loadout-box-value">{equippedTitleObj.name}</strong>
            </div>
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">✨ Nameplate</span>
              <strong className="shop-loadout-box-value">{equippedNameplateObj.name}</strong>
            </div>
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">🖼️ Row Frame</span>
              <strong className="shop-loadout-box-value">{equippedFrameObj.name}</strong>
            </div>
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">🎆 Celebration FX</span>
              <strong className="shop-loadout-box-value">{equippedEffectObj.name}</strong>
            </div>
            <div className="shop-loadout-card" role="listitem">
              <span className="shop-loadout-box-label mono">🔊 Synth Audio</span>
              <strong className="shop-loadout-box-value">{equippedSoundObj.name}</strong>
            </div>
          </div>
        </div>

        {adminAuthed && (
          <div className="shop-preview-bar">
            <span className="mono" style={{ fontSize: 11, color: 'var(--fg-mute)' }}>
              Admin FX Preview (visual/audio only — does NOT alter points or streak):
            </span>
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
              <button type="button" className="btn" style={{ fontSize: 11, padding: '5px 10px' }} onClick={onPreviewWin}>
                🎉 Test Win Burst (+Fanfare)
              </button>
              <button type="button" className="btn" style={{ fontSize: 11, padding: '5px 10px', color: 'var(--accent-warm)' }} onClick={onPreviewRoast}>
                🤬 Test Disappointed Jorn Roast
              </button>
            </div>
          </div>
        )}

        {/* Category Navigation Tabs + Filter Bar */}
        <div className="shop-nav-toolbar">
          <div className="shop-category-tabs">
            {SHOP_CATEGORY_TABS.map(tab => (
              <button
                key={tab.id}
                type="button"
                className={`shop-cat-tab ${activeCategory === tab.id ? 'active' : ''}`}
                onClick={() => setActiveCategory(tab.id)}
              >
                <span className="shop-cat-icon">{tab.icon}</span>
                <span className="shop-cat-main">{tab.label}</span>
              </button>
            ))}
          </div>

          <div className="shop-filter-row">
            {[
              ['all', 'All Tiers'],
              ['unlocked', '✓ Unlocked'],
              ['affordable', '🟢 Can Afford'],
              ['high_roller', '💎 High-Roller (1,000+ pts)'],
            ].map(([fKey, fLabel]) => (
              <button
                key={fKey}
                type="button"
                className={`chip ${shopFilter === fKey ? 'on' : ''}`}
                onClick={() => setShopFilter(fKey)}
              >
                {fLabel}
              </button>
            ))}
          </div>
        </div>

        {(activeCategory === 'themes' || activeCategory === 'all') &&
          renderShopSection(
            'themes',
            '🎨 1. Site-Wide Themes & Palettes',
            'Transforms the entire Lexicon & Learning Dojo instantly — preview any palette for 4 seconds!',
            DOPAMINE_SHOP_ITEMS.themes,
            dopamine.equippedTheme
          )}

        {(activeCategory === 'titles' || activeCategory === 'all') &&
          renderShopSection(
            'titles',
            '👑 2. Learner Titles & Prestige Ranks',
            'Displayed next to your name in the Dojo header and broadcast on the Live Leaderboard',
            DOPAMINE_SHOP_ITEMS.titles,
            dopamine.equippedTitle
          )}

        {(activeCategory === 'nameplates' || activeCategory === 'all') &&
          renderShopSection(
            'nameplates',
            '✨ 3. Animated Username Nameplates & Auras',
            'Give your username a glowing neon, metallic gold, hellfire, or prismatic rainbow aura on the Leaderboard',
            DOPAMINE_SHOP_ITEMS.nameplates,
            dopamine.equippedNameplate
          )}

        {(activeCategory === 'frames' || activeCategory === 'all') &&
          renderShopSection(
            'frames',
            '🖼️ 4. Reactive WebGL Shader Row Frames',
            'Real-time GPU fragment shaders spanning your Leaderboard row — reacts to mouse hover, touch drag, and taps!',
            DOPAMINE_SHOP_ITEMS.frames,
            dopamine.equippedFrame
          )}

        {(activeCategory === 'effects' || activeCategory === 'all') &&
          renderShopSection(
            'effects',
            '🎆 5. Celebration Confetti Effects',
            'Triggered whenever you nail a question in Dopamine Mode — up to 42-particle dual-wave Supernovas!',
            DOPAMINE_SHOP_ITEMS.effects,
            dopamine.equippedEffect
          )}

        {(activeCategory === 'sounds' || activeCategory === 'all') &&
          renderShopSection(
            'sounds',
            '🔊 6. WebAudio Synth Fanfare Packs',
            'Custom procedural WebAudio synthesizer chord progressions played on every correct answer',
            DOPAMINE_SHOP_ITEMS.sounds,
            dopamine.equippedSound
          )}

        {(activeCategory === 'reactions' || activeCategory === 'all') &&
          renderShopSection(
            'reactions',
            '🤩 7. Animated Super Reactions (Global Chat)',
            'Permanently unlock Discord-style Animated Super Reactions to burst, glow, and sparkle on messages in Global Chat!',
            DOPAMINE_SHOP_ITEMS.reactions || [],
            null
          )}

        {(activeCategory === 'perks' || activeCategory === 'all') &&
          renderShopSection(
            'perks',
            '⚡ 8. Gameplay Perks, Streak Shields & Permanent Relics',
            'Spend your hard-earned points on Streak Shields, 2x Overdrive surges, and permanent passive point multipliers!',
            DOPAMINE_SHOP_ITEMS.perks,
            null
          )}
      </div>
    </div>
  );
}

// Live Shared Leaderboard Panel (usable in Leaderboard Tab, Learn Dojo, and Dashboard)
function LeaderboardPanel({
  dopamine,
  leaderboard = [],
  onSaveUsername,
  onDeleteEntry,
  isAdmin,
  onOpenLearn,
  onOpenShop,
  compact = false,
}) {
  const [nameDraft, setNameDraft] = useStateL(dopamine?.username || '');
  useEffectL(() => {
    setNameDraft(dopamine?.username || '');
  }, [dopamine?.username]);

  // STRICT ADMIN CHECK: Only show delete buttons when explicitly in Admin Mode!
  const adminAuthed = Boolean(isAdmin);
  const equippedTitleObj = getEquippedTitleObj(dopamine?.equippedTitle);
  const equippedNameplateObj = getEquippedNameplateObj(dopamine?.equippedNameplate);
  const equippedFrameObj = getEquippedFrameObj(dopamine?.equippedFrame);

  function handleSave(e) {
    e.preventDefault();
    const trimmed = String(nameDraft || '').trim();
    if (!trimmed) return;
    const existing = String(dopamine?.username || '').trim();
    const maxLen = existing && existing.length > 20 && trimmed === existing ? 28 : 20;
    const cleaned = trimmed.slice(0, maxLen);
    onSaveUsername && onSaveUsername(cleaned);
  }

  const displayRows = compact ? leaderboard.slice(0, 10) : leaderboard;

  return (
    <div className="leaderboard-shell">
      <div className="learn-card leaderboard-identity-card">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
          <div>
            <div className="eyebrow"><span className="rule"></span>Live Community Rankings · Synced Across All Learners</div>
            <h3 className="serif" style={{ fontSize: compact ? 24 : 30, margin: '4px 0 6px', color: 'var(--paper)' }}>
              🏆 Seamenese Dopamine Leaderboard
            </h3>
            <p style={{ margin: 0, fontSize: 13.5, color: 'var(--paper-dim)', maxWidth: '64ch' }}>
              Set a persistent username below to broadcast your points, best streak, equipped title, nameplate aura, and reactive WebGL shader row frame live to everyone studying Seamenese!
            </p>
          </div>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {onOpenLearn && (
              <button type="button" className="btn btn-accent" onClick={onOpenLearn}>
                🎓 Enter Learning Dojo →
              </button>
            )}
            {onOpenShop && (
              <button type="button" className="btn btn-warm" onClick={onOpenShop}>
                🛒 Customize Loadout &amp; Perks
              </button>
            )}
          </div>
        </div>

        <form onSubmit={handleSave} className="leaderboard-name-form">
          <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', flex: 1 }}>
            <label className="mono" style={{ fontSize: 12, color: '#fbbf24', fontWeight: 700 }}>
              👤 Your Persistent Username:
            </label>
            <input
              type="text"
              className="field field-mono"
              style={{ maxWidth: 260 }}
              placeholder="Choose your name (max 20 chars)…"
              maxLength={20}
              value={nameDraft}
              onChange={e => setNameDraft(e.target.value)}
            />
            <button type="submit" className="btn btn-accent">
              💾 {dopamine?.username ? 'Update Username & Sync' : 'Claim Username & Join Leaderboard'}
            </button>
          </div>

          <div
            className={`leaderboard-my-stats leaderboard-row ${equippedFrameObj.cssClass || 'row-frame-default'} mono`}
            title="Move mouse or tap/drag on mobile to interact with your equipped row shader!"
          >
            <RowShaderCanvas frameId={equippedFrameObj.id} />
            {dopamine?.username && (
              <span className={`seamenese-nameplate leaderboard-player-name lb-cell-content ${equippedNameplateObj.cssClass}`}>
                {dopamine.username}
              </span>
            )}
            <span className="learn-title-badge lb-cell-content">{equippedTitleObj.name}</span>
            <span className="lb-cell-content">🪙 <strong style={{ color: '#fbbf24' }}>{Number(dopamine?.points || 0).toLocaleString()} pts</strong></span>
            <span className="lb-cell-content">🔥 Streak: <strong>{dopamine?.streak || 0}</strong> (Best: {dopamine?.bestStreak || 0})</span>
          </div>
        </form>
      </div>

      <div className="learn-card" style={{ padding: '8px 12px 14px', overflow: 'hidden' }}>
        {displayRows.length === 0 ? (
          <div style={{ padding: 36, textAlign: 'center', color: 'var(--fg-mute)' }}>
            <div style={{ fontSize: 36, marginBottom: 8 }}>🐟</div>
            <div className="serif" style={{ fontSize: 22, color: 'var(--paper)' }}>No learners on the board yet!</div>
            <p style={{ margin: '6px 0 0', fontSize: 13 }}>
              Enter your username above to be the very first Seamenese champion on the leaderboard.
            </p>
          </div>
        ) : (
          <div className="leaderboard-scroll-wrap">
            <div className={`leaderboard-grid ${adminAuthed ? 'has-admin' : ''}`} role="table" aria-label="Seamenese Leaderboard">
              <div className="leaderboard-grid-head mono" role="row">
                <div className="lb-th">Rank</div>
                <div className="lb-th">Learner</div>
                <div className="lb-th">Equipped Title</div>
                <div className="lb-th">Streak (Now / Best)</div>
                <div className="lb-th">Solved</div>
                <div className="lb-th" style={{ textAlign: 'right' }}>Dopamine Points</div>
                {adminAuthed && <div className="lb-th" style={{ textAlign: 'center' }}>Admin</div>}
              </div>

              <div className="leaderboard-grid-body" role="rowgroup">
                {displayRows.map((row, idx) => {
                  const rank = idx + 1;
                  const rankBadge = rank === 1 ? '🥇 1st' : rank === 2 ? '🥈 2nd' : rank === 3 ? '🥉 3rd' : `#${rank}`;
                  const isMe = row.player_id && row.player_id === dopamine?.playerId;
                  const titleObj = getEquippedTitleObj(row.equipped_title);
                  const npObj = getEquippedNameplateObj(row.equipped_nameplate);
                  const frameObj = getEquippedFrameObj(row.equipped_frame || (isMe ? dopamine?.equippedFrame : 'frame_default'));
                  return (
                    <div
                      key={row.player_id || idx}
                      role="row"
                      className={`leaderboard-row leaderboard-grid-row ${frameObj.cssClass || 'row-frame-default'} ${isMe ? 'leaderboard-row-me' : ''}`}
                    >
                      <RowShaderCanvas frameId={frameObj.id} />

                      <div className="lb-cell lb-rank-cell mono" role="cell" style={{ fontWeight: 700, color: rank <= 3 ? '#fbbf24' : 'var(--fg-mute)' }}>
                        <span className="lb-cell-content">{rankBadge}</span>
                      </div>

                      <div className="lb-cell lb-learner-cell" role="cell">
                        <div className="lb-cell-content" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 }}>
                          <strong
                            className={`seamenese-nameplate leaderboard-player-name ${npObj.cssClass}`}
                            style={{ fontSize: 15 }}
                          >
                            {row.username}
                          </strong>
                          {isMe && (
                            <span className="shop-badge equipped mono" style={{ fontSize: 10 }}>YOU</span>
                          )}
                        </div>
                      </div>

                      <div className="lb-cell lb-title-cell" role="cell">
                        <span className="learn-title-badge lb-cell-content" style={{ fontSize: 12 }}>{titleObj.name}</span>
                      </div>

                      <div className="lb-cell lb-streak-cell mono" role="cell">
                        <span className="lb-cell-content">
                          🔥 {row.current_streak || 0} <span style={{ color: 'rgba(255,255,255,0.62)' }}>/ Best {row.best_streak || 0}</span>
                        </span>
                      </div>

                      <div className="lb-cell lb-solved-cell mono" role="cell" style={{ color: 'var(--paper-dim)' }}>
                        <span className="lb-cell-content">✓ {row.solved_count || 0}</span>
                      </div>

                      <div className="lb-cell lb-points-cell mono" role="cell" style={{ textAlign: 'right', fontSize: 15.5, fontWeight: 700, color: '#fbbf24' }}>
                        <span className="lb-cell-content">🪙 {Number(row.points || 0).toLocaleString()} pts</span>
                      </div>

                      {adminAuthed && (
                        <div className="lb-cell" role="cell" style={{ textAlign: 'center' }}>
                          <button
                            type="button"
                            className="btn lb-cell-content"
                            style={{ padding: '4px 8px', fontSize: 11, color: 'var(--accent-warm)' }}
                            title="Admin: Remove this leaderboard entry"
                            onClick={() => onDeleteEntry && onDeleteEntry(row.player_id)}
                          >
                            🗑️
                          </button>
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// Reusable Difficulty Tier Bar for Scenes, Learn Words, Jai-Semen, and SRS Deck
function DifficultyTierBar({ selectedTier, onSelectTier, tierCounts }) {
  const activeTierObj = DIFFICULTY_TIERS.find(t => t.id === selectedTier) || DIFFICULTY_TIERS[0];
  return (
    <div className="learn-tier-bar">
      <div className="learn-tier-bar-top">
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span className="mono" style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#fbbf24', fontWeight: 700 }}>
            🎚️ Difficulty Tier Pool:
          </span>
          <div className="learn-tier-pills">
            {DIFFICULTY_TIERS.map(t => {
              const isSelected = selectedTier === t.id;
              const countInfo = tierCounts && tierCounts[t.id] !== undefined ? ` (${tierCounts[t.id]})` : '';
              return (
                <button
                  key={String(t.id)}
                  type="button"
                  className={`chip learn-tier-chip ${isSelected ? 'on' : ''}`}
                  onClick={() => onSelectTier(t.id)}
                >
                  <span>{t.short}{countInfo}</span>
                </button>
              );
            })}
          </div>
        </div>
      </div>
      <div className="learn-tier-desc">
        <strong>{activeTierObj.icon} {activeTierObj.label}:</strong> {activeTierObj.desc}
      </div>
    </div>
  );
}

// ============================================================================
// 7. INTEGRATED SEAMENESE LEARNING ENVIRONMENT (All 7 Tabs + Tiered Pools)
// ============================================================================
const STORAGE_KEY_MASTERED_WORDS = 'seamenese_mastered_words_v1';
const STORAGE_KEY_WORDS_MODE = 'seamenese_words_mode_v1';
const STORAGE_KEY_IMMERSION = 'seamenese_immersion_v1';
const STORAGE_KEY_SCENE_VIEW = 'seamenese_scene_view_v1';
const STORAGE_KEY_VERB_STREAK = 'seamenese_verb_streak_v1';
const STORAGE_KEY_ACTIVITY_ECONOMY = 'seamenese_activity_economy_v1';
const STORAGE_KEY_SCENE_ATTEMPTS = 'seamenese_scene_attempts_v1';
const VERB_WINDOW_MS = 10 * 60 * 1000;
const SRS_READ_MS = 2000;
const SRS_MAX_TRIES = 3;

function calculateActivityPoints({
  activity,
  tier = 1,
  attemptCount = 1,
  immersionNoPeek = false,
  immersionEnabled = true,
  exactOrthography = false,
  verbSessionCount = 1,
  srsVerified = false,
  srsIsDue = false,
  srsCramCountToday = 0,
  srsUnverifiedCountToday = 0,
  failedCount = 0,
  srsQuality = 4,
} = {}) {
  const integer = (value, fallback = 0) =>
    Number.isFinite(Number(value))
      ? Math.max(0, Math.floor(Number(value)))
      : fallback;

  const t = [1, 2, 3].includes(Number(tier)) ? Number(tier) : 1;

  const bases = {
    scene: [25, 40, 60],
    words_typing: [24, 36, 52],
    words_mc: [10, 16, 24],
    verb_single: [12, 18, 26],
    verb_multiverb: [38, 38, 38],
    srs: [22, 34, 50],
  };

  const basePoints = bases[activity]?.[t - 1] || 0;
  let rawPoints = basePoints;
  let label = '';
  let hardCap = null;
  let boostEligible = true;
  let countsAsWin = true;
  const breakdown = [`Base ${basePoints}`];

  if (!basePoints) {
    rawPoints = 0;
    label = 'Unknown Activity';
    countsAsWin = false;
  } else if (activity === 'scene') {
    const attempt = Math.max(1, integer(attemptCount, 1));

    if (attempt >= 4) {
      rawPoints = 5;
      hardCap = 5;
      countsAsWin = false;
      label = 'Completion Minimum';
      breakdown.push('Fourth or later try: fixed 5');
    } else {
      if (immersionNoPeek) {
        rawPoints *= 1.25;
        breakdown.push('Strict immersion ×1.25');
      }

      const decay = attempt === 1 ? 1 : attempt === 2 ? 0.5 : 0.2;
      rawPoints *= decay;
      breakdown.push(`Attempt ${attempt} ×${decay}`);

      label = attempt === 1
        ? (immersionNoPeek ? 'Immersion First-Try!' : 'First-Try Scene!')
        : `${immersionNoPeek ? 'Immersion · ' : ''}Try ${attempt} (${Math.round(decay * 100)}%)`;
    }
  } else if (activity === 'words_typing' || activity === 'words_mc') {
    label = activity === 'words_typing'
      ? 'Active Word Recall'
      : 'Word Recognition';

    if (activity === 'words_typing' && exactOrthography) {
      rawPoints += 4;
      label += ' · Diaeresis Mastery';
      breakdown.push('Exact ë +4');
    }

    if (integer(failedCount) > 0) {
      rawPoints *= 0.7;
      label += ' · Redemption (70%)';
      breakdown.push('Redemption ×0.7');
    }
  } else if (activity === 'verb_single' || activity === 'verb_multiverb') {
    const count = Math.max(1, integer(verbSessionCount, 1));
    const decay = count > 30 ? 0.25 : count > 15 ? 0.5 : 1;

    rawPoints *= decay;
    label = activity === 'verb_multiverb'
      ? 'Multi-Verb Chain'
      : 'Verb Conjugation';

    if (decay < 1) {
      boostEligible = false;
      label += ` · Practice ×${decay}`;
      breakdown.push(`${count} drills / rolling 10 min ×${decay}`);
    }
  } else if (activity === 'srs') {
    countsAsWin = false;

    if (!srsIsDue) {
      rawPoints = integer(srsCramCountToday) < 5 ? 2 : 0;
      hardCap = 2;
      label = rawPoints
        ? 'Cram Practice Cap'
        : 'Cram Daily Cap Reached';
    } else if (!srsVerified) {
      rawPoints = integer(srsUnverifiedCountToday) < 10 ? 4 : 0;
      hardCap = 4;
      label = rawPoints
        ? 'Self-Study Cap'
        : 'Self-Study Daily Cap Reached';
    } else if ([4, 5].includes(Number(srsQuality))) {
      label = 'Verified Due Recall';
      countsAsWin = true;
    } else {
      rawPoints = 0;
      hardCap = 0;
      label = 'Review Scheduled · Keep Practising';
    }

    breakdown.push(label);
  }

  if (!immersionEnabled && rawPoints > 0) {
    rawPoints *= 0.8;
    breakdown.push('English assistance ×0.8');
  }

  boostEligible =
    boostEligible && hardCap === null && rawPoints > 0;

  return {
    basePoints,
    rawPoints,
    points: Math.round(rawPoints),
    label,
    hardCap,
    boostEligible,
    countsAsWin: countsAsWin && rawPoints > 0,
    breakdown,
  };
}

function makeActivityEventId() {
  return globalThis.crypto?.randomUUID?.()
    || `activity-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

function ActivityPointsBadge({ tier, ...options }) {
  const immersionEnabled = React.useContext(ImmersionContext);
  const score = calculateActivityPoints({ ...options, tier, immersionEnabled });

  return (
    <span
      className="learn-badge learn-badge-amber mono"
      title={`${score.breakdown.join(' · ')}. Eligible mastery awards may receive perks.`}
    >
      +{score.points} base pts · {score.label}
    </span>
  );
}

function trainerImmersionCue(card, tokenMap) {
  const cues = {
    plural_feet: '1 sobo → 2 ___', type_friends: '1 buana → 2 ___',
    type_lovers_pl: '1 skantar → 2 ___',
    type_my_friend: 'Address a trusted companion belonging to the speaker. Use two words.',
    mc_negation_order: 'Deny understanding something: Ëi + ni + the right verb form.',
    type_sing: 'Imagine making ezuki with your voice. Write the two-word verb phrase.',
    type_sanaami: 'Sanai + Na → Na ___. Preserve the final vowel in the stem.',
    mc_multiverb_chain: 'Combine skani and obisëi ezuki for the subject Ëi. Which verb changes?',
    mc_kiniwako: 'Compare the grammatical roles of Kiniwai and Kiniwako.',
  };
  if (cues[card.id]) return cues[card.id];
  const answer = card.canonicalTerm || card.accepted?.[0] || '';
  const entry = tokenMap[String(answer).toLowerCase()];
  if (entry?.hint) return `Write the Seamenese word: ${cleanImmersionCue(entry.hint)}`;
  if (/^Complete:/.test(card.prompt)) return card.prompt.replace(/\s*\([^)]*\)/g, '');
  return cleanImmersionCue(card.subHint);
}

function trainerChoiceCue(text, lexicon) {
  const entry = [...lexicon, ...BASELINE_LEXICON].find(e => normalizeMeaningKey(e.meaning) === normalizeMeaningKey(text));
  if (entry) return cleanImmersionCue(entry.hint);
  const roles = {
    'Kiniwai = to explain (verb), Kiniwako = explanation (noun)': 'Kiniwai → jai-semen; Kiniwako → noun.',
    'Kiniwai = explanation (noun), Kiniwako = to explain (verb)': 'Kiniwai → noun; Kiniwako → jai-semen.',
    'They are singular and plural of the same noun': 'One noun in two numbers.',
    'Kiniwako is the 3rd-person conjugation of Kiniwai': 'Kiniwako → the La / Lon form of Kiniwai.',
  };
  return roles[text] || text;
}

function readLearningObject(key, fallback = {}) {
  try {
    const parsed = JSON.parse(localStorage.getItem(key) || 'null');
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
      ? parsed
      : fallback;
  } catch {
    return fallback;
  }
}

function saveLearningObject(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch {}
}

function normalizeActivityLedger(raw = {}, now = Date.now()) {
  const day = new Date(now).toISOString().slice(0, 10);
  const sameDay = raw.day === day;
  const count = value =>
    Math.max(0, Math.floor(Number(value) || 0));

  return {
    day,
    cramCount: sameDay ? count(raw.cramCount) : 0,
    unverifiedCount: sameDay ? count(raw.unverifiedCount) : 0,
    verbAttempts: (Array.isArray(raw.verbAttempts)
      ? raw.verbAttempts
      : []
    ).filter(time =>
      Number.isFinite(time)
      && time > now - VERB_WINDOW_MS
      && time <= now
    ),
    srsExposed: raw.srsExposed
      && typeof raw.srsExposed === 'object'
      && !Array.isArray(raw.srsExposed)
        ? raw.srsExposed
        : {},
  };
}

function useActivityEconomy() {
  const [ledger, setLedger] = useStateL(() =>
    normalizeActivityLedger(
      readLearningObject(STORAGE_KEY_ACTIVITY_ECONOMY)
    )
  );
  const ledgerRef = useRefL(ledger);
  const [economyNow, setEconomyNow] = useStateL(Date.now);

  useEffectL(() => {
    const timer = setInterval(() => setEconomyNow(Date.now()), 1000);

    const handleStorage = event => {
      if (event.key !== STORAGE_KEY_ACTIVITY_ECONOMY) return;
      const next = normalizeActivityLedger(
        readLearningObject(STORAGE_KEY_ACTIVITY_ECONOMY)
      );
      ledgerRef.current = next;
      setLedger(next);
      setEconomyNow(Date.now());
    };

    window.addEventListener('storage', handleStorage);
    return () => {
      clearInterval(timer);
      window.removeEventListener('storage', handleStorage);
    };
  }, []);

  const mutateEconomy = useCallbackL(updater => {
    const now = Date.now();
    const before = normalizeActivityLedger(
      readLearningObject(
        STORAGE_KEY_ACTIVITY_ECONOMY,
        ledgerRef.current
      ),
      now
    );
    const after = normalizeActivityLedger(updater(before), now);

    ledgerRef.current = after;
    saveLearningObject(STORAGE_KEY_ACTIVITY_ECONOMY, after);
    setLedger(after);
    setEconomyNow(now);

    return { before, after };
  }, []);

  return {
    economy: normalizeActivityLedger(ledger, economyNow),
    economyNow,
    mutateEconomy,
  };
}

function seaAnswerKey(value, exact = false) {
  let result = String(value || '')
    .normalize(exact ? 'NFC' : 'NFD')
    .toLowerCase();

  if (!exact) result = result.replace(/\p{M}/gu, '');

  return result
    .replace(/[-_\s]+/g, '')
    .replace(/[.,!?;:'"()]/g, '');
}

function getTrainerAcceptedAnswers(card) {
  const declared = Array.isArray(card?.accepted)
    ? card.accepted
    : card?.accepted?.accepted || [];

  const answers = new Set(
    [...declared, card?.canonicalTerm].filter(Boolean)
  );

  Object.values(LEXICON_VARIANT_GROUPS).forEach(group => {
    const variants = group?.accepted || [];
    const matches = variants.some(variant =>
      [...answers].some(answer =>
        seaAnswerKey(answer) === seaAnswerKey(variant)
      )
    );
    if (matches) variants.forEach(answer => answers.add(answer));
  });

  return [...answers];
}

function hasExactTrainerOrthography(card, answer) {
  if (card?.qType !== 'typing') return false;

  const targets = [
    card.canonicalTerm,
    ...String(card.displayAnswer || '').split(/\s*\/\s*/),
    ...getTrainerAcceptedAnswers(card),
  ].filter(target =>
    String(target || '').normalize('NFC')
      .toLowerCase().includes('\u00eb')
  );

  return targets.some(target =>
    seaAnswerKey(target, true) === seaAnswerKey(answer, true)
  );
}

function englishRecallTokens(value) {
  return String(value || '')
    .normalize('NFD')
    .replace(/\p{M}/gu, '')
    .toLowerCase()
    .replace(/[’']/g, '')
    .replace(/[^\p{L}\p{N}\s]/gu, ' ')
    .trim()
    .split(/\s+/)
    .filter(token =>
      token && !['to', 'a', 'an', 'the'].includes(token)
    );
}

function englishRecallMatches(answer, aliases) {
  const matchSingle = (ansStr) => {
    const actual = new Set(englishRecallTokens(ansStr));
    if (!actual.size) return false;
    return aliases.some(alias => {
      const required = new Set(englishRecallTokens(alias));
      return required.size > 0
        && actual.size === required.size
        && [...required].every(token => actual.has(token));
    });
  };

  if (matchSingle(answer)) return true;

  // Also accept when the learner enters multiple valid synonyms together (e.g. "lover/crush", "crush / lover", "to do / to make")
  const parts = String(answer || '')
    .split(/\s*(?:\/|;|,|\bor\b)\s*/i)
    .map(p => p.trim())
    .filter(Boolean);
  if (parts.length > 1 && parts.every(part => matchSingle(part))) {
    return true;
  }

  return false;
}

const SRS_ENGLISH_ALIASES = {
  daka: ['cheers', 'toast'],
  skantar: ['crush', 'lover', 'crush / lover', 'lover / crush'],
  kini: ['need', 'to need'],
  skani: ['like', 'to like', 'love', 'to love'],
  wo: ['big', 'large', 'very', 'amplifier', 'intensifier', 'much'],
  seamenese: ['seamenese', 'conlang', 'the conlang', 'the conlang itself', 'language', 'the language of words'],
  'jai-semen': ['verb', 'action word', 'do word', 'a verb', 'an action word'],
  'zorpe-semen': ['question word', 'interrogative', 'asking word', 'a question word'],
  lipo: ['awake', 'energetic', 'excited', 'not tired', 'alert'],
  sarpo: ['tired', 'sleepy', 'exhausted'],
  fasma: ['tired', 'sleepy', 'exhausted'],
  obisei: ['speak', 'vocalize', 'talk', 'say'],
  'obisëi': ['speak', 'vocalize', 'talk', 'say'],
  sanai: ['cycle', 'loop', 'repeat'],
  ragoi: ['agree', 'vibe'],
  pansai: ['think', 'ponder'],
  zorpei: ['ask', 'question'],
  'zorpëi': ['ask', 'question'],
  wuke: ['night', 'night time', 'nighttime'],
  etwuke: ['tonight', 'this night'],
  etpanke: ['today', 'this day'],
  tamako: ['hello', 'hi', 'hey', 'greeting'],
  taki: ['bye', 'goodbye'],
  la: ['he', 'she', 'it', 'he she it'],
  lai: ['his', 'her', 'its', 'his her its'],
  lon: ['they', 'them', 'they them'],
  na: ['we', 'us', 'we us'],
  nai: ['our', 'ours', 'our ours'],
  ton: ['you', 'you all', 'you plural', 'yall', 'you guys'],
  tei: ['your', 'yours'],
  'tëi': ['your', 'yours'],
  teion: ['your', 'yours', 'your plural', 'your all', 'yalls'],
  'tëion': ['your', 'yours', 'your plural', 'your all', 'yalls'],
  ei: ['i', 'me'],
  'ëi': ['i', 'me'],
  ta: ['my', 'mine'],
  te: ['you'],
  xai: ['be', 'to be', 'is', 'am', 'are'],
};

function getSrsEnglishAnswers(entry) {
  const stripNotes = raw =>
    cleanQuizMeaning(raw, entry?.term)
      .replace(/\([^()]*\)/g, ' ')
      .replace(/\([^()]*$/g, ' ')
      .replace(/\[[^[\]]*\]/g, ' ')
      .replace(/\[[^[\]]*$/g, ' ')
      .trim();

  const meaning = stripNotes(entry?.meaning);
  const baseObj = BASELINE_LEXICON.find(
    b =>
      b.id.toLowerCase() === String(entry?.id || '').toLowerCase() ||
      b.term.toLowerCase() === String(entry?.term || '').toLowerCase()
  );
  const baseMeaning = baseObj ? stripNotes(baseObj.meaning) : '';
  const idKey = String(entry?.id || '').toLowerCase();
  const termKey = String(entry?.term || '').toLowerCase();

  return [
    meaning,
    ...meaning.split(/\s*(?:\/|;|,|\bor\b|—)\s*/i),
    baseMeaning,
    ...baseMeaning.split(/\s*(?:\/|;|,|\bor\b|—)\s*/i),
    ...(SRS_ENGLISH_ALIASES[idKey] || []),
    ...(SRS_ENGLISH_ALIASES[termKey] || []),
    ...(Array.isArray(entry?.srsEnglishAnswers)
      ? entry.srsEnglishAnswers
      : []),
  ].filter(answer => String(answer || '').trim());
}

function getSrsHeadwordAnswers(entry, lexicon) {
  const info = getAcceptedAnswersForEntry(entry, lexicon);
  return info.accepted && info.accepted.length > 0
    ? info.accepted
    : [entry.term];
}

function isValidSrsRecall(entry, answer, direction, lexicon) {
  if (!String(answer || '').trim()) return false;

  if (direction === 'english') {
    return englishRecallMatches(answer, getSrsEnglishAnswers(entry));
  }

  const key = seaAnswerKey(answer);
  return getSrsHeadwordAnswers(entry, lexicon)
    .some(candidate => seaAnswerKey(candidate) === key);
}

function getSceneProgress(scene, progressMap) {
  const fingerprint = JSON.stringify([
    scene.seamenese,
    scene.gloss,
    (scene.options || []).map(option => [
      option.reply,
      Boolean(option.correct),
    ]),
  ]);

  const saved = progressMap[scene.id];

  return {
    fingerprint,
    clickedReplies: saved?.fingerprint === fingerprint
      && Array.isArray(saved.clickedReplies)
        ? saved.clickedReplies
        : [],
    peeked: saved?.fingerprint === fingerprint
      ? Boolean(saved.peeked)
      : false,
  };
}

function srsDueAt(deck, entryId) {
  return Number(deck[entryId]?.dueAt) || 0;
}

function computeSm2Next(card, quality, now = Date.now()) {
  const q = Math.max(0, Math.min(5, Number(quality)));
  const previousEase = Math.max(
    1.3,
    Number(card?.easinessFactor) || 2.5
  );
  const previousRepetitions = Math.max(
    0,
    Math.floor(Number(card?.repetitions) || 0)
  );
  const previousInterval = Math.max(
    1,
    Number(card?.intervalDays) || 1
  );

  const easinessFactor = Math.max(
    1.3,
    previousEase
      + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
  );

  const repetitions = q < 3 ? 0 : previousRepetitions + 1;
  let intervalDays = 1;

  if (q >= 3) {
    if (repetitions === 1) intervalDays = 1;
    else if (repetitions === 2) intervalDays = 3;
    else intervalDays = Math.round(previousInterval * easinessFactor);
  }

  intervalDays = Math.max(1, intervalDays);

  return {
    entryId: card.entryId,
    repetitions,
    easinessFactor,
    intervalDays,
    dueAt: now + intervalDays * 86400000,
  };
}

function useSrsPractice({
  lexicon,
  selectedTier,
  activeTab,
  economy,
  economyNow,
  mutateEconomy,
  onDopamineWin,
  onRecallMiss,
  pushToast,
}) {
  const [deck, setDeck] = useStateL(() =>
    readLearningObject(STORAGE_KEY_SRS_V2)
  );
  const deckRef = useRefL(deck);

  const [attempt, setAttempt] = useStateL(null);
  const attemptRef = useRefL(null);
  const gradedRef = useRefL(false);
  const lastEntryIdRef = useRefL(null);
  const promptCounterRef = useRefL(0);
  const tierRef = useRefL(selectedTier);

  const commitDeck = useCallbackL(next => {
    deckRef.current = next;
    saveLearningObject(STORAGE_KEY_SRS_V2, next);
    setDeck(next);
  }, []);

  const commitAttempt = useCallbackL(next => {
    attemptRef.current = next;
    setAttempt(next);
  }, []);

  const reset = useCallbackL(() => {
    commitAttempt(null);
  }, [commitAttempt]);

  useEffectL(() => {
    const latest = readLearningObject(
      STORAGE_KEY_SRS_V2,
      deckRef.current
    );
    const next = { ...latest };
    let changed = false;

    const validIds = new Set(lexicon.map(entry => entry.id));
    Object.keys(next).forEach(key => {
      const low = String(key).toLowerCase();
      if (low.includes('dingding') || low.includes('-slang-') || (validIds.size > 0 && !validIds.has(key))) {
        delete next[key];
        changed = true;
        return;
      }
      const card = next[key];
      if (
        card
        && typeof card === 'object'
        && Number(card.repetitions || 0) === 0
        && Number(card.easinessFactor) === 2.5
        && Number(card.dueAt) > 0
      ) {
        next[key] = { ...card, dueAt: 0 };
        changed = true;
      }
    });

    lexicon.forEach(entry => {
      if (next[entry.id]) return;

      next[entry.id] = {
        entryId: entry.id,
        repetitions: 0,
        easinessFactor: 2.5,
        intervalDays: 1,
        dueAt: 0,
      };
      changed = true;
    });

    if (changed) commitDeck(next);
  }, [lexicon, commitDeck]);

  useEffectL(() => {
    const handleStorage = event => {
      if (event.key !== STORAGE_KEY_SRS_V2) return;

      const next = readLearningObject(
        STORAGE_KEY_SRS_V2,
        deckRef.current
      );
      deckRef.current = next;
      setDeck(next);
    };

    window.addEventListener('storage', handleStorage);
    return () => window.removeEventListener('storage', handleStorage);
  }, []);

  const tierCounts = useMemoL(() => {
    const counts = { 1: 0, 2: 0, 3: 0, all: lexicon.length };
    lexicon.forEach(entry => {
      counts[inferLexiconTier(entry)] += 1;
    });
    return counts;
  }, [lexicon]);

  const pool = useMemoL(() =>
    selectedTier === 'all'
      ? lexicon
      : lexicon.filter(entry =>
          inferLexiconTier(entry) === selectedTier
        ),
  [lexicon, selectedTier]);

  const orderedEntries = useMemoL(() =>
    [...pool].sort((a, b) => {
      const aDue = srsDueAt(deck, a.id) <= economyNow;
      const bDue = srsDueAt(deck, b.id) <= economyNow;
      if (aDue !== bDue) return aDue ? -1 : 1;
      // Review genuinely scheduled cards before untouched, auto-seeded cards.
      const aScheduled = srsDueAt(deck, a.id) > 0;
      const bScheduled = srsDueAt(deck, b.id) > 0;
      if (aDue && bDue && aScheduled !== bScheduled) return aScheduled ? -1 : 1;

      return inferLexiconTier(a) - inferLexiconTier(b)
        || srsDueAt(deck, a.id) - srsDueAt(deck, b.id);
    }),
  [pool, deck, economyNow]);

  const dueCount = orderedEntries.filter(entry =>
    srsDueAt(deck, entry.id) <= economyNow
  ).length;

  useEffectL(() => {
    if (tierRef.current === selectedTier) return;
    tierRef.current = selectedTier;
    reset();
  }, [selectedTier, reset]);

  useEffectL(() => {
    if (activeTab !== 'srs' || attemptRef.current || !pool.length) {
      return;
    }

    const latestDeck = readLearningObject(
      STORAGE_KEY_SRS_V2,
      deckRef.current
    );
    const now = Date.now();
    const dueEntry = orderedEntries.find(entry =>
      srsDueAt(latestDeck, entry.id) <= now
    );

    const previousIndex = pool.findIndex(entry =>
      entry.id === lastEntryIdRef.current
    );
    const entry = dueEntry
      || pool[(previousIndex + 1) % pool.length];

    const scheduleDueAt = srsDueAt(latestDeck, entry.id);
    const latestEconomy = normalizeActivityLedger(
      readLearningObject(STORAGE_KEY_ACTIVITY_ECONOMY, economy)
    );
    const previouslyExposed =
      latestEconomy.srsExposed[entry.id] === scheduleDueAt;

    gradedRef.current = false;

    commitAttempt({
      id: makeActivityEventId(),
      entryId: entry.id,
      entry: { ...entry },
      scheduleDueAt,
      direction: promptCounterRef.current++ % 2 === 0
        ? 'seamenese'
        : 'english',
      phase: 'front',
      answer: '',
      feedback: '',
      tries: 0,
      canVerify: !previouslyExposed,
      readUntil: 0,
      readReady: false,
    });
  }, [
    activeTab,
    pool,
    orderedEntries,
    attempt,
    economy,
    commitAttempt,
  ]);

  useEffectL(() => {
    if (
      !attempt
      || attempt.phase !== 'self-study'
      || attempt.readReady
    ) {
      return;
    }

    const id = attempt.id;
    const timer = setTimeout(() => {
      const current = attemptRef.current;
      if (!current || current.id !== id) return;

      commitAttempt({
        ...current,
        readReady: true,
      });
    }, Math.max(0, attempt.readUntil - performance.now()));

    return () => clearTimeout(timer);
  }, [
    attempt?.id,
    attempt?.phase,
    attempt?.readReady,
    attempt?.readUntil,
    commitAttempt,
  ]);

  function setAnswer(answer) {
    const current = attemptRef.current;
    if (!current || current.phase !== 'front') return;

    commitAttempt({
      ...current,
      answer,
      feedback: '',
    });
  }

  function reveal(verified = false) {
    const current = attemptRef.current;
    if (!current || current.phase !== 'front') return;

    const isVerified = Boolean(verified && current.canVerify);

    mutateEconomy(previous => ({
      ...previous,
      srsExposed: {
        ...previous.srsExposed,
        [current.entryId]: current.scheduleDueAt,
      },
    }));

    commitAttempt({
      ...current,
      phase: isVerified ? 'verified' : 'self-study',
      feedback: '',
      readUntil: isVerified ? 0 : performance.now() + SRS_READ_MS,
      readReady: isVerified,
    });
  }

  function submitRecall() {
    const current = attemptRef.current;
    if (
      !current
      || current.phase !== 'front'
      || !current.canVerify
      || !current.answer.trim()
    ) {
      return;
    }

    const correct = isValidSrsRecall(
      current.entry,
      current.answer,
      current.direction,
      lexicon
    );

    if (correct) {
      reveal(true);
      return;
    }

    const otherDir = current.direction === 'english' ? 'seamenese' : 'english';
    if (isValidSrsRecall(current.entry, current.answer, otherDir, lexicon)) {
      commitAttempt({
        ...current,
        feedback:
          current.direction === 'english'
            ? `💡 "${current.answer}" is the Seamenese word! Type its English meaning to verify.`
            : `💡 "${current.answer}" is the English meaning! Type the Seamenese word to verify.`,
      });
      return;
    }

    const tries = current.tries + 1;
    commitAttempt({
      ...current,
      tries,
      feedback: tries >= SRS_MAX_TRIES
        ? ''
        : 'That does not match yet. Try again, or reveal for self-study.',
    });

    if (tries >= SRS_MAX_TRIES) reveal(false);
    onRecallMiss?.();
  }

  function grade(quality) {
    const current = attemptRef.current;
    const q = Number(quality);

    if (
      !current
      || current.phase === 'front'
      || gradedRef.current
      || ![1, 3, 4, 5].includes(q)
    ) {
      return;
    }

    const verified = current.phase === 'verified';

    if (!verified) {
      if (q === 5) return;
      if (performance.now() < current.readUntil) return;
    }

    const latestDeck = readLearningObject(
      STORAGE_KEY_SRS_V2,
      deckRef.current
    );
    const liveDueAt = srsDueAt(latestDeck, current.entryId);

    if (liveDueAt !== current.scheduleDueAt) {
      commitDeck(latestDeck);
      reset();
      pushToast?.({
        kind: 'confirmed',
        title: 'Review refreshed',
        body: 'This card’s schedule changed. Its next review is ready.',
      });
      return;
    }

    gradedRef.current = true;

    const now = Date.now();
    const isDue = liveDueAt <= now;

    const { before } = mutateEconomy(previous => ({
      ...previous,
      cramCount: previous.cramCount + (!isDue ? 1 : 0),
      unverifiedCount: previous.unverifiedCount
        + (isDue && !verified ? 1 : 0),
    }));

    const previousCard = latestDeck[current.entryId] || {
      entryId: current.entryId,
      repetitions: 0,
      easinessFactor: 2.5,
      intervalDays: 1,
      dueAt: 0,
    };

    commitDeck({
      ...latestDeck,
      [current.entryId]: computeSm2Next(
        { ...previousCard, entryId: current.entryId },
        q,
        now
      ),
    });

    lastEntryIdRef.current = current.entryId;
    reset();

    onDopamineWin?.(inferLexiconTier(current.entry), {
      activity: 'srs',
      srsVerified: verified,
      srsIsDue: isDue,
      srsQuality: q,
      srsCramCountToday: before.cramCount,
      srsUnverifiedCountToday: before.unverifiedCount,
    });
  }

  function scheduleWord(entry) {
    if (!entry) return;

    const latestDeck = readLearningObject(
      STORAGE_KEY_SRS_V2,
      deckRef.current
    );
    const previousCard = latestDeck[entry.id] || {
      entryId: entry.id,
      repetitions: 0,
      easinessFactor: 2.5,
      intervalDays: 1,
      dueAt: 0,
    };

    commitDeck({
      ...latestDeck,
      [entry.id]: computeSm2Next(
        { ...previousCard, entryId: entry.id },
        2
      ),
    });

    if (attemptRef.current?.entryId === entry.id) reset();
  }

  return {
    deck,
    attempt,
    tierCounts,
    orderedEntries,
    dueCount,
    setAnswer,
    submitRecall,
    reveal,
    grade,
    reset,
    scheduleWord,
  };
}

function SrsPracticeTab({
  srs,
  selectedTier,
  onSelectTier,
  economy,
}) {
  const attempt = srs.attempt;
  const entry = attempt?.entry;
  const card = entry ? srs.deck[entry.id] : null;
  const revealed = attempt && attempt.phase !== 'front';
  const verified = attempt?.phase === 'verified';
  const isDue = entry
    ? srsDueAt(srs.deck, entry.id) <= Date.now()
    : false;

  return (
    <section className="learn-tab-section">
      <DifficultyTierBar
        selectedTier={selectedTier}
        onSelectTier={onSelectTier}
        tierCounts={srs.tierCounts}
      />

      <div className="learn-section-top">
        <div>
          <h3 className="serif learn-section-h">
            SRS Active Recall Deck
          </h3>
          <p className="learn-section-desc">
            Recall before revealing. Verified due reviews earn full points.
          </p>
        </div>

        <span className="learn-badge learn-badge-amber mono">
          Due Now: {srs.dueCount} / {srs.orderedEntries.length}
        </span>
      </div>

      {!entry ? (
        <div className="learn-card">
          {srs.orderedEntries.length
            ? 'Preparing your next review…'
            : 'No cards are available in this tier.'}
        </div>
      ) : (
        <article
          key={attempt.id}
          className="learn-card"
          style={{
            maxWidth: 760,
            margin: '0 auto',
            padding: '28px 24px',
            textAlign: 'center',
          }}
        >
          <div
            style={{
              display: 'flex',
              justifyContent: 'center',
              gap: 8,
              flexWrap: 'wrap',
              marginBottom: 16,
            }}
          >
            <span className="learn-badge mono">
              {getTierMeta(inferLexiconTier(entry)).short}
            </span>

            <span className="learn-badge mono">
              Interval: {card?.intervalDays || 1} day(s)
            </span>

            <span className="learn-badge mono">
              {isDue ? '📅 Due for review' : '🧘 Cram practice'}
            </span>
          </div>

          {!isDue && (
            <div className="learn-feedback-box" role="status" style={{ marginBottom: 14 }}>
              🧘 Cram Mode (Not Due Yet — +2 pts practice cap)
              <div>
                {Math.max(0, 5 - economy.cramCount)} rewarded cram
                {' '}reviews remaining today.
                {economy.cramCount >= 5 && ' Further cram reviews earn 0 pts.'}
              </div>
            </div>
          )}

          {!revealed ? (
            <>
              <h4 className="serif" style={{ margin: '8px 0 6px' }}>Active Recall Check</h4>

              <p className="mono" style={{ fontSize: 12, color: 'var(--fg-mute)' }}>
                {attempt.direction === 'english'
                  ? 'Type the English meaning before revealing:'
                  : 'Type the Seamenese headword before revealing:'}
              </p>

              <div
                className="serif"
                style={{ fontSize: 28, margin: '14px 0', color: 'var(--accent)', fontWeight: 700 }}
              >
                {attempt.direction === 'english'
                  ? entry.term
                  : <ImmersionGloss cue={entry.hint}>{cleanQuizMeaning(entry.meaning, entry.term)}</ImmersionGloss>}
              </div>

              {!attempt.canVerify && (
                <p role="status" style={{ fontSize: 13, color: '#fbbf24' }}>
                  This answer was already revealed for this scheduled
                  review. Self-study credit is available.
                </p>
              )}

              {attempt.canVerify && (
                <p role="status" className="mono" style={{ fontSize: 12, color: 'var(--fg-mute)' }}>
                  {SRS_MAX_TRIES - attempt.tries} guess{SRS_MAX_TRIES - attempt.tries === 1 ? '' : 'es'} remaining.
                  {' '}After {SRS_MAX_TRIES} incorrect guesses, the answer is revealed for self-study.
                </p>
              )}

              <form
                onSubmit={event => {
                  event.preventDefault();
                  srs.submitRecall();
                }}
              >
                <input
                  id={`srs-${attempt.id}`}
                  type="text"
                  className="field field-mono learn-trainer-input"
                  placeholder={attempt.direction === 'english' ? 'Type English meaning…' : 'Type Seamenese word…'}
                  value={attempt.answer}
                  disabled={!attempt.canVerify}
                  onChange={event => srs.setAnswer(event.target.value)}
                  autoComplete="off"
                  autoCorrect="off"
                  autoCapitalize="none"
                  spellCheck={false}
                />

                {attempt.feedback && (
                  <p role="alert" style={{ color: '#fbbf24', fontSize: 13, marginTop: 8 }}>{attempt.feedback}</p>
                )}

                <div
                  style={{
                    display: 'flex',
                    justifyContent: 'center',
                    gap: 10,
                    flexWrap: 'wrap',
                    marginTop: 14,
                  }}
                >
                  <button
                    type="submit"
                    className="btn btn-accent"
                    disabled={!attempt.canVerify || !attempt.answer.trim()}
                  >
                    ✓ Check Recall (Full Points)
                  </button>

                  <button
                    type="button"
                    className="btn"
                    onClick={() => srs.reveal(false)}
                    aria-label="Reveal answer without typing"
                  >
                    👁️ Just Reveal (Self-Study · +4 pts max)
                  </button>
                </div>
              </form>
            </>
          ) : (
            <>
              <div
                className="learn-badge mono"
                role="status"
                style={{ margin: '12px 0' }}
              >
                {verified
                  ? '✅ Verified Recall'
                  : '📖 Self-Study · Easy locked'}
              </div>

              {!verified && attempt.tries >= SRS_MAX_TRIES && (
                <p role="status" style={{ fontSize: 13, color: '#fbbf24' }}>
                  No guesses remaining. The answer was revealed for self-study.
                  {' '}Your last answer was not accepted; recall was not verified.
                </p>
              )}

              <div className="mono" style={{ fontSize: 30, fontWeight: 700, color: 'var(--accent)' }}>
                {entry.term}
              </div>

              <div className="serif" style={{ fontSize: 22, margin: '10px 0' }}>
                {entry.meaning}
              </div>

              {entry.example && (
                <p className="learn-dialogue-line">{entry.example}</p>
              )}

              {entry.notes && (
                <p style={{ color: 'var(--fg-mute)', fontSize: 13 }}>{entry.notes}</p>
              )}

              <div style={{ margin: '10px 0' }}>
                <ActivityPointsBadge
                  tier={inferLexiconTier(entry)}
                  activity="srs"
                  srsVerified={verified}
                  srsIsDue={isDue}
                  srsQuality={4}
                  srsCramCountToday={economy.cramCount}
                  srsUnverifiedCountToday={economy.unverifiedCount}
                />
              </div>

              {!verified && isDue && (
                <p style={{ fontSize: 12, color: 'var(--fg-mute)' }}>
                  Self-study: +4 pts for each of the first 10 due
                  unverified reviews today.
                  {' '}
                  {Math.max(0, 10 - economy.unverifiedCount)} remaining.
                </p>
              )}

              {!attempt.readReady && (
                <p role="status" className="mono" style={{ fontSize: 12, color: '#fbbf24' }}>
                  ⏳ Read the answer for 2 seconds before grading…
                </p>
              )}

              <div className="learn-mc-grid" style={{ marginTop: 18 }}>
                {[
                  { quality: 1, label: 'Again (1d)' },
                  { quality: 3, label: 'Hard (3)' },
                  { quality: 4, label: 'Good (4)' },
                  { quality: 5, label: 'Easy (5)' },
                ].map(({ quality, label }) => (
                  <button
                    key={quality}
                    type="button"
                    className={quality === 5 ? 'btn btn-accent' : 'btn'}
                    style={{ justifyContent: 'center', padding: '12px' }}
                    disabled={
                      !attempt.readReady
                      || (quality === 5 && !verified)
                    }
                    title={
                      quality === 5 && !verified
                        ? 'Verify recall before revealing to unlock Easy.'
                        : undefined
                    }
                    onClick={() => srs.grade(quality)}
                  >
                    {label}
                    {quality === 5 && !verified ? ' 🔒' : ''}
                  </button>
                ))}
              </div>
            </>
          )}
        </article>
      )}
    </section>
  );
}

const LEARN_TABS = [
  { id: 'scenes',      icon: '💬', label: 'Scenes',           sub: 'Tiered Dialogues' },
  { id: 'words',       icon: '🎯', label: 'Learn Words',      sub: 'Tiered Recall' },
  { id: 'verbs',       icon: '⚡', label: 'Verbs',            sub: 'Verb Drills' },
  { id: 'srs',         icon: '🧠', label: 'SRS Deck',         sub: 'SM-2 Recall' },
];

// Graduate Tier 3 Multi-Verb Chaining Drills for Jai-Semen Dojo
const GRADUATE_MULTIVERB_DRILLS = [
  {
    id: 'mv_1',
    prompt: 'Chain "Skani" (to like) + "Obisëi ezuki" (to sing) for Ëi (I):',
    ruleNote: 'Multi-verb rule: ONLY the first verb conjugates ("skan"); the second verb stays infinitive ("obisëi")!',
    correctForm: 'Ëi skan obisëi ezuki',
    options: [
      { form: 'Ëi skan obisëi ezuki', text: 'Ëi skan obisëi ezuki', correct: true },
      { form: 'Ëi skan obise ezuki', text: 'Ëi skan obise ezuki', correct: false },
      { form: 'Ëi skani obisëi ezuki', text: 'Ëi skani obisëi ezuki', correct: false },
      { form: 'Ëi skanai obisëi ezuki', text: 'Ëi skanai obisëi ezuki', correct: false },
    ],
  },
  {
    id: 'mv_2',
    prompt: 'Chain "Kini" (to need) + "Zetëi tumu" (to eat food) for Lon (They):',
    ruleNote: 'Multi-verb rule: "Lon" takes "-ai" on the first verb ("kinai"), while "zetëi" stays infinitive!',
    correctForm: 'Lon kinai zetëi tumu',
    options: [
      { form: 'Lon kinai zetëi tumu', text: 'Lon kinai zetëi tumu', correct: true },
      { form: 'Lon kinai zeteai tumu', text: 'Lon kinai zeteai tumu', correct: false },
      { form: 'Lon kinu zetëi tumu', text: 'Lon kinu zetëi tumu', correct: false },
      { form: 'Lon kini zetëi tumu', text: 'Lon kini zetëi tumu', correct: false },
    ],
  },
  {
    id: 'mv_3',
    prompt: 'Chain "Skani" (to like) + "Zorpëi" (to ask) for Na (We):',
    ruleNote: 'Multi-verb rule: "Na" takes "-ami" on the first verb ("skanami"), and the second verb stays infinitive ("zorpëi")!',
    correctForm: 'Na skanami zorpëi',
    options: [
      { form: 'Na skanami zorpëi', text: 'Na skanami zorpëi', correct: true },
      { form: 'Na skanami zorpeami', text: 'Na skanami zorpeami', correct: false },
      { form: 'Na skanu zorpëi', text: 'Na skanu zorpëi', correct: false },
      { form: 'Na skan zorpëi', text: 'Na skan zorpëi', correct: false },
    ],
  },
];

const BASELINE_ID_LOOKUP = new Set(BASELINE_SCENES.map(s => String(s.id || '').toLowerCase()));

function isSceneStructurallyValidClient(s) {
  if (!s || typeof s !== 'object' || !s.id || !s.seamenese) return false;
  const id = String(s.id || '').trim();
  if (/^(det_combo_|scene_gen_|det_arch_\d+_v\d+)/i.test(id)) return false;
  const textBlob = `${s.title || ''} ${s.scene || ''} ${s.gloss || ''}`;
  if (/\b(?:are\/am\/is|do\/does)\b/i.test(textBlob)) return false;
  if (/\b(?:yellow|dog|dogs|heal|afraid|scared|calm)\b/i.test(textBlob)) return false;
  if (/Practice\s+(?:Te|Ton)\s+\(you/i.test(s.scene || '')) return false;
  if (!Array.isArray(s.options) || s.options.length !== 4) return false;
  const correctOpts = s.options.filter(o => o && o.correct === true);
  if (correctOpts.length !== 1) return false;
  const correctReply = String(correctOpts[0]?.reply || '').trim();
  const promptSea = String(s.seamenese || '').trim();
  const allSea = `${promptSea} ${correctReply}`;
  if (
    /(?:^|[.!?]\s*)ja\s*[,!.?]/i.test(promptSea) ||
    /(?:^|[.!?]\s*)ja\s*[,!.?]/i.test(correctReply) ||
    s.options.some(o => /(?:^|[.!?]\s*)ja\s*[,!.?]/i.test(String(o?.reply || '').trim()))
  ) {
    return false;
  }
  if (/(?:^|[.!?]\s*)beto\s*!/i.test(allSea) && /\bwait\b/i.test(textBlob) && !/\bplease\b/i.test(textBlob)) return false;
  if (/\b,\s*ki\s*\?/i.test(allSea)) return false;
  if (/\bdaka\b/i.test(allSea) && /\bthank(?:s|\s+you)\b/i.test(textBlob) && !/\bcheers\b/i.test(textBlob)) return false;
  if (/\?/.test(promptSea) && /^we\s+/i.test(promptSea) && !/\bwhy\b/i.test(s.gloss || '')) return false;
  if (
    /\b(?:la|lon|etpanke|etwuke|panke|wuke|eke|blubur|tumu|ezuki|semen|hani|vgaggia|small|jorn|blui|luke|(?:ta|tëi|tei|tëion|teion|nai|lai|laion|et)\s+[a-zA-Z\u00C0-\u024F-]+(?:\s+[a-zA-Z\u00C0-\u024F-]+)?)\s+(?:ni\s+|san\s+|wo\s+)*xa\b/i.test(
      allSea
    )
  ) {
    return false;
  }
  if (/\b(?:te|ton)\s+(?:ni\s+|san\s+|wo\s+)*xaai\b/i.test(allSea)) return false;
  if (/\b(?:ëi|ei)\s+(?:ni\s+|san\s+|wo\s+)*(?:xau|xaami|xaai)\b/i.test(allSea)) return false;
  if (/\bna\s+(?:ni\s+|san\s+|wo\s+)*(?:xa|xau|xaai)\b/i.test(allSea)) return false;
  if (/(?:^|[.!?,:;\-]\s*|\b(?:u|zo|ap|wa|we|wu)\s+)(?:la|lon|blui|jorn|luke|vgaggia|small)\s+(?:ni\s+|san\s+|wo\s+)*(?:ja|jau|jaami|jai|skan|skanu|skanami|skani|zete|zeteu|zeteami|zetëi|kin|kinu|kinami|kini|sarp|sarpu|sarpami|sarpi|obise|obiseu|obiseami|obisëi|pon|ponu|ponami|poni)\b/i.test(allSea)) {
    return false;
  }
  if (/(?:^|[.!?,:;\-]\s*|\b(?:u|zo|ap|wa|we|wu)\s+)(?:ta|tëi|tei|tëion|teion|nai|lai|laion)\s+(?:ni\s+|san\s+|wo\s+)*(?:ja|jau|jaami|jaai|jai|xa|xau|xaami|xaai|xai|skan|skanu|skanami|skanai|skani|zete|zeteu|zeteami|zeteai|zetëi|kin|kinu|kinami|kinai|kini)\b/i.test(allSea)) {
    return false;
  }
  if (/\bton\s+(?:ni\s+|san\s+|wo\s+)*(?:obami|kinai|skanai|zeteai|jaai|ponai|pansaai|obiseai|sarpai|zorpeai|sanaai|kiniwaai|itxai|obai|nimai)\b/i.test(allSea)) {
    return false;
  }
  if (/\blon\s+(?:ni\s+|san\s+|wo\s+)*(?:kinu|skanu|zeteu|jau|ponu|pansau|obiseu|sarpu|zorpeu|sanau|kiniwau|itxu|obu|nimu|xau)\b/i.test(allSea)) {
    return false;
  }
  if (/(?:^|[.!?]\s*)(?:zorpëi|zorpei|obisëi|obisei|zetëi|zetei|kiniwai|pansai|sanai|skani|poni|kini|beti|sarpi|obi|nimi|itxi|faki|kixixi)\s+(?:ëi|ei|na|lon|la)\b/i.test(allSea)) {
    return false;
  }
  if (/\b(?:sing|singing|sings|song|songs)\b/i.test(textBlob) && !/\bezuki\b/i.test(allSea)) {
    return false;
  }
  if (/(?:^|[^a-zA-Z\u00C0-\u024F])(?:obis|obisu|obisami|obisai|zorpu|zorpami|zorpai|mik|miku|mikami|mikai)(?=$|[^a-zA-Z\u00C0-\u024F])/i.test(allSea)) {
    return false;
  }
  const allows2ndPersonEcho = /\b(?:say|tell|ask|remind|address|greet)\b[^.?!]*\b(?:to\s+them|to\s+your|you\s+all|ton|you\s+are)\b/i.test(
    s.scene || ''
  );
  if (!allows2ndPersonEcho && /\?/.test(promptSea) && /\b(?:wa|wu|we)\s+(?:[a-zA-Z\u00C0-\u024F-]+\s+){0,2}te\b/i.test(promptSea)) {
    if (/(?:^|[.!?]\s*|\b(?:ha|ni|daka|tamako|rago)\s*[,!]\s*)te\s+(?:ni\s+|san\s+|wo\s+)*(?:kin|skan|zete|ja|pon|pansa|obise|obis|sarp|zorpe|zorp|sana|san|kiniwa|kiniw|mike|mik|itx|ob|nim|fak|kixix|bet|xa)\b/i.test(correctReply)) {
      return false;
    }
  }
  if (!allows2ndPersonEcho && /\?/.test(promptSea) && /\b(?:wa|wu|we)\s+(?:[a-zA-Z\u00C0-\u024F-]+\s+){0,2}ton\b/i.test(promptSea)) {
    if (/(?:^|[.!?]\s*|\b(?:ha|ni|daka|tamako|rago)\s*[,!]\s*)ton\s+(?:ni\s+|san\s+|wo\s+)*(?:kinu|skanu|zeteu|jau|ponu|pansau|obiseu|obisu|sarpu|zorpeu|sanau|kiniwau|itxu|obu|nimu|faku|kixixu|betu|xau)\b/i.test(correctReply)) {
      return false;
    }
  }
  return true;
}

function mergeUniqueScenesWithTiers(...lists) {
  const seen = new Set();
  const out = [];
  for (const list of lists) {
    if (!Array.isArray(list)) continue;
    for (const s of list) {
      if (!isSceneStructurallyValidClient(s)) continue;
      const key = String(s.id).toLowerCase();
      if (seen.has(key)) continue;
      seen.add(key);
      out.push({ ...s, tier: inferSceneTierClient(s) });
    }
  }
  return out;
}

function saveNonBaselineScenesToStorage(sceneList, contentRev) {
  try {
    const custom = (sceneList || []).filter(
      s => s && s.id && isSceneStructurallyValidClient(s) && !BASELINE_ID_LOOKUP.has(String(s.id).toLowerCase())
    );
    localStorage.setItem(STORAGE_KEY_PERSISTED_SCENES, JSON.stringify(custom));
    if (contentRev !== undefined && contentRev !== null) {
      localStorage.setItem(STORAGE_KEY_SCENES_REV, String(contentRev));
    }
  } catch {}
}

// ============================================================================
// PERSISTENT SCENE GENERATION MANAGER (Survives Tab Switches + Drives Progress UI)
// ============================================================================
const SCENE_GEN_ACTIVE_JOB_KEY = 'seamenese_active_scene_job_v1';
const SCENE_GEN_LOCAL_NOTIFS_KEY = 'seamenese_local_scene_notifs_v1';

function loadLocalSceneNotifications() {
  try {
    const raw = JSON.parse(localStorage.getItem(SCENE_GEN_LOCAL_NOTIFS_KEY) || '[]');
    return Array.isArray(raw) ? raw : [];
  } catch {
    return [];
  }
}

function saveLocalSceneNotifications(list) {
  try {
    localStorage.setItem(SCENE_GEN_LOCAL_NOTIFS_KEY, JSON.stringify((list || []).slice(0, 30)));
  } catch {}
}

function addLocalSceneNotification(notif) {
  const existing = loadLocalSceneNotifications();
  if (notif.reference && existing.some(n => n.reference === notif.reference)) return;
  const created = {
    id: notif.id || `local-scene-gen-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
    kind: 'scenes',
    title: notif.title || '✨ +10 Dojo Scenes Ready!',
    body: notif.body || 'New Seamenese dialogue scenes are ready to practice in the Dojo.',
    target: 'scenes',
    reference: notif.reference || `scene-gen:${Date.now()}`,
    created_at: new Date().toISOString(),
    read: false,
    local_only: true,
  };
  saveLocalSceneNotifications([created, ...existing]);
  if (typeof window !== 'undefined') {
    window.dispatchEvent(new Event('seamenese-notifications-updated'));
  }
}

const SeameneseSceneGenManager = (() => {
  let state = {
    active: false,
    justCompleted: false,
    jobId: null,
    tier: null,
    startedAt: 0,
    percent: 0,
    stage: 'idle',
    stageLabel: '',
    detail: '',
    dismissedToDropdown: false,
    lastCompletedBatch: null,
  };
  const listeners = new Set();
  let smoothTimer = null;
  let completionClearTimer = null;

  function emit() {
    const snapshot = { ...state };
    listeners.forEach(fn => {
      try { fn(snapshot); } catch {}
    });
  }

  function setState(patch) {
    state = { ...state, ...patch };
    emit();
  }

  function stopSmoothTimer() {
    if (smoothTimer) {
      clearInterval(smoothTimer);
      smoothTimer = null;
    }
  }

  function startSmoothTimer() {
    stopSmoothTimer();
    smoothTimer = setInterval(() => {
      if (!state.active) {
        stopSmoothTimer();
        return;
      }
      const elapsedSec = Math.max(0, (Date.now() - (state.startedAt || Date.now())) / 1000);
      let nextPct = state.percent || 4;
      let nextStage = state.stage || 'cache_check';
      let nextStageLabel = state.stageLabel;
      let nextDetail = state.detail;

      if (nextStage === 'cache_check' && elapsedSec > 1.2) {
        nextStage = 'context';
        nextPct = Math.max(nextPct, 12);
        nextStageLabel = 'Stage 2/4 · Building Seamenese lexicon & grammar context';
        nextDetail = 'Compiling dictionary words, 20 verb tables & tier grammar rules…';
      } else if (nextStage === 'context' && elapsedSec > 2.6) {
        nextStage = 'ai_generating';
        nextPct = Math.max(nextPct, 18);
        nextStageLabel = 'Stage 3/4 · Drafting 10 Seamenese dialogues with Claude';
        nextDetail = 'Streaming candidate scenes & 4 multiple-choice replies…';
      } else if (nextStage === 'ai_generating') {
        if (nextPct < 74) {
          nextPct = Math.min(74, nextPct + 1);
        } else if (elapsedSec > 18 && nextPct < 84) {
          nextPct = Math.min(84, nextPct + 1);
        }
      } else if (nextStage === 'validating') {
        if (nextPct < 94) {
          nextPct = Math.min(94, nextPct + 1);
        }
      } else if (nextStage === 'saving') {
        if (nextPct < 98) {
          nextPct = Math.min(98, nextPct + 1);
        }
      }

      if (
        nextPct !== state.percent ||
        nextStage !== state.stage ||
        nextStageLabel !== state.stageLabel ||
        nextDetail !== state.detail
      ) {
        setState({
          percent: nextPct,
          stage: nextStage,
          stageLabel: nextStageLabel,
          detail: nextDetail,
        });
      }
    }, 650);
  }

  function applyServerProgress(payload) {
    if (!payload) return;
    if (state.jobId && payload.job_id && payload.job_id !== state.jobId) return;
    const prog = payload.progress || payload;
    if (!prog || typeof prog !== 'object') return;
    const nextPct = typeof prog.percent === 'number'
      ? Math.max(state.percent || 0, Math.min(100, Math.round(prog.percent)))
      : state.percent;
    setState({
      ...(payload.job_id ? { jobId: payload.job_id } : {}),
      percent: nextPct,
      stage: prog.stage || state.stage,
      stageLabel: prog.stageLabel || state.stageLabel,
      detail: prog.detail || state.detail,
    });
  }

  function dismissToDropdown() {
    if (state.active) {
      setState({ dismissedToDropdown: true });
    } else {
      setState({ justCompleted: false, dismissedToDropdown: true });
    }
  }

  function restoreFromDropdown() {
    if (state.active || state.justCompleted) {
      setState({ dismissedToDropdown: false });
    }
  }

  return {
    getState: () => state,
    subscribe: (fn) => {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
    setState,
    startSmoothTimer,
    stopSmoothTimer,
    applyServerProgress,
    dismissToDropdown,
    restoreFromDropdown,
    setCompletionTimer: (fn, ms) => {
      if (completionClearTimer) clearTimeout(completionClearTimer);
      completionClearTimer = setTimeout(fn, ms);
    },
  };
})();

function useSceneGenState() {
  const [st, setSt] = useStateL(() => SeameneseSceneGenManager.getState());
  useEffectL(() => SeameneseSceneGenManager.subscribe(setSt), []);
  return st;
}

if (typeof window !== 'undefined') {
  window.SeameneseSceneGenManager = SeameneseSceneGenManager;
  window.useSceneGenState = useSceneGenState;
  window.loadLocalSceneNotifications = loadLocalSceneNotifications;
  window.saveLocalSceneNotifications = saveLocalSceneNotifications;
}

function LearnEnvironment({
  db,
  onExitLearnMode,
  previousTabLabel,
  dopamine,
  onToggleDopamine,
  onOpenShop,
  onDopamineWin,
  onDopamineFail,
  activeRoast,
  onDismissRoast,
  pushToast,
  isAdmin,
  initialLearnTab = 'scenes',
  immersionMode,
}) {
  const [activeTab, setActiveTab] = useStateL(initialLearnTab || 'scenes');
  useEffectL(() => {
    if (initialLearnTab) setActiveTab(initialLearnTab);
  }, [initialLearnTab]);

  const { economy, economyNow, mutateEconomy } = useActivityEconomy();

  const [inspectedWord, setInspectedWord] = useStateL(null);

  // Persistent Difficulty Tier selection (defaults to Tier 1: Beginner so beginners never get graduate traps first!)
  const [selectedTier, setSelectedTier] = useStateL(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY_TIER);
      if (saved === 'all') return 'all';
      const n = Number(saved);
      if ([1, 2, 3].includes(n)) return n;
      return 1;
    } catch {
      return 1;
    }
  });

  // Live merged data from db prop + baseline fallback (with `.tier` assigned)
  const lexicon = useMemoL(() => buildLiveLexicon(db), [db]);
  const conjugations = useMemoL(() => buildLiveConjugations(db), [db]);
  const tokenMap = useMemoL(() => buildTokenMap(lexicon, conjugations), [lexicon, conjugations]);

  // --------------------------------------------------------------------------
  // TAB 1: SCENES STATE (Tiered + Persistent + Cache-Invalidated + Auto-Rotate)
  // --------------------------------------------------------------------------
  const [scenes, setScenes] = useStateL(() => {
    let savedCustom = [];
    try {
      const savedRev = localStorage.getItem(STORAGE_KEY_SCENES_REV);
      const liveRev = db?.meta?.content_revision !== undefined && db?.meta?.content_revision !== null
        ? String(db.meta.content_revision)
        : null;
      if (!liveRev || !savedRev || savedRev === liveRev) {
        const raw = JSON.parse(localStorage.getItem(STORAGE_KEY_PERSISTED_SCENES) || '[]');
        if (Array.isArray(raw)) savedCustom = raw.filter(isSceneStructurallyValidClient);
      }
    } catch {}
    return mergeUniqueScenesWithTiers(BASELINE_SCENES, savedCustom);
  });
  const [cachedPoolCount, setCachedPoolCount] = useStateL(0);
  const [sceneBatchIndex, setSceneBatchIndex] = useStateL(0);
  const [sceneFilter, setSceneFilter] = useStateL('all'); // 'all' | 'unsolved' | 'solved'
  const [sceneViewMode, setSceneViewMode] = useStateL(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY_SCENE_VIEW);
      return saved === 'grid' ? 'grid' : 'focus';
    } catch {
      return 'focus';
    }
  });

  function handleSwitchSceneViewMode(mode) {
    setSceneViewMode(mode);
    try {
      localStorage.setItem(STORAGE_KEY_SCENE_VIEW, mode);
    } catch {}
  }

  const [activeSceneId, setActiveSceneId] = useStateL(null);
  const [autoRotate, setAutoRotate] = useStateL(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY_AUTO_ROTATE);
      return saved === null ? true : saved === '1';
    } catch {
      return true;
    }
  });
  const [autoRotatePending, setAutoRotatePending] = useStateL(null); // { nextSceneId, nextTier, nextLabel }
  const autoRotateTimerRef = useRefL(null);

  // STRICT ADMIN CHECK: Never fall back to hasAdminToken() when Admin Mode is off
  const adminAuthed = Boolean(isAdmin);

  const [dailyGenUsed, setDailyGenUsed] = useStateL(() => {
    try {
      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY_DAILY_GEN) || '{}');
      if (raw && raw.date === getTodayDateKey()) {
        return Math.max(0, Number(raw.count) || 0);
      }
    } catch {}
    return 0;
  });

  const remainingDailyGens = adminAuthed
    ? Infinity
    : Math.max(0, NON_ADMIN_DAILY_GEN_LIMIT - dailyGenUsed);
  const isGenLimitReached = !adminAuthed && remainingDailyGens <= 0;

  function recordDailyGenIncrement(explicitUsed) {
    if (adminAuthed) return;
    setDailyGenUsed(prev => {
      const next = typeof explicitUsed === 'number'
        ? Math.max(prev, explicitUsed)
        : prev + 1;
      try {
        localStorage.setItem(
          STORAGE_KEY_DAILY_GEN,
          JSON.stringify({ date: getTodayDateKey(), count: next })
        );
      } catch {}
      return next;
    });
  }

  const sceneGen = useSceneGenState();
  const generatingScenes = Boolean(sceneGen.active);
  const lastHandledBatchIdRef = useRefL(null);
  const [solvedScenes, setSolvedScenes] = useStateL(() => {
    try {
      return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY_SCENES_V3) || '[]'));
    } catch {
      return new Set();
    }
  });
  const [sceneGlossVisible, setSceneGlossVisible] = useStateL({});
  const [sceneAttempts, setSceneAttempts] = useStateL(() =>
    readLearningObject(STORAGE_KEY_SCENE_ATTEMPTS)
  );
  const sceneAttemptsRef = useRefL(sceneAttempts);
  const sceneLockedRef = useRefL(new Set());

  const updateSceneProgress = useCallbackL((scene, updater) => {
    const previous = getSceneProgress(scene, sceneAttemptsRef.current);
    const progress = updater(previous);
    const next = {
      ...sceneAttemptsRef.current,
      [scene.id]: progress,
    };

    sceneAttemptsRef.current = next;
    saveLearningObject(STORAGE_KEY_SCENE_ATTEMPTS, next);
    setSceneAttempts(next);
    return progress;
  }, []);

  const markScenePeeked = useCallbackL(scene => {
    if (!scene) return;
    const previous = getSceneProgress(scene, sceneAttemptsRef.current);
    if (previous.peeked) return;

    updateSceneProgress(scene, progress => ({
      ...progress,
      peeked: true,
    }));
  }, [updateSceneProgress]);

  function handleToggleSceneGloss(scene) {
    if (!sceneGlossVisible[scene.id]) markScenePeeked(scene);

    setSceneGlossVisible(previous => ({
      ...previous,
      [scene.id]: !previous[scene.id],
    }));
  }

  const cancelAutoRotate = useCallbackL(() => {
    if (autoRotateTimerRef.current) {
      clearTimeout(autoRotateTimerRef.current);
      autoRotateTimerRef.current = null;
    }
    setAutoRotatePending(null);
  }, []);

  useEffectL(() => {
    return () => {
      if (autoRotateTimerRef.current) clearTimeout(autoRotateTimerRef.current);
    };
  }, []);

  function handleToggleAutoRotate() {
    setAutoRotate(prev => {
      const next = !prev;
      try {
        localStorage.setItem(STORAGE_KEY_AUTO_ROTATE, next ? '1' : '0');
      } catch {}
      if (!next) cancelAutoRotate();
      return next;
    });
  }

  // Stable shuffled options per sceneId — NEVER reshuffled on SSE db updates or hint toggles!
  const [shuffledSceneOptions, setShuffledSceneOptions] = useStateL(() => {
    const init = {};
    scenes.forEach(s => {
      if (s && s.id) init[s.id] = createShuffledMcOptions(s.options || []);
    });
    return init;
  });

  // Ensure any newly loaded or generated scenes get their options shuffled ONCE
  const ensureSceneOptionsShuffled = useCallbackL((sceneList) => {
    setShuffledSceneOptions(prev => {
      let changed = false;
      const next = { ...prev };
      sceneList.forEach(s => {
        if (s && s.id && !next[s.id]) {
          next[s.id] = createShuffledMcOptions(s.options || []);
          changed = true;
        }
      });
      return changed ? next : prev;
    });
  }, []);

  const [serverPoolScenes, setServerPoolScenes] = useStateL([]);
  const initialScenesSyncedRef = useRefL(false);

  // Fetch & validate persisted scenes from GET /api/learn/scenes on mount AND when content_revision changes.
  // Prunes any stale/deleted scenes from localStorage so server-side deletions/edits reflect immediately!
  useEffectL(() => {
    let cancelled = false;
    async function loadApiScenes(isInitialOrRevChange = false) {
      try {
        const res = await fetch('/api/learn/scenes', { cache: 'no-store' });
        if (!res.ok) return;
        const json = await res.json();
        if (typeof json.cached_pool_count === 'number') {
          setCachedPoolCount(json.cached_pool_count);
        }
        const apiList = Array.isArray(json) ? json : (Array.isArray(json.scenes) ? json.scenes : null);
        const serverValidIds = Array.isArray(json.valid_scene_ids)
          ? new Set(json.valid_scene_ids.map(id => String(id).toLowerCase()))
          : null;
        const serverRev = json.content_revision ?? db?.meta?.content_revision ?? null;

        if (!cancelled && apiList && apiList.length > 0) {
          setServerPoolScenes(apiList.filter(isSceneStructurallyValidClient));
          const shouldAutoMergeAll = !initialScenesSyncedRef.current || isInitialOrRevChange;
          initialScenesSyncedRef.current = true;

          setScenes(prev => {
            // Filter out any cached local scenes that were removed or invalidated on the server
            const validPrev = serverValidIds
              ? prev.filter(s => s && s.id && (BASELINE_ID_LOOKUP.has(String(s.id).toLowerCase()) || serverValidIds.has(String(s.id).toLowerCase())))
              : prev;
            const merged = shouldAutoMergeAll
              ? mergeUniqueScenesWithTiers(BASELINE_SCENES, apiList, validPrev)
              : mergeUniqueScenesWithTiers(BASELINE_SCENES, validPrev);
            saveNonBaselineScenesToStorage(merged, serverRev);
            ensureSceneOptionsShuffled(merged);

            // Also prune solvedScenes IDs that no longer exist so solved % never exceeds 100%
            const validMergedIds = new Set(merged.map(s => s.id));
            setSolvedScenes(prevSolved => {
              const filteredSolved = Array.from(prevSolved).filter(id => validMergedIds.has(id));
              if (filteredSolved.length !== prevSolved.size) {
                try {
                  localStorage.setItem(STORAGE_KEY_SCENES_V3, JSON.stringify(filteredSolved));
                } catch {}
                return new Set(filteredSolved);
              }
              return prevSolved;
            });

            return merged;
          });
        }
      } catch {
        // Offline / fallback to BASELINE_SCENES + localStorage already in state
      }
    }
    loadApiScenes(true);
    const onFocusOrVisible = () => {
      if (typeof document === 'undefined' || document.visibilityState === 'visible') {
        loadApiScenes(false);
      }
    };
    window.addEventListener('focus', onFocusOrVisible);
    document.addEventListener('visibilitychange', onFocusOrVisible);
    return () => {
      cancelled = true;
      window.removeEventListener('focus', onFocusOrVisible);
      document.removeEventListener('visibilitychange', onFocusOrVisible);
    };
  }, [ensureSceneOptionsShuffled, db?.meta?.content_revision]);

  const unseenCachedCount = useMemoL(() => {
    const haveSet = new Set(scenes.map(s => String(s?.id || '').toLowerCase()));
    const unseenMatching = serverPoolScenes.filter(s => {
      if (!s || !s.id || !isSceneStructurallyValidClient(s)) return false;
      if (haveSet.has(String(s.id).toLowerCase())) return false;
      if (selectedTier !== 'all' && inferSceneTierClient(s) !== selectedTier) return false;
      return true;
    });
    if (unseenMatching.length > 0 || !isGenLimitReached) return unseenMatching.length;
    return serverPoolScenes.filter(
      s => s && s.id && isSceneStructurallyValidClient(s) && !haveSet.has(String(s.id).toLowerCase())
    ).length;
  }, [serverPoolScenes, scenes, selectedTier, isGenLimitReached]);

  // Sync any batch that finished while LearnEnvironment was mounted OR while user was on another main tab!
  useEffectL(() => {
    const batch = sceneGen.lastCompletedBatch;
    if (!batch || !batch.completedId || lastHandledBatchIdRef.current === batch.completedId) return;
    lastHandledBatchIdRef.current = batch.completedId;
    if (typeof batch.serverDailyUsed === 'number') {
      recordDailyGenIncrement(batch.serverDailyUsed);
    } else if (!batch.fromCache && batch.generatedNewCount > 0) {
      recordDailyGenIncrement();
    }
    if (Array.isArray(batch.returnedBatch) && batch.returnedBatch.length > 0) {
      setServerPoolScenes(prev => mergeUniqueScenesWithTiers(prev, batch.returnedBatch));
      setScenes(prev => {
        const merged = mergeUniqueScenesWithTiers(BASELINE_SCENES, prev, batch.returnedBatch);
        saveNonBaselineScenesToStorage(merged, db?.meta?.content_revision);
        ensureSceneOptionsShuffled(merged);
        return merged;
      });
    }
    if (typeof batch.totalScenes === 'number') {
      setCachedPoolCount(Math.max(0, batch.totalScenes - BASELINE_SCENES.length));
    }
    if (batch.firstAddedScene) {
      if (selectedTier !== 'all' && batch.firstAddedScene.tier && batch.firstAddedScene.tier !== selectedTier) {
        setSelectedTier(batch.firstAddedScene.tier);
        try {
          localStorage.setItem(STORAGE_KEY_TIER, String(batch.firstAddedScene.tier));
        } catch {}
      }
      setActiveSceneId(batch.firstAddedScene.id);
    }
  }, [sceneGen.lastCompletedBatch, db?.meta?.content_revision, ensureSceneOptionsShuffled, selectedTier]);

  async function handleGenerateMoreScenes() {
    if (SeameneseSceneGenManager.getState().active) return;
    if (!adminAuthed && isGenLimitReached && unseenCachedCount === 0) {
      pushToast && pushToast({
        kind: 'warn',
        title: '🔒 Daily Limit Reached (2/2)',
        body: 'You have used your 2 daily scene generations and already have all cached community scenes!',
      });
      return;
    }
    cancelAutoRotate();
    const requestedTier = selectedTier === 'all' ? null : selectedTier;
    const initialScenesSnapshot = [...scenes];
    SeameneseSceneGenManager.setState({
      active: true,
      justCompleted: false,
      jobId: null,
      tier: requestedTier,
      startedAt: Date.now(),
      percent: 6,
      stage: 'cache_check',
      stageLabel: 'Stage 1/4 · Checking community scene cache',
      detail: 'Scanning cached scenes & checking unseen dialogues…',
      dismissedToDropdown: false,
    });
    SeameneseSceneGenManager.startSmoothTimer();

    try {
      let appendedScenes = null;
      let firstAddedScene = null;
      let addedCount = 0;
      let fromCache = false;
      let reusedCount = 0;
      let generatedNewCount = 0;
      let shortfallCount = 0;
      let serverDailyUsed = null;
      let usedServer = false;
      let returnedBatch = [];
      let totalScenesServer = null;
      let finalJobId = null;

      try {
        let adminToken = '';
        if (adminAuthed) {
          try {
            adminToken = localStorage.getItem('bonk-conlang-admin-token') || sessionStorage.getItem('seamenese_admin_token') || '';
          } catch {}
        }
        let res = await fetch('/api/learn/generate', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...(adminAuthed && adminToken ? { 'X-Admin-Token': adminToken } : {}),
          },
          body: JSON.stringify({
            count: 10,
            have_ids: initialScenesSnapshot.map(s => s.id),
            tier: requestedTier,
            player_id: dopamine?.playerId || '',
            is_admin_mode: adminAuthed,
          }),
        });

        // AI / Batch-API generation can take minutes — far longer than one HTTP request may
        // live behind the tunnel — so the server answers 202 + job_id and we poll for the result.
        if (res.status === 202) {
          const pending = await res.json().catch(() => ({}));
          const jobId = pending && pending.job_id;
          finalJobId = jobId || null;
          if (pending?.progress) {
            SeameneseSceneGenManager.applyServerProgress(pending);
          } else if (jobId) {
            SeameneseSceneGenManager.setState({ jobId });
          }
          try {
            if (jobId) sessionStorage.setItem(SCENE_GEN_ACTIVE_JOB_KEY, jobId);
          } catch {}
          const pollDeadline = Date.now() + 12 * 60 * 1000;
          let polled = null;
          while (jobId && Date.now() < pollDeadline) {
            await new Promise(r => setTimeout(r, 1500));
            let pr = null;
            try {
              pr = await fetch(`/api/learn/generate/${encodeURIComponent(jobId)}`, { cache: 'no-store' });
            } catch {
              continue; // transient network blip — keep polling
            }
            if (pr.status === 202) {
              const pData = await pr.json().catch(() => ({}));
              if (pData?.progress) {
                SeameneseSceneGenManager.applyServerProgress(pData);
              }
              continue;
            }
            polled = pr;
            break;
          }
          try {
            sessionStorage.removeItem(SCENE_GEN_ACTIVE_JOB_KEY);
          } catch {}
          if (!polled) {
            SeameneseSceneGenManager.stopSmoothTimer();
            SeameneseSceneGenManager.setState({ active: false, justCompleted: false });
            pushToast && pushToast({
              kind: 'warn',
              title: '⏳ Still generating on the server',
              body: 'This is taking longer than usual. Your scenes will be saved automatically — refresh in a minute to load them.',
            });
            return;
          }
          if (polled.status === 404) {
            SeameneseSceneGenManager.stopSmoothTimer();
            SeameneseSceneGenManager.setState({ active: false, justCompleted: false });
            pushToast && pushToast({
              kind: 'warn',
              title: 'Generation job expired',
              body: 'Refresh the page — any scenes it finished are already saved on the server.',
            });
            return;
          }
          res = polled;
        }
        if (res.status === 429) {
          usedServer = true;
          SeameneseSceneGenManager.stopSmoothTimer();
          SeameneseSceneGenManager.setState({ active: false, justCompleted: false });
          const errData = await res.json().catch(() => ({}));
          if (errData.daily_limit_reached) {
            recordDailyGenIncrement(NON_ADMIN_DAILY_GEN_LIMIT);
            pushToast && pushToast({
              kind: 'warn',
              title: '🔒 Daily Limit Reached (2/2) · No Unseen Cached Scenes Yet',
              body: errData.error || 'You have used your 2 daily new generations and already have all cached scenes. When someone else generates a batch, click again to fetch it for free!',
            });
          } else {
            pushToast && pushToast({
              kind: 'warn',
              title: '⏳ Generator Busy · Try Again Shortly',
              body: errData.error || 'Too many concurrent AI generation requests right now. Please wait a few seconds and try again.',
            });
          }
          return;
        }
        if (res.ok) {
          usedServer = true;
          const data = await res.json();
          finalJobId = data.job_id || finalJobId;
          fromCache = Boolean(data.from_cache);
          reusedCount = Number(data.reused_count) || 0;
          generatedNewCount = Number(data.generated_new_count) || 0;
          shortfallCount = Number(data.shortfall) || 0;
          if (typeof data.daily_used === 'number') {
            serverDailyUsed = data.daily_used;
          }
          returnedBatch = Array.isArray(data.scenes)
            ? data.scenes
            : (Array.isArray(data) ? data : []);
          if (returnedBatch.length > 0) {
            let latestStored = initialScenesSnapshot;
            try {
              const rawSaved = JSON.parse(localStorage.getItem(STORAGE_KEY_PERSISTED_SCENES) || '[]');
              if (Array.isArray(rawSaved)) {
                latestStored = mergeUniqueScenesWithTiers(BASELINE_SCENES, initialScenesSnapshot, rawSaved);
              }
            } catch {}
            const existingIds = new Set(latestStored.map(s => String(s.id || '').toLowerCase()));
            const freshOnly = returnedBatch
              .filter(s => s && s.id && isSceneStructurallyValidClient(s) && !existingIds.has(String(s.id).toLowerCase()))
              .map(s => ({ ...s, tier: inferSceneTierClient(s) }));
            addedCount = freshOnly.length;
            if (freshOnly.length > 0) firstAddedScene = freshOnly[0];
            appendedScenes = mergeUniqueScenesWithTiers(BASELINE_SCENES, latestStored, returnedBatch);
          }
          if (typeof data.total_scenes === 'number') {
            totalScenesServer = data.total_scenes;
          }
        }
      } catch {}

      if (!appendedScenes || addedCount === 0) {
        SeameneseSceneGenManager.stopSmoothTimer();
        SeameneseSceneGenManager.setState({ active: false, justCompleted: false });
        if (usedServer) {
          if (typeof serverDailyUsed === 'number') {
            recordDailyGenIncrement(serverDailyUsed);
          } else if (!adminAuthed && !fromCache) {
            recordDailyGenIncrement();
          }
          pushToast && pushToast({
            kind: 'warn',
            title: '✨ All Cached Scenes Loaded for This Tier',
            body: 'You have already loaded all currently available scenes in this tier. Try switching to another Difficulty Tier!',
          });
          return;
        }
        pushToast && pushToast({
          kind: 'warn',
          title: '⚠️ Scene generator unavailable',
          body: 'Could not generate scenes right now — nothing was used from your daily limit. Please try again in a moment.',
        });
        return;
      }

      if (!adminAuthed) {
        try {
          const raw = JSON.parse(localStorage.getItem(STORAGE_KEY_DAILY_GEN) || '{}');
          const prevUsed = raw && raw.date === getTodayDateKey() ? Math.max(0, Number(raw.count) || 0) : 0;
          const nextUsed = typeof serverDailyUsed === 'number'
            ? Math.max(prevUsed, serverDailyUsed)
            : (!fromCache && generatedNewCount > 0 ? prevUsed + 1 : prevUsed);
          localStorage.setItem(STORAGE_KEY_DAILY_GEN, JSON.stringify({ date: getTodayDateKey(), count: nextUsed }));
        } catch {}
      }

      saveNonBaselineScenesToStorage(appendedScenes, db?.meta?.content_revision);
      const unlockedGenCount = Math.max(0, appendedScenes.length - BASELINE_SCENES.length);
      try {
        localStorage.setItem(STORAGE_KEY_UNLOCKED_GEN, String(unlockedGenCount));
      } catch {}

      const effectiveUsed = serverDailyUsed !== null
        ? Math.max(dailyGenUsed, serverDailyUsed)
        : (!fromCache ? dailyGenUsed + 1 : dailyGenUsed);
      const nextLeft = adminAuthed
        ? 'Unlimited (Admin)'
        : `${Math.max(0, NON_ADMIN_DAILY_GEN_LIMIT - effectiveUsed)}/${NON_ADMIN_DAILY_GEN_LIMIT} AI gens left today`;

      const tierText = requestedTier ? `Tier ${requestedTier} ` : '';
      const doneTitle = fromCache
        ? `🎁 Loaded ${addedCount || reusedCount || 10} Free Cached ${tierText}Scenes!`
        : reusedCount > 0
          ? `✨ Loaded ${reusedCount} Cached + ${generatedNewCount} New ${tierText}Scenes!`
          : `✨ ${addedCount || 10} New ${tierText}Scenes Ready!`;
      const doneDetail = `${fromCache ? 'Fetched from community cache (0 quota used).' : 'All scenes passed grammar validation & saved.'}${!fromCache && shortfallCount > 0 ? ` (${shortfallCount} draft${shortfallCount === 1 ? '' : 's'} filtered by grammar check.)` : ''} Quota: ${nextLeft}.`;

      // Record in notification dropdown (client-guaranteed + server notification sync)
      addLocalSceneNotification({
        title: doneTitle,
        body: doneDetail,
        reference: `scene-gen:${finalJobId || Date.now()}`,
      });

      const completedId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
      SeameneseSceneGenManager.stopSmoothTimer();
      SeameneseSceneGenManager.setState({
        active: false,
        justCompleted: true,
        dismissedToDropdown: false,
        percent: 100,
        stage: 'done',
        stageLabel: doneTitle,
        detail: doneDetail,
        lastCompletedBatch: {
          completedId,
          returnedBatch,
          addedCount,
          firstAddedScene,
          fromCache,
          reusedCount,
          generatedNewCount,
          shortfallCount,
          serverDailyUsed,
          totalScenes: totalScenesServer,
        },
      });
      SeameneseSceneGenManager.setCompletionTimer(() => {
        SeameneseSceneGenManager.setState({ justCompleted: false });
      }, 5000);
    } catch {
      SeameneseSceneGenManager.stopSmoothTimer();
      SeameneseSceneGenManager.setState({ active: false, justCompleted: false });
    }
  }


  function findNextUnsolvedSceneTarget(currentSceneId, nextSolvedSet) {
    const withTier = scenes.map(s => ({ ...s, tier: inferSceneTierClient(s) }));
    const currentPool = selectedTier === 'all'
      ? [...withTier].sort((a, b) => a.tier - b.tier)
      : withTier.filter(s => s.tier === selectedTier);

    if (currentPool.length > 0) {
      const curIdx = currentPool.findIndex(s => s.id === currentSceneId);
      for (let offset = 1; offset <= currentPool.length; offset++) {
        const candidate = currentPool[(Math.max(0, curIdx) + offset) % currentPool.length];
        if (candidate && candidate.id !== currentSceneId && !nextSolvedSet.has(candidate.id)) {
          const candNum = currentPool.findIndex(s => s.id === candidate.id) + 1;
          return {
            nextSceneId: candidate.id,
            nextTier: selectedTier,
            nextLabel: `Scene #${String(candNum).padStart(2, '0')} (${candidate.title})`,
            promoted: false,
          };
        }
      }
    }

    // If all scenes in the current tier are solved, look in the next higher tier!
    if (selectedTier === 1 || selectedTier === 2) {
      for (const nextT of [selectedTier + 1, 3]) {
        if (nextT > 3) continue;
        const nextTierPool = withTier.filter(s => s.tier === nextT);
        const unsolvedInNext = nextTierPool.find(s => !nextSolvedSet.has(s.id));
        if (unsolvedInNext) {
          return {
            nextSceneId: unsolvedInNext.id,
            nextTier: nextT,
            nextLabel: `Tier ${nextT}: ${unsolvedInNext.title}`,
            promoted: true,
          };
        }
      }
    }
    return null;
  }

  function executeAutoRotate(target) {
    if (!target) return;
    if (autoRotateTimerRef.current) {
      clearTimeout(autoRotateTimerRef.current);
      autoRotateTimerRef.current = null;
    }
    setAutoRotatePending(null);

    if (target.promoted && target.nextTier !== selectedTier) {
      setSelectedTier(target.nextTier);
      try {
        localStorage.setItem(STORAGE_KEY_TIER, String(target.nextTier));
      } catch {}
      setSceneBatchIndex(0);
      pushToast && pushToast({
        kind: 'confirmed',
        title: `🎉 Tier Complete! Auto-Advanced to Tier ${target.nextTier}!`,
        body: `All scenes in the previous tier are complete. Welcome to ${getTierMeta(target.nextTier).label}!`,
      });
    } else {
      const pool = scenes
        .map(s => ({ ...s, tier: inferSceneTierClient(s) }))
        .filter(s => (target.nextTier === 'all' ? true : s.tier === target.nextTier));
      const idxInPool = pool.findIndex(s => s.id === target.nextSceneId);
      if (idxInPool >= 0) {
        setSceneBatchIndex(Math.floor(idxInPool / 10));
      }
    }
    setActiveSceneId(target.nextSceneId);
  }

  function handleJumpToScene(sceneId) {
    if (!sceneId) return;
    cancelAutoRotate();
    setActiveSceneId(sceneId);
    const pool = scenes
      .map(s => ({ ...s, tier: inferSceneTierClient(s) }))
      .filter(s => (selectedTier === 'all' ? true : s.tier === selectedTier));
    const idxInPool = pool.findIndex(s => s.id === sceneId);
    if (idxInPool >= 0) {
      setSceneBatchIndex(Math.floor(idxInPool / 10));
    }
  }

  function handleSelectSceneOption(scene, opt) {
    if (!scene || !opt) return;
    if (solvedScenes.has(scene.id) || sceneLockedRef.current.has(scene.id)) {
      return;
    }

    const previous = getSceneProgress(scene, sceneAttemptsRef.current);
    if (previous.clickedReplies.includes(opt.reply)) return;

    const progress = updateSceneProgress(scene, current => ({
      ...current,
      clickedReplies: [...current.clickedReplies, opt.reply],
      peeked: current.peeked
        || !immersionMode
        || Boolean(sceneGlossVisible[scene.id]),
    }));

    if (!opt.correct) {
      onDopamineFail && onDopamineFail();
      return;
    }

    sceneLockedRef.current.add(scene.id);

    const nextSolvedSet = new Set([
      ...solvedScenes,
      ...sceneLockedRef.current,
    ]);
    setSolvedScenes(nextSolvedSet);

    saveLearningObject(
      STORAGE_KEY_SCENES_V3,
      Array.from(nextSolvedSet)
    );

    onDopamineWin && onDopamineWin(inferSceneTierClient(scene), {
      activity: 'scene',
      attemptCount: progress.clickedReplies.length,
      immersionNoPeek: immersionMode
        && !progress.peeked
        && !sceneGlossVisible[scene.id],
    });

    if (autoRotate) {
      cancelAutoRotate();
      const target = findNextUnsolvedSceneTarget(scene.id, nextSolvedSet);

      if (target) {
        setAutoRotatePending({ ...target, fromSceneId: scene.id });
        autoRotateTimerRef.current = setTimeout(() => {
          executeAutoRotate(target);
        }, 2000);
      }
    }
  }

  // --------------------------------------------------------------------------
  // TAB 2: WORDS & PLURALS TRAINER STATE (Tiered + Persistent + Synonym-Aware)
  // --------------------------------------------------------------------------
  const [learnMode, setLearnMode] = useStateL(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY_WORDS_MODE);
      return ['mixed', 'typing', 'mc'].includes(saved) ? saved : 'mixed';
    } catch {
      return 'mixed';
    }
  });
  const [masteredIds, setMasteredIds] = useStateL(() => {
    try {
      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY_MASTERED_WORDS) || '[]');
      return Array.isArray(raw) ? new Set(raw) : new Set();
    } catch {
      return new Set();
    }
  });
  const [learnQueue, setLearnQueue] = useStateL([]);
  const [currentCard, setCurrentCard] = useStateL(null);
  const [currentCardMcOptions, setCurrentCardMcOptions] = useStateL([]);
  const [typingInput, setTypingInput] = useStateL('');
  const [cardResult, setCardResult] = useStateL(null);
  const typingInputRef = useRefL(null);
  const learnInitializedRef = useRefL(false);
  const trainerAnsweredRef = useRefL(false);
  const trainerFailureCountsRef = useRefL({});

  const buildDeckFromLexicon = useCallbackL((lexList) => {
    const dynamicItems = [];

    lexList.forEach(entry => {
      const cleanTerm = entry.term.toLowerCase();
      const entryTier = inferLexiconTier(entry);
      const tierBadge = getTierMeta(entryTier).short;
      const acceptedInfo = getAcceptedAnswersForEntry(entry, lexList);
      if (acceptedInfo.skipDuplicateCard) return;
      const acceptedAnswers = acceptedInfo.accepted;
      const entryNormMeaning = normalizeMeaningKey(entry.meaning);

      // Distractors chosen from same tier first, strictly excluding synonyms with the same meaning!
      const sameTierMeanings = Array.from(new Set(
        lexList
          .filter(e =>
            inferLexiconTier(e) === entryTier &&
            e.meaning &&
            normalizeMeaningKey(e.meaning) !== entryNormMeaning &&
            !acceptedInfo.synonymMeanings.has(e.meaning)
          )
          .map(e => e.meaning)
      ));
      const fallbackMeanings = Array.from(new Set(
        lexList
          .filter(e =>
            e.meaning &&
            normalizeMeaningKey(e.meaning) !== entryNormMeaning &&
            !acceptedInfo.synonymMeanings.has(e.meaning)
          )
          .map(e => e.meaning)
      ));
      const wrongPool = sameTierMeanings.length >= 3 ? sameTierMeanings : fallbackMeanings;
      const pickedWrong = window.fisherYatesShuffle(wrongPool).slice(0, 3);
      const rawChoices = [entry.meaning, ...pickedWrong];

      const displayAnswerText = acceptedInfo.displayAnswer || entry.term;

      dynamicItems.push({
        id: `lex_mc_${entry.id}`,
        tier: entryTier,
        qType: 'mc',
        prompt: `What is "${entry.term}"?`,
        subHint: `${tierBadge} • ${entry.emoji || '🌊'} Example: "${entry.example}"`,
        correctChoice: entry.meaning,
        choices: rawChoices,
        displayAnswer: entry.meaning,
        explanation: `${displayAnswerText} (${entry.pos}) means "${entry.fullMeaning || entry.meaning}". ${entry.notes || ''}`,
      });

      dynamicItems.push({
        id: `lex_type_${entry.id}`,
        tier: entryTier,
        qType: 'typing',
        prompt: `Write "${entry.meaning}" in Seamenese`,
        subHint: `${tierBadge} • ${entry.emoji || '🌊'} Part of speech: ${entry.pos.toUpperCase()} • Cue: ${entry.hint}`,
        accepted: acceptedAnswers,
        bareStem: acceptedInfo.bareStem,
        canonicalTerm: cleanTerm,
        displayAnswer: displayAnswerText,
        explanation: `${displayAnswerText} (${entry.pos}) = ${entry.fullMeaning || entry.meaning}. Example: "${entry.example}"`,
      });
    });

    const allCards = [...PRIORITY_TRAINER_CARDS, ...dynamicItems];
    // Sort progressively by tier (1 -> 2 -> 3)
    return allCards.sort((a, b) => (a.tier || 1) - (b.tier || 1));
  }, []);

  const fullTrainerDeck = useMemoL(() => buildDeckFromLexicon(lexicon), [buildDeckFromLexicon, lexicon]);

  const trainerTierCounts = useMemoL(() => {
    const counts = { 1: 0, 2: 0, 3: 0, all: fullTrainerDeck.length };
    fullTrainerDeck.forEach(c => {
      const t = c.tier || 1;
      counts[t] = (counts[t] || 0) + 1;
    });
    return counts;
  }, [fullTrainerDeck]);

  const presentLearnCard = useCallbackL((card) => {
    trainerAnsweredRef.current = false;
    setCurrentCard(card || null);
    setTypingInput('');
    setCardResult(null);
    if (card && card.qType === 'mc') {
      const rawOpts = (card.choices || []).map(ch => ({
        text: ch,
        correct: ch === card.correctChoice,
      }));
      setCurrentCardMcOptions(createShuffledMcOptions(rawOpts));
    } else {
      setCurrentCardMcOptions([]);
      setTimeout(() => typingInputRef.current?.focus(), 40);
    }
  }, []);

  const rebuildQueueForModeAndTier = useCallbackL(
    (mode, tierFilter, masteredSet) => {
      const filtered = fullTrainerDeck
        .filter(item => {
          if (masteredSet.has(item.id)) return false;
          if (tierFilter !== 'all' && (item.tier || 1) !== tierFilter) {
            return false;
          }
          if (mode === 'typing') return item.qType === 'typing';
          if (mode === 'mc') return item.qType === 'mc';
          return true;
        })
        .map(item => ({
          ...item,
          failedCount: Math.max(
            Number(item.failedCount) || 0,
            trainerFailureCountsRef.current[item.id] || 0
          ),
        }));

      setLearnQueue(filtered);
      presentLearnCard(filtered[0] || null);
    },
    [fullTrainerDeck, presentLearnCard]
  );

  // Initialize trainer deck ONCE so SSE dictionary refreshes never wipe the active question
  useEffectL(() => {
    if (!learnInitializedRef.current && fullTrainerDeck.length > 0) {
      learnInitializedRef.current = true;
      rebuildQueueForModeAndTier(learnMode, selectedTier, masteredIds);
    }
  }, [fullTrainerDeck, learnMode, selectedTier, masteredIds, rebuildQueueForModeAndTier]);

  function handleSwitchLearnMode(newMode) {
    setLearnMode(newMode);
    try {
      localStorage.setItem(STORAGE_KEY_WORDS_MODE, newMode);
    } catch {}
    rebuildQueueForModeAndTier(newMode, selectedTier, masteredIds);
  }

  function handleResetLearnDeck() {
    const emptySet = new Set();
    setMasteredIds(emptySet);
    try {
      localStorage.removeItem(STORAGE_KEY_MASTERED_WORDS);
    } catch {}
    rebuildQueueForModeAndTier(learnMode, selectedTier, emptySet);
  }

  function insertSpecialChar(ch) {
    if (cardResult || trainerAnsweredRef.current) return;
    const inp = typingInputRef.current;
    if (!inp) {
      setTypingInput(prev => prev + ch);
      return;
    }
    const start = inp.selectionStart ?? typingInput.length;
    const end = inp.selectionEnd ?? typingInput.length;
    const nextVal = typingInput.slice(0, start) + ch + typingInput.slice(end);
    setTypingInput(nextVal);
    setTimeout(() => {
      inp.focus();
      inp.setSelectionRange(start + ch.length, start + ch.length);
    }, 10);
  }

  function evaluateTrainerOutcome(isCorrect, userAnswerText, orthoReminder = '', selectedOptId = null) {
    if (!currentCard || cardResult || trainerAnsweredRef.current) return;
    trainerAnsweredRef.current = true;

    const rest = learnQueue.slice(1);
    const failedCount = Math.max(
      Number(currentCard.failedCount) || 0,
      trainerFailureCountsRef.current[currentCard.id] || 0
    );

    const result = {
      correct: isCorrect,
      userAnswerText,
      orthoNote: orthoReminder,
      displayAnswer: currentCard.displayAnswer,
      explanation: currentCard.explanation,
      selectedOptId,
    };

    if (isCorrect) {
      const nextMastered = new Set(masteredIds);
      nextMastered.add(currentCard.id);
      setMasteredIds(nextMastered);
      saveLearningObject(
        STORAGE_KEY_MASTERED_WORDS,
        Array.from(nextMastered)
      );

      setLearnQueue(rest);
      setCardResult(result);

      onDopamineWin && onDopamineWin(currentCard.tier || 1, {
        activity: currentCard.qType === 'typing'
          ? 'words_typing'
          : 'words_mc',
        exactOrthography: hasExactTrainerOrthography(
          currentCard,
          userAnswerText
        ),
        failedCount,
      });
      return;
    }

    const nextFailedCount = failedCount + 1;
    trainerFailureCountsRef.current[currentCard.id] = nextFailedCount;

    const reinsertIndex = Math.min(2, rest.length);
    const nextQueue = [...rest];
    nextQueue.splice(reinsertIndex, 0, {
      ...currentCard,
      failedCount: nextFailedCount,
    });

    setLearnQueue(nextQueue);
    setCardResult({
      ...result,
      requeueSteps: reinsertIndex + 1,
    });
    onDopamineFail && onDopamineFail();
  }

  function handleSubmitTyping() {
    if (!currentCard || cardResult || trainerAnsweredRef.current) return;
    const answer = typingInput.trim();
    if (!answer) return;

    const key = seaAnswerKey(answer);
    const accepted = getTrainerAcceptedAnswers(currentCard);
    const isCorrect = accepted.some(candidate =>
      seaAnswerKey(candidate) === key
    );

    const hasDiaeresisTarget = [
      currentCard.canonicalTerm,
      currentCard.displayAnswer,
      ...accepted,
    ].some(candidate =>
      String(candidate || '').normalize('NFC')
        .toLowerCase().includes('\u00eb')
    );

    let orthoNote = '';
    if (isCorrect && currentCard.bareStem && key === seaAnswerKey(currentCard.bareStem)) {
      orthoNote = `💡 Bare stem accepted! Note: "${currentCard.displayAnswer}" is the infinitive, while "${currentCard.bareStem}" is the 1sg/2sg conjugated stem (Ëi/Te).`;
    } else if (
      isCorrect &&
      hasDiaeresisTarget &&
      !hasExactTrainerOrthography(currentCard, answer)
    ) {
      orthoNote = `💡 Accepted! Use the canonical ë spelling ("${currentCard.displayAnswer}") for the +4 Diaeresis Mastery bonus.`;
    } else if (
      isCorrect &&
      currentCard.canonicalTerm &&
      key !== seaAnswerKey(currentCard.canonicalTerm)
    ) {
      orthoNote = `💡 Variant / Synonym accepted! "${answer}" is valid (${currentCard.displayAnswer}).`;
    }

    evaluateTrainerOutcome(isCorrect, answer, orthoNote);
  }

  function handleSelectLearnMcOption(opt) {
    if (!currentCard || cardResult || trainerAnsweredRef.current) return;
    evaluateTrainerOutcome(opt.correct, opt.text, '', opt.optId);
  }

  function handleAdvanceLearnCard() {
    presentLearnCard(learnQueue[0] || null);
  }

  // --------------------------------------------------------------------------
  // TAB 3: JAI-SEMEN CONJUGATION DOJO STATE (Tiered + Anti-Spam + All 19 Tables)
  // --------------------------------------------------------------------------
  const PRONOUN_DRILL_LIST = [
    { key: 'ei',  label: 'Ëi (I)',            pronWord: 'Ëi',  tier: 1 },
    { key: 'te',  label: 'Te (You sg.)',      pronWord: 'Te',  tier: 1 },
    { key: 'la',  label: 'La (He/She/It)',    pronWord: 'La',  tier: 1 },
    { key: 'na',  label: 'Na (We)',           pronWord: 'Na',  tier: 2 },
    { key: 'ton', label: 'Ton (You pl.)',     pronWord: 'Ton', tier: 2 },
    { key: 'lon', label: 'Lon (They)',        pronWord: 'Lon', tier: 2 },
  ];

  const [verbStreak, setVerbStreak] = useStateL(() => {
    try {
      return Math.max(0, Number(localStorage.getItem(STORAGE_KEY_VERB_STREAK)) || 0);
    } catch {
      return 0;
    }
  });
  const [currentDrill, setCurrentDrill] = useStateL(null);
  const [drillAnswerState, setDrillAnswerState] = useStateL(null);
  const drillInitRef = useRefL(false);
  const drillAnsweredRef = useRefL(false);

  const verbTierCounts = useMemoL(() => {
    const counts = { 1: 0, 2: 0, 3: 0, all: conjugations.length };
    conjugations.forEach(c => {
      const t = inferVerbTier(c);
      counts[t] = (counts[t] || 0) + 1;
    });
    return counts;
  }, [conjugations]);

  const loadNewVerbDrill = useCallbackL((overrideTier) => {
    drillAnsweredRef.current = false;
    if (!conjugations || conjugations.length === 0) return;
    const activeTier = overrideTier !== undefined ? overrideTier : selectedTier;

    // On Graduate Tier 3 (or All Tiers 25% of the time), mix in Multi-Verb Chaining Drills!
    if ((activeTier === 3 || activeTier === 'all') && Math.random() < (activeTier === 3 ? 0.35 : 0.15)) {
      const mv = GRADUATE_MULTIVERB_DRILLS[Math.floor(Math.random() * GRADUATE_MULTIVERB_DRILLS.length)];
      setCurrentDrill({
        isMultiVerb: true,
        tier: 3,
        promptText: mv.prompt,
        ruleNote: mv.ruleNote,
        correctForm: mv.correctForm,
        options: createShuffledMcOptions(mv.options),
      });
      setDrillAnswerState(null);
      return;
    }

    const tierPool = activeTier === 'all'
      ? conjugations
      : conjugations.filter(c => inferVerbTier(c) === activeTier);
    const usableVerbs = tierPool.length > 0 ? tierPool : conjugations;
    const verb = usableVerbs[Math.floor(Math.random() * usableVerbs.length)];
    const verbTier = inferVerbTier(verb);

    // Tier 1 focuses on core singular/common pronouns (Ëi, Te, La, Na); Tiers 2 & 3 use all 6 pronouns
    const pronPool = activeTier === 1
      ? PRONOUN_DRILL_LIST.slice(0, 4)
      : PRONOUN_DRILL_LIST;
    const pron = pronPool[Math.floor(Math.random() * pronPool.length)];
    const correctForm = verb[pron.key];

    const distractorPool = new Set([correctForm]);
    [verb.ei, verb.ton, verb.na, verb.la, verb.infinitive].forEach(f => {
      if (f) distractorPool.add(f);
    });
    const uniqueForms = Array.from(distractorPool).slice(0, 4);
    while (uniqueForms.length < 4) {
      uniqueForms.push(`${verb.ei}u`);
    }

    const rawOptions = uniqueForms.map(form => ({
      form,
      text: `${pron.pronWord} ${form}`,
      correct: form.toLowerCase() === correctForm.toLowerCase(),
    }));

    setCurrentDrill({
      isMultiVerb: false,
      tier: verbTier,
      verb,
      pron,
      correctForm,
      options: createShuffledMcOptions(rawOptions),
    });
    setDrillAnswerState(null);
  }, [conjugations, selectedTier]);

  useEffectL(() => {
    if (!drillInitRef.current && conjugations.length > 0) {
      drillInitRef.current = true;
      loadNewVerbDrill(selectedTier);
    }
  }, [conjugations, loadNewVerbDrill, selectedTier]);

  function handleAnswerVerbDrill(opt) {
    if (!currentDrill || drillAnswerState || drillAnsweredRef.current) {
      return;
    }
    drillAnsweredRef.current = true;

    const answeredAt = Date.now();
    const { after } = mutateEconomy(previous => ({
      ...previous,
      verbAttempts: [...previous.verbAttempts, answeredAt],
    }));
    const verbSessionCount = after.verbAttempts.length;

    setDrillAnswerState({
      correct: opt.correct,
      chosenOptId: opt.optId,
      chosenForm: opt.form,
      verbSessionCount,
    });

    if (opt.correct) {
      const nextStreak = verbStreak + 1;
      setVerbStreak(nextStreak);
      try {
        localStorage.setItem(STORAGE_KEY_VERB_STREAK, String(nextStreak));
      } catch {}

      onDopamineWin && onDopamineWin(currentDrill.tier || 1, {
        activity: currentDrill.isMultiVerb
          ? 'verb_multiverb'
          : 'verb_single',
        verbSessionCount,
      });
    } else {
      setVerbStreak(0);
      try {
        localStorage.setItem(STORAGE_KEY_VERB_STREAK, '0');
      } catch {}
      onDopamineFail && onDopamineFail();
    }
  }

  // --------------------------------------------------------------------------
  // TAB 4: GRAMMAR, PLURALIZER WIDGET & 45 CORPUS STATE
  // --------------------------------------------------------------------------
  // --------------------------------------------------------------------------
  // TAB 5: SM-2 SRS ACTIVE RECALL DECK STATE (Tiered + Anti-Cheat + Active Recall)
  // --------------------------------------------------------------------------
  const srs = useSrsPractice({
    lexicon,
    selectedTier,
    activeTab,
    economy,
    economyNow,
    mutateEconomy,
    onDopamineWin,
    onRecallMiss: () => {
      if (dopamine.enabled) {
        playDopamineSynth('fail', dopamine.soundEnabled, dopamine.equippedSound);
      }
    },
    pushToast,
  });

  // Unified Tier Switcher across all pools
  function handleSelectTier(newTier) {
    cancelAutoRotate();
    setSelectedTier(newTier);
    try {
      localStorage.setItem(STORAGE_KEY_TIER, String(newTier));
    } catch {}
    setSceneBatchIndex(0);
    setActiveSceneId(null); // Auto-focus first unsolved scene in the new tier
    srs.reset();
    rebuildQueueForModeAndTier(learnMode, newTier, masteredIds);
    loadNewVerbDrill(newTier);
  }

  // --------------------------------------------------------------------------
  // TAB 6: INTERACTIVE LEXICON STATE
  // --------------------------------------------------------------------------

  const equippedTitleObj = getEquippedTitleObj(dopamine.equippedTitle);
  const activeTotalMult = getTotalActivePointMultiplier(dopamine);
  const shieldsCount = Number(dopamine.perks?.streak_shields || 0);
  const doubleXpCount = getOverdriveCharges(dopamine);

  // Scene counts per tier + filtered scenes for active tier
  const sceneTierCounts = useMemoL(() => {
    const counts = { 1: 0, 2: 0, 3: 0, all: scenes.length };
    scenes.forEach(s => {
      const t = inferSceneTierClient(s);
      counts[t] = (counts[t] || 0) + 1;
    });
    return counts;
  }, [scenes]);

  const tierFilteredScenes = useMemoL(() => {
    const withTier = scenes.map(s => ({ ...s, tier: inferSceneTierClient(s) }));
    if (selectedTier === 'all') {
      return [...withTier].sort((a, b) => a.tier - b.tier);
    }
    const matches = withTier.filter(s => s.tier === selectedTier);
    return matches.length > 0 ? matches : withTier;
  }, [scenes, selectedTier]);

  const tierSolvedCount = useMemoL(
    () => tierFilteredScenes.filter(s => solvedScenes.has(s.id)).length,
    [tierFilteredScenes, solvedScenes]
  );

  // Active scene in Focus Mode (defaults to first unsolved scene in current tier)
  const activeSceneIndex = useMemoL(() => {
    if (tierFilteredScenes.length === 0) return 0;
    if (activeSceneId) {
      const idx = tierFilteredScenes.findIndex(s => s.id === activeSceneId);
      if (idx >= 0) return idx;
    }
    const firstUnsolvedIdx = tierFilteredScenes.findIndex(s => !solvedScenes.has(s.id));
    return firstUnsolvedIdx >= 0 ? firstUnsolvedIdx : 0;
  }, [tierFilteredScenes, activeSceneId, solvedScenes]);

  const activeScene = tierFilteredScenes[activeSceneIndex] || tierFilteredScenes[0] || null;

  const tierProgressPct = tierFilteredScenes.length
    ? Math.round((tierSolvedCount / tierFilteredScenes.length) * 100)
    : 0;

  const visibleAllScenes = useMemoL(() => {
    return tierFilteredScenes.filter(s => {
      const isSolved = solvedScenes.has(s.id);
      if (sceneFilter === 'unsolved') return !isSolved;
      if (sceneFilter === 'solved') return isSolved;
      return true;
    });
  }, [tierFilteredScenes, solvedScenes, sceneFilter]);

  useEffectL(() => {
    if (activeTab !== 'scenes') return;

    const visibleScenes = sceneViewMode === 'grid'
      ? visibleAllScenes
      : [activeScene];

    visibleScenes.filter(Boolean).forEach(scene => {
      if (!immersionMode || sceneGlossVisible[scene.id]) {
        markScenePeeked(scene);
      }
    });
  }, [
    activeTab,
    immersionMode,
    sceneViewMode,
    visibleAllScenes,
    activeScene,
    sceneGlossVisible,
    markScenePeeked,
  ]);

  function renderSceneCard(scene, numLabel, totalInTier) {
    if (!scene) return null;
    const isSolved = solvedScenes.has(scene.id);
    const options = shuffledSceneOptions[scene.id] || createShuffledMcOptions(scene.options || []);
    const progress = getSceneProgress(scene, sceneAttempts);
    const selectedMap = Object.fromEntries(
      options
        .filter(option => progress.clickedReplies.includes(option.reply))
        .map(option => [option.optId, true])
    );
    const sceneScoreOptions = {
      activity: 'scene',
      attemptCount: progress.clickedReplies.length + 1,
      immersionNoPeek: immersionMode
        && !progress.peeked
        && !sceneGlossVisible[scene.id],
    };
    const glossShown = !immersionMode || !!sceneGlossVisible[scene.id];
    const tierMeta = getTierMeta(inferSceneTierClient(scene));
    const isAutoRotateHere = autoRotatePending && autoRotatePending.fromSceneId === scene.id;

    return (
      <article
        key={scene.id}
        id={`learn-scene-card-${scene.id}`}
        className={`learn-card ${isSolved ? 'is-solved' : ''}`}
      >
        <div className="learn-card-head">
          <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
            {numLabel && (
              <span className="learn-badge mono" style={{ fontSize: 11 }}>
                Scene #{numLabel}{totalInTier ? ` of ${totalInTier}` : ''}
              </span>
            )}
            <span className="learn-badge mono" style={{ fontSize: 10 }}>
              {tierMeta.short}
            </span>
            {!isSolved && (
              <ActivityPointsBadge
                tier={inferSceneTierClient(scene)}
                {...sceneScoreOptions}
              />
            )}
          </div>
          {isSolved ? (
            <span className="learn-badge learn-badge-success">✓ Completed</span>
          ) : (
            <span className="learn-badge learn-badge-amber">1 Correct Answer</span>
          )}
        </div>

        <h4 className="serif learn-card-title">{scene.title}</h4>

        <p className="learn-scene-setup">{scene.scene}</p>

        {Array.isArray(scene.turns) && scene.turns.map((turn, tIdx) => (
          <div key={tIdx} className="learn-dialogue-line" style={{ marginBottom: 6, opacity: 0.92 }}>
            <strong>{turn.speaker}:</strong> "
            <InteractiveSeameneseText
              text={turn.seamenese}
              tokenMap={tokenMap}
              onInspectWord={entry => {
                markScenePeeked(scene);
                setInspectedWord(entry);
              }}
            />
            "
            {glossShown && turn.gloss && (
              <div className="learn-scene-gloss" style={{ marginTop: 4, marginBottom: 0 }}>
                English Turn Gloss: "{turn.gloss}"
              </div>
            )}
          </div>
        ))}

        <div className="learn-dialogue-line">
          <strong>{scene.speaker}:</strong> "
          <InteractiveSeameneseText
            text={scene.seamenese}
            tokenMap={tokenMap}
            onInspectWord={entry => {
              markScenePeeked(scene);
              setInspectedWord(entry);
            }}
          />
          "
        </div>

        {glossShown && (
          <div className="learn-scene-gloss">
            English Prompt Gloss: "{scene.gloss}"
          </div>
        )}

        {immersionMode && (
          <button
            type="button"
            className="btn"
            style={{ marginBottom: 10, fontSize: 11, padding: '6px 12px' }}
            onClick={() => handleToggleSceneGloss(scene)}
          >
            👁️ {sceneGlossVisible[scene.id] ? 'Hide English Hint' : 'Show English Hint'}
          </button>
        )}

        <div className="learn-options-list">
          {options.map(opt => {
            const wasClicked = !!selectedMap[opt.optId];
            const showCorrectOnSolved = isSolved && opt.correct;
            return (
              <div key={opt.optId}>
                <button
                  type="button"
                  disabled={isSolved || wasClicked}
                  className={`learn-reply-btn ${wasClicked || showCorrectOnSolved ? (opt.correct ? 'correct-choice' : 'wrong-choice') : ''}`}
                  onClick={() => handleSelectSceneOption(scene, opt)}
                >
                  <span className="learn-opt-letter mono">{opt.letter}</span>
                  <span className="learn-opt-text">{opt.reply}</span>
                </button>
                {(wasClicked || showCorrectOnSolved) && (
                  <div className={`learn-feedback-box ${opt.correct ? 'is-correct' : 'is-wrong'}`}>
                    <strong>{opt.correct ? '✅ Wo tuk! Correct:' : '❌ Not quite:'}</strong> {opt.feedback}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {isAutoRotateHere && (
          <div className="learn-auto-rotate-banner">
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              <span className="mono" style={{ fontSize: 12, color: '#34d399', fontWeight: 700 }}>
                {autoRotatePending.promoted
                  ? `🎉 Tier complete! Auto-advancing to ${autoRotatePending.nextLabel}…`
                  : `⏭️ Auto-rotating to ${autoRotatePending.nextLabel}…`}
              </span>
            </div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
              <button
                type="button"
                className="btn btn-accent"
                style={{ padding: '6px 12px', fontSize: 12 }}
                onClick={() => executeAutoRotate(autoRotatePending)}
              >
                Next Now →
              </button>
              <button
                type="button"
                className="btn"
                style={{ padding: '6px 12px', fontSize: 12 }}
                onClick={cancelAutoRotate}
              >
                ⏸️ Stay Here
              </button>
            </div>
          </div>
        )}
      </article>
    );
  }

  return (
    <div className="learn-env-shell">
      {/* Top Learning Environment Header */}
      <header className="learn-env-header">
        <div className="learn-env-header-top">
          <div className="learn-env-header-left">
            <div className="learn-env-title-row">
              <h2 className="serif learn-env-title">🌊 Seamenese Dojo</h2>
              <span className="learn-badge learn-badge-amber mono">
                {lexicon.length} Words · {scenes.length} Scenes · {conjugations.length} Verbs
              </span>
            </div>
          </div>

          <div className="learn-env-header-right">
            {/* Silly Dopamine Mode Toggle (Separated from active stats so portrait never overflows) */}
            <button
              type="button"
              className={`learn-dopamine-toggle ${dopamine.enabled ? 'is-on' : ''}`}
              onClick={onToggleDopamine}
              title="Toggle Silly Dopamine Mode"
            >
              <span className="mono">⚡ Dopamine: {dopamine.enabled ? 'ON' : 'OFF'}</span>
            </button>

            {/* Points Balance + Dedicated Store Button */}
            <button
              type="button"
              className="learn-shop-btn"
              onClick={onOpenShop}
              title="View your points or open the Seamenese Grand Customization Emporium"
            >
              <span className="mono" style={{ color: '#fbbf24', fontWeight: 700 }}>
                🪙 {Number(dopamine.points || 0).toLocaleString()}p
              </span>
            </button>

            <button
              type="button"
              className="learn-shop-btn learn-store-explicit-btn"
              onClick={onOpenShop}
              title="Open Seamenese Grand Customization Emporium"
            >
              <span className="mono" style={{ fontWeight: 700 }}>
                🛒 Store
              </span>
            </button>
          </div>
        </div>

        {/* Dedicated Active Stats, Power-Ups, Multiplier, Streak & Title Strip */}
        <div className="learn-active-stats-bar">
          <button
            type="button"
            className="learn-stat-pill learn-stat-title"
            onClick={onOpenShop}
            title="Equipped Learner Title — click to customize in the Emporium"
          >
            <span className="learn-title-badge">{equippedTitleObj.name}</span>
          </button>

          {dopamine.enabled && (
            <>
              <span
                className="learn-stat-pill learn-stat-streak mono"
                title={`Current Streak: ${dopamine.streak || 0} (Best: ${dopamine.bestStreak || 0})`}
              >
                🔥 Streak: <strong>{dopamine.streak || 0}</strong>
              </span>

              <span
                className={`learn-stat-pill learn-stat-mult mono ${activeTotalMult > 1 ? 'is-boosted' : ''}`}
                title="Applies to eligible mastery awards. Practice caps and diminished verb drills are excluded."
              >
                🚀 <strong>{activeTotalMult}x</strong> Pts
              </span>

              <button
                type="button"
                className={`learn-stat-pill learn-stat-shield mono ${shieldsCount > 0 ? 'is-active' : ''}`}
                onClick={onOpenShop}
                title="Active Streak Shields — protects your streak on a wrong answer (click to buy in Perks)"
              >
                🛡️ <strong>{shieldsCount}</strong> {shieldsCount === 1 ? 'Shield' : 'Shields'}
              </button>

              {doubleXpCount > 0 && (
                <button
                  type="button"
                  className="learn-stat-pill learn-stat-overdrive is-active mono"
                  onClick={onOpenShop}
                  title="2x Overdrive Charges Remaining"
                >
                  ⚡ 2x Overdrive: <strong>{doubleXpCount}</strong>
                </button>
              )}
            </>
          )}
        </div>
      </header>

      {/* Savage Seamenese Reprimand Banner (when a wrong answer triggers Disappointed Jorn!) */}
      {dopamine.enabled && activeRoast && (
        <div className="savage-roast-banner" onClick={onDismissRoast}>
          <div className="savage-roast-avatar">😤</div>
          <div className="savage-roast-content">
            <div className="savage-roast-eyebrow mono">DISAPPOINTED JORN REPRIMAND · STREAK RESET TO 0</div>
            <div className="savage-roast-text">{activeRoast}</div>
          </div>
          <button type="button" className="savage-roast-close">✕</button>
        </div>
      )}

      {/* Exercise navigation */}
      <nav className="learn-nav-tabs" role="tablist">
        {LEARN_TABS.map(t => (
          <button
            key={t.id}
            type="button"
            role="tab"
            aria-selected={activeTab === t.id}
            className={`learn-nav-tab ${activeTab === t.id ? 'active' : ''}`}
            onClick={() => setActiveTab(t.id)}
          >
            <span className="learn-nav-icon">{t.icon}</span>
            <div className="learn-nav-labels">
              <span className="learn-nav-main">{t.label}</span>
              <span className="learn-nav-sub mono">{t.sub}</span>
            </div>
          </button>
        ))}
      </nav>

      {/* =====================================================================
          TAB 1: SCENES (Comprehensible Input Dialogues — Centered Focus + Auto-Rotate)
         ===================================================================== */}
      {activeTab === 'scenes' && (
        <section className="learn-tab-section">
          <div className="learn-focus-shell">
            <DifficultyTierBar
              selectedTier={selectedTier}
              onSelectTier={handleSelectTier}
              tierCounts={sceneTierCounts}
            />

            <div className="learn-section-top">
              <div>
                <h3 className="serif learn-section-h">
                  Comprehensible Input Scenes
                </h3>
                <p className="learn-section-desc">
                  Tap any underlined Seamenese word to inspect its meaning &amp; conjugation.
                </p>
              </div>
              <div className="learn-top-actions-row">
                <button
                  type="button"
                  className={`chip ${sceneViewMode === 'focus' ? 'on' : ''}`}
                  onClick={() => handleSwitchSceneViewMode('focus')}
                >
                  🎯 Focus Card
                </button>
                <button
                  type="button"
                  className={`chip ${sceneViewMode === 'grid' ? 'on' : ''}`}
                  onClick={() => handleSwitchSceneViewMode('grid')}
                >
                  📋 Browse All ({tierFilteredScenes.length})
                </button>
                <button
                  type="button"
                  className={`chip ${autoRotate ? 'on' : ''}`}
                  onClick={handleToggleAutoRotate}
                  title="Automatically rotate to the next unsolved scene 2 seconds after solving"
                >
                  🔄 Auto-Rotate: {autoRotate ? 'ON' : 'OFF'}
                </button>
                <button
                  type="button"
                  className="btn btn-warm learn-gen-btn"
                  onClick={handleGenerateMoreScenes}
                  disabled={generatingScenes || (!adminAuthed && isGenLimitReached && unseenCachedCount === 0)}
                  title={
                    generatingScenes
                      ? `${sceneGen.stageLabel || 'Generating scenes…'}${sceneGen.detail ? ` — ${sceneGen.detail}` : ''}`
                      : adminAuthed
                        ? 'Admin: Unlimited scene generations'
                        : unseenCachedCount > 0
                          ? `Fetch ${Math.min(10, unseenCachedCount)} unseen community-cached scenes for FREE (0 daily quota used!)`
                          : isGenLimitReached
                            ? `Daily AI generation limit reached (${NON_ADMIN_DAILY_GEN_LIMIT}/${NON_ADMIN_DAILY_GEN_LIMIT} today) and all cached scenes are already loaded.`
                            : `Generate up to ${NON_ADMIN_DAILY_GEN_LIMIT} new scene batches per day (${remainingDailyGens}/${NON_ADMIN_DAILY_GEN_LIMIT} left today) — fetching existing community-cached batches is always free!`
                  }
                >
                  {generatingScenes && (
                    <span
                      className="learn-gen-btn-fill"
                      style={{ width: `${Math.max(5, Math.min(100, sceneGen.percent || 5))}%` }}
                    />
                  )}
                  <span className="learn-gen-btn-label">
                    {generatingScenes
                      ? `⏳ ${sceneGen.percent || 5}% · ${sceneGen.stageLabel || 'Generating…'}`
                      : unseenCachedCount > 0
                        ? `🎁 +${Math.min(10, unseenCachedCount)} Free Cached ${selectedTier !== 'all' ? `Tier ${selectedTier} ` : ''}Scenes (${unseenCachedCount} ready)`
                        : isGenLimitReached && !adminAuthed
                          ? `🔒 Daily Limit Reached (0/${NON_ADMIN_DAILY_GEN_LIMIT} today)`
                          : adminAuthed
                            ? `✨ +10 ${selectedTier !== 'all' ? `Tier ${selectedTier} ` : ''}Scenes (∞ Admin)`
                            : `✨ +10 ${selectedTier !== 'all' ? `Tier ${selectedTier} ` : ''}Scenes (${remainingDailyGens}/${NON_ADMIN_DAILY_GEN_LIMIT} today)`}
                  </span>
                </button>
              </div>
            </div>

            {/* Clean Progress Bar Card (replaces the cluttered scene number strip) */}
            <div className="learn-progress-card">
              <div className="learn-progress-meta mono">
                <span>
                  {sceneViewMode === 'focus' && activeScene
                    ? `Scene #${String(activeSceneIndex + 1).padStart(2, '0')} of ${tierFilteredScenes.length}`
                    : `${selectedTier === 'all' ? 'All Tiers' : `Tier ${selectedTier}`} Progress`}
                </span>
                <span className="learn-progress-stat">
                  ✓ {tierSolvedCount} / {tierFilteredScenes.length} Solved ({tierProgressPct}%)
                </span>
              </div>
              <div className="learn-progress-track">
                <div
                  className="learn-progress-fill"
                  style={{ width: `${tierProgressPct}%` }}
                />
              </div>
            </div>

            {sceneViewMode === 'focus' && activeScene && (
              <>
                {renderSceneCard(
                  activeScene,
                  String(activeSceneIndex + 1).padStart(2, '0'),
                  tierFilteredScenes.length
                )}

                {/* Two Static Bottom Navigation Buttons — ONLY shown in Focus Card mode & ALWAYS clickable */}
                <div className="learn-focus-bottom-nav">
                  <button
                    type="button"
                    className="btn learn-focus-nav-btn"
                    onClick={() => {
                      if (tierFilteredScenes.length === 0) return;
                      const prevIdx = (activeSceneIndex - 1 + tierFilteredScenes.length) % tierFilteredScenes.length;
                      handleJumpToScene(tierFilteredScenes[prevIdx]?.id);
                    }}
                  >
                    ← Prev Scene
                  </button>
                  <button
                    type="button"
                    className="btn btn-accent learn-focus-nav-btn"
                    onClick={() => {
                      if (tierFilteredScenes.length === 0) return;
                      const nextIdx = (activeSceneIndex + 1) % tierFilteredScenes.length;
                      handleJumpToScene(tierFilteredScenes[nextIdx]?.id);
                    }}
                  >
                    Next Scene →
                  </button>
                </div>
              </>
            )}
          </div>

          {sceneViewMode === 'grid' && (
            <>
              <div className="learn-batch-toolbar">
                <div className="learn-batch-filters">
                  {[
                    ['all', `All Scenes (${tierFilteredScenes.length})`],
                    ['unsolved', `Unsolved (${tierFilteredScenes.length - tierSolvedCount})`],
                    ['solved', `Completed (${tierSolvedCount})`],
                  ].map(([k, label]) => (
                    <button
                      key={k}
                      type="button"
                      className={`chip ${sceneFilter === k ? 'on' : ''}`}
                      onClick={() => setSceneFilter(k)}
                    >
                      {label}
                    </button>
                  ))}
                </div>
              </div>

              <div className="learn-scenes-grid">
                {visibleAllScenes.map(scene => {
                  const idxInTier = tierFilteredScenes.findIndex(s => s.id === scene.id);
                  return renderSceneCard(
                    scene,
                    String((idxInTier >= 0 ? idxInTier : 0) + 1).padStart(2, '0'),
                    tierFilteredScenes.length
                  );
                })}
              </div>
            </>
          )}
        </section>
      )}

      {/* =====================================================================
          TAB 2: WORDS & PLURALS TRAINER (Tiered Active Recall)
         ===================================================================== */}
      {activeTab === 'words' && (
        <section className="learn-tab-section">
          <DifficultyTierBar
            selectedTier={selectedTier}
            onSelectTier={handleSelectTier}
            tierCounts={trainerTierCounts}
          />

          <div className="learn-section-top">
            <div>
              <h3 className="serif learn-section-h">Learn Words &amp; Plurals Trainer</h3>
              <p className="learn-section-desc">
                Questions are organized into <strong>3 Difficulty Tiers</strong> so beginners start with core words &amp; greetings before moving to <code>-on</code> plurals and graduate multi-verb chains. Missed cards re-queue <strong>2 spots later</strong>!
              </p>
            </div>
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
              <span className="learn-badge mono">Remaining in Tier: {learnQueue.length}</span>
              <span className="learn-badge learn-badge-amber mono">Mastered: {masteredIds.size}</span>
            </div>
          </div>

          <div className="learn-progress-track">
            <div
              className="learn-progress-fill"
              style={{
                width: `${
                  learnQueue.length + masteredIds.size > 0
                    ? Math.round((masteredIds.size / (learnQueue.length + masteredIds.size)) * 100)
                    : 100
                }%`,
              }}
            />
          </div>

          <div className="learn-filter-row">
            {[
              ['mixed', 'Mixed (Typing + Multiple Choice)'],
              ['typing', '✍️ Typing Only'],
              ['mc', '🔘 Multiple Choice Only'],
            ].map(([mKey, mLabel]) => (
              <button
                key={mKey}
                type="button"
                className={`chip ${learnMode === mKey ? 'on' : ''}`}
                onClick={() => handleSwitchLearnMode(mKey)}
              >
                {mLabel}
              </button>
            ))}
            <button type="button" className="chip" onClick={handleResetLearnDeck}>
              ↻ Reset Deck
            </button>
          </div>

          {!currentCard ? (
            <div className="learn-card" style={{ textAlign: 'center', padding: 40 }}>
              <div style={{ fontSize: 42 }}>🎉</div>
              <h4 className="serif" style={{ fontSize: 28, margin: '8px 0' }}>
                Wo tuk! {selectedTier === 'all' ? 'All Tiers' : `Tier ${selectedTier}`} Complete!
              </h4>
              <p style={{ color: 'var(--fg-mute)' }}>
                You have mastered every card in this tier and mode.
              </p>
              <div style={{ display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap', marginTop: 14 }}>
                {selectedTier === 1 && (
                  <button type="button" className="btn btn-warm" onClick={() => handleSelectTier(2)}>
                    🐟 Advance to Tier 2: Blobber (Intermediate) →
                  </button>
                )}
                {selectedTier === 2 && (
                  <button type="button" className="btn btn-warm" onClick={() => handleSelectTier(3)}>
                    👑 Advance to Tier 3: Grandmaster (Graduate) →
                  </button>
                )}
                <button type="button" className="btn btn-accent" onClick={handleResetLearnDeck}>
                  ↻ Reset Deck &amp; Practice Again
                </button>
              </div>
            </div>
          ) : (
            <div className="learn-card learn-trainer-card">
              <div className="learn-card-head">
                <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
                  <span className="learn-badge">
                    {currentCard.qType === 'typing' ? '✍️ Type in Seamenese' : '🔘 Multiple Choice'}
                  </span>
                  <span className="learn-badge mono">
                    {getTierMeta(currentCard.tier || 1).short}
                  </span>
                  <ActivityPointsBadge
                    tier={currentCard.tier || 1}
                    activity={currentCard.qType === 'typing'
                      ? 'words_typing'
                      : 'words_mc'}
                    failedCount={currentCard.failedCount || 0}
                    exactOrthography={hasExactTrainerOrthography(
                      currentCard,
                      typingInput
                    )}
                  />
                </div>
                {currentCard.failedCount > 0 && (
                  <span className="learn-badge learn-badge-amber mono">
                    🔁 Reappeared (Missed {currentCard.failedCount}x)
                  </span>
                )}
              </div>

              <div className="learn-trainer-prompt serif">{immersionMode ? trainerImmersionCue(currentCard, tokenMap) : currentCard.prompt}</div>
              <div className="learn-trainer-subhint">{immersionMode ? "Use the context to recall the word or form." : currentCard.subHint}</div>

              {currentCard.qType === 'typing' ? (
                <div className="learn-typing-area">
                  <div className="learn-char-bar">
                    <span className="mono" style={{ fontSize: 11, color: 'var(--fg-mute)', alignSelf: 'center' }}>
                      Insert special char:
                    </span>
                    {['\u00cb', '\u00eb', '\u00cbi', '\u00ebi', '-'].map(ch => (
                      <button
                        key={ch}
                        type="button"
                        className="learn-char-btn mono"
                        onClick={() => insertSpecialChar(ch)}
                        disabled={!!cardResult}
                      >
                        {ch}
                      </button>
                    ))}
                  </div>

                  <input
                    ref={typingInputRef}
                    type="text"
                    className="field field-mono learn-trainer-input"
                    placeholder="Type your answer in Seamenese… (e accepted for ë)"
                    value={typingInput}
                    disabled={!!cardResult}
                    onChange={e => setTypingInput(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === 'Enter') {
                        e.preventDefault();
                        if (cardResult) handleAdvanceLearnCard();
                        else handleSubmitTyping();
                      }
                    }}
                  />

                  {!cardResult && (
                    <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 12 }}>
                      <button
                        type="button"
                        className="btn btn-accent"
                        style={{ flex: 2, justifyContent: 'center', padding: '14px 20px', fontSize: 14 }}
                        onClick={handleSubmitTyping}
                      >
                        Check Answer ✓
                      </button>
                      <button
                        type="button"
                        className="btn"
                        style={{ flex: 1, justifyContent: 'center', padding: '14px 16px' }}
                        onClick={() => evaluateTrainerOutcome(false, 'Skipped')}
                      >
                        Don't Know / Show Answer
                      </button>
                    </div>
                  )}
                </div>
              ) : (
                <div className="learn-mc-grid">
                  {currentCardMcOptions.map(opt => {
                    const isSelected = cardResult && cardResult.selectedOptId === opt.optId;
                    const showCorrect = cardResult && opt.correct;
                    const showWrong = isSelected && !opt.correct;
                    return (
                      <button
                        key={opt.optId}
                        type="button"
                        disabled={!!cardResult}
                        className={`learn-reply-btn ${showCorrect ? 'correct-choice' : ''} ${showWrong ? 'wrong-choice' : ''}`}
                        onClick={() => handleSelectLearnMcOption(opt)}
                      >
                        <span className="learn-opt-letter mono">{opt.letter}</span>
                        <span className="learn-opt-text">{immersionMode ? trainerChoiceCue(opt.text, lexicon) : opt.text}</span>
                      </button>
                    );
                  })}
                </div>
              )}

              {cardResult && (
                <div className={`learn-feedback-box ${cardResult.correct ? 'is-correct' : 'is-wrong'}`} style={{ marginTop: 16 }}>
                  <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>
                    {cardResult.correct ? (
                      <>✅ Wo tuk! Correct: <span className="mono">{immersionMode ? trainerChoiceCue(cardResult.displayAnswer, lexicon) : cardResult.displayAnswer}</span></>
                    ) : (
                      <>❌ Not quite ({cardResult.userAnswerText})! Correct Answer: <span className="mono" style={{ textDecoration: 'underline' }}>{immersionMode ? trainerChoiceCue(cardResult.displayAnswer, lexicon) : cardResult.displayAnswer}</span></>
                    )}
                  </div>
                  <div><ImmersionGloss cue="Compare your answer with the Seamenese examples, or inspect a word for more context.">{cardResult.explanation}</ImmersionGloss></div>
                  {cardResult.orthoNote && (
                    <div className="learn-ortho-reminder">{cardResult.orthoNote}</div>
                  )}
                  {!cardResult.correct && (
                    <div className="mono" style={{ marginTop: 8, fontSize: 12, color: '#fbbf24' }}>
                      🔁 Re-queued {cardResult.requeueSteps} cards later until you master it!
                    </div>
                  )}
                </div>
              )}

              {cardResult && (
                <button
                  type="button"
                  className="btn btn-warm learn-next-big-btn"
                  onClick={handleAdvanceLearnCard}
                >
                  Next Question → (Press Enter)
                </button>
              )}
            </div>
          )}
        </section>
      )}

      {/* =====================================================================
          TAB 3: JAI-SEMEN (Tiered Verb Conjugation Dojo + All 20 Official Tables)
         ===================================================================== */}
      {activeTab === 'verbs' && (
        <section className="learn-tab-section">
          <DifficultyTierBar
            selectedTier={selectedTier}
            onSelectTier={handleSelectTier}
            tierCounts={verbTierCounts}
          />

          <div className="learn-section-top">
            <div>
              <h3 className="serif learn-section-h">Verb conjugation drills</h3>
              <p className="learn-section-desc">
                <strong>Tier 1</strong> drills core regular verbs (<em>Xai, Skani, Poni, Kini, Beti, Sarpi</em>), <strong>Tier 2</strong> introduces <code>-ëi</code> verbs &amp; plural pronouns (<em>Ton -u, Na -ami, Lon -ai</em>), and <strong>Tier 3</strong> adds vowel-preserving stems (<em>Sanai → sanaami</em>) &amp; multi-verb chaining!
              </p>
            </div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
              <span className="learn-badge mono" role="status">
                {economy.verbAttempts.length} answered drills in the last 10 minutes
              </span>
              <span className="learn-badge learn-badge-amber mono" style={{ fontSize: 14 }}>
                Dojo Streak: {verbStreak} 🔥
              </span>
            </div>
          </div>

          {economy.verbAttempts.length >= 15 && (
            <div className="learn-feedback-box" style={{ margin: '12px 0' }}>
              <strong>
                🌱 Verb practice rewards:
                {' '}
                {economy.verbAttempts.length >= 30 ? '25%' : '50%'}
                {' '}on your next drill.
              </strong>
              <p style={{ margin: '6px 0 10px' }}>
                Practise Scenes or Word Recall for full points while this rolling window cools down.
              </p>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                <button
                  type="button"
                  className="btn"
                  onClick={() => setActiveTab('scenes')}
                >
                  Practise Scenes
                </button>
                <button
                  type="button"
                  className="btn"
                  onClick={() => setActiveTab('words')}
                >
                  Practise Word Recall
                </button>
              </div>
            </div>
          )}

          {currentDrill && (
            <div className="learn-card learn-trainer-card">
              <div className="learn-card-head">
                <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
                  <span className="learn-badge">
                    {currentDrill.isMultiVerb ? '👑 Multi-Verb Chain Drill' : '⚡ Interactive Jai-Semen Drill'}
                  </span>
                  <span className="learn-badge mono">
                    {getTierMeta(currentDrill.tier || 1).short}
                  </span>
                  <ActivityPointsBadge
                    tier={currentDrill.tier || 1}
                    activity={currentDrill.isMultiVerb
                      ? 'verb_multiverb'
                      : 'verb_single'}
                    verbSessionCount={
                      drillAnswerState?.verbSessionCount
                      || economy.verbAttempts.length + 1
                    }
                  />
                </div>
                <span className="mono" style={{ fontSize: 12, color: 'var(--fg-mute)' }}>
                  4 Fisher–Yates Shuffled Options
                </span>
              </div>

              {currentDrill.isMultiVerb ? (
                <div className="learn-trainer-prompt serif" style={{ margin: '12px 0 16px' }}>
                  {currentDrill.promptText}
                </div>
              ) : (
                <div className="learn-trainer-prompt serif" style={{ margin: '12px 0 16px' }}>
                  Conjugate <span className="mono" style={{ color: 'var(--accent)' }}>{currentDrill.verb.infinitive}</span>{' '}
                  (<ImmersionGloss cue={lexicon.find(e => e.term === currentDrill.verb.infinitive)?.hint || "Use the subject to choose the ending."}>{currentDrill.verb.meaning}</ImmersionGloss>) for{' '}
                  <span className="mono" style={{ color: '#fbbf24' }}>{currentDrill.pron.label}</span>:
                </div>
              )}

              <div className="learn-mc-grid">
                {currentDrill.options.map(opt => {
                  const isChosen = drillAnswerState && drillAnswerState.chosenOptId === opt.optId;
                  const showCorrect = drillAnswerState && opt.correct;
                  const showWrong = isChosen && !opt.correct;
                  return (
                    <button
                      key={opt.optId}
                      type="button"
                      disabled={!!drillAnswerState}
                      className={`learn-reply-btn ${showCorrect ? 'correct-choice' : ''} ${showWrong ? 'wrong-choice' : ''}`}
                      onClick={() => handleAnswerVerbDrill(opt)}
                    >
                      <span className="learn-opt-letter mono">{opt.letter}</span>
                      <span className="learn-opt-text mono">{opt.text}</span>
                    </button>
                  );
                })}
              </div>

              {drillAnswerState && (
                <div className={`learn-feedback-box ${drillAnswerState.correct ? 'is-correct' : 'is-wrong'}`} style={{ marginTop: 16 }}>
                  {currentDrill.isMultiVerb ? (
                    drillAnswerState.correct ? (
                      <>
                        <strong>✅ Wo tuk!</strong> <span className="mono">{currentDrill.correctForm}</span> — {currentDrill.ruleNote}
                      </>
                    ) : (
                      <>
                        <strong>❌ Not quite!</strong> The correct chain is <strong className="mono">{currentDrill.correctForm}</strong>. {currentDrill.ruleNote}
                      </>
                    )
                  ) : drillAnswerState.correct ? (
                    <>
                      <strong>✅ Wo tuk!</strong>{' '}
                      <span className="mono">{currentDrill.pron.pronWord} {currentDrill.correctForm}</span> is the exact conjugation! ({currentDrill.verb.note})
                    </>
                  ) : (
                    <>
                      <strong>❌ Not quite!</strong> For <strong>{currentDrill.pron.label}</strong>, the correct form is{' '}
                      <strong className="mono">{currentDrill.pron.pronWord} {currentDrill.correctForm}</strong>. ({currentDrill.verb.note})
                    </>
                  )}
                </div>
              )}

              <div style={{ display: 'flex', gap: 12, marginTop: 16, flexWrap: 'wrap' }}>
                {drillAnswerState && (
                  <button
                    type="button"
                    className="btn btn-warm"
                    style={{ flex: 2, justifyContent: 'center', padding: '14px 20px', fontSize: 15 }}
                    onClick={() => loadNewVerbDrill(selectedTier)}
                  >
                    Next Verb Drill →
                  </button>
                )}
                <button
                  type="button"
                  className="btn"
                  style={{ flex: 1, justifyContent: 'center', padding: '14px 16px' }}
                  onClick={() => loadNewVerbDrill(selectedTier)}
                >
                  ↻ Skip / Load New Drill
                </button>
              </div>
            </div>
          )}

        </section>
      )}

      {/* =====================================================================
          TAB 4: GRAMMAR, PLURALS, COMPOUNDS & 45 CANONICAL CORPUS
         ===================================================================== */}
      {/* =====================================================================
          TAB 5: SM-2 SRS ACTIVE RECALL DECK (Tiered + Anti-Cheat + Active Recall)
         ===================================================================== */}
      {activeTab === 'srs' && (
        <SrsPracticeTab
          srs={srs}
          selectedTier={selectedTier}
          onSelectTier={handleSelectTier}
          economy={economy}
        />
      )}

      {/* =====================================================================
          TAB 6: INTERACTIVE LEXICON (Live Active Dictionary)
         ===================================================================== */}
      {/* Word Inspector Modal */}
      <WordInspectorModal
        entry={inspectedWord}
        conjugations={conjugations}
        immersionMode={immersionMode}
        onClose={() => setInspectedWord(null)}
        onPeekScheduleSrs={srs.scheduleWord}
      />
    </div>
  );
}

// ============================================================================
// 8. DOPAMINE FLOATING FX OVERLAY (Confetti Burst + Floating Score Popups)
// ============================================================================
function DopamineFxOverlay({ bursts, popups, redFlash }) {
  return (
    <div className="dopamine-fx-layer" aria-hidden="true">
      {redFlash && <div className="dopamine-red-flash" />}
      {bursts.map(b => (
        <div key={b.id} className="dopamine-confetti-wrap">
          {b.particles.map(p => (
            <span
              key={p.id}
              className={`dopamine-confetti-particle ${p.isSupernova ? 'is-supernova' : ''}`}
              style={{
                left: `${p.left}%`,
                '--dx': `${p.dx}px`,
                '--dy': `${p.dy}px`,
                '--rot': `${p.rot}deg`,
                animationDelay: `${p.delay}s`,
              }}
            >
              {p.emoji}
            </span>
          ))}
        </div>
      ))}
      {popups.map(pop => (
        <div
          key={pop.id}
          className={`dopamine-score-popup mono ${pop.kind === 'fail' ? 'is-fail' : 'is-win'}`}
        >
          {pop.text}
        </div>
      ))}
    </div>
  );
}

Object.assign(window, {
  fisherYatesShuffle,
  DIFFICULTY_TIERS,
  getTierMeta,
  SHOP_RARITIES,
  getShopRarityMeta,
  DOPAMINE_SHOP_ITEMS,
  DISAPPOINTED_JORN_ROASTS,
  loadDopamineState,
  saveDopamineState,
  applyDopamineThemeToDom,
  getComboMultiplier,
  getComboLabel,
  getPermanentPointMultiplier,
  getOverdriveCharges,
  getTotalActivePointMultiplier,
  calculateActivityPoints,
  makeActivityEventId,
  ActivityPointsBadge,
  seaAnswerKey,
  getTrainerAcceptedAnswers,
  hasExactTrainerOrthography,
  isValidSrsRecall,
  computeSm2Next,
  getEquippedThemeObj,
  getEquippedTitleObj,
  getEquippedNameplateObj,
  getEquippedFrameObj,
  getEquippedEffectObj,
  getEquippedSoundObj,
  playDopamineSynth,
  cleanQuizMeaning,
  getAcceptedAnswersForEntry,
  DopamineShopModal,
  LeaderboardPanel,
  DifficultyTierBar,
  DopamineFxOverlay,
  LearnEnvironment,
});



