// ══════════════════════════════════════════════════════════════════════════
// THE DIRECTOR'S LAB — one page, several pages inside it (owner, 2026-09-07).
//
//   "I want it as several pages inside this one main page: a page for the idea
//    and the story, a page for the scripts and the scenes, a page for the
//    prompts — images and videos alike — with every tool I might need to
//    understand, read, edit and improve the way the work flows."
//
// The first lab put a raw JSON textarea where the brief goes, bare <select>s
// where the choices go, and printed every output as JSON.stringify. The owner's
// verdict was "this page cannot be used by a human". So: nothing on this page
// is JSON unless he opens "advanced: JSON" himself, every output is rendered as
// the thing it is (a treatment is a card, a shot list is a table, a prompt is a
// text area you can edit and copy), and every stage carries its own tools —
// the assembled prompt, the rules that reached it, the feedback that writes the
// next rule.
//
// FIVE TABS, one workspace underneath them:
//   Idea & story   — the brief as a form, then the three ideas as cards.
//   Script & scenes— script beats, then the shot list as an editable table.
//   Prompts        — the working shot's keyframe/video prompts, refs, one image.
//   Film           — the film path (2026-09-27): the Script Writer's
//                    `film_script`, the Director's `film_plan`, and the paid
//                    `film_draft` / `film_final` rows, READ-ONLY.
//   Runs & rules   — history, compare, the rule budget of every stage.
//
// This page never creates a film and never approves anything. Text runs spend
// subscription tokens; the ONE priced button is «Render image», and its price
// is fetched from the backend and shown before it can be pressed. A draft or a
// final is bought on the simple view only, where its price is shown first;
// here its row is shown and nothing on it can be pressed.
//
// Every top-level name here is prefixed `Lab`/`LAB_`/`lab` on purpose: the
// compiler concatenates every .jsx into ONE scope (CLAUDE.md §6b).
//
// 2026-09-07 — THIS PAGE IS NOW THE «Advanced» HALF. The owner's verdict was
// that it is built for an engineer, so `page-director-fast.jsx` owns the
// default view and `DirectorLabPage` there is the switch between the two.
// Nothing below changed: the engineer's workspace is intact, one toggle away.
// ══════════════════════════════════════════════════════════════════════════

const LAB_TABS = [
  { id: 'idea',    label: 'Idea & story' },
  { id: 'script',  label: 'Script & scenes' },
  { id: 'prompts', label: 'Prompts' },
  { id: 'film',    label: 'Film (one generation)' },
  { id: 'runs',    label: 'Runs & rules' },
];

// The stage map, in the order the real lane walks them. `tab` is where each
// stage is run from, so the map doubles as navigation.
const LAB_STAGES = [
  { id: 'treatment',       label: 'Ideas ×3',      tab: 'idea',
    blurb: 'three ideas from one brief, each carrying one value' },
  { id: 'script',          label: 'Script',        tab: 'script',
    blurb: 'beats and the lines she speaks' },
  { id: 'shot_list',       label: 'Shot list',     tab: 'script',
    blurb: 'the chosen treatment, cut into shots' },
  { id: 'keyframe_prompt', label: 'Keyframe prompt', tab: 'prompts',
    blurb: 'the still the video is animated from' },
  { id: 'video_prompt',    label: 'Video prompt',  tab: 'prompts',
    blurb: 'what the video model is told to move' },
  // The film path (2026-09-27, docs/research/lab-film-path-2026-09-27.md).
  // `film_script`, not `script`: that name is the beat writer's above, and its
  // rules are filed under it.
  { id: 'film_script',     label: 'Film script',   tab: 'film',
    blurb: 'the Script Writer: the whole film, second by second, from the chosen idea' },
  { id: 'film_plan',       label: 'Film plan',     tab: 'film',
    blurb: 'the Director: one take or cuts, and the words of ONE generation' },
];

// The two PAID stages of the film path. They are not writing stages — no
// agent, no rules — so they are not in the map above; their rows are shown
// read-only, and nothing on this page can buy, stop or re-make one.
const LAB_PAID_STAGES = ['film_draft', 'film_final'];

const LAB_MISSING = ['hook', 'idea', 'specificity', 'character', 'place',
                     'motion', 'dialogue', 'ending', 'platform', 'canon', 'other'];

const LAB_BLOCK_LABELS = {
  doctrine: 'doctrine — the stage’s standing instructions',
  lessons: 'lessons + craft',
  rules: 'your rules',
  client_context: 'client context',
  brief: 'the brief',
  platform: 'platform',
  canon: 'canon clause — appended by code, not by the author',
  concept: 'concept',
  roster: 'roster',
  treatment: 'the chosen treatment',
  examples: 'your past gate decisions',
  record: 'the client’s record — what the author writes from',
  precedence: 'which wins — the stage’s rules over the craft notes',
  proposed: 'already proposed for this client — the ideas never to repeat',
};

const LAB_AUDIENCE = [
  { id: 'cold',       label: 'Cold',        hint: 'never heard of her' },
  { id: 'saw_teaser', label: 'Saw a teaser', hint: 'has seen one thing' },
  { id: 'knows_her',  label: 'Knows her',   hint: 'follows her' },
];

// The platform line is fixed because the lane's motion and duration rules are
// written against it. Shown, never typed.
const LAB_PLATFORM = 'Instagram Reels / TikTok 9:16 15–20 s sound on';

// «Sample default» — the shape of a first-film brief from the spec. A starting
// point he edits, not a decision made for him; the character line is read off
// the record, so the sample names nobody.
const LAB_DEFAULT_BRIEF = {
  who: 'the brand character — confident, warm, sharp; a star in its own right',
  objective: 'introduce the character',
  audience_state: 'cold',
  platform: LAB_PLATFORM,
  duration_s: 18,
  feeling: 'who is that? I want to see them again',
  must: ['a hook in the first 1.5 s',
         'a clear reason she is here — the one value this film makes easy to feel',
         'the character speaks ≥ 1 line in its own voice',
         'the character\'s name once'],
  never: ['food, dishes, menu, prices, restaurant interior',
          'purchase CTA',
          'explanation cards doing the story’s job',
          'any shot whose only event is a blink'],
  cta: 'follow / name recall',
  language: { dialogue: 'ar', on_screen: 'ar' },
  audio: { voice: 'locked', bed: 'library', native: 'ambience' },
  budget: { video_usd: 25, image_usd: 3, retakes_per_shot: 2 },
};

const LAB_BLANK_BRIEF = {
  who: '', objective: '', audience_state: 'cold', platform: LAB_PLATFORM,
  duration_s: 18, feeling: '', must: [], never: [], cta: '',
  language: { dialogue: 'ar', on_screen: 'ar' },
  audio: { voice: 'locked', bed: 'library', native: 'ambience' },
  budget: { video_usd: 25, image_usd: 3, retakes_per_shot: 2 },
};

const labNum = (v, fallback) => {
  const n = parseFloat(v);
  return Number.isFinite(n) ? n : fallback;
};

const labMoney = (n) => {
  const v = Number(n) || 0;
  return v && v < 0.01 ? `$${v.toFixed(4)}` : `$${v.toFixed(2)}`;
};

const labSecs = (ms) => `${((Number(ms) || 0) / 1000).toFixed(1)}s`;

const LAB_INPUT = {
  width: '100%', padding: '7px 9px', fontSize: 12.5, lineHeight: 1.55,
  background: 'var(--bg-2)', border: '1px solid var(--border-2)',
  borderRadius: 6, color: 'var(--text-0)', fontFamily: 'inherit',
};

// ── small building blocks ────────────────────────────────────────────────

const LabLabel = ({ children }) => (
  <div style={{ fontSize: 10.5, letterSpacing: '0.05em', textTransform: 'uppercase',
                color: 'var(--text-3)', marginBottom: 5, fontWeight: 500 }}>
    {children}
  </div>
);

const LabField = ({ label, hint, children }) => (
  <div>
    <LabLabel>{label}</LabLabel>
    {children}
    {hint ? (
      <div className="muted" style={{ fontSize: 10.5, marginTop: 4, lineHeight: 1.5 }}>
        {hint}
      </div>
    ) : null}
  </div>
);

