// Brain Ride worlds. Any exercise can appear in any world; a ride is a timed list of segments:
// phase (launch / cruise / recovery / sprint / cooldown — auto-advancing, spoken), encode (memorise for later), burst (an exercise).
const WC = d => `var(--gg-domain-${d})`;
const rnd = n => Math.floor(Math.random() * n);
const pick = a => a[rnd(a.length)];
const shuffle = a => { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = rnd(i + 1); [a[i], a[j]] = [a[j], a[i]]; } return a; };
const targets = (n, rate) => shuffle(Array.from({ length: n }, (_, i) => i < Math.max(1, Math.round(n * rate))));
const fmtT = s => Math.floor(Math.max(0, s) / 60) + ':' + String(Math.max(0, s) % 60).padStart(2, '0');
const IC = (name, size, color, fill) => { const { Icon } = window.GG; return <span style={{ color, display: 'flex' }}><Icon name={name} size={size} fill={fill} /></span>; };
const Txt = (t, size) => <span style={{ fontSize: size, lineHeight: 1.15, letterSpacing: '-0.01em', textWrap: 'balance' }}>{t}</span>;

const ITEMS = [['photo_camera', 'Camera'], ['luggage', 'Suitcase'], ['explore', 'Compass'], ['camping', 'Tent'], ['map', 'Map'], ['flashlight_on', 'Lantern'], ['sailing', 'Sailboat'], ['pedal_bike', 'Bicycle'], ['landscape', 'Mountain'], ['beach_access', 'Umbrella'], ['key', 'Key'], ['anchor', 'Anchor'], ['park', 'Tree'], ['headphones', 'Headphones'], ['watch', 'Watch'], ['local_florist', 'Flower'], ['sunny', 'Sun'], ['umbrella', 'Raincoat']];
function ItemCue({ items, color }) {
  return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14 }}>
    {items.map((it, k) => <React.Fragment key={k}>
      {k > 0 && IC('link', 36, 'var(--text-muted)')}
      <span style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>{IC(it[0], items.length > 1 ? 64 : 92, color, true)}<span style={{ fontSize: 22, fontWeight: 800, letterSpacing: 0 }}>{it[1]}</span></span>
    </React.Fragment>)}
  </div>;
}
const makeMem = () => { const s = shuffle(ITEMS); return { seen: s.slice(0, 4), pairs: [[s[4], s[5]], [s[6], s[7]], [s[8], s[9]]], pool: s.slice(10) }; };

