// bingo.jsx — TFT GW Bingo card

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "purple-gold",
  "fontFamily": "serif-jp",
  "stampStyle": "star",
  "darkMode": false,
  "highlightBingo": true,
  "centerGoalText": "TFTを1回プレイする",
  "cell25Text": "自分の目標を入れる"
}/*EDITMODE-END*/;

const CELLS = [
  // Row 1
  "プラチナに到達",
  "4コスを★3にする",
  "レベル10に到達",
  "3-2までに★3を作る",
  "50ゲームする",
  // Row 2
  "連敗ボーナスでアニマを発動(800テック)",
  "マスターに行く",
  "ブースターパックを5回取る",
  "5コスを★3にする",
  "富の分かち合い+で全員がアーリを取る",
  // Row 3
  "専用オーグをとる",
  "全部紋章のオーグメントを取る",
  null, // center — replaced by centerGoalText (default: TFTを1回プレイする)
  "21ゴールドを獲得をとる",
  "ダブルアップを異性とプレイ",
  // Row 4
  "ノーダメージで勝利(HP100維持)",
  "5回連続で1位を取る",
  "ダークスター6発動",
  "★学長試練★ 星の観測者11(隠し特性)発動",
  "アーティファクトを3つつけたチャンピオンで勝利",
  // Row 5
  "5回連続で上位に入る",
  "グレイブスを連続8ターン出す",
  "虹シナジーを出す",
  "全部5コス盤面で勝利",
  null, // cell 25 — replaced by cell25Text (user free-entry)
];

// ── Themes ─────────────────────────────────────────────────────────────────
const THEMES = {
  "purple-gold": {
    parchmentA: "#f6ead0",
    parchmentB: "#e8d6a8",
    parchmentEdge: "#7a5a2a",
    ink: "#2a1a44",
    inkSoft: "#5a4a78",
    accent: "#c9a13b",
    accent2: "#5b2a8a",
    accent3: "#8a3fc8",
    cellBg: "rgba(255,248,224,0.55)",
    cellBorder: "rgba(91,42,138,0.35)",
    bingo: "#c9a13b",
    bingoGlow: "rgba(201,161,59,0.55)",
  },
  "crimson-gold": {
    parchmentA: "#f6ead0",
    parchmentB: "#e8c8a0",
    parchmentEdge: "#6b3a1a",
    ink: "#3a0e0e",
    inkSoft: "#6a2a2a",
    accent: "#caa040",
    accent2: "#962020",
    accent3: "#d24a2a",
    cellBg: "rgba(255,246,222,0.55)",
    cellBorder: "rgba(150,32,32,0.32)",
    bingo: "#caa040",
    bingoGlow: "rgba(202,160,64,0.55)",
  },
  "emerald-gold": {
    parchmentA: "#f4ecd2",
    parchmentB: "#e0d4a4",
    parchmentEdge: "#4a4a18",
    ink: "#1a3a26",
    inkSoft: "#3a5a40",
    accent: "#caa040",
    accent2: "#1f6a3e",
    accent3: "#2f9a5e",
    cellBg: "rgba(255,250,228,0.55)",
    cellBorder: "rgba(31,106,62,0.32)",
    bingo: "#caa040",
    bingoGlow: "rgba(202,160,64,0.55)",
  },
  "midnight-gold": {
    parchmentA: "#3a2a52",
    parchmentB: "#1f1638",
    parchmentEdge: "#0a0418",
    ink: "#f4e4b0",
    inkSoft: "#c8b888",
    accent: "#e6c258",
    accent2: "#9a6cd8",
    accent3: "#c89af0",
    cellBg: "rgba(20,10,40,0.45)",
    cellBorder: "rgba(230,194,88,0.35)",
    bingo: "#e6c258",
    bingoGlow: "rgba(230,194,88,0.65)",
  },
};

const FONT_FAMILIES = {
  "serif-jp": `"Shippori Mincho", "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", serif`,
  "sans-jp": `"Zen Kaku Gothic New", "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif`,
  "rounded-jp": `"Klee One", "Zen Maru Gothic", "Yu Gothic", sans-serif`,
  "display": `"Cinzel", "Shippori Mincho", "Noto Serif JP", serif`,
};

