// ─────────────────────────────────────────────────────────────
// WORKBENCH SHEETS — secondary screens and modals.
// All exported on window for workbench.jsx to consume.
//
//   WBSheetShell      — bottom-sheet wrapper (grab handle + close)
//   WBFullScreen      — full-shell overlay (Library, Deploy)
//   WBModal           — centered modal (Switch-to-Prod confirm)
//
//   WBLibrarySheet    — list every idea across all stages
//   WBDeploySheet     — fake deploy log timeline
//   WBSettingsSheet   — env / renderer / quotas
//   WBProdConfirm     — switch-to-prod warning
//   WBJsonSheet       — raw idea JSON viewer
//   WBVoiceSheet      — grouped voice picker
//   WBBgSheet         — BG prompt editor + tone pills
//   WBVideoModal      — fake video player
//   WBPlatformSheet   — TikTok / YouTube link preview
// ─────────────────────────────────────────────────────────────

// ──────────────── PRIMITIVE: BOTTOM SHEET ────────────────
function WBSheetShell({ w, title, eyebrow, onClose, children, maxHeight = '86%' }) {
  return (
    <div style={{
      // sheet-pin-2026-06-12 (Scott: "i have to scroll down a bunch to see the
      // post copy"): 'absolute' anchored the bottom-sheet to the bottom of the
      // full PAGE (root grows with the board), not the screen. Same fix as the
      // video player: 'fixed' pins to the viewport.
      position: 'fixed', inset: 0, zIndex: 30,
      background: 'rgba(20,17,11,0.42)',
      display: 'flex', alignItems: 'flex-end',
      animation: 'wbFadeIn 160ms ease-out',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()}
        style={{
          width: '100%',
          background: w.paper,
          borderTopLeftRadius: 16, borderTopRightRadius: 16,
          padding: '14px 18px 24px',
          maxHeight,
          display: 'flex', flexDirection: 'column',
          animation: 'wbSlideUp 220ms cubic-bezier(.2,.7,.2,1)',
        }}>
        <div style={{
          width: 32, height: 4, borderRadius: 2,
          background: w.rule, margin: '0 auto 12px', flexShrink: 0,
        }} />
        {(eyebrow || title) && (
          <div style={{
            display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
            marginBottom: 12, flexShrink: 0,
          }}>
            <div>
              {eyebrow && (
                <div style={{
                  fontSize: 9.5, fontWeight: 700,
                  letterSpacing: '0.16em', textTransform: 'uppercase',
                  color: w.inkFaint, marginBottom: 2,
                }}>{eyebrow}</div>
              )}
              {title && (
                <h3 style={{
                  fontSize: 17, fontWeight: 700, letterSpacing: '-0.02em',
                  color: w.ink, margin: 0,
                }}>{title}</h3>
              )}
            </div>
            <button data-no-swipe onClick={onClose}
              style={{
                background: 'transparent', border: 'none',
                color: w.inkDim, fontSize: 16, cursor: 'pointer',
                fontFamily: 'inherit', padding: 4,
              }}>✕</button>
          </div>
        )}
        {children}
      </div>
    </div>
  );
}

// ──────────────── PRIMITIVE: FULL-SHELL OVERLAY ────────────────
function WBFullScreen({ w, title, subtitle, onClose, children, rightAction }) {
  const scrollRef = usePhoneScroll();
  return (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 30,
      background: w.bg,
      display: 'flex', flexDirection: 'column',
      animation: 'wbSlideIn 220ms cubic-bezier(.2,.7,.2,1)',
    }}>
      <header style={{
        flexShrink: 0,
        padding: '12px 14px 10px',
        borderBottom: `1px solid ${w.rule}`,
        background: w.bg,
        display: 'flex', alignItems: 'center', gap: 10,
      }}>
        <button data-no-swipe onClick={onClose}
          style={{
            background: 'transparent', border: 'none',
            color: w.ink, fontSize: 18, cursor: 'pointer',
            fontFamily: 'inherit', padding: 0,
            width: 28, height: 28,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>‹</button>
        <div style={{ flex: 1 }}>
          <h2 style={{
            fontSize: 17, fontWeight: 700, letterSpacing: '-0.02em',
            color: w.ink, margin: 0, lineHeight: 1.1,
          }}>{title}</h2>
          {subtitle && (
            <div style={{
              fontSize: 11, color: w.inkDim,
              fontFamily: '"Geist Mono", monospace',
              marginTop: 2,
            }}>{subtitle}</div>
          )}
        </div>
        {rightAction}
      </header>
      <div ref={scrollRef}
        onWheel={e => e.stopPropagation()}
        style={{
          flex: 1, overflow: 'auto',
          padding: '12px 14px 28px',
        }}>
        {children}
      </div>
    </div>
  );
}

// ──────────────── PRIMITIVE: CENTERED MODAL ────────────────
function WBModal({ w, onClose, children }) {
  return (
    <div style={{
      // sheet-pin-2026-06-12: same viewport-pin as WBSheetShell.
      position: 'fixed', inset: 0, zIndex: 32,
      background: 'rgba(20,17,11,0.48)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: 24,
      animation: 'wbFadeIn 160ms ease-out',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()}
        style={{
          width: '100%', maxWidth: 320,
          background: w.paper,
          border: `1px solid ${w.rule}`,
          borderRadius: 12,
          padding: '18px 18px 16px',
          boxShadow: '0 30px 60px -10px rgba(20,17,11,0.5)',
          animation: 'wbModalIn 220ms cubic-bezier(.2,.7,.2,1)',
        }}>
        {children}
      </div>
    </div>
  );
}

// ──────────────── LIBRARY ────────────────
function WBLibrarySheet({ w, ideas, onClose, onJump }) {
  const [filter, setFilter] = React.useState('all');
  const stages = ['all', 'pending', 'curating', 'audio', 'animation', 'final', 'published'];
  const list = filter === 'all' ? ideas : ideas.filter(i => i.stage === filter);
  return (
    <WBFullScreen w={w} title="Library" subtitle={`${ideas.length} ideas · all stages`} onClose={onClose}
      rightAction={
        <span style={{
          fontFamily: '"Geist Mono", monospace',
          fontSize: 10, color: w.inkFaint,
        }}>D1 ✓</span>
      }>
      {/* filter chips */}
      <div style={{
        display: 'flex', gap: 5, overflowX: 'auto',
        margin: '0 -14px 12px', padding: '0 14px 4px',
        scrollbarWidth: 'none',
      }}>
        {stages.map(s => {
          const active = filter === s;
          return (
            <button key={s} data-no-swipe onClick={() => setFilter(s)}
              style={{
                flexShrink: 0,
                background: active ? w.ink : 'transparent',
                color: active ? w.paper : w.inkDim,
                border: `1px solid ${active ? w.ink : w.rule}`,
                borderRadius: 100,
                padding: '4px 10px',
                fontFamily: 'inherit', fontSize: 10.5,
                fontWeight: active ? 600 : 500,
                cursor: 'pointer', whiteSpace: 'nowrap',
                textTransform: 'capitalize',
              }}>{s}</button>
          );
        })}
      </div>

      {/* list */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {list.length === 0 && (
          <div style={{
            padding: '36px 0', textAlign: 'center',
            color: w.inkFaint, fontSize: 12, fontStyle: 'italic',
          }}>Nothing in this stage.</div>
        )}
        {list.map(idea => {
          // Guard unknown stages (e.g. 'failed') — an unguarded meta.label here
          // crashed the whole Library sheet the moment a failed idea existed,
          // same class as the 2026-06-06 blank-dashboard bug (2026-06-09).
          const meta = WB_STAGE_META[idea.stage]
            || { label: idea.stage === 'failed' ? 'Failed' : (idea.stage || '—') };
          const isPub = idea.stage === 'published';
          const totalViews = isPub ? ((idea.posted?.tiktok?.views || 0) + (idea.posted?.youtube?.views || 0)) : 0;
          return (
            <button key={idea.id} data-no-swipe
              onClick={() => { onJump(idea.id); onClose(); }}
              style={{
                background: w.paper,
                border: `1px solid ${w.rule}`,
                borderRadius: 7,
                padding: '10px 12px',
                cursor: 'pointer', textAlign: 'left',
                fontFamily: 'inherit',
                display: 'flex', alignItems: 'center', gap: 10,
              }}>
              <div style={{
                width: 30, aspectRatio: '9/16',
                borderRadius: 3, overflow: 'hidden',
                border: `1px solid ${w.rule}`,
                flexShrink: 0,
              }}>
                <KeyframePlaceholder seed={idea.slug.charCodeAt(1)} label="" brighter={isPub} />
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                  fontSize: 12.5, fontWeight: 600, color: w.ink,
                  letterSpacing: '-0.005em',
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{idea.title}</div>
                <div style={{
                  fontFamily: '"Geist Mono", monospace',
                  fontSize: 9.5, color: w.inkFaint,
                  marginTop: 2,
                  display: 'flex', alignItems: 'center', gap: 6,
                }}>
                  <span style={{
                    color: idea.stage === 'published' ? w.success : (bucketFor(idea.stage) === 'you' ? w.urgent : w.accent),
                    fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.10em',
                  }}>{meta.label}</span>
                  <span>·</span>
                  <span>{idea.slug}</span>
                  {isPub && (
                    <>
                      <span>·</span>
                      <span style={{ color: w.ink }}>{formatN(totalViews)} views</span>
                    </>
                  )}
                </div>
              </div>
              <span style={{ color: w.inkFaint, fontSize: 14 }}>›</span>
            </button>
          );
        })}
      </div>
    </WBFullScreen>
  );
}

// ──────────────── DEPLOY LOG ────────────────
function WBDeploySheet({ w, onClose }) {
  const log = [
    { sha: 'a858a1c', who: 'scott',  msg: 'workbench: in-flight auto-advance', t: '14s ago',  status: 'live' },
    { sha: 'b21fe44', who: 'scott',  msg: 'fix: regen note appears on candidate strip',         t: '42m ago',  status: 'past' },
    { sha: '8c70fa2', who: 'scott',  msg: 'audio: SFX cue volume default 0.4',                  t: '2h ago',   status: 'past' },
    { sha: '3f009ee', who: 'scott',  msg: 'curate: lazy-init scenes on approve',                t: '4h ago',   status: 'past' },
    { sha: 'd0bb1c9', who: 'scott',  msg: 'mock: 5 ideas spanning every stage',                 t: 'yesterday', status: 'past' },
    { sha: '7714fb0', who: 'scott',  msg: 'workbench skeleton — buckets + cards',                t: '2d ago',   status: 'past' },
    { sha: '1aa3e80', who: 'scott',  msg: 'design: bone/cobalt palette locked',                 t: '3d ago',   status: 'past' },
  ];
  return (
    <WBFullScreen w={w} title="Deploy log" subtitle="dev.simplyknown.co · pages" onClose={onClose}
      rightAction={
        <button data-no-swipe
          style={{
            background: w.ink, color: w.paper, border: 'none',
            borderRadius: 5, padding: '4px 9px',
            fontFamily: '"Geist Mono", monospace',
            fontSize: 10, fontWeight: 600,
            cursor: 'pointer', letterSpacing: '0.04em',
            textTransform: 'uppercase',
          }}>↻ Pull</button>
      }>
      <div style={{
        fontSize: 10, fontWeight: 700,
        letterSpacing: '0.16em', textTransform: 'uppercase',
        color: w.inkFaint, marginBottom: 8,
      }}>Recent</div>

      <div style={{
        position: 'relative',
        paddingLeft: 14,
      }}>
        {/* spine */}
        <div style={{
          position: 'absolute', left: 4, top: 6, bottom: 6,
          width: 1, background: w.rule,
        }} />
        {log.map((e, i) => (
          <div key={e.sha} style={{
            position: 'relative',
            paddingBottom: 14,
          }}>
            <div style={{
              position: 'absolute', left: -14, top: 5,
              width: 9, height: 9, borderRadius: '50%',
              background: e.status === 'live' ? w.success : w.paper,
              border: `2px solid ${e.status === 'live' ? w.success : w.rule}`,
              boxShadow: e.status === 'live' ? `0 0 8px ${w.success}` : 'none',
            }} />
            <div style={{
              display: 'flex', alignItems: 'center', gap: 6,
              marginBottom: 3,
            }}>
              <span style={{
                fontFamily: '"Geist Mono", monospace',
                fontSize: 11, fontWeight: 700,
                color: e.status === 'live' ? w.success : w.ink,
              }}>{e.sha}</span>
              {e.status === 'live' && (
                <span style={{
                  fontSize: 8, fontWeight: 700,
                  color: w.success, letterSpacing: '0.16em',
                  textTransform: 'uppercase',
                  border: `1px solid ${w.success}`,
                  padding: '0 4px', borderRadius: 2,
                }}>LIVE</span>
              )}
              <span style={{ flex: 1 }} />
              <span style={{
                fontFamily: '"Geist Mono", monospace',
                fontSize: 10, color: w.inkFaint,
              }}>{e.t}</span>
            </div>
            <div style={{
              fontSize: 12.5, color: w.ink, lineHeight: 1.4,
            }}>{e.msg}</div>
            <div style={{
              fontFamily: '"Geist Mono", monospace',
              fontSize: 10, color: w.inkFaint, marginTop: 2,
            }}>@{e.who}</div>
          </div>
        ))}
      </div>
    </WBFullScreen>
  );
}

// ──────────────── SETTINGS ────────────────
function WBSettingsSheet({ w, onClose, onSwitchProd }) {
  const [autopost, setAutopost] = React.useState(false);
  const [budget, setBudget] = React.useState(8);
  const [pollSec, setPollSec] = React.useState(15);
  const [renderer, setRenderer] = React.useState(true);

  const Row = ({ label, hint, children }) => (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '12px 0',
      borderBottom: `1px solid ${w.ruleSoft}`,
    }}>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 13, color: w.ink, fontWeight: 500 }}>{label}</div>
        {hint && (
          <div style={{
            fontSize: 10.5, color: w.inkFaint, marginTop: 2,
            fontFamily: '"Geist Mono", monospace',
          }}>{hint}</div>
        )}
      </div>
      {children}
    </div>
  );

  return (
    <WBFullScreen w={w} title="Settings" subtitle="dev environment" onClose={onClose}>
      <SettingsSection w={w} label="Environment">
        <Row label="API host"
          hint="simplyknown-queue-api-dev.workers.dev">
          <span style={{
            fontFamily: '"Geist Mono", monospace', fontSize: 10,
            color: w.success, fontWeight: 600,
          }}>● online</span>
        </Row>
        <Row label="Renderer"
          hint={renderer ? "Mini PC · a858a1c" : "offline"}>
          <WBToggle w={w} value={renderer} onChange={setRenderer} />
        </Row>
        <Row label="Tunnel"
          hint="cloudflare · up">
          <span style={{
            fontFamily: '"Geist Mono", monospace', fontSize: 10,
            color: w.success, fontWeight: 600,
          }}>● up</span>
        </Row>
      </SettingsSection>

      <SettingsSection w={w} label="Pipeline">
        <Row label="Auto-post to platforms"
          hint="when a video reaches Ready">
          <WBToggle w={w} value={autopost} onChange={setAutopost} />
        </Row>
        <Row label="Poll interval"
          hint={`${pollSec}s`}>
          <select data-no-swipe value={pollSec}
            onChange={e => setPollSec(parseInt(e.target.value))}
            style={{
              background: w.raised, border: `1px solid ${w.rule}`,
              borderRadius: 5, padding: '4px 8px',
              fontFamily: '"Geist Mono", monospace', fontSize: 11,
              color: w.ink, cursor: 'pointer', outline: 'none',
            }}>
            <option value={5}>5s</option>
            <option value={15}>15s</option>
            <option value={30}>30s</option>
            <option value={60}>60s</option>
          </select>
        </Row>
        <Row label="Daily spend budget"
          hint={`$${budget.toFixed(2)}/day · ${STATUS.spendToday.toFixed(2)} used today`}>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 4,
            fontFamily: '"Geist Mono", monospace', fontSize: 12,
            color: w.ink,
          }}>
            <button data-no-swipe onClick={() => setBudget(b => Math.max(1, b - 1))}
              style={{
                width: 22, height: 22, borderRadius: 4,
                background: w.raised, border: `1px solid ${w.rule}`,
                color: w.ink, cursor: 'pointer',
                fontFamily: 'inherit', fontSize: 12,
              }}>−</button>
            <span style={{ minWidth: 30, textAlign: 'center', fontWeight: 600 }}>${budget}</span>
            <button data-no-swipe onClick={() => setBudget(b => b + 1)}
              style={{
                width: 22, height: 22, borderRadius: 4,
                background: w.raised, border: `1px solid ${w.rule}`,
                color: w.ink, cursor: 'pointer',
                fontFamily: 'inherit', fontSize: 12,
              }}>+</button>
          </div>
        </Row>
      </SettingsSection>

      <SettingsSection w={w} label="Danger zone">
        <button data-no-swipe onClick={onSwitchProd}
          style={{
            width: '100%',
            background: 'transparent',
            color: w.urgent,
            border: `1px solid ${w.urgent}`,
            borderRadius: 6,
            padding: '10px 12px', marginTop: 4,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
            cursor: 'pointer', textAlign: 'left',
          }}>
          Switch to production →
          <div style={{
            fontFamily: '"Geist Mono", monospace',
            fontSize: 10, color: w.inkFaint,
            marginTop: 2, fontWeight: 400,
          }}>posts will go live on tiktok + youtube</div>
        </button>
      </SettingsSection>

      <div style={{
        marginTop: 18, padding: '10px 0',
        fontFamily: '"Geist Mono", monospace',
        fontSize: 10, color: w.inkFaint,
        textAlign: 'center', lineHeight: 1.6,
      }}>
        signed in as scott<br/>
        build {STATUS.deploySha} · 2026.05.15
      </div>
    </WBFullScreen>
  );
}

