// Tobeads — ExportModal (factory / supply-chain production export)
//
// Builds a production-ready export from the REAL Creator-Mode pattern data
// (pattern.grid + pattern.beadCounts). Outputs:
//   • On-screen bead map with row/col coordinates + per-cell color key
//   • Bead count table (No · chip · code · name · hex · qty)
//   • Factory pick list with a configurable waste buffer (default 5%)
//   • Exports: PDF (print sheet), PNG (production sheet), CSV (counts+grid map), JSON
//
// No backend. Nothing is faked — if no real pattern exists, an empty state
// is shown and exports are disabled.

const { useState: useStateExp, useMemo: useMemoExp, useRef: useRefExp } = React;

// ── data builder ────────────────────────────────────────────────────────
function buildExportData(pattern, brand, wastePct, notes) {
  const brandLabel = (window.BEAD_BRANDS[brand] && window.BEAD_BRANDS[brand].label) || brand;
  const grid = pattern.grid;
  const rows = grid.length, cols = grid[0].length;

  // legend: unique colors ranked by count (No. 1 = most used)
  const counts = pattern.beadCounts || window.countBeadsFromGrid(grid);
  const legend = Object.entries(counts)
    .map(([id, qty]) => {
      const b = window.BEAD_BY_ID[id] || {};
      return { beadColorId: id, brand: b.brand || brandLabel, code: b.code || '??', name: b.name || 'Unknown', hex: b.hex || '#000000', quantity: qty };
    })
    .filter(e => e.quantity > 0)
    .sort((a, b) => b.quantity - a.quantity)
    .map((e, i) => {
      const buffer = Math.ceil(e.quantity * (wastePct / 100));
      return { ...e, no: i + 1, wasteBuffer: buffer, totalWithWaste: e.quantity + buffer };
    });

  const noById = {}; legend.forEach(e => { noById[e.beadColorId] = e.no; });
  const metaById = {}; legend.forEach(e => { metaById[e.beadColorId] = e; });

  const totalBeads = legend.reduce((s, e) => s + e.quantity, 0);

  return {
    patternId: pattern.id,
    createdAt: new Date().toISOString(),
    brand: brandLabel,
    palette: brandLabel + ' Standard',
    gridSize: pattern.gridSize || rows,
    rows, cols,
    width: cols, height: rows,
    totalBeads,
    uniqueColors: legend.length,
    wastePct,
    legend, noById, metaById,
    grid,
    notes: notes || '',
  };
}

// ── tiny download helpers ────────────────────────────────────────────────
function dl(filename, mime, content) {
  const blob = content instanceof Blob ? content : new Blob([content], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename; a.click();
  setTimeout(() => URL.revokeObjectURL(url), 2000);
}
function lum(hex) {
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0,2),16), g = parseInt(h.slice(2,4),16), b = parseInt(h.slice(4,6),16);
  return (0.299*r + 0.587*g + 0.114*b) / 255;
}