// ── Stamps ─────────────────────────────────────────────────────────────────
function Stamp({ kind, color }) {
  if (kind === "star") {
    return (
      <svg viewBox="0 0 100 100" className="stamp-svg" aria-hidden="true">
        <defs>
          <radialGradient id="starGlow" cx="50%" cy="50%" r="60%">
            <stop offset="0%" stopColor={color} stopOpacity="0.55" />
            <stop offset="70%" stopColor={color} stopOpacity="0" />
          </radialGradient>
        </defs>
        <circle cx="50" cy="50" r="46" fill="url(#starGlow)" />
        <path d="M50 10 L61 39 L92 41 L67 60 L76 90 L50 73 L24 90 L33 60 L8 41 L39 39 Z"
              fill={color} stroke="#fff" strokeWidth="2.5" strokeLinejoin="round" />
        <path d="M50 22 L57 41 L77 42 L62 54 L67 73 L50 62 Z" fill="rgba(255,255,255,0.35)" />
      </svg>
    );
  }
  if (kind === "hanko") {
    return (
      <svg viewBox="0 0 100 100" className="stamp-svg" aria-hidden="true">
        <circle cx="50" cy="50" r="40" fill="none" stroke={color} strokeWidth="6" />
        <text x="50" y="62" textAnchor="middle"
              fontFamily='"Shippori Mincho", serif' fontWeight="700"
              fontSize="44" fill={color}>済</text>
      </svg>
    );
  }
  if (kind === "check") {
    return (
      <svg viewBox="0 0 100 100" className="stamp-svg" aria-hidden="true">
        <circle cx="50" cy="50" r="38" fill={color} opacity="0.18" />
        <path d="M28 52 L44 68 L74 34" fill="none" stroke={color}
              strokeWidth="9" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    );
  }
  if (kind === "coin") {
    return (
      <svg viewBox="0 0 100 100" className="stamp-svg" aria-hidden="true">
        <circle cx="50" cy="50" r="42" fill={color} />
        <circle cx="50" cy="50" r="42" fill="none" stroke="rgba(0,0,0,0.25)" strokeWidth="2" />
        <circle cx="50" cy="50" r="34" fill="none" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" />
        <path d="M30 58 L36 38 L44 50 L50 34 L56 50 L64 38 L70 58 Z"
              fill="rgba(255,255,255,0.85)" stroke="rgba(0,0,0,0.3)" strokeWidth="1.2"
              strokeLinejoin="round" />
        <circle cx="36" cy="40" r="2" fill="rgba(0,0,0,0.35)" />
        <circle cx="50" cy="36" r="2" fill="rgba(0,0,0,0.35)" />
        <circle cx="64" cy="40" r="2" fill="rgba(0,0,0,0.35)" />
      </svg>
    );
  }
  return null;
}

