/* The main playable Cuttle table — used for both directions, themed via props. */

// Late-bound: text/babel scripts evaluate before engine.js (async ES module)
// finishes loading. Re-read window.CuttleEngine on every property access.
const E = new Proxy({}, { get: (_, p) => window.CuttleEngine[p] });

function useGame(seed, theme) {
  const [state, setState] = React.useState(() => E.initialState(seed));
  const [selected, setSelected] = React.useState(null); // {kind:'play'|'one_off'|'jack'|'scuttle', cardId}
  const [hint, setHint] = React.useState(null); // hover preview
  const [fourPicks, setFourPicks] = React.useState([]);
  const [threePick, setThreePick] = React.useState(null);
  const [calloutKey, setCalloutKey] = React.useState(0);
  const [callout, setCallout] = React.useState(null);
  // Hold the callout-clear timer in a ref so it survives effect re-runs.
  // The previous version stored it in the effect's local scope and let the
  // cleanup clear it, which meant any *subsequent* state.flash that didn't
  // produce a new callout (e.g. kind 'play', 'ace', 'scuttle') would cancel
  // the timer without replacing it — leaving JACKED! / COUNTERED! stuck.
  const calloutTimerRef = React.useRef(null);
  const [aceAnim, setAceAnim] = React.useState(null); // { sourceCard, sourcePlayer, wipedCards, key } or null
  const [scuttleAnim, setScuttleAnim] = React.useState(null); // { sourceCard, sourcePlayer, targetCard, targetPlayer, key } or null
  const aceAnimating = aceAnim != null || scuttleAnim != null;

  // Cache of the AI's chosen action when it's the reactor — peeked once per
  // state and reused so we don't re-run the search and risk a flip-flop.
  const aiReactionDecisionRef = React.useRef(null); // { state, action }

  // Pre-apply AI passes synchronously so the reaction panel never appears for
  // them. Runs in a layout effect so the WAITING_FOR_REACTION render that
  // would have mounted the panel never reaches paint — the state moves on
  // first. Only applies to PASS_REACTION; counters fall through and get the
  // mount + delay treatment in the regular AI driver below.
  React.useLayoutEffect(() => {
    if (aceAnimating) {
      aiReactionDecisionRef.current = null;
      return;
    }
    const isReaction = state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 1;
    if (!isReaction) {
      aiReactionDecisionRef.current = null;
      return;
    }
    if (aiReactionDecisionRef.current?.state !== state) {
      aiReactionDecisionRef.current = { state, action: E.aiChooseAction(state) };
    }
    const action = aiReactionDecisionRef.current.action;
    if (action?.kind === 'PASS_REACTION') {
      doAction(action, 1);
    }
  }, [state, aceAnimating]);

  // Drive AI turns — paused while the Ace cinematic plays. AI counters use a
  // brief delay so the panel can mount + the COUNTERED! beat detect chain
  // growth on the next render. Player reactions get unlimited time as before.
  React.useEffect(() => {
    if (aceAnimating) return;
    const isReaction = state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 1;
    if (state.phase === 'AI_TURN' || isReaction
      || (state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 1)
      || (state.phase === 'FOUR_DISCARD' && state.fourDiscarder === 1)) {
      const cached = isReaction && aiReactionDecisionRef.current?.state === state
        ? aiReactionDecisionRef.current.action : null;
      // PASS already applied synchronously by the layout effect above.
      if (isReaction && cached?.kind === 'PASS_REACTION') return;
      const delay = isReaction ? 400 : 850;
      const t = setTimeout(() => {
        const a = cached || E.aiChooseAction(state);
        if (a) doAction(a, 1);
      }, delay);
      return () => clearTimeout(t);
    }
  }, [state.phase, state.pending, state.sevenDraw, state.fourDiscarder, state.turn, aceAnimating]);

  // When entering SEVEN_DECISION as the player, auto-select the drawn card
  // with fromSeven so chooseAction routes plays through the targeting flow
  // and dispatches them wrapped as SEVEN_PLAY.
  React.useEffect(() => {
    if (state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 0) {
      const id = state.sevenDraw.card;
      setSelected(prev => (prev?.cardId === id && prev?.fromSeven) ? prev : { cardId: id, fromSeven: true });
    }
  }, [state.phase, state.sevenDraw?.card, state.sevenDraw?.player]);

  // Show callout from flash (and trigger Ace / Scuttle cinematic when applicable)
  React.useEffect(() => {
    if (!state.flash) return;
    if (state.flash.kind === 'ace') {
      setAceAnim({
        sourceCard: state.flash.sourceCard,
        sourcePlayer: state.flash.sourcePlayer,
        wipedCards: state.flash.wipedCards,
        key: Date.now(),
      });
      return;
    }
    if (state.flash.kind === 'scuttle') {
      setScuttleAnim({
        sourceCard: state.flash.cardId,
        sourcePlayer: state.flash.player,
        targetCard: state.flash.targetCardId,
        targetPlayer: 1 - state.flash.player,
        key: Date.now(),
      });
      return;
    }
    // 'counter' is owned by the reaction panel's own beat in arcade — suppress
    // the generic COUNTERED! overlay so they don't stack on top of each other.
    const txt = ({
      counter: theme === 'arcade' ? null : 'COUNTERED!',
      jack: 'JACKED!',
      win: state.flash.winner === 'player' ? 'YOU WIN' : 'CUTTLEBOT WINS',
    })[state.flash.kind];
    if (txt) {
      setCallout({ text: txt, tone: state.flash.kind === 'win' ? 'win' : 'attack' });
      setCalloutKey(k => k + 1);
      if (calloutTimerRef.current) clearTimeout(calloutTimerRef.current);
      calloutTimerRef.current = setTimeout(() => {
        setCallout(null);
        calloutTimerRef.current = null;
      }, 1100);
    }
  }, [state.flash]);

  function doAction(action, player) {
    setState(prev => E.applyAction(prev, action, player));
  }

  function reset() {
    setState(E.initialState(Math.floor(Math.random() * 1e9)));
    setSelected(null); setHint(null); setFourPicks([]); setThreePick(null);
    setAceAnim(null); setScuttleAnim(null);
  }

  return { state, doAction, selected, setSelected, hint, setHint, fourPicks, setFourPicks, threePick, setThreePick, callout, calloutKey, aceAnim, clearAceAnim: () => setAceAnim(null), scuttleAnim, clearScuttleAnim: () => setScuttleAnim(null), reset };
}