// ── CSV / JSON ─────────────────────────────────────────────────────────
function exportCSV(d) {
  const esc = (v) => { const s = String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
  let out = '';
  out += `# TOBEADS PRODUCTION EXPORT\n# Pattern ID,${d.patternId}\n# Created,${d.createdAt}\n# Brand,${d.brand}\n# Grid,${d.cols}x${d.rows}\n# Total beads,${d.totalBeads}\n# Unique colors,${d.uniqueColors}\n# Waste buffer,${d.wastePct}%\n\n`;
  out += `# SECTION 1 — BEAD COUNT (FACTORY PICK LIST)\n`;
  out += `no,brand,color_code,color_name,hex,quantity,estimated_extra,total_with_waste\n`;
  d.legend.forEach(e => {
    out += [e.no, esc(e.brand), esc(e.code), esc(e.name), e.hex, e.quantity, e.wasteBuffer, e.totalWithWaste].join(',') + '\n';
  });
  out += `\n# SECTION 2 — GRID MAP (row,col → bead)\n`;
  out += `row,col,brand,color_code,color_name,hex\n`;
  for (let r = 0; r < d.rows; r++) {
    for (let c = 0; c < d.cols; c++) {
      const m = d.metaById[d.grid[r][c]];
      if (!m) continue;
      out += [r + 1, c + 1, esc(m.brand), esc(m.code), esc(m.name), m.hex].join(',') + '\n';
    }
  }
  dl(`tobeads-${d.patternId}.csv`, 'text/csv', out);
}

function exportJSON(d) {
  const obj = {
    patternId: d.patternId,
    createdAt: d.createdAt,
    brand: d.brand,
    palette: d.palette,
    gridSize: d.gridSize,
    width: d.width,
    height: d.height,
    totalBeads: d.totalBeads,
    uniqueColors: d.uniqueColors,
    wasteBuffer: d.wastePct + '%',
    beadCounts: d.legend.map(e => ({
      no: e.no, beadColorId: e.beadColorId, brand: e.brand, code: e.code,
      name: e.name, hex: e.hex, quantity: e.quantity, wasteBuffer: e.wasteBuffer, totalWithWaste: e.totalWithWaste,
    })),
    grid: d.grid.map((row, r) => row.map((id, c) => {
      const m = d.metaById[id] || {};
      return { row: r + 1, col: c + 1, beadColorId: id, brand: m.brand, code: m.code, name: m.name, hex: m.hex };
    })),
    notes: d.notes,
  };
  dl(`tobeads-${d.patternId}.json`, 'application/json', JSON.stringify(obj, null, 2));
}

// ── production sheet → canvas (PNG) ───────────────────────────────────────
function drawProductionSheet(d, { cellSize = 18, cellMode = 'both', showGridLabels = true } = {}) {
  const M = 44;                       // page margin
  const cw = cellSize;
  const gut = showGridLabels ? Math.max(24, cw) : 0;  // label gutter
  const gridW = d.cols * cw, gridH = d.rows * cw;
  const gridBlockW = gut + gridW;
  // table geometry
  const colX = [0, 44, 86, 150, 360, 470, 560, 660]; // relative offsets
  const tableW = 740;
  const rowH = 26;
  const tableH = 40 + (d.legend.length + 1) * rowH;
  const notesH = 120;

  const contentW = Math.max(gridBlockW, tableW);
  const W = M * 2 + contentW;
  const headerH = 96;
  const metaH = 54;
  const gridLabelH = 34;
  const H = M + headerH + metaH + gridLabelH + gridBlockH(gridH, gut) + 40 + 34 + tableH + 34 + notesH + M;

  function gridBlockH(gh, g) { return g + gh; }

  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const cv = document.createElement('canvas');
  cv.width = W * dpr; cv.height = H * dpr;
  const ctx = cv.getContext('2d');
  ctx.scale(dpr, dpr);
  ctx.textBaseline = 'top';

  // white page
  ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, W, H);

  let y = M;
  // header
  ctx.fillStyle = '#7F00FF'; ctx.font = '700 30px Space Grotesk, system-ui, sans-serif';
  ctx.fillText('TO', M, y);
  const tw = ctx.measureText('TO').width;
  ctx.fillStyle = '#000080'; ctx.fillText('BEADS', M + tw, y);
  ctx.fillStyle = '#111'; ctx.font = '700 13px monospace';
  ctx.fillText('BEAD PRODUCTION SHEET', M, y + 38);
  // right meta
  ctx.textAlign = 'right'; ctx.fillStyle = '#444'; ctx.font = '11px monospace';
  ctx.fillText('PATTERN  ' + d.patternId, W - M, y + 2);
  ctx.fillText('DATE  ' + d.createdAt.slice(0, 10), W - M, y + 20);
  ctx.fillText('WASTE BUFFER  ' + d.wastePct + '%', W - M, y + 38);
  ctx.textAlign = 'left';
  y += headerH;

  // divider
  ctx.strokeStyle = '#111'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(M, y); ctx.lineTo(W - M, y); ctx.stroke();
  y += 16;

  // meta chips
  const metas = [
    ['BRAND / PALETTE', d.brand + ' · ' + d.palette],
    ['GRID', d.cols + ' × ' + d.rows],
    ['TOTAL BEADS', d.totalBeads.toLocaleString()],
    ['UNIQUE COLORS', String(d.uniqueColors)],
    ['BUILD SIZE', (d.cols * 0.5).toFixed(0) + ' × ' + (d.rows * 0.5).toFixed(0) + ' cm'],
  ];
  let mx = M;
  const mwAll = (W - M * 2) / metas.length;
  metas.forEach(([k, v]) => {
    ctx.fillStyle = '#888'; ctx.font = '9px monospace';
    ctx.fillText(k, mx, y);
    ctx.fillStyle = '#111'; ctx.font = '700 14px Space Grotesk, system-ui, sans-serif';
    ctx.fillText(v, mx, y + 14);
    mx += mwAll;
  });
  y += metaH;

  // grid title
  ctx.fillStyle = '#111'; ctx.font = '700 12px monospace';
  ctx.fillText('BEAD MAP — ROW / COL COORDINATES' + (cellMode === 'print' ? ' (PRINTABLE)' : ''), M, y);
  y += gridLabelH;

  // grid origin
  const gx = M + gut, gy = y + gut;
  // column numbers (every 5)
  ctx.font = '9px monospace'; ctx.fillStyle = '#555'; ctx.textAlign = 'center';
  if (showGridLabels) {
    for (let c = 0; c < d.cols; c++) {
      if (c === 0 || (c + 1) % 5 === 0) ctx.fillText(String(c + 1), gx + c * cw + cw / 2, y + 6);
    }
    // row numbers
    ctx.textAlign = 'right';
    for (let r = 0; r < d.rows; r++) {
      if (r === 0 || (r + 1) % 5 === 0) ctx.fillText(String(r + 1), gx - 6, gy + r * cw + cw / 2 - 5);
    }
    ctx.textAlign = 'left';
  }

  // cells
  const showNo = (cellMode === 'no' || cellMode === 'both' || cellMode === 'print') && cw >= 13;
  ctx.textAlign = 'center';
  for (let r = 0; r < d.rows; r++) {
    for (let c = 0; c < d.cols; c++) {
      const id = d.grid[r][c];
      const m = d.metaById[id];
      const x = gx + c * cw, yy = gy + r * cw;
      if (cellMode === 'print') {
        ctx.fillStyle = '#FFFFFF'; ctx.fillRect(x, yy, cw, cw);
        // small swatch corner
        ctx.fillStyle = m ? m.hex : '#fff'; ctx.fillRect(x + 1, yy + 1, Math.max(3, cw * 0.28), Math.max(3, cw * 0.28));
      } else {
        ctx.fillStyle = m ? m.hex : '#FFFFFF'; ctx.fillRect(x, yy, cw, cw);
      }
      if (showNo && m) {
        ctx.fillStyle = cellMode === 'print' ? '#111' : (lum(m.hex) > 0.55 ? '#111' : '#fff');
        ctx.font = `${Math.floor(cw * 0.46)}px monospace`;
        ctx.fillText(String(m.no), x + cw / 2, yy + cw / 2 - Math.floor(cw * 0.28));
      }
    }
  }
  // gridlines (thin) + bold every 10
  ctx.strokeStyle = 'rgba(0,0,0,0.14)'; ctx.lineWidth = 1;
  for (let c = 0; c <= d.cols; c++) { ctx.beginPath(); ctx.moveTo(gx + c * cw, gy); ctx.lineTo(gx + c * cw, gy + gridH); ctx.stroke(); }
  for (let r = 0; r <= d.rows; r++) { ctx.beginPath(); ctx.moveTo(gx, gy + r * cw); ctx.lineTo(gx + gridW, gy + r * cw); ctx.stroke(); }
  ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1.5;
  for (let c = 0; c <= d.cols; c += 10) { ctx.beginPath(); ctx.moveTo(gx + c * cw, gy); ctx.lineTo(gx + c * cw, gy + gridH); ctx.stroke(); }
  for (let r = 0; r <= d.rows; r += 10) { ctx.beginPath(); ctx.moveTo(gx, gy + r * cw); ctx.lineTo(gx + gridW, gy + r * cw); ctx.stroke(); }
  ctx.textAlign = 'left';

  y = gy + gridH + 40;

  // count table
  ctx.fillStyle = '#111'; ctx.font = '700 12px monospace';
  ctx.fillText('BEAD COUNT & FACTORY PICK LIST  [' + d.brand.toUpperCase() + ' — ' + d.totalBeads.toLocaleString() + ' BEADS]', M, y);
  y += 22;
  const tx = M;
  const headers = ['NO', 'CHIP', 'CODE', 'COLOR NAME', 'HEX', 'QTY', '+' + d.wastePct + '%', 'TOTAL'];
  ctx.font = '9px monospace'; ctx.fillStyle = '#888';
  headers.forEach((h, i) => ctx.fillText(h, tx + colX[i], y));
  y += 8;
  ctx.strokeStyle = '#111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(M, y); ctx.lineTo(M + tableW, y); ctx.stroke();
  y += 8;
  d.legend.forEach((e) => {
    ctx.fillStyle = '#111'; ctx.font = '11px monospace';
    ctx.fillText(String(e.no), tx + colX[0], y + 4);
    ctx.fillStyle = e.hex; ctx.fillRect(tx + colX[1], y, 18, 18);
    ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 1; ctx.strokeRect(tx + colX[1], y, 18, 18);
    ctx.fillStyle = '#111'; ctx.font = '700 11px monospace';
    ctx.fillText(e.code, tx + colX[2], y + 4);
    ctx.font = '12px Space Grotesk, system-ui, sans-serif';
    ctx.fillText(e.name, tx + colX[3], y + 3);
    ctx.font = '11px monospace'; ctx.fillStyle = '#555';
    ctx.fillText(e.hex.toUpperCase(), tx + colX[4], y + 4);
    ctx.fillStyle = '#111';
    ctx.fillText(e.quantity.toLocaleString(), tx + colX[5], y + 4);
    ctx.fillStyle = '#888';
    ctx.fillText('+' + e.wasteBuffer.toLocaleString(), tx + colX[6], y + 4);
    ctx.fillStyle = '#7F00FF'; ctx.font = '700 11px monospace';
    ctx.fillText(e.totalWithWaste.toLocaleString(), tx + colX[7], y + 4);
    y += rowH;
    ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(M, y - 4); ctx.lineTo(M + tableW, y - 4); ctx.stroke();
  });

  y += 24;
  // factory notes
  ctx.fillStyle = '#111'; ctx.font = '700 11px monospace'; ctx.fillText('FACTORY NOTES', M, y);
  y += 16;
  ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = 1.5; ctx.strokeRect(M, y, contentW, 80);
  ctx.fillStyle = '#333'; ctx.font = '12px Space Grotesk, system-ui, sans-serif';
  wrapText(ctx, d.notes || 'Pack one sealed bag per color code. Verify counts include the listed waste buffer. Match every cell to the grid map (row,col).', M + 10, y + 12, contentW - 20, 16);

  return cv;
}
function wrapText(ctx, text, x, y, maxW, lh) {
  const words = String(text).split(/\s+/); let line = '', yy = y;
  for (const w of words) {
    const test = line ? line + ' ' + w : w;
    if (ctx.measureText(test).width > maxW) { ctx.fillText(line, x, yy); line = w; yy += lh; }
    else line = test;
  }
  if (line) ctx.fillText(line, x, yy);
}