// ── Card ───────────────────────────────────────────────────────────────────
function BingoApp() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [stamped, setStamped] = React.useState(() => Array(25).fill(false));
  const [pulse, setPulse] = React.useState(-1);
  const [isCapturing, setIsCapturing] = React.useState(false);
  const [isSaving, setIsSaving] = React.useState(false);
  const [captureError, setCaptureError] = React.useState('');
  const [shareHint, setShareHint] = React.useState('');
  const [editingCell, setEditingCell] = React.useState(-1);
  const [cardScale, setCardScale] = React.useState(0.85);
  const [barPos, setBarPos] = React.useState(null); // null = CSS default (fixed bottom center)
  const [isDragging, setIsDragging] = React.useState(false);

  const SIZES = [
    { label: '小', scale: 0.65 },
    { label: '中', scale: 0.85 },
    { label: '大', scale: 1.0  },
  ];

  const onBarDragStart = (clientX, clientY, rect) => {
    const offsetX = clientX - rect.left;
    const offsetY = clientY - rect.top;
    setIsDragging(true);

    const onMouseMove = (e) => {
      setBarPos({ x: e.clientX - offsetX, y: e.clientY - offsetY });
    };
    const onTouchMove = (e) => {
      e.preventDefault();
      const t = e.touches[0];
      setBarPos({ x: t.clientX - offsetX, y: t.clientY - offsetY });
    };
    const onEnd = () => {
      setIsDragging(false);
      window.removeEventListener('mousemove', onMouseMove);
      window.removeEventListener('mouseup', onEnd);
      window.removeEventListener('touchmove', onTouchMove);
      window.removeEventListener('touchend', onEnd);
    };
    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('mouseup', onEnd);
    window.addEventListener('touchmove', onTouchMove, { passive: false });
    window.addEventListener('touchend', onEnd);
  };

  const handleBarMouseDown = (e) => {
    if (e.target.closest('button')) return;
    e.preventDefault();
    onBarDragStart(e.clientX, e.clientY, e.currentTarget.getBoundingClientRect());
  };

  const handleBarTouchStart = (e) => {
    if (e.target.closest('button')) return;
    const t = e.touches[0];
    onBarDragStart(t.clientX, t.clientY, e.currentTarget.getBoundingClientRect());
  };

  const barStyle = barPos
    ? { left: barPos.x, top: barPos.y, bottom: 'auto', transform: 'none', cursor: isDragging ? 'grabbing' : 'grab' }
    : { cursor: isDragging ? 'grabbing' : 'grab' };

  const theme = THEMES[t.theme] || THEMES["purple-gold"];
  const fontStack = FONT_FAMILIES[t.fontFamily] || FONT_FAMILIES["serif-jp"];

  const isDark = t.darkMode || t.theme === "midnight-gold";

  const winningCells = React.useMemo(() => {
    if (!t.highlightBingo) return new Set();
    const lines = [];
    for (let r = 0; r < 5; r++) lines.push([0,1,2,3,4].map(c => r*5+c));
    for (let c = 0; c < 5; c++) lines.push([0,1,2,3,4].map(r => r*5+c));
    lines.push([0,6,12,18,24]);
    lines.push([4,8,12,16,20]);
    const winners = new Set();
    for (const line of lines) {
      if (line.every(i => stamped[i])) line.forEach(i => winners.add(i));
    }
    return winners;
  }, [stamped, t.highlightBingo]);

  const bingoCount = React.useMemo(() => {
    let count = 0;
    const lines = [];
    for (let r = 0; r < 5; r++) lines.push([0,1,2,3,4].map(c => r*5+c));
    for (let c = 0; c < 5; c++) lines.push([0,1,2,3,4].map(r => r*5+c));
    lines.push([0,6,12,18,24]);
    lines.push([4,8,12,16,20]);
    for (const line of lines) if (line.every(i => stamped[i])) count++;
    return count;
  }, [stamped]);

  const stampedCount = stamped.filter(Boolean).length;

  const toggle = (i) => {
    setStamped(prev => {
      const next = [...prev];
      next[i] = !next[i];
      return next;
    });
    setPulse(i);
    setTimeout(() => setPulse(-1), 600);
  };

  const reset = () => setStamped(Array(25).fill(false));

  // Shared image capture helper
  const captureBlob = async () => {
    const target = document.querySelector('.frame-outer');
    if (!target) return null;
    const filterFn = (node) => {
      if (!node || !node.classList) return true;
      return !node.classList.contains('twk-panel') && !node.classList.contains('site-header');
    };
    if (window.htmlToImage) {
      try {
        const dataUrl = await window.htmlToImage.toPng(target, {
          pixelRatio: 2, backgroundColor: '#1a1228', cacheBust: true, filter: filterFn,
        });
        const res = await fetch(dataUrl);
        return await res.blob();
      } catch (e) { console.warn('html-to-image failed:', e); }
    }
    if (window.html2canvas) {
      try {
        const canvas = await window.html2canvas(target, {
          scale: 2, useCORS: true, backgroundColor: '#1a1228', logging: false,
          ignoreElements: (el) => el.classList && (el.classList.contains('twk-panel') || el.classList.contains('site-header')),
        });
        return await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
      } catch (e) { console.warn('html2canvas failed:', e); }
    }
    return null;
  };

  const tweetText = `ゴールデンウィークTFTビンゴ\n${bingoCount}ビンゴをしました！\nあなたは何ビンゴまでできるかな？\n#TFT #TFTビンゴ2026 #TFTDUO`;

  const handleSave = async () => {
    if (isSaving || isCapturing) return;
    setIsSaving(true);
    try {
      const blob = await captureBlob();
      if (!blob) return;
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `tft-bingo-${bingoCount}bingo.png`;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } finally {
      setIsSaving(false);
    }
  };

  const handleShare = async () => {
    if (isCapturing || isSaving) return;
    setIsCapturing(true);
    setCaptureError('');
    setShareHint('');
    try {
      const blob = await captureBlob();
      const file = blob ? new File([blob], 'tft-bingo.png', { type: 'image/png' }) : null;

      // Mobile: Web Share API with image file
      if (file && navigator.canShare && navigator.canShare({ files: [file] })) {
        try {
          await navigator.share({ text: tweetText, files: [file] });
          return;
        } catch (e) {
          if (e.name === 'AbortError') return;
        }
      }

      // Desktop: upload to server → Twitter Card URL (single URL in tweet)
      if (blob) {
        try {
          const res = await fetch('/api/bingo-share', {
            method: 'POST',
            headers: { 'Content-Type': 'image/png' },
            body: blob,
          });
          if (res.ok) {
            const { shareUrl } = await res.json();
            // shareUrl only — no siteUrl in text to avoid duplicate URLs
            window.open(
              `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(shareUrl)}`,
              '_blank', 'noopener,noreferrer,width=560,height=620'
            );
            return;
          }
        } catch (e) { console.warn('server upload failed:', e); }

        // Clipboard fallback
        try {
          await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
          setShareHint('画像をクリップボードにコピーしました。ツイート欄に貼り付け (Ctrl+V / ⌘V) してください');
          setTimeout(() => setShareHint(''), 10000);
        } catch (_) {}
      }

      window.open(
        `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`,
        '_blank', 'noopener,noreferrer,width=560,height=620'
      );
    } finally {
      setTimeout(() => setIsCapturing(false), 300);
    }
  };

  const cardBg = isDark
    ? `radial-gradient(ellipse at 30% 20%, ${theme.parchmentA} 0%, ${theme.parchmentB} 70%, ${theme.parchmentEdge} 100%)`
    : `radial-gradient(ellipse at 50% 0%, ${theme.parchmentA} 0%, ${theme.parchmentB} 75%, ${theme.parchmentEdge} 100%)`;

  return (
    <div className="page" style={{
      "--ink": theme.ink,
      "--ink-soft": theme.inkSoft,
      "--accent": theme.accent,
      "--accent2": theme.accent2,
      "--accent3": theme.accent3,
      "--bingo": theme.bingo,
      "--bingo-glow": theme.bingoGlow,
      "--cell-bg": theme.cellBg,
      "--cell-border": theme.cellBorder,
      "--font": fontStack,
    }}>
      <div className="frame-outer" style={{ '--card-scale': cardScale }}>
        <div className="card" style={{ background: cardBg }}>
          <div className="parchment-grain" />
          <div className="parchment-stains" />

          <CornerOrnament pos="tl" />
          <CornerOrnament pos="tr" />
          <CornerOrnament pos="bl" />
          <CornerOrnament pos="br" />

          <header className="card-hd">
            <div className="hd-flourish hd-flourish-l">
              <FlourishL />
            </div>
            <div className="hd-text">
              <div className="hd-eyebrow">— Golden Week 2026 —</div>
              <h1 className="hd-title">
                <span className="title-line title-line-1">ゴールデンウィーク</span>
                <span className="title-line title-line-2">TFT <em>ビンゴ</em>!</span>
              </h1>
              <div className="hd-sub">挑戦者よ、25 の試練に挑め</div>
            </div>
            <div className="hd-flourish hd-flourish-r">
              <FlourishL flip />
            </div>
          </header>

          <div className="hd-divider" aria-hidden="true">
            <span className="diamond" />
            <span className="rule" />
            <span className="diamond center-diamond" />
            <span className="rule" />
            <span className="diamond" />
          </div>

          <div className="grid" role="grid" aria-label="TFT Bingo">
            {CELLS.map((label, i) => {
              const isCenter = i === 12;
              const isFreeCell = i === 24;
              const editable = isCenter || isFreeCell;
              const text = isCenter ? t.centerGoalText : (isFreeCell ? t.cell25Text : label);
              const isStamped = stamped[i];
              const isWinner = winningCells.has(i);
              const isEditing = editingCell === i;
              return (
                <button
                  key={i}
                  role="gridcell"
                  className={[
                    "cell",
                    isStamped && "is-stamped",
                    isWinner && "is-winner",
                    pulse === i && "is-pulse",
                    isCenter && "is-center",
                    isFreeCell && "is-free",
                    editable && "is-editable",
                    isEditing && "is-editing",
                  ].filter(Boolean).join(" ")}
                  onClick={() => { if (!isEditing) toggle(i); }}
                  aria-pressed={isStamped}
                >
                  <span className="cell-corner cell-corner-tl">✦</span>
                  <span className="cell-corner cell-corner-br">✦</span>
                  <span className="cell-num">{String(i + 1).padStart(2, "0")}</span>
                  {isEditing ? (
                    <textarea
                      className="cell-input"
                      autoFocus
                      defaultValue={text}
                      onClick={(e) => e.stopPropagation()}
                      onBlur={(e) => {
                        const v = e.target.value.trim() || (isCenter ? "TFTを1回プレイする" : "自分の目標を入れる");
                        setTweak(isCenter ? "centerGoalText" : "cell25Text", v);
                        setEditingCell(-1);
                      }}
                      onKeyDown={(e) => {
                        if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); e.target.blur(); }
                        if (e.key === "Escape") { e.target.value = text; e.target.blur(); }
                      }}
                    />
                  ) : (
                    <span className="cell-text">{text}</span>
                  )}
                  {editable && !isEditing && (
                    <span
                      className="cell-edit"
                      role="button"
                      aria-label="編集"
                      onClick={(e) => { e.stopPropagation(); setEditingCell(i); }}
                    >
                      <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">
                        <path fill="currentColor" d="M11.5 1.5a1.7 1.7 0 0 1 2.4 2.4l-.9.9-2.4-2.4zM10 4l2.4 2.4L4.4 14.4 2 15l.6-2.4z"/>
                      </svg>
                    </span>
                  )}
                  {isStamped && (
                    <span className="cell-stamp">
                      <Stamp kind={t.stampStyle} color={theme.accent2} />
                    </span>
                  )}
                </button>
              );
            })}
          </div>

          <div className="hd-divider" aria-hidden="true">
            <span className="diamond" />
            <span className="rule" />
            <span className="diamond center-diamond" />
            <span className="rule" />
            <span className="diamond" />
          </div>
          <footer className="card-ft">
            <div className="ft-stat">
              <div className="ft-stat-num">{stampedCount}<span className="ft-stat-den">/25</span></div>
              <div className="ft-stat-lbl">クリア</div>
            </div>
            <div className="ft-divider" />
            <div className="ft-stat ft-stat-bingo">
              <div className="ft-stat-num">
                {bingoCount > 0 && <span className="bingo-badge">BINGO</span>}
                {bingoCount === 0 && <span className="ft-stat-zero">—</span>}
              </div>
              <div className="ft-stat-lbl">{bingoCount}列達成</div>
            </div>
            <div className="ft-divider" />
            <div className="ft-stat ft-stat-action">
              <button className="reset-btn" onClick={reset} aria-label="リセット">
                <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
                  <path d="M4 12a8 8 0 0 1 13.66-5.66L20 4v6h-6l2.34-2.34A6 6 0 1 0 18 12"
                        fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
                やり直す
              </button>
              <button
                className="x-share-btn"
                disabled={isCapturing || isSaving}
                onClick={handleShare}
                aria-label="Xでシェア"
              >
                <svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true">
                  <path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-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>
                <span>{isCapturing ? 'シェアを準備中…' : 'Xにシェア'}</span>
              </button>
            </div>
          </footer>

          <div className="card-sig">#TFT #ゴールデンウィーク #ビンゴ</div>
        </div>
      </div>

      <div
        className={`page-actions${isDragging ? ' is-dragging' : ''}`}
        style={barStyle}
        onMouseDown={handleBarMouseDown}
        onTouchStart={handleBarTouchStart}
      >
        <span className="bar-grip" aria-hidden="true">⠿</span>
        <div className="size-btn-group" role="group" aria-label="カードサイズ">
          {SIZES.map(({ label, scale }) => (
            <button
              key={label}
              className={`size-btn${cardScale === scale ? ' active' : ''}`}
              onClick={() => setCardScale(scale)}
            >{label}</button>
          ))}
        </div>
        <div className="page-sep" />
        <button
          className="save-img-btn"
          disabled={isSaving || isCapturing}
          onClick={handleSave}
          aria-label="画像を保存"
        >
          <svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true">
            <path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
              d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
          </svg>
          {isSaving ? '保存中…' : '画像を保存'}
        </button>
      </div>

      {captureError && (
        <div className="x-share-error">{captureError}</div>
      )}
      {shareHint && (
        <div className="x-share-hint">{shareHint}</div>
      )}

      {isCapturing && (
        <div className="share-overlay" aria-live="polite">
          <div className="share-overlay-box">
            <div className="share-spinner" />
            <div className="share-overlay-text">シェアを準備中…</div>
          </div>
        </div>
      )}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Theme" />
        <TweakSelect
          label="カラーテーマ"
          value={t.theme}
          options={[
            { value: "purple-gold", label: "紫×ゴールド (ロイヤル)" },
            { value: "crimson-gold", label: "紅×ゴールド (祝祭)" },
            { value: "emerald-gold", label: "翠×ゴールド (神秘)" },
            { value: "midnight-gold", label: "深夜×ゴールド (ダーク)" },
          ]}
          onChange={(v) => setTweak("theme", v)}
        />
        <TweakToggle
          label="ダークモード調整"
          value={t.darkMode}
          onChange={(v) => setTweak("darkMode", v)}
        />
        <TweakSelect
          label="フォント"
          value={t.fontFamily}
          options={[
            { value: "serif-jp", label: "明朝 (古風)" },
            { value: "sans-jp", label: "ゴシック (現代)" },
            { value: "rounded-jp", label: "丸ゴシック (柔和)" },
            { value: "display", label: "ディスプレイ (装飾)" },
          ]}
          onChange={(v) => setTweak("fontFamily", v)}
        />

        <TweakSection label="Stamp" />
        <TweakRadio
          label="スタンプの種類"
          value={t.stampStyle}
          options={[
            { value: "star", label: "★" },
            { value: "hanko", label: "済" },
            { value: "check", label: "✓" },
            { value: "coin", label: "金" },
          ]}
          onChange={(v) => setTweak("stampStyle", v)}
        />
        <TweakToggle
          label="ビンゴ判定ハイライト"
          value={t.highlightBingo}
          onChange={(v) => setTweak("highlightBingo", v)}
        />

        <TweakSection label="Custom cells" />
        <TweakText
          label="13. 中央の目標"
          value={t.centerGoalText}
          placeholder="TFTを1回プレイする"
          onChange={(v) => setTweak("centerGoalText", v)}
        />
        <TweakText
          label="25. 自由マス"
          value={t.cell25Text}
          placeholder="自分の目標を入れる"
          onChange={(v) => setTweak("cell25Text", v)}
        />

        <TweakSection label="Actions" />
        <TweakButton label="ビンゴカードをリセット" onClick={reset} secondary />
      </TweaksPanel>
    </div>
  );
}