function SettingsSection({ w, label, children }) {
  return (
    <section style={{ marginBottom: 18 }}>
      <div style={{
        fontSize: 10, fontWeight: 700,
        letterSpacing: '0.16em', textTransform: 'uppercase',
        color: w.inkFaint, marginBottom: 4,
      }}>{label}</div>
      <div style={{
        background: w.paper, border: `1px solid ${w.rule}`,
        borderRadius: 8, padding: '0 12px',
      }}>
        {children}
      </div>
    </section>
  );
}

function WBToggle({ w, value, onChange }) {
  return (
    <button data-no-swipe onClick={() => onChange(!value)}
      style={{
        width: 38, height: 22,
        borderRadius: 11,
        background: value ? w.success : w.rule,
        border: 'none', padding: 2,
        cursor: 'pointer',
        position: 'relative',
        transition: 'background 160ms',
      }}>
      <div style={{
        width: 18, height: 18, borderRadius: '50%',
        background: '#fff',
        transform: value ? 'translateX(16px)' : 'translateX(0)',
        transition: 'transform 160ms',
        boxShadow: '0 1px 3px rgba(0,0,0,0.18)',
      }} />
    </button>
  );
}

// ──────────────── SWITCH-TO-PROD CONFIRM ────────────────
function WBProdConfirm({ w, onClose, onConfirm }) {
  return (
    <WBModal w={w} onClose={onClose}>
      <div style={{
        fontSize: 10, fontWeight: 700,
        letterSpacing: '0.16em', textTransform: 'uppercase',
        color: w.urgent, marginBottom: 6,
      }}>⚠ Live posts</div>
      <h3 style={{
        fontSize: 18, fontWeight: 700, letterSpacing: '-0.02em',
        color: w.ink, margin: '0 0 8px', lineHeight: 1.2,
      }}>Switch to production?</h3>
      <p style={{
        fontSize: 12.5, color: w.inkDim, lineHeight: 1.5,
        margin: '0 0 14px',
      }}>
        You'll be working against <b style={{ color: w.ink }}>simplyknown-queue-api</b> — actions hit real D1, real R2, real posting tokens. Anything published will appear on TikTok and YouTube within ~30 seconds.
      </p>
      <div style={{
        background: w.bg, borderRadius: 6, padding: '8px 10px',
        marginBottom: 14,
        fontFamily: '"Geist Mono", monospace', fontSize: 10.5,
        color: w.inkDim, lineHeight: 1.5,
      }}>
        <div><span style={{ color: w.inkFaint }}>host</span> simplyknown.co</div>
        <div><span style={{ color: w.inkFaint }}>posts</span> <span style={{ color: w.urgent }}>live</span></div>
        <div><span style={{ color: w.inkFaint }}>budget</span> $32/day</div>
      </div>
      <div style={{ display: 'flex', gap: 6 }}>
        <button data-no-swipe onClick={onClose}
          style={{
            flex: 1, height: 38,
            background: 'transparent', color: w.ink,
            border: `1px solid ${w.rule}`,
            borderRadius: 6,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500,
            cursor: 'pointer',
          }}>Cancel</button>
        <button data-no-swipe onClick={onConfirm}
          style={{
            flex: 1.4, height: 38,
            background: w.urgent, color: '#fff',
            border: 'none', borderRadius: 6,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
            cursor: 'pointer',
          }}>Switch →</button>
      </div>
    </WBModal>
  );
}