function exportPNG(d, opts) {
  const cv = drawProductionSheet(d, opts);
  cv.toBlob((blob) => dl(`tobeads-${d.patternId}-sheet.png`, 'image/png', blob), 'image/png');
}

function exportPDF(d, opts, paper) {
  // Render grid+sheet image, embed in a clean printable HTML doc, print via
  // a hidden iframe (avoids popup blockers). User saves as PDF.
  const cv = drawProductionSheet(d, opts);
  const img = cv.toDataURL('image/png');
  const rowsHtml = d.legend.map(e => `<tr>
      <td>${e.no}</td>
      <td><span style="display:inline-block;width:16px;height:16px;background:${e.hex};border:1px solid #999;vertical-align:middle"></span></td>
      <td><b>${e.code}</b></td><td>${e.name}</td><td style="color:#666">${e.hex.toUpperCase()}</td>
      <td>${e.quantity.toLocaleString()}</td><td style="color:#888">+${e.wasteBuffer.toLocaleString()}</td>
      <td style="color:#7F00FF;font-weight:700">${e.totalWithWaste.toLocaleString()}</td>
    </tr>`).join('');
  const html = `<!doctype html><html><head><meta charset="utf-8"><title>Tobeads ${d.patternId}</title>
    <style>
      @page { size: ${paper === 'Letter' ? 'Letter' : 'A4'}; margin: 12mm; }
      body { font-family: 'Space Grotesk', system-ui, Arial, sans-serif; color:#111; margin:0; }
      .sheet-img { width:100%; height:auto; display:block; }
      table { width:100%; border-collapse:collapse; font-size:11px; margin-top:14px; }
      th,td { text-align:left; padding:5px 8px; border-bottom:1px solid #e3e3e3; }
      th { font:700 9px monospace; color:#888; text-transform:uppercase; border-bottom:2px solid #111; }
      h2 { font:700 12px monospace; margin:18px 0 4px; }
    </style></head><body onload="window.focus();window.print();">
      <img class="sheet-img" src="${img}"/>
    </body></html>`;
  const iframe = document.createElement('iframe');
  iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;';
  document.body.appendChild(iframe);
  const doc = iframe.contentWindow.document;
  doc.open(); doc.write(html); doc.close();
  setTimeout(() => { try { iframe.contentWindow.focus(); iframe.contentWindow.print(); } catch (e) {} }, 600);
  setTimeout(() => document.body.removeChild(iframe), 60000);
}