// ── Decorative SVGs ─────────────────────────────────────────────────────────
function CornerOrnament({ pos }) {
  return (
    <svg className={`corner corner-${pos}`} viewBox="0 0 80 80" aria-hidden="true">
      <g fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
        <path d="M6 6 L6 32 M6 6 L32 6" />
        <path d="M10 10 L10 26 M10 10 L26 10" opacity="0.55" />
        <path d="M14 22 Q22 22 22 14" opacity="0.7" />
        <circle cx="6" cy="6" r="2.5" fill="currentColor" />
        <circle cx="22" cy="22" r="1.4" fill="currentColor" opacity="0.7" />
      </g>
      <g fill="currentColor" opacity="0.85">
        <path d="M6 38 L9 41 L6 44 L3 41 Z" />
      </g>
    </svg>
  );
}

function FlourishL({ flip }) {
  return (
    <svg viewBox="0 0 120 60" className={flip ? "flourish flip" : "flourish"} aria-hidden="true">
      <g fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
        <path d="M2 30 Q 30 30 50 30 M 50 30 Q 60 30 60 22 M 60 22 Q 60 14 70 14 M 70 14 Q 78 14 78 22" />
        <path d="M50 30 Q 60 30 60 38 M 60 38 Q 60 46 70 46 M 70 46 Q 78 46 78 38" opacity="0.7" />
        <path d="M85 30 L 110 30" />
        <circle cx="50" cy="30" r="2" fill="currentColor" />
        <circle cx="78" cy="30" r="2.5" fill="currentColor" />
        <path d="M115 26 L 119 30 L 115 34 L 111 30 Z" fill="currentColor" />
      </g>
    </svg>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<BingoApp />);