function CuttleTable({ theme = 'lagoon', layout = 'stacked', showHints = true, cardHints = true, cardStyle = 'realistic', animSpeed = 1, seed, onGameOver, onExit }) {
  const { state, doAction, selected, setSelected, hint, setHint, fourPicks, setFourPicks, threePick, setThreePick, callout, calloutKey, aceAnim, clearAceAnim, scuttleAnim, clearScuttleAnim, reset } = useGame(seed, theme);

  // Continuously snapshot every visible card's bounding rect, keyed by id. We
  // never delete entries, so when a card is scrapped its last on-screen
  // position remains available for the Ace cinematic.
  const cardRectsRef = React.useRef(new Map());
  const scrapRef = React.useRef(null);
  const aiHandRef = React.useRef(null);
  const prevStateRef = React.useRef(null);
  const [playAnims, setPlayAnims] = React.useState([]); // [{ key, cardId, fromRect, toRect, faceDown }]
  const [removalFx, setRemovalFx] = React.useState([]); // [{ kind, key, x, y }]
  const queueRemoval = React.useCallback((eff) => {
    setRemovalFx(prev => [...prev, eff]);
    setTimeout(() => setRemovalFx(prev => prev.filter(e => e.key !== eff.key)), 600);
  }, []);

  // Detect cards that just left a hand for either a field (PLAY_POINT/QUEEN/
  // KING/EIGHT) or the scrap pile (FOUR force-discards, plus the discarded 2
  // from a counter and the played one-off body) and schedule a flying-card
  // animation. Runs BEFORE the rect-snapshot effect below so it can read each
  // card's previous-render position from cardRectsRef before it gets
  // overwritten with the new destination.
  React.useLayoutEffect(() => {
    const prev = prevStateRef.current;
    prevStateRef.current = state;
    if (!prev) return;
    if (prev.aiHand === state.aiHand && prev.playerHand === state.playerHand
        && prev.aiField === state.aiField && prev.playerField === state.playerField
        && prev.scrap === state.scrap) return;

    const detected = [];
    const peeking = state.playerField.eights.length > 0 || prev.playerField.eights.length > 0;
    const prevScrapSet = new Set(prev.scrap);
    // Suppress per-card flying ghosts for cards owned by a cinematic — the
    // ScuttleFlash and AceFlash overlays animate them themselves.
    const suppressed = new Set();
    if (state.flash?.kind === 'scuttle') {
      if (state.flash.cardId !== undefined)        suppressed.add(state.flash.cardId);
      if (state.flash.targetCardId !== undefined)  suppressed.add(state.flash.targetCardId);
    }

    for (const owner of [0, 1]) {
      const prevField = owner === 0 ? prev.playerField : prev.aiField;
      const nextField = owner === 0 ? state.playerField : state.aiField;
      const prevHand  = owner === 0 ? prev.playerHand  : prev.aiHand;
      const nextHand  = owner === 0 ? state.playerHand : state.aiHand;
      const prevFieldIds = new Set([
        ...prevField.points.map(p => p.id),
        ...prevField.queens, ...prevField.kings, ...prevField.eights,
      ]);
      const nextHandSet  = new Set(nextHand);

      // Hand → field plays.
      const nextFieldEntries = [
        ...nextField.points.map(p => p.id),
        ...nextField.queens, ...nextField.kings, ...nextField.eights,
      ];
      for (const id of nextFieldEntries) {
        if (prevFieldIds.has(id)) continue;
        if (!prevHand.includes(id)) continue; // not a fresh play (e.g. jack steal)
        if (suppressed.has(id)) continue;
        const fromRect = cardRectsRef.current.get(id) || (owner === 1
          ? aiHandRef.current?.getBoundingClientRect() : null);
        const el = document.querySelector(`[data-card-id="${id}"]`);
        const toRect = el?.getBoundingClientRect();
        if (!fromRect || !toRect) continue;
        const faceDown = owner === 1 && !peeking;
        detected.push({
          key: `play-${state.turn}-${id}-${Date.now()}`,
          cardId: id,
          fromRect: { left: fromRect.left, top: fromRect.top, width: fromRect.width, height: fromRect.height },
          toRect:   { left: toRect.left,   top: toRect.top,   width: toRect.width,   height: toRect.height   },
          faceDown,
        });
      }

      // Hand → scrap (FOUR discards, counter twos, resolved/cancelled one-offs).
      for (const id of prevHand) {
        if (nextHandSet.has(id)) continue;        // still in hand
        if (prevScrapSet.has(id)) continue;        // already in scrap (shouldn't happen)
        if (!state.scrap.includes(id)) continue;   // not in scrap; went to field — handled above
        if (suppressed.has(id)) continue;

        const fromRect = cardRectsRef.current.get(id) || (owner === 1
          ? aiHandRef.current?.getBoundingClientRect() : null);
        const el = document.querySelector(`[data-card-id="${id}"]`);
        const toRect = el?.getBoundingClientRect();
        if (!fromRect || !toRect) continue;
        const faceDown = owner === 1 && !peeking;
        detected.push({
          key: `scrap-${state.turn}-${id}-${Date.now()}`,
          cardId: id,
          fromRect: { left: fromRect.left, top: fromRect.top, width: fromRect.width, height: fromRect.height },
          toRect:   { left: toRect.left,   top: toRect.top,   width: toRect.width,   height: toRect.height   },
          faceDown,
        });
      }

      // Deck → hand (FIVE-effect draws, AI auto-draws, and any other multi-card
      // draws). The single-card button-triggered draw owns its own DrawAnimCard
      // path via pendingDrawFromRef, so skip when only one card moved into the
      // player's hand and the existing draw mechanism captured the deck rect.
      const prevDeckSet = new Set(prev.deck);
      const newToHand = nextHand.filter(id => !prevHand.includes(id) && prevDeckSet.has(id));
      const isButtonSingleDraw = owner === 0 && newToHand.length === 1 && pendingDrawFromRef.current != null;
      if (newToHand.length > 0 && !isButtonSingleDraw) {
        const deckRect = deckRef.current?.getBoundingClientRect();
        const aiRect = owner === 1 ? aiHandRef.current?.getBoundingClientRect() : null;
        if (deckRect) {
          newToHand.forEach((id, i) => {
            // Player draws aim at the actual hand slot (data-card-id); AI draws
            // can't really aim at a specific slot since face-down rectangles
            // don't have stable geometry for the freshly-added card, so we send
            // them at the AI hand strip's right edge as a stand-in.
            let toRect;
            if (owner === 0) {
              const el = document.querySelector(`[data-card-id="${id}"]`);
              toRect = el?.getBoundingClientRect();
            } else if (aiRect) {
              toRect = { left: aiRect.right - 28, top: aiRect.top + 4, width: 22, height: 32 };
            }
            if (!toRect) return;
            detected.push({
              key: `draw-${state.turn}-${id}-${Date.now()}-${i}`,
              cardId: id,
              fromRect: { left: deckRect.left, top: deckRect.top, width: deckRect.width, height: deckRect.height },
              toRect:   { left: toRect.left,   top: toRect.top,   width: toRect.width,   height: toRect.height   },
              faceDown: true,
              holdFaceDown: owner === 1 && !peeking,
              delay: i * 220,
            });
          });
        }
      }
    }
    if (detected.length > 0) setPlayAnims(prev => [...prev, ...detected]);

    // Removal effects: any permanent that just left the field for the scrap.
    // Royalties only leave via Six (royalty wipe); 8s leave via Six or Ten. If
    // any royalty was wiped at the same time, treat the whole batch as a Six
    // (shatter); otherwise a lone 8 leaving = a Ten (crack). Detection is
    // diff-based, not flash-based, because the actual wipe lands on the
    // resolving PASS_REACTION (which has flash = null), not on the
    // PLAY_ONE_OFF move itself.
    const removedItems = [];
    for (const owner of [0, 1]) {
      const prevField = owner === 0 ? prev.playerField : prev.aiField;
      const nextField = owner === 0 ? state.playerField : state.aiField;
      for (const id of prevField.queens) if (!nextField.queens.includes(id)) {
        const r = cardRectsRef.current.get(id);
        if (r) removedItems.push({ id, type: 'queen', rect: r });
      }
      for (const id of prevField.kings) if (!nextField.kings.includes(id)) {
        const r = cardRectsRef.current.get(id);
        if (r) removedItems.push({ id, type: 'king', rect: r });
      }
      for (const id of prevField.eights) if (!nextField.eights.includes(id)) {
        const r = cardRectsRef.current.get(id);
        if (r) removedItems.push({ id, type: 'eight', rect: r });
      }
    }
    if (removedItems.length > 0) {
      const hasRoyalty = removedItems.some(i => i.type === 'queen' || i.type === 'king');
      for (const item of removedItems) {
        const fxKind = (hasRoyalty || item.type !== 'eight') ? 'shatter' : 'crack';
        queueRemoval({
          kind: fxKind,
          key: `${fxKind}-${state.turn}-${item.id}`,
          x: item.rect.left + item.rect.width / 2,
          y: item.rect.top + item.rect.height / 2,
        });
      }
    }
  });

  // Hide the static field-card while its flying ghost is still en route.
  // useLayoutEffect (not useEffect) so the rule lands before paint, otherwise
  // the freshly-played card flashes on the field for one frame.
  React.useLayoutEffect(() => {
    if (playAnims.length === 0) return;
    const style = document.createElement('style');
    style.textContent = playAnims
      .map(a => `[data-card-id="${a.cardId}"] { opacity: 0 !important; }`)
      .join('\n');
    document.head.appendChild(style);
    return () => style.remove();
  }, [playAnims]);

  React.useLayoutEffect(() => {
    // Don't overwrite a card's tracked rect with a scrap-pile position — the
    // Ace cinematic and other "from where the card came" animations rely on
    // the previous (hand/field) rect persisting after a card hits the scrap.
    const scrapSet = new Set(state.scrap);
    document.querySelectorAll('[data-card-id]').forEach(el => {
      const id = Number(el.dataset.cardId);
      if (!Number.isFinite(id)) return;
      if (scrapSet.has(id)) return;
      cardRectsRef.current.set(id, el.getBoundingClientRect());
    });
  });
  const fired = React.useRef(false);
  React.useEffect(() => {
    if (state.winner && !fired.current) {
      fired.current = true;
      onGameOver && onGameOver({ winner: state.winner, playerScore: E.score(state.playerField), aiScore: E.score(state.aiField), turns: state.turn, endedAt: Date.now() });
    }
    if (!state.winner) fired.current = false;
  }, [state.winner]);

  const palette = themePalette(theme);
  const myActions = state.phase === 'PLAYER_TURN' || state.phase === 'WAITING_FOR_REACTION' || state.phase === 'SEVEN_DECISION' || state.phase === 'FOUR_DISCARD' ? E.legalActions(state, 0) : [];
  const aiThinking = state.phase === 'AI_TURN' || (state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 1);

  // ── Click handler for cards in our hand
  function onHandCardClick(id) {
    if (state.phase === 'GAME_OVER') return;
    if (state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 0) {
      // Counter with this 2 if it's a 2
      if (E.cardOf(id).rank === 1) doAction({ kind: 'COUNTER_WITH_TWO', cardId: id }, 0);
      return;
    }
    if (state.phase === 'FOUR_DISCARD' && state.fourDiscarder === 0) {
      setFourPicks(prev => prev.includes(id) ? prev.filter(x => x !== id) : (prev.length < 2 ? [...prev, id] : prev));
      return;
    }
    if (state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 0) {
      // can't pick from hand during 7 — only the drawn card
      return;
    }
    if (state.phase !== 'PLAYER_TURN') return;
    setSelected(prev => prev?.cardId === id ? null : { cardId: id });
  }

  function onSevenCardClick() {
    if (state.phase !== 'SEVEN_DECISION' || state.sevenDraw?.player !== 0) return;
    const id = state.sevenDraw.card;
    setSelected(prev => prev?.cardId === id ? null : { cardId: id, fromSeven: true });
  }

  // Action menu for selected card
  const selectedCard = selected ? E.cardOf(selected.cardId) : null;
  const selectedActions = selected ? myActions.filter(a => a.cardId === selected.cardId) : [];

  // Build a target-highlight set for selected action menu
  const [actionPick, setActionPick] = React.useState(null); // {kind, cardId, needTarget?}
  React.useEffect(() => { setActionPick(null); }, [selected?.cardId]);

  const validTargets = (() => {
    if (!actionPick) return null;
    return selectedActions.filter(a => a.kind === actionPick.kind).map(a => ({ id: a.target, kind: a.targetKind }));
  })();

  const canDraw = state.phase === 'PLAYER_TURN' && state.deck.length > 0;

  // Draw animation: capture the deck rect at click time; once the new card
  // lands in the hand, fly a face-down ghost card from the deck to the new
  // hand slot and flip it face-up mid-flight.
  const deckRef = React.useRef(null);
  const pendingDrawFromRef = React.useRef(null);
  const prevHandRef = React.useRef(state.playerHand);
  const [drawAnim, setDrawAnim] = React.useState(null); // { cardId, fromRect, toRect } | null

  React.useLayoutEffect(() => {
    const prev = prevHandRef.current;
    const next = state.playerHand;
    if (pendingDrawFromRef.current && next.length > prev.length) {
      const newId = next.find(id => !prev.includes(id));
      if (newId !== undefined) {
        setDrawAnim({ cardId: newId, fromRect: pendingDrawFromRef.current, toRect: null });
      }
      pendingDrawFromRef.current = null;
    }
    prevHandRef.current = next;
  }, [state.playerHand]);

  React.useLayoutEffect(() => {
    if (drawAnim && drawAnim.cardId !== undefined && !drawAnim.toRect) {
      const el = document.querySelector(`[data-card-id="${drawAnim.cardId}"]`);
      if (el) {
        const r = el.getBoundingClientRect();
        setDrawAnim(prev => prev && prev.cardId === drawAnim.cardId ? { ...prev, toRect: r } : prev);
      }
    }
  }, [drawAnim?.cardId]);

  function triggerDraw() {
    if (!canDraw) return;
    const r = deckRef.current?.getBoundingClientRect();
    if (r) pendingDrawFromRef.current = r;
    doAction({ kind: 'DRAW' }, 0);
    setSelected(null); setActionPick(null);
  }

  function chooseAction(act) {
    if (act.kind === 'DRAW') {
      triggerDraw();
      return;
    }
    // PASS_REACTION comes from a drawer where no hand card is selected;
    // it must dispatch without going through the !selected guard below.
    if (act.kind === 'PASS_REACTION') {
      doAction(act, 0);
      setSelected(null); setActionPick(null);
      return;
    }
    if (act.kind === 'SEVEN_DISCARD') {
      doAction(act, 0);
      setSelected(null); setActionPick(null);
      return;
    }
    if (!selected) return;
    const fromSeven = !!selected.fromSeven;
    const dispatch = (a) => {
      doAction(fromSeven ? { kind: 'SEVEN_PLAY', subAction: a } : a, 0);
      setSelected(null); setActionPick(null);
    };
    if (act.kind === 'PLAY_POINT' || act.kind === 'PLAY_QUEEN' || act.kind === 'PLAY_KING' || act.kind === 'PLAY_EIGHT') {
      dispatch(act);
      return;
    }
    if (act.kind === 'PLAY_ONE_OFF' && (E.cardOf(selected.cardId).rank === 0 || E.cardOf(selected.cardId).rank === 4 || E.cardOf(selected.cardId).rank === 5 || E.cardOf(selected.cardId).rank === 6 || E.cardOf(selected.cardId).rank === 3)) {
      // No target needed
      dispatch(act);
      return;
    }
    if (act.kind === 'PLAY_ONE_OFF' && E.cardOf(selected.cardId).rank === 2) {
      // Three — pick from scrap
      setActionPick({ kind: 'PLAY_ONE_OFF', mode: 'scrap' });
      return;
    }
    // Targeted — defer dispatch until the user picks a target on the field.
    setActionPick({ kind: act.kind });
  }

  // When user picks a target on the field
  function onFieldCardClick(targetId, targetField, targetKind) {
    if (!selected || !actionPick) return;
    const wrap = (a) => selected.fromSeven ? { kind: 'SEVEN_PLAY', subAction: a } : a;
    if (actionPick.kind === 'PLAY_ONE_OFF') {
      const a = selectedActions.find(x => x.kind === 'PLAY_ONE_OFF' && x.target === targetId && x.targetKind === targetKind);
      if (a) {
        doAction(wrap(a), 0);
        setSelected(null); setActionPick(null);
      }
    } else if (actionPick.kind === 'PLAY_JACK') {
      const a = selectedActions.find(x => x.kind === 'PLAY_JACK' && x.target === targetId);
      if (a) {
        doAction(wrap(a), 0);
        setSelected(null); setActionPick(null);
      }
    } else if (actionPick.kind === 'PLAY_SCUTTLE') {
      const a = selectedActions.find(x => x.kind === 'PLAY_SCUTTLE' && x.target === targetId);
      if (a) {
        doAction(wrap(a), 0);
        setSelected(null); setActionPick(null);
      }
    }
  }

  function onScrapCardClick(id) {
    if (actionPick?.mode === 'scrap') {
      const card = E.cardOf(selected.cardId);
      const a = { kind: 'PLAY_ONE_OFF', cardId: selected.cardId, target: id, targetKind: 'scrap' };
      if (selected.fromSeven) doAction({ kind: 'SEVEN_PLAY', subAction: a }, 0);
      else doAction(a, 0);
      setSelected(null); setActionPick(null);
    }
  }

  function commitFour() {
    doAction({ kind: 'FOUR_DISCARD_DONE', cards: fourPicks }, 0);
    setFourPicks([]);
  }

  // CSS once
  React.useEffect(() => {
    const id = '__cuttle_styles';
    if (document.getElementById(id)) return;
    const s = document.createElement('style'); s.id = id;
    s.textContent = `
      @keyframes pulseGlow { 0%,100% { opacity: 0.7 } 50% { opacity: 1 } }
      @keyframes cuttleFloat { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-3px) } }
      @keyframes cuttleBob { 0%,100% { transform: translateY(0) rotate(-2deg) } 50% { transform: translateY(-4px) rotate(2deg) } }
      @keyframes cuttleShake { 0%,100% { transform: translateX(0) } 25% { transform: translateX(-2px) } 75% { transform: translateX(2px) } }
      @keyframes cuttleBounce { 0%,100% { transform: translateY(0) scale(1) } 50% { transform: translateY(-6px) scale(1.04) } }
      @keyframes pixelRingExpand { 0% { transform: scale(0.55); opacity: 0.95 } 100% { transform: scale(1.15); opacity: 0 } }
      @keyframes pixelCrackDraw { 0% { opacity: 0; clip-path: inset(0 100% 0 0) } 25% { opacity: 1; clip-path: inset(0 75% 0 0) } 50% { opacity: 1; clip-path: inset(0 50% 0 0) } 75% { opacity: 1; clip-path: inset(0 25% 0 0) } 100% { opacity: 1; clip-path: inset(0 0 0 0) } }
      @keyframes thinkingDots { 0%,20% { opacity: 0 } 50% { opacity: 1 } 100% { opacity: 0 } }
      @keyframes calloutWobble { 0%,100% { transform: rotate(-3deg) scale(1) } 50% { transform: rotate(-1deg) scale(1.04) } }
      @keyframes drawerPulse { 0%,100% { box-shadow: 0 0 0 0 rgba(243,166,130,0.6) } 50% { box-shadow: 0 0 0 8px rgba(243,166,130,0) } }
      @keyframes pulseGlasses { 0%,100% { box-shadow: 0 0 0 0 rgba(255,210,63,0.55); opacity: 1; } 50% { box-shadow: 0 0 0 4px rgba(255,210,63,0); opacity: 0.8; } }
      @keyframes reactionPanelIn { from { opacity: 0; transform: scale(0.7) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
      @keyframes reactionThreatPulse { 0%,100% { filter: drop-shadow(0 0 12px rgba(255,80,80,0.55)); transform: translateY(0); } 50% { filter: drop-shadow(0 0 22px rgba(255,80,80,0.95)); transform: translateY(-2px); } }
      @keyframes reactionUrgentBorder { 0%,100% { box-shadow: inset 0 0 0 2px rgba(255,255,255,0.04), inset 0 0 24px rgba(0,0,0,0.4), 0 0 0 3px var(--bg-deep), 0 0 0 6px #ff5050, 8px 8px 0 4px rgba(0,0,0,0.7), 0 0 30px rgba(255,80,80,0.45); } 50% { box-shadow: inset 0 0 0 2px rgba(255,255,255,0.04), inset 0 0 24px rgba(0,0,0,0.4), 0 0 0 3px var(--bg-deep), 0 0 0 6px #ff8080, 8px 8px 0 4px rgba(0,0,0,0.7), 0 0 50px rgba(255,80,80,0.85); } }
      @keyframes reactionCounterBeat { 0% { transform: scale(0.55) rotate(-6deg); opacity: 0; } 60% { transform: scale(1.12) rotate(2deg); opacity: 1; } 100% { transform: scale(1) rotate(0deg); opacity: 1; } }
      @keyframes reactionFreshTwo { 0% { transform: rotate(var(--rot)) scale(0.6); filter: drop-shadow(0 0 16px rgba(255,210,63,1)); } 50% { transform: rotate(var(--rot)) scale(1.18); filter: drop-shadow(0 0 22px rgba(255,210,63,0.95)); } 100% { transform: rotate(var(--rot)) scale(1); filter: drop-shadow(0 0 0 rgba(255,210,63,0)); } }
      @keyframes reactionPanelShake { 0%,100% { transform: translate(0,0); } 8% { transform: translate(-4px,2px); } 18% { transform: translate(5px,-2px); } 28% { transform: translate(-3px,-3px); } 38% { transform: translate(4px,2px); } 48% { transform: translate(-2px,3px); } 58% { transform: translate(3px,-2px); } 68% { transform: translate(-3px,1px); } 78% { transform: translate(2px,0); } 88% { transform: translate(-1px,1px); } }
      @keyframes reactionCounterGlitch { 0% { transform: scale(0.55) rotate(-7deg) translateX(0); opacity: 0; text-shadow: 0 0 14px rgba(255,80,80,0.95), 4px 4px 0 #1a0e08, -2px -2px 0 var(--accent-2); } 18% { transform: scale(1.22) rotate(3deg) translateX(-4px); opacity: 1; text-shadow: 5px 0 0 #ff3b8b, -5px 0 0 #00f0c8, 4px 4px 0 #1a0e08; } 30% { transform: scale(1.05) rotate(-1deg) translateX(3px); text-shadow: -3px 0 0 #ff3b8b, 3px 0 0 #00f0c8, 4px 4px 0 #1a0e08; } 45% { transform: scale(1.12) rotate(1deg) translateX(-2px); text-shadow: 2px 0 0 #ff3b8b, -2px 0 0 #00f0c8, 4px 4px 0 #1a0e08; } 65% { transform: scale(1.06) rotate(0deg) translateX(1px); text-shadow: -1px 0 0 #ff3b8b, 1px 0 0 #00f0c8, 4px 4px 0 #1a0e08; } 100% { transform: scale(1) rotate(0deg) translateX(0); opacity: 1; text-shadow: 0 0 14px rgba(255,80,80,0.95), 4px 4px 0 #1a0e08, -2px -2px 0 var(--accent-2); } }
      @keyframes reactionChainFlash { 0%,100% { filter: brightness(1); } 25% { filter: brightness(2.4) saturate(1.4) drop-shadow(0 0 18px rgba(255,210,63,1)); } 60% { filter: brightness(1.4) saturate(1.2) drop-shadow(0 0 8px rgba(255,210,63,0.6)); } }
      @keyframes reactionShockwave { 0% { transform: scale(0.4); opacity: 0.85; } 100% { transform: scale(2.4); opacity: 0; } }
    `;
    document.head.appendChild(s);
  }, []);

  // Layout switch
  const sharedProps = { state, palette, theme, cardStyle, showHints, cardHints, callout, calloutKey, aiThinking, selected, setSelected, actionPick, validTargets, hint, setHint, onHandCardClick, onSevenCardClick, onFieldCardClick, onScrapCardClick, chooseAction, selectedCard, selectedActions, fourPicks, commitFour, reset, animSpeed, deckRef, scrapRef, aiHandRef, canDraw, triggerDraw, animatingCardId: drawAnim?.cardId };
  const layoutEl = layout === 'stacked'
    ? <StackedLayout {...sharedProps} />
    : <SplitLayout {...sharedProps} />;
  return (
    <>
      {layoutEl}
      {drawAnim?.fromRect && drawAnim?.toRect && (
        <DrawAnimCard
          fromRect={drawAnim.fromRect}
          toRect={drawAnim.toRect}
          cardId={drawAnim.cardId}
          theme={theme}
          onDone={() => setDrawAnim(null)}
        />
      )}
      {playAnims.map(a => (
        <PlayAnimCard
          key={a.key}
          cardId={a.cardId}
          fromRect={a.fromRect}
          toRect={a.toRect}
          faceDown={a.faceDown}
          holdFaceDown={a.holdFaceDown}
          delay={a.delay}
          theme={theme}
          onDone={() => setPlayAnims(prev => prev.filter(x => x.key !== a.key))}
        />
      ))}
      {aceAnim && (
        <AceFlash
          key={aceAnim.key}
          spec={aceAnim}
          theme={theme}
          palette={palette}
          cardRectsRef={cardRectsRef}
          scrapRef={scrapRef}
          onDone={clearAceAnim}
        />
      )}
      {scuttleAnim && (
        <ScuttleFlash
          key={scuttleAnim.key}
          spec={scuttleAnim}
          theme={theme}
          palette={palette}
          cardRectsRef={cardRectsRef}
          scrapRef={scrapRef}
          onDone={clearScuttleAnim}
        />
      )}
      {removalFx.map(eff => (
        <div key={eff.key} style={{
          position: 'fixed', left: eff.x, top: eff.y, zIndex: 1000,
          pointerEvents: 'none', width: 0, height: 0,
        }}>
          {eff.kind === 'shatter' && <PixelShatter color="#a78bfa" />}
          {eff.kind === 'crack' && <PixelCrack color="#ffd23f" size={76} />}
        </div>
      ))}
      <SevenReveal
        state={state}
        palette={palette}
        theme={theme}
        cardStyle={cardStyle}
        selectedActions={selectedActions}
        chooseAction={chooseAction}
        actionPick={actionPick}
      />
    </>
  );
}

function themePalette(theme) {
  if (theme === 'arcade') {
    return {
      bg: 'linear-gradient(180deg, #2a1810 0%, #1a0e08 100%)',
      table: '#1a4a3a',         // deep felt green
      tableEdge: '#d4a64a',     // brass/gold trim
      ink: '#f4e6c4',           // warm cream — readable on green
      sub: '#b89a6e',           // muted brass
      accent: '#d4a64a',         // arcade gold
      accent2: '#ffd23f',        // bright yellow highlight
      panel: '#2a1810',          // warm wood panels
      panelEdge: '#6b4528',      // wood seam
      hint: '#ffd23f',
      danger: '#c83030',
      feltDark: '#0d2a22',
      feltLight: '#236a52',
      brass: '#d4a64a',
      magenta: '#ff3b8b',
    };
  }
  if (theme === 'tideline') {
    return {
      bg: 'linear-gradient(165deg, #e8f3ee 0%, #d3e8de 100%)',
      table: '#cce4d8',
      tableEdge: '#a7cdbc',
      ink: '#1f3530',
      sub: '#5e7a73',
      accent: '#3d7a6a',
      accent2: '#7fc8b6',
      panel: '#fcfaf5',
      panelEdge: '#cce4d8',
      hint: '#7fc8b6',
      danger: '#d97a73',
    };
  }
  return {
    bg: 'linear-gradient(165deg, #fbeede 0%, #f3dcc4 100%)',
    table: '#f4dcc6',
    tableEdge: '#dcb88f',
    ink: '#2c1d12',
    sub: '#7a5d49',
    accent: '#c2453d',
    accent2: '#f3a682',
    panel: '#fefdf8',
    panelEdge: '#e8d4b8',
    hint: '#f3a682',
    danger: '#c2453d',
  };
}