const LabCard = ({ title, hint, right, children, tone }) => (
  <div className="card" style={{ marginBottom: 14,
        borderColor: tone === 'warn' ? 'var(--warn)'
                   : tone === 'err' ? 'var(--err)' : undefined }}>
    {(title || right) && (
      <div className="card-header" style={{ display: 'flex', alignItems: 'center',
                                            gap: 10 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="card-title">{title}</div>
          {hint ? (
            <div className="muted" style={{ fontSize: 11, marginTop: 3,
                                            lineHeight: 1.5 }}>{hint}</div>
          ) : null}
        </div>
        {right}
      </div>
    )}
    <div className="card-body">{children}</div>
  </div>
);

const LabEmpty = ({ what }) => (
  <div className="muted" style={{ fontSize: 12, padding: '14px 0',
                                  fontStyle: 'italic' }}>{what}</div>
);

const LabNote = ({ tone, children }) => {
  const c = tone === 'err' ? 'var(--err)' : tone === 'ok' ? 'var(--ok)'
          : tone === 'info' ? 'var(--info)' : 'var(--warn)';
  const bg = tone === 'err' ? 'var(--err-soft)' : tone === 'ok' ? 'var(--ok-soft)'
          : tone === 'info' ? 'var(--info-soft)' : 'var(--warn-soft)';
  return (
    <div style={{ padding: '9px 12px', borderRadius: 7, background: bg,
                  border: `1px solid ${c}`, color: 'var(--text-1)',
                  fontSize: 11.5, lineHeight: 1.55, marginTop: 8 }} dir="auto">
      {children}
    </div>
  );
};

// A list the owner edits one line at a time — «must» and «never» are not a
// paragraph, they are a list of things, and each of them can be wrong on its own.
const LabListEditor = ({ rows, onChange, placeholder }) => {
  const list = Array.isArray(rows) ? rows : [];
  const set = (i, v) => onChange(list.map((x, j) => (j === i ? v : x)));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      {list.map((r, i) => (
        <div key={i} style={{ display: 'flex', gap: 6, alignItems: 'flex-start' }}>
          <div className="muted" style={{ fontSize: 11, paddingTop: 8, width: 14 }}>
            {i + 1}
          </div>
          <input value={r} dir="auto" style={LAB_INPUT}
                 placeholder={placeholder}
                 onChange={e => set(i, e.target.value)}/>
          <button className="btn small ghost" title="remove this line"
                  onClick={() => onChange(list.filter((_, j) => j !== i))}>×</button>
        </div>
      ))}
      <div>
        <button className="btn small ghost"
                onClick={() => onChange([...list, ''])}>+ add a line</button>
      </div>
      {!list.length && <div className="muted" style={{ fontSize: 11 }}>
        nothing here yet</div>}
    </div>
  );
};

const LabCopyButton = ({ text, label }) => {
  const [done, setDone] = useState(false);
  return (
    <button className="btn small ghost" disabled={!text}
            onClick={() => {
              try {
                navigator.clipboard.writeText(String(text || ''));
                setDone(true);
                setTimeout(() => setDone(false), 1400);
              } catch (e) { toast('the browser refused the clipboard', 'err'); }
            }}>
      {done ? 'copied' : (label || 'copy')}
    </button>
  );
};

const LabStars = ({ value, onChange }) => (
  <div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
    {[1, 2, 3, 4, 5].map(n => (
      <button key={n} className="btn small ghost"
              title={`${n} of 5`}
              style={{ padding: '2px 5px', fontSize: 17, lineHeight: 1,
                       color: n <= value ? 'var(--accent)' : 'var(--text-4)' }}
              onClick={() => onChange(n)}>★</button>
    ))}
    <span className="muted" style={{ fontSize: 11, marginLeft: 4 }}>{value}/5</span>
  </div>
);

// ── the assembled prompt, block by block ─────────────────────────────────
// A prompt shown as one wall of text is a prompt nobody audits.
const LabAgentView = ({ run, open }) => {
  if (!run) return null;
  const a = run.assembled || {};
  const blocks = a.blocks || {};
  const keys = Object.keys(blocks).filter(k => String(blocks[k] || '').trim());
  const imgs = a.images || [];
  return (
    <details open={!!open} style={{ marginTop: 12, borderTop: '1px solid var(--border-1)',
                                    paddingTop: 10 }}>
      <summary style={{ cursor: 'pointer', fontSize: 11.5, color: 'var(--text-2)' }}>
        Agent’s view — the exact prompt this run received
        {' · '}{keys.length} block{keys.length === 1 ? '' : 's'}
        {' · '}{imgs.length} image ref{imgs.length === 1 ? '' : 's'}
      </summary>
      <div style={{ marginTop: 10 }}>
        <div className="muted" style={{ fontSize: 11, marginBottom: 8 }}>
          {run.agent_name || 'the agent'} · {run.model || 'model unrecorded'} ·
          tag <span className="mono">{run.tag || '—'}</span> ·
          {' '}{labMoney(run.cost_usd)} · {labSecs(run.duration_ms)} ·
          {' '}{run.tokens_in || 0} in / {run.tokens_out || 0} out
        </div>
        {(a.notes || []).map((n, i) => <LabNote key={i}>{n}</LabNote>)}
        {imgs.length > 0 && (
          <div style={{ marginTop: 10 }}>
            <LabLabel>the images that rode with it</LabLabel>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              {imgs.map((im, i) => (
                <div key={i} style={{ width: 92 }}>
                  <img src={window.ASSET_URL(im.path)} alt={im.label || ''}
                       style={{ width: 92, height: 92, objectFit: 'cover',
                                borderRadius: 6, border: '1px solid var(--border-1)',
                                background: 'var(--bg-2)' }}/>
                  <div className="muted" style={{ fontSize: 10, marginTop: 3,
                                                  wordBreak: 'break-word' }}>
                    #{im.ref_id} {im.label || ''}
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
        {keys.map(k => (
          <details key={k} style={{ marginTop: 8 }}>
            <summary style={{ cursor: 'pointer', fontSize: 11.5,
                              color: 'var(--text-1)' }}>
              {LAB_BLOCK_LABELS[k] || k}
              <span className="muted"> · {String(blocks[k]).length} chars</span>
            </summary>
            <pre className="mono" dir="auto"
                 style={{ maxHeight: 260, overflow: 'auto', whiteSpace: 'pre-wrap',
                          fontSize: 11, lineHeight: 1.6, background: 'var(--bg-2)',
                          border: '1px solid var(--border-1)', borderRadius: 6,
                          padding: '9px 11px', marginTop: 6 }}>{blocks[k]}</pre>
          </details>
        ))}
        <details style={{ marginTop: 8 }}>
          <summary style={{ cursor: 'pointer', fontSize: 11.5, color: 'var(--text-2)' }}>
            the whole system prompt · {String(a.system || '').length} chars
          </summary>
          <pre className="mono" dir="auto"
               style={{ maxHeight: 300, overflow: 'auto', whiteSpace: 'pre-wrap',
                        fontSize: 11, lineHeight: 1.6, background: 'var(--bg-2)',
                        border: '1px solid var(--border-1)', borderRadius: 6,
                        padding: '9px 11px', marginTop: 6 }}>{a.system}</pre>
        </details>
        <details style={{ marginTop: 8 }}>
          <summary style={{ cursor: 'pointer', fontSize: 11.5, color: 'var(--text-2)' }}>
            the whole user message · {String(a.user || '').length} chars
          </summary>
          <pre className="mono" dir="auto"
               style={{ maxHeight: 300, overflow: 'auto', whiteSpace: 'pre-wrap',
                        fontSize: 11, lineHeight: 1.6, background: 'var(--bg-2)',
                        border: '1px solid var(--border-1)', borderRadius: 6,
                        padding: '9px 11px', marginTop: 6 }}>{a.user}</pre>
        </details>
      </div>
    </details>
  );
};

// The author's own rule-breaks, counted in code. Loud on purpose.
const LabFindings = ({ findings, what }) => {
  const rows = findings || [];
  if (!rows.length) {
    return <div style={{ fontSize: 11.5, color: 'var(--ok)', marginTop: 8 }}>
      ✓ this {what} breaks none of the lane’s rules</div>;
  }
  return (
    <LabNote>
      <div style={{ fontWeight: 600, marginBottom: 4 }}>
        {rows.length} thing{rows.length === 1 ? '' : 's'} wrong with this {what}
      </div>
      {rows.map((f, i) => <div key={i} style={{ marginTop: 3 }}>· {f}</div>)}
    </LabNote>
  );
};

// ── the rules of ONE stage, with the budget meter on top ─────────────────
// The owner's verdict on the first lab named this exactly: a Script Writer rule
// about something else was shown inside the treatment stage. A rule belongs to
// one stage. The backend scopes them by the «<stage>:» trigger prefix; when it
// is an older backend that still answers agent-wide, the same prefix scopes
// them here, and the count of the others is stated rather than hidden.
const LabRulesPanel = ({ stage, budget, onRetire, compact }) => {
  if (!budget) return <LabEmpty what="the rules of this stage have not loaded"/>;
  const prefix = budget.trigger_prefix || `${stage}:`;
  const all = budget.active || [];
  const scopedByServer = budget.other_stage_rules !== undefined;
  const mine = scopedByServer ? all
    : all.filter(r => String(r.trigger || '').trim().toLowerCase().startsWith(prefix));
  const others = scopedByServer ? (budget.other_stage_rules || 0)
                                : (all.length - mine.length);
  const chars = scopedByServer ? budget.chars
    : mine.reduce((n, r) => n + String(r.rule || '').length, 0);
  const pctC = Math.min(100, Math.round((mine.length / (budget.max_count || 12)) * 100));
  const pctH = Math.min(100, Math.round((chars / (budget.max_chars || 2400)) * 100));
  const over = mine.length > (budget.max_count || 12) || chars > (budget.max_chars || 2400);
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
                    flexWrap: 'wrap' }}>
        <div style={{ fontSize: 12.5, color: over ? 'var(--err)' : 'var(--text-1)' }}>
          {mine.length}/{budget.max_count} rules · {chars}/{budget.max_chars} chars
        </div>
        <div className="muted" style={{ fontSize: 11 }}>
          carried by {budget.agent_name || 'this stage’s agent'}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
        {[pctC, pctH].map((p, i) => (
          <div key={i} style={{ flex: 1, height: 4, borderRadius: 3,
                                background: 'var(--bg-3)', overflow: 'hidden' }}>
            <div style={{ width: `${p}%`, height: '100%',
                          background: over ? 'var(--err)' : 'var(--accent)' }}/>
          </div>
        ))}
      </div>
      {budget.reaches_prompt === false && (
        <LabNote>This stage reads no rules today — a rule written here is kept
          and shown, and does not yet reach the prompt.</LabNote>
      )}
      <div style={{ marginTop: 10 }}>
        {mine.map(r => (
          <div key={r.id} style={{ display: 'flex', gap: 8, alignItems: 'flex-start',
                                   padding: '8px 0',
                                   borderTop: '1px solid var(--border-1)' }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12, lineHeight: 1.55, color: 'var(--text-1)' }}
                   dir="auto">{r.rule}</div>
              <div className="muted" style={{ fontSize: 10.5, marginTop: 3 }}>
                when: {r.trigger || '—'} · used {r.uses || 0}× · by {r.created_by || '—'}
                {r.prohibition ? ' · reads as a prohibition' : ''}
              </div>
            </div>
            <button className="btn small ghost" title="retire this rule"
                    onClick={() => {
                      if (window.confirm(`Retire this rule?\n\n${r.rule}`)) onRetire(r.id);
                    }}>×</button>
          </div>
        ))}
        {!mine.length && (
          <div className="muted" style={{ fontSize: 11.5, paddingTop: 6 }}>
            no rules on this stage yet
          </div>
        )}
      </div>
      {others > 0 && !compact && (
        <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>
          this agent carries {others} rule{others === 1 ? '' : 's'} for its other
          stages — they are not shown here and they do not reach this stage.
        </div>
      )}
    </div>
  );
};

// ── the feedback drawer that becomes a rule ──────────────────────────────
const LabFeedback = ({ stage, run, fb, setFb, budget, onSave, onRetire,
                       busy, diff }) => {
  // English only (S14b): a .jsx surface carries no Arabic script, and there is
  // no per-line exemption tag for the UI the way there is for backend code, so
  // the Arabic negations this used to recognise are gone with the Arabic mode.
  const nudge = /(^|\W)(never|don'?t|do not|no |avoid|must not|stop)/i
    .test(fb.rule_text || fb.note || '');
  const prefix = (budget && budget.trigger_prefix) || `${stage}:`;
  return (
    <LabCard title={`What was missing — ${stage.replace('_', ' ')}`}
             hint="Your score and your sentence. With one more tap the sentence becomes a rule this stage reads next time.">
      {!run && <LabEmpty what="run this stage first — feedback attaches to a run"/>}
      {run && (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <LabField label="score">
              <LabStars value={fb.score} onChange={v => setFb({ ...fb, score: v })}/>
            </LabField>
            <LabField label="what is missing"
                      hint="one tag — the nuance goes in the note underneath.">
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                {LAB_MISSING.map(m => (
                  <span key={m}
                        className={`chip clickable${fb.missing === m ? ' active' : ''}`}
                        onClick={() => setFb({ ...fb, missing: m })}>{m}</span>
                ))}
              </div>
            </LabField>
            <LabField label="note">
              <textarea rows={4} dir="auto" value={fb.note} style={LAB_INPUT}
                        placeholder="what you would have wanted instead"
                        onChange={e => setFb({ ...fb, note: e.target.value })}/>
            </LabField>
            <label style={{ display: 'flex', gap: 7, alignItems: 'center',
                            fontSize: 12 }}>
              <input type="checkbox" checked={!!fb.as_rule}
                     onChange={e => setFb({ ...fb, as_rule: e.target.checked,
                                            rule_text: fb.rule_text || fb.note })}/>
              write this as a rule for <b>{stage.replace('_', ' ')}</b>
            </label>
            {fb.as_rule && (
              <div>
                <textarea rows={3} dir="auto" value={fb.rule_text} maxLength={240}
                          style={LAB_INPUT}
                          onChange={e => setFb({ ...fb, rule_text: e.target.value })}/>
                <div className="muted" style={{ fontSize: 10.5, marginTop: 4 }}>
                  {String(fb.rule_text || '').length}/240 chars · it will be filed
                  under «{prefix} {fb.missing}» so only this stage reads it.
                </div>
                {nudge && (
                  <LabNote>Prefer a positive example: say what to do, not what to
                    avoid. A prohibition tells the author what the film is not.</LabNote>
                )}
              </div>
            )}
            <div style={{ display: 'flex', gap: 8 }}>
              <button className="btn primary" disabled={busy}
                      onClick={onSave}>
                {busy ? 'saving…' : (fb.as_rule ? 'save and make it a rule'
                                                : 'save feedback')}
              </button>
            </div>
            {diff ? (
              <div>
                <LabLabel>what changed in the prompt</LabLabel>
                <pre className="mono" style={{ maxHeight: 220, overflow: 'auto',
                       whiteSpace: 'pre-wrap', fontSize: 11, lineHeight: 1.6,
                       background: 'var(--bg-2)', border: '1px solid var(--border-1)',
                       borderRadius: 6, padding: '9px 11px' }}>{diff}</pre>
              </div>
            ) : null}
          </div>
          <div>
            <LabLabel>the rules this stage carries</LabLabel>
            <LabRulesPanel stage={stage} budget={budget} onRetire={onRetire}/>
          </div>
        </div>
      )}
    </LabCard>
  );
};

// ── a run's header line: model, cost, time, findings ─────────────────────
const LabRunHeader = ({ run, label }) => {
  if (!run) return null;
  return (
    <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap',
                  marginBottom: 8 }}>
      <span className="pill ai">run #{run.id}</span>
      <span className="muted" style={{ fontSize: 11 }}>
        {label} · {run.agent_name || '—'} · {run.model || '—'} ·
        {' '}{labMoney(run.cost_usd)} · {labSecs(run.duration_ms)} ·
        {' '}{(run.created_at || '').slice(0, 16).replace('T', ' ')}
      </span>
    </div>
  );
};

// ── IDEA & STORY ─────────────────────────────────────────────────────────

const LabBriefForm = ({ brief, setBrief, language, advanced, setAdvanced,
                        rawText, setRawText }) => {
  const b = brief || {};
  const set = (k, v) => setBrief({ ...b, [k]: v });
  const setIn = (grp, k, v) => setBrief({ ...b, [grp]: { ...(b[grp] || {}), [k]: v } });
  const audio = b.audio || {};
  const budget = b.budget || {};
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <LabField label="Who she is"
                hint="The character line. Her record is the authority; this is how the brief describes her.">
        <textarea rows={2} dir="auto" value={b.who || ''} style={LAB_INPUT}
                  onChange={e => set('who', e.target.value)}/>
      </LabField>

      <LabField label="Objective — what is this film FOR?"
                hint="Never guessed. A blank objective parks a real film and asks.">
        <input dir="auto" value={b.objective || ''} style={LAB_INPUT}
               onChange={e => set('objective', e.target.value)}/>
      </LabField>

      <LabField label="Audience state"
                hint="A film for a stranger and a film for a follower are different films.">
        <div className="segmented">
          {LAB_AUDIENCE.map(a => (
            <button key={a.id} title={a.hint}
                    className={b.audience_state === a.id ? 'active' : ''}
                    onClick={() => set('audience_state', a.id)}>{a.label}</button>
          ))}
        </div>
        <div className="muted" style={{ fontSize: 10.5, marginTop: 5 }}>
          {(LAB_AUDIENCE.find(a => a.id === b.audience_state) || {}).hint || ''}
        </div>
      </LabField>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 130px', gap: 12 }}>
        <LabField label="Platform"
                  hint="Fixed: the lane’s motion and duration rules are written against this line.">
          <div style={{ ...LAB_INPUT, background: 'var(--bg-3)',
                        color: 'var(--text-2)' }}>{b.platform || LAB_PLATFORM}</div>
        </LabField>
        <LabField label="Duration (s)">
          <input type="number" min="5" max="60" step="1" style={LAB_INPUT}
                 value={b.duration_s === undefined ? 18 : b.duration_s}
                 onChange={e => set('duration_s', labNum(e.target.value, 18))}/>
        </LabField>
      </div>

      <LabField label="The feeling it should leave behind">
        <input dir="auto" value={b.feeling || ''} style={LAB_INPUT}
               onChange={e => set('feeling', e.target.value)}/>
      </LabField>

      <LabField label="Must" hint="each line is a thing the film has to do">
        <LabListEditor rows={b.must || []} placeholder="a hook in the first 1.5 s"
                       onChange={v => set('must', v)}/>
      </LabField>
      <LabField label="Never" hint="each line is a thing the film may not do">
        <LabListEditor rows={b.never || []} placeholder="food, dishes, menu"
                       onChange={v => set('never', v)}/>
      </LabField>

      <LabField label="CTA">
        <input dir="auto" value={b.cta || ''} style={LAB_INPUT}
               onChange={e => set('cta', e.target.value)}/>
      </LabField>

      <LabField label="Language"
                hint={language
                  ? `Read-only — it comes from ${language.name}'s record, not from this brief. Change it on her character page.`
                  : 'Read-only — it comes from the character record, not from this brief.'}>
        <div style={{ ...LAB_INPUT, background: 'var(--bg-3)', color: 'var(--text-2)' }}>
          {language
            ? `${language.primary || '—'}${language.mix ? ` · ${language.mix}` : ''}${language.dialect ? ` · ${language.dialect}` : ''}`
            : `dialogue ${(b.language || {}).dialogue || '—'} · on-screen ${(b.language || {}).on_screen || '—'}`}
        </div>
      </LabField>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        <LabField label="Audio">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 7 }}>
            {[['voice', 'voice'], ['bed', 'music bed'], ['native', 'native']].map(
              ([k, lbl]) => (
                <div key={k}>
                  <div className="muted" style={{ fontSize: 10, marginBottom: 3 }}>{lbl}</div>
                  <input value={audio[k] || ''} style={LAB_INPUT}
                         onChange={e => setIn('audio', k, e.target.value)}/>
                </div>
              ))}
          </div>
        </LabField>
        <LabField label="Budget" hint="the lab never spends this — it travels in the brief so the author knows the film's size">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 7 }}>
            {[['video_usd', 'video $'], ['image_usd', 'image $'],
              ['retakes_per_shot', 'retakes']].map(([k, lbl]) => (
              <div key={k}>
                <div className="muted" style={{ fontSize: 10, marginBottom: 3 }}>{lbl}</div>
                <input type="number" step="1" style={LAB_INPUT}
                       value={budget[k] === undefined ? '' : budget[k]}
                       onChange={e => setIn('budget', k, labNum(e.target.value, 0))}/>
              </div>
            ))}
          </div>
        </LabField>
      </div>

      <details open={advanced} onToggle={e => setAdvanced(e.target.open)}>
        <summary style={{ cursor: 'pointer', fontSize: 11.5, color: 'var(--text-3)' }}>
          advanced: the brief as JSON
        </summary>
        <div style={{ marginTop: 8 }}>
          <textarea rows={14} className="mono" value={rawText}
                    style={{ ...LAB_INPUT, fontSize: 11, lineHeight: 1.6 }}
                    onChange={e => setRawText(e.target.value)}/>
          <div className="muted" style={{ fontSize: 10.5, marginTop: 4 }}>
            Edit here and press «apply JSON» to overwrite the form above.
          </div>
        </div>
      </details>
    </div>
  );
};