const EX = {
  focus: { name: 'Focus Finder', domain: 'Sustained attention', dk: 'focus', mode: 'tap', yes: 'Tap for 2', windowMs: 2200, rule: 'Tap for 2. Ignore other numbers.',
    gen: n => targets(n, 0.35).map(t => ({ v: t ? 2 : pick([1, 3, 4, 5, 7, 8, 9]), isTrue: t })), cue: t => t.v, say: t => String(t.v), explain: t => t.isTrue ? '2 → tap' : `${t.v} → hold off` },
  pairs: { name: 'Prefrontal Pairs', domain: 'Working memory', dk: 'memory', mode: 'lr', yes: 'Match', no: 'Different', windowMs: 2600, rule: 'Same symbol as the last one?',
    gen: n => { const S = [['change_history', 'Triangle'], ['circle', 'Circle'], ['square', 'Square'], ['star', 'Star']]; const out = []; for (let i = 0; i < n; i++) { const rep = i > 0 && Math.random() < 0.4; const s = rep ? out[i - 1].s : pick(S.filter(x => !out[i - 1] || x !== out[i - 1].s)); out.push({ s, isTrue: rep }); } return out; },
    cue: (t, c) => IC(t.s[0], 120, c, true), say: t => t.s[1], explain: t => t.isTrue ? 'Same as the last one → Match' : 'Not the same → Different' },
  rapid: { name: 'Rapid Response', domain: 'Processing speed', dk: 'speed', mode: 'tap', yes: 'Tap!', windowMs: 1600, rt: true, rule: 'Tap the moment the shape lights up.',
    gen: n => Array.from({ length: n }, () => ({ isTrue: true, lead: 700 + rnd(1500) })), cue: (t, c) => IC('hexagon', 120, c, true), snd: () => [740, 'square', 0.06], explain: () => 'Shape lit up → tap fast' },
  brakes: { name: 'Brain Brakes', domain: 'Inhibitory control', dk: 'flexibility', mode: 'tap', yes: 'Tap for the bell', windowMs: 2400, rule: 'Tap for the bell. Ignore the gong.',
    gen: n => targets(n, 0.6).map(t => ({ isTrue: t, caption: t ? 'Bell' : 'Gong' })), cue: (t, c) => IC(t.isTrue ? 'notifications_active' : 'do_not_disturb_on', 110, c, true), say: t => t.caption, snd: t => t.isTrue ? [880, 'triangle', 0.25] : [260, 'sine', 0.4], explain: t => t.isTrue ? 'Bell → tap' : 'Gong → hold off' },
  descartes: { name: 'Descartes Decisions', domain: 'Deductive reasoning', dk: 'problem', mode: 'lr', yes: 'Right route', no: 'Left route', windowMs: 4500, cueSize: 30, rule: 'Listen to the clue. Pick the route.',
    gen: n => Array.from({ length: n }, () => { const a = pick(['castle', 'river', 'bridge', 'market', 'lighthouse', 'park']); const f = rnd(4); const txt = [`The ${a} is on the left.`, `The ${a} is on the right.`, `The ${a} is not on the left.`, `The ${a} is not on the right.`][f]; return { txt, isTrue: f === 1 || f === 2, caption: `Which way to the ${a}?` }; }),
    cue: t => Txt(t.txt, 30), say: t => t.txt, explain: t => t.isTrue ? 'It must be right → Right route' : 'It must be left → Left route' },
  riddles: { name: 'Reasoning Riddles', domain: 'Verbal reasoning', dk: 'problem', mode: 'lr', yes: 'Right', no: 'Left', windowMs: 6000, cueSize: 26, rule: 'Hear the riddle. Tap the answer.',
    gen: n => { const R = [['I have keys but no locks. What am I?', 'Piano', 'Map'], ["What has hands but can't clap?", 'Clock', 'Glove'], ['What gets wetter the more it dries?', 'Towel', 'Sponge'], ['What has a neck but no head?', 'Bottle', 'Scarf'], ["What can you catch but not throw?", 'A cold', 'A ball'], ["What has teeth but can't bite?", 'Comb', 'Shark'], ['What goes up but never comes down?', 'Your age', 'A kite']]; return shuffle(R).concat(shuffle(R)).slice(0, n).map(([q, a, b]) => { const r = Math.random() < 0.5; return { q, ans: a, isTrue: r, yes: r ? a : b, no: r ? b : a }; }); },
    cue: t => Txt(t.q, 26), say: t => t.q, explain: t => `Answer: ${t.ans}` },
  central: { name: 'Central Sound', domain: 'Auditory attention', dk: 'focus', mode: 'tap', yes: 'Tap for the high ping', windowMs: 2000, rule: 'Tap for the high ping only.',
    gen: n => targets(n, 0.25).map(t => ({ isTrue: t, caption: 'Listen…' })), cue: (t, c) => IC('graphic_eq', 110, c), snd: t => t.isTrue ? [1320, 'sine', 0.22] : [520, 'sine', 0.22], explain: t => t.isTrue ? 'High ping → tap' : 'Low ping → hold off' },
  words: { name: "Wernicke's Words", domain: 'Semantic processing', dk: 'problem', mode: 'lr', yes: 'Vehicle', no: 'Animal', windowMs: 2400, cueSize: 54, rule: 'Animal or vehicle?',
    gen: n => targets(n, 0.5).map(t => ({ w: pick(t ? ['Bus', 'Tractor', 'Scooter', 'Train', 'Ferry', 'Truck', 'Tram'] : ['Elephant', 'Tiger', 'Rabbit', 'Eagle', 'Dolphin', 'Horse', 'Penguin']), isTrue: t })), cue: t => t.w, say: t => t.w, explain: t => `${t.w} → ${t.isTrue ? 'Vehicle' : 'Animal'}` },
  odd: { name: 'Odd Outlier', domain: 'Cognitive flexibility', dk: 'flexibility', mode: 'lr', yes: 'Odd one out', no: 'All belong', windowMs: 3600, cueSize: 30, rule: 'Do all three belong together?',
    gen: n => { const C = { Fruit: ['Apple', 'Pear', 'Plum', 'Mango', 'Peach'], Tools: ['Hammer', 'Saw', 'Drill', 'Wrench', 'Chisel'], Colours: ['Red', 'Blue', 'Green', 'Yellow', 'Purple'], 'Sea life': ['Shark', 'Whale', 'Crab', 'Octopus', 'Squid'] }; const K = Object.keys(C);
      return targets(n, 0.5).map(t => { const k = pick(K); const w = shuffle(C[k]).slice(0, 3); let odd = null; if (t) { odd = pick(C[pick(K.filter(x => x !== k))]); w[rnd(3)] = odd; } return { w, odd, isTrue: t }; }); },
    cue: t => <span style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 30 }}>{t.w.map(x => <span key={x}>{x}</span>)}</span>, say: t => t.w.join(', '), explain: t => t.isTrue ? `${t.odd} doesn't belong → Odd one out` : 'All one group → All belong' },
  recall: { name: 'Recall Repeats', domain: 'Working memory (1-back)', dk: 'memory', mode: 'lr', yes: 'Repeat', no: 'New', windowMs: 2600, rule: 'Same number as the last one?',
    gen: n => { const o = []; for (let i = 0; i < n; i++) { const rep = i > 0 && Math.random() < 0.4; o.push({ v: rep ? o[i - 1].v : pick([1, 2, 3, 4, 5, 6, 7, 8, 9].filter(x => !o[i - 1] || x !== o[i - 1].v)), isTrue: rep }); } return o; },
    cue: t => t.v, say: t => String(t.v), explain: t => t.isTrue ? 'Same as last → Repeat' : 'Different → New' },
  travel: { name: 'Parahippocampal Pairs', domain: 'Associative memory', dk: 'memory', mode: 'lr', yes: 'Paired', no: 'Not paired', windowMs: 3400, rule: 'Were these two shown as a pair?',
    gen: (n, m) => targets(n, 0.5).map(t => { const i = rnd(3); const p = m.pairs[i]; const q = m.pairs[(i + 1 + rnd(2)) % 3]; return { items: t ? p : [p[0], q[1]], isTrue: t }; }),
    cue: (t, c) => <ItemCue items={t.items} color={c} />, say: t => `${t.items[0][1]} and ${t.items[1][1]}`, explain: t => t.isTrue ? 'They were a pair → Paired' : 'Different pairs → Not paired' },
  nav: { name: 'Neural Navigator', domain: 'Spatial memory', dk: 'memory', mode: 'lr', yes: 'Correct', no: 'Incorrect', windowMs: 3200, rule: 'Follow 3 moves. Is this where it ended?' },
  seen: { name: 'Memory Match', domain: 'Recognition memory', dk: 'memory', mode: 'lr', yes: 'Seen it', no: 'New', windowMs: 3000, rule: 'Did you see this earlier?',
    gen: (n, m) => { const s = shuffle(m.seen), p = shuffle(m.pool); return targets(n, 0.5).map((t, i) => ({ items: [t ? s[i % s.length] : p[i % p.length]], isTrue: t })); },
    cue: (t, c) => <ItemCue items={t.items} color={c} />, say: t => t.items[0][1], explain: t => t.isTrue ? 'Shown at the start → Seen it' : 'Not shown before → New' },
  galileo: { name: "Galileo's Clocks", domain: 'Visuospatial working memory', dk: 'memory', mode: 'lr', yes: 'Match', no: 'Different', windowMs: 4500, cueSize: 64, rule: 'Forward 1, Right 2, Back 3. Do they match?',
    gen: n => { const M = { Forward: 1, Right: 2, Backward: 3 }, D = Object.keys(M); return targets(n, 0.5).map(t => { const seq = [pick(D), pick(D), pick(D)]; const c = seq.map(d => M[d]); if (!t) { const i = rnd(3); c[i] = pick([1, 2, 3].filter(x => x !== c[i])); } return { big: c.join(' '), caption: seq.join(' → '), isTrue: t }; }); },
    cue: t => t.big, say: t => t.caption.replace(/→/g, ',') + '. ' + t.big, explain: t => t.isTrue ? 'Numbers match → Match' : 'One number is off → Different' },
  calc: { name: 'Cognitive Calculations', domain: 'Arithmetic', dk: 'speed', mode: 'lr', yes: 'True', no: 'False', windowMs: 3400, cueSize: 60, meter: true, rule: 'Is the sum right?',
    gen: n => targets(n, 0.5).map(t => { const a = 2 + rnd(8), b = 2 + rnd(8); const r = t ? a + b : a + b + pick([-2, -1, 1, 2]); return { txt: `${a} + ${b} = ${r}`, a, b, r, isTrue: t }; }),
    cue: t => t.txt, say: t => `${t.a} plus ${t.b} equals ${t.r}`, explain: t => t.isTrue ? 'The sum is right → True' : 'The sum is off → False' },
  parietal: { name: 'Parietal Processes', domain: 'Rapid classification', dk: 'speed', mode: 'lr', yes: 'Odd · C', no: 'Even · V', windowMs: 1900, rule: 'C is odd. V is even.',
    gen: n => targets(n, 0.5).map(t => ({ v: t ? 'C' : 'V', isTrue: t })), cue: t => t.v, say: t => t.v, explain: t => t.isTrue ? 'C → Odd' : 'V → Even' },
  bias: { name: 'Bias Buster', domain: 'Selective attention', dk: 'focus', mode: 'tap', yes: 'Tap for DOG', windowMs: 1900, cueSize: 80, rule: 'Tap for DOG only.',
    gen: n => targets(n, 0.4).map(t => ({ w: t ? 'DOG' : pick(['GOD', 'DIG', 'FOG', 'DOT', 'BOG', 'COG']), isTrue: t })), cue: t => t.w, say: t => t.w.toLowerCase(), explain: t => t.isTrue ? 'DOG → tap' : `${t.w} → hold off` },
  mood: { name: 'Mood Matcher', domain: 'Emotion recognition', dk: 'composure', mode: 'lr', yes: 'Happy', no: 'Not happy', windowMs: 2600, rule: 'Is this face happy?',
    gen: n => targets(n, 0.45).map(t => ({ f: t ? ['sentiment_very_satisfied', 'Happy'] : pick([['sentiment_dissatisfied', 'Sad'], ['sentiment_extremely_dissatisfied', 'Angry'], ['sentiment_neutral', 'Neutral']]), isTrue: t })),
    cue: t => IC(t.f[0], 130, '#fff', true), explain: t => `${t.f[1]} → ${t.isTrue ? 'Happy' : 'Not happy'}` },
};
Object.entries(EX).forEach(([k, e]) => { e.key = k; e.color = WC(e.dk); });

