// Cows & Bulls — game hook + shared presentational pieces. Exposed on window.
// Loaded as text/babel. Reads CSS custom properties set by the host App.
(function () {
  const { useState, useEffect, useRef, useMemo } = React;

  // ---- one-time CSS ---------------------------------------------------------
  if (!document.getElementById('cb-styles')) {
    const s = document.createElement('style');
    s.id = 'cb-styles';
    s.textContent = `
    .cb-root{ box-sizing:border-box; width:100%; height:100%; background:var(--cb-bg);
      color:var(--cb-ink); font-family:var(--cb-sans); position:relative;
      -webkit-font-smoothing:antialiased; }
    .cb-root *{ box-sizing:border-box; }
    .cb-mono{ font-family:var(--cb-mono); font-variant-numeric:tabular-nums; }
    .cb-kicker{ font-size:11px; letter-spacing:.18em; text-transform:uppercase;
      color:var(--cb-muted); font-weight:600; }

    /* slots */
    .cb-slots{ display:flex; gap:var(--slot-gap,18px); justify-content:center; }
    .cb-slot{ width:var(--slot-w,72px); height:var(--slot-h,88px);
      display:flex; align-items:center; justify-content:center;
      font-family:var(--cb-mono); font-weight:600; font-size:var(--slot-fz,46px);
      color:var(--cb-ink); border-bottom:3px solid var(--cb-line-strong);
      transition:border-color .18s, color .18s; }
    .cb-slot-cur{ border-bottom-color:var(--cb-accent); }
    .cb-slot-cur::after{ content:''; width:2px; height:var(--caret-h,46px);
      background:var(--cb-accent); opacity:.0; animation:cb-blink 1.1s steps(1) infinite; }
    .cb-slot-empty-dim{ color:var(--cb-faint); }
    @keyframes cb-blink{ 50%{opacity:.9} }
    @keyframes cb-shake{ 0%,100%{transform:translateX(0)} 20%{transform:translateX(-7px)}
      40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(3px)} }
    .cb-shake{ animation:cb-shake .42s ease; }

    /* keypad */
    .cb-keys{ display:flex; flex-wrap:wrap; gap:8px; justify-content:center; }
    .cb-key{ font-family:var(--cb-mono); font-size:19px; font-weight:600;
      min-width:46px; height:52px; padding:0 12px; border-radius:10px;
      border:1px solid var(--cb-line); background:var(--cb-surface); color:var(--cb-ink);
      cursor:pointer; transition:transform .08s, border-color .15s, opacity .15s;
      display:flex; align-items:center; justify-content:center; user-select:none; }
    .cb-key:hover:not(:disabled){ border-color:var(--cb-line-strong); background:var(--cb-key-hover); }
    .cb-key:active:not(:disabled){ transform:translateY(1px); }
    .cb-key:disabled{ opacity:.32; cursor:default; }
    .cb-key-wide{ flex:1; min-width:88px; letter-spacing:.04em; }
    .cb-key-enter{ background:var(--cb-ink); color:var(--cb-bg); border-color:var(--cb-ink);
      font-family:var(--cb-sans); font-weight:600; font-size:14px; letter-spacing:.06em;
      text-transform:uppercase; }
    .cb-key-enter:hover:not(:disabled){ background:var(--cb-ink); opacity:.86; }
    .cb-key-enter:disabled{ opacity:.28; }

    /* pips */
    .cb-pips{ display:inline-flex; gap:4px; align-items:center; }
    .cb-pip{ width:9px; height:9px; border-radius:50%; }
    .cb-pip-bull{ background:var(--cb-accent); }
    .cb-pip-cow{ background:color-mix(in srgb, var(--cb-cow) 16%, transparent); border:1.5px solid var(--cb-cow); }
    .cb-score{ font-family:var(--cb-mono); font-size:13px; font-variant-numeric:tabular-nums;
      letter-spacing:.02em; white-space:nowrap; }
    .cb-score b{ color:var(--cb-accent); font-weight:700; }
    .cb-score i{ color:var(--cb-cow); font-style:normal; font-weight:600; }

    /* guess rows */
    .cb-guess{ display:flex; align-items:center; gap:12px; animation:cb-fade .32s ease; }
    @keyframes cb-fade{ from{transform:translateY(-5px)} to{transform:none} }
    .cb-gidx{ font-family:var(--cb-mono); font-size:12px; color:var(--cb-faint); width:22px;
      text-align:right; flex:0 0 auto; }
    .cb-gnum{ font-family:var(--cb-mono); font-size:22px; font-weight:600; letter-spacing:.14em;
      color:var(--cb-ink); }

    /* leaderboard */
    .cb-lrow{ display:grid; align-items:center; gap:10px; padding:8px 0;
      border-bottom:1px solid var(--cb-line); }
    .cb-lrank{ font-family:var(--cb-mono); font-size:13px; color:var(--cb-muted); width:20px; }
    .cb-lname{ font-size:14px; color:var(--cb-ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .cb-lg{ font-family:var(--cb-mono); font-size:14px; font-weight:600; }
    .cb-lt{ font-family:var(--cb-mono); font-size:12px; color:var(--cb-muted); text-align:right; }
    .cb-lrow-me{ background:var(--cb-accent-soft); margin:0 -10px; padding:8px 10px; border-radius:8px;
      border-bottom-color:transparent; }
    .cb-lrow-top .cb-lg{ color:var(--cb-accent); }

    .cb-link{ background:none; border:none; color:var(--cb-muted); font-family:var(--cb-sans);
      font-size:13px; cursor:pointer; text-decoration:underline; text-underline-offset:3px; padding:0; }
    .cb-link:hover{ color:var(--cb-ink); }
    .cb-cta{ font-family:var(--cb-sans); font-size:14px; font-weight:600; letter-spacing:.03em;
      border:1px solid var(--cb-ink); background:var(--cb-ink); color:var(--cb-bg);
      padding:11px 20px; border-radius:10px; cursor:pointer; transition:opacity .15s; white-space:nowrap; }
    .cb-cta:hover{ opacity:.86; }
    `;
    document.head.appendChild(s);
  }

  // ---- hook -----------------------------------------------------------------
  function useGame({ len, today, version }) {
    const secret = useMemo(() => CB.secretFor(today, len) , [len, today, version]);
    const board = useMemo(() => CB.leaderboard(today, len), [len, today]);
    const [guesses, setGuesses] = useState([]);
    const [input, setInput] = useState('');
    const [msg, setMsg] = useState('');
    const [shake, setShake] = useState(false);
    const [won, setWon] = useState(false);
    const [startedAt, setStartedAt] = useState(null);
    const [elapsed, setElapsed] = useState(0);
    const [winTime, setWinTime] = useState(0);

    useEffect(() => {
      setGuesses([]); setInput(''); setMsg(''); setWon(false);
      setStartedAt(null); setElapsed(0); setWinTime(0);
    }, [len, today, version]);

    useEffect(() => {
      if (startedAt == null || won) return;
      const t = setInterval(() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)), 500);
      return () => clearInterval(t);
    }, [startedAt, won]);

    const flash = (m) => { setMsg(m); setShake(true); setTimeout(() => setShake(false), 440); };

    const api = {
      secret, board, guesses, input, msg, shake, won, len, today,
      elapsed: won ? winTime : elapsed,
      used: new Set(input.split('')),
      typeDigit(ch) {
        if (won || input.length >= len) return;
        if (input.includes(ch)) { flash('No repeated digits'); return; }
        if (startedAt == null) setStartedAt(Date.now());
        setMsg(''); setInput(input + ch);
      },
      del() { if (!won) { setInput(input.slice(0, -1)); setMsg(''); } },
      submit() {
        if (won) return;
        const v = CB.validate(input, len);
        if (!v.ok) { flash(v.msg); return; }
        const { bulls, cows } = CB.score(input, secret);
        setGuesses([{ value: input, bulls, cows, n: guesses.length + 1 }, ...guesses]);
        setInput('');
        if (bulls === len) {
          setWon(true);
          setWinTime(startedAt ? Math.floor((Date.now() - startedAt) / 1000) : 0);
        }
      },
      reset() {
        setGuesses([]); setInput(''); setMsg(''); setWon(false);
        setStartedAt(null); setElapsed(0); setWinTime(0);
      },
    };
    return api;
  }

  // ---- shared keyboard binding ---------------------------------------------
  function useKeyboard(g, active) {
    const ref = useRef(g); ref.current = g;
    useEffect(() => {
      if (!active) return;
      const onKey = (e) => {
        if (e.metaKey || e.ctrlKey || e.altKey) return;
        const tag = (document.activeElement && document.activeElement.tagName) || '';
        if (tag === 'INPUT' || tag === 'TEXTAREA' || (document.activeElement && document.activeElement.isContentEditable)) return;
        if (/^[0-9]$/.test(e.key)) { e.preventDefault(); ref.current.typeDigit(e.key); }
        else if (e.key === 'Backspace') { e.preventDefault(); ref.current.del(); }
        else if (e.key === 'Enter') { e.preventDefault(); ref.current.submit(); }
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [active]);
  }

  // ---- pieces ---------------------------------------------------------------
  function Wordmark({ today, size = 'md', align = 'center' }) {
    const p = CB.dateParts(today);
    const num = CB.puzzleNumber(today);
    const big = size === 'lg';
    return (
      <div style={{ textAlign: align }}>
        <div style={{
          fontWeight: 700, letterSpacing: big ? '-0.02em' : '0.01em',
          fontSize: big ? 40 : 21, lineHeight: 1.02, whiteSpace: 'nowrap',
          display: 'flex', alignItems: 'center', gap: big ? 14 : 9,
          justifyContent: align === 'center' ? 'center' : 'flex-start',
        }}>
          <span style={{ fontSize: big ? 34 : 18, lineHeight: 1 }}>🥯</span>
          <span>Bagels</span>
        </div>
        <div className="cb-mono" style={{ marginTop: 6, fontSize: 12.5, color: 'var(--cb-muted)', letterSpacing: '.04em' }}>
          {p.y} / {p.m} / {p.d}<span style={{ opacity: .5 }}> · No.&nbsp;{num}</span>
        </div>
      </div>
    );
  }

  function Legend({ style }) {
    const row = { display: 'inline-flex', alignItems: 'center', gap: 7 };
    const desc = { fontSize: 12.5, color: 'var(--cb-muted)' };
    return (
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 7, ...style }}>
        <span style={row}>
          <span className="cb-pip cb-pip-bull" /><span className="cb-score"><b>Fermi</b>&nbsp;<span style={{ color: 'var(--cb-muted)' }}>(A)</span></span>
          <span style={desc}>right digit, right spot</span>
        </span>
        <span style={row}>
          <span className="cb-pip cb-pip-cow" /><span className="cb-score"><i>Pico</i>&nbsp;<span style={{ color: 'var(--cb-muted)' }}>(B)</span></span>
          <span style={desc}>right digit, wrong spot</span>
        </span>
        <span style={row}>
          <span style={{ fontSize: 14, lineHeight: 1 }}>🥯</span><span className="cb-score" style={{ color: 'var(--cb-muted)' }}>Bagels</span>
          <span style={desc}>nothing matches</span>
        </span>
      </div>
    );
  }

  function Slots({ input, len, won, shake, slotW = 72, slotH = 88, fz = 46 }) {
    const cells = [];
    for (let i = 0; i < len; i++) {
      const ch = input[i] || '';
      const isCur = !won && i === input.length;
      cells.push(
        <div key={i} className={'cb-slot' + (isCur ? ' cb-slot-cur' : '')}
          style={{ '--slot-w': slotW + 'px', '--slot-h': slotH + 'px', '--slot-fz': fz + 'px', '--caret-h': Math.round(fz * 0.9) + 'px' }}>
          {ch || (isCur ? '' : '\u00A0')}
        </div>
      );
    }
    return <div className={'cb-slots' + (shake ? ' cb-shake' : '')} style={{ '--slot-gap': Math.round(slotW * 0.22) + 'px' }}>{cells}</div>;
  }

  function ResultPips({ bulls, cows, len, showText = true }) {
    if (bulls === 0 && cows === 0) {
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
          <span style={{ fontSize: 15, lineHeight: 1 }}>🥯</span>
          {showText && <span className="cb-score" style={{ color: 'var(--cb-muted)' }}>Bagels</span>}
        </span>
      );
    }
    const pips = [];
    for (let i = 0; i < bulls; i++) pips.push(<span key={'b' + i} className="cb-pip cb-pip-bull" />);
    for (let i = 0; i < cows; i++) pips.push(<span key={'c' + i} className="cb-pip cb-pip-cow" />);
    return (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 12 }}>
        <span className="cb-pips">{pips}</span>
        {showText && <span className="cb-score"><b>{bulls}A</b> <i>{cows}B</i></span>}
      </span>
    );
  }

  function Keypad({ g, compact }) {
    const digits = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
    const full = g.input.length === g.len;
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: '100%' }}>
        <div className="cb-keys">
          {digits.map((d) => (
            <button key={d} className="cb-key" disabled={g.won || g.used.has(d)}
              onClick={() => g.typeDigit(d)}>{d}</button>
          ))}
        </div>
        <div className="cb-keys" style={{ flexWrap: 'nowrap' }}>
          <button className="cb-key cb-key-wide" disabled={g.won || !g.input.length} onClick={() => g.del()}
            title="Delete" aria-label="Delete">⌫</button>
          <button className="cb-key cb-key-wide cb-key-enter" disabled={g.won || !full} onClick={() => g.submit()}>
            Guess
          </button>
        </div>
      </div>
    );
  }

  function MessageLine({ g, center }) {
    return (
      <div style={{ minHeight: 20, textAlign: center ? 'center' : 'left', fontSize: 13,
        color: g.won ? 'var(--cb-accent)' : 'var(--cb-muted)', fontWeight: g.won ? 600 : 400 }}>
        {g.won ? 'Solved!' : (g.msg || (g.guesses.length ? '' : '\u00A0'))}
      </div>
    );
  }

  function Timer({ g, style }) {
    return (
      <span className="cb-mono" style={{ fontSize: 13, color: 'var(--cb-muted)', letterSpacing: '.04em', ...style }}>
        {CB.fmtTime(g.elapsed)}
      </span>
    );
  }

  function Leaderboard({ g, variant = 'list', max = 8 }) {
    const board = g.board.slice(0, max);
    const me = g.won ? CB.rankFor(g.board, g.guesses.length, g.elapsed) : null;
    const cols = '20px 1fr auto 52px';
    if (variant === 'podium') {
      const top = g.board.slice(0, 3);
      const rest = g.board.slice(3, max);
      return (
        <div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, alignItems: 'end', marginBottom: 18 }}>
            {[top[1], top[0], top[2]].map((r, i) => {
              if (!r) return <div key={i} />;
              const place = r.rank;
              const h = place === 1 ? 96 : place === 2 ? 74 : 60;
              return (
                <div key={i} style={{ textAlign: 'center' }}>
                  <div style={{ fontSize: 13, color: 'var(--cb-ink)', fontWeight: 600, marginBottom: 4,
                    overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</div>
                  <div className="cb-mono" style={{ fontSize: 11, color: 'var(--cb-muted)', marginBottom: 6 }}>{r.timeStr}</div>
                  <div style={{ height: h, borderRadius: '8px 8px 0 0',
                    background: place === 1 ? 'var(--cb-accent)' : 'var(--cb-line-strong)',
                    color: place === 1 ? 'var(--cb-bg)' : 'var(--cb-ink)',
                    display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start',
                    paddingTop: 10 }}>
                    <div className="cb-mono" style={{ fontSize: 26, fontWeight: 700, lineHeight: 1 }}>{r.guesses}</div>
                    <div style={{ fontSize: 9, letterSpacing: '.12em', textTransform: 'uppercase', opacity: .8, marginTop: 2 }}>guesses</div>
                  </div>
                  <div className="cb-mono" style={{ fontSize: 12, color: 'var(--cb-muted)', marginTop: 6 }}>#{place}</div>
                </div>
              );
            })}
          </div>
          <div>
            {rest.map((r) => (
              <div key={r.rank} className="cb-lrow" style={{ gridTemplateColumns: cols }}>
                <span className="cb-lrank">{r.rank}</span>
                <span className="cb-lname">{r.name}</span>
                <span className="cb-lg cb-mono">{r.guesses}</span>
                <span className="cb-lt">{r.timeStr}</span>
              </div>
            ))}
            {me && (
              <div className="cb-lrow cb-lrow-me" style={{ gridTemplateColumns: cols }}>
                <span className="cb-lrank" style={{ color: 'var(--cb-accent)' }}>{me}</span>
                <span className="cb-lname" style={{ fontWeight: 700 }}>You</span>
                <span className="cb-lg cb-mono">{g.guesses.length}</span>
                <span className="cb-lt">{CB.fmtTime(g.elapsed)}</span>
              </div>
            )}
          </div>
        </div>
      );
    }
    return (
      <div>
        {board.map((r) => (
          <div key={r.rank} className={'cb-lrow' + (r.rank === 1 ? ' cb-lrow-top' : '')} style={{ gridTemplateColumns: cols }}>
            <span className="cb-lrank">{r.rank === 1 ? '★' : r.rank}</span>
            <span className="cb-lname">{r.name}</span>
            <span className="cb-lg cb-mono">{r.guesses}</span>
            <span className="cb-lt">{r.timeStr}</span>
          </div>
        ))}
        {me && (
          <div className="cb-lrow cb-lrow-me" style={{ gridTemplateColumns: cols }}>
            <span className="cb-lrank" style={{ color: 'var(--cb-accent)' }}>{me}</span>
            <span className="cb-lname" style={{ fontWeight: 700 }}>You</span>
            <span className="cb-lg cb-mono">{g.guesses.length}</span>
            <span className="cb-lt">{CB.fmtTime(g.elapsed)}</span>
          </div>
        )}
      </div>
    );
  }

  // ---- shareable result card (canvas) --------------------------------------
  function readTheme() {
    const cs = getComputedStyle(document.documentElement);
    const v = (k, f) => (cs.getPropertyValue(k).trim() || f);
    return {
      bg: v('--cb-bg', '#faf9f7'), surface: v('--cb-surface', '#fff'),
      ink: v('--cb-ink', '#1c1a17'), muted: v('--cb-muted', '#8a847c'),
      faint: v('--cb-faint', '#b8b2a9'), line: v('--cb-line', '#ebe8e3'),
      lineStrong: v('--cb-line-strong', '#d7d2ca'),
      accent: v('--cb-accent', '#c2410c'), cow: v('--cb-cow', '#2563c9'),
      sans: v('--cb-sans', 'system-ui, sans-serif'),
      mono: v('--cb-mono', 'ui-monospace, monospace'),
    };
  }
  function dot(ctx, x, y, r, color, filled) {
    ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2);
    if (filled) { ctx.fillStyle = color; ctx.fill(); }
    else { ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke(); }
  }
  function drawSpacedDigits(ctx, str, cx, y, font, gap, color) {
    ctx.font = font; ctx.textAlign = 'left'; ctx.fillStyle = color;
    const w = [...str].map((ch) => ctx.measureText(ch).width);
    const total = w.reduce((a, b) => a + b, 0) + gap * (str.length - 1);
    let x = cx - total / 2;
    for (let i = 0; i < str.length; i++) { ctx.fillText(str[i], x, y); x += w[i] + gap; }
  }
  function drawWordmark(ctx, cx, y, t) {
    ctx.textBaseline = 'alphabetic';
    const emoji = '🥯', word = 'Bagels', gap = 14;
    ctx.font = `700 40px ${t.sans}`; const we = ctx.measureText(emoji).width;
    ctx.font = `700 46px ${t.sans}`; const ww = ctx.measureText(word).width;
    let x = cx - (we + gap + ww) / 2;
    ctx.textAlign = 'left';
    ctx.font = `700 40px ${t.sans}`; ctx.fillText(emoji, x, y); x += we + gap;
    ctx.font = `700 46px ${t.sans}`; ctx.fillStyle = t.ink; ctx.fillText(word, x, y);
  }
  function drawShare(canvas, g) {
    const t = readTheme();
    const seq = g.guesses.slice().reverse();
    const n = seq.length, len = g.len;
    const W = 1000, padTop = 88, padBottom = 84;
    const titleH = 116, numberH = 250, gridLabelH = 56, footerH = 78;
    const rowH = n > 9 ? Math.max(30, Math.floor(380 / n)) : 44;
    const gridH = gridLabelH + n * rowH;
    const H = padTop + titleH + numberH + gridH + footerH + padBottom;
    const dpr = 2;
    canvas.width = W * dpr; canvas.height = H * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.fillStyle = t.bg; ctx.fillRect(0, 0, W, H);
    ctx.strokeStyle = t.line; ctx.lineWidth = 2; ctx.strokeRect(30, 30, W - 60, H - 60);
    const cx = W / 2;
    let y = padTop;

    drawWordmark(ctx, cx, y + 46, t);
    const p = CB.dateParts(g.today), num = CB.puzzleNumber(g.today);
    ctx.fillStyle = t.muted; ctx.font = `500 22px ${t.mono}`; ctx.textAlign = 'center';
    ctx.fillText(`No. ${num}  ·  ${p.y} / ${p.m} / ${p.d}`, cx, y + 88);
    y += titleH;

    ctx.fillStyle = t.muted; ctx.font = `600 15px ${t.sans}`; ctx.textAlign = 'center';
    ctx.fillText('T O D A Y \u2019 S   N U M B E R', cx, y + 22);
    drawSpacedDigits(ctx, g.secret, cx, y + 168, `700 132px ${t.mono}`, 30, t.ink);
    y += numberH;

    ctx.fillStyle = t.muted; ctx.font = `600 15px ${t.sans}`; ctx.textAlign = 'center';
    ctx.fillText('E V E R Y   G U E S S', cx, y + 28);
    y += gridLabelH;

    const cellR = Math.min(9.5, rowH * 0.22);
    const cellGap = cellR * 2 + 12;
    const gridW = len * cellGap;
    for (let i = 0; i < n; i++) {
      const gr = seq[i];
      const ry = y + i * rowH + rowH / 2;
      ctx.fillStyle = t.faint; ctx.font = `500 16px ${t.mono}`; ctx.textAlign = 'right';
      ctx.fillText(String(i + 1), cx - gridW / 2 - 26, ry + 5);
      const startX = cx - gridW / 2 + cellGap / 2;
      let k = 0;
      for (let b = 0; b < gr.bulls; b++, k++) dot(ctx, startX + k * cellGap, ry, cellR, t.accent, true);
      for (let c = 0; c < gr.cows; c++, k++) dot(ctx, startX + k * cellGap, ry, cellR, t.cow, true);
      for (; k < len; k++) dot(ctx, startX + k * cellGap, ry, cellR, t.lineStrong, false);
    }
    y += n * rowH + 20;

    const rank = CB.rankFor(g.board, n, g.elapsed);
    ctx.fillStyle = t.ink; ctx.font = `600 23px ${t.sans}`; ctx.textAlign = 'center';
    ctx.fillText(`Solved in ${n} \u00b7 ${CB.fmtTime(g.elapsed)} \u00b7 #${rank} today`, cx, y + 36);
  }
  function shareText(g) {
    const seq = g.guesses.slice().reverse();
    const head = `Bagels No.${CB.puzzleNumber(g.today)} — ${seq.length} guesses · ${CB.fmtTime(g.elapsed)}`;
    const grid = seq.map((gr) => {
      let s = '';
      for (let i = 0; i < gr.bulls; i++) s += '🟧';
      for (let i = 0; i < gr.cows; i++) s += '🟦';
      for (let k = gr.bulls + gr.cows; k < g.len; k++) s += '⬜';
      return s;
    }).join('\n');
    return head + '\n' + grid;
  }

  const SHARE_URL = (typeof window !== 'undefined' && window.CB_SHARE_URL) || 'https://bagels.game';
  const NET_ICONS = {
    x: <svg width="19" height="19" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>,
    instagram: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2.5" y="2.5" width="19" height="19" rx="5.5"/><circle cx="12" cy="12" r="4.2"/><circle cx="17.3" cy="6.7" r="1.1" fill="currentColor" stroke="none"/></svg>,
    telegram: <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.78 18.65l.28-4.23 7.68-6.92c.34-.31-.07-.46-.52-.19L7.74 13.3 3.64 12c-.88-.25-.89-.86.2-1.3l15.97-6.16c.73-.33 1.43.18 1.15 1.3l-2.72 12.81c-.19.91-.74 1.13-1.5.71L12.6 16.3l-1.99 1.93c-.23.23-.42.42-.83.42z"/></svg>,
    whatsapp: <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12.04 2c-5.46 0-9.91 4.45-9.91 9.91 0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21 5.46 0 9.91-4.45 9.91-9.91S17.5 2 12.04 2zm4.52 11.99c-.25-.12-1.47-.72-1.69-.81-.23-.08-.39-.12-.56.12-.16.25-.64.81-.78.97-.14.17-.29.19-.54.06-.25-.12-1.05-.39-1.99-1.23-.74-.66-1.23-1.47-1.38-1.72-.14-.25-.02-.38.11-.5.11-.11.25-.29.37-.43.12-.14.16-.25.25-.41.08-.17.04-.31-.02-.43-.06-.12-.56-1.34-.76-1.84-.2-.48-.41-.42-.56-.42-.14 0-.31-.02-.47-.02-.17 0-.43.06-.66.31-.23.25-.86.85-.86 2.07 0 1.22.89 2.4 1.01 2.56.12.17 1.75 2.67 4.23 3.74.59.26 1.05.41 1.41.52.59.19 1.13.16 1.56.1.48-.07 1.47-.6 1.68-1.18.21-.58.21-1.07.14-1.18-.06-.11-.22-.17-.47-.29z"/></svg>,
    facebook: <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M22 12.06C22 6.5 17.52 2 12 2S2 6.5 2 12.06c0 5.02 3.66 9.18 8.44 9.94v-7.03H7.9v-2.9h2.54V9.85c0-2.51 1.49-3.9 3.78-3.9 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56v1.88h2.78l-.44 2.9h-2.34V22c4.78-.76 8.44-4.92 8.44-9.94z"/></svg>,
  };

  function SocialBtn({ label, color, onClick, children }) {
    const [h, setH] = useState(false);
    return (
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
        <button title={'Share to ' + label} onClick={onClick}
          onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
          style={{ width: 48, height: 48, borderRadius: 24, border: '1px solid var(--cb-line-strong)',
            background: h ? color : 'var(--cb-surface)', color: h ? '#fff' : 'var(--cb-ink)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
            transition: 'background .15s, color .15s, transform .12s, border-color .15s',
            transform: h ? 'translateY(-2px)' : 'none', borderColor: h ? 'transparent' : 'var(--cb-line-strong)' }}>
          {children}
        </button>
        <span style={{ fontSize: 11, color: 'var(--cb-muted)' }}>{label}</span>
      </div>
    );
  }

  function ShareModal({ g, onClose }) {
    const ref = useRef(null);
    const [copied, setCopied] = useState('');
    const [toast, setToast] = useState('');
    useEffect(() => {
      let cancel = false;
      (async () => { try { await document.fonts.ready; } catch (e) {} if (!cancel && ref.current) drawShare(ref.current, g); })();
      return () => { cancel = true; };
    }, []);
    useEffect(() => {
      const k = (e) => { if (e.key === 'Escape') onClose(); };
      document.addEventListener('keydown', k, true);
      return () => document.removeEventListener('keydown', k, true);
    }, [onClose]);
    const flash = (w) => { setCopied(w); setTimeout(() => setCopied(''), 1600); };
    const blip = (m) => { setToast(m); setTimeout(() => setToast(''), 2400); };
    const toBlob = () => new Promise((r) => ref.current.toBlob(r, 'image/png'));
    const download = async () => {
      const bl = await toBlob();
      const a = document.createElement('a');
      a.href = URL.createObjectURL(bl); a.download = `bagels-${CB.puzzleNumber(g.today)}.png`; a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 1000);
    };
    const copyImg = async () => {
      try { await navigator.clipboard.write([new ClipboardItem({ 'image/png': await toBlob() })]); flash('image'); }
      catch (e) { download(); }
    };
    const copyTxt = async () => { try { await navigator.clipboard.writeText(shareText(g) + '\n' + SHARE_URL); flash('text'); } catch (e) {} };

    // Native OS share sheet — shares the real PNG to ANY app (Instagram, X,
    // Telegram, Messages…). Best path on mobile. Returns false if unsupported.
    const nativeShare = async () => {
      try {
        const file = new File([await toBlob()], `bagels-${CB.puzzleNumber(g.today)}.png`, { type: 'image/png' });
        if (navigator.canShare && navigator.canShare({ files: [file] })) {
          await navigator.share({ files: [file], text: shareText(g), title: 'Bagels' });
          return true;
        }
      } catch (e) { if (e && e.name === 'AbortError') return true; }
      return false;
    };
    const openIntent = (href) => window.open(href, '_blank', 'noopener,noreferrer');
    const txt = encodeURIComponent(shareText(g));
    const url = encodeURIComponent(SHARE_URL);
    const intents = {
      x: () => openIntent(`https://twitter.com/intent/tweet?text=${txt}&url=${url}`),
      telegram: () => openIntent(`https://t.me/share/url?url=${url}&text=${txt}`),
      whatsapp: () => openIntent(`https://wa.me/?text=${encodeURIComponent(shareText(g) + '\n' + SHARE_URL)}`),
      facebook: () => openIntent(`https://www.facebook.com/sharer/sharer.php?u=${url}`),
    };
    // Instagram has no web post URL — try the native sheet, else save the
    // image so they can post it manually.
    const toInstagram = async () => {
      if (await nativeShare()) return;
      await download(); blip('Image saved — open Instagram and post it from your camera roll.');
    };
    const tryX = async () => { if (!(await nativeShare())) intents.x(); };

    const nets = [
      { key: 'x', label: 'X', color: '#000', onClick: tryX },
      { key: 'instagram', label: 'Instagram', color: 'linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888)', onClick: toInstagram },
      { key: 'telegram', label: 'Telegram', color: '#229ED9', onClick: intents.telegram },
      { key: 'whatsapp', label: 'WhatsApp', color: '#25D366', onClick: intents.whatsapp },
      { key: 'facebook', label: 'Facebook', color: '#1877F2', onClick: intents.facebook },
    ];

    return ReactDOM.createPortal(
      <div onPointerDown={onClose} style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(20,16,12,.62)',
        backdropFilter: 'blur(8px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
        fontFamily: 'var(--cb-sans)' }}>
        <div onPointerDown={(e) => e.stopPropagation()} style={{ background: 'var(--cb-surface)', borderRadius: 16,
          padding: 26, width: 'min(420px, 92vw)', maxHeight: '92vh', overflow: 'auto', boxShadow: '0 24px 80px rgba(0,0,0,.4)', color: 'var(--cb-ink)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
            <div style={{ fontWeight: 700, fontSize: 17 }}>Share your result</div>
            <button className="cb-link" style={{ textDecoration: 'none', fontSize: 20, lineHeight: 1 }} onClick={onClose}>×</button>
          </div>
          <canvas ref={ref} style={{ width: '100%', height: 'auto', display: 'block', borderRadius: 10,
            border: '1px solid var(--cb-line)' }} />

          <div style={{ display: 'flex', justifyContent: 'center', gap: 14, marginTop: 20, flexWrap: 'wrap' }}>
            {nets.map((n) => (
              <SocialBtn key={n.key} label={n.label} color={n.color} onClick={n.onClick}>{NET_ICONS[n.key]}</SocialBtn>
            ))}
          </div>

          <div style={{ height: 1, background: 'var(--cb-line)', margin: '20px 0 16px' }} />

          <div style={{ display: 'flex', gap: 10 }}>
            <button className="cb-cta" style={{ flex: 1 }} onClick={copyImg}>{copied === 'image' ? 'Copied ✓' : 'Copy image'}</button>
            <button className="cb-cta" style={{ flex: 1, background: 'transparent', color: 'var(--cb-ink)', borderColor: 'var(--cb-line-strong)' }} onClick={download}>Download</button>
          </div>
          <button className="cb-link" style={{ marginTop: 14, display: 'block', marginInline: 'auto' }} onClick={copyTxt}>
            {copied === 'text' ? 'Copied ✓' : 'Copy as emoji grid'}
          </button>

          {toast && (
            <div style={{ marginTop: 16, fontSize: 12.5, color: 'var(--cb-muted)', textAlign: 'center', lineHeight: 1.5 }}>{toast}</div>
          )}
        </div>
      </div>,
      document.body,
    );
  }

  function WinNote({ g, inline }) {
    const [share, setShare] = useState(false);
    const rank = CB.rankFor(g.board, g.guesses.length, g.elapsed);
    return (
      <div style={{ animation: 'cb-fade .4s ease' }}>
        <div style={{ fontSize: 14, color: 'var(--cb-ink)' }}>
          Solved in <b className="cb-mono">{g.guesses.length}</b> guesses · <span className="cb-mono">{CB.fmtTime(g.elapsed)}</span>
        </div>
        <div style={{ fontSize: 13, color: 'var(--cb-muted)', marginTop: 4 }}>
          That puts you at <b style={{ color: 'var(--cb-accent)' }}>#{rank}</b> on today's board.
        </div>
        <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginTop: 14 }}>
          <button className="cb-cta" onClick={() => setShare(true)}>Share results</button>
          <button className="cb-link" onClick={() => g.reset()}>Practice round →</button>
        </div>
        {share && <ShareModal g={g} onClose={() => setShare(false)} />}
      </div>
    );
  }

  Object.assign(window, {
    useGame, useKeyboard, Wordmark, Legend, Slots, ResultPips, Keypad,
    MessageLine, Timer, Leaderboard, WinNote, ShareModal,
  });
})();