// ─── Deck pile (clickable when player can draw) ──────────────────────────────
const DeckPile = React.forwardRef(function DeckPile({ count, theme, palette, canDraw, onDraw, compact }, ref) {
  const [hover, setHover] = React.useState(false);
  const [pressed, setPressed] = React.useState(false);
  const isArcade = theme === 'arcade';
  const w = compact ? 64 : 88;
  const h = w * 1.4;
  const empty = count === 0;
  const interactive = canDraw && !empty;

  const backColor = isArcade ? '#7a1c44' : (theme === 'tideline' ? '#3d7a6a' : '#b35637');

  return (
    <div style={{ position: 'relative', display: 'inline-block', paddingTop: 6, paddingRight: 10 }}>
      <div
        ref={ref}
        role={interactive ? 'button' : undefined}
        aria-label={interactive ? 'Draw from deck' : undefined}
        title={interactive ? 'Click to draw' : undefined}
        onClick={interactive ? onDraw : undefined}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => { setHover(false); setPressed(false); }}
        onMouseDown={() => interactive && setPressed(true)}
        onMouseUp={() => setPressed(false)}
        style={{
          position: 'relative', width: w, height: h,
          cursor: interactive ? 'pointer' : 'default',
          transform: interactive
            ? `translateY(${pressed ? 1 : (hover ? -4 : 0)}px)`
            : 'translateY(0)',
          transition: 'transform 180ms cubic-bezier(.2,.9,.2,1)',
          opacity: empty ? 0.5 : 1,
          filter: empty ? 'grayscale(0.7)' : 'none',
        }}
      >
        {/* Stack depth — three rectangles peeking behind */}
        {[0, 1, 2].map(i => (
          <div key={i} style={{
            position: 'absolute', top: -i * 2, left: -i * 2,
            width: w, height: h,
            borderRadius: isArcade ? 0 : w * 0.10,
            background: backColor,
            border: isArcade ? '1px solid #d4a64a' : 'none',
            boxShadow: isArcade ? 'inset 0 0 0 1px #ffd23f' : '0 1px 0 rgba(255,255,255,0.18) inset',
          }} />
        ))}
        {/* Top card — face down */}
        <div style={{ position: 'absolute', top: 0, left: 0 }}>
          <PlayingCard faceDown w={w} theme={theme} glow={interactive && hover ? palette.accent : undefined} />
        </div>
        {/* Pulsing ring when drawable */}
        {interactive && (
          <div style={{
            position: 'absolute', inset: -4,
            borderRadius: isArcade ? 0 : w * 0.12,
            boxShadow: `0 0 0 0 ${palette.accent}`,
            animation: 'drawerPulse 1.8s ease-out infinite',
            pointerEvents: 'none', opacity: hover ? 0 : 0.85,
            transition: 'opacity 150ms',
          }} />
        )}
        {/* Count badge */}
        <div style={{
          position: 'absolute', top: -8, right: -10,
          minWidth: 24, padding: '2px 7px', textAlign: 'center',
          background: empty ? palette.sub : palette.accent,
          color: '#fefdf8',
          fontSize: 11, fontWeight: 800, letterSpacing: '0.04em',
          borderRadius: 999,
          fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif',
          boxShadow: '0 4px 10px -4px rgba(0,0,0,0.3)',
        }}>{count}</div>
      </div>
      {/* Hover hint */}
      {interactive && hover && (
        <div style={{
          position: 'absolute', left: w / 2, bottom: -28,
          transform: 'translateX(-50%)',
          padding: '4px 10px', borderRadius: 999,
          background: palette.ink, color: palette.panel,
          fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase',
          whiteSpace: 'nowrap', pointerEvents: 'none',
          fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif',
          boxShadow: '0 6px 14px -4px rgba(0,0,0,0.3)',
          zIndex: 5,
        }}>Click to draw</div>
      )}
    </div>
  );
});

// ─── Flying card overlay for the draw animation ──────────────────────────────
function DrawAnimCard({ fromRect, toRect, cardId, theme, onDone }) {
  const [moved, setMoved] = React.useState(false);
  const [flipped, setFlipped] = React.useState(false);

  React.useLayoutEffect(() => {
    const rid = requestAnimationFrame(() => {
      // double-rAF to make sure the initial transform is committed first
      requestAnimationFrame(() => setMoved(true));
    });
    const t1 = setTimeout(() => setFlipped(true), 260);
    const t2 = setTimeout(() => onDone && onDone(), 720);
    return () => { cancelAnimationFrame(rid); clearTimeout(t1); clearTimeout(t2); };
  }, []);

  if (!fromRect || !toRect) return null;
  const dx = toRect.left - fromRect.left;
  const dy = toRect.top - fromRect.top;
  const sx = toRect.width / fromRect.width;
  const sy = toRect.height / fromRect.height;
  const card = E.cardOf(cardId);

  return (
    <div style={{
      position: 'fixed', left: fromRect.left, top: fromRect.top,
      width: fromRect.width, height: fromRect.height,
      transformOrigin: 'top left',
      transform: moved
        ? `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`
        : 'translate(0, 0) scale(1, 1)',
      transition: 'transform 620ms cubic-bezier(.34, 1.15, .64, 1)',
      perspective: 900,
      zIndex: 1000, pointerEvents: 'none',
      filter: 'drop-shadow(0 14px 18px rgba(0,0,0,0.35))',
    }}>
      <div style={{
        position: 'relative', width: '100%', height: '100%',
        transformStyle: 'preserve-3d',
        transform: flipped ? 'rotateY(0deg)' : 'rotateY(180deg)',
        transition: 'transform 380ms cubic-bezier(.4,0,.2,1)',
      }}>
        <div style={{ position: 'absolute', inset: 0, backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}>
          <PlayingCard card={card} w={fromRect.width} theme={theme} />
        </div>
        <div style={{
          position: 'absolute', inset: 0,
          backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden',
          transform: 'rotateY(180deg)',
        }}>
          <PlayingCard faceDown w={fromRect.width} theme={theme} />
        </div>
      </div>
    </div>
  );
}

// ─── Flying card overlay for plays from a hand to a field ────────────────────
// Mirrors DrawAnimCard but starts at the played card's last hand position and
// lands on its new field slot. AI plays without Glasses are rendered face-down
// at the start and flip face-up mid-flight.
function PlayAnimCard({ cardId, fromRect, toRect, faceDown, holdFaceDown, delay = 0, theme, onDone }) {
  const [moved, setMoved] = React.useState(false);
  const [started, setStarted] = React.useState(delay === 0);
  const [revealed, setRevealed] = React.useState(!faceDown);

  React.useLayoutEffect(() => {
    let rid, kickRid, t0, t1, t2;
    function kick() {
      kickRid = requestAnimationFrame(() => {
        rid = requestAnimationFrame(() => setMoved(true));
      });
      // Reveal mid-flight unless we're meant to hold face-down (AI hand).
      if (faceDown && !holdFaceDown) {
        t1 = setTimeout(() => setRevealed(true), 220);
      }
      t2 = setTimeout(() => onDone && onDone(), 600);
    }
    if (delay === 0) { setStarted(true); kick(); }
    else { t0 = setTimeout(() => { setStarted(true); kick(); }, delay); }
    return () => {
      if (kickRid) cancelAnimationFrame(kickRid);
      if (rid) cancelAnimationFrame(rid);
      if (t0) clearTimeout(t0);
      if (t1) clearTimeout(t1);
      if (t2) clearTimeout(t2);
    };
  }, []);

  const dx = toRect.left - fromRect.left;
  const dy = toRect.top - fromRect.top;
  const sx = toRect.width / fromRect.width;
  const sy = toRect.height / fromRect.height;
  const card = E.cardOf(cardId);

  return (
    <div style={{
      position: 'fixed', left: fromRect.left, top: fromRect.top,
      width: fromRect.width, height: fromRect.height,
      transformOrigin: 'top left',
      transform: moved
        ? `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`
        : 'translate(0, 0) scale(1, 1)',
      transition: 'transform 520ms cubic-bezier(.34, 1.15, .64, 1)',
      perspective: 900,
      zIndex: 1000, pointerEvents: 'none',
      opacity: started ? 1 : 0,
      filter: 'drop-shadow(0 14px 18px rgba(0,0,0,0.35))',
    }}>
      <div style={{
        position: 'relative', width: '100%', height: '100%',
        transformStyle: 'preserve-3d',
        transform: revealed ? 'rotateY(0deg)' : 'rotateY(180deg)',
        transition: 'transform 360ms cubic-bezier(.4,0,.2,1)',
      }}>
        <div style={{ position: 'absolute', inset: 0, backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}>
          <PlayingCard card={card} w={fromRect.width} theme={theme} />
        </div>
        <div style={{
          position: 'absolute', inset: 0,
          backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden',
          transform: 'rotateY(180deg)',
        }}>
          <PlayingCard faceDown w={fromRect.width} theme={theme} />
        </div>
      </div>
    </div>
  );
}

// ─── Ace cinematic ────────────────────────────────────────────────────────────
// Beat sheet — Ace gets the same hero treatment as Scuttle:
//   t=0      mount: Ace at hand origin, wiped cards at their field positions.
//   t=80ms   Ace zooms big to center; ACED! popup pops in. (stage 1)
//   t=950ms  WIPE moment — Ace pulses, shockwave rings fire, each wiped point
//            shatters in place + flashes white. Impact shake. (stage 2)
//   t=1500ms Hold the wreckage so the player reads who/what got blown up. (stage 3)
//   t=2050ms Wiped points spin and fly to the scrap pile. (stage 4)
//   t=2700ms Ace spins and flies to the scrap pile; popup fades. (stage 5)
//   t=3200ms onDone — overlay unmounts.
function AceFlash({ spec, theme, palette, cardRectsRef, scrapRef, onDone }) {
  const [stage, setStage] = React.useState(0);
  const [rects, setRects] = React.useState(null);
  const cardW = 200;

  React.useLayoutEffect(() => {
    const vw = window.innerWidth, vh = window.innerHeight;
    const cardH = Math.round(cardW * (3.5 / 2.5));
    const center = { left: vw / 2 - cardW / 2, top: vh / 2 - cardH / 2, width: cardW, height: cardH };
    const sr = scrapRef?.current?.getBoundingClientRect();
    const scrap = sr
      ? { left: sr.left + sr.width / 2 - 18, top: sr.top + sr.height / 2 - 25, width: 36, height: 50 }
      : { left: vw - 80, top: vh / 2 - 25, width: 36, height: 50 };
    setRects({ center, scrap });
  }, []);

  React.useEffect(() => {
    const t1 = setTimeout(() => setStage(1), 80);
    const t2 = setTimeout(() => setStage(2), 950);
    const t3 = setTimeout(() => setStage(3), 1500);
    const t4 = setTimeout(() => setStage(4), 2050);
    const t5 = setTimeout(() => setStage(5), 2700);
    const t6 = setTimeout(() => onDone && onDone(), 3200);
    return () => { [t1,t2,t3,t4,t5,t6].forEach(clearTimeout); };
  }, []);

  if (!rects) return null;
  const { center, scrap } = rects;

  const aceFromRect = cardRectsRef.current.get(spec.sourceCard);
  const aceOrigin = aceFromRect || {
    left: window.innerWidth / 2 - 50,
    top: spec.sourcePlayer === 0 ? window.innerHeight - 200 : 90,
    width: 100, height: 140,
  };

  const aceRect = stage === 0 ? aceOrigin
                : stage === 5 ? scrap
                              : center;
  const aceCard = E.cardOf(spec.sourceCard);

  const aceXform = stage === 2 ? 'rotate(-4deg) scale(1.06)'
                 : stage === 3 ? 'rotate(-3deg) scale(1)'
                 : stage === 4 ? 'rotate(-2deg) scale(1)'
                 : stage === 5 ? 'rotate(720deg) scale(0.55)'
                               : 'rotate(0deg)';

  const wipedRects = spec.wipedCards.map((id, i) => {
    const r = cardRectsRef.current.get(id);
    if (r) return { id, rect: r };
    const dx = (i - spec.wipedCards.length / 2) * 90;
    return {
      id,
      rect: {
        left: window.innerWidth / 2 - 38 + dx,
        top: window.innerHeight / 2 - 50,
        width: 76, height: 106,
      },
    };
  });
  const cardsFlying = stage >= 4;

  // Pixel-style impact shake at the WIPE moment (stage 2), not just the landing.
  const overlayRef = React.useRef(null);
  React.useEffect(() => {
    if (stage !== 2 || !overlayRef.current) return;
    overlayRef.current.animate([
      { transform: 'translate(0, 0)' },
      { transform: 'translate(-3px, 2px)' },
      { transform: 'translate(3px, -2px)' },
      { transform: 'translate(-2px, 1px)' },
      { transform: 'translate(2px, 0)' },
      { transform: 'translate(0, 0)' },
    ], { duration: 280, easing: 'steps(5, end)' });
  }, [stage]);

  return (
    <div ref={overlayRef} style={{
      position: 'fixed', inset: 0, zIndex: 1100, pointerEvents: 'auto',
      background: stage === 0
        ? 'radial-gradient(ellipse at center, rgba(212,166,74,0.10) 0%, rgba(0,0,0,0.30) 70%)'
        : stage === 2
        ? 'radial-gradient(ellipse at center, rgba(255,210,63,0.42) 0%, rgba(255,59,139,0.18) 40%, rgba(0,0,0,0.85) 75%)'
        : stage <= 4
        ? 'radial-gradient(ellipse at center, rgba(255,210,63,0.22) 0%, rgba(0,0,0,0.7) 70%)'
        : 'rgba(0,0,0,0.25)',
      transition: 'background 380ms ease-out',
      overflow: 'hidden',
    }}>
      {/* CRT scanline shimmer */}
      <div style={{
        position: 'absolute', inset: 0,
        background: 'repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(255,210,63,0.06) 2px 3px)',
        mixBlendMode: 'screen', opacity: 0.7,
      }} />

      {/* Radial flash burst — softer at landing, blazes at the wipe moment. */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        width: 720, height: 720, marginLeft: -360, marginTop: -360,
        borderRadius: '50%',
        background: 'radial-gradient(circle, rgba(255,210,63,0.6) 0%, rgba(255,59,139,0.25) 35%, transparent 70%)',
        opacity: stage === 2 ? 1 : (stage === 1 || stage === 3) ? 0.7 : 0,
        transform: `scale(${stage === 2 ? 1.05 : stage >= 1 ? 1 : 0.3})`,
        transition: 'opacity 280ms ease-out, transform 360ms cubic-bezier(.34,1.5,.5,1)',
        mixBlendMode: 'screen', pointerEvents: 'none',
      }} />

      {/* Pixel-stepped shockwaves: a soft one at landing, a heavy one at the wipe. */}
      {stage === 1 && (
        <div style={{ position: 'absolute', left: '50%', top: '50%', pointerEvents: 'none' }}>
          <PixelRing key="acew-land" color="#ffd23f" size={380} thickness={2} duration={500} />
        </div>
      )}
      {stage === 2 && (
        <div style={{ position: 'absolute', left: '50%', top: '50%', pointerEvents: 'none' }}>
          <PixelRing key="acew-fire-1" color="#ffd23f" size={520} thickness={3} duration={600} />
          <PixelRing key="acew-fire-2" color="#ff3b8b" size={680} thickness={2} duration={700} />
        </div>
      )}

      {/* Ace card */}
      <div style={{
        position: 'absolute',
        left: aceRect.left, top: aceRect.top,
        width: aceRect.width, height: aceRect.height,
        transition: 'left 700ms cubic-bezier(.34,1.4,.5,1), top 700ms cubic-bezier(.34,1.4,.5,1), width 700ms cubic-bezier(.34,1.4,.5,1), height 700ms cubic-bezier(.34,1.4,.5,1), transform 380ms cubic-bezier(.34,1.7,.5,1)',
        transform: aceXform,
        filter: stage >= 1 && stage <= 4
          ? 'drop-shadow(0 0 28px rgba(255,210,63,0.9)) drop-shadow(0 0 60px rgba(212,166,74,0.7))'
          : 'drop-shadow(0 14px 18px rgba(0,0,0,0.5))',
        zIndex: 4,
      }}>
        <PlayingCard card={aceCard} w={aceRect.width} theme={theme} />
      </div>

      {/* ACED! popup */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        transform: stage === 2
          ? 'translate(-50%, calc(-50% + 195px)) scale(1.12) rotate(-3deg)'
          : stage === 1 || stage === 3 || stage === 4
          ? 'translate(-50%, calc(-50% + 195px)) scale(1) rotate(-4deg)'
          : stage === 0
          ? 'translate(-50%, calc(-50% + 195px)) scale(0.3) rotate(-14deg)'
          : 'translate(-50%, calc(-50% + 195px)) scale(0.7) rotate(10deg)',
        opacity: stage >= 1 && stage <= 4 ? 1 : 0,
        transition: 'transform 380ms cubic-bezier(.34,1.7,.5,1), opacity 280ms ease-out',
        fontFamily: '"VT323", "Courier New", monospace',
        fontSize: 168, lineHeight: 1, letterSpacing: '0.04em',
        color: '#ffd23f',
        textShadow: '0 0 24px rgba(255,210,63,0.95), 0 0 60px rgba(212,166,74,0.75), 6px 6px 0 #1a0e08, -3px -3px 0 #ff3b8b, 3px -3px 0 #5fc8b6',
        whiteSpace: 'nowrap', userSelect: 'none', zIndex: 5,
        animation: stage >= 1 && stage <= 4 ? 'aceWobble 360ms ease-in-out infinite alternate' : 'none',
      }}>ACED!</div>

      {/* Subtitle: who + how many */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        transform: 'translate(-50%, calc(-50% + 270px))',
        opacity: stage >= 1 && stage <= 4 ? 0.9 : 0,
        transition: 'opacity 320ms ease-out 120ms',
        fontFamily: '"VT323", monospace', fontSize: 26, letterSpacing: '0.3em',
        color: '#f4e6c4', textShadow: '0 0 8px rgba(255,210,63,0.6)',
        userSelect: 'none', zIndex: 5,
      }}>
        ── {spec.sourcePlayer === 0 ? 'YOU' : 'CUTTLEBOT'} WIPED {spec.wipedCards.length} POINT CARD{spec.wipedCards.length === 1 ? '' : 'S'} ──
      </div>

      {/* Wiped point cards: stay in place on stages 1-3, then fly to scrap on stage 4. */}
      {wipedRects.map(({ id, rect }, i) => {
        const delay = i * 60;
        const flying = stage >= 4;
        const dest = flying ? scrap : rect;
        // Brief white-out flash at the wipe moment.
        const flashing = stage === 2;
        return (
          <div key={id} style={{
            position: 'absolute',
            left: dest.left, top: dest.top,
            width: dest.width, height: dest.height,
            transition: `left 700ms cubic-bezier(.5,.05,.7,1) ${delay}ms, top 700ms cubic-bezier(.5,.05,.7,1) ${delay}ms, width 700ms cubic-bezier(.5,.05,.7,1) ${delay}ms, height 700ms cubic-bezier(.5,.05,.7,1) ${delay}ms, transform 700ms cubic-bezier(.5,.05,.7,1) ${delay}ms, opacity 320ms ease-out ${delay + 580}ms, filter 220ms ease-out`,
            transform: flying ? `rotate(${(i % 2 ? 1 : -1) * (200 + i * 30)}deg) scale(0.5)` : 'rotate(0deg)',
            opacity: stage === 5 ? 0 : 1,
            zIndex: 3,
            filter: flashing
              ? 'drop-shadow(0 0 18px rgba(255,255,255,0.9)) brightness(1.6) saturate(0.4)'
              : 'drop-shadow(0 6px 8px rgba(0,0,0,0.45))',
          }}>
            <PlayingCard card={E.cardOf(id)} w={dest.width} theme={theme} />
            {/* Pixel shatter chunks fly off each wiped point at the wipe moment. */}
            {stage === 2 && <PixelShatter key={`sh-${id}`} color="#ffd23f" duration={420} />}
          </div>
        );
      })}

      <style>{`@keyframes aceWobble { 0% { letter-spacing: 0.04em } 100% { letter-spacing: 0.09em } }`}</style>
    </div>
  );
}