const PH = {
  launch: { badge: 'Launch', title: 'Settle into a comfortable pace', body: 'Easy pedaling while you learn today’s flight instructions.', pace: 'comfortable', spoken: 'Launch. Settle into a comfortable pace.' },
  cruise: { badge: 'Cruise', title: 'Mostly riding…', body: 'Hold your pace. Next Brain Burst incoming.', pace: 'steady', spoken: 'Cruise. Hold your pace.' },
  recovery: { badge: 'Recovery', title: 'Ease your pace', pace: 'easy', spoken: 'Recover. Return to your comfortable pace.' },
  sprint: { badge: 'Brain Sprint', title: 'Increase your cadence!', body: 'The next burst runs at sprint pace. Push until it ends.', pace: 'push', spoken: 'Final Brain Sprint. Increase your cadence.' },
  cooldown: { badge: 'Cool Down', title: 'Pedal easy', body: 'Brain work is done. Let your heart rate settle.', pace: 'easy', spoken: 'Cool down. Pedal easy and let your heart rate settle.' },
};
const P = (kind, secs, o = {}) => ({ type: 'phase', kind, secs, ...PH[kind], ...o });
const B = (ex, title, sprint = false) => ({ type: 'burst', ex, title, sprint });

const WORLDS = {
  space: { id: 'space', name: 'Space Mission', icon: 'rocket_launch', color: WC('focus'), finale: 'You landed on Mars.', again: 'Fly Again',
    blurb: 'Navigate signals, asteroid fields and hyperdrive on the way to Mars.',
    bg: 'linear-gradient(180deg, rgba(7,7,10,0.35) 0%, rgba(7,7,10,0.55) 100%), radial-gradient(circle at 82% 16%, #f3c27a 0, #c9783e 5%, #6b2f1c 9%, transparent 9.4%), radial-gradient(ellipse 22% 3% at 82% 16%, transparent 60%, rgba(243,194,122,0.5) 62%, transparent 70%), radial-gradient(circle at 12% 88%, #2b5fd6 0, #142e78 18%, #071233 30%, transparent 30.5%), radial-gradient(1px 1px at 8% 12%, #fff, transparent), radial-gradient(1px 1px at 22% 38%, #cfe9ff, transparent), radial-gradient(1.5px 1.5px at 41% 18%, #fff, transparent), radial-gradient(1px 1px at 63% 9%, #fff, transparent), radial-gradient(1px 1px at 70% 40%, #cfe9ff, transparent), radial-gradient(1.5px 1.5px at 92% 34%, #fff, transparent), radial-gradient(1px 1px at 34% 62%, #fff, transparent), radial-gradient(1px 1px at 56% 52%, #fff, transparent), radial-gradient(1px 1px at 84% 66%, #cfe9ff, transparent), radial-gradient(1.5px 1.5px at 48% 80%, #fff, transparent), radial-gradient(1px 1px at 74% 88%, #fff, transparent), linear-gradient(180deg, #05060f 0%, #0a0820 60%, #07070a 100%)',
    segs: [P('launch', 180, { body: 'Easy pedaling. Mission Control is about to send your first signal.', spoken: 'Launch. Settle into a comfortable pace. Prepare to navigate.' }), B('focus', 'Signal Hunt'), P('recovery', 45), B('brakes', 'Asteroid Field'), P('recovery', 45), B('nav', 'Neural Navigator'), P('cruise', 120), B('galileo', 'Mission Control'), P('sprint', 20), B('parietal', 'Hyperdrive', true), P('cooldown', 60)] },
  city: { id: 'city', name: 'City Challenge', icon: 'location_city', color: WC('problem'), finale: 'You made it downtown.', again: 'Ride Again',
    blurb: 'Read the signs, pick your routes and lock onto targets through the city at night.',
    bg: 'linear-gradient(180deg, rgba(7,7,10,0.35) 0%, rgba(7,7,10,0.55) 100%), repeating-linear-gradient(0deg, transparent 0 9px, rgba(255,210,122,0.18) 9px 12px) 0 100%/100% 34% no-repeat, linear-gradient(#0d0a1a,#0d0a1a) 2% 100%/12% 30% no-repeat, linear-gradient(#120d22,#120d22) 13% 100%/10% 46% no-repeat, linear-gradient(#0d0a1a,#0d0a1a) 24% 100%/14% 36% no-repeat, linear-gradient(#150f28,#150f28) 39% 100%/9% 58% no-repeat, linear-gradient(#0d0a1a,#0d0a1a) 50% 100%/13% 40% no-repeat, linear-gradient(#120d22,#120d22) 64% 100%/11% 52% no-repeat, linear-gradient(#0d0a1a,#0d0a1a) 77% 100%/12% 34% no-repeat, linear-gradient(#150f28,#150f28) 92% 100%/12% 48% no-repeat, radial-gradient(ellipse 80% 30% at 50% 70%, rgba(255,120,200,0.25), transparent 70%), linear-gradient(180deg, #1a0d33 0%, #3a1650 45%, #5c1e4a 62%, #0b0712 100%)',
    segs: [P('launch', 120, { body: 'Rush hour is clearing. Find your rhythm through the streets.', spoken: 'Launch. Find your rhythm through the city.' }), B('pairs', 'Match Lane'), P('recovery', 45), B('words', 'Word Rides'), P('recovery', 45), B('descartes', 'Escape Route'), P('cruise', 90), B('mood', 'Read the Room'), P('sprint', 20, { title: 'Sprint the last block!' }), B('bias', 'Target Lock', true), P('cooldown', 60)] },
  expedition: { id: 'expedition', name: 'Expedition', icon: 'hiking', color: WC('composure'), finale: 'You reached the summit.', again: 'Ride Again',
    blurb: 'Spot landmarks on the trail, then prove you remember them at the summit.',
    bg: 'linear-gradient(180deg, rgba(7,7,10,0.35) 0%, rgba(7,7,10,0.55) 100%), conic-gradient(from 140deg at 70% 52%, transparent 0deg, #0c2a20 0deg 80deg, transparent 80deg), conic-gradient(from 135deg at 28% 44%, transparent 0deg, #10382a 0deg 90deg, transparent 90deg), conic-gradient(from 135deg at 28% 44%, transparent 0deg, rgba(255,255,255,0.75) 0deg 90deg, transparent 90deg) 0 0/100% 50% no-repeat, conic-gradient(from 145deg at 52% 58%, transparent 0deg, #082019 0deg 70deg, transparent 70deg), radial-gradient(circle at 80% 22%, #ffe2a8 0, #ffb45a 4%, transparent 9%), linear-gradient(180deg, #ff9a5a 0%, #c95a6a 28%, #3a2a5a 50%, #0a1a14 70%, #07070a 100%)',
    segs: [P('launch', 90, { badge: 'Trailhead', body: 'Easy pedaling. Look out for landmarks — you’ll need them later.', spoken: 'Trailhead. Settle in, and look out for landmarks.' }), { type: 'encode' }, P('cruise', 60), B('central', 'Signal Scanner'), P('recovery', 45), B('riddles', 'Brain Quest'), P('recovery', 45), B('odd', 'Spot the Impostor'), P('cruise', 90), B('seen', 'Seen It?'), P('sprint', 20, { title: 'Final push uphill!' }), B('travel', 'Travel Pairs', true), P('cooldown', 60)] },
  track: { id: 'track', name: 'Speed Race', icon: 'sports_score', color: WC('speed'), finale: 'You crossed the finish line.', again: 'Race Again',
    blurb: 'Short, fast and competitive. Charge the power meter, then sprint for the line.',
    bg: 'linear-gradient(180deg, rgba(7,7,10,0.35) 0%, rgba(7,7,10,0.55) 100%), conic-gradient(from 155deg at 50% 42%, transparent 0deg, #e9e9ee 0deg 2deg, #2a2d36 2deg 48deg, #e9e9ee 48deg 50deg, transparent 50deg), conic-gradient(from 151deg at 50% 42%, transparent 0deg, #ff375f 0deg 4deg, transparent 4deg 54deg, #ff375f 54deg 58deg, transparent 58deg), repeating-linear-gradient(90deg, rgba(255,255,255,0.08) 0 2px, transparent 2px 40px) 0 0/100% 42% no-repeat, radial-gradient(ellipse 120% 40% at 50% 42%, rgba(255,55,95,0.35), transparent 60%), linear-gradient(180deg, #0a0c18 0%, #1a1030 40%, #121318 42%, #07070a 100%)',
    segs: [P('launch', 90, { badge: 'Warm-up Lap', body: 'Build your pace. Lights go green soon.', spoken: 'Warm-up lap. Build your pace.' }), B('recall', 'Echo Ride'), P('recovery', 45), B('calc', 'Power Meter'), P('sprint', 20), B('rapid', 'Lightning Sprint', true), P('recovery', 45), P('sprint', 20, { title: 'Final push!' }), B('brakes', 'Brake or Boost', true), P('cooldown', 60)] },
};
const WORLD_ORDER = ['space', 'city', 'expedition', 'track'];
const rideLen = (w, full) => w.segs.reduce((a, s) => a + (s.type === 'phase' ? (full ? s.secs : Math.max(6, Math.round(s.secs / 10))) : s.type === 'encode' ? 20 : (full ? 20 : 6) * 3.4), 0);
const burstCount = w => w.segs.filter(s => s.type === 'burst').length;