const LabTreatmentCard = ({ t, idx, working, onUse, onEdit }) => {
  const [editing, setEditing] = useState(false);
  const beats = t.beats || [];
  const set = (k, v) => onEdit({ ...t, [k]: v });
  const setBeat = (i, k, v) => onEdit({ ...t,
    beats: beats.map((b, j) => (j === i ? { ...b, [k]: v } : b)) });
  const total = beats.reduce((n, b) => n + (Number(b.seconds) || 0), 0);
  return (
    <div className="card" style={{ marginBottom: 12,
          borderColor: working ? 'var(--accent)' : undefined,
          boxShadow: working ? 'var(--accent-glow)' : undefined }}>
      <div className="card-header" style={{ display: 'flex', gap: 10,
                                            alignItems: 'flex-start' }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="muted" style={{ fontSize: 10.5 }}>
            treatment {idx + 1} · {t.id}{working ? ' · working treatment' : ''}
          </div>
          {editing
            ? <input value={t.idea || ''} dir="auto" style={{ ...LAB_INPUT, marginTop: 5 }}
                     onChange={e => set('idea', e.target.value)}/>
            : <div className="card-title" dir="auto" style={{ fontSize: 14 }}>
                {t.idea || <span className="muted">— no idea line —</span>}
              </div>}
        </div>
        <button className="btn small ghost" onClick={() => setEditing(!editing)}>
          {editing ? 'done' : 'edit'}
        </button>
        <button className={`btn small${working ? '' : ' primary'}`} onClick={onUse}>
          {working ? 'in use' : 'use for the shot list'}
        </button>
      </div>
      <div className="card-body" style={{ display: 'flex', flexDirection: 'column',
                                          gap: 12 }}>
        {/* The three-idea shape (2026-09-27). Shown when the idea carries it
            (an older five-treatment run has none of these and skips them),
            and always editable in edit mode, like the hook and the line. */}
        {[['value', 'the value it makes easy to feel'],
          ['mirror', 'the mirror'],
          ['human_truth', 'the moment we all know — the human truth'],
          ['why_here', 'why she is here'],
          ['story', 'the story'],
          ['turn', 'the turn — what nobody sees coming'],
          ['last_image', 'the last picture — the frame the film ends on']].map(([k, label]) => (editing ? (
          <div key={k}>
            <LabLabel>{label}</LabLabel>
            <textarea rows={k === 'story' ? 3 : 2} dir="auto" value={t[k] || ''}
                      style={LAB_INPUT} onChange={e => set(k, e.target.value)}/>
          </div>
        ) : t[k] ? (
          <div key={k}>
            <LabLabel>{label}</LabLabel>
            <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6,
                                     color: 'var(--text-1)' }}>{t[k]}</div>
          </div>
        ) : null))}
        <div>
          <LabLabel>hook — the first 1.5 seconds</LabLabel>
          {editing
            ? <textarea rows={2} dir="auto" value={t.hook || ''} style={LAB_INPUT}
                        onChange={e => set('hook', e.target.value)}/>
            : <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6,
                                       color: 'var(--text-1)' }}>
                {t.hook || <span className="muted">—</span>}</div>}
        </div>

        <div>
          <LabLabel>beats — {beats.length}, {total || 0}s in total</LabLabel>
          {!beats.length && <LabEmpty what="no beats on this treatment"/>}
          <ol style={{ margin: 0, paddingLeft: 20, display: 'flex',
                       flexDirection: 'column', gap: 7 }}>
            {beats.map((b, i) => (
              <li key={i} style={{ fontSize: 12.5, lineHeight: 1.6,
                                   color: 'var(--text-1)' }}>
                {editing ? (
                  // `what_changes` is an older run's field: edited where the
                  // beat carries it, never offered on a three-idea beat.
                  <div style={{ display: 'grid', gap: 6,
                                gridTemplateColumns: b.what_changes !== undefined
                                  ? '1fr 130px 70px' : '1fr 70px' }}>
                    <input value={b.what_happens || ''} dir="auto" style={LAB_INPUT}
                           onChange={e => setBeat(i, 'what_happens', e.target.value)}/>
                    {b.what_changes !== undefined ? (
                      <input value={b.what_changes || ''} style={LAB_INPUT}
                             onChange={e => setBeat(i, 'what_changes', e.target.value)}/>
                    ) : null}
                    <input type="number" step="0.5" style={LAB_INPUT}
                           value={b.seconds === null || b.seconds === undefined ? '' : b.seconds}
                           onChange={e => setBeat(i, 'seconds', labNum(e.target.value, null))}/>
                  </div>
                ) : (
                  <span dir="auto">
                    {b.what_happens || <span className="muted">—</span>}
                    {b.what_changes ? <span className="chip muted"
                        style={{ marginLeft: 6 }}>changes: {b.what_changes}</span> : null}
                    {b.seconds !== null && b.seconds !== undefined
                      ? <span className="muted" style={{ marginLeft: 6, fontSize: 11 }}>
                          {b.seconds}s</span> : null}
                  </span>
                )}
              </li>
            ))}
          </ol>
        </div>

        <div>
          <LabLabel>the line she speaks</LabLabel>
          {editing
            ? <input value={t.line || ''} dir="auto" style={LAB_INPUT}
                     onChange={e => set('line', e.target.value)}/>
            : <div dir="auto" style={{ fontSize: 13, lineHeight: 1.6,
                                       color: 'var(--text-0)', fontStyle: 'italic' }}>
                {t.line ? `«${t.line}»` : <span className="muted">— she says nothing —</span>}
              </div>}
        </div>

        {t.button ? (
          <div>
            <LabLabel>the button — how it ends</LabLabel>
            <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6,
                                     color: 'var(--text-1)' }}>{t.button}</div>
          </div>
        ) : null}

        {/* «why it fits» is an older run's field; a three-idea card says it
            in `why_here` above and shows no empty box here. */}
        <div style={{ display: 'grid', gap: 14,
                      gridTemplateColumns: t.why_it_fits ? '1fr 1fr' : '1fr' }}>
          {t.why_it_fits ? (
            <div>
              <LabLabel>why it fits</LabLabel>
              <div dir="auto" style={{ fontSize: 12, lineHeight: 1.6,
                                       color: 'var(--text-1)' }}>{t.why_it_fits}</div>
            </div>
          ) : null}
          <div>
            <LabLabel>risk</LabLabel>
            <div dir="auto" style={{ fontSize: 12, lineHeight: 1.6,
                                     color: 'var(--warn)' }}>
              {t.risk || <span className="muted">— none named —</span>}</div>
          </div>
        </div>

        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          {t.setting ? <span className="chip">setting · {t.setting}</span> : null}
          {t.mechanism ? <span className="chip">mechanism · {t.mechanism}</span> : null}
        </div>
      </div>
    </div>
  );
};