// ─── Scuttle cinematic ────────────────────────────────────────────────────────
// Beat sheet — the player gets up close with the cards:
//   t=0      mount: both cards rendered at their last on-screen rects.
//   t=80ms   they fly to centre — scuttler on the left, victim on the right —
//            and grow large; SCUTTLED! popup pops in with overshoot.
//   t=950ms  collide: cards lurch toward each other and tilt; impact flash.
//   t=1500ms hold the wreckage for a beat so the player can read both cards.
//   t=2000ms both cards spin and fly to the scrap pile; popup fades.
//   t=2700ms onDone — overlay unmounts.
function ScuttleFlash({ spec, theme, palette, cardRectsRef, scrapRef, onDone }) {
  const [stage, setStage] = React.useState(0); // 0 origin, 1 enter, 2 collide, 3 hold, 4 exit
  const [centers, setCenters] = React.useState(null);
  const cardW = 200;
  const cardH = Math.round(cardW * (3.5 / 2.5));

  React.useLayoutEffect(() => {
    const vw = window.innerWidth, vh = window.innerHeight;
    const gap = 36;
    const top = vh / 2 - cardH / 2;
    const sourceCenter = { left: vw / 2 - cardW - gap / 2, top, width: cardW, height: cardH };
    const targetCenter = { left: vw / 2 + gap / 2,         top, width: cardW, height: cardH };
    const sr = scrapRef?.current?.getBoundingClientRect();
    const scrap = sr
      ? { left: sr.left + sr.width / 2 - 18, top: sr.top + sr.height / 2 - 25, width: 36, height: 50 }
      : { left: vw - 80, top: vh / 2 - 25, width: 36, height: 50 };
    setCenters({ sourceCenter, targetCenter, scrap });
  }, []);

  React.useEffect(() => {
    const t1 = setTimeout(() => setStage(1), 80);
    const t2 = setTimeout(() => setStage(2), 950);
    const t3 = setTimeout(() => setStage(3), 1500);
    const t4 = setTimeout(() => setStage(4), 2000);
    const t5 = setTimeout(() => onDone && onDone(), 2700);
    return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); clearTimeout(t4); clearTimeout(t5); };
  }, []);

  if (!centers) return null;
  const { sourceCenter, targetCenter, scrap } = centers;

  const sourceFromRect = cardRectsRef.current.get(spec.sourceCard);
  const targetFromRect = cardRectsRef.current.get(spec.targetCard);
  const sourceOrigin = sourceFromRect || {
    left: window.innerWidth / 2 - 50,
    top: spec.sourcePlayer === 0 ? window.innerHeight - 200 : 90,
    width: 100, height: 140,
  };
  const targetOrigin = targetFromRect || {
    left: window.innerWidth / 2 - 50,
    top: spec.targetPlayer === 0 ? window.innerHeight - 200 : 90,
    width: 100, height: 140,
  };

  // For each stage, where each card is.
  const sourceRect = stage === 0 ? sourceOrigin : stage === 4 ? scrap : sourceCenter;
  const targetRect = stage === 0 ? targetOrigin : stage === 4 ? scrap : targetCenter;

  // Tilt / lurch transforms per stage.
  const sourceXform = stage === 2
    ? 'rotate(-8deg) translate(28px, 6px)'
    : stage === 3
    ? 'rotate(-6deg) translate(20px, 4px)'
    : stage === 4
    ? 'rotate(-380deg) scale(0.6)'
    : 'rotate(0deg)';
  const targetXform = stage === 2
    ? 'rotate(10deg) translate(-26px, -4px)'
    : stage === 3
    ? 'rotate(7deg) translate(-18px, -2px)'
    : stage === 4
    ? 'rotate(420deg) scale(0.6)'
    : 'rotate(0deg)';

  const sourceCard = E.cardOf(spec.sourceCard);
  const targetCard = E.cardOf(spec.targetCard);

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 1100, pointerEvents: 'auto',
      background: stage === 0
        ? 'radial-gradient(ellipse at center, rgba(200,48,48,0.10) 0%, rgba(0,0,0,0.30) 70%)'
        : stage === 2
        ? 'radial-gradient(ellipse at center, rgba(255,80,80,0.32) 0%, rgba(0,0,0,0.85) 70%)'
        : stage <= 3
        ? 'radial-gradient(ellipse at center, rgba(255,59,139,0.20) 0%, rgba(0,0,0,0.75) 70%)'
        : 'rgba(0,0,0,0.30)',
      transition: 'background 280ms ease-out',
      overflow: 'hidden',
    }}>
      {/* CRT scanline shimmer */}
      <div style={{
        position: 'absolute', inset: 0,
        background: 'repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(255,80,80,0.06) 2px 3px)',
        mixBlendMode: 'screen', opacity: 0.7,
      }} />

      {/* Impact starburst on collide */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        width: 540, height: 540, marginLeft: -270, marginTop: -270,
        borderRadius: '50%',
        background: 'radial-gradient(circle, rgba(255,80,80,0.65) 0%, rgba(255,210,63,0.30) 35%, transparent 70%)',
        opacity: stage === 2 ? 1 : 0,
        transform: `scale(${stage === 2 ? 1 : 0.4})`,
        transition: 'opacity 240ms ease-out, transform 320ms cubic-bezier(.34,1.7,.5,1)',
        mixBlendMode: 'screen', pointerEvents: 'none',
      }} />

      {/* Scuttler card */}
      <div style={{
        position: 'absolute',
        left: sourceRect.left, top: sourceRect.top,
        width: sourceRect.width, height: sourceRect.height,
        transition: 'left 700ms cubic-bezier(.34,1.4,.5,1), top 700ms cubic-bezier(.34,1.4,.5,1), width 700ms cubic-bezier(.34,1.4,.5,1), height 700ms cubic-bezier(.34,1.4,.5,1), transform 380ms cubic-bezier(.34,1.7,.5,1)',
        transform: sourceXform,
        filter: stage >= 1 && stage <= 3
          ? 'drop-shadow(0 0 22px rgba(255,210,63,0.7)) drop-shadow(0 0 50px rgba(212,166,74,0.6))'
          : 'drop-shadow(0 14px 18px rgba(0,0,0,0.5))',
        zIndex: 4,
      }}>
        <PlayingCard card={sourceCard} w={sourceRect.width} theme={theme} />
      </div>

      {/* Victim card */}
      <div style={{
        position: 'absolute',
        left: targetRect.left, top: targetRect.top,
        width: targetRect.width, height: targetRect.height,
        transition: 'left 700ms cubic-bezier(.34,1.4,.5,1), top 700ms cubic-bezier(.34,1.4,.5,1), width 700ms cubic-bezier(.34,1.4,.5,1), height 700ms cubic-bezier(.34,1.4,.5,1), transform 380ms cubic-bezier(.34,1.7,.5,1)',
        transform: targetXform,
        filter: stage >= 2 && stage <= 3
          ? 'drop-shadow(0 0 18px rgba(200,48,48,0.85)) brightness(0.85)'
          : stage >= 1
          ? 'drop-shadow(0 14px 18px rgba(0,0,0,0.55))'
          : 'drop-shadow(0 14px 18px rgba(0,0,0,0.5))',
        zIndex: 4,
      }}>
        <PlayingCard card={targetCard} w={targetRect.width} theme={theme} />
        {stage === 2 && <PixelSlash key="slash" length={Math.round(targetRect.width * 1.7)} thickness={5} color="#ff5050" angle={-54} duration={260} />}
      </div>

      {/* SCUTTLED! popup */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        transform: stage === 2
          ? 'translate(-50%, calc(-50% + 195px)) scale(1.08) rotate(-3deg)'
          : stage === 1 || stage === 3
          ? 'translate(-50%, calc(-50% + 195px)) scale(1) rotate(-4deg)'
          : stage === 0
          ? 'translate(-50%, calc(-50% + 195px)) scale(0.3) rotate(-14deg)'
          : 'translate(-50%, calc(-50% + 195px)) scale(0.7) rotate(8deg)',
        opacity: stage === 1 || stage === 2 || stage === 3 ? 1 : 0,
        transition: 'transform 380ms cubic-bezier(.34,1.7,.5,1), opacity 280ms ease-out',
        fontFamily: '"VT323", "Courier New", monospace',
        fontSize: 168, lineHeight: 1, letterSpacing: '0.04em',
        color: '#ff5050',
        textShadow: '0 0 24px rgba(255,80,80,0.95), 0 0 60px rgba(200,48,48,0.75), 6px 6px 0 #1a0e08, -3px -3px 0 #ffd23f, 3px -3px 0 #5fc8b6',
        whiteSpace: 'nowrap', userSelect: 'none', zIndex: 5,
        animation: stage === 1 || stage === 2 || stage === 3 ? 'scuttleWobble 360ms ease-in-out infinite alternate' : 'none',
      }}>SCUTTLED!</div>

      {/* Subtitle */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        transform: 'translate(-50%, calc(-50% + 305px))',
        opacity: stage === 1 || stage === 2 || stage === 3 ? 0.9 : 0,
        transition: 'opacity 320ms ease-out 120ms',
        fontFamily: '"VT323", monospace', fontSize: 22, letterSpacing: '0.3em',
        color: '#f4e6c4', textShadow: '0 0 8px rgba(255,80,80,0.7)',
        userSelect: 'none', zIndex: 5,
      }}>
        ── {spec.sourcePlayer === 0 ? 'YOU TAKE OUT' : 'CUTTLEBOT TAKES OUT'} {pointLabel(targetCard)} ──
      </div>

      <style>{`@keyframes scuttleWobble { 0% { letter-spacing: 0.04em } 100% { letter-spacing: 0.09em } }`}</style>
    </div>
  );
}

function pointLabel(card) {
  if (!card) return '';
  // 'A' / '2'..'10' / 'J' / 'Q' / 'K' from RANK_LABELS, but we want raw rank
  // text without the suit glyph. Falling back to .label minus its trailing
  // letter is enough for our purposes.
  const lbl = card.label || '';
  return lbl.replace(/[cdhs]$/, '');
}

// ─── Card-on-field component ──────────────────────────────────────────────────
function FieldCard({ fc, w, theme, onClick, glow, dim, cardStyle, onJackClick, jackGlow, cardHints, palette }) {
  const card = E.cardOf(fc.id);
  // Top jack on the stack is the only one currently controlling the point card,
  // so it's the one that 2/9 can target. After it scraps, the next jack becomes
  // the new top and can be targeted next turn.
  const topJackId = fc.attachedJacks && fc.attachedJacks.length > 0
    ? fc.attachedJacks[fc.attachedJacks.length - 1].id
    : null;
  const jackInteractive = !!onJackClick && topJackId !== null;
  const [hover, setHover] = React.useState(null); // 'point' | 'jack' | null
  return (
    <div
      data-card-id={fc.id}
      style={{ position: 'relative' }}
      onMouseEnter={() => setHover('point')}
      onMouseLeave={() => setHover(null)}
    >
      <PlayingCard card={card} w={w} theme={theme} onClick={onClick} glow={glow} dim={dim} attachedJacks={fc.attachedJacks} compact={cardStyle === 'minimal'} />
      {jackInteractive && (
        <div
          data-card-id={topJackId}
          data-target-kind="jack"
          onClick={(e) => { e.stopPropagation(); onJackClick(topJackId); }}
          onMouseEnter={(e) => { e.stopPropagation(); setHover('jack'); }}
          onMouseLeave={(e) => { e.stopPropagation(); setHover('point'); }}
          title={fc.attachedJacks.length > 1 ? `Scrap top of ${fc.attachedJacks.length} stacked jacks` : 'Scrap this jack'}
          style={{
            position: 'absolute',
            top: w * 0.02, right: w * 0.02,
            width: w * 0.34, height: w * 0.34,
            borderRadius: '50%',
            cursor: 'pointer',
            boxShadow: jackGlow ? `0 0 0 3px ${jackGlow}, 0 0 18px ${jackGlow}` : 'none',
            animation: jackGlow ? 'pulseGlow 1.6s ease-in-out infinite' : 'none',
          }} />
      )}
      {cardHints && palette && (
        <HintTooltip
          visible={!!hover}
          text={hover === 'jack' ? cardHintText(card, 'jack') : cardHintText(card, 'point')}
          palette={palette}
          position="top"
        />
      )}
    </div>
  );
}

function FieldRoyalty({ id, w, theme, onClick, glow, dim, cardStyle }) {
  const card = E.cardOf(id);
  return (
    <div data-card-id={id} style={{ position: 'relative' }}>
      <PlayingCard card={card} w={w} theme={theme} onClick={onClick} glow={glow} dim={dim} compact={cardStyle === 'minimal'} />
    </div>
  );
}

// Wraps a permanent on the field with hover tracking + the shared HintTooltip.
function FieldPermanent({ id, role, cardHints, palette, wrapStyle, children }) {
  const [hover, setHover] = React.useState(false);
  const card = E.cardOf(id);
  return (
    <div
      style={{ position: 'relative', ...(wrapStyle || {}) }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      {children}
      {cardHints && palette && (
        <HintTooltip visible={hover} text={cardHintText(card, role)} palette={palette} position="top" />
      )}
    </div>
  );
}

// ─── Stacked layout (Direction A — Lagoon) ───────────────────────────────────
function StackedLayout(props) {
  const { state, palette, theme, cardStyle, cardHints, callout, calloutKey, aiThinking, selected, actionPick, validTargets, onHandCardClick, onSevenCardClick, onFieldCardClick, onScrapCardClick, chooseAction, selectedCard, selectedActions, fourPicks, commitFour, reset, deckRef, scrapRef, aiHandRef, canDraw, triggerDraw, animatingCardId } = props;
  const handW = 120, fieldW = 76;
  const isArcade = theme === 'arcade';

  return (
    <div style={{
      width: '100%', height: '100%', position: 'relative',
      background: palette.bg, color: palette.ink,
      fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, system-ui, sans-serif',
      display: 'grid', gridTemplateColumns: '1fr 280px', overflow: 'hidden',
    }}>
      {/* TABLE COLUMN */}
      <div style={{ position: 'relative', display: 'flex', flexDirection: 'column', padding: 24, gap: 12, minWidth: 0 }}>
        {/* AI strip */}
        <AIStrip state={state} palette={palette} theme={theme} aiThinking={aiThinking} cardStyle={cardStyle} aiHandRef={aiHandRef} />

        {/* Felt table */}
        <div style={{
          flex: 1, position: 'relative',
          background: isArcade
            ? `linear-gradient(180deg, ${palette.feltLight} 0%, ${palette.table} 50%, ${palette.feltDark} 100%)`
            : palette.table,
          border: isArcade ? `3px solid ${palette.brass}` : `1px solid ${palette.tableEdge}`,
          borderRadius: isArcade ? 0 : 28,
          boxShadow: isArcade
            ? `inset 0 0 0 2px #1a0e08, inset 0 0 0 5px ${palette.brass}, inset 0 0 80px rgba(0,0,0,0.55), 0 0 0 6px #1a0e08, 0 0 0 8px ${palette.brass}, 0 0 32px rgba(0,0,0,0.6)`
            : `inset 0 2px 0 rgba(255,255,255,0.4), inset 0 -2px 8px rgba(0,0,0,0.06)`,
          padding: 24, display: 'flex', flexDirection: 'column', gap: 14, minHeight: 0,
        }}>
          {/* faint felt texture */}
          <div style={{ position: 'absolute', inset: 0, borderRadius: isArcade ? 0 : 28, pointerEvents: 'none',
            background: isArcade
              ? `radial-gradient(ellipse 70% 55% at 50% 50%, rgba(255,210,63,0.06), transparent 70%),
                 repeating-linear-gradient(45deg, rgba(0,0,0,0.08) 0 2px, transparent 2px 6px),
                 repeating-linear-gradient(-45deg, rgba(255,255,255,0.025) 0 2px, transparent 2px 6px),
                 repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,0.18) 2px 3px)`
              : 'radial-gradient(80% 60% at 50% 0%, rgba(255,255,255,0.25), transparent 60%)' }} />
          {/* center logo watermark on the felt */}
          {isArcade && (
            <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 0 }}>
              <div style={{
                fontFamily: '"VT323", monospace', fontSize: 180, color: palette.brass,
                opacity: 0.06, letterSpacing: '0.1em', textShadow: 'none', userSelect: 'none',
              }}>CUTTLE</div>
            </div>
          )}

          {/* AI field */}
          <PlayerFieldRow
            field={state.aiField}
            owner={1}
            fieldW={fieldW}
            theme={theme}
            cardStyle={cardStyle}
            cardHints={cardHints}
            label="CUTTLEBOT'S FIELD"
            palette={palette}
            score={E.score(state.aiField)}
            target={E.pointsToWin(state.aiField)}
            onCardClick={(id, kind) => onFieldCardClick(id, 1, kind)}
            validTargets={validTargets}
            flash={state.flash}
            turn={state.turn}
          />

          {/* Center reaction zone */}
          <ReactionZone state={state} palette={palette} theme={theme} cardStyle={cardStyle} />

          {/* Player field */}
          <PlayerFieldRow
            field={state.playerField}
            owner={0}
            fieldW={fieldW}
            theme={theme}
            cardStyle={cardStyle}
            cardHints={cardHints}
            label="YOUR FIELD"
            palette={palette}
            score={E.score(state.playerField)}
            target={E.pointsToWin(state.playerField)}
            onCardClick={(id, kind) => onFieldCardClick(id, 0, kind)}
            validTargets={validTargets}
            mine
            flash={state.flash}
            turn={state.turn}
          />
        </div>

        {/* Hand */}
        <Hand
          ids={state.playerHand}
          handW={handW}
          theme={theme}
          cardStyle={cardStyle}
          cardHints={cardHints}
          palette={palette}
          state={state}
          selected={selected}
          fourPicks={fourPicks}
          onCardClick={onHandCardClick}
          animatingCardId={animatingCardId}
        />

        {/* Action drawer */}
        <ActionDrawer
          state={state}
          palette={palette}
          selected={selected}
          selectedCard={selectedCard}
          selectedActions={selectedActions}
          actionPick={actionPick}
          chooseAction={chooseAction}
          fourPicks={fourPicks}
          commitFour={commitFour}
          onPass={() => props.doAction ? null : null}
          doAction={(a) => props.chooseAction ? null : null}
          theme={theme}
        />

        <Callout text={callout?.text} tone={callout?.tone} show={!!callout} key={calloutKey} theme={theme} />
      </div>

      {/* SIDEBAR */}
      <Sidebar state={state} palette={palette} theme={theme} reset={reset} aiThinking={aiThinking} cardStyle={cardStyle} onScrapCardClick={onScrapCardClick} actionPick={actionPick} deckRef={deckRef} canDraw={canDraw} triggerDraw={triggerDraw} />
    </div>
  );
}