function WorldFrame({ w, sprint, portrait, children }) {
  const hc = GP().highContrast;
  return <Shell portrait={portrait} bg="var(--gg-ink-1000)" layer={<>
    <div style={{ position: 'absolute', inset: 0, background: w.bg, pointerEvents: 'none' }}></div>
    {sprint && !hc && <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', animation: 'gg-sprint 1s ease-in-out infinite' }}></div>}
  </>}>{children}</Shell>;
}
function BurstRail({ done, total, color }) {
  return <div style={{ display: 'flex', gap: 5 }}>{Array.from({ length: total }, (_, k) => <div key={k} style={{ flex: 1, height: 5, borderRadius: 3, background: k < done ? color : 'var(--surface-track)', boxShadow: k < done ? '0 0 6px ' + color : 'none' }}></div>)}</div>;
}
function PacePill({ pace, sprint }) {
  return <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 14px', borderRadius: 9999, background: 'var(--surface-track)', border: '1px solid ' + (sprint ? 'color-mix(in srgb, var(--gg-domain-speed) 45%, transparent)' : 'var(--gg-line)'), fontSize: GP().largeText ? 18 : 16, fontWeight: 800, color: '#fff' }}>
    <span className="material-symbols-outlined" style={{ fontSize: 20, color: sprint ? 'var(--gg-domain-speed)' : 'var(--gg-cyan)' }}>directions_bike</span>Pace: {pace}</span>;
}
function SkipLink({ onClick }) { return <QuietLink onClick={onClick}>Skip ››</QuietLink>; }

function PhaseSeg({ w, seg, secs, nextTitle, last, done, total, voice, onNext, onExit }) {
  const { ActivityRings } = window.GG;
  const { speak, tone, haptic } = window.GGData;
  const L = GP().landscape; const lg = GP().largeText;
  const [ms, setMs] = React.useState(secs * 1000);
  const t = Math.ceil(ms / 1000);
  const fired = React.useRef(false);
  const sprint = seg.kind === 'sprint';
  const c = sprint ? WC('speed') : w.color;
  const ann = Math.min(10, Math.max(3, Math.floor(secs / 2)));
  const next = () => { if (fired.current) return; fired.current = true; onNext(); };
  React.useEffect(() => { speak(seg.spoken, voice); haptic(sprint ? 'go' : 'count'); if (sprint) tone(720, 'triangle', 0.3); const t0 = performance.now(); let raf; const loop = now => { const left = Math.max(0, secs * 1000 - (now - t0)); setMs(left); if (left > 0) raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, []);
  React.useEffect(() => { if (t === ann && nextTitle && secs > 6) { speak(`Brain Burst in ${ann} seconds.`, voice); tone(660, 'sine', 0.12); } if (t <= 0) next(); }, [t]);
  const ring = <ActivityRings size={L ? 150 : 190} stroke={L ? 10 : 12} rings={[{ value: ms / (secs * 1000), color: c }]}><span style={{ fontSize: L ? 34 : 44, fontWeight: 800, letterSpacing: '-0.03em', fontVariantNumeric: 'tabular-nums' }}>{fmtT(t)}</span></ActivityRings>;
  const text = <div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: L ? 'flex-start' : 'center', textAlign: L ? 'left' : 'center', minWidth: 0 }}>
    <Meta color={c}>{seg.badge}</Meta>
    <h1 style={{ margin: 0, fontSize: L ? 30 : 34, lineHeight: 1.08, fontWeight: 800, letterSpacing: '-0.025em', textWrap: 'balance' }}>{seg.title}</h1>
    {nextTitle && <span style={{ fontSize: lg ? 19 : 17, fontWeight: 700, color: 'var(--text-muted)' }}>Up next: <b style={{ color: '#fff' }}>{nextTitle}</b></span>}
  </div>;
  return (
    <WorldFrame w={w} sprint={sprint}>
      <div style={{ display: 'grid', gridTemplateColumns: '56px 1fr 56px', alignItems: 'center', gap: 8, padding: '6px 0 8px' }}>
        <ExitButton onClick={() => { window.GGData.hush(); onExit(); }} />
        <div style={{ textAlign: 'center' }}><Meta>{w.name}</Meta></div><span></span>
      </div>
      <BurstRail done={done} total={total} color={w.color} />
      <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: L ? 'row' : 'column', alignItems: 'center', justifyContent: 'center', gap: L ? 36 : 24, animation: 'gg-fadein .35s ease' }}>{ring}{text}</div>
      <div style={{ display: 'flex', justifyContent: 'center' }}><SkipLink onClick={next} /></div>
    </WorldFrame>
  );
}