// ── component ─────────────────────────────────────────────────────────────
function ExportModal({ pattern, brand, onClose }) {
  const isReady = pattern && !pattern.isDemo;
  const [cellMode, setCellMode] = useStateExp('both');     // color | no | both | print
  const [showGridLabels, setShowGridLabels] = useStateExp(true);
  const [showCountTable, setShowCountTable] = useStateExp(true);
  const [layout, setLayout] = useStateExp('side');         // side | stack
  const [cellSize, setCellSize] = useStateExp(16);
  const [paper, setPaper] = useStateExp('A4');
  const [wastePct, setWastePct] = useStateExp(5);
  const [notes, setNotes] = useStateExp('');

  const data = useMemoExp(() => isReady ? buildExportData(pattern, brand, wastePct, notes) : null, [pattern, brand, wastePct, notes, isReady]);

  return (
    <div className="exp-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="exp-modal">
        {/* header */}
        <div className="exp-head">
          <div className="flex items-center gap-3">
            <window.TobeadsNav size={13} onLight/>
            <span className="font-mono" style={{ fontSize: 10, letterSpacing: '0.14em', color: 'var(--muted)', textTransform: 'uppercase' }}>Production Export</span>
            {isReady && <span className="font-mono" style={{ fontSize: 10, color: 'var(--muted)' }}>· {pattern.id}</span>}
          </div>
          <button onClick={onClose} className="tool-btn" title="Close"><window.Icon.Close size={16}/></button>
        </div>

        {!isReady ? (
          <div className="exp-empty">
            <div style={{ width: 56, height: 56, background: 'var(--yellow)', border: '1.5px solid var(--ink)', borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <window.Icon.Image size={24}/>
            </div>
            <div className="font-display" style={{ fontSize: 20, marginTop: 16 }}>Nothing to export yet</div>
            <p style={{ fontSize: 13, color: 'var(--muted)', marginTop: 8, maxWidth: 320, textAlign: 'center' }}>
              Upload a photo or generate a pattern before exporting. The production sheet is built from your real bead map.
            </p>
            <button onClick={onClose} className="btn btn-primary" style={{ marginTop: 20 }}>Back to editor</button>
          </div>
        ) : (
          <div className={`exp-body ${layout === 'stack' ? 'stacked' : ''}`}>
            {/* LEFT — bead map preview */}
            <div className="exp-preview">
              <BeadMapPreview data={data} cellMode={cellMode} showGridLabels={showGridLabels} cellSize={cellSize}/>
            </div>

            {/* MIDDLE — count table + pick list */}
            {showCountTable && (
              <div className="exp-tables">
                <CountTable data={data}/>
              </div>
            )}

            {/* RIGHT — settings */}
            <div className="exp-settings">
              <ExpGroup title="Cell display">
                <Seg value={cellMode} setValue={setCellMode} options={[['color','Color'],['no','Key No.'],['both','Color + No.'],['print','Printable']]}/>
              </ExpGroup>
              <ExpGroup title="Bead map">
                <ExpToggle label="Row / column numbers" on={showGridLabels} onChange={() => setShowGridLabels(v => !v)}/>
                <ExpToggle label="Bead count table" on={showCountTable} onChange={() => setShowCountTable(v => !v)}/>
                <div className="flex items-center justify-between" style={{ marginTop: 10 }}>
                  <span style={{ fontSize: 12 }}>Cell size</span>
                  <span className="font-mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{cellSize}px</span>
                </div>
                <input type="range" min={8} max={30} step={1} value={cellSize} onChange={e => setCellSize(Number(e.target.value))} style={{ width: '100%' }}/>
              </ExpGroup>
              <ExpGroup title="Layout & paper">
                <Seg value={layout} setValue={setLayout} options={[['side','Side'],['stack','Stack']]}/>
                <div style={{ height: 8 }}/>
                <Seg value={paper} setValue={setPaper} options={[['A4','A4'],['Letter','Letter']]}/>
              </ExpGroup>
              <ExpGroup title="Waste buffer">
                <div className="flex items-center justify-between">
                  <span style={{ fontSize: 12 }}>Extra beads per color</span>
                  <span className="font-mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{wastePct}%</span>
                </div>
                <input type="range" min={0} max={15} step={1} value={wastePct} onChange={e => setWastePct(Number(e.target.value))} style={{ width: '100%' }}/>
              </ExpGroup>
              <ExpGroup title="Factory notes">
                <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder="e.g. Pack one bag per color code. Pegboard 50×50." rows={3}
                  style={{ width: '100%', fontSize: 12, padding: 8, border: '1.5px solid var(--line)', borderRadius: 8, resize: 'vertical', fontFamily: 'inherit' }}/>
              </ExpGroup>

              <div className="exp-actions">
                <button onClick={() => exportPDF(data, { cellSize, cellMode, showGridLabels }, paper)} className="btn btn-primary" style={{ width: '100%' }}><window.Icon.Download size={14}/> Export PDF</button>
                <div className="grid grid-cols-3 gap-2" style={{ marginTop: 8 }}>
                  <button onClick={() => exportPNG(data, { cellSize, cellMode, showGridLabels })} className="btn btn-ghost btn-sm">PNG</button>
                  <button onClick={() => exportCSV(data)} className="btn btn-ghost btn-sm">CSV</button>
                  <button onClick={() => exportJSON(data)} className="btn btn-ghost btn-sm">JSON</button>
                </div>
                <button onClick={onClose} className="btn btn-soft btn-sm" style={{ width: '100%', marginTop: 8 }}>Close</button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
window.ExportModal = ExportModal;

// ── preview grid (DOM, scrollable, with coordinates) ──────────────────────
function BeadMapPreview({ data, cellMode, showGridLabels, cellSize }) {
  const cw = Math.max(8, cellSize);
  const showNo = (cellMode === 'no' || cellMode === 'both' || cellMode === 'print') && cw >= 13;
  const print = cellMode === 'print';
  const gut = showGridLabels ? Math.max(18, cw) : 0;

  const colHeader = useMemoExp(() => {
    if (!showGridLabels) return null;
    const cells = [];
    cells.push(<div key="corner" style={{ width: gut, height: gut }}/>);
    for (let c = 0; c < data.cols; c++) {
      const show = c === 0 || (c + 1) % 5 === 0;
      cells.push(<div key={'c'+c} className="exp-axis" style={{ width: cw, height: gut, fontSize: Math.min(9, cw * 0.6) }}>{show ? c + 1 : ''}</div>);
    }
    return <div style={{ display: 'flex' }}>{cells}</div>;
  }, [data, cw, gut, showGridLabels]);

  const body = useMemoExp(() => {
    return data.grid.map((row, r) => (
      <div key={'r'+r} style={{ display: 'flex' }}>
        {showGridLabels && (
          <div className="exp-axis" style={{ width: gut, height: cw, fontSize: Math.min(9, cw * 0.6), justifyContent: 'flex-end', paddingRight: 3 }}>
            {(r === 0 || (r + 1) % 5 === 0) ? r + 1 : ''}
          </div>
        )}
        {row.map((id, c) => {
          const m = data.metaById[id];
          const bg = print ? '#fff' : (m ? m.hex : '#fff');
          const txt = m && !print ? (lum(m.hex) > 0.55 ? '#111' : '#fff') : '#111';
          return (
            <div key={c} title={m ? `(${c+1}, ${r+1}) · ${m.code} ${m.name}` : ''} style={{
              width: cw, height: cw, background: bg,
              borderRight: '1px solid rgba(0,0,0,0.10)', borderBottom: '1px solid rgba(0,0,0,0.10)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'monospace', fontSize: Math.floor(cw * 0.5), color: txt, position: 'relative',
            }}>
              {print && m && <span style={{ position: 'absolute', top: 0, left: 0, width: Math.max(3, cw*0.3), height: Math.max(3, cw*0.3), background: m.hex }}/>}
              {showNo && m ? m.no : ''}
            </div>
          );
        })}
      </div>
    ));
  }, [data, cw, gut, showNo, print, showGridLabels]);

  return (
    <div>
      <div className="flex items-center justify-between" style={{ marginBottom: 10 }}>
        <span className="font-mono" style={{ fontSize: 10, letterSpacing: '0.12em', color: 'var(--muted)', textTransform: 'uppercase' }}>Bead Map · {data.cols}×{data.rows}</span>
        <span className="font-mono" style={{ fontSize: 10, color: 'var(--muted)' }}>{data.totalBeads.toLocaleString()} beads · {data.uniqueColors} colors</span>
      </div>
      <div className="exp-grid-scroll">
        <div style={{ display: 'inline-block', background: '#fff', padding: 8, border: '1px solid var(--line)' }}>
          {colHeader}
          {body}
        </div>
      </div>
    </div>
  );
}

// ── count table / pick list ───────────────────────────────────────────────
function CountTable({ data }) {
  return (
    <div>
      <div className="font-mono" style={{ fontSize: 10, letterSpacing: '0.12em', color: 'var(--muted)', textTransform: 'uppercase', marginBottom: 6 }}>
        {data.brand} — {data.totalBeads.toLocaleString()} beads
      </div>
      <div className="font-mono" style={{ fontSize: 9, color: 'var(--muted-2)', marginBottom: 12 }}>Factory pick list · +{data.wastePct}% waste buffer</div>
      <div className="exp-table-scroll">
        <table className="exp-table">
          <thead>
            <tr><th>No</th><th>Chip</th><th>Code</th><th>Color name</th><th>Hex</th><th style={{ textAlign: 'right' }}>Qty</th><th style={{ textAlign: 'right' }}>+{data.wastePct}%</th><th style={{ textAlign: 'right' }}>Total</th></tr>
          </thead>
          <tbody>
            {data.legend.map(e => (
              <tr key={e.beadColorId}>
                <td className="mono">{e.no}</td>
                <td><span style={{ display: 'inline-block', width: 16, height: 16, background: e.hex, border: '1px solid rgba(0,0,0,0.25)', verticalAlign: 'middle' }}/></td>
                <td className="mono" style={{ fontWeight: 700 }}>{e.code}</td>
                <td>{e.name}</td>
                <td className="mono" style={{ color: 'var(--muted)' }}>{e.hex.toUpperCase()}</td>
                <td className="mono" style={{ textAlign: 'right' }}>{e.quantity.toLocaleString()}</td>
                <td className="mono" style={{ textAlign: 'right', color: 'var(--muted)' }}>+{e.wasteBuffer.toLocaleString()}</td>
                <td className="mono" style={{ textAlign: 'right', color: 'var(--v)', fontWeight: 700 }}>{e.totalWithWaste.toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ── small settings UI ───────────────────────────────────────────────────
function ExpGroup({ title, children }) {
  return (
    <div style={{ marginBottom: 18 }}>
      <div className="font-mono" style={{ fontSize: 9, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 8 }}>{title}</div>
      {children}
    </div>
  );
}
function Seg({ value, setValue, options }) {
  return (
    <div className="seg" style={{ display: 'flex', width: '100%', flexWrap: 'wrap' }}>
      {options.map(([v, label]) => (
        <button key={v} onClick={() => setValue(v)} className={v === value ? 'on' : ''} style={{ flex: '1 0 auto' }}>{label}</button>
      ))}
    </div>
  );
}
function ExpToggle({ label, on, onChange }) {
  return (
    <button onClick={onChange} className="flex items-center w-full text-left" style={{ padding: '6px 0', justifyContent: 'space-between', background: 'transparent', border: 0, cursor: 'pointer' }}>
      <span style={{ fontSize: 12 }}>{label}</span>
      <div style={{ width: 34, height: 20, background: on ? 'var(--ink)' : 'rgba(17,17,17,0.15)', borderRadius: 999, position: 'relative', flexShrink: 0 }}>
        <div style={{ position: 'absolute', top: 2, left: on ? 16 : 2, width: 16, height: 16, background: on ? 'var(--yellow)' : '#fff', borderRadius: 999, transition: 'all .15s' }}/>
      </div>
    </button>
  );
}