// ── SCRIPT & SCENES ──────────────────────────────────────────────────────

const LabScriptBeats = ({ script }) => {
  const rows = (script && (script.beats || script.script || script)) || [];
  const list = Array.isArray(rows) ? rows : (rows.beats || []);
  if (!list.length) return <LabEmpty what="this run produced no beats"/>;
  return (
    <ol style={{ margin: 0, paddingLeft: 20, display: 'flex',
                 flexDirection: 'column', gap: 10 }}>
      {list.map((b, i) => (
        <li key={i} style={{ fontSize: 12.5, lineHeight: 1.65, color: 'var(--text-1)' }}>
          <div dir="auto">{b.what_happens || b.action || b.beat || b.text || ''}</div>
          {b.what_changes ? (
            <span className="chip muted" style={{ marginTop: 4 }}>
              changes: {b.what_changes}</span>
          ) : null}
          {(b.dialogue || b.line || b.vo) ? (
            <div dir="auto" style={{ marginTop: 5, fontSize: 12.5,
                                     fontStyle: 'italic', color: 'var(--text-0)' }}>
              «{b.dialogue || b.line || b.vo}»
              {b.language ? <span className="chip muted"
                                  style={{ marginLeft: 6 }}>{b.language}</span> : null}
            </div>
          ) : null}
          {b.seconds !== null && b.seconds !== undefined ? (
            <span className="muted" style={{ fontSize: 11 }}> {b.seconds}s</span>
          ) : null}
        </li>
      ))}
    </ol>
  );
};

const LAB_SHOT_COLS = [
  { k: 'idx',                label: '#',        w: 34,  type: 'ro' },
  { k: 'cut_length',         label: 'cut s',    w: 62,  type: 'num' },
  { k: 'rendered_duration',  label: 'render s', w: 68,  type: 'num' },
  { k: 'subject_action',     label: 'subject action',      w: 0, type: 'text' },
  { k: 'environment_motion', label: 'environment motion',  w: 0, type: 'text' },
  { k: 'camera.move',        label: 'camera move',         w: 0, type: 'text' },
  { k: 'camera.motivation',  label: 'why the camera moves', w: 0, type: 'text' },
  { k: 'audio.dialogue',     label: 'audio',               w: 0, type: 'text' },
  { k: 'handoff_state',      label: 'handoff',             w: 0, type: 'text' },
  { k: 'camera_fixed',       label: 'fixed',   w: 52,  type: 'bool' },
];

const labGet = (o, path) => path.split('.').reduce(
  (v, k) => (v && typeof v === 'object' ? v[k] : undefined), o);

const labSet = (o, path, v) => {
  const parts = path.split('.');
  if (parts.length === 1) return { ...o, [parts[0]]: v };
  return { ...o, [parts[0]]: { ...(o[parts[0]] || {}), [parts[1]]: v } };
};