function EncodeSeg({ w, mem, voice, onNext, onExit, done, total }) {
  const { speak, tone } = window.GGData;
  const list = [...mem.seen.map(it => [it]), ...mem.pairs];
  const [k, setK] = React.useState(0);
  const cur = list[Math.min(k, list.length - 1)];
  React.useEffect(() => {
    if (k >= list.length) { onNext(); return; }
    speak((cur.length > 1 ? 'Remember this pair: ' + cur[0][1] + ' and ' + cur[1][1] : 'Remember: ' + cur[0][1]), voice); tone(600, 'sine', 0.1);
    const id = setTimeout(() => setK(x => x + 1), 2600); return () => clearTimeout(id);
  }, [k]);
  return (
    <WorldFrame w={w}>
      <div style={{ display: 'grid', gridTemplateColumns: '56px 1fr 56px', alignItems: 'center', gap: 8, padding: '6px 0 8px' }}>
        <ExitButton onClick={() => { window.GGData.hush(); onExit(); }} /><div style={{ textAlign: 'center' }}><Meta>{w.name}</Meta></div><span></span>
      </div>
      <BurstRail done={done} total={total} color={w.color} />
      <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 18, textAlign: 'center' }}>
        <Meta color={w.color}>Landmark {Math.min(k + 1, list.length)} of {list.length}</Meta>
        <h1 style={{ margin: 0, fontSize: 30, lineHeight: 1.1, fontWeight: 800, letterSpacing: '-0.025em' }}>{cur.length > 1 ? 'Remember this pair' : 'Remember this'}</h1>
        <div key={k} style={{ animation: 'gg-fadein .3s ease' }}><ItemCue items={cur} color={w.color} /></div>
        <p style={{ margin: 0, fontSize: 18, color: 'var(--text-secondary)', fontWeight: 500 }}>You’ll be asked about these near the summit.</p>
        <PacePill pace="comfortable" />
      </div>
      <div style={{ display: 'flex', justifyContent: 'center' }}><SkipLink onClick={onNext} /></div>
    </WorldFrame>
  );
}