// ──────────────── JSON VIEWER ────────────────
function WBJsonSheet({ w, idea, onClose }) {
  const json = React.useMemo(() => {
    // Drop heavy fields for readability
    const { sceneTexts, scenes, ...rest } = idea;
    return JSON.stringify({
      ...rest,
      ...(sceneTexts ? { sceneTexts: `[${sceneTexts.length} scenes]` } : {}),
      ...(scenes ? { scenes: `[${scenes.length} scenes · ${scenes.filter(s => s.pickedId).length} picked]` } : {}),
    }, null, 2);
  }, [idea]);

  const [copied, setCopied] = React.useState(false);
  const onCopy = () => {
    try { navigator.clipboard?.writeText(json); } catch (e) {}
    setCopied(true);
    setTimeout(() => setCopied(false), 1400);
  };

  // Crude syntax highlight
  const highlight = (s) => s.split('\n').map((line, i) => {
    const m = line.match(/^(\s*)(".*?")(:\s*)(.*)$/);
    if (m) {
      const [, ws, key, sep, val] = m;
      const isStr = val.startsWith('"');
      return (
        <div key={i}>
          <span style={{ color: w.inkFaint }}>{ws}</span>
          <span style={{ color: w.accent }}>{key}</span>
          <span style={{ color: w.inkDim }}>{sep}</span>
          <span style={{ color: isStr ? w.success : w.amber }}>{val}</span>
        </div>
      );
    }
    return <div key={i} style={{ color: w.inkDim }}>{line}</div>;
  });

  return (
    <WBSheetShell w={w}
      eyebrow={`${idea.slug} · ${idea.stage}`}
      title="Raw idea JSON" onClose={onClose}>
      <div style={{
        flex: 1, overflow: 'auto',
        background: w.bg,
        border: `1px solid ${w.rule}`,
        borderRadius: 6,
        padding: 10,
        fontFamily: '"Geist Mono", monospace',
        fontSize: 10.5, lineHeight: 1.6,
        marginBottom: 10,
        minHeight: 200,
      }}>
        {highlight(json)}
      </div>
      <button data-no-swipe onClick={onCopy}
        style={{
          width: '100%', height: 36,
          background: copied ? w.success : w.ink,
          color: '#fff', border: 'none', borderRadius: 6,
          fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
          cursor: 'pointer', letterSpacing: '-0.005em',
          transition: 'background 160ms',
        }}>{copied ? '✓ Copied' : 'Copy to clipboard'}</button>
    </WBSheetShell>
  );
}