// ─── Split layout (Direction B — Tideline) ──────────────────────────────────
function SplitLayout(props) {
  const { state, palette, theme, cardStyle, cardHints, callout, calloutKey, aiThinking, selected, actionPick, validTargets, onHandCardClick, onFieldCardClick, onScrapCardClick, chooseAction, selectedCard, selectedActions, fourPicks, commitFour, reset, deckRef, scrapRef, aiHandRef, canDraw, triggerDraw, animatingCardId } = props;
  const handW = 114, fieldW = 70;

  return (
    <div style={{
      width: '100%', height: '100%', position: 'relative', overflow: 'hidden',
      background: palette.bg, color: palette.ink,
      fontFamily: 'Inter, system-ui, sans-serif',
      display: 'flex', flexDirection: 'column',
    }}>
      {/* AI HUD strip up top */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 16, padding: '14px 22px',
        background: palette.panel, borderBottom: `1px solid ${palette.panelEdge}`,
      }}>
        <PixelCuttlefish mood={pickAIMood(state, { aiThinking })} size={48} />
        <div style={{ display: 'flex', flexDirection: 'column', minWidth: 200 }}>
          <div style={{ fontFamily: '"Fraunces", serif', fontSize: 22, fontWeight: 600, fontStyle: 'italic' }}>Cuttlebot</div>
          <div style={{ fontSize: 12, color: palette.sub, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
            {aiThinking ? 'Thinking…' : 'Resting'} · {state.aiHand.length} cards
          </div>
        </div>
        <div style={{ flex: 1 }} />
        <ScoreReadout score={E.score(state.aiField)} target={E.pointsToWin(state.aiField)} palette={palette} label="AI SCORE" />
        <div style={{ width: 1, height: 36, background: palette.panelEdge }} />
        <ScoreReadout score={E.score(state.playerField)} target={E.pointsToWin(state.playerField)} palette={palette} label="YOUR SCORE" mine />
        <div style={{ flex: 1 }} />
        <PeekableAIHand state={state} palette={palette} theme={theme} cardW={24} cardH={34} aiHandRef={aiHandRef} />
        <button onClick={reset} style={btnStyle(palette, true)}>↻ New game</button>
      </div>

      {/* Main split */}
      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1fr 1fr', minHeight: 0 }}>
        {/* AI side */}
        <div style={{
          padding: 24, background: palette.table,
          borderRight: `1px solid ${palette.tableEdge}`,
          position: 'relative', display: 'flex', flexDirection: 'column', gap: 14, minHeight: 0,
        }}>
          <SectionLabel palette={palette}>CUTTLEBOT</SectionLabel>
          <PlayerFieldRow field={state.aiField} owner={1} fieldW={fieldW} theme={theme} cardStyle={cardStyle} cardHints={cardHints} palette={palette} variant="vertical" onCardClick={(id, kind) => onFieldCardClick(id, 1, kind)} validTargets={validTargets} flash={state.flash} turn={state.turn} />
        </div>

        {/* You side */}
        <div style={{
          padding: 24, background: palette.bg,
          position: 'relative', display: 'flex', flexDirection: 'column', gap: 14, minHeight: 0,
        }}>
          <SectionLabel palette={palette}>YOU</SectionLabel>
          <PlayerFieldRow field={state.playerField} owner={0} fieldW={fieldW} theme={theme} cardStyle={cardStyle} cardHints={cardHints} palette={palette} variant="vertical" onCardClick={(id, kind) => onFieldCardClick(id, 0, kind)} validTargets={validTargets} mine flash={state.flash} turn={state.turn} />
        </div>

        <ReactionZoneOverlay state={state} palette={palette} theme={theme} cardStyle={cardStyle} />
      </div>

      {/* Bottom — hand + drawer + log */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', borderTop: `1px solid ${palette.panelEdge}`, background: palette.panel, minHeight: 220 }}>
        <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <Hand ids={state.playerHand} handW={handW} theme={theme} cardStyle={cardStyle} cardHints={cardHints} palette={palette} state={state} selected={selected} fourPicks={fourPicks} onCardClick={onHandCardClick} compact animatingCardId={animatingCardId} />
          <ActionDrawer state={state} palette={palette} selected={selected} selectedCard={selectedCard} selectedActions={selectedActions} actionPick={actionPick} chooseAction={chooseAction} fourPicks={fourPicks} commitFour={commitFour} theme={theme} compact />
        </div>
        <div style={{ padding: 18, borderLeft: `1px solid ${palette.panelEdge}`, display: 'flex', flexDirection: 'column', gap: 8, minHeight: 0 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
            <SectionLabel palette={palette}>SCRAP · {state.scrap.length}</SectionLabel>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }}>
              <SectionLabel palette={palette}>DECK · {state.deck.length}</SectionLabel>
              <DeckPile ref={deckRef} count={state.deck.length} theme={theme} palette={palette} canDraw={canDraw} onDraw={triggerDraw} compact />
            </div>
          </div>
          <ScrapStrip scrap={state.scrap} theme={theme} cardStyle={cardStyle} actionPick={actionPick} onScrapCardClick={onScrapCardClick} scrapRef={scrapRef} />
          <LogStrip log={state.log} palette={palette} compact />
        </div>
      </div>

      <Callout text={callout?.text} tone={callout?.tone} show={!!callout} key={calloutKey} theme={theme} />
    </div>
  );
}

// ─── Sub-components ──────────────────────────────────────────────────────────

function AIStrip({ state, palette, theme, aiThinking, cardStyle, aiHandRef }) {
  const isArcade = theme === 'arcade';
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 14, padding: '12px 16px',
      background: palette.panel,
      border: isArcade ? `3px solid ${palette.brass}` : `1px solid ${palette.panelEdge}`,
      borderRadius: isArcade ? 0 : 16,
      boxShadow: isArcade ? `0 0 0 2px #1a0e08, 0 0 0 3px ${palette.brass}, 3px 3px 0 3px rgba(0,0,0,0.5)` : 'none',
    }}>
      <PixelCuttlefish mood={pickAIMood(state, { aiThinking })} size={56} />
      <div style={{ display: 'flex', flexDirection: 'column' }}>
        <div style={isArcade
          ? { fontFamily: '"VT323", monospace', fontSize: 28, color: palette.accent2, lineHeight: 1, letterSpacing: '0.1em', textShadow: `0 0 8px ${palette.accent2}` }
          : { fontFamily: '"Fraunces", serif', fontSize: 22, fontWeight: 600, fontStyle: 'italic', lineHeight: 1 }}>
          {isArcade ? 'CUTTLEBOT' : 'Cuttlebot'}
        </div>
        <div style={{ fontSize: 11, color: palette.sub, letterSpacing: '0.08em', textTransform: 'uppercase', marginTop: 4, fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif' }}>
          {aiThinking ? '> thinking…' : '> awaits your move'}
        </div>
      </div>
      <div style={{ flex: 1 }} />
      <div style={{ marginRight: 12 }}>
        <PeekableAIHand state={state} palette={palette} theme={theme} cardW={22} cardH={32} aiHandRef={aiHandRef} />
      </div>
      <ScoreReadout score={E.score(state.aiField)} target={E.pointsToWin(state.aiField)} palette={palette} label="SCORE" theme={theme} />
    </div>
  );
}

const ROLE_RING_COLORS = { queen: '#ffd23f', king: '#ff3b8b', glasses: '#00f0c8' };

// Wraps a freshly placed point card and gives it a stepped scale-pop on its
// landing turn. Heavier point plays (8-10) get a tiny pixel-dust burst alongside.
function PoppingPoint({ popKey, value, children }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (popKey == null || !ref.current) return;
    const heavy = value >= 4;
    const peak = heavy ? 1.08 : 1.05;
    ref.current.animate([
      { transform: `scale(${peak})` },
      { transform: 'scale(0.98)' },
      { transform: 'scale(1)' },
    ], { duration: 220, easing: 'steps(3, end)' });
  }, [popKey, value]);
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-block', transformOrigin: '50% 100%' }}>
      {children}
      {popKey != null && value >= 8 && <PixelDustBurst key={`d-${popKey}`} />}
    </div>
  );
}

// Tiny pixel-dust burst: 5 single-pixel dots that fan upward and fade in 4 stepped frames.
// Deterministic angles so it looks the same each time but still organic.
function PixelDustBurst({ color = '#a78bfa' }) {
  const dots = React.useMemo(() => ([
    { dx: -10, dy: -6,  delay:   0 },
    { dx:  -4, dy: -10, delay:  20 },
    { dx:   3, dy: -11, delay:  40 },
    { dx:  10, dy: -7,  delay:  60 },
    { dx:  14, dy: -2,  delay:  80 },
  ]), []);
  return (
    <div style={{ position: 'absolute', left: '50%', bottom: 0, width: 0, height: 0, pointerEvents: 'none' }}>
      {dots.map((d, i) => <PixelDustDot key={i} {...d} color={color} />)}
    </div>
  );
}
function PixelDustDot({ dx, dy, delay, color }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!ref.current) return;
    ref.current.animate([
      { transform: 'translate(-50%, 0)', opacity: 1 },
      { transform: `translate(calc(-50% + ${dx}px), ${dy}px)`, opacity: 0 },
    ], { duration: 360, easing: 'steps(4, end)', delay, fill: 'forwards' });
  }, []);
  return <div ref={ref} style={{
    position: 'absolute', left: 0, top: 0,
    width: 2, height: 2, background: color, opacity: 0,
  }} />;
}