function BurstPlay({ w, seg, ex, n, mem, voice, onExit, onDone, fromDemo }) {
  const { speak, tone } = window.GGData;
  const color = ex.color;
  const sprint = !!seg.sprint;
  const trials = React.useMemo(() => ex.gen(n, mem), []);
  const T = useTrials({ countdown: !fromDemo, count: trials.length, baseMs: ex.windowMs * (sprint ? 0.85 : 1), tapMode: ex.mode === 'tap', showRT: !!ex.rt, isTarget: i => trials[i].isTrue, labels: { yes: ex.yes, no: ex.no },
    leadMs: ex.rt ? i => trials[i].lead : undefined, onDone: res => onDone(res, sprint),
    onCue: i => { const t = trials[i]; ex.say && speak(ex.say(t), voice, sprint ? 1.12 : 1); if (!ex.rt) { const s = ex.snd && ex.snd(t); s ? tone(...s) : tone(sprint ? 640 : 560, 'sine', 0.08); } } });
  const t = trials[T.i];
  React.useEffect(() => { if (ex.rt && T.ready) tone(...ex.snd(t)); }, [T.ready]);
  const cue = ex.rt && !T.ready ? <span style={{ fontSize: 28, color: 'var(--text-secondary)', letterSpacing: 0 }}>Wait for it…</span> : ex.cue(t, color);
  const meter = ex.meter && <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
    <span className="material-symbols-outlined" style={{ fontSize: 22, color: 'var(--gg-domain-memory)', fontVariationSettings: '"FILL" 1' }}>bolt</span>
    <div style={{ flex: 1, height: 12, borderRadius: 9999, background: 'var(--surface-track)', overflow: 'hidden' }}><div style={{ width: ((T.score || 0) / trials.length * 100) + '%', height: '100%', background: 'linear-gradient(90deg, var(--gg-domain-memory), var(--gg-domain-composure))', transition: 'width 400ms var(--ease)' }}></div></div>
    <Meta>Power</Meta></div>;
  return (
    <WorldFrame w={w} sprint={sprint}>
      <GameFrame T={T} color={color} title={seg.title} rule={ex.rule} onEnd={onExit} right={sprint ? <Meta color="var(--gg-domain-speed)">Sprint</Meta> : null}>
        <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
          {meter}
          <Stage mode={ex.mode} color={color} cue={cue} cueSize={t.cueSize || ex.cueSize || 140} caption={t.caption} no={t.no || ex.no} yes={t.yes || ex.yes} onAnswer={T.answer} fb={T.fb} running={T.running && !T.fb} windowMs={T.windowMs} runKey={T.i} />
        </div>
      </GameFrame>
    </WorldFrame>
  );
}