// ──────────────── VOICE PICKER ────────────────
function WBVoiceSheet({ w, idea, onClose, onPick }) {
  const [filter, setFilter] = React.useState('all');
  const cats = [{ key: 'all', label: 'All' }, ...VOICE_CATEGORIES];
  const list = filter === 'all' ? VOICES : VOICES.filter(v => v.category === filter);

  // 2026-06-09: the Sample ▶ was a dead button. The worker streams real
  // samples at /voice-sample/{voiceId}; play through one shared element.
  const sampleRef = React.useRef(null);
  const [samplingId, setSamplingId] = React.useState(null);
  const toggleSample = (vid) => {
    const el = sampleRef.current;
    if (!el || !window.SK || !window.SK.mediaUrl) return;
    if (samplingId === vid) {
      try { el.pause(); } catch (e) {}
      setSamplingId(null);
      return;
    }
    // 2026-06-09: samples in R2 are keyed by ELEVENLABS id, rows store slugs —
    // resolve through the catalog or the stream 404s silently.
    const v = (typeof VOICES !== 'undefined' ? VOICES : []).find(x => x.id === vid);
    const sampleKey = (v && v.elevenId) || vid;
    el.onerror = () => {
      setSamplingId(null);
      if (typeof window.flashToast === 'function') window.flashToast('No sample available for this voice yet.');
    };
    el.src = window.SK.mediaUrl('/voice-sample/' + encodeURIComponent(sampleKey));
    try { el.play && el.play().catch(() => setSamplingId(null)); } catch (e) {}
    setSamplingId(vid);
  };
  React.useEffect(() => () => {
    if (sampleRef.current) try { sampleRef.current.pause(); } catch (e) {}
  }, []);

  return (
    <WBSheetShell w={w}
      eyebrow={idea.slug}
      title="Pick a voice" onClose={onClose}>
      <audio ref={sampleRef} preload="none" onEnded={() => setSamplingId(null)} />
      <div style={{
        display: 'flex', gap: 4, overflowX: 'auto',
        margin: '0 -2px 10px', padding: '0 2px 2px',
        scrollbarWidth: 'none', flexShrink: 0,
      }}>
        {cats.map(c => {
          const active = filter === c.key;
          return (
            <button key={c.key} data-no-swipe onClick={() => setFilter(c.key)}
              style={{
                flexShrink: 0,
                background: active ? w.ink : 'transparent',
                color: active ? w.paper : w.inkDim,
                border: `1px solid ${active ? w.ink : w.rule}`,
                borderRadius: 100,
                padding: '4px 10px',
                fontFamily: 'inherit', fontSize: 10.5,
                fontWeight: active ? 600 : 500,
                cursor: 'pointer', whiteSpace: 'nowrap',
              }}>{c.label}</button>
          );
        })}
      </div>
      <div style={{
        flex: 1, overflow: 'auto',
        display: 'flex', flexDirection: 'column', gap: 4,
        margin: '0 -2px', padding: '0 2px',
      }}>
        {list.map(v => {
          const active = v.id === idea.voiceId;
          return (
            <button key={v.id} data-no-swipe
              onClick={() => onPick(v.id)}
              style={{
                background: active ? w.bg : w.paper,
                border: `1px solid ${active ? w.accent : w.rule}`,
                borderRadius: 6,
                padding: '8px 10px',
                cursor: 'pointer', textAlign: 'left',
                fontFamily: 'inherit',
                display: 'flex', alignItems: 'center', gap: 10,
              }}>
              <div style={{
                width: 32, height: 32, borderRadius: '50%',
                background: `linear-gradient(135deg, hsl(${(v.id.charCodeAt(0) * 13) % 360}, 35%, 35%), hsl(${(v.id.charCodeAt(2) * 17) % 360}, 35%, 50%))`,
                color: '#fff',
                fontFamily: '"Geist Mono", monospace',
                fontSize: 11, fontWeight: 700,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                flexShrink: 0,
              }}>{v.name.slice(0, 1)}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                  fontSize: 12.5, fontWeight: 600, color: w.ink,
                  display: 'flex', alignItems: 'baseline', gap: 6,
                }}>
                  {v.name}
                  <span style={{
                    fontFamily: '"Geist Mono", monospace',
                    fontSize: 9, color: w.inkFaint,
                    fontWeight: 500, letterSpacing: '0.04em',
                    textTransform: 'uppercase',
                  }}>{v.accent}</span>
                </div>
                <div style={{
                  fontSize: 11, color: w.inkDim,
                  marginTop: 1, lineHeight: 1.35,
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{v.blurb}</div>
              </div>
              <button data-no-swipe
                onClick={e => { e.stopPropagation(); toggleSample(v.id); }}
                title="Sample"
                style={{
                  width: 28, height: 28,
                  background: samplingId === v.id ? w.accent : 'transparent',
                  border: `1px solid ${samplingId === v.id ? w.accent : w.rule}`,
                  borderRadius: '50%',
                  color: samplingId === v.id ? '#fff' : w.inkDim,
                  cursor: 'pointer',
                  fontFamily: 'inherit', fontSize: 10,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  flexShrink: 0,
                }}>{samplingId === v.id ? '❚❚' : '▶'}</button>
              {active && (
                <span style={{
                  width: 14, height: 14, borderRadius: '50%',
                  background: w.accent, color: '#fff',
                  fontSize: 8, fontWeight: 700,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>✓</span>
              )}
            </button>
          );
        })}
      </div>
    </WBSheetShell>
  );
}

// ──────────────── BG PROMPT EDITOR ────────────────
function WBBgSheet({ w, idea, onClose, onSave }) {
  const [prompt, setPrompt] = React.useState(idea.mix?.bgPrompt || '');
  const [tone, setTone] = React.useState(idea.mix?.bgTone || 'historic');
  const tones = [
    { key: 'historic',   label: 'Historic',   blurb: 'low strings, slow march' },
    { key: 'mysterious', label: 'Mysterious', blurb: 'piano + distant violin' },
    { key: 'tense',      label: 'Tense',      blurb: 'pulsing bass, ticking' },
    { key: 'epic',       label: 'Epic',       blurb: 'horns, big drums' },
    { key: 'somber',     label: 'Somber',     blurb: 'sparse cello, soft' },
    { key: 'curious',    label: 'Curious',    blurb: 'plucked strings, soft' },
  ];

  return (
    <WBSheetShell w={w}
      eyebrow={`${idea.slug} · audio`}
      title="Background score" onClose={onClose}>
      <label style={{
        display: 'block', fontSize: 9, fontWeight: 700,
        letterSpacing: '0.14em', textTransform: 'uppercase',
        color: w.inkDim, marginBottom: 5,
      }}>Tone</label>
      <div style={{
        display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 5,
        marginBottom: 12,
      }}>
        {tones.map(t => {
          const active = tone === t.key;
          return (
            <button key={t.key} data-no-swipe onClick={() => setTone(t.key)}
              style={{
                background: active ? w.bg : w.paper,
                border: `1px solid ${active ? w.accent : w.rule}`,
                borderRadius: 6,
                padding: '8px 10px',
                cursor: 'pointer', textAlign: 'left',
                fontFamily: 'inherit',
              }}>
              <div style={{
                fontSize: 12, fontWeight: 600, color: w.ink,
                marginBottom: 1,
              }}>{t.label}</div>
              <div style={{
                fontSize: 10, color: w.inkFaint,
                fontFamily: '"Geist Mono", monospace',
              }}>{t.blurb}</div>
            </button>
          );
        })}
      </div>

      <label style={{
        display: 'block', fontSize: 9, fontWeight: 700,
        letterSpacing: '0.14em', textTransform: 'uppercase',
        color: w.inkDim, marginBottom: 5,
      }}>Prompt</label>
      <textarea data-no-swipe value={prompt}
        onChange={e => setPrompt(e.target.value)}
        rows={4} autoFocus
        placeholder="describe the music in plain english…"
        style={{
          width: '100%',
          background: w.bg, border: `1px solid ${w.rule}`,
          borderRadius: 6, padding: '10px 12px',
          color: w.ink, fontFamily: 'inherit',
          fontSize: 13, lineHeight: 1.45,
          outline: 'none', resize: 'none',
          marginBottom: 6,
        }} />
      <div style={{
        fontFamily: '"Geist Mono", monospace',
        fontSize: 10, color: w.inkFaint,
        marginBottom: 14,
      }}>~$0.18 · MusicGen Stereo · 60s loop</div>

      <div style={{ display: 'flex', gap: 6 }}>
        <button data-no-swipe onClick={onClose}
          style={{
            flex: 1, height: 38,
            background: 'transparent', color: w.ink,
            border: `1px solid ${w.rule}`, borderRadius: 6,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500,
            cursor: 'pointer',
          }}>Cancel</button>
        <button data-no-swipe
          onClick={() => onSave(prompt, tone)}
          style={{
            flex: 2, height: 38,
            background: w.accent, color: '#fff',
            border: 'none', borderRadius: 6,
            fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
            cursor: 'pointer',
          }}>Re-score →</button>
      </div>
    </WBSheetShell>
  );
}

// ──────────────── VIDEO PLAYER ────────────────
function WBVideoModal({ w, idea, onClose }) {
  const [playing, setPlaying] = React.useState(true);
  const [t, setT] = React.useState(0);
  const dur = idea.durationSec || 60;
  const SK = window.SK;
  const isFinal = idea.stage === 'published' || idea.stage === 'final';
  const realSrc = SK ? (isFinal ? SK.videoUrl(idea.id) : (SK.previewVideoUrl(idea.id) || SK.videoUrl(idea.id))) : null;
  const videoRef = React.useRef(null);
  const [videoFailed, setVideoFailed] = React.useState(false);
  const useReal = !!realSrc && !videoFailed;
  // player-scrub-2026-06-12 (Scott: "can't scroll the video to fast forward or
  // rewind"): the progress bar was display-only. realDur = the mp4's true
  // length (idea.durationSec is the nominal 60s and drifts from the final cut),
  // barRef + seekFrom make the bar a tap/drag seek control.
  const [realDur, setRealDur] = React.useState(0);
  const barRef = React.useRef(null);
  const effDur = (useReal && realDur > 0) ? realDur : dur;
  const seekFrom = React.useCallback((clientX) => {
    const bar = barRef.current;
    if (!bar) return;
    const rect = bar.getBoundingClientRect();
    const frac = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
    const target = frac * effDur;
    if (useReal && videoRef.current) {
      try { videoRef.current.currentTime = target; } catch (e) {}
    }
    setT(target);
  }, [useReal, effDur]);
  const onScrubStart = (e) => {
    e.preventDefault(); e.stopPropagation();
    const cx = e.clientX != null ? e.clientX : (e.touches && e.touches[0] ? e.touches[0].clientX : 0);
    seekFrom(cx);
    const move = (ev) => seekFrom(ev.clientX != null ? ev.clientX : (ev.touches && ev.touches[0] ? ev.touches[0].clientX : 0));
    const up = () => {
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  // mock playhead — drives subtitles + scrubber when no real video
  React.useEffect(() => {
    if (!playing || useReal) return;
    const iv = setInterval(() => {
      setT(t => {
        const n = t + 0.1;
        return n >= dur ? 0 : n;
      });
    }, 100);
    return () => clearInterval(iv);
  }, [playing, dur, useReal]);

  // wire <video> playback ↔ React state
  React.useEffect(() => {
    if (!useReal || !videoRef.current) return;
    const v = videoRef.current;
    const onTime = () => setT(v.currentTime);
    v.addEventListener('timeupdate', onTime);
    if (playing) { try { v.play && v.play().catch(()=>{}); } catch (e) {} }
    else         { try { v.pause && v.pause(); } catch (e) {} }
    return () => v.removeEventListener('timeupdate', onTime);
  }, [useReal, playing]);

  // Switch keyframe every 4s (mock subtitle/keyframe driver)
  const sceneIdx = Math.floor(t / 4) % 8;

  const fmt = (s) => {
    const m = Math.floor(s / 60);
    const sec = Math.floor(s % 60);
    return `${m}:${sec.toString().padStart(2, '0')}`;
  };

  return (
    <div style={{
      // player-pin-2026-06-12 (Scott, mobile): 'absolute' pinned the overlay to
      // the full PAGE (min-height root grows with the board), so the video sat
      // mid-page and the ✕ at page top — scroll down to play, scroll up to
      // close. 'fixed' pins it to the SCREEN: video + ✕ always visible.
      position: 'fixed', inset: 0, zIndex: 32,
      background: 'rgba(0,0,0,0.78)',
      display: 'flex', flexDirection: 'column',
      animation: 'wbFadeIn 160ms ease-out',
    }} onClick={onClose}>
      <div style={{
        flexShrink: 0,
        padding: '14px 16px 8px',
        display: 'flex', alignItems: 'center', gap: 10,
        color: '#fff',
      }}>
        <button data-no-swipe
          onClick={e => { e.stopPropagation(); onClose(); }}
          style={{
            background: 'transparent', border: 'none',
            color: '#fff', fontSize: 22, cursor: 'pointer',
            fontFamily: 'inherit', padding: 0,
            width: 28, height: 28,
          }}>✕</button>
        <div style={{ flex: 1 }}>
          <div style={{
            fontSize: 13, fontWeight: 600,
            letterSpacing: '-0.005em',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>{idea.title}</div>
          <div style={{
            fontFamily: '"Geist Mono", monospace',
            fontSize: 10, color: 'rgba(255,255,255,0.5)',
          }}>{idea.slug} · {idea.durationSec}s · {voiceById(idea.voiceId).name}</div>
        </div>
      </div>

      <div onClick={e => e.stopPropagation()}
        style={{
          flex: 1,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          padding: '0 20px',
        }}>
        <div style={{
          width: 240, aspectRatio: '9/16',
          borderRadius: 8, overflow: 'hidden',
          position: 'relative',
          boxShadow: '0 30px 60px rgba(0,0,0,0.5)',
          background: '#000',
        }}>
          {useReal ? (
            <video ref={videoRef}
              src={realSrc} playsInline
              onError={() => setVideoFailed(true)}
              onLoadedMetadata={e => setRealDur(Number(e.target.duration) || 0)}
              onEnded={() => setPlaying(false)}
              style={{
                position: 'absolute', inset: 0,
                width: '100%', height: '100%',
                objectFit: 'cover', display: 'block',
                background: '#000',
              }} />
          ) : (
            <KeyframePlaceholder seed={idea.slug.charCodeAt(1) + sceneIdx * 5} label="" brighter />
          )}
          {/* Simulated subtitles for mock/placeholder mode ONLY. A real mp4 already
              has burned-in captions; drawing previewLine on top produced two caption
              sets in the player (dual-captions fix 2026-06-04). */}
          {!useReal && (
            <div style={{
              position: 'absolute', bottom: 20, left: 12, right: 12,
              textAlign: 'center',
              color: '#fff',
              fontFamily: 'Inter, system-ui, sans-serif',
              fontWeight: 800, fontSize: 17,
              textShadow: '0 2px 8px rgba(0,0,0,0.8)',
              lineHeight: 1.2,
              letterSpacing: '-0.01em',
            }}>
              {previewLine(idea, t)}
            </div>
          )}
          {/* Tap to pause overlay */}
          <button data-no-swipe
            onClick={() => setPlaying(p => !p)}
            style={{
              position: 'absolute', inset: 0,
              background: playing ? 'transparent' : 'rgba(0,0,0,0.35)',
              border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: '#fff',
              transition: 'background 160ms',
            }}>
            {!playing && (
              <span style={{
                width: 56, height: 56, borderRadius: '50%',
                background: 'rgba(255,255,255,0.96)',
                color: '#000', fontSize: 22,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>▶</span>
            )}
          </button>
        </div>
      </div>

      {/* Controls */}
      <div onClick={e => e.stopPropagation()}
        style={{
          flexShrink: 0,
          padding: '14px 20px 24px',
          color: '#fff',
        }}>
        {/* player-scrub-2026-06-12: tap/drag anywhere on the bar to seek.
            24px-tall hit area (a 3px line is untappable on a phone); the
            visible bar stays thin. touchAction none so a drag scrubs instead
            of scrolling the page. */}
        <div ref={barRef} data-no-swipe
          onPointerDown={onScrubStart}
          style={{
            paddingTop: 10, paddingBottom: 11,
            marginBottom: -3, cursor: 'pointer',
            touchAction: 'none',
          }}>
          <div style={{
            height: 3, background: 'rgba(255,255,255,0.18)',
            borderRadius: 2, overflow: 'hidden',
          }}>
            <div style={{
              width: `${Math.min(100, (t / effDur) * 100)}%`, height: '100%',
              background: '#fff',
              transition: 'width 100ms linear',
            }} />
          </div>
        </div>
        <div style={{
          display: 'flex', justifyContent: 'space-between',
          fontFamily: '"Geist Mono", monospace', fontSize: 11,
          color: 'rgba(255,255,255,0.7)',
          fontVariantNumeric: 'tabular-nums',
        }}>
          <span>{fmt(t)}</span>
          <span>{fmt(effDur)}</span>
        </div>
      </div>
    </div>
  );
}

// Pick a transcript chunk for "subtitles" based on time
function previewLine(idea, t) {
  const tx = (idea.transcript || '').trim();
  if (!tx) return idea.title || '';
  // Split into ~6-word chunks
  const words = tx.split(/\s+/);
  const chunkSize = 5;
  const chunks = [];
  for (let i = 0; i < words.length; i += chunkSize) {
    chunks.push(words.slice(i, i + chunkSize).join(' '));
  }
  const idx = Math.floor((t / (idea.durationSec || 60)) * chunks.length) % chunks.length;
  return chunks[idx] || '';
}

// Read the per-platform copy ({description,hashtags,full_text,...}) for one
// platform out of whatever shape the idea carries it in. sk-data may expose a
// parsed `platformCopy` object; some rows carry the raw `platform_copy_json`
// string. Returns null if no copy exists yet.
function readPlatformCopy(idea, platform) {
  if (!idea) return null;
  let pc = idea.platformCopy;
  if (!pc && idea.platform_copy_json) {
    try { pc = JSON.parse(idea.platform_copy_json); } catch (_) { pc = null; }
  }
  if (!pc || typeof pc !== 'object') return null;
  return pc[platform] || null;
}

// ──────────────── PLATFORM LINK SHEET ────────────────
function WBPlatformSheet({ w, idea, platform, onClose }) {
  const stats = idea.posted?.[platform] || {};
  const meta = {
    tiktok:    { name: 'TikTok',    dot: '#ff2a59', label: 'tiktok.com/@simplyknown_facts', link: stats.link || 'tiktok.com/@simplyknown_facts/v/...' },
    youtube:   { name: 'YouTube',   dot: '#ff0000', label: 'youtube.com/@simplyknown',      link: stats.link || 'youtube.com/shorts/...' },
    // ig-chip-2026-06-12: the worker has generated instagram copy all along
    // (generatePlatformCopy writes tiktok/instagram/youtube) — the sheet just
    // never had an entry for it.
    instagram: { name: 'Instagram', dot: '#e1306c', label: 'instagram.com/@simplyknown_facts', link: stats.link || 'instagram.com/reels/...' },
  }[platform];

  // Post copy (caption + hashtags). Seed from the idea, allow live (re)generate.
  const [copy, setCopy] = React.useState(() => readPlatformCopy(idea, platform));
  const [genState, setGenState] = React.useState('idle'); // idle | loading | error
  const [genErr, setGenErr] = React.useState(null);
  const [copiedKey, setCopiedKey] = React.useState(null);

  const copyToClipboard = (text, key) => {
    if (!text) return;
    try { navigator.clipboard?.writeText(text); } catch (e) {}
    setCopiedKey(key);
    setTimeout(() => setCopiedKey(k => (k === key ? null : k)), 1400);
  };

  const onGenerate = async () => {
    if (!window.SK || !window.SK.generatePlatformCopy) return;
    setGenState('loading'); setGenErr(null);
    try {
      const res = await window.SK.generatePlatformCopy(idea.id);
      const pc = res && res.platform_copy ? res.platform_copy : null;
      if (pc && pc[platform]) {
        setCopy(pc[platform]);
        setGenState('idle');
      } else {
        // mock mode (no-op) or unexpected shape — surface gently.
        setGenState('error');
        setGenErr(window.SK.isLive ? 'no copy returned' : 'live mode only');
      }
    } catch (e) {
      setGenState('error');
      setGenErr((e && e.message) || 'generate failed');
    }
  };

  // auto-copy-2026-06-12 (Scott: "no YouTube or tiktok hashtags appeared"):
  // rows where Generate was never tapped opened to an empty prompt. Auto-run
  // the generator on first open — one call writes all three platforms to
  // platform_copy_json, so every later open is instant.
  React.useEffect(() => {
    if (!copy && window.SK && window.SK.isLive) onGenerate();
    // run once on mount only
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const hashtagText = copy && Array.isArray(copy.hashtags) ? copy.hashtags.join(' ') : '';
  const captionText = copy ? (copy.description || '') : '';
  const fullText = copy ? (copy.full_text || [captionText, hashtagText].filter(Boolean).join('\n\n')) : '';

  const CopyBtn = ({ label, text, ckey }) => {
    const active = copiedKey === ckey;
    return (
      <button data-no-swipe disabled={!text}
        onClick={() => copyToClipboard(text, ckey)}
        style={{
          flex: 1,
          background: active ? w.success : (text ? w.ink : w.raised),
          color: text ? '#fff' : w.inkFaint,
          border: 'none', borderRadius: 5,
          padding: '7px 10px',
          fontFamily: 'inherit', fontSize: 11.5, fontWeight: 600,
          cursor: text ? 'pointer' : 'default', whiteSpace: 'nowrap',
          transition: 'background 160ms',
        }}>{active ? '✓ Copied' : label}</button>
    );
  };

  const Row = ({ k, v }) => (
    <div style={{
      display: 'flex', justifyContent: 'space-between',
      padding: '8px 0',
      borderBottom: `1px solid ${w.ruleSoft}`,
    }}>
      <span style={{
        fontSize: 11, color: w.inkDim,
        fontFamily: '"Geist Mono", monospace',
        letterSpacing: '0.04em',
      }}>{k}</span>
      <span style={{
        fontSize: 12.5, fontWeight: 600, color: w.ink,
        fontFamily: '"Geist Mono", monospace',
        fontVariantNumeric: 'tabular-nums',
      }}>{v}</span>
    </div>
  );

  return (
    <WBSheetShell w={w}
      eyebrow={`${idea.slug} · ${idea.stage === 'published' ? 'published' : 'post copy'}`}
      title={meta.name} onClose={onClose}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 10,
        background: w.bg, border: `1px solid ${w.rule}`,
        borderRadius: 7, padding: '10px 12px',
        marginBottom: 12,
      }}>
        <div style={{
          width: 28, height: 28, borderRadius: 7,
          background: meta.dot,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          color: '#fff', fontWeight: 700, fontSize: 14,
        }}>{meta.name[0]}</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 12.5, fontWeight: 600, color: w.ink }}>{meta.name}</div>
          <div style={{
            fontFamily: '"Geist Mono", monospace',
            fontSize: 10, color: w.inkFaint,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>{meta.label}</div>
        </div>
        {/* 2026-06-09: real link when the row carries one; the old href="#"
            was a dead button pretending to work. Hidden when no link exists. */}
        {stats.link ? (
          <a data-no-swipe target="_blank" rel="noopener noreferrer"
            href={String(stats.link).startsWith('http') ? stats.link : 'https://' + stats.link}
            style={{
              background: w.ink, color: '#fff',
              textDecoration: 'none',
              padding: '6px 10px', borderRadius: 5,
              fontFamily: 'inherit', fontSize: 11, fontWeight: 600,
              cursor: 'pointer', whiteSpace: 'nowrap',
            }}>Open ↗</a>
        ) : null}
      </div>

      {/* ── POST COPY (caption + hashtags) ── */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        marginBottom: 4,
      }}>
        <div style={{
          fontSize: 10, fontWeight: 700,
          letterSpacing: '0.16em', textTransform: 'uppercase',
          color: w.inkFaint,
        }}>Post copy</div>
        <button data-no-swipe onClick={onGenerate}
          disabled={genState === 'loading'}
          style={{
            background: 'transparent', color: w.accent,
            border: `1px solid ${w.rule}`, borderRadius: 5,
            padding: '4px 9px',
            fontFamily: 'inherit', fontSize: 10.5, fontWeight: 600,
            cursor: genState === 'loading' ? 'default' : 'pointer',
            opacity: genState === 'loading' ? 0.6 : 1,
            whiteSpace: 'nowrap',
          }}>
          {genState === 'loading' ? 'Generating…' : (copy ? '↻ Regenerate' : '✦ Generate copy')}
        </button>
      </div>

      <div style={{
        background: w.paper, border: `1px solid ${w.rule}`,
        borderRadius: 7, padding: '10px 12px', marginBottom: 12,
      }}>
        {!copy && genState !== 'loading' && (
          <div style={{
            fontSize: 12, color: w.inkFaint, fontStyle: 'italic',
            lineHeight: 1.5, padding: '6px 0',
          }}>
            No caption yet — tap “Generate copy” to have Haiku write a {meta.name} caption + hashtags.
          </div>
        )}
        {genState === 'loading' && !copy && (
          <div style={{
            fontSize: 12, color: w.inkFaint, fontStyle: 'italic', padding: '6px 0',
          }}>Writing copy…</div>
        )}
        {copy && (
          <>
            <div style={{
              fontSize: 13, color: w.ink, lineHeight: 1.5,
              whiteSpace: 'pre-wrap', wordBreak: 'break-word',
              marginBottom: hashtagText ? 8 : 4,
            }}>{captionText || '(no caption text)'}</div>
            {hashtagText && (
              <div style={{
                fontSize: 12, color: w.accent, lineHeight: 1.5,
                fontFamily: '"Geist Mono", monospace',
                wordBreak: 'break-word', marginBottom: 8,
              }}>{hashtagText}</div>
            )}
            {copy.char_count != null && (
              <div style={{
                fontFamily: '"Geist Mono", monospace',
                fontSize: 10, marginBottom: 8,
                color: copy.over_limit ? w.urgent : w.inkFaint,
              }}>
                {copy.char_count}{copy.max_chars ? `/${copy.max_chars}` : ''} chars{copy.over_limit ? ' · OVER LIMIT' : ''}
              </div>
            )}
            <div style={{ display: 'flex', gap: 6 }}>
              <CopyBtn label="Copy caption" text={fullText} ckey="full" />
              <CopyBtn label="Tags only"    text={hashtagText} ckey="tags" />
            </div>
          </>
        )}
        {genState === 'error' && (
          <div style={{
            marginTop: 8, fontSize: 10.5,
            fontFamily: '"Geist Mono", monospace',
            color: w.urgent,
          }}>{genErr}</div>
        )}
      </div>

      <div style={{
        fontSize: 10, fontWeight: 700,
        letterSpacing: '0.16em', textTransform: 'uppercase',
        color: w.inkFaint, marginBottom: 4,
      }}>Performance</div>
      <div style={{
        background: w.paper, border: `1px solid ${w.rule}`,
        borderRadius: 7, padding: '0 12px', marginBottom: 12,
      }}>
        <Row k="views"    v={formatN(stats.views || 0)} />
        <Row k="likes"    v={formatN(stats.likes || 0)} />
        <Row k="comments" v={formatN(stats.comments || 0)} />
        <Row k="shares"   v={formatN(stats.shares || 0)} />
        <div style={{
          display: 'flex', justifyContent: 'space-between',
          padding: '8px 0',
        }}>
          <span style={{
            fontSize: 11, color: w.inkDim,
            fontFamily: '"Geist Mono", monospace',
          }}>posted</span>
          <span style={{
            fontSize: 12.5, color: w.ink,
            fontFamily: '"Geist Mono", monospace',
          }}>{idea.postedAt}</span>
        </div>
      </div>

      {/* 2026-06-09: no analytics sync is wired — the old "synced from api ·
          ~5m ago" line was a fabrication. Honest label instead. */}
      <div style={{
        fontFamily: '"Geist Mono", monospace',
        fontSize: 10, color: w.inkFaint,
        textAlign: 'center', marginTop: 6,
      }}>
        {(window.SK && window.SK.isLive)
          ? 'stats not synced — analytics not wired yet'
          : `synced from ${platform} api · ~5m ago (mock)`}
      </div>
    </WBSheetShell>
  );
}

Object.assign(window, {
  WBSheetShell, WBFullScreen, WBModal,
  WBLibrarySheet, WBDeploySheet, WBSettingsSheet, WBProdConfirm,
  WBJsonSheet, WBVoiceSheet, WBBgSheet, WBVideoModal, WBPlatformSheet,
  WBAudioLibrarySheet, WBAuditLogSheet,
});

// ──────────────── AUDIO LIBRARY (SFX + BG tabs) ────────────────
// bg-picker-2026-06-11 (Scott: "I want a list of background music organized
// by category with a search bar"): initialTab lets the BG row's Edit button
// open straight to the BG tab; onCustomBg (optional) exposes the old free-text
// score-prompt sheet as a power-user escape hatch.
function WBAudioLibrarySheet({ w, idea, onClose, onUseSfx, onUseBg, initialTab, onCustomBg }) {
  const [tab, setTab] = React.useState(initialTab === 'bg' ? 'bg' : 'sfx');
  const [q, setQ] = React.useState('');
  const [catalog, setCatalog] = React.useState({ sfx: [], bg: [], loading: true, error: null });
  const [playingId, setPlayingId] = React.useState(null);
  const audioRef = React.useRef(null);

  React.useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        // worker /audio-library/catalog returns { tree, entries } where entries
        // is keyed by `${kind}/${sha}`. Transform into per-tab display items
        // that ALSO carry the worker apply-payload fields (sha, prompt,
        // duration, start_s, library_path) so the "Use →" handler can POST
        // straight to /library-add-sfx and /library-set-bg.
        const data = window.SK ? await window.SK.loadAudioCatalog() : null;
        if (cancelled) return;
        if (data && data.entries) {
          const lists = catalogToLists(data);
          setCatalog({ sfx: lists.sfx, bg: lists.bg, loading: false, error: null });
        } else {
          setCatalog({
            sfx: WB_MOCK_AUDIO.sfx, bg: WB_MOCK_AUDIO.bg,
            loading: false, error: window.SK?.isLive ? null : 'mock',
          });
        }
      } catch (e) {
        if (!cancelled) setCatalog({
          sfx: WB_MOCK_AUDIO.sfx, bg: WB_MOCK_AUDIO.bg,
          loading: false, error: e?.message || 'load failed (showing mock)',
        });
      }
    };
    load();
    return () => { cancelled = true; };
  }, []);

  // single audio element shared across rows for preview
  const togglePreview = (item) => {
    if (!audioRef.current) return;
    if (playingId === item.id) {
      audioRef.current.pause();
      setPlayingId(null);
      return;
    }
    // Real catalog items expose previewUrl (/audio-library/preview/{kind}/{sha}).
    // Mock items may carry .url / .path. Fall back to a best-effort media path.
    const url = item.previewUrl
      || item.url
      || (window.SK?.mediaUrl ? window.SK.mediaUrl(item.path || `/audio_library/${item.id}`) : null)
      || '';
    if (!url) return;
    audioRef.current.src = url;
    audioRef.current.volume = 0.6;
    try {
      audioRef.current.play && audioRef.current.play().catch(()=>{});
    } catch (e) {}
    setPlayingId(item.id);
  };

  React.useEffect(() => () => {
    if (audioRef.current) try { audioRef.current.pause(); } catch (e) {}
  }, []);

  // Search across label/prompt/tags/category; then group by category label so
  // the list reads as sections instead of one flat newest-first dump.
  const _qn = q.trim().toLowerCase();
  const _base = tab === 'sfx' ? catalog.sfx : catalog.bg;
  const list = !_qn ? _base : _base.filter(i => (
    `${i.label || ''} ${i.prompt || ''} ${(i.tags || []).join(' ')} ${i.category_path || ''} ${i.source || ''}`
      .toLowerCase().includes(_qn)
  ));
  const groups = (() => {
    const order = [];
    const by = new Map();
    for (const item of list) {
      const k = item.source || 'Uncategorized';
      if (!by.has(k)) { by.set(k, []); order.push(k); }
      by.get(k).push(item);
    }
    order.sort((a, b) => a.localeCompare(b));
    return order.map(k => ({ label: k, items: by.get(k) }));
  })();

  return (
    <WBSheetShell w={w}
      eyebrow={idea ? `${idea.slug} · audio` : 'audio'}
      title="Audio library" onClose={onClose} maxHeight="86%">
      <audio ref={audioRef} preload="none" />
      {/* tabs */}
      <div style={{ display: 'flex', gap: 4, marginBottom: 10 }}>
        {['sfx', 'bg'].map(k => {
          const active = tab === k;
          const label = k === 'sfx' ? `SFX · ${catalog.sfx.length}` : `BG · ${catalog.bg.length}`;
          return (
            <button key={k} data-no-swipe onClick={() => setTab(k)}
              style={{
                flex: 1, padding: '7px 10px',
                background: active ? w.ink : 'transparent',
                color: active ? w.paper : w.inkDim,
                border: `1px solid ${active ? w.ink : w.rule}`,
                borderRadius: 6,
                fontFamily: 'inherit', fontSize: 12,
                fontWeight: active ? 600 : 500,
                cursor: 'pointer',
                letterSpacing: '-0.005em',
              }}>{label}</button>
          );
        })}
      </div>

      {/* search */}
      <input data-no-swipe type="search" value={q}
        onChange={e => setQ(e.target.value)}
        placeholder={tab === 'bg' ? 'Search music… (e.g. drums, tense, orchestral)' : 'Search sounds… (e.g. waves, bell, marching)'}
        style={{
          width: '100%', boxSizing: 'border-box', marginBottom: 8,
          background: w.bg, border: `1px solid ${w.rule}`, borderRadius: 6,
          padding: '8px 10px', fontFamily: 'inherit', fontSize: 12.5,
          color: w.ink, outline: 'none',
        }} />

      {catalog.error && catalog.error !== 'mock' && (
        <div style={{
          padding: '8px 10px', marginBottom: 8,
          background: w.bg, border: `1px solid ${w.rule}`, borderRadius: 5,
          fontSize: 10.5, fontFamily: '"Geist Mono", monospace',
          color: w.urgent,
        }}>{catalog.error}</div>
      )}

      <div style={{
        flex: 1, overflow: 'auto',
        display: 'flex', flexDirection: 'column', gap: 4,
        margin: '0 -2px', padding: '0 2px',
      }}>
        {catalog.loading && (
          <div style={{
            padding: '36px 0', textAlign: 'center',
            color: w.inkFaint, fontSize: 12, fontStyle: 'italic',
          }}>Loading…</div>
        )}
        {!catalog.loading && list.length === 0 && (
          <div style={{
            padding: '36px 0', textAlign: 'center',
            color: w.inkFaint, fontSize: 12, fontStyle: 'italic',
          }}>{_qn ? `Nothing matches “${q.trim()}”.` : 'Nothing in this catalog yet.'}</div>
        )}
        {groups.map(g => (
          <React.Fragment key={g.label}>
            <div style={{
              padding: '8px 2px 2px',
              fontSize: 9.5, fontWeight: 700,
              letterSpacing: '0.10em', textTransform: 'uppercase',
              color: w.inkDim,
            }}>{g.label} · {g.items.length}</div>
        {g.items.map(item => {
          const isPlaying = playingId === item.id;
          return (
            <div key={item.id} style={{
              display: 'flex', alignItems: 'center', gap: 8,
              background: w.paper,
              border: `1px solid ${w.rule}`,
              borderRadius: 6, padding: '8px 10px',
            }}>
              <button data-no-swipe onClick={() => togglePreview(item)}
                title="Preview"
                style={{
                  width: 28, height: 28, borderRadius: '50%',
                  background: isPlaying ? w.accent : 'transparent',
                  color: isPlaying ? '#fff' : w.inkDim,
                  border: `1px solid ${isPlaying ? w.accent : w.rule}`,
                  cursor: 'pointer', fontFamily: 'inherit',
                  fontSize: 10, flexShrink: 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>{isPlaying ? '❚❚' : '▶'}</button>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                  fontSize: 12.5, fontWeight: 600, color: w.ink,
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{item.label || item.name || item.id}</div>
                <div style={{
                  fontSize: 10, color: w.inkFaint, marginTop: 1,
                  fontFamily: '"Geist Mono", monospace',
                  display: 'flex', gap: 6,
                }}>
                  {item.tags && <span>{item.tags.slice(0, 2).join(' · ')}</span>}
                  {item.duration != null && <span>{Number(item.duration).toFixed(1)}s</span>}
                  {item.source && <span>· {item.source}</span>}
                </div>
              </div>
              <button data-no-swipe
                onClick={() => {
                  // Hand the parent an object that is BOTH a worker apply-payload
                  // ({sha,prompt,duration,start_s,library_path}) AND carries the
                  // display fields (label/tone/duration/id) the optimistic mixer
                  // update reads. Real items already have sha/prompt baked in by
                  // catalogToLists; mock items lack sha (and live POST no-ops in
                  // mock mode anyway).
                  const payload = (tab === 'sfx')
                    ? {
                        ...item,
                        sha: item.sha,
                        prompt: item.prompt || item.label || item.name || '',
                        duration: item.duration != null ? Number(item.duration) : 4,
                        volume: 0.30,
                        start_s: 0,
                        library_path: item.library_path || item.category_path || '',
                      }
                    : {
                        ...item,
                        sha: item.sha,
                        prompt: item.prompt || item.label || item.name || '',
                        library_path: item.library_path || item.category_path || '',
                      };
                  (tab === 'sfx' ? onUseSfx : onUseBg)(payload);
                  onClose();
                }}
                style={{
                  background: w.ink, color: '#fff',
                  border: 'none', borderRadius: 5,
                  padding: '5px 10px',
                  fontFamily: 'inherit', fontSize: 11, fontWeight: 600,
                  cursor: 'pointer', whiteSpace: 'nowrap', flexShrink: 0,
                }}>Use →</button>
            </div>
          );
        })}
          </React.Fragment>
        ))}
      </div>

      {/* bg-picker-2026-06-11: the old free-text score-prompt sheet survives as
          an explicit power-user path instead of being the default Edit page. */}
      {tab === 'bg' && typeof onCustomBg === 'function' && (
        <button data-no-swipe
          onClick={() => { onClose(); onCustomBg(); }}
          style={{
            marginTop: 8, width: '100%', height: 32,
            background: 'transparent', color: w.inkDim,
            border: `1px dashed ${w.rule}`, borderRadius: 6,
            fontFamily: 'inherit', fontSize: 11.5, cursor: 'pointer',
          }}>✎ Write a custom score prompt instead…</button>
      )}

      <div style={{
        marginTop: 10, fontSize: 10, color: w.inkFaint,
        fontFamily: '"Geist Mono", monospace', textAlign: 'center',
      }}>
        {catalog.error === 'mock' ? 'showing local mock catalog · live mode talks to /audio_library' : (tab === 'bg' ? 'live · only 60s+ tracks are offered as background music' : 'live · /audio_library')}
      </div>
    </WBSheetShell>
  );
}

// Transform the worker's /audio-library/catalog { tree, entries } into two
// display lists (sfx, bg). entries is keyed `${kind}/${sha}`; each value carries
// { kind, sha, prompt, category_path, category_label, duration, tags,
//   created_at, volume_override }. We bake the worker apply-payload fields
// (sha/prompt/duration/library_path) straight onto each item so "Use →" can
// POST without further lookup. Matches dev1's _libRenderClipList parsing.
function catalogToLists(catalog) {
  const entries = Object.values((catalog && catalog.entries) || {});
  const sfx = [];
  const bg = [];
  for (const e of entries) {
    if (!e || !e.sha) continue;
    const path = e.category_path || '';
    const item = {
      id: `${e.kind}/${e.sha}`,
      sha: e.sha,
      kind: e.kind,
      prompt: e.prompt || '',
      label: (e.prompt || e.category_label || e.sha).slice(0, 80),
      duration: e.duration != null ? Number(e.duration) : null,
      tags: Array.isArray(e.tags) ? e.tags : [],
      source: e.category_label || (path ? path.split('/')[0] : ''),
      category_path: path,
      library_path: path,
      tone: path ? path.split('/')[0].replace(/_/g, ' ') : '',
      created_at: e.created_at || 0,
      volume_override: e.volume_override != null ? e.volume_override : null,
      previewUrl: (window.SK && window.SK.mediaUrl)
        ? window.SK.mediaUrl(`/audio-library/preview/${e.kind}/${e.sha}`)
        : null,
    };
    if (e.kind === 'sfx') sfx.push(item);
    else if (e.kind === 'bg') {
      // bg-60s-floor-2026-06-11 (Scott: ZERO BG options under 60s). Short
      // legacy beds were migrated to sfx/ambient_lib; this guard keeps any
      // straggler (e.g. an old catalog on another machine) from being offered.
      if (!(Number(e.duration) >= 60)) continue;
      bg.push(item);
    }
  }
  // newest first, mirroring dev1's sort by created_at desc.
  sfx.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
  bg.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
  return { sfx, bg };
}

const WB_MOCK_AUDIO = {
  sfx: [
    { id: 'wind_snow_01',    label: 'Wind across snow',          tags: ['wind','exterior'],     duration: 2.4, source: 'pack/v1' },
    { id: 'hoof_hard_01',    label: 'Horse hoof, hard ground',   tags: ['animal','impact'],     duration: 0.8, source: 'pack/v1' },
    { id: 'coin_clink_01',   label: 'Coin metal clink',          tags: ['metal','impact'],      duration: 0.5, source: 'pack/v1' },
    { id: 'trumpet_far_01',  label: 'Distant trumpet, single',   tags: ['horn','military'],     duration: 2.0, source: 'pack/v1' },
    { id: 'door_wood_01',    label: 'Heavy wood door slam',      tags: ['door','impact'],       duration: 1.4, source: 'pack/v1' },
    { id: 'gunshot_far_01',  label: 'Distant muffled gunshot',   tags: ['weapon','distant'],    duration: 0.8, source: 'pack/v1' },
    { id: 'glass_case_01',   label: 'Glass display case slide',  tags: ['glass','museum'],      duration: 2.0, source: 'pack/v1' },
    { id: 'crowd_gasp_01',   label: 'Crowd sharp inhale',        tags: ['crowd','reaction'],    duration: 1.2, source: 'pack/v1' },
    { id: 'paper_unroll_01', label: 'Parchment unroll',          tags: ['paper','foley'],       duration: 1.0, source: 'pack/v1' },
    { id: 'water_lap_01',    label: 'Water lapping shore',       tags: ['water','exterior'],    duration: 3.2, source: 'pack/v1' },
  ],
  bg: [
    { id: 'historic_low_strings',  label: 'Low strings, slow march',   tone: 'historic',   tags: ['strings'], duration: 60, source: 'musicgen' },
    { id: 'mysterious_piano',      label: 'Sparse piano + violin',     tone: 'mysterious', tags: ['piano'],   duration: 60, source: 'musicgen' },
    { id: 'tense_pulse',           label: 'Pulsing bass, ticking',     tone: 'tense',      tags: ['bass'],    duration: 60, source: 'musicgen' },
    { id: 'epic_horns',            label: 'Horns + big drums',         tone: 'epic',       tags: ['horns'],   duration: 60, source: 'musicgen' },
    { id: 'somber_cello',          label: 'Sparse cello, soft',        tone: 'somber',     tags: ['cello'],   duration: 60, source: 'musicgen' },
    { id: 'curious_plucks',        label: 'Plucked strings, soft',     tone: 'curious',    tags: ['pluck'],   duration: 60, source: 'musicgen' },
  ],
};

// ──────────────── AUDIT LOG ────────────────
function WBAuditLogSheet({ w, videoId, onClose }) {
  const [entries, setEntries] = React.useState({ items: null, error: null });

  React.useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        const data = window.SK ? await window.SK.loadAuditLog({ video_id: videoId, limit: 100 }) : null;
        if (cancelled) return;
        if (data && Array.isArray(data.items || data)) {
          setEntries({ items: data.items || data, error: null });
        } else {
          setEntries({ items: WB_MOCK_AUDIT, error: window.SK?.isLive ? null : 'mock' });
        }
      } catch (e) {
        if (!cancelled) setEntries({ items: WB_MOCK_AUDIT, error: e?.message || 'load failed' });
      }
    };
    load();
    return () => { cancelled = true; };
  }, [videoId]);

  return (
    <WBFullScreen w={w}
      title="Audit log"
      subtitle={videoId ? `filtered · ${videoId.slice(-8)}` : 'all videos · last 100'}
      onClose={onClose}
      rightAction={
        <span style={{
          fontFamily: '"Geist Mono", monospace',
          fontSize: 10, color: entries.error && entries.error !== 'mock' ? w.urgent : w.inkFaint,
        }}>{entries.error === 'mock' ? 'mock' : (entries.error ? 'error · showing mock' : 'D1 ✓')}</span>
      }>
      {entries.items == null && (
        <div style={{
          padding: '36px 0', textAlign: 'center',
          color: w.inkFaint, fontSize: 12, fontStyle: 'italic',
        }}>Loading…</div>
      )}
      {entries.items && entries.items.length === 0 && (
        <div style={{
          padding: '36px 0', textAlign: 'center',
          color: w.inkFaint, fontSize: 12, fontStyle: 'italic',
        }}>No log entries.</div>
      )}
      {entries.items && entries.items.map((e, i) => (
        <div key={e.id || i} style={{
          background: w.paper,
          border: `1px solid ${w.ruleSoft}`,
          borderRadius: 6,
          padding: '8px 10px', marginBottom: 5,
          display: 'flex', alignItems: 'flex-start', gap: 8,
        }}>
          <span style={{
            width: 6, height: 6, borderRadius: '50%',
            background: e.level === 'error' ? w.urgent : e.level === 'warn' ? w.amber : w.accent,
            marginTop: 6, flexShrink: 0,
          }} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{
              display: 'flex', gap: 6, alignItems: 'baseline',
              fontFamily: '"Geist Mono", monospace',
              fontSize: 10, color: w.inkFaint,
            }}>
              <span style={{
                color: w.ink, fontWeight: 700, letterSpacing: '0.06em',
                textTransform: 'uppercase',
              }}>{e.action}</span>
              {e.actor && <span>· {e.actor}</span>}
              {e.video_id && <span>· {String(e.video_id).slice(-8)}</span>}
              <span style={{ flex: 1 }} />
              <span>{e.t || e.timestamp || ''}</span>
            </div>
            {e.detail && (
              <div style={{
                fontSize: 12, color: w.ink, marginTop: 2,
                lineHeight: 1.4,
              }}>{e.detail}</div>
            )}
          </div>
        </div>
      ))}
    </WBFullScreen>
  );
}

