/* =============================================================================
   SheetHead — Rules: "Learn to play" (chronological, animated) and "Full rules" (reference)
   The animated examples are tiny scenes built from the real Card component. Scenarios live in rules-data.js and are
   replayed through the engine in tests/rules-demos.test.js; playable / not playable states below come from the
   engine's rankLegalOn(), never from hard-coded flags. Presentation only.
============================================================================= */
(function () {
  const { useState, useEffect, useMemo, useRef, useCallback } = React;
  const UI = window.SH.ui;
  const { cx, Card, Pip, Frame, Tabs, Icon, Button } = UI;
  const E = window.SH.engine;
  const RD = window.SH.rulesData;
  const S = RD.SCENARIOS;

  // ---------------------------------------------------------------------------------------------- scene model
  const BASE = { x: 50, y: 38, o: 0, rot: 0, z: 1, sc: 1, mode: null, back: false, as: null };
  const P = { pile: { x: 50, y: 38 }, draw: { x: 28, y: 38 }, burn: { x: 73, y: 38 } };
  const SEATS = [{ n: 'You', x: 8, y: 90 }, { n: 'Nova', x: 6, y: 47 }, { n: 'Kairo', x: 50, y: 7 }, { n: 'Zephyr', x: 94, y: 47 }];
  const lg = (rank, top) => (E.rankLegalOn(rank, top) ? 'legal' : 'illegal');
  const spread = (n, gap = 12) => Array.from({ length: n }, (_, i) => 50 + (i - (n - 1) / 2) * gap);
  const put = (ids, xs, y, extra) => Object.fromEntries(ids.map((id, i) => [id, { x: xs[i], y, o: 1, ...extra }]));
  const onPile = (k, z) => ({ x: P.pile.x + k * 0.7, y: P.pile.y - k * 0.7, rot: [-4, 3, -2, 4][k % 4], z: z || 10 + k, o: 1, mode: null });
  const K = (id, rank, suit) => ({ id, rank, suit: suit || null });
  const back = { back: true };
  const HAND_Y = 86;

  // steps patch entities cumulatively; `seats`, when given, patches the seat marks (also cumulative)
  function compile(def) {
    const cur = {}; Object.keys(def.cards).forEach(id => { cur[id] = { ...BASE }; });
    let seats = { active: null, skipped: [], extra: null, note: {} };
    return def.steps.map(st => {
      Object.entries(st.at || {}).forEach(([id, patch]) => { cur[id] = { ...cur[id], ...patch }; });
      if (st.seats) seats = { ...seats, ...st.seats };
      const ents = {}; Object.keys(cur).forEach(id => { ents[id] = { ...cur[id] }; });
      return { cap: st.cap, ms: st.ms, tags: st.tags || [], seats: { ...seats }, ents };
    });
  }

  // ---------------------------------------------------------------------------------------------- the demos
  const DEMOS = {};

  DEMOS.setup = (() => {
    const showZones = false;
    const fd = ['fd1', 'fd2', 'fd3'], fu = ['fu1', 'fu2', 'fu3'], h = ['h1', 'h2', 'h3'];
    const cards = { deck: K('deck', 'K', 'S'), fd1: K('fd1', '6', 'C'), fd2: K('fd2', 'J', 'D'), fd3: K('fd3', '3', 'S'), fu1: K('fu1', '4', 'H'), fu2: K('fu2', 'Q', 'S'), fu3: K('fu3', '8', 'D'), h1: K('h1', '5', 'H'), h2: K('h2', '9', 'S'), h3: K('h3', 'K', 'C') };
    const stack = { x: 50, y: 38, o: 1, z: 1 };
    return { showZones, cards, steps: [
      { cap: 'A full deck (52 cards plus 2 Jokers) sits in the middle. Every player is dealt three groups of three cards.', at: { deck: { ...stack, back: true, z: 20 }, ...Object.fromEntries([...fd, ...fu, ...h].map(id => [id, { ...stack, back: true }])) }, tags: [{ t: 'label', x: 50, y: 58, text: '54 cards' }] },
      { cap: 'First, 3 cards go face down in front of you. Nobody looks at these; you will meet them last.', at: { fd1: { x: 41, y: 70, back: true }, fd2: { x: 50, y: 70, back: true }, fd3: { x: 59, y: 70, back: true } }, tags: [{ t: 'label', x: 22, y: 70, text: 'Face-down' }] },
      { cap: 'Next, 3 cards face up on top of them. Everyone can see these.', at: { fu1: { x: 44, y: 63, back: false, z: 3 }, fu2: { x: 53, y: 63, back: false, z: 3 }, fu3: { x: 62, y: 63, back: false, z: 3 } }, tags: [{ t: 'label', x: 22, y: 62, text: 'Face-up' }] },
      { cap: 'Then 3 cards in your hand. Only you can see these, and you play from them first.', at: { h1: { x: 40, y: HAND_Y, back: false, z: 5 }, h2: { x: 50, y: HAND_Y, back: false, z: 5 }, h3: { x: 60, y: HAND_Y, back: false, z: 5 } }, tags: [{ t: 'label', x: 22, y: HAND_Y, text: 'Hand' }] },
      { cap: 'The other three players get the same. Whatever is left becomes the draw pile.', at: { deck: { x: P.draw.x, y: P.draw.y, z: 2 } }, tags: [{ t: 'label', x: P.draw.x, y: 55, text: 'Draw pile' }] }
    ] };
  })();

  DEMOS.starter = { cards: {}, steps: [
    { cap: 'Before the first card is played, the game picks one player at random to start. It is a different seat every match.', seats: { note: { 2: 'STARTS' } } },
    { cap: 'That player takes the first turn.', seats: { active: 2, note: {} } },
    { cap: 'Then the turn moves clockwise: to the player on their left...', seats: { active: 3 } },
    { cap: '...then the next one...', seats: { active: 0 } },
    { cap: '...and round again. Turns always go clockwise (unless a card says otherwise).', seats: { active: 1 } }
  ] };

  DEMOS.normal = (() => {
    const d = S.normal;
    const cards = { draw: K('draw', 'K', 'S'), t: K('t', d.top, 'C'), h5: K('h5', d.hand[0], 'H'), h9: K('h9', d.hand[1], 'D'), hK: K('hK', d.hand[2], 'S'), q: K('q', d.draw, 'H') };
    const hand3 = put(['h5', 'h9', 'hK'], [38, 50, 62], HAND_Y, { z: 5 });
    return { cards, steps: [
      { cap: 'Your turn. The pile shows a 6, and your hand is 5, 9 and K.', at: { draw: { ...P.draw, o: 1, ...back }, t: { ...onPile(0), o: 1 }, ...hand3 }, seats: { active: 0 } },
      { cap: 'Cards that can go on a 6 stay bright and lit. The gray 5 cannot be played right now.', at: { h5: { mode: lg('5', d.top) }, h9: { mode: lg('9', d.top) }, hK: { mode: lg('K', d.top) } } },
      { cap: 'Choose a legal card (or several of the same rank) and play it.', at: { h9: { mode: 'sel' }, h5: { mode: null }, hK: { mode: null } } },
      { cap: 'The 9 lands on the pile. The top card is now a 9, and that is what the next player must answer.', at: { h9: { ...onPile(1, 12), mode: null }, h5: { x: 44 }, hK: { x: 56 } }, tags: [{ t: 'label', x: 50, y: 20, text: '9 on top' }] },
      { cap: 'Your hand has fewer than 3 cards, so you draw back up to 3 from the draw pile.', at: { q: { x: 62, y: HAND_Y, o: 1, z: 6, back: false }, h5: { x: 38 }, hK: { x: 50 } }, tags: [{ t: 'label', x: P.draw.x, y: 55, text: 'Draw' }] },
      { cap: 'That is your whole turn. Play passes clockwise to the next player.', seats: { active: 1 } }
    ] };
  })();

  DEMOS.cannot = (() => {
    const d = S.cannot, top = d.pile[d.pile.length - 1];
    const cards = { p8: K('p8', d.pile[0], 'S'), pJ: K('pJ', d.pile[1], 'D'), pQ: K('pQ', d.pile[2], 'C'), h5: K('h5', d.hand[0], 'H'), h6: K('h6', d.hand[1], 'C'), h9: K('h9', d.hand[2], 'D') };
    const six = ['h5', 'h6', 'p8', 'h9', 'pJ', 'pQ'], xs = spread(6, 9.5);
    return { cards, steps: [
      { cap: 'A Queen is on top of the pile. Only a Q, K or A (or a Joker acting as one) can go on it. Your hand is 5, 6 and 9.', at: { p8: onPile(0), pJ: onPile(1), pQ: onPile(2), ...put(['h5', 'h6', 'h9'], [38, 50, 62], HAND_Y, { z: 5 }) }, seats: { active: 0 } },
      { cap: 'Every card is grayed out: none of them can be played.', at: { h5: { mode: lg('5', top) }, h6: { mode: lg('6', top) }, h9: { mode: lg('9', top) } }, tags: [{ t: 'label', x: 50, y: 62, text: 'Nothing fits', tone: 'no' }] },
      { cap: 'So you must pick up the whole pile into your hand.', at: { h5: { mode: null, x: xs[0] }, h6: { mode: null, x: xs[1] }, p8: { x: xs[2], y: HAND_Y, rot: 0, z: 5 }, h9: { mode: null, x: xs[3] }, pJ: { x: xs[4], y: HAND_Y, rot: 0, z: 5 }, pQ: { x: xs[5], y: HAND_Y, rot: 0, z: 5 } } },
      { cap: 'Your turn ends and the next player goes. You are still in the HAND phase, now with six cards. With 3 or more cards you do not draw.', seats: { active: 1 } }
    ] };
  })();

  DEMOS.skip = (() => {
    const d = S.skip;
    const cards = { t: K('t', d.top, 'S'), h8: K('h8', d.play, 'C'), hK: K('hK', 'K', 'H'), hA: K('hA', 'A', 'D') };
    return { showDir: true, cards, steps: [
      { cap: 'You play an 8 on the 6. An 8 can go on 2, 4, 5, 6 and 8.', at: { t: { ...onPile(0), o: 1 }, ...put(['h8', 'hK', 'hA'], [38, 50, 62], HAND_Y, { z: 5 }), h8: { x: 38, y: HAND_Y, o: 1, mode: lg('8', d.top), z: 5 } }, seats: { active: 0 } },
      { cap: 'The 8 lands on the pile and skips the next player, once.', at: { h8: { ...onPile(1, 12), mode: null }, hK: { x: 44 }, hA: { x: 56 } }, tags: [{ t: 'label', x: 50, y: 20, text: '8 = skip' }] },
      { cap: 'Nova would be next, but she misses this turn.', seats: { skipped: [1] } },
      { cap: 'Play jumps straight to Kairo. (A 3 played on an 8 keeps the 8 rank, but does not skip again.)', seats: { active: 2 } }
    ] };
  })();

  DEMOS.joker = (() => {
    const d = S.joker, nx = d.next;
    const asks = E.RANKS.filter(r => E.rankLegalOn(r, d.top));
    const cards = { t: K('t', d.top, 'S'), jk: K('jk', 'JOKER'), hK: K('hK', 'K', 'H'), h5: K('h5', '5', 'D'), nK: K('nK', nx.hand[0], 'C'), n4: K('n4', nx.hand[1], 'S'), n7: K('n7', nx.hand[2], 'H') };
    return { cards, steps: [
      { cap: 'You hold a Joker. It can pretend to be any rank that is legal on the pile.', at: { t: { ...onPile(0), o: 1 }, ...put(['jk', 'hK', 'h5'], [38, 50, 62], HAND_Y, { z: 5 }) }, seats: { active: 0 } },
      { cap: 'When you play it, you choose which rank it stands for. Only ranks that are legal are offered.', at: { jk: { mode: 'sel' } }, tags: [{ t: 'chips', x: 50, y: 60, items: asks }] },
      { cap: 'You choose 9.', at: { jk: { mode: 'sel', as: d.declared } }, tags: [{ t: 'chips', x: 50, y: 60, items: asks, hi: d.declared }] },
      { cap: 'The Joker now counts as a 9 for everyone. The table shows JOKER → 9.', at: { jk: { ...onPile(1, 12), mode: null, as: d.declared }, hK: { x: 44 }, h5: { x: 56 } }, tags: [{ t: 'label', x: 50, y: 20, text: 'Joker → 9' }] },
      { cap: 'Nova must now answer a 9. Her K is fine, but her 4 and 7 are not allowed on a 9.', at: { nK: { x: 15, y: 62, o: 1, z: 4, mode: lg(nx.hand[0], d.declared) }, n4: { x: 25.5, y: 62, o: 1, z: 4, mode: lg(nx.hand[1], d.declared) }, n7: { x: 36, y: 62, o: 1, z: 4, mode: lg(nx.hand[2], d.declared) } }, seats: { active: 1 }, tags: [{ t: 'label', x: 25.5, y: 81, text: "Nova's hand" }] }
    ] };
  })();

  DEMOS.ten = (() => {
    const d = S.ten;
    const cards = { t: K('t', d.top, 'D'), h10: K('h10', d.play, 'H'), h5: K('h5', '5', 'S'), h6: K('h6', '6', 'C') };
    return { cards, steps: [
      { cap: 'A King is on top. You hold a 10, and a 10 can go on almost anything (only a 7 or a Queen stops it).', at: { t: { ...onPile(0), o: 1 }, ...put(['h10', 'h5', 'h6'], [38, 50, 62], HAND_Y, { z: 5 }), h10: { x: 38, y: HAND_Y, o: 1, mode: lg('10', d.top), z: 5 } }, seats: { active: 0 } },
      { cap: 'Play the 10.', at: { h10: { mode: 'sel' } } },
      { cap: 'It lands on the pile...', at: { h10: { ...onPile(1, 12), mode: null }, h5: { x: 44 }, h6: { x: 56 } } },
      { cap: '...and immediately burns the whole pile. Burned cards are out of the game for good.', at: { t: { x: P.burn.x, y: P.burn.y, rot: -8, o: 0.85, z: 3 }, h10: { x: P.burn.x + 1, y: P.burn.y - 1, rot: 6, o: 0.85, z: 4 } }, tags: [{ t: 'flame', x: 50, y: 38 }, { t: 'label', x: P.burn.x, y: 57, text: 'Burned', tone: 'burn' }] },
      { cap: 'The table is empty and, because you played a 10, it is your turn again.', at: { t: { o: 0 }, h10: { o: 0 } }, seats: { active: 0, extra: 0 }, tags: [{ t: 'label', x: 24, y: 78, text: 'Play again' }] }
    ] };
  })();

  DEMOS.quad = (() => {
    const d = S.quad;
    const cards = { a: K('a', '9', 'S'), b: K('b', '9', 'D'), c: K('c', '9', 'C'), h9: K('h9', d.play, 'H'), hK: K('hK', 'K', 'S'), h5: K('h5', '5', 'D') };
    const row = (k) => ({ x: 34 + k * 10.5, y: 38, o: 1, rot: 0, z: 10 + k });
    return { cards, steps: [
      { cap: 'Three 9s are on top of the pile, one after another. You have the fourth.', at: { a: row(0), b: row(1), c: row(2), ...put(['h9', 'hK', 'h5'], [38, 50, 62], HAND_Y, { z: 5 }), h9: { x: 38, y: HAND_Y, o: 1, mode: lg('9', '9'), z: 5 } }, seats: { active: 0 } },
      { cap: 'Play it: that makes four cards of the same rank in a row.', at: { h9: { mode: 'sel' } } },
      { cap: 'Four in a row on top of the pile...', at: { h9: { ...row(3), mode: null }, hK: { x: 44 }, h5: { x: 56 } }, tags: [{ t: 'label', x: 50, y: 20, text: '4 × 9' }] },
      { cap: '...burns the whole pile.', at: { a: { x: P.burn.x - 4, y: P.burn.y, o: 0.85, rot: -6 }, b: { x: P.burn.x - 1, y: P.burn.y, o: 0.85, rot: -2 }, c: { x: P.burn.x + 2, y: P.burn.y, o: 0.85, rot: 3 }, h9: { x: P.burn.x + 5, y: P.burn.y, o: 0.85, rot: 7 } }, tags: [{ t: 'flame', x: 50, y: 38 }, { t: 'label', x: P.burn.x, y: 57, text: 'Four of a kind', tone: 'burn' }] },
      { cap: 'You get another turn. A four-of-a-kind can even be made out of turn, before the next player acts (never with a Joker, and never from face-down cards).', at: { a: { o: 0 }, b: { o: 0 }, c: { o: 0 }, h9: { o: 0 } }, seats: { active: 0, extra: 0 }, ms: 3600 }
    ] };
  })();

  DEMOS.stick = (() => {
    const d = S.stick;
    const cards = { draw: K('draw', 'K', 'S'), t: K('t', d.top, 'C'), h9: K('h9', d.hand[0], 'H'), hK: K('hK', d.hand[1], 'S'), h5: K('h5', d.hand[2], 'D'), q: K('q', d.draw, 'S') };
    return { cards, steps: [
      { cap: 'You play a 9 on the 6...', at: { draw: { ...P.draw, o: 1, ...back }, t: { ...onPile(0), o: 1 }, ...put(['h9', 'hK', 'h5'], [38, 50, 62], HAND_Y, { z: 5 }), h9: { x: 38, y: HAND_Y, o: 1, mode: 'sel', z: 5 } }, seats: { active: 0 } },
      { cap: '...and your hand drops below 3, so you draw.', at: { h9: { ...onPile(1, 12), mode: null }, hK: { x: 44 }, h5: { x: 56 } } },
      { cap: 'The card you drew is another 9!', at: { q: { x: 62, y: HAND_Y, o: 1, z: 6, back: false }, hK: { x: 38 }, h5: { x: 50 } }, tags: [{ t: 'label', x: 62, y: 66, text: 'Another 9' }] },
      { cap: 'You may "stick" it: add it to the play you just made, before the next player acts. In the game it glows gold; double-click it.', at: { q: { mode: 'stick' } }, ms: 3200 },
      { cap: 'Both 9s are now on the table and the turn passes on.', at: { q: { ...onPile(2, 14), mode: null } }, seats: { active: 1 }, tags: [{ t: 'label', x: 50, y: 20, text: '9 + 9' }] }
    ] };
  })();

  DEMOS.phases = (() => {
    const d = S.phases;
    const cards = { t: K('t', d.top, 'C'), h9: K('h9', d.last, 'H'), fuK: K('fuK', d.faceUp[0], 'S'), fdA: K('fdA', d.faceDown[0], 'H'), fd5: K('fd5', d.faceDown[1], 'C') };
    return { cards, steps: [
      { cap: 'The draw pile is empty and you have one card left in hand: a 9. A face-up K and two face-down cards wait behind it.', at: { t: { ...onPile(0), o: 1 }, h9: { x: 50, y: HAND_Y, o: 1, z: 5, mode: lg('9', d.top) }, fdA: { x: 41, y: 71, o: 1, back: true }, fd5: { x: 59, y: 71, o: 1, back: true }, fuK: { x: 50, y: 64, o: 1, z: 3 } }, seats: { active: 0 }, tags: [{ t: 'label', x: 22, y: HAND_Y, text: 'Hand' }, { t: 'label', x: 20, y: 61, text: 'Face-up' }, { t: 'label', x: 20, y: 72, text: 'Face-down' }] },
      { cap: 'You play your last hand card.', at: { h9: { ...onPile(1, 12), mode: null } } },
      { cap: 'Hand empty and draw pile empty: your face-up cards come into play. (If none of them fits, you take the pile plus every face-up card of one rank you choose.)', at: { fuK: { mode: lg(d.faceUp[0], d.last) } }, tags: [{ t: 'label', x: 50, y: 20, text: 'Face-up phase' }], ms: 3600 },
      { cap: 'Your K can go on the 9, so you play it.', at: { fuK: { ...onPile(2, 14), mode: null } } },
      { cap: 'No hand and no face-up cards left: now you play your face-down cards, blind, one per turn.', at: { fdA: { mode: 'legal' }, fd5: { mode: 'legal' } }, tags: [{ t: 'label', x: 50, y: 20, text: 'Face-down phase' }] },
      { cap: 'You flip one. If it is legal it is played (an Ace fits on the K). If not, you pick up the pile together with it.', at: { fdA: { ...onPile(3, 16), back: false, mode: null, sc: 1 }, fd5: { mode: null } }, ms: 3600 }
    ] };
  })();

  // ---------------------------------------------------------------------------------------------- the stage
  function Stage({ def, frame }) {
    const zones = def.showZones !== false && Object.keys(def.cards).length > 0;
    return (
      <div className="demo-stage" aria-hidden="true">
        <div className="ds-table" />
        <div className="ds-scene">
          {zones && <>
            <div className="ds-zone" style={{ left: P.pile.x + '%', top: P.pile.y + '%' }} />
            <div className="ds-zone dash" style={{ left: P.burn.x + '%', top: P.burn.y + '%' }}><span>Burn</span></div>
          </>}
          {def.showDir && <div className="ds-dir">Clockwise</div>}
          {SEATS.map((s, i) => {
            const on = frame.seats.active === i, sk = frame.seats.skipped.includes(i), note = frame.seats.note[i];
            return (
              <div key={i} className={cx('ds-seat', on && 'on', sk && 'skip', frame.seats.extra === i && 'extra')} style={{ left: s.x + '%', top: s.y + '%' }}>
                <span className="ds-plate">{s.n[0]}</span>
                <span className="ds-name">{s.n}</span>
                {sk ? <span className="ds-mark no">Skipped</span> : note ? <span className="ds-mark">{note}</span> : on ? <span className="ds-mark go">{frame.seats.extra === i ? 'Again' : 'Turn'}</span> : null}
              </div>
            );
          })}
          {Object.entries(frame.ents).map(([id, e]) => (
            <div key={id} className="dc" style={{ left: e.x + '%', top: e.y + '%', zIndex: e.z, opacity: e.o, transform: `translate(-50%,-50%) rotate(${e.rot}deg) scale(${e.sc})` }}>
              <Card card={{ ...def.cards[id], as: e.as }} faceDown={e.back} showDeclared={!!e.as}
                    legal={e.mode === 'legal'} illegal={e.mode === 'illegal'} selected={e.mode === 'sel' ? true : undefined} stick={e.mode === 'stick'}
                    style={{ '--cw': 'var(--dcw)' }} />
            </div>
          ))}
          {frame.tags.map((t, k) => t.t === 'flame'
            ? <div key={'f' + k} className="dflame" style={{ left: t.x + '%', top: t.y + '%' }} />
            : t.t === 'chips'
              ? <div key={'c' + k + (t.hi || '')} className="dchips" style={{ left: t.x + '%', top: t.y + '%' }}>{t.items.map(r => <i key={r} className={r === t.hi ? 'hi' : ''}>{r}</i>)}</div>
              : <span key={t.text} className={cx('dtag', t.tone)} style={{ left: t.x + '%', top: t.y + '%' }}>{t.text}</span>)}
        </div>
      </div>
    );
  }

  function useReduced(settings) {
    const [os, setOs] = useState(() => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches);
    useEffect(() => { if (typeof matchMedia !== 'function') return undefined; const m = matchMedia('(prefers-reduced-motion: reduce)'), f = () => setOs(m.matches); m.addEventListener && m.addEventListener('change', f); return () => m.removeEventListener && m.removeEventListener('change', f); }, []);
    return !!(os || (settings && (settings.reducedMotion || settings.animations === false)));
  }

  // Plays through the steps while the demo is on screen (looping), pauses off screen or on request, and can be stepped by hand.
  function Demo({ def, title, kicker, settings }) {
    const frames = useMemo(() => compile(def), [def]);
    const reduced = useReduced(settings);
    const [i, setI] = useState(0), [playing, setPlaying] = useState(false), [snap, setSnap] = useState(false);
    const ref = useRef(null), paused = useRef(false), inView = useRef(false), started = useRef(false);
    const last = frames.length - 1;
    const jump = useCallback((n, instant) => {
      if (instant) { setSnap(true); setI(n); requestAnimationFrame(() => requestAnimationFrame(() => setSnap(false))); } else setI(n);
    }, []);
    useEffect(() => {
      const el = ref.current; if (!el || !('IntersectionObserver' in window)) return undefined;
      const io = new IntersectionObserver((es) => {
        inView.current = es[0].isIntersecting;
        if (inView.current) { if (!reduced && !paused.current) { if (!started.current) { started.current = true; jump(0, true); } setPlaying(true); } }
        else setPlaying(false);
      }, { threshold: 0.6 });
      io.observe(el); return () => io.disconnect();
    }, [reduced]);
    useEffect(() => {
      if (!playing || reduced) return undefined;
      const t = setTimeout(() => { if (i >= last) jump(0, true); else setI(i + 1); }, i >= last ? 3400 : (frames[i].ms || 2700));
      return () => clearTimeout(t);
    }, [playing, i, reduced]);
    const f = frames[i];
    const toggle = () => { if (playing) { paused.current = true; setPlaying(false); } else { paused.current = false; if (i >= last) jump(0, true); setPlaying(true); } };
    const go = (n) => { paused.current = true; setPlaying(false); jump(n, n === 0); };
    return (
      <figure className={cx('demo', reduced && 'reduced', snap && 'snap')} ref={ref}>
        <div className="demo-head">
          <div><span className="kicker">{kicker}</span><b>{title}</b></div>
          <div className="demo-ctl">
            <button type="button" onClick={() => go(Math.max(0, i - 1))} disabled={i === 0} aria-label="Previous step">{'‹'}</button>
            <button type="button" onClick={toggle} aria-label={playing ? 'Pause' : 'Play'}>{playing ? '❚❚' : '▶'}</button>
            <button type="button" onClick={() => go(Math.min(last, i + 1))} disabled={i === last} aria-label="Next step">{'›'}</button>
            <button type="button" onClick={() => { paused.current = false; jump(0, true); setPlaying(!reduced); }} aria-label="Replay from the start">{'↺'}</button>
          </div>
        </div>
        <Stage def={def} frame={f} />
        <figcaption className="demo-cap">
          <span className="step-n">Step {i + 1}<em>/{frames.length}</em></span>
          <p key={i}>{f.cap}</p>
        </figcaption>
        <div className="demo-dots" role="group" aria-label={title + ' steps'}>
          {frames.map((_, n) => <button key={n} type="button" className={cx(n === i && 'on', n < i && 'done')} onClick={() => go(n)} aria-label={'Step ' + (n + 1)} aria-current={n === i ? 'step' : undefined} />)}
        </div>
        <ol className="sr-only">{frames.map((fr, n) => <li key={n}>{fr.cap}</li>)}</ol>
      </figure>
    );
  }

  // ---------------------------------------------------------------------------------------------- interactive legality explorer
  const TOP_NOTES = {
    EMPTY: 'The table is empty: any card may be played.',
    '2': 'A 2 resets the pile: after it, almost anything goes.',
    '7': 'After a 7 only 2, 3, 4, 5, 6 and 7 are allowed. A 10 cannot go on a 7.',
    Q: 'After a Queen only Q, K and A can follow (a Joker may act as one of them).'
  };
  function Explorer() {
    const tops = ['EMPTY', ...E.RANKS.filter(r => r !== '3' && r !== '10')];
    const [top, setTop] = useState('6');
    const suits = ['S', 'H', 'D', 'C'];
    const ok = (r) => E.rankLegalOn(r, top);
    const legalList = E.RANKS.filter(ok);
    return (
      <div className="explorer">
        <div className="ex-top">
          <div className="ex-pile" aria-label={top === 'EMPTY' ? 'Empty table' : 'Top of the pile: ' + top}>
            {top === 'EMPTY' ? <div className="ex-empty">Empty</div> : <Card card={{ id: 'ex-top', rank: top, suit: 'D' }} style={{ '--cw': '58px' }} />}
            <span>On the pile</span>
          </div>
          <div className="ex-pick">
            <span className="ex-label">Tap the top card of the pile</span>
            <div className="ex-tops" role="radiogroup" aria-label="Top card of the pile">
              {tops.map(r => <button key={r} type="button" role="radio" aria-checked={top === r} className={cx(top === r && 'on')} onClick={() => setTop(r)}>{r === 'EMPTY' ? 'Empty' : r}</button>)}
            </div>
          </div>
        </div>
        <div className="ex-hand" aria-label="Your possible cards">
          {E.RANKS.map((r, n) => <Card key={r} card={{ id: 'ex' + r, rank: r, suit: suits[n % 4] }} legal={ok(r)} illegal={!ok(r)} style={{ '--cw': 'var(--exw)' }} label={r + (ok(r) ? ', playable' : ', not playable')} />)}
          <Card card={{ id: 'exJ', rank: 'JOKER' }} legal illegal={false} style={{ '--cw': 'var(--exw)' }} label="Joker, playable as a legal rank" />
        </div>
        <p className="ex-sum"><b>{top === 'EMPTY' ? 'Anything can be played.' : `On a ${top} you can play:`}</b> {top === 'EMPTY' ? '' : legalList.join(' · ') + ' · or a Joker as one of those.'}</p>
        {TOP_NOTES[top] && <p className="ex-note">{TOP_NOTES[top]}</p>}
      </div>
    );
  }

  // ---------------------------------------------------------------------------------------------- content
  const POWERS = [
    ['2', 'Reset', 'Goes on anything except a Queen and cancels the restriction: after a 2, anything can follow.'],
    ['3', 'Mirror', 'Goes on anything except a Queen. The rank underneath still governs, so a 3 on a 7 keeps the 7 limit.'],
    ['7', 'Low', 'After a 7 only 2 to 7 may follow. A 10 is not allowed on a 7.'],
    ['8', 'Skip', 'The next player misses their turn, once.'],
    ['10', 'Burn', 'Burns the whole pile and you play again. Not allowed on a 7 or a Queen.'],
    ['Q', 'Blocker', 'After a Queen only Q, K or A may follow.'],
    ['JOKER', 'Joker', 'Pretends to be any rank that is legal. You choose it when you play it, and everyone sees JOKER → that rank.']
  ];

  function Chapter({ id, n, title, lead, children }) {
    return (
      <section className="ch" data-ch={id} aria-labelledby={'ch-' + id}>
        <div className="ch-num" aria-hidden="true">{String(n).padStart(2, '0')}</div>
        <h2 id={'ch-' + id}>{title}</h2>
        {lead && <p className="ch-lead">{lead}</p>}
        {children}
      </section>
    );
  }
  const Tip = ({ children, label = 'Good to know' }) => <aside className="tip"><b>{label}</b><p>{children}</p></aside>;

  function LearnBody({ settings, startSinglePlayer }) {
    const s = settings;
    return (
      <>
        <Chapter id="goal" n={1} title="The goal" lead="Get rid of every card you own before anyone else does.">
          <p>SheetHead is a four-player card game. You are <b>out</b> when your <b>hand</b>, your <b>face-up cards</b> and your <b>face-down cards</b> are all gone. The <b>first player out wins</b>, and the match ends right then.</p>
          <p>Cards are played onto a shared <b>pile</b> in the middle. What you may play depends on what is on top of it, so the game is about choosing the right card at the right time.</p>
        </Chapter>
        <Chapter id="setup" n={2} title="Setup" lead="A full deck, dealt in three groups of three.">
          <Demo def={DEMOS.setup} title="The deal" kicker="Setup" settings={s} />
          <ul><li>The deck is a standard 52 cards <b>plus 2 Jokers</b> (54 in all).</li><li>Each player gets <b>3 face-down</b>, <b>3 face-up</b> and <b>3 in hand</b>. You start by playing from your hand; the other two groups wait until later.</li></ul>
        </Chapter>
        <Chapter id="start" n={3} title="Who starts" lead="Chosen at random, then clockwise.">
          <Demo def={DEMOS.starter} title="The starting player" kicker="Turn order" settings={s} />
          <p>The starting player is picked at random for every new game (it is never automatically you). After that the turn always moves <b>clockwise</b>.</p>
          <Tip>Before the very first move, a player holding a 4 can jump in early. That special case is explained under <i>The 4 burst</i> in the Full rules.</Tip>
        </Chapter>
        <Chapter id="turn" n={4} title="Your turn" lead="Play a card that is allowed on the pile.">
          <Demo def={DEMOS.normal} title="A normal turn" kicker="Playing" settings={s} />
          <ul>
            <li>Choose a card from your hand that is <b>allowed on the top card</b>. In the game, playable cards stay bright and lit; cards you cannot play are grayed out.</li>
            <li>You may play <b>several cards at once</b>, as long as they are all the <b>same rank</b>.</li>
            <li>Play by selecting cards and pressing <b>Play</b>, by double-clicking a card, or by dragging cards onto the pile.</li>
            <li>You have <b>40 seconds</b> per turn. If the time runs out, the game plays a legal move for you.</li>
          </ul>
        </Chapter>
        <Chapter id="which" n={5} title="Which cards go on which" lead="It is not “higher wins”. Every rank has its own list.">
          <p>Pick a card for the top of the pile and see which cards you could play on it. Bright cards can be played; gray ones cannot.</p>
          <Explorer />
          <p className="note">The complete chart is in the <b>Full rules</b> tab.</p>
        </Chapter>
        <Chapter id="draw" n={6} title="Drawing back up" lead="Keep three cards in hand while the draw pile lasts.">
          <ul>
            <li>After you play, if the draw pile still has cards and you hold <b>fewer than 3</b>, you draw until you hold exactly 3.</li>
            <li>If you hold 3 or more, you draw nothing. (Picking up a pile can give you many more than 3; nothing is ever thrown away.)</li>
            <li>When the draw pile is empty, your hand just shrinks as you play.</li>
          </ul>
        </Chapter>
        <Chapter id="stuck" n={7} title="When you cannot play" lead="Pick up the pile.">
          <Demo def={DEMOS.cannot} title="No legal card" kicker="Picking up" settings={s} />
          <p>If none of your cards is allowed, you take the <b>entire pile</b> into your hand and your turn ends. That is a setback, but it also gives you plenty of cards to work with next time.</p>
        </Chapter>
        <Chapter id="special" n={8} title="Special cards" lead="Some cards change the rules of the pile.">
          <div className="powers">
            {POWERS.map(([r, name, text]) => (
              <div className={cx('power', r === 'JOKER' && 'joker')} key={name}>
                <Card card={r === 'JOKER' ? { id: 'pw' + r, rank: 'JOKER' } : { id: 'pw' + r, rank: r, suit: r === '2' || r === '10' ? 'H' : 'S' }} style={{ '--cw': '46px' }} />
                <div><b>{name}</b><p>{text}</p></div>
              </div>
            ))}
          </div>
          <h3 className="ch-sub">The 8: skip</h3>
          <Demo def={DEMOS.skip} title="Skipping a player" kicker="Special card" settings={s} />
          <h3 className="ch-sub">The Joker</h3>
          <Demo def={DEMOS.joker} title="Playing a Joker" kicker="Special card" settings={s} />
          <p>A Joker is never stuck: whatever rank you declare becomes the rank the next player has to answer. It stays a Joker, though: you play it on its own (never together with a real card, even one of the rank it imitates), it cannot be stuck onto a play, and it can never be used to make a four-of-a-kind.</p>
        </Chapter>
        <Chapter id="burn" n={9} title="Burning the pile" lead="Clear the table and go again.">
          <p>Burned cards leave the game. There are two ways to burn the pile, and both give you an <b>extra turn</b>.</p>
          <h3 className="ch-sub">Play a 10</h3>
          <Demo def={DEMOS.ten} title="The 10 burn" kicker="Burning" settings={s} />
          <h3 className="ch-sub">Four of a kind</h3>
          <Demo def={DEMOS.quad} title="Four in a row" kicker="Burning" settings={s} />
          <ul><li>The top four cards must be the <b>same rank, one after another</b> (9, 9, 3, 9 does not count).</li><li>You can complete it from your hand or your face-up cards, even <b>out of turn</b>, if you do it before the next player has played. Face-down cards never interrupt.</li></ul>
        </Chapter>
        <Chapter id="stick" n={10} title="Stick" lead="A lucky draw can join your play.">
          <Demo def={DEMOS.stick} title="Sticking a card" kicker="Bonus move" settings={s} />
          <p>If the card you draw after playing has the <b>same rank</b> as the cards you just played, you may stick it onto that play, before the next player acts. Skip it and the turn simply passes.</p>
        </Chapter>
        <Chapter id="phases" n={11} title="Face-up and face-down" lead="Your cards play in three stages: hand, face-up, face-down.">
          <Demo def={DEMOS.phases} title="From hand to face-down" kicker="The three phases" settings={s} />
          <ul>
            <li><b>Hand</b> first. When your hand is empty <i>and</i> the draw pile is empty, you move to <b>face-up</b>.</li>
            <li><b>Face-up</b>: play them like hand cards (equal ranks may go together). If none fits, you take the pile <b>plus every face-up card of one rank you choose</b>, and you are back in the hand phase.</li>
            <li><b>Face-down</b>: when nothing else is left, flip one card blindly on your own turn. If it is legal it is played; if not, it and the pile go to your hand. Three matching cards on top plus a matching flip is a four-of-a-kind burn.</li>
          </ul>
        </Chapter>
        <Chapter id="win" n={12} title="Winning" lead="Empty everything first.">
          <p>The moment a player has <b>no hand, no face-up cards and no face-down cards</b>, they win and the match ends. Any burn or pickup caused by your last move is settled first.</p>
          <div className="cta-row left"><Button variant="primary" onClick={() => startSinglePlayer('easy')} icon={<Icon name="play" size={16} />}>Try an easy game</Button></div>
        </Chapter>
      </>
    );
  }

  function FullBody() {
    const Ch = Chapter;
    return (
      <>
        <Ch id="f-match" n={1} title="Match and setup">
          <ul>
            <li>Exactly 4 players, 54 cards (52 + 2 Jokers). Each player gets 3 face-down, 3 face-up and 3 hand cards; the rest is the draw pile. The table pile starts empty.</li>
            <li>The <b>first player with no hand, face-up and face-down cards wins</b>; the match ends immediately, after every consequence of that move is resolved.</li>
            <li>The <b>starting player is chosen at random</b> for each new game and is part of the game state, so everyone agrees on it. Play goes clockwise.</li>
            <li>Each turn has a <b>40-second</b> clock (a large countdown shows during the last 15 seconds). On timeout the game makes a legal move for you.</li>
          </ul>
        </Ch>
        <Ch id="f-hand" n={2} title="Hand phase">
          <ul>
            <li>You are in the hand phase whenever you hold at least one hand card. Play one or more cards of <b>one rank</b> that is legal on the effective top of the pile.</li>
            <li><b>Refill:</b> after a play, if the draw pile has cards and your hand is below 3, draw exactly enough to reach 3. At 3 or more, draw nothing. A hand may be bigger than 3 after picking up; cards are never discarded.</li>
            <li><b>No legal card:</b> pick up the entire pile; your turn ends and you stay in the hand phase.</li>
            <li><b>Stick:</b> if the refill gives you a card of the same rank as the cards you just played, you may add it to that play before the next player acts. It is a short window tied to that move only.</li>
          </ul>
        </Ch>
        <Ch id="f-chart" n={3} title="What can go on what">
          <div className="chart" role="table" aria-label="Playable ranks">
            {RD.CHART.map(([r, on]) => (
              <div className="chart-row" role="row" key={r}>
                <span className="cr-rank" role="rowheader">{r}</span>
                <span className="cr-on" role="cell">{Array.isArray(on) ? on.map(x => <i key={x}>{x}</i>) : <em>{on}</em>}</span>
              </div>
            ))}
          </div>
          <p className="note">Read a row as: this card may be played on top of these ranks. On an <b>empty table</b>, any card may be played. This is a fixed chart, not “same or higher”; the game shows the <i>effective</i> rank that governs the next play.</p>
        </Ch>
        <Ch id="f-special" n={4} title="Special cards in detail">
          <ul>
            <li><b>2 (reset):</b> legal on anything except Q. After it, legality is checked against a 2, so a 2 on a 7 cancels the 7 limit.</li>
            <li><b>3 (mirror):</b> legal on anything except Q. The rank underneath stays the effective rank. A 3 on a 7 keeps the 7 limit; a 3 on an 8 keeps the rank 8 but does <b>not</b> skip again, since one-shot effects are not repeated.</li>
            <li><b>7 (low):</b> after an effective 7, only 2, 3, 4, 5, 6 and 7 are legal. A 10 is not.</li>
            <li><b>8 (skip):</b> skips exactly the next player once. Each real 8 skips once.</li>
            <li><b>10 (burn):</b> legal on everything except 7 and Q. It burns the whole pile and you play again. This holds from any phase, including a blind face-down flip.</li>
            <li><b>Q (blocker):</b> after an effective Q only Q, K and A are ordinary answers (a Joker may act as one of them). 2, 3, 4, 7, 8 and 10 are not legal.</li>
            <li><b>Joker:</b> imitates a rank that would be legal. The declared rank is recorded and is the effective rank afterwards. It stays a Joker physically: it is played alone (or with other Jokers), never together with real cards (not even ones of its declared rank), never creates or joins a stick, and never counts toward, or completes, a four-of-a-kind.</li>
          </ul>
        </Ch>
        <Ch id="f-quad" n={5} title="Four of a kind">
          <ul>
            <li>When the <b>top four cards are the same rank, consecutively</b> (9, 9, 9, 9), the pile burns and the player who made it plays again. 9, 9, 3, 9 and 9, 3, 9, 9 do not count.</li>
            <li>From your <b>hand</b> or <b>face-up</b> cards you may complete it in any legal way (3 + 1, 2 + 2, 1 + 3, or 4 together), on your own turn or <b>out of turn</b> before the next player acts. An out-of-turn burner takes the turn and the extra turn; play then continues clockwise after them.</li>
            <li><b>Face-down</b> cards never interrupt. On your own face-down turn, if the top three are one rank and your flipped card matches, it burns.</li>
          </ul>
        </Ch>
        <Ch id="f-burst" n={6} title="The 4 burst">
          <ul>
            <li>A 4 is legal on 2, 4, 7 or an empty table, and has a special out-of-turn use when the table is <b>empty</b>.</li>
            <li><b>Before the first move of the game:</b> if another player bursts a 4, the designated starter is skipped and the player immediately <b>clockwise after the burster</b> becomes the starter.</li>
            <li><b>Later in the game:</b> an out-of-turn burst does not change whose turn it is (the burster refills if in the hand phase).</li>
            <li>A 4 played from your <b>hand</b> on an empty table <b>on your own turn</b> is a burst too: you play again. From <b>face-up</b> cards on your own turn it is a normal play. Face-down cards cannot burst.</li>
          </ul>
        </Ch>
        <Ch id="f-combo" n={7} title="Hand + face-up combo">
          <ul>
            <li>Only when the draw pile is empty. Every remaining hand card must be the <b>same rank X</b>, you must hold a face-up X, and X must be legal.</li>
            <li>You then play <b>all</b> your hand cards plus one or more face-up Xs in one move, leaving your hand empty and moving you to the face-up phase. Keeping one X in hand is not allowed.</li>
          </ul>
        </Ch>
        <Ch id="f-faceup" n={8} title="Face-up phase">
          <ul>
            <li>Requires an empty hand, an empty draw pile and at least one face-up card. Equal legal ranks may be played together.</li>
            <li><b>No legal face-up card:</b> take the pile plus <b>all remaining face-up cards of one rank you choose</b> (one, two or three cards). You return to the hand phase and your other face-up cards wait until your hand is empty again. The turn ends.</li>
            <li>With no face-up cards left (and no hand), you enter the face-down phase.</li>
          </ul>
        </Ch>
        <Ch id="f-facedown" n={9} title="Face-down phase">
          <ul>
            <li>On your own turn, choose one hidden card, reveal it, and resolve it.</li>
            <li><b>Legal</b> (even if three matching cards lie on top): it is played with its normal effect. <b>Matching the top three</b>: four-of-a-kind burn and an extra turn. <b>A 10</b> (legal): burns and gives an extra turn. A flipped Joker is declared automatically.</li>
            <li><b>Illegal:</b> the revealed card and the whole pile go into your hand, you return to the hand phase, and the turn ends.</li>
          </ul>
        </Ch>
        <Ch id="f-turns" n={10} title="Turn order details">
          <ul>
            <li>Turns go clockwise. An <b>8</b> skips the next player. A <b>10</b> or a <b>four-of-a-kind</b> gives the player an <b>extra turn</b>. An out-of-turn four-of-a-kind takes the turn; a later out-of-turn 4 burst does not.</li>
            <li>An out-of-turn action must happen before the next player's action is committed. If two players act at once, the game resolves it in a fixed, deterministic order rather than by timing.</li>
          </ul>
        </Ch>
        <Ch id="f-tourney" n={11} title="Tournament mode">
          <ul>
            <li>A tournament is a series of normal games. Choose a target of <b>3, 5, 7 or 10</b>; every game win is <b>1 point</b> and the first player to reach the target wins the tournament. Standings show for 5 seconds between games, then the next game starts by itself.</li>
            <li><b>Game 1</b> is exactly a normal game (random starter). From <b>game 2</b> the previous game's winner is the starting player.</li>
            <li>That winner is dealt 3 face-down cards and <b>6 private cards</b>, and chooses <b>3 of the 6 as FACE UP</b>; the other 3 become the hand. Nobody else sees the six cards, and the deck is the same shuffled deck as in a normal game.</li>
            <li>The 40-second turn clock is already running while they choose (with the usual 15-second warning). If it runs out, the choice is made automatically and the normal timeout move follows.</li>
            <li>The table is still empty, so the pre-first-move <b>4 burst</b> still works. If another player bursts while the winner is choosing, the winner's cards are chosen automatically, the winner is skipped as starter, and the player after the burster starts.</li>
          </ul>
        </Ch>
      </>
    );
  }

  const LEARN_TOC = [['goal', 'The goal'], ['setup', 'Setup'], ['start', 'Who starts'], ['turn', 'Your turn'], ['which', 'Which cards go on which'], ['draw', 'Drawing back up'], ['stuck', 'When you cannot play'], ['special', 'Special cards'], ['burn', 'Burning the pile'], ['stick', 'Stick'], ['phases', 'Face-up and face-down'], ['win', 'Winning']];
  const FULL_TOC = [['f-match', 'Match and setup'], ['f-hand', 'Hand phase'], ['f-chart', 'What goes on what'], ['f-special', 'Special cards'], ['f-quad', 'Four of a kind'], ['f-burst', 'The 4 burst'], ['f-combo', 'Hand + face-up combo'], ['f-faceup', 'Face-up phase'], ['f-facedown', 'Face-down phase'], ['f-turns', 'Turn order'], ['f-tourney', 'Tournament mode']];

  function RulesScreen({ back, settings, startSinglePlayer }) {
    const [tab, setTab] = useState('learn');
    const toc = tab === 'learn' ? LEARN_TOC : FULL_TOC;
    const [active, setActive] = useState(toc[0][0]);
    const bodyRef = useRef(null);
    useEffect(() => { setActive(toc[0][0]); if (bodyRef.current) bodyRef.current.scrollTop = 0; }, [tab]);
    useEffect(() => {
      const root = bodyRef.current; if (!root || !('IntersectionObserver' in window)) return undefined;
      const io = new IntersectionObserver((es) => { es.forEach(e => { if (e.isIntersecting) setActive(e.target.dataset.ch); }); }, { root, rootMargin: '-8% 0px -80% 0px' });
      root.querySelectorAll('[data-ch]').forEach(el => io.observe(el));
      return () => io.disconnect();
    }, [tab]);
    const go = (id) => { const el = bodyRef.current && bodyRef.current.querySelector('[data-ch="' + id + '"]'); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); setActive(id); } };
    return (
      <Frame title="The rules of the table" kicker="How to play" onBack={back} wide className="frame-rules">
        <div className="rules-tabs">
          <Tabs className="tabs-dark" label="Rules sections" options={[['learn', 'Learn to play'], ['full', 'Full rules']]} value={tab} onChange={setTab} />
          <p>{tab === 'learn' ? 'A short walkthrough in the order a game unfolds, with animated examples.' : 'Exact rules and edge cases, for reference.'}</p>
        </div>
        <div className="rules">
          <nav className="toc" aria-label="Chapters">
            {toc.map(([id, t], i) => <button key={id} type="button" className={cx('toc-item', active === id && 'on')} onClick={() => go(id)} aria-current={active === id ? 'true' : undefined}><span>{String(i + 1).padStart(2, '0')}</span>{t}</button>)}
          </nav>
          <article className="book scrollbar-thin" ref={bodyRef} key={tab}>
            {tab === 'learn' ? <LearnBody settings={settings} startSinglePlayer={startSinglePlayer} /> : <FullBody />}
          </article>
        </div>
      </Frame>
    );
  }

  UI.RulesScreen = RulesScreen;
})();