function navTrials(n) {
  const DIRS = { Up: [0, -1], Down: [0, 1], Left: [-1, 0], Right: [1, 0] }, K = Object.keys(DIRS), cl = v => Math.max(0, Math.min(2, v));
  return targets(n, 0.5).map(ok => { let p = [1, 1]; const moves = []; for (let i = 0; i < 3; i++) { const m = pick(K); moves.push(m); p = [cl(p[0] + DIRS[m][0]), cl(p[1] + DIRS[m][1])]; }
    let claim = p.slice(); if (!ok) { do { claim = [rnd(3), rnd(3)]; } while (claim[0] === p[0] && claim[1] === p[1]); } return { moves, claim, isTrue: ok }; });
}
function NavGrid({ cell, color }) {
  const s = GP().landscape ? 36 : 46;
  return <div style={{ display: 'grid', gridTemplateColumns: `repeat(3, ${s}px)`, gap: 7 }}>{[0, 1, 2].flatMap(r => [0, 1, 2].map(c => { const on = cell[0] === c && cell[1] === r;
    return <div key={r + '-' + c} style={{ width: s, height: s, borderRadius: 12, background: on ? color : 'var(--surface-card)', border: '2px solid ' + (on ? color : 'var(--gg-line-strong)'), boxShadow: on ? '0 0 16px color-mix(in srgb, ' + color + ' 55%, transparent)' : 'none', transition: 'all 200ms' }}></div>; }))}</div>;
}
function NavCue({ status, cell, color }) {
  return <div style={{ display: 'flex', flexDirection: GP().landscape ? 'row' : 'column', alignItems: 'center', gap: 12 }}>
    <div style={{ fontSize: 20, fontWeight: 800, letterSpacing: 0, lineHeight: 1.2, color: '#fff' }}>{status}</div><NavGrid cell={cell} color={color} /></div>;
}
function NeuralNav({ w, seg, n, voice, onExit, onDone, fromDemo }) {
  const { speak, tone } = window.GGData;
  const ex = EX.nav; const color = ex.color;
  const trials = React.useMemo(() => navTrials(n), []);
  const [cell, setCell] = React.useState([1, 1]);
  const [status, setStatus] = React.useState('');
  const [claim, setClaim] = React.useState(false);
  const T = useTrials({ countdown: !fromDemo, count: trials.length, baseMs: ex.windowMs, isTarget: i => trials[i].isTrue, labels: { yes: ex.yes, no: ex.no }, onDone: res => onDone(res, !!seg.sprint),
    leadMs: i => 500 + trials[i].moves.length * 1000 + 500,
    onCue: i => { const t = trials[i]; const ts = []; setCell([1, 1]); setClaim(false); setStatus('Track the moves…'); speak('Track the moves.', voice);
      t.moves.forEach((m, k) => ts.push(setTimeout(() => { setStatus(m); speak(m, voice); tone(500, 'triangle', 0.1); }, 500 + k * 1000)));
      ts.push(setTimeout(() => { setCell(t.claim); setClaim(true); setStatus('Where it ended?'); speak('Is this where it ended?', voice); }, 500 + t.moves.length * 1000 + 500));
      return () => ts.forEach(clearTimeout); } });
  return (
    <WorldFrame w={w}>
      <GameFrame T={T} color={color} title={seg.title} rule={ex.rule} onEnd={onExit}>
        <TrialStage color={color} forceVisual cue={<NavCue status={status} cell={cell} color={claim ? color : 'var(--gg-cyan)'} />} cueSize={20} no={ex.no} yes={ex.yes} onAnswer={T.answer} fb={T.fb} running={T.running && !T.fb} windowMs={T.windowMs} runKey={T.i} />
      </GameFrame>
    </WorldFrame>
  );
}

function demoFor(ex, mem) {
  if (ex.key === 'nav') return navTrials(3).map(t => ({ cue: <NavCue status={'Moves: ' + t.moves.join(' → ')} cell={t.claim} color={ex.color} />, cueSize: 20, forceVisual: true, spoken: t.moves.join('. '), isTrue: t.isTrue, explain: (t.isTrue ? 'Position is right' : 'Position is wrong') + ' → ' + (t.isTrue ? 'Correct' : 'Incorrect') }));
  return ex.gen(3, mem).map(t => ({ cue: ex.cue(t, ex.color), cueSize: t.cueSize || ex.cueSize, caption: t.caption, spoken: ex.say && ex.say(t), tone: ex.snd && ex.snd(t), isTrue: t.isTrue, yes: t.yes, no: t.no, explain: ex.explain(t) }));
}
function DemoGate({ w, ex, mem, onBack, children }) {
  const [go, setGo] = React.useState(!!GP().skipRobot);
  const [fromDemo, setFromDemo] = React.useState(false);
  const Frame = React.useMemo(() => ({ children: c }) => <WorldFrame w={w}>{c}</WorldFrame>, [w.id]);
  const trials = React.useMemo(() => demoFor(ex, mem), []);
  return go ? (fromDemo ? React.cloneElement(children, { fromDemo: true }) : children) : <PracticeDemo Frame={Frame} mode={ex.mode} title={ex.name} color={ex.color} rule={ex.rule} no={ex.no} yes={ex.yes} trials={trials} onBack={onBack} onPlay={() => { setFromDemo(true); setGo(true); }} />;
}