const WB_MOCK_AUDIT = [
  { id: '1', action: 'promote_preview', actor: 'scott',   video_id: 'idea_seed_1777650167_c7f984', t: '12s ago',  level: 'info',  detail: 'Curate → Audio (12 picks locked)' },
  { id: '2', action: 'save_mix',        actor: 'scott',   video_id: 'idea_seed_1777650168_60f333', t: '46s ago',  level: 'info',  detail: 'voice 1.15 · bg 0.50 · sfx 0.35' },
  { id: '3', action: 'regen_candidate', actor: 'scott',   video_id: 'idea_seed_1777650163_d75302', t: '2m ago',   level: 'info',  detail: 'scene_4 · candidate 2 · "more dramatic lighting"' },
  { id: '4', action: 'approve_idea',    actor: 'scott',   video_id: 'idea_seed_1777650162_e1f102', t: '8m ago',   level: 'info',  detail: 'queued · 2 engines · ~$0.30' },
  { id: '5', action: 'render_fail',     actor: 'system',  video_id: 'idea_seed_1777650162_e1f102', t: '11m ago',  level: 'warn',  detail: 'photon timeout · retried' },
  { id: '6', action: 'approve_final',   actor: 'scott',   video_id: 'idea_seed_1777650160_88a431', t: '2h ago',   level: 'info',  detail: 'posted to tiktok + youtube' },
  { id: '7', action: 'save_edits',      actor: 'scott',   video_id: 'idea_seed_1777650167_c7f984', t: '3h ago',   level: 'info',  detail: 'title edited · voice → knox_dark' },
  { id: '8', action: 'auth.refresh',    actor: 'system',  video_id: null,                          t: '5h ago',   level: 'info',  detail: 'JWT rotated · 4 quotas reset' },
  { id: '9', action: 'budget.alert',    actor: 'system',  video_id: null,                          t: 'yesterday', level: 'error', detail: 'daily $8 budget reached · 18:42 UTC' },
];

// Sheet animations
(function injectSheetKeyframes() {
  if (document.getElementById('wb-sheet-keyframes')) return;
  const s = document.createElement('style');
  s.id = 'wb-sheet-keyframes';
  s.textContent = `
    @keyframes wbFadeIn { from { opacity: 0; } to { opacity: 1; } }
    @keyframes wbSlideUp { from { transform: translateY(28px); opacity: 0.8; } to { transform: translateY(0); opacity: 1; } }
    @keyframes wbSlideIn { from { transform: translateX(28px); opacity: 0.7; } to { transform: translateX(0); opacity: 1; } }
    @keyframes wbModalIn { from { transform: scale(0.96); opacity: 0; } to { transform: scale(1); opacity: 1; } }
  `;
  document.head.appendChild(s);
})();