function PlayerFieldRow({ field, owner, fieldW, theme, cardStyle, cardHints, label, palette, score, target, onCardClick, validTargets, variant, mine, flash, turn }) {
  const targetIds = validTargets ? validTargets.map(t => t.id) : null;
  const validJackIds = validTargets
    ? new Set(validTargets.filter(t => t.kind === 'jack').map(t => t.id))
    : null;

  // Field "thump" on point plays — amplitude scales with point value.
  const cardsRowRef = React.useRef(null);
  React.useEffect(() => {
    if (!cardsRowRef.current) return;
    if (!flash || flash.kind !== 'point' || flash.player !== owner) return;
    const v = flash.value || 1;
    const amp = v <= 3 ? 1 : v <= 7 ? 2 : 3;
    cardsRowRef.current.animate([
      { transform: 'translateY(0)' },
      { transform: `translateY(${-amp}px)` },
      { transform: 'translateY(0)' },
      { transform: `translateY(${Math.max(0, amp - 1)}px)` },
      { transform: 'translateY(0)' },
    ], { duration: 220, easing: 'steps(4, end)' });
  }, [turn, flash, owner]);

  // A pixel ring overlay if the just-placed permanent matches this card.
  const ringColor = (id, role) => (flash && flash.kind === role && flash.cardId === id)
    ? ROLE_RING_COLORS[role] : null;
  const ringSize = Math.round(fieldW * 1.4 + 12);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {label && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ fontSize: 11, fontFamily: 'Inter, sans-serif', letterSpacing: '0.12em', color: palette.sub, fontWeight: 600 }}>{label}</div>
          {field.queens.length > 0 && <Pill palette={palette} accent>👑 PROTECTED</Pill>}
          {field.eights.length > 0 && <Pill palette={palette} accent>GLASSES</Pill>}
          <div style={{ flex: 1 }} />
          {target != null && <ScoreReadout score={score} target={target} palette={palette} compact mine={mine} theme={theme} />}
        </div>
      )}
      <div ref={cardsRowRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, minHeight: fieldW * 1.4 + 8, alignItems: 'center' }}>
        {field.points.length === 0 && field.queens.length === 0 && field.kings.length === 0 && field.eights.length === 0 && (
          <div style={{ fontStyle: theme === 'arcade' ? 'normal' : 'italic', color: palette.sub, fontSize: 13, fontFamily: theme === 'arcade' ? '"VT323", monospace' : 'inherit', letterSpacing: theme === 'arcade' ? '0.1em' : 0 }}>{theme === 'arcade' ? '─── EMPTY ───' : 'empty field'}</div>
        )}
        {/* POINTS group (left) */}
        {field.points.length > 0 && (
          <div style={{ display: 'flex', gap: 10, alignItems: 'center', position: 'relative' }}>
            <ZoneTag palette={palette} theme={theme}>POINTS</ZoneTag>
            {field.points.map(fc => {
              const isTarget = targetIds && targetIds.includes(fc.id);
              const topJackId = fc.attachedJacks && fc.attachedJacks.length > 0
                ? fc.attachedJacks[fc.attachedJacks.length - 1].id
                : null;
              const jackTargetable = validJackIds && topJackId !== null && validJackIds.has(topJackId);
              const popPoint = flash?.kind === 'point' && flash.cardId === fc.id;
              const popJack  = flash?.kind === 'jack'  && flash.targetCardId === fc.id;
              const popping  = popPoint || popJack;
              const popValue = popPoint ? (flash.value || 1) : (popJack ? E.cardOf(fc.id).pointValue : 0);
              return (
                <PoppingPoint key={fc.id} popKey={popping ? turn : null} value={popValue}>
                  <FieldCard fc={fc} w={fieldW} theme={theme} cardStyle={cardStyle}
                    cardHints={cardHints} palette={palette}
                    onClick={isTarget ? () => onCardClick(fc.id, 'point') : undefined}
                    glow={isTarget ? palette.hint : undefined}
                    onJackClick={jackTargetable ? (jId) => onCardClick(jId, 'jack') : undefined}
                    jackGlow={jackTargetable ? palette.hint : undefined} />
                </PoppingPoint>
              );
            })}
          </div>
        )}
        {/* DIVIDER between point cards and effects */}
        {field.points.length > 0 && (field.queens.length > 0 || field.kings.length > 0 || field.eights.length > 0) && (
          <div style={{
            width: theme === 'arcade' ? 2 : 1,
            alignSelf: 'stretch',
            margin: '0 4px',
            background: theme === 'arcade'
              ? `repeating-linear-gradient(to bottom, ${palette.brass} 0 3px, transparent 3px 6px)`
              : palette.panelEdge,
            opacity: 0.55,
          }} />
        )}
        {/* EFFECTS group (right) */}
        {(field.queens.length > 0 || field.kings.length > 0 || field.eights.length > 0) && (
          <div style={{ display: 'flex', gap: 10, alignItems: 'center', position: 'relative' }}>
            <ZoneTag palette={palette} theme={theme}>EFFECTS</ZoneTag>
            {field.queens.map(id => {
              const isTarget = targetIds && targetIds.includes(id);
              const ring = ringColor(id, 'queen');
              return (
                <FieldPermanent key={id} id={id} role="queen" cardHints={cardHints} palette={palette}>
                  <FieldRoyalty id={id} w={fieldW} theme={theme} cardStyle={cardStyle} onClick={isTarget ? () => onCardClick(id, 'queen') : undefined} glow={isTarget ? palette.hint : undefined} />
                  <div style={{ position: 'absolute', top: -4, right: -4, fontSize: 16 }}>👑</div>
                  {ring && <PixelRing key={`r-${id}-${turn}`} color={ring} size={ringSize} />}
                </FieldPermanent>
              );
            })}
            {field.kings.map(id => {
              const isTarget = targetIds && targetIds.includes(id);
              const ring = ringColor(id, 'king');
              return (
                <FieldPermanent key={id} id={id} role="king" cardHints={cardHints} palette={palette}>
                  <FieldRoyalty id={id} w={fieldW} theme={theme} cardStyle={cardStyle} onClick={isTarget ? () => onCardClick(id, 'king') : undefined} glow={isTarget ? palette.hint : undefined} />
                  <div style={{ position: 'absolute', top: -4, right: -4, fontSize: 14, color: palette.accent, fontWeight: 800, background: palette.panel, padding: '0 4px', borderRadius: 4 }}>−</div>
                  {ring && <PixelRing key={`r-${id}-${turn}`} color={ring} size={ringSize} />}
                </FieldPermanent>
              );
            })}
            {field.eights.map(id => {
              const isTarget = targetIds && targetIds.includes(id);
              const card = E.cardOf(id);
              const ring = ringColor(id, 'glasses');
              // Eight played as Glasses: render the card flipped on its side, as in the physical game.
              return (
                <FieldPermanent key={id} id={id} role="glasses" cardHints={cardHints} palette={palette} wrapStyle={{
                  width: fieldW * 1.4, height: fieldW,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  <div style={{ transform: 'rotate(-90deg)' }}>
                    <PlayingCard card={card} w={fieldW} theme={theme} compact={cardStyle === 'minimal'}
                      onClick={isTarget ? () => onCardClick(id, 'eight') : undefined}
                      glow={isTarget ? palette.hint : undefined} />
                  </div>
                  {ring && <PixelRing key={`r-${id}-${turn}`} color={ring} size={ringSize} />}
                </FieldPermanent>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

function ReactionZone({ state, palette, theme, cardStyle }) {
  if (state.phase !== 'WAITING_FOR_REACTION') return null;
  if (theme === 'arcade') {
    return <ArcadeReactionZone state={state} palette={palette} theme={theme} cardStyle={cardStyle} />;
  }
  const card = E.cardOf(state.pending.sourceCard);
  const sourceWho = state.pending.sourcePlayer === 0 ? 'YOU' : 'CUTTLEBOT';
  const reactingMe = state.pending.reactingPlayer === 0;
  return (
    <div style={{
      position: 'absolute', inset: 0, pointerEvents: 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 20,
    }}>
      <div style={{
        background: 'rgba(20,12,6,0.55)', borderRadius: 20, padding: '18px 24px',
        display: 'flex', alignItems: 'center', gap: 18,
        backdropFilter: 'blur(4px)', pointerEvents: 'auto',
        boxShadow: '0 18px 50px rgba(0,0,0,0.3)',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          {state.pending.counterTwos.map((id, i) => (
            <PlayingCard key={`c${i}`} card={E.cardOf(id)} w={56} theme={theme} compact={cardStyle === 'minimal'} />
          ))}
          <div data-card-id={card.id}>
            <PlayingCard card={card} w={72} theme={theme} compact={cardStyle === 'minimal'} glow={palette.danger} />
          </div>
        </div>
        <div style={{ color: '#fefdf8', fontFamily: '"Fraunces", serif', fontStyle: 'italic' }}>
          <div style={{ fontSize: 22, fontWeight: 600 }}>{sourceWho} played {E.cardLabel ? E.cardLabel(card.id) : card.label}</div>
          <div style={{ fontSize: 13, opacity: 0.8, fontFamily: 'Inter, sans-serif', fontStyle: 'normal', letterSpacing: '0.06em', textTransform: 'uppercase', marginTop: 4 }}>
            {reactingMe ? 'Counter with a 2 or pass' : 'Cuttlebot is deciding…'}
          </div>
        </div>
      </div>
    </div>
  );
}

const REACTION_RANK_NAMES = ['ACE','TWO','THREE','FOUR','FIVE','SIX','SEVEN','EIGHT','NINE','TEN','JACK','QUEEN','KING'];

// Counter-chain escalation tiers. The "tier" is the chain length when the
// beat fires (1 = first counter, 2 = re-counter, 3+ = override).
//   - hold:        how long the splash holds before reverting to the prompt
//   - headline:    big VT323 text shown during the beat
//   - headlineSize, animation: visual ramp up per tier
//   - shake:       panel translate-jitter for the first ~420ms of the beat
//   - chainFlash:  briefly brightness/saturate-pulse the previous chain cards
//   - shockwave:   radial shockwave overlay behind the threat card
const REACTION_TIER_CONFIG = {
  1: {
    hold: 1900,
    headline: 'COUNTERED!',
    headlineSize: 56,
    headlineAnim: 'reactionCounterBeat 360ms cubic-bezier(.34,1.7,.5,1)',
    shake: false,
    chainFlash: false,
    shockwave: false,
  },
  2: {
    hold: 2800,
    headline: 'RE-COUNTERED!',
    headlineSize: 60,
    headlineAnim: 'reactionCounterGlitch 520ms cubic-bezier(.2,1.7,.5,1)',
    shake: true,
    chainFlash: true,
    shockwave: true,
  },
  3: {
    // Stub — same visuals as Tier 2 but with a bigger headline and longer
    // hold so the rare 2-2-2 still feels like a finisher.
    hold: 3200,
    headline: 'OVERRIDE!!!',
    headlineSize: 68,
    headlineAnim: 'reactionCounterGlitch 520ms cubic-bezier(.2,1.7,.5,1)',
    shake: true,
    chainFlash: true,
    shockwave: true,
  },
};
const tierForChain = (n) => (n >= 3 ? 3 : (n >= 1 ? n : 0));

function ArcadeReactionZone({ state, palette, theme, cardStyle }) {
  const card = E.cardOf(state.pending.sourceCard);
  const sourceWho = state.pending.sourcePlayer === 0 ? 'YOU' : 'CUTTLEBOT';
  const reactingMe = state.pending.reactingPlayer === 0;
  const counters = state.pending.counterTwos || [];
  const cardName = REACTION_RANK_NAMES[card.rank] || card.label;
  // Even count of stacked twos → effect resolves; odd → cancels.
  const willFire = counters.length % 2 === 0;
  const myTwos = state.playerHand.filter(id => E.cardOf(id).rank === 1);
  const hasTwo = myTwos.length > 0;

  // Counter beat: when the chain grows, hold a tier-appropriate splash
  // inside the panel before swapping to the normal prompt. Tier comes from
  // the new chain length (1 = first counter, 2 = re-counter, 3+ = override).
  const [beatTier, setBeatTier] = React.useState(0);
  const prevChainLenRef = React.useRef(counters.length);
  React.useEffect(() => {
    if (counters.length > prevChainLenRef.current) {
      const tier = tierForChain(counters.length);
      setBeatTier(tier);
      const cfg = REACTION_TIER_CONFIG[tier];
      const t = setTimeout(() => setBeatTier(0), cfg.hold);
      prevChainLenRef.current = counters.length;
      return () => clearTimeout(t);
    }
    prevChainLenRef.current = counters.length;
  }, [counters.length]);
  const beatActive = beatTier > 0;
  const beatCfg = beatActive ? REACTION_TIER_CONFIG[beatTier] : null;
  const counterer = reactingMe ? 'CUTTLEBOT' : 'YOU';

  const tagBg = reactingMe ? '#ff5050' : 'var(--accent-2)';
  const panelBoxShadow = reactingMe
    ? 'inset 0 0 0 2px rgba(255,255,255,0.04), inset 0 0 24px rgba(0,0,0,0.4), 0 0 0 3px var(--bg-deep), 0 0 0 6px #ff5050, 8px 8px 0 4px rgba(0,0,0,0.7), 0 0 30px rgba(255,80,80,0.45)'
    : 'inset 0 0 0 2px rgba(255,255,255,0.04), inset 0 0 24px rgba(0,0,0,0.4), 0 0 0 3px var(--bg-deep), 0 0 0 6px var(--accent), 8px 8px 0 4px rgba(0,0,0,0.7), 0 0 28px rgba(212,166,74,0.35)';

  return (
    <div style={{
      position: 'absolute', inset: 0, pointerEvents: 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 20,
    }}>
      {/* Soft dark aura behind panel */}
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        width: 820, height: 380, marginLeft: -410, marginTop: -190,
        background: 'radial-gradient(ellipse at center, rgba(0,0,0,0.55), transparent 70%)',
        pointerEvents: 'none',
      }} />

      {/* The panel */}
      <div style={{
        position: 'relative',
        background:
          'repeating-linear-gradient(90deg, rgba(0,0,0,0) 0 8px, rgba(0,0,0,0.08) 8px 9px),' +
          'linear-gradient(180deg, var(--bg-panel-2) 0%, var(--bg-panel) 100%)',
        padding: '24px 32px',
        boxShadow: panelBoxShadow,
        animation: [
          // Panel-in animates transform — leave it off while the shake (also
          // transform) is running so they don't fight over the property.
          beatActive && beatCfg.shake ? null : 'reactionPanelIn 320ms cubic-bezier(.34,1.56,.64,1) both',
          reactingMe && !beatActive ? 'reactionUrgentBorder 1.6s ease-in-out 320ms infinite' : null,
          beatActive && beatCfg.shake ? 'reactionPanelShake 480ms ease-in-out' : null,
        ].filter(Boolean).join(', '),
        pointerEvents: 'auto',
      }}>
        {/* Top marquee tag */}
        <div style={{
          position: 'absolute', top: -14, left: 22,
          padding: '3px 14px',
          background: tagBg, color: 'var(--bg-deep)',
          fontFamily: '"VT323", monospace', fontSize: 22, lineHeight: 1,
          letterSpacing: '0.18em', textTransform: 'uppercase', fontWeight: 700,
          boxShadow: '4px 4px 0 var(--bg-deep)',
        }}>
          ◆ {reactingMe ? 'INCOMING — REACT' : 'REACTION WINDOW'}
        </div>

        <div style={{ display: 'flex', alignItems: 'center', gap: 22 }}>
          {/* Counter chain */}
          {counters.length > 0 && (
            <>
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
                <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 10, color: 'var(--ink-dim)', letterSpacing: '0.2em', fontWeight: 700 }}>
                  CHAIN · {String(counters.length).padStart(2, '0')}
                </div>
                <div style={{ position: 'relative', height: 78, width: 60 + counters.length * 14 }}>
                  {counters.map((id, i) => {
                    const rot = (i - (counters.length - 1) / 2) * 5;
                    const isLatest = beatActive && i === counters.length - 1;
                    const isOlder = beatActive && beatCfg.chainFlash && i !== counters.length - 1;
                    return (
                      <div key={`c${i}`} style={{
                        position: 'absolute',
                        left: i * 14,
                        top: 4,
                        transform: `rotate(${rot}deg)`,
                        transformOrigin: 'bottom center',
                        zIndex: isLatest ? 10 : i,
                        ...(isLatest ? {
                          '--rot': `${rot}deg`,
                          animation: 'reactionFreshTwo 1100ms ease-out',
                        } : isOlder ? {
                          animation: 'reactionChainFlash 700ms ease-out',
                        } : null),
                      }}>
                        <PlayingCard card={E.cardOf(id)} w={50} theme={theme} compact={cardStyle === 'minimal'} />
                      </div>
                    );
                  })}
                  {/* Shockwave behind the chain, fired with Tier 2+ beats */}
                  {beatActive && beatCfg.shockwave && (
                    <div style={{
                      position: 'absolute',
                      left: '50%', top: '50%',
                      width: 80, height: 80, marginLeft: -40, marginTop: -40,
                      borderRadius: '50%',
                      border: '3px solid rgba(255,210,63,0.85)',
                      boxShadow: '0 0 24px 4px rgba(255,210,63,0.55), inset 0 0 16px rgba(255,80,80,0.45)',
                      animation: 'reactionShockwave 620ms ease-out',
                      pointerEvents: 'none',
                      zIndex: 1,
                    }} />
                  )}
                </div>
              </div>
              <div style={{ fontFamily: '"VT323", monospace', fontSize: 28, color: 'var(--accent-2)', textShadow: '0 0 8px var(--accent-2)' }}>
                ▶
              </div>
            </>
          )}

          {/* Threat card */}
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
            <div style={{
              fontFamily: '"JetBrains Mono", monospace', fontSize: 10,
              color: '#ff8080', letterSpacing: '0.2em', fontWeight: 700,
              textShadow: '0 0 6px rgba(255,80,80,0.6)',
            }}>
              ▸ THREAT
            </div>
            <div data-card-id={card.id} style={{ animation: 'reactionThreatPulse 1.4s ease-in-out infinite' }}>
              <PlayingCard card={card} w={92} theme={theme} compact={cardStyle === 'minimal'} glow={palette.danger} />
            </div>
          </div>

          {/* Prompt */}
          <div style={{ display: 'flex', flexDirection: 'column', minWidth: 260, gap: 6, marginLeft: 4 }}>
            <div style={{
              fontFamily: '"JetBrains Mono", monospace', fontSize: 11,
              color: 'var(--ink-dim)', letterSpacing: '0.18em', textTransform: 'uppercase',
            }}>
              {sourceWho} {sourceWho === 'YOU' ? 'PLAY' : 'PLAYS'} THE {cardName}
            </div>

            {beatActive ? (
              <>
                <div
                  key={`beat-${counters.length}`}
                  style={{
                    fontFamily: '"VT323", monospace',
                    fontSize: beatCfg.headlineSize, lineHeight: 1,
                    color: '#ff5050', letterSpacing: '0.04em',
                    textShadow: '0 0 14px rgba(255,80,80,0.95), 4px 4px 0 #1a0e08, -2px -2px 0 var(--accent-2)',
                    animation: beatCfg.headlineAnim,
                  }}
                >
                  {beatCfg.headline}
                </div>
                <div style={{
                  fontFamily: '"JetBrains Mono", monospace', fontSize: 13,
                  color: 'var(--ink)', lineHeight: 1.6,
                }}>
                  <b style={{ color: 'var(--accent-2)' }}>{counterer}</b> chained a <b style={{ color: 'var(--accent-2)' }}>2</b>.
                  {' '}
                  {willFire
                    ? <>The effect <b style={{ color: '#ff8080' }}>FIRES</b> if no one re-counters.</>
                    : <>The effect <b style={{ color: '#7fbf95' }}>FIZZLES</b> if no one re-counters.</>}
                </div>
              </>
            ) : reactingMe ? (
              <>
                <div style={{
                  fontFamily: '"VT323", monospace', fontSize: 38, lineHeight: 1,
                  color: '#ff5050', letterSpacing: '0.04em',
                  textShadow: '0 0 10px rgba(255,80,80,0.85), 3px 3px 0 #1a0e08',
                }}>
                  YOUR REACTION!
                </div>
                <div style={{
                  fontFamily: '"JetBrains Mono", monospace', fontSize: 13,
                  color: 'var(--ink)', lineHeight: 1.6,
                }}>
                  {hasTwo
                    ? <>Click a <b style={{ color: 'var(--accent-2)' }}>2</b> in your hand to counter,<br/>or hit <b style={{ color: 'var(--accent-2)' }}>PASS</b> to let it resolve.</>
                    : <>No <b style={{ color: 'var(--accent-2)' }}>2</b>s in hand — only <b style={{ color: 'var(--accent-2)' }}>PASS</b> is available.</>}
                </div>
              </>
            ) : (
              <>
                <div style={{
                  fontFamily: '"VT323", monospace', fontSize: 38, lineHeight: 1,
                  color: 'var(--accent-2)', letterSpacing: '0.04em',
                  textShadow: '0 0 10px var(--accent-2), 3px 3px 0 #1a0e08',
                }}>
                  CUTTLEBOT
                  <span style={{ display: 'inline-block', minWidth: '1.2em', marginLeft: 6 }}>
                    <span style={{ animation: 'thinkingDots 1.1s ease-in-out infinite' }}>.</span>
                    <span style={{ animation: 'thinkingDots 1.1s ease-in-out 180ms infinite' }}>.</span>
                    <span style={{ animation: 'thinkingDots 1.1s ease-in-out 360ms infinite' }}>.</span>
                  </span>
                </div>
                <div style={{
                  fontFamily: '"JetBrains Mono", monospace', fontSize: 13,
                  color: 'var(--ink-dim)', lineHeight: 1.6,
                }}>
                  Considering whether to counter the {cardName.toLowerCase()}.
                </div>
              </>
            )}

            {/* Parity hint — what happens if everyone passes from here */}
            <div style={{
              marginTop: 4, padding: '4px 10px',
              background: willFire ? 'rgba(200,48,48,0.18)' : 'rgba(58,138,92,0.18)',
              border: '1px solid ' + (willFire ? 'rgba(200,48,48,0.55)' : 'rgba(58,138,92,0.55)'),
              fontFamily: '"JetBrains Mono", monospace', fontSize: 11,
              letterSpacing: '0.12em', textTransform: 'uppercase', fontWeight: 700,
              color: willFire ? '#ff8080' : '#7fbf95',
              alignSelf: 'flex-start',
            }}>
              {willFire ? '▸ AS-IS · EFFECT FIRES' : '▸ AS-IS · EFFECT FIZZLES'}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function ReactionZoneOverlay(props) { return <ReactionZone {...props} />; }

// ─── SEVEN reveal — flips the deck-drawn card and surfaces the play options ──
function SevenReveal({ state, palette, theme, cardStyle, selectedActions, chooseAction, actionPick }) {
  const isMine = state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 0;
  const isAI = state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 1;
  const cardId = (isMine || isAI) ? state.sevenDraw.card : null;

  const [flipped, setFlipped] = React.useState(false);
  const [shown, setShown] = React.useState(false);

  React.useEffect(() => {
    if (cardId == null) { setShown(false); setFlipped(false); return; }
    setFlipped(false); setShown(false);
    const r = requestAnimationFrame(() => requestAnimationFrame(() => setShown(true)));
    const t = setTimeout(() => setFlipped(true), 280);
    return () => { cancelAnimationFrame(r); clearTimeout(t); };
  }, [cardId]);

  if (cardId == null) return null;
  if (isMine && actionPick) return null;

  const card = E.cardOf(cardId);
  const isArcade = theme === 'arcade';
  const cardW = 132;
  const opts = isMine ? describeOptions(card, selectedActions, state) : [];

  const titleStyle = isArcade
    ? { fontFamily: '"VT323", monospace', fontSize: 30, color: palette.accent2, letterSpacing: '0.18em', textShadow: `0 0 8px ${palette.accent2}`, lineHeight: 1 }
    : { fontFamily: '"Fraunces", serif', fontSize: 22, fontStyle: 'italic', color: palette.ink, fontWeight: 600, lineHeight: 1 };
  const subtitleStyle = {
    fontSize: isArcade ? 14 : 12, color: palette.sub, letterSpacing: '0.14em',
    textTransform: 'uppercase', textAlign: 'center',
    fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif',
  };

  return (
    <div data-testid="seven-reveal" style={{
      position: 'fixed', inset: 0, zIndex: 60,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: isArcade ? 'rgba(10,14,26,0.62)' : 'rgba(20,12,6,0.42)',
      backdropFilter: 'blur(6px)',
      WebkitBackdropFilter: 'blur(6px)',
    }}>
      <div style={{
        position: 'relative',
        display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16,
        padding: isArcade ? '26px 34px 28px' : '22px 28px',
        background: isArcade ? '#1c2440' : palette.panel,
        border: isArcade ? `3px solid ${palette.brass}` : `1px solid ${palette.panelEdge}`,
        borderRadius: isArcade ? 0 : 18,
        boxShadow: isArcade
          ? `0 0 0 2px #0a0e1a, 0 0 0 4px ${palette.brass}, 0 0 32px rgba(255,210,63,0.4), 0 24px 60px rgba(0,0,0,0.6)`
          : '0 26px 70px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.6)',
        opacity: shown ? 1 : 0,
        transform: shown ? 'scale(1)' : 'scale(0.86)',
        transition: 'opacity 200ms ease-out, transform 320ms cubic-bezier(.34,1.4,.64,1)',
        overflow: 'hidden',
      }}>
        {isArcade && (
          <div style={{
            position: 'absolute', inset: 0, pointerEvents: 'none',
            background: 'repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,0.18) 2px 3px)',
            mixBlendMode: 'multiply',
          }} />
        )}

        <div style={titleStyle}>
          {isArcade ? '> SEVEN · TOP OF DECK <' : (isMine ? 'You flipped the top card' : 'Cuttlebot flipped the top card')}
        </div>

        <div style={{
          width: cardW, height: cardW * 1.4,
          perspective: 1200,
          filter: `drop-shadow(0 18px 28px rgba(0,0,0,0.4))${isArcade ? ` drop-shadow(0 0 14px ${palette.accent2}aa)` : ''}`,
        }}>
          <div style={{
            position: 'relative', width: '100%', height: '100%',
            transformStyle: 'preserve-3d',
            transform: flipped ? 'rotateY(0deg)' : 'rotateY(180deg)',
            transition: 'transform 540ms cubic-bezier(.34,1.15,.64,1)',
          }}>
            <div style={{ position: 'absolute', inset: 0, backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}>
              <PlayingCard card={card} w={cardW} theme={theme} compact={cardStyle === 'minimal'} />
            </div>
            <div style={{ position: 'absolute', inset: 0, backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}>
              <PlayingCard faceDown w={cardW} theme={theme} />
            </div>
          </div>
        </div>

        <div style={subtitleStyle}>
          {isMine
            ? (isArcade ? '// PLAY OR SCRAP //' : 'Play it now or scrap it')
            : (isArcade ? '// CUTTLEBOT IS DECIDING //' : 'Cuttlebot is deciding…')}
        </div>

        {isMine && (
          <div style={{
            display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center',
            maxWidth: cardW * 4 + 40,
            opacity: flipped ? 1 : 0,
            transform: flipped ? 'translateY(0)' : 'translateY(8px)',
            transition: 'opacity 240ms ease-out 220ms, transform 240ms ease-out 220ms',
          }}>
            {opts.map((o, i) => (
              <button key={i} onClick={() => chooseAction(o.action)} style={btnStyle(palette, o.primary)}>{o.label}</button>
            ))}
            <button onClick={() => chooseAction({ kind: 'SEVEN_DISCARD' })} style={btnStyle(palette)}>Scrap it</button>
          </div>
        )}
      </div>
    </div>
  );
}

function Hand({ ids, handW, theme, cardStyle, cardHints, palette, state, selected, fourPicks, onCardClick, compact, animatingCardId }) {
  const reactingMe = state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 0;
  const fourMode = state.phase === 'FOUR_DISCARD' && state.fourDiscarder === 0;
  const sevenMine = state.phase === 'SEVEN_DECISION' && state.sevenDraw?.player === 0;
  const myActions = (state.phase === 'PLAYER_TURN') ? E.legalActions(state, 0) : [];
  const playableIds = new Set(myActions.filter(a => a.cardId !== undefined).map(a => a.cardId));

  const [hoverId, setHoverId] = React.useState(null);

  const total = ids.length;
  const fanSpread = Math.min(40, 8 * total);
  // Lighter overlap so each card reads as its own object while still feeling like a held hand.
  const overlap = handW * 0.18;
  return (
    <div data-testid="player-hand" style={{
      position: 'relative', height: handW * 1.7, display: 'flex',
      alignItems: 'flex-end', justifyContent: 'center', padding: '0 12px',
    }}>
      {ids.map((id, i) => {
        const c = E.cardOf(id);
        const angle = total === 1 ? 0 : (i - (total - 1) / 2) * (fanSpread / Math.max(1, total - 1));
        const isSelected = selected?.cardId === id;
        const isPicked = fourPicks.includes(id);
        const isHovered = hoverId === id;
        const dim = (state.phase === 'PLAYER_TURN' && !playableIds.has(id))
          || (reactingMe && c.rank !== 1)
          || (sevenMine);
        const baseTransform = `rotate(${angle}deg) translateY(${Math.abs(angle) * 0.4}px)`;
        // Hover lifts and straightens the card so the player can read it cleanly.
        const hoverTransform = `translateY(-10px) rotate(${angle * 0.25}deg)`;
        return (
          <div
            key={id}
            data-testid="hand-card"
            data-card-id={id}
            onMouseEnter={() => setHoverId(id)}
            onMouseLeave={() => setHoverId(prev => (prev === id ? null : prev))}
            style={{
              transform: isHovered ? hoverTransform : baseTransform,
              margin: `0 ${-overlap}px`,
              transition: 'transform 200ms cubic-bezier(.2,.9,.2,1), opacity 200ms ease-out',
              position: 'relative',
              opacity: animatingCardId === id ? 0 : 1,
              zIndex: isHovered ? 30 : (isSelected || isPicked ? 20 : i),
            }}
          >
            <PlayingCard
              card={c} w={handW} theme={theme}
              selected={isSelected || isPicked}
              dim={dim && !isHovered}
              compact={cardStyle === 'minimal'}
              onClick={() => onCardClick(id)}
            />
            {cardHints && <HintTooltip visible={isHovered} text={cardHintText(c)} palette={palette} position="top" />}
          </div>
        );
      })}
    </div>
  );
}

function ActionDrawer({ state, palette, selected, selectedCard, selectedActions, actionPick, chooseAction, fourPicks, commitFour, theme, compact }) {
  // Reaction prompt overrides
  if (state.phase === 'WAITING_FOR_REACTION' && state.pending?.reactingPlayer === 0) {
    const twos = state.playerHand.filter(id => E.cardOf(id).rank === 1);
    return (
      <Drawer palette={palette}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: palette.sub, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Reaction</span>
          {twos.length === 0 ? (
            <span style={{ fontSize: 14, fontStyle: 'italic', color: palette.sub }}>No Twos in hand</span>
          ) : (
            <span style={{ fontSize: 14, color: palette.ink }}>Click a 2 in your hand to counter, or pass.</span>
          )}
          <div style={{ flex: 1 }} />
          <button onClick={() => chooseAction({ kind: 'PASS_REACTION' })} style={btnStyle(palette)}>Pass</button>
        </div>
      </Drawer>
    );
  }
  if (state.phase === 'FOUR_DISCARD' && state.fourDiscarder === 0) {
    const target = Math.min(2, state.playerHand.length);
    const ready = fourPicks.length >= target;
    const remaining = Math.max(0, target - fourPicks.length);
    return (
      <Drawer palette={palette}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: palette.sub, letterSpacing: '0.1em', textTransform: 'uppercase' }}>FOUR</span>
          <span style={{ fontSize: 14 }}>
            {ready ? 'Confirm to scrap selected cards.' : `Pick ${remaining} more card${remaining === 1 ? '' : 's'} to scrap`}
          </span>
          <div style={{ flex: 1 }} />
          {ready && <button onClick={commitFour} style={btnStyle(palette, true)}>Confirm Scrap</button>}
        </div>
      </Drawer>
    );
  }
  if (actionPick) {
    return (
      <Drawer palette={palette}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: palette.sub, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Pick a target</span>
          <span style={{ fontSize: 14 }}>{actionPickLabel(actionPick, selectedCard)}</span>
        </div>
      </Drawer>
    );
  }
  if (!selected) {
    return (
      <Drawer palette={palette}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: palette.sub, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Your turn</span>
          <span style={{ fontSize: 14 }}>{state.phase === 'PLAYER_TURN' ? 'Click a card to play it' : 'Waiting…'}</span>
          <div style={{ flex: 1 }} />
          {state.phase === 'PLAYER_TURN' && state.deck.length > 0 && (
            <button onClick={() => chooseAction({ kind: 'DRAW' })} style={btnStyle(palette, true)}>Draw a card</button>
          )}
        </div>
      </Drawer>
    );
  }
  // Card selected — show action chips
  const c = selectedCard;
  const opts = describeOptions(c, selectedActions, state);
  return (
    <Drawer palette={palette}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <span style={{ fontFamily: '"Fraunces", serif', fontSize: 18, fontStyle: 'italic', fontWeight: 600 }}>
          {c.label}{SUIT_GLYPHS[c.suit]}
        </span>
        <span style={{ fontSize: 12, color: palette.sub, letterSpacing: '0.08em', textTransform: 'uppercase' }}>{cardRoleLabel(c)}</span>
        <div style={{ flex: 1 }} />
        {opts.map((o, i) => (
          <button key={i} onClick={() => chooseAction(o.action)} style={btnStyle(palette, o.primary)}>{o.label}</button>
        ))}
        {opts.length === 0 && <span style={{ fontSize: 13, color: palette.sub, fontStyle: 'italic' }}>No legal play with this card</span>}
      </div>
    </Drawer>
  );
}

function describeOptions(c, acts, state) {
  // Group selectedActions by kind to produce single buttons (target picking happens after).
  const out = [];
  if (acts.some(a => a.kind === 'PLAY_POINT')) {
    out.push({ label: `Play as point (+${c.pointValue})`, primary: true, action: { kind: 'PLAY_POINT', cardId: c.id } });
  }
  if (acts.some(a => a.kind === 'PLAY_QUEEN')) {
    out.push({ label: 'Play 👑 Queen', primary: true, action: { kind: 'PLAY_QUEEN', cardId: c.id } });
  }
  if (acts.some(a => a.kind === 'PLAY_KING')) {
    out.push({ label: 'Play King — Lower Target', primary: true, action: { kind: 'PLAY_KING', cardId: c.id } });
  }
  if (acts.some(a => a.kind === 'PLAY_JACK')) {
    out.push({ label: 'Jack — steal a point card', primary: true, action: { kind: 'PLAY_JACK', cardId: c.id } });
  }
  if (acts.some(a => a.kind === 'PLAY_ONE_OFF')) {
    out.push({ label: oneOffLabel(c.rank), action: { kind: 'PLAY_ONE_OFF', cardId: c.id } });
  }
  // Special: Eight — needs a manual button since legalActions doesn't currently emit PLAY_EIGHT
  if (c.rank === 7 && (state.phase === 'PLAYER_TURN' || state.phase === 'SEVEN_DECISION')) {
    out.push({ label: 'Play as Glasses', primary: true, action: { kind: 'PLAY_EIGHT', cardId: c.id } });
  }
  if (acts.some(a => a.kind === 'PLAY_SCUTTLE')) {
    out.push({ label: 'Scuttle', action: { kind: 'PLAY_SCUTTLE', cardId: c.id } });
  }
  return out;
}

function oneOffLabel(rank) {
  return ({
    0: 'ACE — wipe all points 💥',
    1: 'TWO — scrap a permanent',
    2: 'THREE — pull from scrap',
    3: 'FOUR — opp scraps 2',
    4: 'FIVE — draw 2',
    5: 'SIX — wipe permanents',
    6: 'SEVEN — flip top card',
    8: 'NINE — bounce a permanent',
  })[rank] || 'Use as one-off';
}
function cardRoleLabel(c) {
  if (c.rank <= 9) return `point ${c.pointValue}`;
  if (c.rank === 10) return 'Jack';
  if (c.rank === 11) return 'Queen';
  return 'King';
}

// Concise one-line hover hints. `role` narrows context for permanents already
// in play; defaults to the card's hand-role description.
function cardHintText(c, role) {
  if (role === 'queen') return 'QUEEN — protects this field\'s other cards from targeting';
  if (role === 'king') return 'KING — lowers controller\'s win threshold (each one shrinks it)';
  if (role === 'glasses') return 'GLASSES (8) — controller sees the opponent\'s hand';
  if (role === 'jack') return 'JACK — controls the point card it\'s attached to';
  if (role === 'point') return `Point card — worth ${c.pointValue}`;
  // Hand role
  switch (c.rank) {
    case 0:  return 'ACE — one-off: scrap all point cards on both fields';
    case 1:  return 'TWO — counter a one-off, or scrap a permanent';
    case 2:  return 'THREE — pull any card from the scrap pile';
    case 3:  return 'FOUR — opponent scraps two cards from hand';
    case 4:  return 'FIVE — draw two cards';
    case 5:  return 'SIX — wipe all Queens, Kings, and Glasses on both fields';
    case 6:  return 'SEVEN — flip the deck\'s top card and play or scrap it';
    case 7:  return 'EIGHT — play as Glasses to see the opponent\'s hand';
    case 8:  return 'NINE — bounce a permanent back to its owner\'s hand';
    case 9:  return 'TEN — point only';
    case 10: return 'JACK — steal an unprotected point card from the opponent';
    case 11: return 'QUEEN — play to your field to protect your other cards';
    case 12: return 'KING — play to your field to lower your win threshold';
    default: return '';
  }
}

// Hover-with-delay tooltip. `visible` is the parent's hover state; this
// component delays showing so passive mouse movement doesn't trigger it.
function HintTooltip({ visible, text, palette, position = 'top' }) {
  const [show, setShow] = React.useState(false);
  React.useEffect(() => {
    if (!visible) { setShow(false); return; }
    const t = setTimeout(() => setShow(true), 380);
    return () => clearTimeout(t);
  }, [visible]);
  if (!text) return null;
  const isArcade = palette.brass === '#d4a64a';
  const placementStyle = position === 'top'
    ? { bottom: '100%', marginBottom: 8 }
    : { top: '100%', marginTop: 8 };
  return (
    <div
      aria-hidden
      style={{
        position: 'absolute',
        left: '50%',
        transform: `translateX(-50%) translateY(${show ? 0 : (position === 'top' ? 4 : -4)}px)`,
        ...placementStyle,
        opacity: show ? 1 : 0,
        transition: 'opacity 140ms ease-out, transform 140ms ease-out',
        pointerEvents: 'none',
        zIndex: 50,
        whiteSpace: 'nowrap',
        background: isArcade ? 'rgba(8,4,2,0.92)' : 'rgba(20,24,32,0.92)',
        color: isArcade ? '#ffd23f' : '#e8edf3',
        border: isArcade ? '1px solid #d4a64a' : '1px solid rgba(255,255,255,0.12)',
        boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
        fontFamily: isArcade ? '"VT323", monospace' : 'Inter, system-ui, sans-serif',
        fontSize: isArcade ? 16 : 12,
        letterSpacing: isArcade ? '0.06em' : '0.02em',
        padding: isArcade ? '4px 10px 5px' : '5px 9px',
        borderRadius: isArcade ? 0 : 6,
      }}
    >
      {text}
    </div>
  );
}
function actionPickLabel(actionPick, card) {
  if (actionPick.mode === 'scrap') return 'Click a card in the SCRAP pile to retrieve';
  if (actionPick.kind === 'PLAY_JACK') return 'Click an unprotected point card on Cuttlebot\'s field';
  if (actionPick.kind === 'PLAY_SCUTTLE') return `Click a point card on Cuttlebot's field worth less than ${card.label}`;
  if (actionPick.kind === 'PLAY_ONE_OFF') {
    if (card.rank === 1) return 'Click a permanent (Queen, King, Glasses, or Jack) on Cuttlebot\'s field';
    if (card.rank === 8) return 'Click a permanent effect card on either field to bounce it home';
  }
  return 'Pick a target';
}

function Drawer({ palette, children }) {
  return (
    <div style={{
      background: palette.panel, border: `1px solid ${palette.panelEdge}`,
      borderRadius: 14, padding: '12px 16px',
      boxShadow: '0 6px 16px -8px rgba(0,0,0,0.15)',
    }}>{children}</div>
  );
}

function btnStyle(palette, primary) {
  const isArcade = palette.brass === '#d4a64a';
  if (isArcade) {
    return {
      padding: '8px 16px 9px',
      border: 0,
      cursor: 'pointer',
      fontFamily: '"VT323", monospace',
      fontSize: 22,
      letterSpacing: '0.08em',
      textTransform: 'uppercase',
      background: primary ? palette.accent : '#3a2418',
      color: primary ? '#1a0e08' : palette.ink,
      boxShadow: primary
        ? `0 0 0 2px #1a0e08, 0 0 0 3px ${palette.accent}, 3px 3px 0 3px rgba(0,0,0,0.6), 0 0 16px rgba(212,166,74,0.4)`
        : `0 0 0 2px #1a0e08, 0 0 0 3px ${palette.accent}, 3px 3px 0 3px rgba(0,0,0,0.5)`,
      transition: 'transform 120ms',
    };
  }
  return {
    padding: '8px 14px', borderRadius: 999, border: 'none', cursor: 'pointer',
    fontFamily: 'Inter, sans-serif', fontSize: 13, fontWeight: 600,
    background: primary ? palette.accent : palette.panel,
    color: primary ? '#fefdf8' : palette.ink,
    boxShadow: primary ? '0 4px 0 rgba(0,0,0,0.1)' : `inset 0 0 0 1px ${palette.panelEdge}`,
    letterSpacing: '0.02em',
    transition: 'transform 120ms',
  };
}

function Sidebar({ state, palette, theme, reset, aiThinking, cardStyle, onScrapCardClick, actionPick, deckRef, canDraw, triggerDraw }) {
  const isArcade = theme === 'arcade';
  return (
    <div style={{
      borderLeft: isArcade ? `2px solid ${palette.accent}` : `1px solid ${palette.panelEdge}`,
      background: palette.panel,
      padding: 18, display: 'flex', flexDirection: 'column', gap: 14, minHeight: 0,
      boxShadow: isArcade ? `inset 4px 0 0 #1a0e08, inset 0 0 40px rgba(0,0,0,0.4)` : 'none',
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
        <div style={isArcade
          ? { fontFamily: '"VT323", monospace', fontSize: 38, color: palette.accent, lineHeight: 1, textShadow: `0 0 8px ${palette.accent}` }
          : { fontFamily: '"Fraunces", serif', fontSize: 28, fontStyle: 'italic', fontWeight: 600 }}>
          {isArcade ? 'CUTTLE' : 'cuttle'}
        </div>
        <div style={{ fontSize: 11, color: palette.sub, letterSpacing: '0.1em', textTransform: 'uppercase', fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'inherit' }}>vs Cuttlebot</div>
        <div style={{ flex: 1 }} />
        <button onClick={reset} style={btnStyle(palette, true)}>↻</button>
      </div>

      <div>
        <SectionLabel palette={palette}>DECK · {state.deck.length}</SectionLabel>
        <div style={{ display: 'flex', gap: 14, marginTop: 10, alignItems: 'flex-start' }}>
          <DeckPile ref={deckRef} count={state.deck.length} theme={theme} palette={palette} canDraw={canDraw} onDraw={triggerDraw} />
          <div style={{ fontSize: 12, color: palette.sub, lineHeight: 1.5, marginTop: 4 }}>
            {state.deck.length === 0
              ? 'Deck is empty'
              : (canDraw ? <span style={{ color: palette.accent, fontWeight: 600 }}>Click to draw</span> : null)}
          </div>
        </div>
      </div>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <SectionLabel palette={palette}>SCRAP · {state.scrap.length}</SectionLabel>
        <ScrapStrip scrap={state.scrap} theme={theme} cardStyle={cardStyle} actionPick={actionPick} onScrapCardClick={onScrapCardClick} />
      </div>

      <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
        <SectionLabel palette={palette}>LOG</SectionLabel>
        <LogStrip log={state.log} palette={palette} />
      </div>
    </div>
  );
}

function SectionLabel({ children, palette, theme }) {
  const isArcade = palette && palette.brass === '#d4a64a';
  return (
    <div style={{ fontSize: isArcade ? 11 : 10, fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif', letterSpacing: '0.16em', color: isArcade ? palette.accent2 : palette.sub, fontWeight: 700, textTransform: 'uppercase' }}>
      {isArcade ? <>▸ {children}</> : children}
    </div>
  );
}

function ZoneTag({ children, palette, theme }) {
  const isArcade = theme === 'arcade';
  return (
    <div style={{
      writingMode: 'vertical-rl',
      transform: 'rotate(180deg)',
      fontSize: 9,
      fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif',
      letterSpacing: '0.22em',
      color: isArcade ? palette.brass : palette.sub,
      fontWeight: 700,
      textTransform: 'uppercase',
      opacity: 0.7,
      paddingRight: 4,
      marginRight: 2,
      alignSelf: 'stretch',
      borderRight: isArcade ? `1px solid ${palette.brass}55` : `1px solid ${palette.panelEdge}`,
      display: 'flex',
      alignItems: 'center',
    }}>{children}</div>
  );
}

// AI hand strip — face-down by default, face-up when player has any 8 in play.
function PeekableAIHand({ state, palette, theme, cardW = 22, cardH = 32, aiHandRef }) {
  const isArcade = theme === 'arcade';
  const peeking = state.playerField.eights.length > 0;
  const cards = state.aiHand;
  const [hoverId, setHoverId] = React.useState(null);

  if (peeking) {
    // Face-up — bigger cards so the peek is actually readable.
    const w = isArcade ? 60 : 56;
    return (
      <div ref={aiHandRef} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{
          fontSize: 10, fontFamily: isArcade ? '"VT323", monospace' : 'Inter, sans-serif',
          letterSpacing: '0.12em', color: '#ffd23f', fontWeight: 700,
          padding: '3px 8px',
          background: 'rgba(255,210,63,0.14)',
          border: '1.5px solid #ffd23f',
          textShadow: isArcade ? '0 0 6px #ffd23f' : 'none',
          animation: 'pulseGlasses 1.6s ease-in-out infinite',
          whiteSpace: 'nowrap',
        }}>GLASSES</div>
        <div style={{ display: 'flex' }}>
          {cards.map((id, i) => {
            const baseRot = (i - (cards.length - 1) / 2) * 1.5;
            const isHover = hoverId === id;
            return (
              <div
                key={id}
                data-card-id={id}
                onMouseEnter={() => setHoverId(id)}
                onMouseLeave={() => setHoverId(prev => prev === id ? null : prev)}
                style={{
                  position: 'relative',
                  marginLeft: i === 0 ? 0 : -Math.round(w * 0.18),
                  transformOrigin: 'top center',
                  transform: isHover
                    ? 'scale(2.6) translateY(8px) rotate(0deg)'
                    : `rotate(${baseRot}deg)`,
                  transition: 'transform 180ms cubic-bezier(.2,.9,.2,1), filter 180ms ease-out',
                  filter: isHover ? 'drop-shadow(0 8px 14px rgba(0,0,0,0.45))' : 'none',
                  zIndex: isHover ? 50 : i,
                  cursor: 'help',
                }}
              >
                <PlayingCard card={E.cardOf(id)} w={w} theme={theme} />
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  // Default: face-down rectangles. Tagged with the real card id so the
  // play-from-AI animation can fly out of the right slot — still anonymous
  // visually, so no information leaks.
  return (
    <div ref={aiHandRef} style={{ display: 'flex', gap: 4 }}>
      {cards.map((id, i) => (
        <div key={id} data-card-id={id} style={isArcade ? {
          width: cardW, height: cardH,
          background: '#7a1c44',
          border: '1px solid #d4a64a',
          boxShadow: 'inset 0 0 0 1px #ffd23f',
        } : {
          width: cardW, height: cardH, borderRadius: 4,
          background: theme === 'tideline' ? 'repeating-linear-gradient(45deg, #5fa893 0 4px, #3d7a6a 4px 8px)' : 'repeating-linear-gradient(45deg, #e07a5f 0 4px, #b35637 4px 8px)',
          border: '1.5px solid #fefdf8',
        }} />
      ))}
    </div>
  );
}

function ScrapStrip({ scrap, theme, cardStyle, actionPick, onScrapCardClick, scrapRef }) {
  const recent = scrap.slice(-12);
  const pickable = actionPick?.mode === 'scrap';
  const isArcade = theme === 'arcade';
  const [hoverId, setHoverId] = React.useState(null);
  return (
    <div ref={scrapRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8, padding: 8,
      background: isArcade ? 'rgba(212,166,74,0.06)' : 'rgba(0,0,0,0.04)',
      border: isArcade ? '1px dashed #6b4528' : 'none',
      borderRadius: isArcade ? 0 : 10, minHeight: 60, alignContent: 'flex-start' }}>
      {recent.length === 0 && <div style={{ fontSize: 12, fontStyle: isArcade ? 'normal' : 'italic', opacity: 0.6, fontFamily: isArcade ? '"VT323", monospace' : 'inherit', letterSpacing: isArcade ? '0.1em' : 0 }}>{isArcade ? '── EMPTY ──' : 'empty'}</div>}
      {scrap.map(id => {
        const isHover = hoverId === id;
        return (
          <div
            key={id}
            data-card-id={id}
            onClick={pickable ? () => onScrapCardClick(id) : undefined}
            onMouseEnter={() => setHoverId(id)}
            onMouseLeave={() => setHoverId(prev => prev === id ? null : prev)}
            style={{
              cursor: pickable ? 'pointer' : 'help',
              position: 'relative',
              transformOrigin: 'center center',
              transform: isHover ? 'scale(3.2)' : 'scale(1)',
              transition: 'transform 160ms cubic-bezier(.2,.9,.2,1), filter 160ms ease-out',
              filter: isHover ? 'drop-shadow(0 6px 14px rgba(0,0,0,0.55))' : 'none',
              zIndex: isHover ? 50 : 0,
            }}
          >
            <PlayingCard card={E.cardOf(id)} w={36} theme={theme} compact glow={pickable ? '#f3a682' : undefined} />
          </div>
        );
      })}
    </div>
  );
}

function LogStrip({ log, palette, compact }) {
  const ref = React.useRef();
  React.useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [log.length]);
  const isArcade = palette && palette.brass === '#d4a64a';
  return (
    <div ref={ref} style={{
      flex: 1, overflow: 'auto', marginTop: 8, padding: '4px 0',
      fontSize: isArcade ? 13 : 12, lineHeight: 1.5,
      fontFamily: isArcade ? '"JetBrains Mono", monospace' : 'Inter, sans-serif',
    }}>
      {log.slice(compact ? -6 : -30).map((l, i) => (
        <div key={i} style={{
          color: l.tone === 'attack' ? palette.accent : (l.tone === 'win' ? (isArcade ? palette.accent2 : palette.accent) : palette.ink),
          fontWeight: l.tone === 'win' ? 700 : 400,
          padding: '2px 0',
        }}>{isArcade ? '> ' : ''}{l.msg}</div>
      ))}
    </div>
  );
}

function ScoreReadout(props) {
  if (props.theme === 'arcade') return <ScoreDial {...props} />;
  return <ScoreBar {...props} />;
}

function ScoreBar({ score, target, palette, label, compact, mine }) {
  const pct = Math.min(1, score / target);
  const color = mine ? palette.accent2 : palette.accent;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: compact ? 100 : 140 }}>
      {label && <div style={{ fontSize: 9, letterSpacing: '0.14em', color: palette.sub, fontWeight: 700, textTransform: 'uppercase' }}>{label}</div>}
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
        <span style={{ fontFamily: '"Fraunces", serif', fontSize: 26, fontStyle: 'italic', fontWeight: 600, lineHeight: 1, color: palette.ink }}>{score}</span>
        <span style={{ fontSize: 12, color: palette.sub }}>/ {target}</span>
      </div>
      <div style={{ height: 4, background: 'rgba(0,0,0,0.08)', borderRadius: 2, overflow: 'hidden' }}>
        <div style={{ height: '100%', width: `${pct*100}%`, background: color, transition: 'width 400ms' }} />
      </div>
    </div>
  );
}

function zonesFor(target) {
  if (target === 21) return { amber: 11, red: 16 };
  if (target === 14) return { amber: 8,  red: 10 };
  if (target === 10) return { amber: 6,  red: 8 };
  if (target === 7)  return { amber: 4,  red: 5 };
  if (target === 5)  return { amber: 3,  red: 4 };
  return { amber: Math.max(1, Math.ceil(target * 0.5)), red: Math.max(1, Math.ceil(target * 0.75)) };
}

// Retro arcade gauge — rounded dome bezel with brass trim, semi-circle of
// green/amber/red zones, triangular needle with a glowing tip, and a pop
// animation on the score readout when it changes.
function ScoreDial({ score, target, palette, label, compact, mine }) {
  const w = compact ? 96 : 140;
  const h = compact ? 56 : 84;
  const cx = w / 2;
  const cy = h - (compact ? 10 : 14);
  const r  = Math.min(cx - (compact ? 9 : 13), cy - (compact ? 4 : 6));
  const strokeW = compact ? 6 : 9;

  const zones = zonesFor(target);
  const clampedScore = Math.max(0, Math.min(target, score));
  const accentColor = mine ? palette.accent2 : palette.accent;
  const greenColor  = '#3a8a5c';
  const amberColor  = '#ffd23f';
  const redColor    = '#c83030';
  const inRedZone   = clampedScore >= zones.red;

  const angleAt = (s) => Math.PI - (Math.max(0, Math.min(target, s)) / target) * Math.PI;
  const point = (s) => {
    const a = angleAt(s);
    return { x: cx + r * Math.cos(a), y: cy - r * Math.sin(a) };
  };
  const arc = (a, b) => {
    const A = point(a), B = point(b);
    return `M ${A.x.toFixed(2)},${A.y.toFixed(2)} A ${r},${r} 0 0 1 ${B.x.toFixed(2)},${B.y.toFixed(2)}`;
  };

  const needleDeg = -90 + (clampedScore / target) * 180;

  // Only zone boundaries get visible ticks — keeps the face uncluttered.
  const tickValues = Array.from(new Set([0, zones.amber, zones.red, target]))
    .sort((a, b) => a - b)
    .filter(v => v >= 0 && v <= target);

  const [pulseKey, setPulseKey] = React.useState(0);
  const [popKey, setPopKey] = React.useState(0);
  const prevTargetRef = React.useRef(target);
  const prevScoreRef = React.useRef(clampedScore);
  React.useEffect(() => {
    if (prevTargetRef.current !== target) {
      prevTargetRef.current = target;
      setPulseKey(k => k + 1);
    }
  }, [target]);
  React.useEffect(() => {
    if (prevScoreRef.current !== clampedScore) {
      prevScoreRef.current = clampedScore;
      setPopKey(k => k + 1);
    }
  }, [clampedScore]);

  // Triangle needle: thin shaft from pivot, fat arrowhead at the tip.
  const tipY = cy - r + 2;
  const baseY = cy - 2;
  const needlePath = `
    M ${cx - 1.5} ${baseY}
    L ${cx + 1.5} ${baseY}
    L ${cx + 1} ${tipY + (compact ? 5 : 7)}
    L ${cx + (compact ? 3 : 4)} ${tipY + (compact ? 5 : 7)}
    L ${cx} ${tipY}
    L ${cx - (compact ? 3 : 4)} ${tipY + (compact ? 5 : 7)}
    L ${cx - 1} ${tipY + (compact ? 5 : 7)}
    Z`;

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
      minWidth: w + 6,
    }}>
      {label && <div style={{
        fontSize: 9, letterSpacing: '0.14em', color: palette.sub, fontWeight: 700,
        textTransform: 'uppercase', fontFamily: '"JetBrains Mono", monospace',
      }}>{label}</div>}

      <div
        key={pulseKey}
        style={{
          position: 'relative', width: w, height: h,
          background: 'linear-gradient(170deg, #321d12 0%, #1a0e08 60%, #0d0704 100%)',
          borderRadius: `${w / 2}px ${w / 2}px ${compact ? 6 : 8}px ${compact ? 6 : 8}px`,
          boxShadow: [
            `inset 0 0 0 2px ${palette.accent}`,
            `inset 0 1.5px 0 1.5px rgba(255,210,63,0.35)`,
            `inset 0 -2px 4px rgba(0,0,0,0.55)`,
            `0 3px 0 -1px rgba(0,0,0,0.5)`,
            inRedZone ? `0 0 16px rgba(200,48,48,0.45)` : `0 0 12px rgba(212,166,74,0.18)`,
          ].join(', '),
          animation: 'scoreDialPulse 600ms ease-out',
          overflow: 'hidden',
          transition: 'box-shadow 280ms ease-out',
        }}
      >
        {/* Glossy dome highlight */}
        <div style={{
          position: 'absolute', top: 2, left: '12%', right: '12%', height: '38%',
          background: 'radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.18), rgba(255,255,255,0.04) 55%, transparent 75%)',
          borderRadius: '50%',
          pointerEvents: 'none',
        }} />

        <svg width={w} height={h} style={{ position: 'absolute', inset: 0 }}>
          {/* Recessed track behind the colored arc */}
          <path d={arc(0, target)} stroke="rgba(0,0,0,0.55)" strokeWidth={strokeW + 4} fill="none" strokeLinecap="round" />
          {/* Colored zones */}
          <path d={arc(0, zones.amber)} stroke={greenColor} strokeWidth={strokeW} fill="none" strokeLinecap="butt" />
          <path d={arc(zones.amber, zones.red)} stroke={amberColor} strokeWidth={strokeW} fill="none" strokeLinecap="butt" />
          <path
            d={arc(zones.red, target)}
            stroke={redColor}
            strokeWidth={strokeW}
            fill="none"
            strokeLinecap="butt"
            style={{
              filter: `drop-shadow(0 0 ${inRedZone ? 5 : 3}px ${redColor})`,
              animation: 'scoreDialRedPulse 600ms ease-out',
            }}
          />
          {/* Major ticks at zone boundaries */}
          {tickValues.map(s => {
            const a = angleAt(s);
            const inner = r - strokeW / 2 - 2;
            const outer = r + strokeW / 2 + 1.5;
            return (
              <line key={s}
                x1={cx + inner * Math.cos(a)} y1={cy - inner * Math.sin(a)}
                x2={cx + outer * Math.cos(a)} y2={cy - outer * Math.sin(a)}
                stroke="#fefdf8" strokeWidth={1.5}
                strokeLinecap="round" />
            );
          })}

          {/* Needle (rotates around pivot) — drop-shadow gives it a glowing tip */}
          <g
            style={{
              transformOrigin: `${cx}px ${cy}px`,
              transform: `rotate(${needleDeg}deg)`,
              transition: 'transform 360ms cubic-bezier(.34, 1.56, .64, 1)',
              filter: `drop-shadow(0 0 4px ${accentColor})`,
            }}
          >
            <path d={needlePath} fill="#fefdf8" />
            {/* Accent arrowhead at the tip */}
            <polygon
              points={`${cx},${tipY} ${cx - (compact ? 3 : 4)},${tipY + (compact ? 5 : 7)} ${cx + (compact ? 3 : 4)},${tipY + (compact ? 5 : 7)}`}
              fill={accentColor}
            />
          </g>

          {/* Brass pivot screw */}
          <circle cx={cx} cy={cy} r={compact ? 4.5 : 6} fill="#1a0e08" stroke={palette.accent} strokeWidth="1.5" />
          <circle cx={cx} cy={cy} r={compact ? 1.6 : 2.2} fill={palette.accent} />
          {/* Slot screw mark */}
          <line
            x1={cx - (compact ? 2 : 3)} y1={cy}
            x2={cx + (compact ? 2 : 3)} y2={cy}
            stroke="#1a0e08" strokeWidth={1} strokeLinecap="round" opacity="0.6" />
        </svg>
      </div>

      <div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
        <span
          key={popKey}
          style={{
            fontFamily: '"VT323", monospace',
            fontSize: compact ? 24 : 30, lineHeight: 1,
            color: accentColor, textShadow: `0 0 8px ${accentColor}`,
            display: 'inline-block',
            animation: popKey > 0 ? 'scoreDialPop 360ms cubic-bezier(.34, 1.56, .64, 1)' : 'none',
          }}
        >{String(clampedScore).padStart(2, '0')}</span>
        <span style={{ fontSize: 11, color: palette.sub, fontFamily: '"JetBrains Mono", monospace', letterSpacing: '0.06em' }}>/ {target}</span>
      </div>
    </div>
  );
}

function Pill({ children, palette, accent }) {
  return (
    <span style={{
      padding: '2px 8px', borderRadius: 999, fontSize: 10, fontWeight: 700, letterSpacing: '0.08em',
      background: accent ? palette.accent2 : palette.panel, color: '#fefdf8',
      textTransform: 'uppercase',
    }}>{children}</span>
  );
}


Object.assign(window, { CuttleTable });
