/* Shared card visuals + cuttlefish + small UI primitives */

const SUIT_GLYPHS = ['♣', '♦', '♥', '♠'];
const SUIT_NAMES = ['clubs', 'diamonds', 'hearts', 'spades'];
const RANK_LABELS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];

// Pip layouts (Ace..Ten). Each is an array of {x, y} 0..1 within the pip area.
const PIP_LAYOUTS = {
  1: [[0.5, 0.5]],
  2: [[0.5, 0.18], [0.5, 0.82]],
  3: [[0.5, 0.18], [0.5, 0.5], [0.5, 0.82]],
  4: [[0.25, 0.18], [0.75, 0.18], [0.25, 0.82], [0.75, 0.82]],
  5: [[0.25, 0.18], [0.75, 0.18], [0.5, 0.5], [0.25, 0.82], [0.75, 0.82]],
  6: [[0.25, 0.18], [0.75, 0.18], [0.25, 0.5], [0.75, 0.5], [0.25, 0.82], [0.75, 0.82]],
  7: [[0.25, 0.18], [0.75, 0.18], [0.5, 0.34], [0.25, 0.5], [0.75, 0.5], [0.25, 0.82], [0.75, 0.82]],
  8: [[0.25, 0.18], [0.75, 0.18], [0.5, 0.34], [0.25, 0.5], [0.75, 0.5], [0.5, 0.66], [0.25, 0.82], [0.75, 0.82]],
  9: [[0.25, 0.16], [0.75, 0.16], [0.25, 0.36], [0.75, 0.36], [0.5, 0.5], [0.25, 0.64], [0.75, 0.64], [0.25, 0.84], [0.75, 0.84]],
  10: [[0.25, 0.14], [0.75, 0.14], [0.25, 0.32], [0.75, 0.32], [0.5, 0.23], [0.5, 0.77], [0.25, 0.68], [0.75, 0.68], [0.25, 0.86], [0.75, 0.86]],
};