const LabShotTable = ({ shots, onEdit, onOpenPrompts, readOnly }) => {
  if (!shots || !shots.length) return <LabEmpty what="no shots yet"/>;
  return (
    <div style={{ overflowX: 'auto' }}>
      <table className="table" style={{ minWidth: 1100 }}>
        <thead>
          <tr>
            {LAB_SHOT_COLS.map(c => <th key={c.k} style={c.w ? { width: c.w } : null}>
              {c.label}</th>)}
            <th style={{ width: 96 }}></th>
          </tr>
        </thead>
        <tbody>
          {shots.map((s, i) => (
            <tr key={i}>
              {LAB_SHOT_COLS.map(c => {
                const v = labGet(s, c.k);
                if (c.type === 'ro' || readOnly) {
                  return <td key={c.k} style={{ fontSize: 11.5, verticalAlign: 'top' }}
                             dir="auto">
                    {c.type === 'bool' ? (v ? 'yes' : 'no')
                      : (v === null || v === undefined || v === '' ? '—' : String(v))}
                  </td>;
                }
                if (c.type === 'bool') {
                  return <td key={c.k} style={{ verticalAlign: 'top' }}>
                    <input type="checkbox" checked={!!v}
                           onChange={e => onEdit(i, labSet(s, c.k, e.target.checked))}/>
                  </td>;
                }
                if (c.type === 'num') {
                  return <td key={c.k} style={{ verticalAlign: 'top' }}>
                    <input type="number" step="0.5"
                           style={{ ...LAB_INPUT, fontSize: 11.5, padding: '4px 6px' }}
                           value={v === null || v === undefined ? '' : v}
                           onChange={e => onEdit(i, labSet(s, c.k, labNum(e.target.value, 0)))}/>
                  </td>;
                }
                return <td key={c.k} style={{ verticalAlign: 'top', minWidth: 150 }}>
                  <textarea rows={2} dir="auto"
                            style={{ ...LAB_INPUT, fontSize: 11.5, padding: '4px 6px',
                                     resize: 'vertical' }}
                            value={v === null || v === undefined ? '' : String(v)}
                            onChange={e => onEdit(i, labSet(s, c.k, e.target.value))}/>
                </td>;
              })}
              <td style={{ verticalAlign: 'top' }}>
                <button className="btn small ghost"
                        onClick={() => onOpenPrompts(i)}>prompts →</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

// ── PROMPTS ──────────────────────────────────────────────────────────────

const LabPromptBox = ({ label, hint, text, onChange, tone }) => (
  <div>
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
      <div style={{ flex: 1 }}><LabLabel>{label}</LabLabel></div>
      <span className="muted" style={{ fontSize: 10.5 }}>
        {String(text || '').length} chars</span>
      <LabCopyButton text={text}/>
    </div>
    <textarea rows={9} dir="auto" value={text || ''}
              style={{ ...LAB_INPUT, fontSize: 12, lineHeight: 1.7,
                       borderColor: tone === 'ok' ? 'var(--ok)' : undefined }}
              onChange={e => onChange && onChange(e.target.value)}
              readOnly={!onChange}/>
    {hint ? <div className="muted" style={{ fontSize: 10.5, marginTop: 4 }}>{hint}</div> : null}
  </div>
);

// ── FILM (2026-09-27) ────────────────────────────────────────────────────

const labSpan = (a, b) =>
  `${labNum(a, 0).toFixed(1)}–${labNum(b, 0).toFixed(1)}s`;

// The Script Writer's whole film, compact: the moments as a table, then the
// lines word for word, then what must appear.
const LabFilmScript = ({ script }) => {
  const s = script || {};
  const moments = Array.isArray(s.moments) ? s.moments : [];
  const lines = Array.isArray(s.lines) ? s.lines : [];
  const must = s.must_appear || {};
  if (!moments.length && !lines.length) return <LabEmpty what="this run wrote no script"/>;
  return (
    <div>
      <div dir="auto" style={{ fontSize: 13.5, color: 'var(--text-0)', fontWeight: 600 }}>
        {s.title || '—'} <span className="muted" style={{ fontWeight: 400 }}>
          · {s.duration_s != null ? `${s.duration_s}s` : 'no length'}</span></div>
      {s.logline ? <div dir="auto" className="muted" style={{ fontSize: 12, marginTop: 3 }}>
        {s.logline}</div> : null}
      {s.mirror ? <div dir="auto" style={{ fontSize: 11.5, marginTop: 5 }}>
        mirror · {s.mirror}</div> : null}
      <div style={{ overflowX: 'auto', marginTop: 10 }}>
        <table className="table">
          <thead><tr><th style={{ width: 90 }}>when</th><th>action</th>
            <th>performance</th><th>sound</th></tr></thead>
          <tbody>
            {moments.map((m, i) => (
              <tr key={i}>
                <td className="mono" style={{ fontSize: 11 }}>{labSpan(m.from_s, m.to_s)}</td>
                <td dir="auto" style={{ fontSize: 11.5 }}>{m.action || '—'}</td>
                <td dir="auto" style={{ fontSize: 11.5 }}>{m.performance || '—'}</td>
                <td dir="auto" style={{ fontSize: 11.5 }}>{m.sound || '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <LabLabel>lines</LabLabel>
      {lines.map((l, i) => (
        <div key={i} dir="auto" style={{ fontSize: 12, marginBottom: 5 }}>
          <span className="mono muted" style={{ fontSize: 10.5 }}>
            {labNum(l.at_s, 0).toFixed(1)}s · {l.language || '?'} · </span>
          <b>{l.who || '—'}</b>: «{l.text || ''}»
          {l.delivery ? <span className="muted"> — {l.delivery}</span> : null}
        </div>
      ))}
      {s.line_note ? <LabNote tone="info">line_note · {s.line_note}</LabNote> : null}
      <div style={{ marginTop: 8 }}>
        <LabLabel>must appear</LabLabel>
        {['offer', 'her_line', 'mirror', 'look'].map(k => (
          <div key={k} dir="auto" style={{ fontSize: 11.5 }}>
            <span className="mono muted">{k}</span> · {must[k] || '—'}</div>
        ))}
      </div>
    </div>
  );
};

// The Director's plan, compact: the form and its reason, the references in
// order, the timeline, and the exact text the video model receives.
const LabFilmPlan = ({ run }) => {
  const out = (run && run.output) || {};
  const p = out.plan || {};
  if (!out.plan) return <LabEmpty what="this run wrote no plan"/>;
  const refs = Array.isArray(p.references) ? p.references : [];
  const res = Array.isArray(out.references_resolved) ? out.references_resolved : [];
  const tl = Array.isArray(p.timeline) ? p.timeline : [];
  return (
    <div>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
        <span className="pill">{p.form || 'no form'}</span>
        <span className="muted" style={{ fontSize: 11 }}>
          {p.duration_s != null ? `${p.duration_s}s` : 'no length'}</span>
      </div>
      <div dir="auto" style={{ fontSize: 12, marginTop: 6 }}>{p.form_reason || '—'}</div>
      <LabLabel>references, in @Image order</LabLabel>
      {refs.map((r, i) => {
        const hit = res.find(x => Number(x.image) === Number(r.image)) || {};
        return (
          <div key={i} dir="auto" style={{ fontSize: 11.5, marginBottom: 3 }}>
            <span className="mono">@Image {r.image}</span> ·{' '}
            {r.plate ? 'new location plate' : `ref #${r.ref_id}`} · {r.job || '—'}
            {hit.label ? <span className="muted"> · {hit.label}</span> : null}
            {r.plate && r.plate_prompt
              ? <div className="muted" style={{ fontSize: 11 }}>{r.plate_prompt}</div> : null}
          </div>
        );
      })}
      <LabLabel>timeline</LabLabel>
      {tl.map((t, i) => (
        <div key={i} dir="auto" style={{ fontSize: 11.5, marginBottom: 3 }}>
          <span className="mono muted">{labSpan(t.from_s, t.to_s)}</span> {t.what || ''}
        </div>
      ))}
      <div style={{ marginTop: 10 }}>
        <LabPromptBox label="sent_prompt — the exact text the video model receives"
                      text={out.sent_prompt || ''}/>
      </div>
    </div>
  );
};

// A PAID row — a draft or a final — shown and never pressed. No button on it
// reaches a route: buying, stopping and re-making happen on the simple view,
// where the price is read first (CLAUDE.md «a gated route is not a gated
// capability»: this surface carries no door at all).
const LabPaidRun = ({ run }) => {
  if (!run) return null;
  const o = run.output || {};
  const ck = o.checks || {};
  const sum = ck.summary || {};
  const cost = Number(o.cost_usd);
  return (
    <div>
      <LabRunHeader run={run} label={String(run.stage || '').replace('_', ' ')}/>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
        <span className="pill">{o.state || 'no state yet'}</span>
        <span className="muted" style={{ fontSize: 11 }}>
          job #{o.job_id != null ? o.job_id : '—'}
          {o.plate_job_id != null ? ` · plate job #${o.plate_job_id}` : ''}
          {o.task_id ? ` · task ${o.task_id}` : ''}
          {Number.isFinite(cost) && o.cost_usd != null
            ? ` · ${labMoney(cost)} on the job` : ' · cost on the job'}
          {o.expires_at ? ` · draft usable until ${String(o.expires_at).slice(0, 16).replace('T', ' ')}` : ''}
        </span>
      </div>
      {run.error ? <LabNote tone="err">{run.error}</LabNote> : null}
      {(o.asset || {}).path ? (
        <div className="mono muted" style={{ fontSize: 11, marginTop: 6 }}>
          asset #{o.asset.id != null ? o.asset.id : '—'} · {o.asset.path}
          {o.asset.duration_s != null ? ` · ${o.asset.duration_s}s` : ''}</div>
      ) : null}
      {o.checks ? (
        <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>
          checks · passed {JSON.stringify(sum.passed != null ? sum.passed : '—')}
          {' · '}failed {JSON.stringify(sum.failed != null ? sum.failed : '—')}
          {' · '}flagged {JSON.stringify(sum.flagged != null ? sum.flagged : '—')}
        </div>
      ) : null}
      <div className="muted" style={{ fontSize: 10.5, marginTop: 6 }}>
        read-only — a draft or a final is bought on the simple view, never here.
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════════════
// THE PAGE
// ══════════════════════════════════════════════════════════════════════════

function DirectorLabAdvanced() {
  const [tab, setTab] = useState('idea');
  const [clients, setClients] = useState([]);
  const [clientId, setClientId] = useState(null);
  const [character, setCharacter] = useState(null);

  const [briefSource, setBriefSource] = useState('default');
  const [filmId, setFilmId] = useState('29');
  const [brief, setBrief] = useState(LAB_DEFAULT_BRIEF);
  const [rawBrief, setRawBrief] = useState(
    JSON.stringify(LAB_DEFAULT_BRIEF, null, 2));
  const [advanced, setAdvanced] = useState(false);
  const [briefNote, setBriefNote] = useState('');

  const [runs, setRuns] = useState({});           // stage -> run
  const [treatments, setTreatments] = useState([]);
  const [workingId, setWorkingId] = useState('');
  const [shots, setShots] = useState([]);
  const [shotIdx, setShotIdx] = useState(0);
  const [kfPrompt, setKfPrompt] = useState('');
  const [vidPrompt, setVidPrompt] = useState('');

  const [rules, setRules] = useState({});         // stage -> budget
  const [price, setPrice] = useState(null);
  const [rendered, setRendered] = useState(null);
  const [history, setHistory] = useState([]);
  const [totalCost, setTotalCost] = useState(0);
  const [pick, setPick] = useState([]);
  const [cmp, setCmp] = useState(null);
  const [diff, setDiff] = useState({});           // stage -> diff text
  const [paidRun, setPaidRun] = useState(null);   // a draft / final, read-only

  const [busy, setBusy] = useState('');
  const [elapsed, setElapsed] = useState(0);
  const [err, setErr] = useState('');
  const [fb, setFb] = useState({});               // stage -> feedback draft

  const feedbackFor = (s) => fb[s] || { score: 3, missing: 'hook', note: '',
                                        as_rule: false, rule_text: '' };
  const setFeedbackFor = (s, v) => setFb(p => ({ ...p, [s]: v }));

  // ── loaders ────────────────────────────────────────────────────────────
  useEffect(() => {
    api.get('/clients').then(r => {
      const rows = r.clients || r.items || (Array.isArray(r) ? r : []);
      setClients(rows);
      // No client is preferred by name: the first row the account may see
      // is the default, and the picker is one click away.
      setClientId(prev => prev || (rows[0] ? rows[0].id : null));
    }).catch(e => setErr(e.message || String(e)));
  }, []);

  useEffect(() => {
    if (!clientId) return;
    api.get('/characters').then(r => {
      const rows = r.characters || r.items || (Array.isArray(r) ? r : []);
      const mine = rows.filter(c => c.client_id === clientId);
      setCharacter(mine[0] || null);
    }).catch(() => setCharacter(null));
  }, [clientId]);

  // A draft or a final shown read-only belongs to the client it was opened
  // for; picking another client takes it off the Film tab (§4).
  useEffect(() => { setPaidRun(null); }, [clientId]);

  const loadRules = useCallback((stage) => {
    api.get(`/lab/rules?stage=${stage}`)
      .then(r => setRules(p => ({ ...p, [stage]: r })))
      .catch(() => {});
  }, []);

  useEffect(() => { LAB_STAGES.forEach(s => loadRules(s.id)); }, [loadRules]);

  const loadHistory = useCallback(() => {
    const q = clientId ? `?client_id=${clientId}&limit=60` : '?limit=60';
    api.get(`/lab/runs${q}`).then(r => {
      setHistory(r.runs || []);
      setTotalCost(r.total_cost_usd || 0);
    }).catch(() => {});
  }, [clientId]);

  useEffect(() => { loadHistory(); }, [loadHistory]);

  useEffect(() => {
    api.get('/lab/price?stage=keyframe_prompt').then(setPrice).catch(() => {});
  }, []);

  useEffect(() => {
    if (!busy) return undefined;
    setElapsed(0);
    const t = setInterval(() => setElapsed(e => e + 1), 1000);
    return () => clearInterval(t);
  }, [busy]);

  // Keep the JSON mirror in step with the form, so «advanced» always shows
  // what would actually be posted.
  useEffect(() => { setRawBrief(JSON.stringify(brief, null, 2)); }, [brief]);

  // ── the working brief ──────────────────────────────────────────────────
  const applySource = async (src) => {
    setBriefSource(src);
    setErr(''); setBriefNote('');
    if (src === 'default') { setBrief(LAB_DEFAULT_BRIEF); return; }
    if (src === 'blank') { setBrief(LAB_BLANK_BRIEF); return; }
    const id = String(filmId || '').trim();
    if (!id) { setErr('type a film number first'); return; }
    setBusy('brief');
    try {
      const d = await api.get(`/shorts/${id}/desk`);
      const b = (d.brief && (d.brief.record || d.brief.draft)) || null;
      if (!b) { setErr(`film ${id} has no brief and no draft`); return; }
      setBrief({ ...LAB_BLANK_BRIEF, ...b });
      setBriefNote(d.brief.record
        ? `loaded the brief RECORD of film ${id} — what the owner approved`
        : `loaded the DRAFT brief of film ${id} — what the agency proposed, not approved`);
      if (d.film && d.film.client_id) setClientId(d.film.client_id);
    } catch (e) { setErr(e.message || String(e)); }
    finally { setBusy(''); }
  };

  const applyRawBrief = () => {
    try { setBrief(JSON.parse(rawBrief)); toast('the form now shows that JSON', 'ok'); }
    catch (e) { setErr(`that is not valid JSON: ${e.message}`); }
  };

  // ── running a stage ────────────────────────────────────────────────────
  const workingTreatment = treatments.find(t => t.id === workingId) || null;

  const runStage = async (stage, extra) => {
    if (!clientId) { setErr('pick a client first'); return null; }
    setBusy(stage); setErr('');
    try {
      const body = { stage, client_id: clientId, brief,
                     ...(extra || {}) };
      const r = await api.post('/lab/runs', body);
      const run = r.run;
      setRuns(p => ({ ...p, [stage]: run }));
      setDiff(p => ({ ...p, [stage]: '' }));
      setFeedbackFor(stage, { score: 3, missing: 'hook', note: '',
                              as_rule: false, rule_text: '' });
      const out = run.output || {};
      if (stage === 'treatment') {
        const five = out.treatments || [];
        setTreatments(five);
        if (five.length && !five.find(t => t.id === workingId)) setWorkingId(five[0].id);
      }
      if (stage === 'shot_list') {
        setShots(out.shots || []);
        setShotIdx(0);
      }
      if (stage === 'keyframe_prompt' || stage === 'video_prompt') {
        const sc = (out.scenes || [])[0] || {};
        if (stage === 'keyframe_prompt') {
          setKfPrompt(sc.keyframe_prompt || sc.prompt || '');
        } else {
          setVidPrompt(sc.video_prompt || sc.motion_prompt || sc.prompt || '');
        }
      }
      if (run.error) setErr(`${stage}: ${run.error}`);
      loadHistory();
      return run;
    } catch (e) { setErr(e.message || String(e)); return null; }
    finally { setBusy(''); }
  };

  const saveFeedback = async (stage) => {
    const run = runs[stage];
    if (!run) return;
    const f = feedbackFor(stage);
    setBusy(`fb-${stage}`); setErr('');
    try {
      const budget = rules[stage] || {};
      const prefix = budget.trigger_prefix || `${stage}:`;
      const r = await api.post(`/lab/runs/${run.id}/feedback`, {
        score: Number(f.score), missing: f.missing, note: f.note,
        as_rule: !!f.as_rule,
        rule_text: f.rule_text || f.note,
        trigger: `${prefix} ${f.missing}`,
      });
      loadRules(stage);
      if (r.rule) {
        toast(`rule #${r.rule.id} is active on ${stage}`, 'ai');
        try {
          const p = await api.get(`/lab/prompt-preview?run_id=${run.id}`);
          setDiff(d => ({ ...d, [stage]: p.diff || '(the prompt did not change)' }));
        } catch (e) { /* the rule landed; the preview is a nicety */ }
      } else {
        toast('feedback stored', 'ok');
      }
    } catch (e) { setErr(e.message || String(e)); }
    finally { setBusy(''); }
  };

  const retireRule = async (stage, id) => {
    try { await api.del(`/lab/rules/${id}`); loadRules(stage); toast('retired', 'ok'); }
    catch (e) { setErr(e.message || String(e)); }
  };

  const renderImage = async () => {
    const run = runs.keyframe_prompt;
    if (!run) return;
    const usd = price && price.usd ? Number(price.usd).toFixed(2) : '?';
    if (!window.confirm(
      `Render ONE image for $${usd} on ${(price && price.model) || 'the default model'}?\n\n` +
      `This is the only button on this page that spends money.`)) return;
    setBusy('image'); setErr('');
    try {
      const r = await api.post(`/lab/runs/${run.id}/render-image`, {});
      setRendered(r);
      toast(`asset #${r.asset_id} · ${labMoney(r.cost_usd)}`, 'ai');
      const fresh = await api.get(`/lab/runs/${run.id}`);
      setRuns(p => ({ ...p, keyframe_prompt: fresh.run }));
    } catch (e) { setErr(e.message || String(e)); }
    finally { setBusy(''); }
  };

  const doCompare = async () => {
    if (pick.length !== 2) return;
    setErr('');
    try { setCmp(await api.get(`/lab/compare?a=${pick[0]}&b=${pick[1]}`)); }
    catch (e) { setErr(e.message || String(e)); }
  };

  const openRun = async (id) => {
    try {
      const r = await api.get(`/lab/runs/${id}`);
      const run = r.run;
      // A paid row opens on the Film tab, read-only, and is never loaded into
      // a stage as if it could be run again from here.
      if (LAB_PAID_STAGES.includes(run.stage)) {
        setPaidRun(run);
        setTab('film');
        toast(`run #${run.id} (${run.stage.replace('_', ' ')}) shown read-only`, 'ok');
        return;
      }
      setRuns(p => ({ ...p, [run.stage]: run }));
      const out = run.output || {};
      if (run.stage === 'treatment') {
        setTreatments(out.treatments || []);
        if ((out.treatments || []).length) setWorkingId(out.treatments[0].id);
      }
      if (run.stage === 'shot_list') { setShots(out.shots || []); setShotIdx(0); }
      if (run.brief) setBrief({ ...LAB_BLANK_BRIEF, ...run.brief });
      const st = (LAB_STAGES.find(s => s.id === run.stage) || {}).tab || 'idea';
      setTab(st);
      toast(`run #${run.id} loaded into ${run.stage.replace('_', ' ')}`, 'ok');
    } catch (e) { setErr(e.message || String(e)); }
  };

  const language = character ? {
    name: character.name || 'the character',
    primary: character.language_primary || '',
    mix: character.language_mix || '',
    dialect: character.dialect_note || '',
  } : null;

  const runningLabel = (stage) =>
    busy === stage ? `running… ${elapsed}s` : null;

  const shot = shots[shotIdx] || null;
  const totalCut = shots.reduce((n, s) => n + (Number(s.cut_length) || 0), 0);
  const durTarget = Number(brief.duration_s) || 0;
  const lengthOff = durTarget ? Math.abs(totalCut - durTarget) > 2 : false;

  // ── render ─────────────────────────────────────────────────────────────
  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">Director’s lab</h1>
          <div className="page-sub">
            One brief, every stage, no film. Text runs spend subscription tokens;
            the only button that spends money is «Render image», and it says the
            price first.
          </div>
        </div>
      </div>

      {err ? <LabNote tone="err">{err}</LabNote> : null}

      {/* ── header strip: the workspace's one working brief ─────────────── */}
      <div className="card" style={{ marginTop: 14, marginBottom: 14 }}>
        <div className="card-body">
          <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap',
                        alignItems: 'flex-end' }}>
            <div style={{ minWidth: 190 }}>
              <LabLabel>client</LabLabel>
              <select className="input" style={LAB_INPUT} value={clientId || ''}
                      onChange={e => setClientId(Number(e.target.value))}>
                {clients.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
              </select>
            </div>
            <div>
              <LabLabel>working brief</LabLabel>
              <div className="segmented">
                <button className={briefSource === 'film' ? 'active' : ''}
                        onClick={() => applySource('film')}>from a film</button>
                <button className={briefSource === 'default' ? 'active' : ''}
                        onClick={() => applySource('default')}>sample default</button>
                <button className={briefSource === 'blank' ? 'active' : ''}
                        onClick={() => applySource('blank')}>blank</button>
              </div>
            </div>
            <div style={{ width: 120 }}>
              <LabLabel>film #</LabLabel>
              <div style={{ display: 'flex', gap: 5 }}>
                <input value={filmId} style={LAB_INPUT} placeholder="29"
                       onChange={e => setFilmId(e.target.value)}/>
                <button className="btn small" disabled={busy === 'brief'}
                        onClick={() => applySource('film')}>
                  {busy === 'brief' ? '…' : 'load'}</button>
              </div>
            </div>
            {character ? (
              <div className="muted" style={{ fontSize: 11, paddingBottom: 6 }}>
                character on file: <b style={{ color: 'var(--text-1)' }}>
                  {character.name}</b>
                {language && language.primary ? ` · speaks ${language.primary}` : ''}
              </div>
            ) : null}
          </div>
          {briefNote ? <LabNote tone="info">{briefNote}</LabNote> : null}

          {/* the stage map */}
          <div style={{ marginTop: 16 }}>
            <LabLabel>the lane, stage by stage</LabLabel>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap',
                          alignItems: 'stretch' }}>
              <div style={{ padding: '8px 11px', borderRadius: 7, minWidth: 130,
                            background: 'var(--bg-2)',
                            border: '1px solid var(--border-1)' }}>
                <div style={{ fontSize: 12, color: 'var(--text-1)' }}>Idea / brief</div>
                <div className="muted" style={{ fontSize: 10.5, marginTop: 2 }}>
                  yours — no agent</div>
              </div>
              {LAB_STAGES.map(s => {
                const b = rules[s.id] || {};
                const prefix = b.trigger_prefix || `${s.id}:`;
                const mine = b.other_stage_rules !== undefined
                  ? (b.active || []).length
                  : (b.active || []).filter(r => String(r.trigger || '')
                      .trim().toLowerCase().startsWith(prefix)).length;
                const done = !!runs[s.id];
                return (
                  <div key={s.id} onClick={() => setTab(s.tab)}
                       title={s.blurb}
                       style={{ padding: '8px 11px', borderRadius: 7, minWidth: 130,
                                cursor: 'pointer', background: 'var(--bg-2)',
                                border: `1px solid ${done ? 'var(--accent-line)'
                                                          : 'var(--border-1)'}` }}>
                    <div style={{ fontSize: 12, color: 'var(--text-1)' }}>
                      {s.label}{done ? ' ✓' : ''}</div>
                    <div className="muted" style={{ fontSize: 10.5, marginTop: 2 }}>
                      {b.reaches_prompt === false
                        ? 'reads no rules'
                        : `${mine} rule${mine === 1 ? '' : 's'} reach it`}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      </div>

      <div className="tabs">
        {LAB_TABS.map(t => (
          <div key={t.id} className={`tab${tab === t.id ? ' active' : ''}`}
               onClick={() => setTab(t.id)}>{t.label}</div>
        ))}
      </div>

      {/* ══ IDEA & STORY ══════════════════════════════════════════════ */}
      {tab === 'idea' && (
        <div>
          <div style={{ display: 'grid', gridTemplateColumns: '440px 1fr',
                        gap: 16, alignItems: 'start' }}>
            <div>
              <LabCard title="The brief"
                       hint="Every field is a question the lane refuses to guess."
                       right={advanced ? (
                         <button className="btn small" onClick={applyRawBrief}>
                           apply JSON</button>) : null}>
                <LabBriefForm brief={brief} setBrief={setBrief} language={language}
                              advanced={advanced} setAdvanced={setAdvanced}
                              rawText={rawBrief} setRawText={setRawBrief}/>
                <button className="btn primary"
                        style={{ marginTop: 16, width: '100%' }}
                        disabled={!!busy || !clientId}
                        onClick={() => runStage('treatment')}>
                  {runningLabel('treatment') || 'Run: 3 ideas'}
                </button>
                <div className="muted" style={{ fontSize: 10.5, marginTop: 6,
                                                textAlign: 'center' }}>
                  free — subscription tokens, no money
                </div>
              </LabCard>
            </div>

            <div>
              {!runs.treatment && (
                <LabCard title="Three ideas"
                         hint="Three ideas from one brief — each one carries one value from the client’s record.">
                  <LabEmpty what="nothing run yet — press «Run: 3 ideas»"/>
                </LabCard>
              )}
              {runs.treatment && (
                <div>
                  <LabCard title={`Ideas — ${treatments.length} came back`}
                           hint="Pick the one the shot list will be built from. You can edit any field before you do.">
                    <LabRunHeader run={runs.treatment} label="treatment"/>
                    <LabFindings findings={runs.treatment.findings}
                                 what="set of treatments"/>
                    {runs.treatment.error ? (
                      <LabNote tone="err">{runs.treatment.error}</LabNote>
                    ) : null}
                    <LabAgentView run={runs.treatment}/>
                  </LabCard>
                  {treatments.map((t, i) => (
                    <LabTreatmentCard key={t.id || i} t={t} idx={i}
                      working={t.id === workingId}
                      onUse={() => { setWorkingId(t.id);
                                     toast(`«${t.idea || t.id}» is the working treatment`, 'ok'); }}
                      onEdit={nt => setTreatments(
                        treatments.map((x, j) => (j === i ? nt : x)))}/>
                  ))}
                </div>
              )}
              <LabFeedback stage="treatment" run={runs.treatment}
                           fb={feedbackFor('treatment')}
                           setFb={v => setFeedbackFor('treatment', v)}
                           budget={rules.treatment}
                           busy={busy === 'fb-treatment'}
                           diff={diff.treatment}
                           onSave={() => saveFeedback('treatment')}
                           onRetire={id => retireRule('treatment', id)}/>
            </div>
          </div>
        </div>
      )}

      {/* ══ SCRIPT & SCENES ═══════════════════════════════════════════ */}
      {tab === 'script' && (
        <div>
          <LabCard title="Script — beats and the lines she speaks"
                   right={
                     <button className="btn primary" disabled={!!busy || !clientId}
                             onClick={() => runStage('script', {
                               treatment: workingTreatment })}>
                       {runningLabel('script') || 'Run: script beats'}
                     </button>}>
            {!runs.script && <LabEmpty what="not run yet"/>}
            {runs.script && (
              <div>
                <LabRunHeader run={runs.script} label="script"/>
                {runs.script.error ? <LabNote tone="err">{runs.script.error}</LabNote> : null}
                <LabScriptBeats script={(runs.script.output || {}).script}/>
                <LabFindings findings={runs.script.findings} what="script"/>
                <LabAgentView run={runs.script}/>
              </div>
            )}
          </LabCard>

          <LabCard title="Shot list"
                   hint={workingTreatment
                     ? `built from the working treatment: «${workingTreatment.idea || workingTreatment.id}»`
                     : 'no working treatment yet — pick one on the Idea & story tab, or the shot list is built from the brief alone.'}
                   right={
                     <div style={{ display: 'flex', gap: 8 }}>
                       <button className="btn primary" disabled={!!busy || !clientId}
                               onClick={() => runStage('shot_list', {
                                 treatment: workingTreatment })}>
                         {runningLabel('shot_list') || 'Run: shot list'}
                       </button>
                       {shots.length > 0 && (
                         <button className="btn ghost" disabled={!!busy}
                                 title="send the table below back as the treatment's beats and ask for a new shot list"
                                 onClick={() => runStage('shot_list', {
                                   treatment: workingTreatment
                                     ? { ...workingTreatment,
                                         beats: shots.map(s => ({
                                           what_happens: s.subject_action,
                                           what_changes: s.handoff_state,
                                           seconds: s.cut_length })) }
                                     : null,
                                   shot: shots[0] || null })}>
                           re-run with my edits
                         </button>
                       )}
                     </div>}>
            {!runs.shot_list && <LabEmpty what="not run yet"/>}
            {runs.shot_list && (
              <div>
                <LabRunHeader run={runs.shot_list} label="shot list"/>
                {runs.shot_list.error ? (
                  <LabNote tone="err">{runs.shot_list.error}</LabNote>) : null}
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap',
                              marginBottom: 10 }}>
                  <span className={`pill${lengthOff ? ' warn' : ' ok'}`}>
                    {shots.length} shots · {totalCut.toFixed(1)}s cut
                    {durTarget ? ` vs ${durTarget}s asked` : ''}
                  </span>
                  {lengthOff ? (
                    <span className="pill warn">more than 2 s off the brief</span>
                  ) : null}
                  <span className="pill">
                    {shots.filter(s => s.camera_fixed).length} locked-camera shots
                  </span>
                </div>
                <LabFindings findings={runs.shot_list.findings} what="shot list"/>
                <div style={{ marginTop: 12 }}>
                  <LabShotTable shots={shots}
                                onEdit={(i, s) => setShots(
                                  shots.map((x, j) => (j === i ? s : x)))}
                                onOpenPrompts={i => { setShotIdx(i); setTab('prompts'); }}/>
                </div>
                <LabAgentView run={runs.shot_list}/>
              </div>
            )}
          </LabCard>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <LabFeedback stage="script" run={runs.script} fb={feedbackFor('script')}
                         setFb={v => setFeedbackFor('script', v)}
                         budget={rules.script} busy={busy === 'fb-script'}
                         diff={diff.script}
                         onSave={() => saveFeedback('script')}
                         onRetire={id => retireRule('script', id)}/>
            <LabFeedback stage="shot_list" run={runs.shot_list}
                         fb={feedbackFor('shot_list')}
                         setFb={v => setFeedbackFor('shot_list', v)}
                         budget={rules.shot_list} busy={busy === 'fb-shot_list'}
                         diff={diff.shot_list}
                         onSave={() => saveFeedback('shot_list')}
                         onRetire={id => retireRule('shot_list', id)}/>
          </div>
        </div>
      )}

      {/* ══ PROMPTS ═══════════════════════════════════════════════════ */}
      {tab === 'prompts' && (
        <div>
          <LabCard title="The working shot"
                   hint="Both prompts below are written for this one shot. Everything else on the film stays where it is.">
            {!shots.length && (
              <LabEmpty what="no shot list yet — run one on the Script & scenes tab, or run a prompt straight off the brief"/>
            )}
            {shots.length > 0 && (
              <div>
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                  {shots.map((s, i) => (
                    <span key={i}
                          className={`chip clickable${i === shotIdx ? ' active' : ''}`}
                          onClick={() => setShotIdx(i)}>
                      shot {i + 1}{s.cut_length ? ` · ${s.cut_length}s` : ''}
                    </span>
                  ))}
                </div>
                {shot && (
                  <div style={{ marginTop: 12, display: 'grid',
                                gridTemplateColumns: '1fr 1fr', gap: 14 }}>
                    <div>
                      <LabLabel>what the subject does</LabLabel>
                      <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6 }}>
                        {shot.subject_action || '—'}</div>
                    </div>
                    <div>
                      <LabLabel>what the environment does</LabLabel>
                      <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6 }}>
                        {shot.environment_motion || '—'}</div>
                    </div>
                    <div>
                      <LabLabel>camera</LabLabel>
                      <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6 }}>
                        {(shot.camera || {}).move || '—'}
                        {(shot.camera || {}).motivation
                          ? ` — because ${(shot.camera || {}).motivation}` : ''}
                      </div>
                    </div>
                    <div>
                      <LabLabel>audio</LabLabel>
                      <div dir="auto" style={{ fontSize: 12.5, lineHeight: 1.6 }}>
                        {[(shot.audio || {}).dialogue, (shot.audio || {}).vo,
                          (shot.audio || {}).sfx, (shot.audio || {}).music_cue]
                          .filter(Boolean).join(' · ') || '—'}
                      </div>
                    </div>
                  </div>
                )}
              </div>
            )}
          </LabCard>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <LabCard title="Keyframe prompt — the still"
                     right={
                       <button className="btn primary" disabled={!!busy || !clientId}
                               onClick={() => runStage('keyframe_prompt', {
                                 treatment: workingTreatment, shot })}>
                         {runningLabel('keyframe_prompt') || 'Run: keyframe prompt'}
                       </button>}>
              {!runs.keyframe_prompt && <LabEmpty what="not run yet"/>}
              {runs.keyframe_prompt && (
                <div>
                  <LabRunHeader run={runs.keyframe_prompt} label="keyframe prompt"/>
                  {runs.keyframe_prompt.error ? (
                    <LabNote tone="err">{runs.keyframe_prompt.error}</LabNote>) : null}
                  <LabPromptBox label="the prompt the image model would receive"
                                text={kfPrompt} onChange={setKfPrompt}
                                hint="editable here; the copy button gives you the exact words."/>
                  <div style={{ marginTop: 12 }}>
                    <LabLabel>the canon clause — appended by code, not by the author</LabLabel>
                    <pre className="mono" dir="auto"
                         style={{ whiteSpace: 'pre-wrap', fontSize: 11, lineHeight: 1.6,
                                  background: 'var(--bg-2)', borderRadius: 6,
                                  border: '1px solid var(--border-1)',
                                  padding: '9px 11px', maxHeight: 160,
                                  overflow: 'auto' }}>
                      {((runs.keyframe_prompt.assembled || {}).blocks || {}).canon
                        || '— this run carried no canon clause —'}
                    </pre>
                  </div>
                  <div style={{ marginTop: 12 }}>
                    <LabLabel>the image refs that would ride with it</LabLabel>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                      {((runs.keyframe_prompt.assembled || {}).images || []).map((im, i) => (
                        <div key={i} style={{ width: 84 }}>
                          <img src={window.ASSET_URL(im.path)} alt={im.label || ''}
                               style={{ width: 84, height: 84, objectFit: 'cover',
                                        borderRadius: 6, background: 'var(--bg-2)',
                                        border: '1px solid var(--border-1)' }}/>
                          <div className="muted" style={{ fontSize: 10, marginTop: 3 }}>
                            #{im.ref_id}</div>
                        </div>
                      ))}
                      {!((runs.keyframe_prompt.assembled || {}).images || []).length && (
                        <div className="muted" style={{ fontSize: 11 }}>
                          no reference images on this run</div>)}
                    </div>
                  </div>
                  <div style={{ marginTop: 14, borderTop: '1px solid var(--border-1)',
                                paddingTop: 12 }}>
                    <button className="btn primary" disabled={!!busy}
                            onClick={renderImage}>
                      {busy === 'image'
                        ? `rendering… ${elapsed}s`
                        : `Render image — ${price && price.usd
                            ? labMoney(price.usd) : '…'}`}
                    </button>
                    <div className="muted" style={{ fontSize: 10.5, marginTop: 5 }}>
                      {price && price.model
                        ? `${price.model} · one image · this is the only button here that spends money`
                        : 'fetching the price…'}
                    </div>
                    {runs.keyframe_prompt.image_asset_id ? (
                      <div style={{ marginTop: 10 }}>
                        {rendered && rendered.path ? (
                          <img src={window.ASSET_URL(rendered.path)}
                               alt="the rendered frame"
                               style={{ maxWidth: '100%', borderRadius: 8,
                                        border: '1px solid var(--border-1)' }}/>
                        ) : null}
                        <div className="muted" style={{ fontSize: 10.5, marginTop: 4 }}>
                          asset #{runs.keyframe_prompt.image_asset_id}
                          {rendered && rendered.cost_usd !== undefined
                            ? ` · this frame cost ${labMoney(rendered.cost_usd)}` : ''}
                        </div>
                      </div>
                    ) : null}
                  </div>
                  <LabAgentView run={runs.keyframe_prompt}/>
                </div>
              )}
            </LabCard>

            <LabCard title="Video prompt — the movement"
                     right={
                       <button className="btn primary" disabled={!!busy || !clientId}
                               onClick={() => runStage('video_prompt', {
                                 treatment: workingTreatment, shot })}>
                         {runningLabel('video_prompt') || 'Run: video prompt'}
                       </button>}>
              {!runs.video_prompt && <LabEmpty what="not run yet"/>}
              {runs.video_prompt && (
                <div>
                  <LabRunHeader run={runs.video_prompt} label="video prompt"/>
                  {runs.video_prompt.error ? (
                    <LabNote tone="err">{runs.video_prompt.error}</LabNote>) : null}
                  <LabPromptBox label="the prompt the video model would receive"
                                text={vidPrompt} onChange={setVidPrompt}/>
                  <div style={{ marginTop: 14 }}>
                    <LabLabel>what the video model will actually receive</LabLabel>
                    <div style={{ background: 'var(--bg-2)', borderRadius: 7,
                                  border: '1px solid var(--border-1)',
                                  padding: '10px 12px', fontSize: 11.5,
                                  lineHeight: 1.7 }} dir="auto">
                      <div style={{ color: 'var(--text-1)' }}>{vidPrompt || '—'}</div>
                      {((runs.video_prompt.assembled || {}).blocks || {}).canon ? (
                        <div style={{ marginTop: 8, paddingTop: 8,
                                      borderTop: '1px dashed var(--border-2)',
                                      color: 'var(--text-2)' }}>
                          + canon clause:{' '}
                          {((runs.video_prompt.assembled || {}).blocks || {}).canon}
                        </div>
                      ) : null}
                      <div style={{ marginTop: 8, paddingTop: 8,
                                    borderTop: '1px dashed var(--border-2)',
                                    color: 'var(--text-3)' }}>
                        + {((runs.video_prompt.assembled || {}).images || []).length}{' '}
                        reference image(s)
                        {shot && shot.camera_fixed
                          ? ' · the shot asks for a locked camera' : ''}
                      </div>
                    </div>
                  </div>
                  <LabAgentView run={runs.video_prompt}/>
                </div>
              )}
            </LabCard>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <LabFeedback stage="keyframe_prompt" run={runs.keyframe_prompt}
                         fb={feedbackFor('keyframe_prompt')}
                         setFb={v => setFeedbackFor('keyframe_prompt', v)}
                         budget={rules.keyframe_prompt}
                         busy={busy === 'fb-keyframe_prompt'}
                         diff={diff.keyframe_prompt}
                         onSave={() => saveFeedback('keyframe_prompt')}
                         onRetire={id => retireRule('keyframe_prompt', id)}/>
            <LabFeedback stage="video_prompt" run={runs.video_prompt}
                         fb={feedbackFor('video_prompt')}
                         setFb={v => setFeedbackFor('video_prompt', v)}
                         budget={rules.video_prompt}
                         busy={busy === 'fb-video_prompt'}
                         diff={diff.video_prompt}
                         onSave={() => saveFeedback('video_prompt')}
                         onRetire={id => retireRule('video_prompt', id)}/>
          </div>
        </div>
      )}

      {/* ══ FILM (2026-09-27) ═════════════════════════════════════════ */}
      {tab === 'film' && (
        <div>
          <LabCard title="Film script — the Script Writer's whole film"
                   hint={workingTreatment
                     ? `written from the working treatment: «${workingTreatment.idea || workingTreatment.id}»`
                     : 'no working treatment yet — pick one on the Idea & story tab first.'}
                   right={
                     <button className="btn primary"
                             disabled={!!busy || !clientId || !workingTreatment}
                             onClick={() => runStage('film_script', {
                               treatment: workingTreatment,
                               ideas_run_id: runs.treatment ? runs.treatment.id : null })}>
                       {runningLabel('film_script') || 'Run: film script'}
                     </button>}>
            {!runs.film_script && <LabEmpty what="not run yet"/>}
            {runs.film_script && (
              <div>
                <LabRunHeader run={runs.film_script} label="film script"/>
                {runs.film_script.error ? (
                  <LabNote tone="err">{runs.film_script.error}</LabNote>) : null}
                <LabFilmScript script={(runs.film_script.output || {}).script}/>
                <LabFindings findings={runs.film_script.findings} what="film script"/>
                <LabAgentView run={runs.film_script}/>
              </div>
            )}
          </LabCard>

          <LabCard title="Film plan — the Director's one generation"
                   hint="The server reads the script by its id (script_run_id); no script text is sent from this page."
                   right={
                     <button className="btn primary"
                             disabled={!!busy || !clientId || !runs.film_script}
                             onClick={() => runStage('film_plan', {
                               script_run_id: runs.film_script ? runs.film_script.id : null })}>
                       {runningLabel('film_plan') || 'Run: film plan'}
                     </button>}>
            {!runs.film_plan && <LabEmpty what="not run yet"/>}
            {runs.film_plan && (
              <div>
                <LabRunHeader run={runs.film_plan} label="film plan"/>
                {runs.film_plan.error ? (
                  <LabNote tone="err">{runs.film_plan.error}</LabNote>) : null}
                <LabFilmPlan run={runs.film_plan}/>
                <LabFindings findings={runs.film_plan.findings} what="film plan"/>
                <LabAgentView run={runs.film_plan}/>
              </div>
            )}
          </LabCard>

          <LabCard title="Drafts and finals — read-only"
                   hint="Bought on the simple view only, where the price is read first. Open one from the list to see its state, its job and its checks.">
            {history.filter(h => LAB_PAID_STAGES.includes(h.stage)).map(h => (
              <div key={h.id} style={{ display: 'flex', gap: 10, alignItems: 'center',
                                       padding: '6px 0',
                                       borderTop: '1px solid var(--border-1)' }}>
                <span className="mono" style={{ fontSize: 11 }}>#{h.id}</span>
                <span style={{ fontSize: 12 }}>{String(h.stage).replace('_', ' ')}</span>
                <span className="muted" style={{ fontSize: 11, flex: 1 }}>
                  {(h.created_at || '').slice(0, 16).replace('T', ' ')}</span>
                <button className="btn small ghost" onClick={() => openRun(h.id)}>show</button>
              </div>
            ))}
            {!history.some(h => LAB_PAID_STAGES.includes(h.stage)) && (
              <LabEmpty what="no draft or final on this bench yet"/>)}
            {paidRun && Number(paidRun.client_id) === Number(clientId) ? (
              <div style={{ marginTop: 12 }}><LabPaidRun run={paidRun}/></div>
            ) : null}
          </LabCard>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
            <LabFeedback stage="film_script" run={runs.film_script}
                         fb={feedbackFor('film_script')}
                         setFb={v => setFeedbackFor('film_script', v)}
                         budget={rules.film_script} busy={busy === 'fb-film_script'}
                         diff={diff.film_script}
                         onSave={() => saveFeedback('film_script')}
                         onRetire={id => retireRule('film_script', id)}/>
            <LabFeedback stage="film_plan" run={runs.film_plan}
                         fb={feedbackFor('film_plan')}
                         setFb={v => setFeedbackFor('film_plan', v)}
                         budget={rules.film_plan} busy={busy === 'fb-film_plan'}
                         diff={diff.film_plan}
                         onSave={() => saveFeedback('film_plan')}
                         onRetire={id => retireRule('film_plan', id)}/>
          </div>
        </div>
      )}

      {/* ══ RUNS & RULES ══════════════════════════════════════════════ */}
      {tab === 'runs' && (
        <div>
          <LabCard title="Every run on this bench"
                   hint="Tick two and compare them — their outputs side by side and the diff of the prompts underneath."
                   right={
                     <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                       <span className="muted" style={{ fontSize: 11 }}>
                         {history.length} runs · {labMoney(totalCost)} in total</span>
                       <button className="btn small" disabled={pick.length !== 2}
                               onClick={doCompare}>compare the two ticked</button>
                     </div>}>
            <div style={{ overflowX: 'auto' }}>
              <table className="table">
                <thead>
                  <tr>
                    <th style={{ width: 34 }}></th>
                    <th style={{ width: 60 }}>run</th>
                    <th>when</th><th>stage</th><th>client</th><th>model</th>
                    <th>cost</th><th>took</th><th>score</th><th></th>
                  </tr>
                </thead>
                <tbody>
                  {history.map(h => {
                    const c = clients.find(x => x.id === h.client_id);
                    // A paid row's dollars are on its job; this column would
                    // say nothing was spent about money that was. It is
                    // read-only here, and has no prompt to compare.
                    const paid = LAB_PAID_STAGES.includes(h.stage);
                    return (
                      <tr key={h.id}>
                        <td><input type="checkbox" checked={pick.includes(h.id)}
                                   disabled={paid}
                                   onChange={() => setPick(p => p.includes(h.id)
                                     ? p.filter(x => x !== h.id)
                                     : [...p, h.id].slice(-2))}/></td>
                        <td>#{h.id}</td>
                        <td className="muted" style={{ fontSize: 11 }}>
                          {(h.created_at || '').slice(0, 16).replace('T', ' ')}</td>
                        <td>{String(h.stage || '').replace('_', ' ')}</td>
                        <td>{c ? c.name : `client ${h.client_id}`}</td>
                        <td className="muted" style={{ fontSize: 11 }}>{h.model}</td>
                        <td>{paid ? <span className="muted" style={{ fontSize: 11 }}>
                          on the job</span> : labMoney(h.cost_usd)}</td>
                        <td>{labSecs(h.duration_ms)}</td>
                        <td>{(h.feedback && h.feedback.score)
                          ? `${'★'.repeat(h.feedback.score)}` : '—'}</td>
                        <td><button className="btn small ghost"
                                    onClick={() => openRun(h.id)}>open</button></td>
                      </tr>
                    );
                  })}
                  {!history.length && (
                    <tr><td colSpan={10} className="muted">no runs yet</td></tr>)}
                </tbody>
              </table>
            </div>
          </LabCard>

          {cmp && (
            <LabCard title={`Run #${cmp.a.id} vs run #${cmp.b.id}`}
                     right={<button className="btn small ghost"
                                    onClick={() => setCmp(null)}>close</button>}>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr',
                            gap: 16 }}>
                {[cmp.a, cmp.b].map((r, i) => (
                  <div key={i}>
                    <LabRunHeader run={r} label={r.stage}/>
                    {r.stage === 'treatment' &&
                      ((r.output || {}).treatments || []).map((t, j) => (
                        <div key={j} style={{ marginBottom: 8, padding: '8px 10px',
                              background: 'var(--bg-2)', borderRadius: 6 }}>
                          <div dir="auto" style={{ fontSize: 12.5,
                                color: 'var(--text-0)' }}>{t.idea}</div>
                          <div dir="auto" className="muted"
                               style={{ fontSize: 11, marginTop: 3 }}>{t.hook}</div>
                        </div>
                      ))}
                    {r.stage === 'shot_list' && (
                      <LabShotTable shots={(r.output || {}).shots || []} readOnly
                                    onEdit={() => {}} onOpenPrompts={() => {}}/>
                    )}
                    {r.stage === 'script' && (
                      <LabScriptBeats script={(r.output || {}).script}/>)}
                    {(r.stage === 'keyframe_prompt' || r.stage === 'video_prompt') && (
                      ((r.output || {}).scenes || []).map((s, j) => (
                        <LabPromptBox key={j} label={`scene ${j + 1}`}
                          text={s.keyframe_prompt || s.video_prompt || s.prompt || ''}/>
                      )))}
                    <LabFindings findings={r.findings} what={r.stage}/>
                  </div>
                ))}
              </div>
              <div style={{ marginTop: 14 }}>
                <LabLabel>what differs in the prompt they received</LabLabel>
                <pre className="mono" style={{ maxHeight: 340, overflow: 'auto',
                       whiteSpace: 'pre-wrap', fontSize: 11, lineHeight: 1.6,
                       background: 'var(--bg-2)', border: '1px solid var(--border-1)',
                       borderRadius: 6, padding: '10px 12px' }}>
                  {cmp.diff || '(the two prompts are identical)'}</pre>
              </div>
            </LabCard>
          )}

          <LabCard title="Your rules, stage by stage"
                   hint="A rule belongs to ONE stage. The budget is what stops a stage from reading a diary.">
            <div style={{ display: 'grid',
                          gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
                          gap: 18 }}>
              {LAB_STAGES.map(s => (
                <div key={s.id}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8,
                                marginBottom: 6 }}>
                    <div style={{ flex: 1, fontSize: 12.5, color: 'var(--text-0)' }}>
                      {s.label}</div>
                    <button className="btn small ghost"
                            onClick={() => setTab(s.tab)}>go to stage →</button>
                  </div>
                  <LabRulesPanel stage={s.id} budget={rules[s.id]}
                                 onRetire={id => retireRule(s.id, id)}/>
                </div>
              ))}
            </div>
          </LabCard>
        </div>
      )}
    </div>
  );
}