function WorldIntro({ w, full, setFull, onStart, onExit, onWorld }) {
  const { Button, Icon, Segmented } = window.GG;
  return (
    <WorldFrame w={w} portrait>
      <div style={{ display: 'grid', gridTemplateColumns: '56px 1fr 56px', alignItems: 'center', gap: 8, padding: '6px 0' }}>
        <ExitButton onClick={onExit} /><div style={{ textAlign: 'center' }}><Meta color={w.color}>Brain Ride</Meta></div><span></span>
      </div>
      <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center', gap: 12 }}>
        <span style={{ color: w.color, display: 'flex' }}><Icon name={w.icon} size={64} /></span>
        <h1 style={{ margin: 0, fontSize: 42, lineHeight: 1.06, fontWeight: 800, letterSpacing: '-0.025em' }}>{w.name}</h1>
        <p style={{ margin: 0, fontSize: 20, fontWeight: 700, color: 'var(--text-secondary)' }}>About {Math.max(1, Math.round(rideLen(w, true) / 60))} minutes</p>
      </div>
      <div style={{ paddingTop: 12, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
        <Button icon={w.icon} onClick={onStart}>Start Ride</Button>
      </div>
    </WorldFrame>
  );
}

function WorldComplete({ w, r, onNextWorld, onHome, onReport }) {
  const nx = WORLDS[WORLD_ORDER[(WORLD_ORDER.indexOf(w.id) + 1) % WORLD_ORDER.length]];
  const { ActivityRings, Button, Icon } = window.GG;
  const { speak, tone } = window.GGData;
  const acc = r.total ? Math.round((r.correct / r.total) * 100) : 0;
  const sAcc = r.sTotal ? Math.round((r.sCorrect / r.sTotal) * 100) : 0;
  const sec = Math.max(1, Math.round(((r.end || Date.now()) - r.start) / 1000));
  const summary = `${w.name} complete. ${w.finale} ${r.bursts} brain bursts with ${acc} percent accuracy. Longest streak ${r.best}. Brain sprint accuracy ${sAcc} percent.`;
  React.useEffect(() => { onReport && onReport({ acc, best: r.best, sAcc, world: w.id }); const id = setTimeout(() => { speak(summary, r.voice, 1.03); tone(660, 'sine', 0.2); setTimeout(() => tone(880, 'sine', 0.25), 180); }, 300); return () => clearTimeout(id); }, []);
  const Stat = ({ label, value, color = '#fff' }) => <div style={{ borderRadius: 'var(--radius-card)', padding: 14, background: 'var(--surface-card)', border: '1px solid var(--gg-line)' }}><Meta>{label}</Meta><div style={{ fontSize: 32, fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1, marginTop: 6, color, fontVariantNumeric: 'tabular-nums' }}>{value}</div></div>;
  return (
    <WorldFrame w={w} portrait>
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', scrollbarWidth: 'none', display: 'flex', flexDirection: 'column' }}>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', paddingTop: 18 }}>
          <ActivityRings size={128} rings={[{ value: acc / 100, color: 'var(--gg-cyan)' }, { value: Math.min(1, r.best / 5), color: 'var(--gg-domain-memory)' }, { value: sAcc / 100, color: 'var(--gg-domain-speed)' }]}><span style={{ color: '#fff', display: 'flex' }}><Icon name={w.icon} size={34} /></span></ActivityRings>
          <h1 style={{ margin: '14px 0 0', fontSize: 30, fontWeight: 800, letterSpacing: '-0.025em', lineHeight: 1.08 }}>{w.name} <span style={{ color: w.color }}>complete</span></h1>
          <p style={{ margin: '6px 0 0', fontSize: 17, fontWeight: 500, color: 'var(--text-secondary)' }}>{w.finale}</p>
        </div>
        <div style={{ marginTop: 18, textAlign: 'center' }}><Meta>Accuracy</Meta><div style={{ fontSize: 72, fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 1, marginTop: 4, color: 'var(--gg-cyan)', fontVariantNumeric: 'tabular-nums' }}>{acc}%</div></div>
        <div style={{ marginTop: 14, display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0,1fr))', gap: 6, textAlign: 'center' }}>
          {[['Time', fmtT(sec)], ['Bursts', r.bursts], ['Streak', r.best], ['Sprint', sAcc + '%']].map(([l, v]) => <div key={l}><div style={{ fontSize: 22, fontWeight: 800, fontVariantNumeric: 'tabular-nums' }}>{v}</div><div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-muted)', marginTop: 2 }}>{l}</div></div>)}
        </div>
        <div style={{ marginTop: 18 }}><HydrationCard final /></div>
      </div>
      <div style={{ paddingTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
        <Button icon={nx.icon} onClick={() => onNextWorld(nx.id)}>Next: {nx.name}</Button>
        <Button tone="ghost" icon={null} onClick={onHome}>Back to Home</Button>
      </div>
    </WorldFrame>
  );
}

function BrainRide({ worldId = 'space', voice, onExit, onComplete, onNextWorld }) {
  const w = WORLDS[worldId] || WORLDS.space;
  const fresh = (full = true) => ({ idx: -1, start: 0, end: 0, bursts: 0, correct: 0, total: 0, best: 0, sCorrect: 0, sTotal: 0, last: null, mem: makeMem(), full: !!full });
  const [r, setR] = React.useState(() => fresh(true));
  const total = burstCount(w);
  const adv = p => (p.idx + 1 >= w.segs.length ? { idx: p.idx + 1, end: Date.now() } : { idx: p.idx + 1 });
  const next = () => setR(p => ({ ...p, ...adv(p) }));
  const burstDone = (res, sprint) => setR(p => ({ ...p, ...adv(p), last: res, bursts: p.bursts + 1, best: Math.max(p.best, res.best),
    ...(sprint ? { sCorrect: p.sCorrect + res.correct, sTotal: p.sTotal + res.total } : { correct: p.correct + res.correct, total: p.total + res.total }) }));
  const seg = w.segs[r.idx];
  const n = r.full ? 20 : 6;
  if (r.idx < 0) return <WorldIntro w={w} full={r.full} setFull={f => setR(p => ({ ...p, full: f }))} onExit={onExit} onStart={() => { window.GGData.tone(660, 'sine', 0.2); setR(p => ({ ...p, idx: 0, start: Date.now() })); }} />;
  if (!seg) return <WorldComplete w={w} r={{ ...r, voice }} onReport={onComplete} onHome={onExit} onNextWorld={onNextWorld} />;
  if (seg.type === 'phase') { const nx = w.segs[r.idx + 1]; return <PhaseSeg key={r.idx} w={w} seg={seg} secs={r.full ? seg.secs : Math.max(6, Math.round(seg.secs / 10))} nextTitle={nx && nx.type === 'burst' ? nx.title : null} last={r.last} done={r.bursts} total={total} voice={voice} onNext={next} onExit={onExit} />; }
  if (seg.type === 'encode') return <EncodeSeg key={r.idx} w={w} mem={r.mem} voice={voice} onNext={next} onExit={onExit} done={r.bursts} total={total} />;
  const ex = EX[seg.ex];
  return <DemoGate key={'g' + r.idx} w={w} ex={ex} mem={r.mem} onBack={onExit}>
    {ex.key === 'nav' ? <NeuralNav w={w} seg={seg} n={Math.min(n, 8)} voice={voice} onExit={onExit} onDone={burstDone} /> : <BurstPlay w={w} seg={seg} ex={ex} n={n} mem={r.mem} voice={voice} onExit={onExit} onDone={burstDone} />}
  </DemoGate>;
}
Object.assign(window, { BrainRide, WORLDS, WORLD_ORDER, EX, rideLen, burstCount });