function PlayingCard({ card, w = 84, faceDown = false, selected = false, dim = false, glow, style, onClick, onMouseEnter, onMouseLeave, attachedJacks = [], compact = false, theme = 'lagoon' }) {
  const h = w * 1.4;
  if (theme === 'arcade') {
    const Pixel = window.ArcadePixelCard;
    if (Pixel) {
      return (
        <Pixel
          card={card} w={w} faceDown={faceDown} selected={selected} dim={dim}
          glow={glow} style={style} onClick={onClick}
          onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}
          attachedJacks={attachedJacks} compact={compact}
        />
      );
    }
  }
  const isRed = card && (card.suit === 1 || card.suit === 2);
  const ranNum = card ? (card.rank + 1) : 0;
  const isCourt = card && card.rank >= 10;
  const isAce = card && card.rank === 0;

  const isArcade = theme === 'arcade';
  const cardBg = isArcade ? '#1c2440' : (theme === 'tideline' ? '#fcfaf5' : '#fefdf8');
  const ink = isArcade ? (isRed ? '#ff3b8b' : '#00f0c8') : (isRed ? '#c2453d' : '#1e2a2f');
  const subInk = isArcade ? (isRed ? '#ff7ab0' : '#7be8d4') : (isRed ? '#d97a73' : '#5a6a72');

  const baseStyle = {
    width: w,
    height: h,
    borderRadius: isArcade ? 2 : w * 0.10,
    background: cardBg,
    boxShadow: isArcade
      ? (selected
          ? `0 0 0 2px #0a0e1a, 0 0 0 4px #00f0c8, 0 0 18px #00f0c8, 0 -10px 0 0 transparent`
          : `0 0 0 2px #0a0e1a, 0 0 0 3px ${ink}, 0 0 12px ${ink}55`)
      : (selected
          ? `0 0 0 3px ${theme === 'tideline' ? '#7fc8b6' : '#f3a682'}, 0 18px 30px -10px rgba(40,30,20,0.35)`
          : '0 6px 14px -4px rgba(40,30,20,0.25), 0 1px 0 rgba(255,255,255,0.6) inset'),
    position: 'relative',
    cursor: onClick ? 'pointer' : 'default',
    transition: 'transform 220ms cubic-bezier(.2,.8,.2,1), box-shadow 200ms, opacity 200ms',
    transform: selected ? 'translateY(-14px)' : 'translateY(0)',
    opacity: dim ? 0.45 : 1,
    color: ink,
    fontFamily: isArcade ? '"VT323", "Courier New", monospace' : '"Fraunces", Georgia, serif',
    userSelect: 'none',
    overflow: 'hidden',
    ...(style || {}),
  };

  if (faceDown) {
    return (
      <div style={baseStyle} onClick={onClick}>
        <CardBack w={w} theme={theme} />
      </div>
    );
  }

  if (!card) return <div style={baseStyle} />;

  return (
    <div style={baseStyle} onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
      {/* paper / scanline texture */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background: isArcade
          ? 'repeating-linear-gradient(to bottom, rgba(255,255,255,0) 0px, rgba(255,255,255,0) 2px, rgba(0,0,0,0.18) 2px, rgba(0,0,0,0.18) 3px)'
          : 'radial-gradient(140% 90% at 0% 0%, rgba(255,255,255,0.5), transparent 60%), radial-gradient(120% 90% at 100% 100%, rgba(0,0,0,0.04), transparent 50%)',
      }} />
      {glow && (
        <div style={{
          position: 'absolute', inset: -3, borderRadius: w * 0.12,
          boxShadow: `0 0 0 3px ${glow}, 0 0 24px ${glow}`,
          pointerEvents: 'none', animation: 'pulseGlow 1.6s ease-in-out infinite',
        }} />
      )}
      {/* Top-left index */}
      <div style={{
        position: 'absolute', top: w * 0.07, left: w * 0.09,
        fontSize: w * 0.22, lineHeight: 1, fontWeight: 600, letterSpacing: '-0.03em',
      }}>
        <div>{RANK_LABELS[card.rank]}</div>
        <div style={{ fontSize: w * 0.20, marginTop: w * 0.02 }}>{SUIT_GLYPHS[card.suit]}</div>
      </div>
      {/* Bottom-right index, rotated */}
      <div style={{
        position: 'absolute', bottom: w * 0.07, right: w * 0.09,
        fontSize: w * 0.22, lineHeight: 1, fontWeight: 600, transform: 'rotate(180deg)', letterSpacing: '-0.03em',
      }}>
        <div>{RANK_LABELS[card.rank]}</div>
        <div style={{ fontSize: w * 0.20, marginTop: w * 0.02 }}>{SUIT_GLYPHS[card.suit]}</div>
      </div>

      {/* Center */}
      {!compact && (
        <div style={{ position: 'absolute', inset: `${w*0.32}px ${w*0.22}px ${w*0.32}px ${w*0.22}px` }}>
          {isCourt ? <CourtArt rank={card.rank} suit={card.suit} ink={ink} subInk={subInk} w={w} theme={theme} /> :
            <PipsArt n={ranNum} suit={card.suit} ink={ink} />}
        </div>
      )}

      {/* attached jack indicator */}
      {attachedJacks.length > 0 && (
        <div style={{
          position: 'absolute', top: w*0.04, right: w*0.04,
          width: w*0.28, height: w*0.28, borderRadius: '50%',
          background: theme === 'tideline' ? '#7fc8b6' : '#f3a682',
          color: '#fff', fontFamily: 'Inter, sans-serif',
          fontSize: w*0.16, fontWeight: 700,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          boxShadow: '0 2px 4px rgba(0,0,0,0.18)',
        }}>J{attachedJacks.length > 1 ? attachedJacks.length : ''}</div>
      )}
    </div>
  );
}

function PipsArt({ n, suit, ink }) {
  const layout = PIP_LAYOUTS[n];
  if (!layout) return null;
  const glyph = SUIT_GLYPHS[suit];
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      {layout.map(([x, y], i) => {
        const flip = y > 0.5 && [3,4,5,6,7,8,9,10].includes(n);
        return (
          <div key={i} style={{
            position: 'absolute', left: `${x*100}%`, top: `${y*100}%`,
            transform: `translate(-50%, -50%) ${flip ? 'rotate(180deg)' : ''}`,
            color: ink, fontSize: '1.5em', lineHeight: 1,
          }}>{glyph}</div>
        );
      })}
    </div>
  );
}

function CourtArt({ rank, suit, ink, subInk, w, theme }) {
  // Rank 10 = Jack, 11 = Queen, 12 = King. Stylized geometric placeholder.
  const letter = ['J','Q','K'][rank - 10];
  const isArcade = theme === 'arcade';
  if (isArcade) {
    return (
      <div style={{
        position: 'relative', width: '100%', height: '100%',
        background: 'rgba(0, 240, 200, 0.08)',
        border: `2px solid ${ink}`,
        boxShadow: `inset 0 0 8px ${ink}66`,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexDirection: 'column',
      }}>
        <div style={{
          fontSize: w * 0.55, fontFamily: '"VT323", monospace', fontWeight: 400,
          color: ink, lineHeight: 1,
          textShadow: `0 0 8px ${ink}`,
        }}>{letter}</div>
        <div style={{ fontSize: w * 0.18, color: subInk, marginTop: w*0.02 }}>{SUIT_GLYPHS[suit]}</div>
      </div>
    );
  }
  const accent = rank === 10 ? '#f3a682' : rank === 11 ? '#e6b4a3' : '#cfa46b';
  return (
    <div style={{
      position: 'relative', width: '100%', height: '100%',
      background: `linear-gradient(180deg, ${accent}33 0%, ${accent}10 100%)`,
      borderRadius: w * 0.04,
      border: `1px solid ${ink}22`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      flexDirection: 'column',
    }}>
      <div style={{
        fontSize: w * 0.55, fontFamily: '"Fraunces", serif', fontWeight: 500,
        color: ink, lineHeight: 1, fontStyle: 'italic',
      }}>{letter}</div>
      <div style={{ fontSize: w * 0.18, color: subInk, marginTop: w*0.05 }}>{SUIT_GLYPHS[suit]}</div>
    </div>
  );
}

function CardBack({ w, theme }) {
  if (theme === 'arcade') {
    return (
      <div style={{
        position: 'absolute', inset: w*0.05, borderRadius: 0,
        background: '#0a0e1a',
        border: '2px solid #ff3b8b',
        boxShadow: 'inset 0 0 0 2px #00f0c8, inset 0 0 12px rgba(255,59,139,0.5)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <div style={{
          fontFamily: '"VT323", monospace',
          fontSize: w * 0.5,
          color: '#ff3b8b',
          textShadow: '0 0 8px #ff3b8b, 0 0 16px #ff3b8b',
          lineHeight: 1,
        }}>C</div>
      </div>
    );
  }
  const a = theme === 'tideline' ? '#5fa893' : '#e07a5f';
  const b = theme === 'tideline' ? '#3d7a6a' : '#b35637';
  return (
    <div style={{
      position: 'absolute', inset: w*0.05, borderRadius: w*0.07,
      background: `repeating-linear-gradient(45deg, ${a} 0 ${w*0.06}px, ${b} ${w*0.06}px ${w*0.12}px)`,
      border: `2px solid ${theme === 'tideline' ? '#fefdf8' : '#fefdf8'}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{
        width: w * 0.5, height: w * 0.5, borderRadius: '50%',
        background: theme === 'tideline' ? '#fcfaf5' : '#fefdf8',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: '"Fraunces", serif', fontSize: w*0.28, fontStyle: 'italic',
        color: a,
      }}>c</div>
    </div>
  );
}

// ─── Pixel Cuttlefish ──────────────────────────────────────────────────────────
// 16x16 grid drawn as box-shadows. Mood: 'idle' | 'thinking' | 'smug' | 'concede' | 'happy'

const CF_COLORS = {
  body: '#a78bfa',
  bodyDark: '#7c5cd8',
  bodyHL: '#d4c4ff',
  eye: '#ffffff',
  pupil: '#1e1b30',
  blush: '#ffb3b3',
  fin: '#c8b3fb',
  sweat: '#7fd0ff',
  heart: '#ff5b8a',
  spark: '#ffd23f',
  angry: '#c2453d',
  tongue: '#ff7ab0',
};

const CUTTLE_MOODS = [
  'idle', 'thinking', 'smug', 'concede', 'happy',
  'surprised', 'angry', 'annoyed', 'scheming', 'shocked',
  'embarrassed', 'sleepy', 'wink', 'focused', 'suspicious',
  'determined', 'defeated', 'excited', 'confused', 'nervous',
  'smitten', 'proud',
];

function makeCuttleGrid(mood) {
  // 16x16. Returns 2D array of color codes; '.' = transparent.
  const g = Array.from({ length: 16 }, () => Array(16).fill('.'));

  // Body silhouette (mantle on top, tentacles below)
  const body = [
    '................',
    '.....DDDDDD.....',
    '...DDBBBBBBDD...',
    '..DBBBBBBBBBBD..',
    '.DBBBBBBBBBBBBD.',
    '.DBBBHBBBBHBBBD.',
    '.DBBHHBBBBHHBBD.',
    '.DBBBBBBBBBBBBD.',
    '.DBBBBBBBBBBBBD.',
    '.DBBBBBBBBBBBBD.',
    '..DBBBBBBBBBBD..',
    '...DDBBBBBBDD...',
    '....D.DDDD.D....',
    '....D.D..D.D....',
    '...DD.D..D.DD...',
    '..DD..D..D..DD..',
  ];
  for (let y = 0; y < 16; y++) {
    for (let x = 0; x < 16; x++) {
      const c = body[y][x];
      if (c === 'D') g[y][x] = CF_COLORS.bodyDark;
      else if (c === 'B') g[y][x] = CF_COLORS.body;
      else if (c === 'H') g[y][x] = CF_COLORS.bodyHL;
    }
  }

  // ── helpers ──────────────────────────────────────────────────────────────
  const eR = 5;            // eye top row
  const L = 4, R = 9;      // eye left columns
  const set = (y, x, c) => { if (y >= 0 && y < 16 && x >= 0 && x < 16) g[y][x] = c; };
  const fill2x2 = (y, x, c) => { set(y, x, c); set(y, x+1, c); set(y+1, x, c); set(y+1, x+1, c); };
  const blush = (color = CF_COLORS.blush) => { set(7, 2, color); set(7, 13, color); };
  const wideBlush = (color = CF_COLORS.blush) => {
    set(7, 2, color); set(7, 3, color); set(8, 2, color);
    set(7, 12, color); set(7, 13, color); set(8, 13, color);
  };
  const eyebrowsAngry = () => {
    set(4, L, CF_COLORS.bodyDark);   set(4, L+1, CF_COLORS.angry);
    set(4, R, CF_COLORS.angry);      set(4, R+1, CF_COLORS.bodyDark);
  };
  const eyebrowsFurrow = () => {
    set(4, L, CF_COLORS.bodyDark);   set(4, L+1, CF_COLORS.bodyDark);
    set(4, R, CF_COLORS.bodyDark);   set(4, R+1, CF_COLORS.bodyDark);
  };

  switch (mood) {
    case 'thinking': {
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L+1, CF_COLORS.pupil); set(eR+1, R+1, CF_COLORS.pupil);
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      // thought-bubble dots
      set(2, 12, CF_COLORS.eye); set(1, 13, CF_COLORS.eye);
      set(0, 14, CF_COLORS.eye);
      break;
    }

    case 'smug': {
      // Half-lidded
      set(eR, L, CF_COLORS.bodyDark); set(eR, L+1, CF_COLORS.bodyDark);
      set(eR, R, CF_COLORS.bodyDark); set(eR, R+1, CF_COLORS.bodyDark);
      set(eR+1, L, CF_COLORS.eye); set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.eye); set(eR+1, R+1, CF_COLORS.pupil);
      // Grin
      set(7, 6, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      blush();
      break;
    }

    case 'concede': {
      // X eyes
      fill2x2(eR, L, CF_COLORS.pupil);
      fill2x2(eR, R, CF_COLORS.pupil);
      // Small frown
      set(8, 6, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      break;
    }

    case 'happy': {
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L, CF_COLORS.pupil); set(eR+1, R, CF_COLORS.pupil);
      // U-smile
      set(8, 6, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      blush();
      break;
    }

    case 'surprised': {
      // Wide eyes, small pupils centered
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L, CF_COLORS.pupil); set(eR+1, R+1, CF_COLORS.pupil);
      // 'O' mouth
      set(7, 7, CF_COLORS.bodyDark); set(7, 8, CF_COLORS.bodyDark);
      set(8, 7, CF_COLORS.pupil); set(8, 8, CF_COLORS.pupil);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      break;
    }

    case 'angry': {
      // Squinted: only bottom-row whites + angled pupils
      set(eR+1, L, CF_COLORS.eye);    set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.pupil);  set(eR+1, R+1, CF_COLORS.eye);
      eyebrowsAngry();
      // Snarl mouth
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      // Anger spark above head
      set(0, 13, CF_COLORS.angry);
      set(1, 12, CF_COLORS.angry); set(1, 14, CF_COLORS.angry);
      set(2, 13, CF_COLORS.angry);
      break;
    }

    case 'annoyed': {
      // Right eye half-lid, left normal
      fill2x2(eR, L, CF_COLORS.eye); set(eR+1, L+1, CF_COLORS.pupil);
      set(eR, R, CF_COLORS.bodyDark); set(eR, R+1, CF_COLORS.bodyDark);
      set(eR+1, R, CF_COLORS.eye);    set(eR+1, R+1, CF_COLORS.pupil);
      // Flat short mouth
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      // "tch" mark
      set(2, 13, CF_COLORS.bodyDark); set(2, 14, CF_COLORS.bodyDark);
      break;
    }

    case 'scheming': {
      // Both eyes half-lidded, pupils glanced right
      set(eR, L, CF_COLORS.bodyDark); set(eR, L+1, CF_COLORS.bodyDark);
      set(eR, R, CF_COLORS.bodyDark); set(eR, R+1, CF_COLORS.bodyDark);
      set(eR+1, L, CF_COLORS.eye);    set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.eye);    set(eR+1, R+1, CF_COLORS.pupil);
      // Crooked grin (right corner up)
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      blush();
      break;
    }

    case 'shocked': {
      // Huge eyes — extend up to row 4
      set(4, L, CF_COLORS.eye); set(4, L+1, CF_COLORS.eye);
      set(4, R, CF_COLORS.eye); set(4, R+1, CF_COLORS.eye);
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L+1, CF_COLORS.pupil); set(eR+1, R, CF_COLORS.pupil);
      // Open mouth
      set(7, 6, CF_COLORS.bodyDark); set(7, 7, CF_COLORS.bodyDark);
      set(7, 8, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.pupil);
      set(8, 8, CF_COLORS.pupil); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      break;
    }

    case 'embarrassed': {
      // Eyes glance down — pupils on bottom row
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L, CF_COLORS.pupil); set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.pupil); set(eR+1, R+1, CF_COLORS.pupil);
      // Sheepish wavy mouth
      set(8, 6, CF_COLORS.bodyDark); set(7, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      wideBlush();
      break;
    }

    case 'sleepy': {
      // Closed eyes — bottom-row lines
      set(eR+1, L, CF_COLORS.bodyDark); set(eR+1, L+1, CF_COLORS.bodyDark);
      set(eR+1, R, CF_COLORS.bodyDark); set(eR+1, R+1, CF_COLORS.bodyDark);
      // Small flat mouth
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      // Z above head
      set(0, 12, CF_COLORS.eye); set(0, 13, CF_COLORS.eye); set(0, 14, CF_COLORS.eye);
      set(1, 13, CF_COLORS.eye);
      set(2, 12, CF_COLORS.eye); set(2, 13, CF_COLORS.eye); set(2, 14, CF_COLORS.eye);
      break;
    }

    case 'wink': {
      // Left eye normal
      fill2x2(eR, L, CF_COLORS.eye); set(eR+1, L, CF_COLORS.pupil);
      // Right eye closed
      set(eR+1, R, CF_COLORS.bodyDark); set(eR+1, R+1, CF_COLORS.bodyDark);
      // Smile
      set(8, 6, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      blush();
      break;
    }

    case 'focused': {
      // Pupils only — small dilated focus dots
      set(eR, L+1, CF_COLORS.pupil); set(eR+1, L+1, CF_COLORS.pupil);
      set(eR, R, CF_COLORS.pupil);   set(eR+1, R, CF_COLORS.pupil);
      eyebrowsFurrow();
      // Tight mouth
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      break;
    }

    case 'suspicious': {
      // Half-lid both eyes, pupils glance right
      set(eR, L, CF_COLORS.bodyDark); set(eR, L+1, CF_COLORS.bodyDark);
      set(eR, R, CF_COLORS.bodyDark); set(eR, R+1, CF_COLORS.bodyDark);
      set(eR+1, L, CF_COLORS.eye);    set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.eye);    set(eR+1, R+1, CF_COLORS.pupil);
      // Slight frown
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      set(9, 6, CF_COLORS.bodyDark); set(9, 9, CF_COLORS.bodyDark);
      break;
    }

    case 'determined': {
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L, CF_COLORS.pupil); set(eR+1, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.pupil); set(eR+1, R+1, CF_COLORS.pupil);
      eyebrowsFurrow();
      // Gritted teeth
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 6, CF_COLORS.bodyDark); set(9, 9, CF_COLORS.bodyDark);
      break;
    }

    case 'defeated': {
      // Droopy: lids on top, pupils sagging in opposite corners. Inner-bottom
      // pixels are explicitly white so the underlying body highlight doesn't
      // leak through and make the eyes look mismatched.
      set(eR, L, CF_COLORS.bodyDark); set(eR, L+1, CF_COLORS.bodyDark);
      set(eR, R, CF_COLORS.bodyDark); set(eR, R+1, CF_COLORS.bodyDark);
      set(eR+1, L, CF_COLORS.pupil);  set(eR+1, L+1, CF_COLORS.eye);
      set(eR+1, R, CF_COLORS.eye);    set(eR+1, R+1, CF_COLORS.pupil);
      // Downturned mouth
      set(7, 6, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      break;
    }

    case 'excited': {
      // Sparkle eyes: white whites + a yellow highlight pixel
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR, L, CF_COLORS.spark);
      set(eR, R+1, CF_COLORS.spark);
      set(eR+1, L+1, CF_COLORS.pupil); set(eR+1, R, CF_COLORS.pupil);
      // Wide smile
      set(7, 6, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 6, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      // Sparkles around head
      set(2, 2, CF_COLORS.spark); set(3, 13, CF_COLORS.spark);
      set(1, 8, CF_COLORS.spark);
      blush();
      break;
    }

    case 'confused': {
      // Asymmetric pupils
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR, L+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.pupil);
      // Wavy mouth
      set(8, 6, CF_COLORS.bodyDark); set(7, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      // '?' floating above head
      set(0, 13, CF_COLORS.eye); set(0, 14, CF_COLORS.eye);
      set(1, 14, CF_COLORS.eye);
      set(2, 13, CF_COLORS.eye);
      set(4, 13, CF_COLORS.eye);
      break;
    }

    case 'nervous': {
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L+1, CF_COLORS.pupil); set(eR+1, R, CF_COLORS.pupil);
      // Wavy frown
      set(8, 6, CF_COLORS.bodyDark); set(9, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(9, 9, CF_COLORS.bodyDark);
      // Sweat drop
      set(1, 13, CF_COLORS.sweat);
      set(2, 12, CF_COLORS.sweat); set(2, 13, CF_COLORS.sweat);
      set(3, 13, CF_COLORS.sweat);
      break;
    }

    case 'smitten': {
      // Heart-shaped eyes
      set(eR, L, CF_COLORS.heart);   set(eR, L+1, CF_COLORS.heart);
      set(eR, R, CF_COLORS.heart);   set(eR, R+1, CF_COLORS.heart);
      set(eR+1, L, CF_COLORS.heart);
      set(eR+1, R+1, CF_COLORS.heart);
      // Big smile
      set(7, 6, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 6, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      set(9, 7, CF_COLORS.bodyDark); set(9, 8, CF_COLORS.bodyDark);
      // Floating hearts
      set(2, 2, CF_COLORS.heart); set(2, 13, CF_COLORS.heart);
      blush();
      break;
    }

    case 'proud': {
      // Closed-arc eyes (^_^) — drawn in pupil so they pop against the body.
      set(eR, L, CF_COLORS.pupil);   set(eR, L+1, CF_COLORS.pupil);
      set(eR+1, L+1, CF_COLORS.pupil);
      set(eR, R, CF_COLORS.pupil);   set(eR, R+1, CF_COLORS.pupil);
      set(eR+1, R, CF_COLORS.pupil);
      // Big grin
      set(7, 6, CF_COLORS.bodyDark); set(7, 9, CF_COLORS.bodyDark);
      set(8, 6, CF_COLORS.bodyDark); set(8, 7, CF_COLORS.bodyDark);
      set(8, 8, CF_COLORS.bodyDark); set(8, 9, CF_COLORS.bodyDark);
      blush();
      break;
    }

    case 'idle':
    default: {
      fill2x2(eR, L, CF_COLORS.eye); fill2x2(eR, R, CF_COLORS.eye);
      set(eR+1, L, CF_COLORS.pupil); set(eR+1, R, CF_COLORS.pupil);
      set(8, 7, CF_COLORS.bodyDark); set(8, 8, CF_COLORS.bodyDark);
      break;
    }
  }
  return g;
}

const MOOD_ANIMATIONS = {
  thinking:    'cuttleBob 1.2s ease-in-out infinite',
  scheming:    'cuttleBob 2.4s ease-in-out infinite',
  focused:     'cuttleFloat 4s ease-in-out infinite',
  nervous:     'cuttleShake 0.6s ease-in-out infinite',
  shocked:     'cuttleShake 0.4s ease-in-out infinite',
  excited:     'cuttleBounce 0.8s ease-in-out infinite',
  smitten:     'cuttleBounce 1.6s ease-in-out infinite',
  sleepy:      'cuttleFloat 6s ease-in-out infinite',
  defeated:    'cuttleFloat 5s ease-in-out infinite',
};

function PixelCuttlefish({ mood = 'idle', size = 96 }) {
  const grid = makeCuttleGrid(mood);
  const px = size / 16;
  return (
    <div style={{
      width: size, height: size, position: 'relative',
      imageRendering: 'pixelated',
      animation: MOOD_ANIMATIONS[mood] || 'cuttleFloat 3s ease-in-out infinite',
    }}>
      {grid.map((row, y) => row.map((c, x) => c === '.' ? null : (
        <div key={`${y}-${x}`} style={{
          position: 'absolute',
          left: x * px, top: y * px,
          width: px, height: px,
          background: c,
        }} />
      )))}
    </div>
  );
}

// Stable pick: same (options, seed) → same choice, so the face doesn't flicker on rerender.
function seededPick(options, seed) {
  const h = ((seed | 0) * 2654435761) >>> 0;
  return options[h % options.length];
}

// Decide what face to show based on game state. Slight variation so the same
// situation doesn't always produce the same expression — picks are seeded by
// state.turn, so within a single turn the face is stable across rerenders.
function pickAIMood(state, opts = {}) {
  const { aiThinking = false } = opts;
  const turn = state?.turn ?? 0;

  // Game over.
  if (state?.winner === 'ai') {
    return seededPick(['happy', 'proud'], turn);
  }
  if (state?.winner === 'player') {
    return seededPick(['concede', 'defeated', 'concede'], turn);
  }

  // Computing a move.
  if (aiThinking) {
    return seededPick(['thinking', 'thinking', 'focused', 'scheming', 'suspicious'], turn);
  }

  // Reaction phase.
  if (state?.phase === 'WAITING_FOR_REACTION') {
    if (state.pending?.reactingPlayer === 1) {
      return seededPick(['thinking', 'scheming', 'suspicious', 'focused'], turn);
    }
    // Player must react — Cuttlebot just landed something.
    return seededPick(['smug', 'excited', 'scheming', 'proud'], turn);
  }

  // AI is being forced to discard from a Four.
  if (state?.phase === 'FOUR_DISCARD' && state.fourDiscarder === 1) {
    return seededPick(['annoyed', 'angry', 'defeated'], turn);
  }

  // React to the most recent log line.
  const last = state?.log?.[state.log.length - 1];
  if (last && last.msg) {
    const m = last.msg.toLowerCase();
    if (m.startsWith('you ')) {
      if (m.includes('jacked'))                  return seededPick(['angry', 'annoyed', 'shocked'], turn);
      if (m.includes('scuttled'))                return seededPick(['annoyed', 'defeated', 'nervous'], turn);
      if (m.includes('countered with'))          return seededPick(['annoyed', 'angry', 'suspicious'], turn);
      if (m.includes('put on glasses'))          return seededPick(['embarrassed', 'annoyed', 'wink'], turn);
      if (m.includes('played') && m.includes('for its effect'))
        return seededPick(['surprised', 'shocked', 'annoyed'], turn);
    } else if (m.startsWith('cuttlebot ')) {
      if (m.includes('scuttled'))                return seededPick(['smug', 'proud', 'excited'], turn);
      if (m.includes('jacked'))                  return seededPick(['smug', 'scheming', 'proud'], turn);
      if (m.includes('countered with'))          return seededPick(['smug', 'scheming', 'suspicious'], turn);
      if (m.includes('placed') && m.includes('king'))
        return seededPick(['proud', 'smug', 'excited'], turn);
      if (m.includes('put on glasses'))          return seededPick(['scheming', 'smug'], turn);
    }
  }

  // Player has Glasses out — Cuttlebot's hand is exposed.
  if (state?.playerField?.eights?.length > 0) {
    return seededPick(['embarrassed', 'annoyed', 'wink', 'idle', 'suspicious'], turn);
  }

  // Score-margin default.
  const aiScore = state?._aiScore ?? 0;
  const plScore = state?._playerScore ?? 0;
  const margin  = aiScore - plScore;
  if (margin >= 8)   return seededPick(['smug', 'proud', 'scheming', 'excited'], turn);
  if (margin >= 3)   return seededPick(['smug', 'scheming', 'idle'], turn);
  if (margin <= -8)  return seededPick(['nervous', 'defeated', 'annoyed'], turn);
  if (margin <= -3)  return seededPick(['nervous', 'suspicious', 'idle'], turn);

  return seededPick(['idle', 'smug', 'idle', 'suspicious'], turn);
}

// ─── Field-effect primitives (subtle, pixel-arty) ────────────────────────────
// Pixel-stepped expanding ring. Snaps through 4 frames so it reads as sprite-
// like rather than smooth. Used when a permanent (Queen / King / Glasses) is
// placed; color encodes the role.
function PixelRing({ color = '#ffd23f', size = 96, thickness = 2, duration = 280 }) {
  return (
    <div style={{
      position: 'absolute', left: '50%', top: '50%',
      width: size, height: size,
      marginLeft: -size / 2, marginTop: -size / 2,
      border: `${thickness}px solid ${color}`,
      boxSizing: 'border-box',
      pointerEvents: 'none',
      animation: `pixelRingExpand ${duration}ms steps(4, end) forwards`,
      opacity: 0,
    }} />
  );
}

// Pixel-stepped diagonal slash. Grows from 0 → length in 4 stepped frames then
// fades. Used at the impact moment of a Scuttle.
function PixelSlash({ length = 280, thickness = 4, color = '#ffd23f', angle = -54, duration = 220 }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!ref.current) return;
    ref.current.animate([
      { width: '0px', opacity: 1 },
      { width: `${length}px`, opacity: 1, offset: 0.6 },
      { width: `${length}px`, opacity: 0 },
    ], { duration, easing: 'steps(4, end)', fill: 'forwards' });
  }, [length, duration]);
  return (
    <div ref={ref} style={{
      position: 'absolute', left: '50%', top: '50%',
      transform: `translate(-50%, -50%) rotate(${angle}deg)`,
      transformOrigin: 'center',
      width: 0, height: thickness,
      background: color,
      pointerEvents: 'none',
    }} />
  );
}

// Pixel-stepped crack overlay — jagged segments drawn in. Used when an 8 is
// broken by a Ten.
function PixelCrack({ color = '#1e1b30', size = 76, duration = 260 }) {
  return (
    <svg width={size} height={size * 1.4} viewBox="0 0 76 106"
         style={{
           position: 'absolute', left: '50%', top: '50%',
           transform: 'translate(-50%, -50%)',
           pointerEvents: 'none',
           animation: `pixelCrackDraw ${duration}ms steps(4, end) forwards`,
           opacity: 0,
         }}>
      <path d="M14,18 L28,40 L20,52 L36,68 L26,84 L48,92" stroke={color}
            strokeWidth="3" fill="none" shapeRendering="crispEdges" />
      <path d="M44,12 L52,30 L60,40 L48,56 L62,72" stroke={color}
            strokeWidth="3" fill="none" shapeRendering="crispEdges" />
    </svg>
  );
}

// Pixel-stepped shatter: 4 small chunks fly diagonally and fade. Used when a
// royalty is wiped by a Six.
function PixelShatter({ color = '#a78bfa', duration = 320 }) {
  const chunks = [
    { dx: -16, dy: -14, rot: -25 },
    { dx:  18, dy: -12, rot:  20 },
    { dx: -14, dy:  16, rot: -15 },
    { dx:  16, dy:  18, rot:  30 },
  ];
  return (
    <div style={{
      position: 'absolute', left: '50%', top: '50%',
      width: 0, height: 0, pointerEvents: 'none',
    }}>
      {chunks.map((c, i) => <PixelShatterChunk key={i} {...c} color={color} duration={duration} />)}
    </div>
  );
}
function PixelShatterChunk({ dx, dy, rot, color, duration }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!ref.current) return;
    ref.current.animate([
      { transform: 'translate(-50%, -50%) rotate(0deg)', opacity: 1 },
      { transform: `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px)) rotate(${rot}deg)`, opacity: 0 },
    ], { duration, easing: 'steps(4, end)', fill: 'forwards' });
  }, []);
  return <div ref={ref} style={{
    position: 'absolute', left: 0, top: 0,
    width: 6, height: 6, background: color, opacity: 0,
  }} />;
}

// ─── Punchy callout ───────────────────────────────────────────────────────────
function Callout({ text, tone = 'attack', show, theme = 'lagoon' }) {
  if (theme === 'arcade') {
    const arcadeColor = tone === 'win' ? '#ffd23f' : tone === 'play' ? '#5fc8b6' : '#ff3b8b';
    const splitR = tone === 'win' ? '#ff3b8b' : tone === 'play' ? '#ffd23f' : '#ffd23f';
    const splitL = tone === 'win' ? '#5fc8b6' : tone === 'play' ? '#ff3b8b' : '#5fc8b6';
    return (
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        zIndex: 30,
      }}>
        <div style={{
          fontFamily: '"VT323", "Courier New", monospace',
          fontSize: 140, lineHeight: 0.9,
          letterSpacing: '0.08em', textTransform: 'uppercase',
          color: arcadeColor,
          textShadow: [
            `4px 0 0 ${splitR}`,
            `-4px 0 0 ${splitL}`,
            '6px 6px 0 #1a0e08',
            '0 0 14px currentColor',
            '0 0 28px currentColor',
          ].join(', '),
          opacity: show ? 1 : 0,
          animation: show ? 'arcadeCalloutIn 360ms steps(6,end) both, arcadeCalloutShake 480ms steps(8,end) 320ms both' : 'none',
          transition: 'opacity 120ms',
        }}>{text}</div>
      </div>
    );
  }
  const colors = {
    attack: theme === 'tideline' ? '#3d7a6a' : '#c2453d',
    play:   theme === 'tideline' ? '#5fa893' : '#e07a5f',
    win:    '#d4a23a',
  };
  return (
    <div style={{
      position: 'absolute', inset: 0, pointerEvents: 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      zIndex: 30,
    }}>
      <div style={{
        fontFamily: '"Fraunces", serif', fontWeight: 700, fontStyle: 'italic',
        fontSize: 92, letterSpacing: '-0.02em',
        color: colors[tone],
        textShadow: '0 4px 0 rgba(255,255,255,0.4)',
        opacity: show ? 1 : 0,
        transform: show ? 'scale(1) rotate(-3deg)' : 'scale(0.6) rotate(-3deg)',
        transition: 'opacity 240ms, transform 240ms cubic-bezier(.2,1.7,.4,1)',
      }}>{text}</div>
    </div>
  );
}

Object.assign(window, {
  PlayingCard, CardBack, PixelCuttlefish, Callout,
  PixelRing, PixelSlash, PixelCrack, PixelShatter,
  SUIT_GLYPHS, RANK_LABELS, CUTTLE_MOODS, pickAIMood,
});
