// The Legal admin's library, ladders, governance and retention — WP-U13, whole.
//
// THE ACTING HALVES ARE BUILT (D-5, 2026-08-02). Every act defers to the
// database's authority and renders its refusals verbatim; no permission is
// decided in this file.
//
// THE ONE INVARIANT EVERY ACT HERE OBEYS: there is no in-place edit of
// approved wording, anywhere. Retiring withdraws; superseding MINTS A NEW
// VERSION with its history intact (cw.supersede_clause); replacing a ladder
// retires the old one and publishes a successor, because past concessions are
// recorded as "we went to rung 2" and reordering in place would rewrite what
// they meant. An edit affordance would be the mutation-surface invariant
// broken in the UI rather than in the schema.
//
// FRICTION BELONGS WHERE THE IRREVERSIBILITY IS (the reviewer desk's rule).
// Retiring and superseding confirm with a preview of exactly what will be
// written. Filters and drawers do not.
//
// THE COVERAGE-GAP RULE, from the common anti-pattern. A gap is surfaced, never
// framed as a system failure. The system's job ends at making the gap visible
// and giving the responsible person a place to act; the gap itself belongs to
// the people who own the library. The copy below is written to that line and
// should stay on it.

const { useState } = React;

// The database's sentence, rendered verbatim. Every act form uses this rather
// than rewording a refusal into something friendlier and less true.
function ActError({ error }) {
  if (!error) return null;
  return (
    <div className="panel p-3 mt-3" style={{ borderColor: 'var(--err, #b91c1c)' }}
         data-testid="act-error">
      <div className="section-label">refused</div>
      <div className="caption mt-1">{error}</div>
    </div>
  );
}

// ── Retiring wording: withdrawal, with a reason, confirmed ────────────────
function RetireForm({ clause, onDone }) {
  const [reason, setReason] = useState('');
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  return (
    <div className="panel p-3 mt-2" data-testid="retire-form">
      <div className="section-label">retire {clause.clause_id}@v{clause.version}</div>
      {clause.is_a_floor && (
        // WARNED, NOT GATED (the house rule). Retiring a floor turns an intact
        // ladder into floor_unusable; the person acting is told before the act,
        // and the ladder board will say so after it.
        <div className="caption mt-1" data-testid="floor-warning">
          <strong>This version is holding up a ladder as its floor.</strong>{' '}
          Retiring it leaves that ladder unusable at its stopping point until a
          replacement is published.
        </div>
      )}
      {!confirming ? (
        <div className="mt-2">
          <label className="section-label">Why it is withdrawn</label>
          <input aria-label="Why it is withdrawn" className="mt-1.5 w-full" value={reason} placeholder="the reason on the record"
                 onChange={(e) => setReason(e.target.value)} data-testid="retire-reason" />
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => onDone(false)}>cancel</button>
            <button className="btn" disabled={!reason.trim()}
                    data-testid="review-retire"
                    onClick={() => setConfirming(true)}>
              review what will be withdrawn…
            </button>
          </div>
        </div>
      ) : (
        <div className="mt-2">
          <div className="caption" style={{ whiteSpace: 'pre-wrap' }}>
            {clause.clause_id}@v{clause.version} — “{clause.title}” — stops being
            selectable, permanently. Its wording and history stay readable, and
            every contract already carrying it is untouched. Reason: {reason.trim()}
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setConfirming(false)}>← back</button>
            <ActButton className="btn btn-primary" disabled={busy}
                    data-testid="confirm-retire"
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const r = await API.retireClause({
                        clause_id: clause.clause_id, version: clause.version,
                        reason: reason.trim(),
                      });
                      setBusy(false);
                      if (!r.ok) { setError(r.reason); setConfirming(false); return; }
                      onDone(true);
                    }}>
              {busy ? 'retiring…' : '✓ retire this wording'}
            </ActButton>
          </div>
        </div>
      )}
      <ActError error={error} />
    </div>
  );
}

// ── Superseding: a NEW version is minted; nothing is rewritten ────────────
function SupersedeForm({ clause, onDone }) {
  const [title, setTitle] = useState(clause.title || '');
  const [body, setBody] = useState('');
  const [rationale, setRationale] = useState('');
  const [reason, setReason] = useState('');
  const [expires, setExpires] = useState('');
  const [disposition, setDisposition] = useState('run_off');
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const complete = title.trim() && body.trim() && rationale.trim() && reason.trim();

  return (
    <div className="panel p-3 mt-2" data-testid="supersede-form">
      <div className="section-label">supersede {clause.clause_id}@v{clause.version}</div>
      {!confirming ? (
        <div className="mt-2">
          <label className="section-label">Successor title</label>
          <input aria-label="Successor title" className="mt-1.5 w-full" value={title}
                 onChange={(e) => setTitle(e.target.value)} data-testid="supersede-title" />
          <label className="section-label mt-3">The successor wording</label>
          <textarea aria-label="The successor wording" className="mt-1.5 w-full font-mono" rows={5} value={body}
                    placeholder="the wording that replaces it, in full"
                    onChange={(e) => setBody(e.target.value)} data-testid="supersede-body" />
          <label className="section-label mt-3">Why this wording</label>
          <input aria-label="Why this wording" className="mt-1.5 w-full" value={rationale}
                 placeholder="shown to reviewers with the version"
                 onChange={(e) => setRationale(e.target.value)} />
          <label className="section-label mt-3">Why the old one is replaced</label>
          <input aria-label="Why the old one is replaced" className="mt-1.5 w-full" value={reason}
                 placeholder="the supersession reason on the record"
                 onChange={(e) => setReason(e.target.value)} />
          <div className="flex gap-3 mt-3 items-end">
            <div>
              <label className="section-label">Expires (optional)</label>
              <input aria-label="Expires (optional)" className="mt-1.5 font-mono" type="date" value={expires}
                     onChange={(e) => setExpires(e.target.value)} />
            </div>
            <div>
              <label className="section-label">The predecessor</label>
              <select aria-label="The predecessor" className="mt-1.5 font-mono" value={disposition}
                      onChange={(e) => setDisposition(e.target.value)}>
                <option value="run_off">runs off — existing deals keep it</option>
                <option value="retire_now">retire now</option>
              </select>
            </div>
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => onDone(false)}>cancel</button>
            <button className="btn" disabled={!complete}
                    data-testid="review-supersede"
                    onClick={() => setConfirming(true)}>
              review what will be minted…
            </button>
          </div>
        </div>
      ) : (
        <div className="mt-2">
          <div className="caption" style={{ whiteSpace: 'pre-wrap' }}>
            A new version of {clause.clause_id} will be minted — “{title.trim()}”
            — and v{clause.version} recorded as superseded by it. Nothing already
            signed or in flight is rewritten; deals carrying the old wording are
            flagged on the drift report instead.
          </div>
          <div className="caption mt-2 font-mono" style={{ whiteSpace: 'pre-wrap' }}>
            {body.trim()}
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setConfirming(false)}>← back</button>
            <ActButton className="btn btn-primary" disabled={busy}
                    data-testid="confirm-supersede"
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const r = await API.supersedeClause({
                        clause_id: clause.clause_id, version: clause.version,
                        title: title.trim(), body: body.trim(),
                        rationale: rationale.trim(), reason: reason.trim(),
                        expires_on: expires || null, disposition,
                      });
                      setBusy(false);
                      if (!r.ok) { setError(r.reason); setConfirming(false); return; }
                      onDone(true);
                    }}>
              {busy ? 'minting…' : '✓ mint the successor'}
            </ActButton>
          </div>
        </div>
      )}
      <ActError error={error} />
    </div>
  );
}

// ── Authoring a move: strategy prose enters the playbook (0081) ───────────
// A move is doctrine, never contract language. The ONE wording it may propose
// is a fallback REFERENCE to an approved, selectable version — picked below
// from the library already on this screen, never typed into a free field. A
// version input a person could type into would be the smuggling door 0081
// built this table not to have.
function AuthorMoveForm({ clause, versions, onDone }) {
  // The decided vocabulary, 0081's CHECK. Widening it is a migration on
  // purpose, so this list changes when the schema does and not before.
  const kinds = ['reframe', 'trade', 'defer', 'split', 'escalate', 'fallback_reference'];
  const [form, setForm] = useState({
    kind: 'reframe', seq: '', title: '', guidance: '', fallback: '',
  });
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  const selectable = versions.filter((v) => v.selectable);
  const isFallback = form.kind === 'fallback_reference';
  const complete = form.title.trim() && form.guidance.trim()
    && form.seq.trim() !== '' && (!isFallback || form.fallback);

  const fallbackParts = () => {
    if (!isFallback || !form.fallback) {
      return { fallback_clause_id: null, fallback_version: null };
    }
    const at = form.fallback.lastIndexOf('@');
    return {
      fallback_clause_id: form.fallback.slice(0, at),
      fallback_version: Number(form.fallback.slice(at + 1)),
    };
  };

  return (
    <div className="panel p-3 mt-2" data-testid="move-form">
      <div className="section-label">author a move on {clause.clause_id}</div>
      {!confirming ? (
        <div className="mt-2">
          <div className="flex gap-3">
            <div>
              <label className="section-label">Kind</label>
              <select aria-label="Kind" className="mt-1.5 font-mono" value={form.kind}
                      data-testid="move-kind"
                      onChange={(e) => setForm({ ...form, kind: e.target.value })}>
                {kinds.map((k) => <option key={k} value={k}>{k}</option>)}
              </select>
            </div>
            <div>
              <label className="section-label">Seq</label>
              <input aria-label="Seq" className="mt-1.5 font-mono" style={{ width: 70 }}
                     type="number" min="0" value={form.seq} data-testid="move-seq"
                     onChange={(e) => setForm({ ...form, seq: e.target.value })} />
            </div>
            <div className="grow">
              <label className="section-label">Title</label>
              <input aria-label="Title" className="mt-1.5 w-full" value={form.title}
                     onChange={(e) => setForm({ ...form, title: e.target.value })} />
            </div>
          </div>
          <label className="section-label mt-3">Guidance — how to argue it</label>
          <textarea aria-label="Guidance — how to argue it" className="mt-1.5 w-full" rows={3} value={form.guidance}
                    placeholder="strategy prose: what to offer, when to walk it up — never contract language"
                    onChange={(e) => setForm({ ...form, guidance: e.target.value })}
                    data-testid="move-guidance" />
          {isFallback && (
            <div className="mt-3">
              <label className="section-label">Falls back to — an approved version</label>
              <select aria-label="Falls back to — an approved version" className="mt-1.5 w-full font-mono" value={form.fallback}
                      data-testid="move-fallback-pick"
                      onChange={(e) => setForm({ ...form, fallback: e.target.value })}>
                <option value="">choose the approved wording…</option>
                {selectable.map((v) => (
                  <option key={`${v.clause_id}@${v.version}`}
                          value={`${v.clause_id}@${v.version}`}>
                    {v.clause_id}@v{v.version} — {v.title} ({v.category_label} · {v.severity})
                  </option>
                ))}
              </select>
            </div>
          )}
          {/* NO RISK-ESTIMATE FIELD, deliberately. U14d's estimate is a
              model's number with its model recorded; no estimator is wired
              yet, and both columns staying null IS the honest record — an
              estimate nobody computed is not zero, and not a person's guess
              typed into a box either. */}
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => onDone(false)}>cancel</button>
            <button className="btn" disabled={!complete}
                    data-testid="review-move"
                    onClick={() => setConfirming(true)}>
              review what will be recorded…
            </button>
          </div>
        </div>
      ) : (
        <div className="mt-2">
          <div className="caption" style={{ whiteSpace: 'pre-wrap' }}>
            Move {form.seq} on {clause.clause_id} — {form.kind} — “{form.title.trim()}”.
            {' '}{form.guidance.trim()}
            {isFallback ? `\nFalls back to ${form.fallback.replace('@', '@v')} — a reference to approved wording, nothing else.` : ''}
            {'\nRecorded under your name from the connection, permanently: a move is never edited, only retired. No risk estimate is recorded — none was computed, and absence is written down as absence.'}
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setConfirming(false)}>← back</button>
            <ActButton className="btn btn-primary" disabled={busy}
                    data-testid="confirm-move"
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const r = await API.authorMove({
                        clause_id: clause.clause_id, kind: form.kind,
                        seq: Number(form.seq), title: form.title.trim(),
                        guidance: form.guidance.trim(),
                        ...fallbackParts(),
                      });
                      setBusy(false);
                      if (!r.ok) { setError(r.reason); setConfirming(false); return; }
                      onDone(true);
                    }}>
              {busy ? 'recording…' : '✓ record this move'}
            </ActButton>
          </div>
        </div>
      )}
      <ActError error={error} />
    </div>
  );
}

// ── The playbook per clause: the moves it holds in reserve (0081) ─────────
// ADVISORY THROUGHOUT, like the migration says: a move gates nothing, moves
// no position, and no resolution consults it. What renders here is what
// cw.move_board returned — this component only narrows to the open clause,
// which is narrowing what the rule ALREADY returned, never asking wider.
function MovesSection({ clause, moves, versions, onChanged }) {
  const [authoring, setAuthoring] = useState(false);
  const [retiring, setRetiring] = useState(null); // move_id
  const [reason, setReason] = useState('');
  const [history, setHistory] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  const body = () => {
    const mine = moves.rows.filter((m) => m.clause_id === clause.clause_id);
    // The board orders itself — clause, retired last, seq — and nothing here
    // re-sorts, for the ladder board's reason.
    const live = mine.filter((m) => m.live);
    const retired = mine.filter((m) => !m.live);
    return (
      <div className="mt-1">
        {live.length === 0 && (
          <div className="caption">
            No live move is authored for this clause. A negotiator reaching it
            has the ladder to descend and nothing else written down.
          </div>
        )}
        {live.map((m) => (
          <div className="py-1.5 border-b hair" key={m.move_id} data-testid="move">
            <div className="text-[12.5px]">
              <span className="font-mono">{m.seq}</span>
              <span className="chip chip-std ml-2">{m.kind}</span>
              <span className="ml-2" style={{ color: 'var(--ink)' }}>{m.title}</span>
            </div>
            <div className="font-serif italic mt-1"
                 style={{ fontSize: 13.5, color: 'var(--mute)' }}>
              {m.guidance}
            </div>
            <div className="caption mt-0.5">
              {/* U14d's number, badged as the advice it is — and its absence
                  rendered as absence, because an estimate nobody computed is
                  not zero (0068's reasoning, kept on screen). */}
              {m.risk_transfer_estimate !== null && m.risk_transfer_estimate !== undefined
                ? <span data-testid="move-estimate">
                    risk transfer est. {m.risk_transfer_estimate}
                    {m.model ? ` (${m.model}${m.model_version ? ' ' + m.model_version : ''})` : ''}
                    {' '}— advisory
                  </span>
                : <span data-testid="move-no-estimate">
                    no risk estimate was asked — recorded as absent, not as zero
                  </span>}
              {' · '}authored by {m.authored_by}
            </div>
            {m.kind === 'fallback_reference' && (
              <div className="caption mt-0.5" data-testid="move-fallback">
                falls back to {m.fallback_clause_id}@v{m.fallback_version}
                {m.fallback_title ? ` — “${m.fallback_title}”` : ''}
                {/* WHERE THAT WORDING SITS TODAY — advisory context from the
                    board, never enforcement (D5): the ladder under it can be
                    republished tomorrow, and this line will simply change. */}
                {m.fallback_rung !== null && m.fallback_rung !== undefined
                  ? ` · rung ${m.fallback_rung} of its clause's live ladder today${m.fallback_is_floor ? ' — the floor' : ''}`
                  : ' · on no live ladder today'}
              </div>
            )}
            {retiring === m.move_id ? (
              <div className="flex gap-2 mt-2 items-end" data-testid="move-retire-form">
                <input className="w-full" aria-label="Why this move is withdrawn"
                       placeholder="why it is withdrawn — required"
                       value={reason} onChange={(e) => setReason(e.target.value)} />
                <button className="btn btn-sm" onClick={() => setRetiring(null)}>cancel</button>
                <ActButton className="btn btn-sm btn-primary" disabled={busy || !reason.trim()}
                        data-testid="confirm-move-retire"
                        onClick={async () => {
                          setBusy(true); setError(null);
                          const r = await API.retireMove({
                            move_id: m.move_id, reason: reason.trim(),
                          });
                          setBusy(false);
                          if (!r.ok) { setError(r.reason); return; }
                          setRetiring(null); setReason(''); onChanged();
                        }}>✓ retire</ActButton>
              </div>
            ) : (
              <button className="btn btn-sm mt-1" data-testid="open-move-retire"
                      onClick={() => { setRetiring(m.move_id); setReason(''); setError(null); }}>
                retire…
              </button>
            )}
          </div>
        ))}
        {retired.length > 0 && (
          <div className="mt-2">
            <button className="btn btn-sm" data-testid="move-history"
                    onClick={() => setHistory(!history)}>
              {history ? 'hide' : 'show'} retired moves ({retired.length})
            </button>
            {history && retired.map((m) => (
              <div className="caption mt-1" key={m.move_id} data-testid="retired-move">
                {m.seq} · {m.kind} · “{m.title}” — retired {m.retired_on} — {m.retired_reason}
              </div>
            ))}
          </div>
        )}
        {!authoring ? (
          <button className="btn btn-sm mt-2" data-testid="open-move-form"
                  onClick={() => setAuthoring(true)}>
            author a move…
          </button>
        ) : (
          <AuthorMoveForm clause={clause} versions={versions}
            onDone={(did) => { setAuthoring(false); if (did) onChanged(); }} />
        )}
        <ActError error={error} />
      </div>
    );
  };

  return (
    <div className="mt-3" data-testid="moves-section">
      <div className="section-label">the playbook — advice, gates nothing</div>
      {moves.status === 'loading' ? (
        <div className="caption mt-1">loading…</div>
      ) : moves.status === 'failed' ? (
        // The database's sentence, verbatim, in this section alone. A refusal
        // to show the playbook is not a reason to blank the clause around it.
        <div className="caption mt-1" data-testid="moves-refused">“{moves.reason}”</div>
      ) : body()}
    </div>
  );
}

// ── The library ───────────────────────────────────────────────────────────
// THE CATALOGUE OF NARROWINGS THIS LIST OFFERS (0110), declared once and read
// twice — by the tiles that raise them, and by the saved views that put them
// back. A focus is a `test` function and no store holds one, so what a saved
// view keeps is the KEY; this is where the key is turned back into the test.
//
// DECLARED OUTSIDE THE COMPONENT deliberately: a catalogue rebuilt on every
// render would be a new array of new objects each time, and the hook that puts
// a focus back would be looking things up in a list that changed underneath it.
const LIBRARY_FOCUSES = [
  { key: 'selectable', label: 'selectable now', test: (c) => c.selectable },
  { key: 'expires_soon', label: 'expiring within 90 days', test: (c) => c.expires_soon },
  { key: 'provenance_gap', label: 'no approval or expiry date',
    test: (c) => c.provenance_gap },
];
const libraryFocus = (key) => LIBRARY_FOCUSES.find((f) => f.key === key);

// ── The tags a conflict rule is written against (0004, on screen 2026-08-24) ──
//
// 0004's own comment: tags are namespaced `namespace:value`, and "the namespace
// is what lets a rule say 'these two clauses disagree about jurisdiction'
// without knowing which jurisdictions exist". `engine/loader.py` has read them
// onto every clause it loads since 0004, and the rule grammar's three
// primitives are evaluated over exactly that array.
//
// AND NO ENDPOINT EVER WROTE ONE. 0004 granted insert and update to the Legal
// admin behind an `admin_writes` policy and read to all five roles, and every
// tag in this system came from a seed script — so a clause minted through the
// review queue, the entrance this product is FOR, arrived untagged and stayed
// invisible to every rule written about its namespace. Found by the write
// census; this is the door.
//
// THE CONTROL IS DRAWN FOR EVERYBODY AND REFUSED BY THE DATABASE, which is this
// pane's existing rule rather than a new one: `retire…` and `supersede…` beside
// it are the Legal admin's alone and are drawn to whoever opens a clause. A
// refusal here arrives in the database's own sentence.
//
// THERE IS NO UNTAGGING, and that is a permission nobody has rather than a door
// left shut: 0004 grants DELETE on cw.clause_tag to no role at all. Removing
// one is a migration and a decision — a rule cites the namespace it fires on,
// so a tag that can vanish changes which contracts were blocked and why.
function ClauseTags({ clause, tags, onChanged }) {
  const acts = useActs();
  const [adding, setAdding] = useState(false);
  const [tag, setTag] = useState('');
  const [error, setError] = useState(null);

  const mine = (tags.rows ?? []).filter(
    (t) => t.clause_id === clause.clause_id && t.version === clause.version);

  const add = () => acts.run(`tag-${clause.clause_id}-${clause.version}`, async () => {
    setError(null);
    const r = await API.tagClause({
      clause_id: clause.clause_id, version: clause.version, tag: tag.trim() });
    if (!r.ok) { setError(r.reason); return r; }
    setTag(''); setAdding(false);
    onChanged();
    return r;
  });

  return (
    <div className="mt-3" data-testid="clause-tags">
      <div className="section-label">tags a rule can read</div>
      {tags.status === 'loading' && <div className="caption mt-1">reading…</div>}
      {tags.status === 'failed' && (
        <div className="caption mt-1">the tags could not be read: {tags.reason}</div>
      )}
      {tags.status === 'loaded' && mine.length === 0 && (
        <div className="caption mt-1" style={{ lineHeight: 1.6 }}>
          This version carries no tags, so no conflict rule written about a
          namespace can see it.
        </div>
      )}
      {tags.status === 'loaded' && mine.length > 0 && (
        <div className="mt-1 flex gap-1.5 flex-wrap">
          {mine.map((t) => (
            <span className="chip chip-std font-mono" key={t.tag}
                  title={`tagged by ${t.tagged_by} on ${String(t.tagged_on ?? '').slice(0, 10)}`}>
              {t.tag}
            </span>
          ))}
        </div>
      )}

      {!adding && (
        <button className="btn btn-sm mt-2" data-testid="open-tag"
                onClick={() => { setAdding(true); setError(null); }}>
          tag…
        </button>
      )}

      {adding && (
        <div className="mt-2">
          <div className="flex gap-2 flex-wrap items-center">
            <input className="font-mono" style={{ padding: '5px 9px', minWidth: 200 }}
                   placeholder="jurisdiction:england" aria-label="The tag to attach"
                   data-testid="tag-value" value={tag}
                   onChange={(e) => setTag(e.target.value)} />
            <ActButton className="btn btn-sm btn-primary" data-testid="tag-attach"
                       disabled={!tag.trim()} onClick={add}>
              attach
            </ActButton>
            <button className="btn btn-sm"
                    onClick={() => { setAdding(false); setTag(''); setError(null); }}>
              cancel
            </button>
          </div>
          <div className="caption mt-1.5" style={{ lineHeight: 1.6 }}>
            <span className="font-mono">namespace:value</span>, lower case. The
            shape is the table's own rule, not this form's — and a tag cannot be
            taken off again, because a rule cites the namespace it fires on.
          </div>
          <ActError error={error} />
        </div>
      )}
      {!adding && <ActError error={error} />}
    </div>
  );
}

function LibraryPane() {
  const pane = usePane(() => API.library());
  // The shared filter, above every early return as hooks must be.
  const filter = useListFilter(pane.rows, {
    view: 'library:versions',
    focuses: LIBRARY_FOCUSES,
    fields: ['clause_id', 'title', 'category_label'],
    facet: 'state',
  });
  // The playbook, fetched once for the whole pane; each open clause narrows
  // what the board already returned. Its load states render inside the
  // drawer's own section, never over the library around it.
  const moves = usePane(() => API.moves());
  // The ladder board, fetched here only to know which coverage gaps ALSO
  // have no ladder — so the gap banner can offer the composing act instead
  // of only pointing at the tab it lives on.
  const boards = usePane(() => API.ladders());
  // The tags, fetched once for the whole pane like the playbook above; each
  // open clause narrows what the read already returned. cw.clause_tag is
  // read_all, so this widens nothing.
  const tags = usePane(() => API.clauseTags());
  const [open, setOpen] = useState(null);
  const [acting, setActing] = useState(null); // { key, kind: 'retire'|'supersede' }
  const [designing, setDesigning] = useState(null); // { category_key, category_label, severity }

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  if (pane.rows.length === 0) {
    return (
      <div>
        <PaneHead title="The library" sub="Every approved position, and its history." />
        <Empty
          kicker="the library"
          line="No clause has been approved yet."
          sub="Wording enters through the review queue. An empty library is a
               true state of this system, not a failure to load." />
      </div>
    );
  }

  // ONE FILTER. This pane held its own copy of the search-and-facet logic —
  // one of five, beside the shared hook. Same behaviour, with one improvement
  // that came free: useListFilter lowercases both sides, so the search is now
  // case-insensitive on every field rather than on some of them.
  const rows = filter.shown;

  // The categories with nothing selectable behind them, derived from the rows
  // rather than fetched again — cw.library_entry carries the flag per row
  // precisely so the screen does not have to ask twice.
  const uncovered = [...new Map(
    pane.rows.filter((c) => c.category_uncovered)
      .map((c) => [`${c.category_key}/${c.severity}`,
                   { key: c.category_key, label: c.category_label,
                     severity: c.severity }])).values()];

  // The pairs a live ladder already covers. Known only once the board has
  // answered — a board that has not loaded is not a board saying "no ladder",
  // so the composing affordance waits rather than guessing.
  const laddered = new Set(
    (boards.status === 'loaded' ? boards.rows : [])
      .filter((r) => !r.retired_on)
      .map((r) => `${r.category_key}/${r.severity}`));
  const ladderless = boards.status === 'loaded'
    ? uncovered.filter((u) => !laddered.has(`${u.key}/${u.severity}`))
    : [];


  return (
    <div>
      <PaneHead title="The library" sub="Every approved position, and its history." />

      {uncovered.length > 0 && (
        // SURFACED, NOT BLAMED. The system's responsibility ends at making this
        // visible and naming who can act; the gap itself belongs to the library's
        // owners. Note what this does not say: not "error", not "misconfigured",
        // not "the system cannot resolve". It says what is missing and whose it
        // is to fill.
        <div className="panel p-3 mb-4" data-testid="coverage-gap">
          <div className="section-label">Nothing approved to fall back on</div>
          <div className="font-serif italic mt-1" style={{ fontSize: 15, color: 'var(--mute)' }}>
            {uncovered.length === 1 ? 'One category has' : `${uncovered.length} categories have`}
            {' '}no selectable wording at a severity the engine may be asked for.
          </div>
          <div className="caption mt-2">
            {uncovered.map((u) => `${u.label} · ${u.severity}`).join('  ·  ')}
          </div>
          <div className="caption mt-2">
            A run that reaches one of these has nothing to offer and will say so.
            <strong> Closing it is Legal's to do</strong> — the system's part is to
            show it here rather than discover it mid-negotiation.
          </div>
          {/* THE PLACE TO ACT, right where the gap is named. Offered only for
              a pair the ladder board says has no live ladder — the board
              answered, not this screen guessing from silence. */}
          {ladderless.length > 0 && (
            <div className="mt-2 flex gap-2 flex-wrap">
              {ladderless.map((u) => (
                <button className="btn btn-sm" key={`${u.key}/${u.severity}`}
                        data-testid="design-ladder-gap"
                        onClick={() => setDesigning(
                          designing && designing.category_key === u.key
                            && designing.severity === u.severity
                            ? null
                            : { category_key: u.key, category_label: u.label,
                                severity: u.severity })}>
                  design a ladder — {u.label} · {u.severity}…
                </button>
              ))}
            </div>
          )}
        </div>
      )}

      {designing && (
        <div className="mb-4">
          <ComposeLadderForm target={designing} versions={pane.rows}
            onDone={(did) => {
              setDesigning(null);
              if (did) { pane.reload(); boards.reload(); }
            }} />
        </div>
      )}

      {/* EACH FIGURE IS A WAY INTO THE SET IT COUNTED. "1 expiring within 90
          days" over fourteen rows meant reading fourteen rows to find the one
          — and the register already records a sweep that read the list, saw
          every row saying "608 days", and concluded the tile was wrong: the
          row that justified it was below where the sample was cut. At 214
          clauses that figure is unusable without this.

          `versions` counts the whole book and so has nothing to narrow to;
          it clears the focus instead, which is the honest thing for a total.
          The narrowing appears as a chip beside the search box, because a list
          quietly showing a subset is exactly the trap this is meant to close. */}
      <TileStrip tiles={[
        { label: 'versions', n: pane.rows.length,
          to: () => filter.setFocus(null),
          describe: `show all ${pane.rows.length} versions`,
          on: filter.focus === null },
        { label: 'selectable now', n: pane.rows.filter((c) => c.selectable).length,
          to: () => filter.focusOn(libraryFocus('selectable')),
          on: filter.focus && filter.focus.key === 'selectable' },
        { label: 'expiring within 90 days', n: pane.rows.filter((c) => c.expires_soon).length,
          to: () => filter.focusOn(libraryFocus('expires_soon')),
          on: filter.focus && filter.focus.key === 'expires_soon' },
        { label: 'no approval or expiry date', n: pane.rows.filter((c) => c.provenance_gap).length,
          to: () => filter.focusOn(libraryFocus('provenance_gap')),
          on: filter.focus && filter.focus.key === 'provenance_gap' },
      ]} />

      <ListFilter filter={filter} testid="library"
                  placeholder="clause, title or category"
                  facetLabel="every state" />
      <div className="caption mb-3"><FilterCount filter={filter} /></div>

      {rows.length === 0 ? (
        <Empty kicker="the library" line="No version matches that."
               sub="Clear the filters to see the whole library." />
      ) : (
        <div className="panel">
          {rows.map((c) => {
            const key = `${c.clause_id}@${c.version}`;
            const isOpen = open === key;
            return (
              <div className="waiting-row" key={key} style={{ alignItems: 'flex-start' }}>
                <div className="min-w-0">
                  <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                    {c.title}
                    <span className="caption"> · {c.clause_id}@v{c.version} · {c.category_label} · {c.severity}</span>
                  </div>
                  <div className="caption mt-0.5">
                    {c.expires_on
                      ? `expires ${c.expires_on}${c.days_to_expiry !== null && c.days_to_expiry !== undefined
                          ? ` (${c.days_to_expiry} days)` : ''}`
                      : 'no expiry date recorded'}
                    {c.approved_on ? ` · approved ${c.approved_on}` : ' · no approval date recorded'}
                    {c.reviewer ? ` by ${c.reviewer}` : ''}
                  </div>
                  {/* LOAD-BEARING, AND THE REASON THIS COLUMN EXISTS. The question
                      to answer BEFORE retiring something is "is this the floor of
                      a ladder?" — retiring a floor turns an intact ladder into
                      floor_unusable, and finding out afterwards means resolution
                      is already refusing to descend it. */}
                  {c.on_ladders > 0 && (
                    <div className="caption mt-0.5" data-testid="on-ladders">
                      {c.is_a_floor
                        ? 'holding up a ladder as its floor'
                        : `on ${c.on_ladders} ladder${c.on_ladders === 1 ? '' : 's'}`}
                    </div>
                  )}
                  {isOpen && (
                    <div className="caption mt-2" style={{ whiteSpace: 'pre-wrap' }}
                         data-testid="rationale-drawer">
                      <div className="section-label">why this wording</div>
                      <div className="mt-1">{c.rationale || 'No rationale was recorded with this version.'}</div>
                      <div className="section-label mt-3">the wording</div>
                      <div className="mt-1">{c.body}</div>
                      {c.state === 'superseded' && (
                        <div className="mt-2">
                          superseded by v{c.successor_version}
                          {c.superseded_reason ? ` — ${c.superseded_reason}` : ''}
                          {c.predecessor_disposition ? ` (${c.predecessor_disposition})` : ''}
                        </div>
                      )}
                      {c.state === 'active' && (
                        <div className="flex gap-2 mt-3">
                          <button className="btn btn-sm" data-testid="open-retire"
                                  onClick={() => setActing(
                                    acting?.key === key && acting?.kind === 'retire'
                                      ? null : { key, kind: 'retire' })}>
                            retire…
                          </button>
                          <button className="btn btn-sm" data-testid="open-supersede"
                                  onClick={() => setActing(
                                    acting?.key === key && acting?.kind === 'supersede'
                                      ? null : { key, kind: 'supersede' })}>
                            supersede…
                          </button>
                        </div>
                      )}
                      {acting?.key === key && acting.kind === 'retire' && (
                        <RetireForm clause={c}
                          onDone={(did) => { setActing(null); if (did) pane.reload(); }} />
                      )}
                      {acting?.key === key && acting.kind === 'supersede' && (
                        <SupersedeForm clause={c}
                          onDone={(did) => { setActing(null); if (did) pane.reload(); }} />
                      )}
                      <ClauseTags clause={c} tags={tags}
                        onChanged={() => tags.reload()} />
                      <MovesSection clause={c} moves={moves} versions={pane.rows}
                        onChanged={() => moves.reload()} />
                    </div>
                  )}
                </div>
                <div className="flex items-center gap-3 shrink-0">
                  {c.provenance_gap && <span className="chip chip-unknown">undated</span>}
                  {c.expires_soon && <span className="chip chip-pending">expiring</span>}
                  <span className={`chip ${c.state === 'active' ? 'chip-ok'
                    : c.state === 'retired' || c.state === 'expired' ? 'chip-err' : 'chip-std'}`}>
                    {c.state}
                  </span>
                  <button className="btn btn-sm" onClick={() => setOpen(isOpen ? null : key)}
                          data-testid="rationale-toggle">
                    {isOpen ? 'close' : 'why'}
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      )}

      <p className="caption mt-3">
        Every change here mints a <strong>new version</strong> with its history
        intact, or withdraws one. There is deliberately no in-place edit:
        rewriting approved wording under the decisions already taken on it is the
        one thing this library exists to prevent. New wording enters through the
        review queue; activation is the minting itself.
      </p>
    </div>
  );
}

// ── Moving the floor: a policy call, confirmed, audited ───────────────────
function FloorForm({ ladder, onDone }) {
  const [rung, setRung] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  return (
    <div className="panel p-3 mt-2" data-testid="floor-form">
      <div className="section-label">move the floor</div>
      <div className="caption mt-1">
        The floor is the last position we will accept — below it, escalation is
        mandatory. Moving it changes how far down every future negotiation in
        this category may go, and the move lands on the audit chain.
      </div>
      <div className="flex gap-2 mt-2 items-end">
        <select className="font-mono" value={rung} aria-label="Which rung becomes the floor"
                onChange={(e) => setRung(e.target.value)} data-testid="floor-rung">
          <option value="">choose the rung</option>
          {ladder.steps.map((s) => (
            <option key={s.rung} value={s.rung}>
              rung {s.rung} · {s.clause_id}@v{s.version}{s.is_floor ? ' (the floor today)' : ''}
            </option>
          ))}
        </select>
        <button className="btn" onClick={() => onDone(false)}>cancel</button>
        <ActButton className="btn btn-primary" disabled={busy || rung === ''}
                data-testid="confirm-floor"
                onClick={async () => {
                  setBusy(true); setError(null);
                  const r = await API.moveFloor({
                    ladder_id: ladder.ladder_id, rung: Number(rung),
                  });
                  setBusy(false);
                  if (!r.ok) { setError(r.reason); return; }
                  onDone(true);
                }}>
          {busy ? 'moving…' : '✓ set the floor there'}
        </ActButton>
      </div>
      <ActError error={error} />
    </div>
  );
}

// ── Replacing a ladder: reorder = retire + publish, in one recorded act ───
// Rung order is immutable in place because past concessions are recorded as
// "we went to rung 2" — so the reordering act is publishing a successor
// ladder. The old one retires and stays readable forever.
function ReplaceLadderForm({ ladder, onDone }) {
  const [draft, setDraft] = useState(ladder.steps.map((s) => ({ ...s })));
  const [floorAt, setFloorAt] = useState(
    Math.max(0, ladder.steps.findIndex((s) => s.is_floor)));
  const [reason, setReason] = useState('');
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  const move = (i, d) => {
    const j = i + d;
    if (j < 0 || j >= draft.length) return;
    const next = draft.slice();
    [next[i], next[j]] = [next[j], next[i]];
    setDraft(next);
    if (floorAt === i) setFloorAt(j); else if (floorAt === j) setFloorAt(i);
  };

  return (
    <div className="panel p-3 mt-2" data-testid="replace-form">
      <div className="section-label">replace this ladder</div>
      <div className="caption mt-1">
        The order below becomes a <strong>new</strong> ladder; this one retires
        and stays readable, so every past concession keeps meaning what it
        meant. Both halves are one recorded act.
      </div>
      {!confirming ? (
        <div className="mt-2">
          {draft.map((s, i) => (
            <div className="flex gap-2 items-center mt-1.5" key={`${s.clause_id}@${s.version}`}
                 data-testid="draft-rung">
              <span className="font-mono caption" style={{ width: 28 }}>{i}</span>
              <span className="text-[13px] min-w-0 truncate" style={{ color: 'var(--ink)' }}>
                {s.rung_title ?? s.clause_id}
                <span className="caption"> · {s.clause_id}@v{s.version}</span>
              </span>
              <span className="flex gap-1 shrink-0 ml-auto items-center">
                <label className="caption">
                  <input type="radio" name={`floor-${ladder.ladder_id}`}
                         checked={floorAt === i} onChange={() => setFloorAt(i)} /> floor
                </label>
                <button className="btn btn-sm" disabled={i === 0}
                        onClick={() => move(i, -1)}>↑</button>
                <button className="btn btn-sm" disabled={i === draft.length - 1}
                        onClick={() => move(i, 1)}>↓</button>
              </span>
            </div>
          ))}
          <label className="section-label mt-3">Why the path is changing</label>
          <input aria-label="Why the path is changing" className="mt-1.5 w-full" value={reason}
                 placeholder="the reason on the record"
                 onChange={(e) => setReason(e.target.value)} data-testid="replace-reason" />
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => onDone(false)}>cancel</button>
            <button className="btn" disabled={reason.trim().length < 5}
                    data-testid="review-replace"
                    onClick={() => setConfirming(true)}>
              review what will be published…
            </button>
          </div>
        </div>
      ) : (
        <div className="mt-2">
          <div className="caption" style={{ whiteSpace: 'pre-wrap' }}>
            {`A new ${ladder.category_label} · ${ladder.severity} ladder:\n`}
            {draft.map((s, i) =>
              `  rung ${i} · ${s.clause_id}@v${s.version}${i === floorAt ? '  ← the floor' : ''}`
            ).join('\n')}
            {`\nThe current ladder retires. Reason: ${reason.trim()}`}
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setConfirming(false)}>← back</button>
            <ActButton className="btn btn-primary" disabled={busy}
                    data-testid="confirm-replace"
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const r = await API.publishLadder({
                        category_key: ladder.category_key, severity: ladder.severity,
                        clause_ids: draft.map((s) => s.clause_id),
                        versions: draft.map((s) => s.version),
                        floor_rung: floorAt, reason: reason.trim(),
                      });
                      setBusy(false);
                      if (!r.ok) { setError(r.reason); setConfirming(false); return; }
                      onDone(true);
                    }}>
              {busy ? 'publishing…' : '✓ publish the replacement'}
            </ActButton>
          </div>
        </div>
      )}
      <ActError error={error} />
    </div>
  );
}

// ── Designing a ladder where none stands ──────────────────────────────────
// The same mechanics as replacing one — ordered rungs, one floor, a reason,
// review then confirm — starting from nothing, for a (category, severity)
// pair with no live ladder. It goes through the SAME one recorded act,
// cw.publish_ladder, which handles a first ladder as a publication with no
// predecessor to retire; there is no second door for "the first one".
function ComposeLadderForm({ target, versions, onDone }) {
  const [draft, setDraft] = useState([]);
  const [floorAt, setFloorAt] = useState(0);
  const [picking, setPicking] = useState('');
  const [reason, setReason] = useState('');
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  // Rungs are picked from the SELECTABLE versions of this category — library
  // wording the review door already minted, never typed. The same structural
  // rule the fallback picker keeps: a rung is a reference or it is nothing.
  const inDraft = new Set(draft.map((s) => `${s.clause_id}@${s.version}`));
  const candidates = versions.filter((v) =>
    v.selectable && v.category_key === target.category_key
    && !inDraft.has(`${v.clause_id}@${v.version}`));

  const move = (i, d) => {
    const j = i + d;
    if (j < 0 || j >= draft.length) return;
    const next = draft.slice();
    [next[i], next[j]] = [next[j], next[i]];
    setDraft(next);
    if (floorAt === i) setFloorAt(j); else if (floorAt === j) setFloorAt(i);
  };

  return (
    <div className="panel p-3 mt-2" data-testid="compose-form">
      <div className="section-label">
        design a ladder — {target.category_label} · {target.severity}
      </div>
      <div className="caption mt-1">
        No live ladder stands for this pair. The order below becomes one, in
        one recorded act: preferred position first, the floor marking the last
        position we will accept.
      </div>
      {!confirming ? (
        <div className="mt-2">
          {draft.map((s, i) => (
            <div className="flex gap-2 items-center mt-1.5" key={`${s.clause_id}@${s.version}`}
                 data-testid="compose-rung">
              <span className="font-mono caption" style={{ width: 28 }}>{i}</span>
              <span className="text-[13px] min-w-0 truncate" style={{ color: 'var(--ink)' }}>
                {s.title ?? s.clause_id}
                <span className="caption"> · {s.clause_id}@v{s.version}</span>
              </span>
              <span className="flex gap-1 shrink-0 ml-auto items-center">
                <label className="caption">
                  <input type="radio" name={`compose-floor-${target.category_key}-${target.severity}`}
                         checked={floorAt === i} onChange={() => setFloorAt(i)} /> floor
                </label>
                <button className="btn btn-sm" disabled={i === 0}
                        onClick={() => move(i, -1)}>↑</button>
                <button className="btn btn-sm" disabled={i === draft.length - 1}
                        onClick={() => move(i, 1)}>↓</button>
                <button className="btn btn-sm"
                        onClick={() => {
                          setDraft(draft.filter((_, k) => k !== i));
                          if (floorAt === i) setFloorAt(0);
                          else if (floorAt > i) setFloorAt(floorAt - 1);
                        }}>remove</button>
              </span>
            </div>
          ))}
          {candidates.length === 0 && draft.length === 0 ? (
            // The gap underneath the gap, said plainly: a ladder is made of
            // approved wording, and this category has none selectable to make
            // one from. Wording enters through the review queue first.
            <div className="caption mt-2" data-testid="compose-nothing-selectable">
              Nothing in this category is selectable to build a rung from.
              Approved wording comes first — through the review queue — and the
              ladder second.
            </div>
          ) : (
            <div className="flex gap-2 mt-2 items-end">
              <select className="font-mono min-w-0 grow" value={picking}
                      aria-label="Add a rung — choose an approved clause version"
                      data-testid="compose-pick"
                      onChange={(e) => setPicking(e.target.value)}>
                <option value="">add a rung — an approved version…</option>
                {candidates.map((v) => (
                  <option key={`${v.clause_id}@${v.version}`}
                          value={`${v.clause_id}@${v.version}`}>
                    {v.clause_id}@v{v.version} — {v.title}
                  </option>
                ))}
              </select>
              <button className="btn btn-sm" disabled={!picking}
                      data-testid="compose-add"
                      onClick={() => {
                        const v = candidates.find(
                          (c) => `${c.clause_id}@${c.version}` === picking);
                        if (v) setDraft([...draft, v]);
                        setPicking('');
                      }}>add</button>
            </div>
          )}
          <label className="section-label mt-3">Why this retreat path</label>
          <input aria-label="Why this retreat path" className="mt-1.5 w-full" value={reason}
                 placeholder="the reason on the record"
                 onChange={(e) => setReason(e.target.value)} data-testid="compose-reason" />
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => onDone(false)}>cancel</button>
            <button className="btn" disabled={draft.length === 0 || reason.trim().length < 5}
                    data-testid="review-compose"
                    onClick={() => setConfirming(true)}>
              review what will be published…
            </button>
          </div>
        </div>
      ) : (
        <div className="mt-2">
          <div className="caption" style={{ whiteSpace: 'pre-wrap' }}>
            {`A new ${target.category_label} · ${target.severity} ladder:\n`}
            {draft.map((s, i) =>
              `  rung ${i} · ${s.clause_id}@v${s.version}${i === floorAt ? '  ← the floor' : ''}`
            ).join('\n')}
            {`\nNo ladder retires — none stands. Reason: ${reason.trim()}`}
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setConfirming(false)}>← back</button>
            <ActButton className="btn btn-primary" disabled={busy}
                    data-testid="confirm-compose"
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const r = await API.publishLadder({
                        category_key: target.category_key, severity: target.severity,
                        clause_ids: draft.map((s) => s.clause_id),
                        versions: draft.map((s) => s.version),
                        floor_rung: floorAt, reason: reason.trim(),
                      });
                      setBusy(false);
                      if (!r.ok) { setError(r.reason); setConfirming(false); return; }
                      onDone(true);
                    }}>
              {busy ? 'publishing…' : '✓ publish the ladder'}
            </ActButton>
          </div>
        </div>
      )}
      <ActError error={error} />
    </div>
  );
}

// ── Ladders ───────────────────────────────────────────────────────────────
function LaddersPane() {
  const pane = usePane(() => API.ladders());
  // The library, fetched here to know which (category, severity) pairs have
  // selectable wording and NO live ladder — the pairs a ladder could be
  // designed for. Derived from rows already scoped by the endpoints; nothing
  // here asks wider than either read returns.
  const lib = usePane(() => API.library());
  const [acting, setActing] = useState(null); // { id, kind: 'floor'|'replace' }
  const [designing, setDesigning] = useState(null); // { category_key, category_label, severity }
  const [history, setHistory] = useState(false);

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  // NO EARLY RETURN ON EMPTY. The rules and the promotion queue live on this
  // pane too, and an early return here made them unreachable the moment no
  // ladder existed — found in the browser on 2026-08-02, the exact
  // trap-5.2 shape: absence of one thing rendering as absence of everything.

  // Group the rung rows back into ladders. cw.ladder_board repeats the ladder's
  // health onto every rung so a table render needs no second result set; this
  // regroups for a per-ladder card.
  const ladders = [];
  for (const r of pane.rows) {
    let l = ladders.find((x) => x.ladder_id === r.ladder_id);
    if (!l) {
      l = { ladder_id: r.ladder_id, category_key: r.category_key,
            category_label: r.category_label,
            severity: r.severity, owner: r.owner, reviewed_on: r.reviewed_on,
            status: r.ladder_status, rungs: r.rungs, unusable: r.unusable_rungs,
            has_floor: r.has_floor, retired_on: r.retired_on, steps: [] };
      ladders.push(l);
    }
    // AN EMPTY LADDER ARRIVES AS ONE ROW WITH A NULL RUNG, and that row is the
    // one that matters most. cw.ladder_board LEFT JOINs its rungs precisely so a
    // rungless ladder still appears; dropping the null here would undo that at
    // the last possible moment and report a configuration error as absence.
    if (r.rung !== null && r.rung !== undefined) l.steps.push(r);
  }

  // The live board and the history. A retired ladder stays readable forever —
  // past concessions name its rungs — but it is history, not the retreat path.
  const live = ladders.filter((l) => !l.retired_on);
  const retired = ladders.filter((l) => l.retired_on);

  // The pairs with selectable wording and no live ladder — where a retreat
  // path COULD stand and does not. Derivable only once the library answered;
  // silence from it is not "every pair is covered", so nothing is offered
  // until it speaks, and its refusal is shown below rather than swallowed.
  const liveKeys = new Set(live.map((l) => `${l.category_key}/${l.severity}`));
  const unladdered = [...new Map(
    (lib.status === 'loaded' ? lib.rows : [])
      .filter((v) => v.selectable)
      .map((v) => [`${v.category_key}/${v.severity}`,
                   { category_key: v.category_key, category_label: v.category_label,
                     severity: v.severity }])).values()]
    .filter((p) => !liveKeys.has(`${p.category_key}/${p.severity}`));

  return (
    <div>
      <PaneHead title="Ladders & rules" sub="The retreat path, per category and severity." />

      {live.length === 0 && (
        <Empty
          kicker="ladders"
          line="No ladder is published."
          sub="A ladder is the pre-approved retreat: preferred position, fallback,
               floor. Without one, a negotiation has nothing to descend." />
      )}

      {live.length > 0 && <TileStrip tiles={[
        { label: 'ladders', n: live.length },
        { label: 'intact', n: live.filter((l) => l.status === 'intact').length },
        { label: 'need attention', n: live.filter((l) => l.status !== 'intact').length },
      ]} />}

      <div className="mt-4">
        {live.map((l) => (
          <div className="panel p-3 mb-3" key={l.ladder_id} data-testid="ladder">
            <div className="flex items-end justify-between">
              <div>
                <div className="section-label">{l.category_label} · {l.severity}</div>
                <div className="caption mt-0.5">
                  owner {l.owner}
                  {l.reviewed_on ? ` · reviewed ${l.reviewed_on}` : ' · never reviewed'}
                </div>
              </div>
              <span className={`chip ${l.status === 'intact' ? 'chip-ok'
                : l.status === 'empty' || l.status === 'floorless' || l.status === 'floor_unusable'
                  ? 'chip-err' : 'chip-pending'}`}
                    data-testid="ladder-status">
                {l.status}
              </span>
            </div>

            {l.steps.length === 0 ? (
              // The empty ladder, said out loud. This is a configuration error
              // somebody has to fix, and it must never render as a short healthy
              // list or as nothing at all.
              <div className="caption mt-2" data-testid="ladder-empty">
                <strong>This ladder has no rungs.</strong> A negotiation reaching
                this category and severity has nothing to retreat to, and the
                engine will refuse to descend it rather than invent a position.
              </div>
            ) : (
              <div className="mt-2">
                {l.steps.map((r) => (
                  <div className="flex gap-3 items-center mt-1.5" key={r.rung}
                       data-testid="rung">
                    <span className="font-mono caption shrink-0" style={{ width: 28 }}>
                      {r.rung}
                    </span>
                    <span className="text-[13px] min-w-0 truncate" style={{ color: 'var(--ink)' }}>
                      {r.rung_title ?? r.clause_id}
                      <span className="caption"> · {r.clause_id}@v{r.version}</span>
                    </span>
                    <span className="flex gap-2 items-center shrink-0 ml-3">
                      {r.is_floor && <span className="chip chip-std">floor</span>}
                      {!r.rung_selectable && (
                        <span className="chip chip-err" data-testid="rung-unusable">
                          {r.rung_state}
                        </span>
                      )}
                    </span>
                  </div>
                ))}
                {l.unusable > 0 && (
                  <div className="caption mt-2">
                    {l.unusable} of {l.rungs} rungs cannot be used. They stay on the
                    ladder because removing them would hide the problem rather than
                    fix it — the wording behind them needs replacing.
                  </div>
                )}
                {!l.has_floor && (
                  <div className="caption mt-2">
                    <strong>No floor.</strong> Nothing marks the position below
                    which this category may not go, so there is no stopping point
                    to enforce.
                  </div>
                )}
                <div className="flex gap-2 mt-3">
                  <button className="btn btn-sm" data-testid="open-floor"
                          onClick={() => setActing(
                            acting?.id === l.ladder_id && acting?.kind === 'floor'
                              ? null : { id: l.ladder_id, kind: 'floor' })}>
                    move the floor…
                  </button>
                  <button className="btn btn-sm" data-testid="open-replace"
                          onClick={() => setActing(
                            acting?.id === l.ladder_id && acting?.kind === 'replace'
                              ? null : { id: l.ladder_id, kind: 'replace' })}>
                    replace this ladder…
                  </button>
                </div>
                {acting?.id === l.ladder_id && acting.kind === 'floor' && (
                  <FloorForm ladder={l}
                    onDone={(did) => { setActing(null); if (did) pane.reload(); }} />
                )}
                {acting?.id === l.ladder_id && acting.kind === 'replace' && (
                  <ReplaceLadderForm ladder={l}
                    onDone={(did) => { setActing(null); if (did) pane.reload(); }} />
                )}
              </div>
            )}
          </div>
        ))}
      </div>

      {/* WHERE NO LADDER STANDS. The pairs come from the library the pane
          already reads; choosing one opens the composing form, which starts
          empty and publishes through the same one recorded act a replacement
          uses. A failed library read is said out loud — silence here would
          render "nothing to design" over "we could not ask". */}
      {lib.status === 'failed' && (
        <div className="caption mt-4" data-testid="unladdered-refused">
          Which pairs have no ladder could not be derived — “{lib.reason}”
        </div>
      )}
      {unladdered.length > 0 && (
        <div className="panel p-3 mt-4" data-testid="unladdered">
          <div className="section-label">No retreat path stands for</div>
          <div className="mt-2 flex gap-2 flex-wrap">
            {unladdered.map((p) => (
              <button className="btn btn-sm" key={`${p.category_key}/${p.severity}`}
                      data-testid="design-ladder"
                      onClick={() => setDesigning(
                        designing && designing.category_key === p.category_key
                          && designing.severity === p.severity
                          ? null : p)}>
                design a ladder — {p.category_label} · {p.severity}…
              </button>
            ))}
          </div>
          {designing && (
            <ComposeLadderForm target={designing} versions={lib.rows}
              onDone={(did) => {
                setDesigning(null);
                if (did) { pane.reload(); lib.reload(); }
              }} />
          )}
        </div>
      )}

      {retired.length > 0 && (
        <div className="mt-4">
          <button className="btn btn-sm" data-testid="ladder-history"
                  onClick={() => setHistory(!history)}>
            {history ? 'hide' : 'show'} retired ladders ({retired.length})
          </button>
          {history && retired.map((l) => (
            <div className="panel p-3 mt-2" key={l.ladder_id} data-testid="retired-ladder">
              <div className="section-label">
                {l.category_label} · {l.severity} · retired {l.retired_on}
              </div>
              <div className="caption mt-1">
                Kept readable forever: concessions taken on this ladder name its
                rungs, and those records keep meaning what they meant.
              </div>
              {l.steps.map((r) => (
                <div className="caption mt-1" key={r.rung}>
                  rung {r.rung} · {r.clause_id}@v{r.version}{r.is_floor ? ' · was the floor' : ''}
                </div>
              ))}
            </div>
          ))}
        </div>
      )}

      <RulesSection />
      <PromotionSection />
      <AnalyticsSection />
    </div>
  );
}

// ── Conflict rules: published as versions, retired with a reason ──────────
// A rule is a legal judgement expressed as data. "Editing" one is publishing
// its next version — cw.active_conflict_rule takes the latest effective one,
// so the predecessor stops deciding without being touched.
function RulesSection() {
  const pane = usePane(() => API.rules());
  const [authoring, setAuthoring] = useState(false);
  const [retiring, setRetiring] = useState(null); // `${rule_id}@${version}`
  const [reason, setReason] = useState('');
  const [form, setForm] = useState({
    rule_id: '', name: '', severity: 'Standard', title: '', detail: '',
    primitive: 'all_present', value: '',
  });
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  // The catalogue, newest version first within each rule.
  const rules = [...pane.rows].sort((a, b) =>
    a.rule_id === b.rule_id ? b.version - a.version
      : a.rule_id < b.rule_id ? -1 : 1);
  const latest = new Map();
  for (const r of pane.rows) {
    if (!latest.has(r.rule_id) || latest.get(r.rule_id) < r.version)
      latest.set(r.rule_id, r.version);
  }

  const predicate = () =>
    form.primitive === 'conflicting_values'
      ? { conflicting_values: form.value.trim() }
      : { [form.primitive]: form.value.split(',').map((s) => s.trim()).filter(Boolean) };

  return (
    <div className="mt-6">
      <PanelHead title="Conflict rules" sub="What may not appear together, as data with a version." />
      {rules.length === 0 ? (
        <Empty kicker="rules" line="No conflict rule has been published."
               sub="A rule is authored here and versioned like wording: published,
                    effective, retirable — never edited in place." />
      ) : (
        <div className="panel">
          {rules.map((r) => {
            const key = `${r.rule_id}@${r.version}`;
            const inForce = !r.retired && latest.get(r.rule_id) === r.version;
            return (
              <div className="waiting-row" key={key} style={{ alignItems: 'flex-start' }}>
                <div className="min-w-0">
                  <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                    {r.title}
                    <span className="caption"> · {r.rule_id}@v{r.version} · {r.severity}</span>
                  </div>
                  <div className="caption mt-0.5">{r.detail}</div>
                  <div className="caption mt-0.5 font-mono">{JSON.stringify(r.predicate)}</div>
                  {r.retired && (
                    <div className="caption mt-0.5">retired — {r.retired_reason}</div>
                  )}
                  {retiring === key && (
                    <div className="flex gap-2 mt-2 items-end" data-testid="rule-retire-form">
                      <input className="w-full" aria-label="Why this rule is withdrawn"
                             placeholder="why it is withdrawn"
                             value={reason} onChange={(e) => setReason(e.target.value)} />
                      <button className="btn btn-sm" onClick={() => setRetiring(null)}>cancel</button>
                      <ActButton className="btn btn-sm btn-primary" disabled={busy || !reason.trim()}
                              data-testid="confirm-rule-retire"
                              onClick={async () => {
                                setBusy(true); setError(null);
                                const res = await API.retireRule({
                                  rule_id: r.rule_id, version: r.version,
                                  reason: reason.trim(),
                                });
                                setBusy(false);
                                if (!res.ok) { setError(res.reason); return; }
                                setRetiring(null); setReason(''); pane.reload();
                              }}>✓ retire</ActButton>
                    </div>
                  )}
                </div>
                <div className="flex items-center gap-3 shrink-0">
                  <span className={`chip ${r.retired ? 'chip-err' : inForce ? 'chip-ok' : 'chip-std'}`}>
                    {r.retired ? 'retired' : inForce ? 'in force' : 'superseded'}
                  </span>
                  {!r.retired && (
                    <button className="btn btn-sm" data-testid="open-rule-retire"
                            onClick={() => { setRetiring(retiring === key ? null : key); setReason(''); }}>
                      retire…
                    </button>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      )}

      {!authoring ? (
        <button className="btn mt-3" data-testid="open-rule-form"
                onClick={() => setAuthoring(true)}>
          publish a rule…
        </button>
      ) : (
        <div className="panel p-3 mt-3" data-testid="rule-form">
          <div className="section-label">publish a rule version</div>
          <div className="caption mt-1">
            Publishing under an existing id mints its next version, which takes
            over from the current one. Three primitives, nothing else — a rule a
            lawyer cannot say in them needs a new primitive added deliberately,
            not a freer grammar.
          </div>
          <div className="flex gap-3 mt-2">
            <div>
              <label className="section-label">Rule id</label>
              <input aria-label="Rule id" className="mt-1.5 font-mono" style={{ width: 120 }} placeholder="DP-001"
                     value={form.rule_id}
                     onChange={(e) => setForm({ ...form, rule_id: e.target.value })} />
            </div>
            <div>
              <label className="section-label">Name</label>
              <input aria-label="Name" className="mt-1.5" value={form.name}
                     onChange={(e) => setForm({ ...form, name: e.target.value })} />
            </div>
            <div>
              <label className="section-label">Severity</label>
              <select aria-label="Severity" className="mt-1.5 font-mono" value={form.severity}
                      onChange={(e) => setForm({ ...form, severity: e.target.value })}>
                <option>Standard</option><option>High</option>
              </select>
            </div>
          </div>
          <label className="section-label mt-3">Title</label>
          <input aria-label="Title" className="mt-1.5 w-full" value={form.title}
                 onChange={(e) => setForm({ ...form, title: e.target.value })} />
          <label className="section-label mt-3">What it asks, in words</label>
          <input aria-label="What it asks, in words" className="mt-1.5 w-full" value={form.detail}
                 onChange={(e) => setForm({ ...form, detail: e.target.value })} />
          <div className="flex gap-3 mt-3 items-end">
            <div>
              <label className="section-label">Primitive</label>
              <select aria-label="Primitive" className="mt-1.5 font-mono" value={form.primitive}
                      onChange={(e) => setForm({ ...form, primitive: e.target.value })}>
                <option value="all_present">all present</option>
                <option value="none_present">none present</option>
                <option value="conflicting_values">conflicting values</option>
              </select>
            </div>
            <div className="min-w-0 grow">
              <label className="section-label">
                {form.primitive === 'conflicting_values'
                  ? 'Tag namespace' : 'Tags, comma-separated'}
              </label>
              {/* The name is the SAME expression the label renders, so the two
                  cannot drift apart when the primitive changes. */}
              <input className="mt-1.5 w-full font-mono" value={form.value}
                     aria-label={form.primitive === 'conflicting_values'
                       ? 'Tag namespace' : 'Tags, comma-separated'}
                     onChange={(e) => setForm({ ...form, value: e.target.value })} />
            </div>
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setAuthoring(false)}>cancel</button>
            <ActButton className="btn btn-primary" data-testid="confirm-rule"
                    disabled={busy || !form.rule_id.trim() || !form.name.trim()
                              || !form.title.trim() || !form.detail.trim() || !form.value.trim()}
                    onClick={async () => {
                      setBusy(true); setError(null);
                      const res = await API.publishRule({
                        rule_id: form.rule_id.trim(), name: form.name.trim(),
                        severity: form.severity, title: form.title.trim(),
                        detail: form.detail.trim(), predicate: predicate(),
                      });
                      setBusy(false);
                      if (!res.ok) { setError(res.reason); return; }
                      setAuthoring(false);
                      setForm({ rule_id: '', name: '', severity: 'Standard',
                                title: '', detail: '', primitive: 'all_present', value: '' });
                      pane.reload();
                    }}>
              {busy ? 'publishing…' : '✓ publish'}
            </ActButton>
          </div>
          <ActError error={error} />
        </div>
      )}
      <ActError error={retiring ? error : null} />
    </div>
  );
}

// ── Concession promotion: vendor-shaped wording enters the library ────────
function promotionReady(c) {
  // concession_state calls a recorded settlement 'approved', not 'settled'.
  return c.state === 'approved' && c.vendor_text != null && !c.promoted_to_clause;
}

function PromotionReview({ concession: c, onClose, onPromoted }) {
  const draftKey = `concession-promotion:${c.concession_id}`;
  const [form, setForm] = useRetainedState(draftKey, { new_clause_id: '', title: '', rationale: '' });
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const complete = !!(form.new_clause_id.trim() && form.title.trim() && form.rationale.trim());

  return (
    <div className="panel p-4 mt-3 promotion-review" data-testid="promote-form">
      <h3 className="section-label">Review concession #{c.concession_id} · {c.agreement_id}</h3>
      <p className="caption mt-2">Settled by {c.settled_by} on {String(c.settled_on ?? '').slice(0, 10)}.
        Promotion adds this exact wording to the library; it does not rewrite the concession.</p>
      <div className="section-label mt-4">Settled supplier wording</div>
      <div className="promotion-wording mt-2" data-testid="promotion-wording">{c.vendor_text}</div>
      <div className="promotion-fields mt-4">
        <div>
          <label className="section-label">New clause id</label>
          <input aria-label="New clause id" className="mt-1.5 w-full font-mono"
                 disabled={busy || confirming} value={form.new_clause_id}
                 onChange={(e) => setForm({ ...form, new_clause_id: e.target.value })} />
        </div>
        <div>
          <label className="section-label">Title</label>
          <input aria-label="Title" className="mt-1.5 w-full" disabled={busy || confirming}
                 value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
        </div>
      </div>
      <label className="section-label mt-3">Why it becomes a position</label>
      <textarea aria-label="Why it becomes a position" className="mt-1.5 w-full" rows={3}
                disabled={busy || confirming} value={form.rationale}
                onChange={(e) => setForm({ ...form, rationale: e.target.value })} />
      {confirming && (
        <div className="panel p-3 mt-3" data-testid="promotion-confirmation">
          <div className="section-label">Add {form.new_clause_id.trim()} · {form.title.trim()}</div>
          <p className="caption mt-2">Concession #{c.concession_id} on {c.agreement_id} becomes reusable
            library language under your name. Its source and the original position's severity are kept.</p>
        </div>
      )}
      <div className="promotion-actions mt-3">
        <button className="btn" disabled={busy} onClick={onClose}>close · keep draft</button>
        {confirming ? <>
          <button className="btn" disabled={busy} onClick={() => setConfirming(false)}>back to details</button>
          <ActButton className="btn btn-primary" data-testid="confirm-promote"
                     disabled={busy || !complete || !promotionReady(c)}
                     onClick={async () => {
                       setBusy(true); setError(null);
                       const res = await API.promoteConcession({
                         concession_id: c.concession_id,
                         new_clause_id: form.new_clause_id.trim(),
                         title: form.title.trim(),
                         rationale: form.rationale.trim(),
                       });
                       setBusy(false);
                       if (!res.ok) { setError(res.reason); return; }
                       discardDraft(draftKey);
                       onPromoted(c.concession_id, res.rows?.[0]?.minted);
                     }}>
            {busy ? 'promoting…' : '✓ promote into the library'}
          </ActButton>
        </> : (
          <button className="btn btn-primary" data-testid="review-promotion"
                  disabled={!complete || !promotionReady(c)} onClick={() => setConfirming(true)}>
            review promotion →
          </button>
        )}
      </div>
      <ActError error={error} />
    </div>
  );
}

function PromotionSection() {
  const pane = usePane(() => API.concessions());
  const [promoting, setPromoting] = useState(null); // concession_id
  const [receipts, setReceipts] = useState({});
  const finishPromotion = (id, minted) => {
    setReceipts((old) => ({ ...old, [id]: minted || true }));
    setPromoting((selected) => String(selected) === String(id) ? null : selected);
    pane.reload();
  };

  if (pane.status === 'loading' && !pane.rows.length) return <Loading />;
  if (pane.status === 'failed' && !pane.rows.length) return <LoadFailed reason={pane.reason} />;

  // Keep settled rungs and previous promotions visible as their own outcomes.
  const candidates = pane.rows.filter((c) => c.state === 'approved' || c.promoted_to_clause);

  return (
    <div className="mt-6">
      <PanelHead title="Concession promotion"
                 sub="Review settled supplier wording before making it reusable library language." />
      {pane.status === 'failed' && <LoadFailed reason={pane.reason} />}
      {candidates.length === 0 ? (
        <Empty kicker="promotion" line="No settled concession is waiting."
               sub="When the same ground is conceded again and again, promoting it
                    turns the retreat into an approved position." />
      ) : (
        <div className="panel">
          {candidates.map((c) => {
            const receipt = c.promoted_to_clause || receipts[c.concession_id];
            return (
            <div className="waiting-row promotion-row" key={c.concession_id} data-testid="promotion-row">
              <div className="promotion-summary">
                <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                  #{c.concession_id} · {c.agreement_id} · {c.category_key}
                  <span className="caption"> · from {c.standard_clause_id}@v{c.standard_version}
                    {c.conceded_rung !== null && c.conceded_rung !== undefined
                      ? ` · to rung ${c.conceded_rung}` : ' · vendor wording'}</span>
                </div>
                <div className="caption mt-0.5">{c.reason}</div>
              </div>
              <div className="promotion-actions">
                {receipt ? <div className="caption" role="status" data-testid="promotion-receipt">
                  Promoted{typeof receipt === 'string' ? ` · ${receipt}` : ' · recorded'}
                </div> : promotionReady(c) ? (
                  <button className="btn btn-sm" data-testid="open-promote"
                          aria-expanded={String(promoting) === String(c.concession_id)}
                          onClick={() => setPromoting((selected) =>
                            String(selected) === String(c.concession_id) ? null : c.concession_id)}>
                    review wording & promote…
                  </button>
                ) : <span className="caption" data-testid="promotion-existing-rung">
                  Settled on an approved rung · already library language
                </span>}
              </div>
              {!receipt && promotionReady(c) && String(promoting) === String(c.concession_id) && (
                <PromotionReview key={c.concession_id} concession={c}
                  onClose={() => setPromoting((selected) =>
                    String(selected) === String(c.concession_id) ? null : selected)}
                  onPromoted={finishPromotion} />
              )}
            </div>
          ); })}
        </div>
      )}
    </div>
  );
}

// ── What a proposal actually rests on ─────────────────────────────────────
//
// 0003 wrote the promise into the view itself: "Evidence is always traceable
// to source records — a recommendation without its concessions is an
// assertion, and this system does not make assertions." The panel repeats it
// in its own subtitle. Nothing delivered it: a lawyer reading "candidate for a
// new approved rung" had to go and find the concessions by hand.
//
// THREE THINGS THIS SHOWS THAT THE SENTENCE ABOVE IT CANNOT, and each was
// checked against the SQL before being drawn:
//
//   1. HOW MANY OF THE CONCESSIONS ARE IN FORCE. `cw.concession_rate` counts
//      `cw.concession`, not `cw.concession_in_force` — so a concession nobody
//      has approved, and one that was WITHDRAWN, both count toward a proposal
//      to change the approved library. `cw.concession_state`'s own comment
//      says a proposed concession "binds nobody". On the seeded library, the
//      liab proposal rests on three concessions of which ONE is in force.
//   2. WHICH FIGURE WAS COMPUTED OVER WHAT. `settlement_rung` is
//      `mode() within group (order by conceded_rung)` and `avg_rung` is an
//      average of the same column — and a vendor-language concession has a
//      NULL rung, which both aggregates skip. So "3 concessions, settles at
//      rung 1" can be three concessions and a rung taken from two of them.
//   3. EVERY TRIGGER THAT FIRED. The view's CASE returns the FIRST matching
//      branch, so a clause that has both taken vendor language three times AND
//      breached its floor three times is described by one of those and not the
//      other. The seeded `DP-H-001` is exactly that row.
//
// WHAT THIS DOES NOT DO: change the aggregate. Repointing `cw.concession_rate`
// at in-force concessions would change what the learning loop learns from —
// a decision with a cost, and the owner's. The system's job here ends at
// making the gap visible (the Product Boundary), which is what this is.
//
// EVERY FIGURE IS COUNTED FROM THE ROWS BELOW IT, never from the aggregate, so
// a figure and the rows it describes are one set by construction (S363).
function evidenceFor(rows, proposal) {
  const mine = (rows || []).filter(
    (e) => e.standard_clause_id === proposal.standard_clause_id
        && e.category_key === proposal.category_key);
  const count = (test) => mine.filter(test).length;
  return {
    rows: mine,
    recorded: mine.length,
    inForce: count((e) => e.state === 'approved'),
    onlyProposed: count((e) => e.state === 'proposed'),
    withdrawn: count((e) => e.state === 'withdrawn'),
    // The set the rung aggregates were computed over — never the whole set.
    named: count((e) => e.conceded_rung !== null && e.conceded_rung !== undefined),
    vendorLanguage: count((e) => e.vendor_language),
    neededOverride: count((e) => e.override_ref),
    belowFloor: count((e) => e.conceded_rung !== null && e.conceded_rung !== undefined
                          && e.ladder_floor_rung !== null && e.ladder_floor_rung !== undefined
                          && e.conceded_rung > e.ladder_floor_rung),
  };
}

// The three tests the view's CASE runs, in its order, so the screen can say
// which fired rather than which was reported. Read out of the SQL, not
// invented: to_vendor_language >= 3, required_override >= 3, settlement_rung > 0.
function triggersFired(ev, proposal) {
  const out = [];
  if (ev.vendorLanguage >= 3) {
    out.push(`vendor language taken ${ev.vendorLanguage} times`);
  }
  if (ev.neededOverride >= 3) {
    out.push(`an override was needed ${ev.neededOverride} times`);
  }
  if (proposal.settlement_rung !== null && proposal.settlement_rung !== undefined
      && proposal.settlement_rung > 0) {
    out.push(`deals settle at rung ${proposal.settlement_rung}`);
  }
  return out;
}

function ProposalEvidence({ ev, proposal, failed }) {
  if (failed) {
    return (
      <div className="p-3" data-testid="evidence-refused">
        <div className="tag" style={{ color: 'var(--danger)' }}>evidence unavailable</div>
        {/* THE REASON, NOT AN EMPTY LIST. A refused read drawn as "no
            concessions" says the proposal rests on nothing, which is the
            opposite of what happened (S331). */}
        <div className="caption mt-1">{failed}</div>
      </div>
    );
  }
  if (!ev.recorded) {
    return (
      <div className="p-3 caption" data-testid="evidence-empty">
        The read returned no concessions for this proposal. That cannot be true
        of a proposal — it is derived from them — so this is a scope or a
        timing answer, not a measurement.
      </div>
    );
  }
  const fired = triggersFired(ev, proposal);
  return (
    <div className="mt-3" data-testid="proposal-evidence">
      <TileStrip tiles={[
        { label: 'concessions counted', n: ev.recorded },
        { label: 'in force', n: ev.inForce },
        { label: 'only proposed', n: ev.onlyProposed },
        { label: 'withdrawn', n: ev.withdrawn },
      ]} />

      {(ev.onlyProposed > 0 || ev.withdrawn > 0) && (
        <div className="caption mt-2" data-testid="evidence-caveat">
          <strong>{ev.inForce} of {ev.recorded} are in force.</strong>{' '}
          The count above is every concession on the record, because that is
          what the rate view counts — a concession that is only proposed, or
          that was withdrawn, is counted here too. The record's own words for a
          concession that has not settled are that it binds nobody.
        </div>
      )}

      {fired.length > 1 && (
        <div className="caption mt-2" data-testid="evidence-triggers">
          <strong>{fired.length} patterns fired, and the sentence above names
          one:</strong> {fired.join(' · ')}.
        </div>
      )}

      {proposal.settlement_rung !== null && proposal.settlement_rung !== undefined
        && ev.named < ev.recorded && (
        <div className="caption mt-2" data-testid="evidence-rung-basis">
          <strong>“Settles at rung {proposal.settlement_rung}” was computed from{' '}
          {ev.named} of {ev.recorded}.</strong>{' '}
          A concession that took vendor language names no rung, and the rung
          figures skip it rather than counting it as zero.
        </div>
      )}

      <div className="panel mt-3">
        {ev.rows.map((e) => (
          <div className="waiting-row" key={e.concession_id} data-testid="evidence-row">
            <div className="min-w-0">
              <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                {e.agreement_id}
                <span className="caption"> · concession {e.concession_id}</span>
              </div>
              <div className="caption mt-0.5 truncate">
                {/* THE VENDOR TEXT ITSELF IS NEVER SHOWN. It is quarantined
                    wording (0003) — the record says whether it was taken, not
                    what it said, and the only route into the library is
                    cw.promote_concession(). */}
                {e.vendor_language
                  ? 'vendor language taken'
                  : `conceded to rung ${e.conceded_rung}`}
                {e.ladder_floor_rung !== null && e.ladder_floor_rung !== undefined
                  && ` · floor was rung ${e.ladder_floor_rung}`}
                {e.override_ref && ` · override ${e.override_ref}`}
                {' · '}approved by {e.approved_by} on {e.conceded_on}
              </div>
              <div className="caption mt-0.5 truncate">{e.reason}</div>
              {/* WHAT LEGAL CONCLUDED ABOUT DOING IT AGAIN (0106). This is the
                  screen where somebody decides whether a concession should
                  become standard wording, so the judgment belongs here — with
                  the attorney's own words under it, because a verdict with no
                  reasoning beneath it is an assertion. */}
              {(e.rationales || []).map((r, i) => (
                <div className="caption mt-0.5" key={i} style={{ lineHeight: 1.6 }}>
                  <span className="font-mono">{r.approver}</span>
                  {r.reuse && (
                    <span className={r.reuse === 'one_off' ? '' : 'font-mono'}>
                      {' · '}{r.reuse === 'one_off' ? 'one-off, not a pattern'
                        : r.reuse === 'similar_deals' ? 'acceptable again in similar deals'
                        : 'says our standard position is wrong'}
                    </span>
                  )}
                  {r.rationale && <span className="italic">{' — '}{r.rationale}</span>}
                </div>
              ))}
            </div>
            <div className="flex items-center gap-2 shrink-0">
              {/* THREE ANSWERS, NOT TWO. `unjudged` is drawn as itself: nobody
                  has ruled, and it counts toward the pattern exactly as it did
                  before 0106. Silence is not a veto. */}
              <span className={e.verdict === 'one_off' ? 'chip'
                               : e.verdict === 'reusable' ? 'chip chip-std' : 'chip'}
                    data-testid="reuse-verdict"
                    title={e.contested
                      ? 'two attorneys disagreed; contested evidence is not evidence yet'
                      : undefined}>
                {e.verdict === 'one_off' ? 'one-off'
                  : e.verdict === 'reusable' ? 'reusable' : 'not judged'}
                {e.contested && ' · contested'}
              </span>
              <span className="chip chip-std">{e.state}</span>
              <span className="waiting-age">
                {e.settled_on ? `settled ${e.settled_on}` : 'not settled'}
              </span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Analytics: what the record says about the library (0003's two views) ──
// TWO BLOCKS, TWO FETCHES, ON PURPOSE. An aggregate of what we give away
// under pressure reads like the concessions themselves, so the GRANT is the
// whole control: reviewer, admin and auditor get rows, and a refused role
// gets the database's sentence, verbatim, in the refused block alone. One
// block being refused must never blank its sibling or the pane — a refusal
// is an answer, and it is only THAT question's answer.
function AnalyticsSection() {
  const proposals = usePane(() => API.libraryProposals());
  const rates = usePane(() => API.concessionRates());
  // WHERE THE LIBRARY IS THIN — the third leg, and the one that was missing.
  // `cw.coverage_gap` has been computed since `0002` and served by nothing
  // since; the two views beside it have had a screen since 0081.
  const gaps = usePane(() => API.libraryCoverageGaps());
  // THE CONCESSIONS EACH PROPOSAL RESTS ON. A separate read from the proposals
  // themselves on purpose, the same bargain the two blocks below already
  // strike: one being refused must never blank its sibling. A reader who may
  // see proposals may see these — the read JOINS cw.library_proposal, so the
  // grant is identical by construction rather than by coincidence.
  const evidence = usePane(() => API.proposalEvidence());
  const [openProposal, setOpenProposal] = useState(null);

  return (
    <div className="mt-6">
      {/* ── WHAT WE HAVE NO WORDING FOR ────────────────────────────────
          FIRST, and the order is the argument. The two panels below say what
          the record has LEARNED; this says what it never had. A library
          maintainer's first question is "what are we missing", and answering
          it after two panels of concession analysis puts the answer where
          somebody has to scroll past the lesson to find the hole. */}
      <ThinLibrary pane={gaps} />

      <div className="mt-6">
      <PanelHead title="What the record proposes"
                 sub="Suggestions derived from concessions the record already holds. Open one to see exactly which concessions, and how many of them are in force. Advice — nothing here changes the library." />
      {proposals.status === 'loading' ? <Loading />
        : proposals.status === 'failed' ? <LoadFailed reason={proposals.reason} />
        : proposals.rows.length === 0 ? (
          <Empty kicker="proposals" line="The record proposes nothing."
                 sub="No concession pattern is strong enough to support a
                      suggestion. Every proposal is traceable to the
                      concessions behind it — this system does not assert." />
        ) : (
          <div className="panel">
            {proposals.rows.map((p) => {
              const key = `${p.category_key}/${p.standard_clause_id}`;
              const open = openProposal === key;
              const ev = evidenceFor(
                evidence.status === 'loaded' ? evidence.rows : [], p);
              return (
                <div className="waiting-row" key={key}
                     style={{ alignItems: 'flex-start', display: 'block' }}
                     data-testid="library-proposal">
                  <div className="flex gap-3" style={{ alignItems: 'flex-start' }}
                       {...openableRow(() => setOpenProposal(open ? null : key),
                         open ? `hide the evidence behind ${p.standard_clause_id}`
                              : `show the concessions behind ${p.standard_clause_id}`)}
                       aria-expanded={open}>
                    <div className="min-w-0" style={{ flex: '1 1 auto' }}>
                      <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                        {p.standard_clause_id}
                        <span className="caption"> · {p.category_key}</span>
                      </div>
                      {/* The database's own sentence — the view composes it from
                          the evidence, and rewording it here would be asserting
                          something the record did not say. */}
                      <div className="font-serif italic mt-1"
                           style={{ fontSize: 14, color: 'var(--mute)' }}>
                        {p.proposal}
                      </div>
                    </div>
                    <span className="caption shrink-0">
                      {p.concessions} concession{p.concessions === 1 ? '' : 's'}
                      {p.settlement_rung !== null && p.settlement_rung !== undefined
                        ? ` · settles at rung ${p.settlement_rung}` : ''}
                      {' · '}{open ? 'hide the evidence' : 'show the evidence'}
                    </span>
                  </div>
                  {open && (
                    evidence.status === 'loading'
                      ? <div className="caption p-3">reading the concessions…</div>
                      : <ProposalEvidence ev={ev} proposal={p}
                          failed={evidence.status === 'failed' ? evidence.reason : null} />
                  )}
                </div>
              );
            })}
          </div>
        )}

      </div>

      <div className="mt-6">
        <PanelHead title="Concession rates"
                   sub="Where each standard gives ground, counted from the record." />
        {rates.status === 'loading' ? <Loading />
          : rates.status === 'failed' ? <LoadFailed reason={rates.reason} />
          : rates.rows.length === 0 ? (
            <Empty kicker="rates" line="Nothing has been conceded anywhere."
                   sub="A rate needs concessions to count. None exists — that is
                        the whole answer, not a filter." />
          ) : (
            <div className="panel">
              {rates.rows.map((r) => (
                <div className="waiting-row" key={`${r.category_key}/${r.standard_clause_id}`}
                     data-testid="concession-rate">
                  <div className="min-w-0">
                    <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                      {r.standard_clause_id}
                      <span className="caption"> · {r.category_key}</span>
                    </div>
                    <div className="caption mt-0.5">
                      {r.concessions} conceded · {r.to_vendor_language} to vendor
                      language · {r.required_override} needed an override
                    </div>
                  </div>
                  <span className="caption shrink-0">
                    {/* A rung average over concessions that named no rung is
                        not zero — it is unmeasured, and it says so. */}
                    {r.avg_rung !== null && r.avg_rung !== undefined
                      ? `avg rung ${r.avg_rung}` : 'no rung recorded'}
                    {r.settlement_rung !== null && r.settlement_rung !== undefined
                      ? ` · settles at ${r.settlement_rung}` : ''}
                  </span>
                </div>
              ))}
            </div>
          )}
      </div>
    </div>
  );
}

// ── Where the library is thin ─────────────────────────────────────────────
//
// THE OLDEST UNSERVED COMPUTATION IN THIS SYSTEM. `cw.coverage_gap` was written
// in `0002` — the second migration — with a SELECT grant to all six roles, and
// no endpoint served it until 2026-08-23. Every category the registry defines,
// crossed with both severities, minus everything `cw.selectable_clause` can
// answer today.
//
// A GAP IS A FACT ABOUT THE LIBRARY, NOT A DEFECT IN IT. Some of these are
// deliberate: a category that only ever arises at High needs no Standard
// wording, and Legal may simply have decided this combination never occurs.
// The screen therefore names what is missing and does not tell anybody it is
// wrong — the Product Boundary, exactly: the system's job ends at making the
// gap visible and giving the responsible person a place to act.
//
// AN ALWAYS-INCLUDE CLAUSE DOES NOT CLOSE A GAP, and the first draft of this
// panel said the opposite. `and not s.always_include` in the view means such a
// clause never counts as coverage — it attaches to every agreement whatever the
// severity, so it is not an answer to "which wording do we select for this
// category at this severity". Confirmed against the data: `conf` holds an
// always-include Baseline clause and a High one, has no Standard, and is listed.
// A paragraph that disagreed with the SQL beside it is the defect this
// repository keeps catching — count the sites against the code, never against
// your own sentence.
//
// AND IT IS DERIVED ON EVERY READ, so it cannot go stale. A clause retired this
// morning opens a gap this afternoon; approving wording closes one the moment
// it is approved. There is no job to run and no cache to invalidate, which is
// why this panel never needs a "last computed" date and must not grow one.
function ThinLibrary({ pane }) {
  const filter = useListFilter(pane.rows, {
    view: 'governance:rules',
    fields: ['category_key', 'label', 'severity'],
    facet: 'severity',
  });

  if (pane.status === 'loading') return <Loading />;
  // A REFUSAL IS ITS OWN ANSWER AND MUST NOT BLANK ITS SIBLINGS. The two panels
  // below are a different grant; one block failing takes only that block.
  if (pane.status === 'failed') {
    return (
      <div>
        <PanelHead title="Where the library is thin"
                   sub="This measurement was refused, so nothing below it is being claimed." />
        <LoadFailed reason={pane.reason} />
      </div>
    );
  }

  const high = pane.rows.filter((g) => g.severity === 'High').length;

  return (
    <div>
      <PanelHead
        title="Where the library is thin"
        sub="Every risk category crossed with both severities, minus what your library can answer today. Derived on every read — retire a clause and a gap opens here immediately."
        right={<FilterCount filter={filter} />} />

      {/* NOT A DEFECT LIST, and the screen says so before the rows. A category
          that only ever arises at High needs no Standard wording, and deciding
          that is Legal's. A screen that framed every row as an error would be
          telling somebody their considered choice was a mistake. */}
      <div className="panel-2 p-3 mt-2" data-testid="thin-caveat">
        <div className="tag">what a gap is, and is not</div>
        <div className="text-[12.5px] mt-1.5"
             style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
          A gap means an assembly asking for this category at this severity would
          find no approved wording and record a hard flag instead — never a
          substitution. Some of these are deliberate: a category that only ever
          arises at High needs nothing at Standard. What the system owes is that
          the hole is <strong>visible</strong>; whether it should be filled is
          Legal's. And an always-include clause does <strong>not</strong> close a
          gap: it attaches to every agreement whatever the severity, so it is
          not an answer to "which wording do we select here" — a category holding
          only always-include language still appears.
        </div>
      </div>

      <ListFilter filter={filter} testid="library-gaps"
                  placeholder="search by category" facetLabel="either severity" />

      {pane.rows.length === 0 ? (
        <Empty
          kicker="nothing missing"
          line="Every category has approved wording at both severities."
          sub="A measured zero, derived from the library as it stands right now —
               not a report that has not run. Retire a clause and this stops
               being true the moment you reload." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="gaps" />
      ) : (
        <>
          <div className="caption mb-2" data-testid="thin-summary">
            {pane.rows.length} combination{pane.rows.length === 1 ? '' : 's'} with
            no approved wording{high > 0 && <>, {high} of them at <strong>High</strong></>}.
            An assembly that needs one records a hard flag and blocks.
          </div>
          <div className="panel">
            <table className="ledger w-full">
              <thead>
                <tr><th>category</th><th>key</th><th>severity</th><th>what happens</th></tr>
              </thead>
              <tbody>
                {filter.shown.map((g) => (
                  <tr key={`${g.category_key}/${g.severity}`} data-testid="library-gap">
                    <td>{g.label}</td>
                    <td className="font-mono caption">{g.category_key}</td>
                    <td>
                      {/* HIGH IS THE ONE THAT BLOCKS A BUILD, and it wears the
                          ink every High severity wears elsewhere. Standard is
                          not drawn as calm either — it is simply a different
                          consequence, and the last column says which. */}
                      <span className={`chip ${g.severity === 'High' ? 'chip-pending' : 'chip-std'}`}>
                        {g.severity}
                      </span>
                    </td>
                    <td className="caption">
                      an assembly needing it records a hard flag
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </>
      )}
    </div>
  );
}

// ── Governance: the owner decisions, with the edit that demands a reason ──
// WP-U13's third surface. Reading is everybody's; DECIDING an owner-decision
// row is gated by the settings split (settings-split.test.mjs) and the
// rationale is demanded by the endpoint, not this form.
// ── WHO THE ASSIGNED ATTORNEY IS (0010, reached 2026-08-23) ───────────────
//
// THE SECOND LOCKED DOOR, found by driving the departure arc rather than by
// reading it. `cw.agreement_attorney` was written only by a seed script and a
// test: `0010` granted the read to five roles and the write to the Legal admin,
// and no endpoint ever touched either.
//
// AND IT SITS ON THE PATH OF TWO GATES. Both `cw.sow_override_gate()` and the
// concession settlement fail CLOSED on a deal with no attorney — deliberately,
// by owner decision, so that a deal nobody has staffed does not quietly need one
// fewer approval than it should. With zero rows anywhere, NEITHER COULD EVER BE
// SETTLED BY ANYBODY. A control that can never succeed is the affordance this
// application refuses to draw, so the arc needed this before it needed anything
// else.
//
// LEGAL ADMIN ONLY, because `admin_writes` names that role alone — and this pane
// is the Legal admin's.
//
// REPLACING IS TWO ACTS, AND THE SCHEMA DECIDED THAT, NOT THIS SCREEN. The
// first draft assigned with an upsert; `cw.governance_config_no_update()`
// refuses an in-place update outright, saying why in its own words:
// "Governance configuration changes are remove-plus-add so both sides are
// attributable. An in-place update would bypass the insert actor binding and
// the removal/addition audit pair." So there are two controls, and
// `cw.audit_agreement_attorney` records each.
function AssignedAttorneys() {
  const acts = useActs();
  const pane = usePane(() => API.attorneys());
  const [form, setForm] = useRetainedState('attorney', { agreement_id: '', attorney: '' });
  const [error, setError] = useState(null);
  const filter = useListFilter(pane.rows, {
    view: 'library:attorneys',
    fields: ['agreement_id', 'attorney', 'assigned_by'],
  });

  const remove = (agreement_id) => acts.run(`remove-${agreement_id}`, async () => {
    setError(null);
    const r = await API.removeAttorney({ agreement_id });
    if (!r.ok) { setError(r.reason); return r; }
    pane.reload();
    return r;
  });

  const assign = () => acts.run('assign', async () => {
    setError(null);
    const r = await API.assignAttorney({
      agreement_id: form.agreement_id.trim(), attorney: form.attorney.trim() });
    if (!r.ok) { setError(r.reason); return r; }
    setForm({ agreement_id: '', attorney: '' });
    discardDraft('attorney');
    pane.reload();
    return r;
  });

  return (
    <div className="mt-8 pt-6 border-t hair" data-testid="assigned-attorneys">
      <PanelHead
        title="The assigned attorney"
        sub="One named attorney per deal. A deal with no attorney cannot settle a concession or authorise a departure from a master — deliberately, so a deal nobody has staffed does not quietly need one fewer approval. Replacing one is a removal and an assignment, so both are on the record."
        right={<FilterCount filter={filter} />} />

      {error && (
        <div className="panel-2 p-3 mb-3" data-testid="attorney-error">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>refused</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{error}</div>
        </div>
      )}

      <div className="flex gap-2 flex-wrap mb-3">
        <input className="font-mono" style={{ padding: '6px 9px', minWidth: 150 }}
               placeholder="AG-26-001" aria-label="Which deal"
               data-testid="attorney-deal" value={form.agreement_id}
               onChange={(e) => setForm({ ...form, agreement_id: e.target.value })} />
        <input className="font-mono grow" style={{ padding: '6px 9px', minWidth: 190 }}
               placeholder="name@clausewerk" aria-label="Which attorney"
               data-testid="attorney-person" value={form.attorney}
               onChange={(e) => setForm({ ...form, attorney: e.target.value })} />
        <ActButton className="btn btn-primary" data-testid="attorney-assign"
                   disabled={!form.agreement_id.trim() || !form.attorney.trim()}
                   onClick={assign}>
          assign
        </ActButton>
      </div>

      {pane.status === 'loading' ? <Loading />
        : pane.status === 'failed' ? <LoadFailed reason={pane.reason} />
        : pane.rows.length === 0 ? (
          <Empty
            kicker="attorneys"
            line="No deal has an assigned attorney."
            sub="Until one does, no concession on it can be settled and no departure
                 from a master can be authorised — both gates fail closed on
                 purpose, and this is where the gap is closed." />
        ) : (
          <>
            <ListFilter filter={filter} testid="attorneys"
                        placeholder="deal or attorney" facetLabel="" />
            {filter.shown.length === 0
              ? <NoMatch kicker="attorneys" noun="deal" />
              : (
                <div className="panel">
                  <table className="ledger">
                    <thead><tr><th>Deal</th><th>Attorney</th><th>Assigned</th>
                               <th style={{ textAlign: 'right' }}>Remove</th></tr></thead>
                    <tbody>
                      {filter.shown.map((a) => (
                        <tr key={a.agreement_id}>
                          <td className="mono">{a.agreement_id}</td>
                          <td className="mono">{a.attorney}</td>
                          <td className="mono">
                            {String(a.assigned_on ?? '').slice(0, 10)}
                            <span className="caption"> by {a.assigned_by}</span>
                          </td>
                          <td style={{ textAlign: 'right' }}>
                            <ActButton className="btn btn-sm"
                                       data-testid={`attorney-remove-${a.agreement_id}`}
                                       onClick={() => remove(a.agreement_id)}>
                              remove
                            </ActButton>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
          </>
        )}
    </div>
  );
}

// ── Who must sign a deal off (0010, on screen 2026-08-24) ────────────────
//
// THE THIRD LOCKED DOOR IN 0010, and the one that made the other two answer the
// wrong question. `cw.required_approver` is CLA §4's "who is required for which
// contracts is configuration, not a fixed list", built entire — the seven
// bodies, the actor binding, remove-plus-add, the audit pair, the grants, and a
// read to five roles so the person waiting can see who they are waiting on —
// and named by no endpoint. So every deal in this system had ZERO required
// approvers.
//
// ZERO IS NOT AN EMPTY LIST. IT IS A GATE THAT ALWAYS OPENS.
// `cw.concession_settlement_gate()` and `cw.sow_override_gate()` both work out
// who has not signed yet by joining this table, so the concession settlement
// built on 2026-08-24 and the departure arc built the day before were each
// checking a governance list nobody could put a name on.
//
// THE SEVEN BODIES ARE THE TABLE'S OWN CHECK, read off 0010 rather than
// invented here. A body this screen offered that the schema refused would be a
// control that lies; one the schema allows and this hides would be a capability
// nobody can reach, which is the failure this whole cycle is about.
const APPROVER_BODIES = ['executive', 'management', 'iso', 'privacy',
                         'compliance', 'risk', 'other'];

function RequiredApprovers() {
  const acts = useActs();
  const pane = usePane(() => API.requiredApprovers());
  const [form, setForm] = useRetainedState('required-approver',
    { agreement_id: '', body: 'executive', label: '', must_approve: '' });
  const [error, setError] = useState(null);
  const filter = useListFilter(pane.rows, {
    view: 'library:required-approvers',
    fields: ['agreement_id', 'body', 'label', 'approver', 'added_by'],
    facet: 'body',
  });

  const remove = (row) => acts.run(`remove-${row.required_approver_id}`, async () => {
    setError(null);
    const r = await API.removeRequiredApprover({
      required_approver_id: row.required_approver_id });
    if (!r.ok) { setError(r.reason); return r; }
    pane.reload();
    return r;
  });

  const add = () => acts.run('add', async () => {
    setError(null);
    // `must_approve`, not `approver`. The doorway refuses a field called
    // `approver` outright — on a concession approval that name IS the actor and
    // comes from the connection. Here a Legal admin is naming somebody else,
    // which is the whole content of the act.
    const r = await API.addRequiredApprover({
      agreement_id: form.agreement_id.trim(),
      approver_body: form.body,
      label: form.label.trim(),
      must_approve: form.must_approve.trim(),
    });
    if (!r.ok) { setError(r.reason); return r; }
    setForm({ agreement_id: '', body: 'executive', label: '', must_approve: '' });
    discardDraft('required-approver');
    pane.reload();
    return r;
  });

  const ready = form.agreement_id.trim() && form.label.trim()
                && form.must_approve.trim();

  return (
    <div className="mt-8 pt-6 border-t hair" data-testid="required-approvers">
      <PanelHead
        title="Who must sign off"
        sub="Named sign-offs this company requires on a deal, beyond the requester and the attorney. Every one of them has to have approved before a concession can be settled or a departure from a master authorised — so a deal with none needs two signatures, not none. Changing one is a removal and an addition, both on the record, because one row fewer is one signature fewer."
        right={<FilterCount filter={filter} />} />

      {error && (
        <div className="panel-2 p-3 mb-3" data-testid="required-approver-error">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>refused</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{error}</div>
        </div>
      )}

      <div className="flex gap-2 flex-wrap mb-3">
        <input className="font-mono" style={{ padding: '6px 9px', minWidth: 150 }}
               placeholder="AG-26-001" aria-label="Which deal"
               data-testid="required-deal" value={form.agreement_id}
               onChange={(e) => setForm({ ...form, agreement_id: e.target.value })} />
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="Which body" data-testid="required-body"
                value={form.body}
                onChange={(e) => setForm({ ...form, body: e.target.value })}>
          {APPROVER_BODIES.map((b) => <option key={b} value={b}>{b}</option>)}
        </select>
        <input style={{ padding: '6px 9px', minWidth: 170 }}
               placeholder="what they are signing for" aria-label="What this sign-off is for"
               data-testid="required-label" value={form.label}
               onChange={(e) => setForm({ ...form, label: e.target.value })} />
        <input className="font-mono grow" style={{ padding: '6px 9px', minWidth: 190 }}
               placeholder="name@clausewerk" aria-label="Who must approve"
               data-testid="required-person" value={form.must_approve}
               onChange={(e) => setForm({ ...form, must_approve: e.target.value })} />
        <ActButton className="btn btn-primary" data-testid="required-add"
                   disabled={!ready} onClick={add}>
          add
        </ActButton>
      </div>

      {/* THE PRODUCT BOUNDARY, SAID PLAINLY. 0010's own comment: the system
          cannot tell a person from a team inbox by looking at a string, and
          pretending otherwise would be this product deciding something that
          belongs to the people using it. What it CAN do is require a value and
          record who configured it, and both are true here. */}
      <div className="caption mb-3" style={{ lineHeight: 1.6 }}>
        Name a person, never a team inbox — this system cannot tell the two
        apart by looking, and whoever is named has to sign in as themselves to
        approve. Your own name goes on the configuration either way.
      </div>

      {pane.status === 'loading' ? <Loading />
        : pane.status === 'failed' ? <LoadFailed reason={pane.reason} />
        : pane.rows.length === 0 ? (
          <Empty
            kicker="required approvers"
            line="No deal requires any sign-off beyond the requester and the attorney."
            sub="That is a real answer and not an empty screen: with no row here,
                 both gates that read this list find nothing outstanding and
                 pass. Add one where this company genuinely requires it." />
        ) : (
          <>
            <ListFilter filter={filter} testid="required-approvers"
                        placeholder="deal, person or what it is for"
                        facetLabel="body" />
            {filter.shown.length === 0
              ? <NoMatch kicker="required approvers" noun="sign-off" />
              : (
                <div className="panel">
                  <table className="ledger">
                    <thead><tr><th>Deal</th><th>Body</th><th>For</th>
                               <th>Must approve</th><th>Added</th>
                               <th style={{ textAlign: 'right' }}>Remove</th></tr></thead>
                    <tbody>
                      {filter.shown.map((a) => (
                        <tr key={a.required_approver_id}>
                          <td className="mono">{a.agreement_id}</td>
                          <td><span className="chip chip-std">{a.body}</span></td>
                          <td>{a.label}</td>
                          <td className="mono">{a.approver}</td>
                          <td className="mono">
                            {String(a.added_on ?? '').slice(0, 10)}
                            <span className="caption"> by {a.added_by}</span>
                          </td>
                          <td style={{ textAlign: 'right' }}>
                            <ActButton className="btn btn-sm"
                                       data-testid={`required-remove-${a.required_approver_id}`}
                                       onClick={() => remove(a)}>
                              remove
                            </ActButton>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
          </>
        )}
    </div>
  );
}

// ── Risk categories, which nothing could create ───────────────────────────
//
// WHAT WAS MISSING. `cw.category` is the spine of this product: **thirteen
// foreign keys across ten migrations** point at it, and every clause, ladder,
// conflict rule, negotiation position, concession and recorded build decision
// keys to one. `POST /categories` has been served since `0002` and `api.jsx`
// has offered `addCategory` for months, with no control anywhere — it sat on
// the `REACHED_BY_NO_SCREEN` ledger.
//
// So the list of risks this company can write contracts about was fixed at
// whatever was first loaded, and adding one needed a database administrator.
// A Legal admin owns what the company's contract language IS and could not
// name a new kind of risk.
//
// AND THE DESK ALREADY PROMISED IT. `home.jsx`'s sentence for this area reads
// "Categories, owner decisions, and the switches behind them" — two of three.
//
// ── EVERYTHING HERE IS PERMANENT, WHICH THE FORM HAS TO SAY ───────────────
//
// `0002` grants `insert, update` on `cw.category` and **no delete**, and the
// doorway offers neither an update nor a delete. So a category, once saved,
// is in the record for good: it cannot be renamed through this application
// and cannot be removed at all. A form that collects three fields without
// saying that invites a typo nobody can repair.
//
// ── AND THE TWO-LETTER CODE IS THE SHARP ONE ──────────────────────────────
//
// `short` is `unique` and `check (short ~ '^[A-Z]{2}$')`, and `0002`'s own
// comment records the defect that made it unique: `AC` was ambiguous across
// **Acceptance** and **Anti-Corruption**, so a clause ID stopped identifying
// exactly one category. That code is embedded in every clause ID in the
// category.
//
// So this screen SHOWS WHICH CODES ARE TAKEN rather than letting somebody
// discover a collision by being refused. `label` and `key` are unique too and
// are checked the same way. None of this replaces the database's constraints —
// they are what actually decide — it replaces a refusal a person cannot act on
// with a sentence before they commit.
function CategorySection({ categories, onError, onChanged }) {
  const [open, setOpen] = useState(false);
  const [key, setKey] = useState('');
  const [label, setLabel] = useState('');
  const [short, setShort] = useState('');
  const [busy, setBusy] = useState(false);

  const rows = categories.rows || [];
  const takenKeys = new Set(rows.map((c) => String(c.key).toLowerCase()));
  const takenLabels = new Set(rows.map((c) => String(c.label).toLowerCase()));
  const takenShorts = new Set(rows.map((c) => String(c.short).toUpperCase()));

  const k = key.trim().toLowerCase();
  const l = label.trim();
  const s = short.trim().toUpperCase();

  // EACH CLASH NAMED SEPARATELY. "That is taken" over three fields tells
  // somebody to change the wrong one.
  const keyClash = k !== '' && takenKeys.has(k);
  const labelClash = l !== '' && takenLabels.has(l.toLowerCase());
  const shortClash = s !== '' && takenShorts.has(s);
  const shortMalformed = s !== '' && !/^[A-Z]{2}$/.test(s);

  const ready = k && l && s && !keyClash && !labelClash && !shortClash
                && !shortMalformed && !busy;

  return (
    <div className="mt-8">
      <PanelHead title="Risk categories"
                 sub="What this company writes contracts about. Every clause, ladder, rule, contested point and concession belongs to one of these." />

      {categories.status === 'failed' ? (
        <LoadFailed reason={categories.reason} />
      ) : rows.length === 0 ? (
        <Empty kicker="categories"
               line="No risk category is defined."
               sub="Nothing can be written about a risk this list does not name — a clause, a ladder and a contested point all belong to a category." />
      ) : (
        <div className="panel">
          {rows.map((c) => (
            <div className="waiting-row" key={c.key} data-testid="category-row">
              <div className="min-w-0">
                <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                  {c.label}
                </div>
                <div className="caption font-mono mt-0.5">{c.key}</div>
              </div>
              {/* THE CODE, DRAWN ON EVERY ROW. It is what a person composing a
                  new one has to avoid, and it is what appears inside every
                  clause ID in the category. */}
              <span className="chip chip-std font-mono">{c.short}</span>
            </div>
          ))}
        </div>
      )}

      {!open ? (
        <button className="btn btn-sm mt-3" data-testid="add-category-open"
                onClick={() => setOpen(true)}>
          add a category
        </button>
      ) : (
        <div className="panel-2 p-3 mt-3" data-testid="add-category-form">
          <div className="section-label">A new risk category</div>

          {/* SAID BEFORE THE FIELDS, NOT AFTER. There is no undo for any of
              this: no rename through this application and no delete at all. */}
          <div className="panel p-3 mt-2" style={{ borderColor: 'var(--danger)' }}>
            <div className="tag" style={{ color: 'var(--danger)' }}>permanent</div>
            <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
              A category cannot be deleted and cannot be renamed here. Clauses,
              ladders, rules and settled points all point at it, so it stays in
              the record for good. Check the spelling before you save it.
            </div>
          </div>

          <label className="section-label mt-3">Key</label>
          <input className="mt-1.5 w-full font-mono" aria-label="Key"
                 data-testid="category-key" placeholder="data, liab, ipr…"
                 value={key} onChange={(e) => setKey(e.target.value)} />
          <div className="caption mt-1">
            {keyClash
              ? <span style={{ color: 'var(--danger)' }}>
                  <span className="font-mono">{k}</span> is already a category.
                </span>
              : 'How the rest of the system refers to it. Short, lowercase, and never seen by a supplier.'}
          </div>

          <label className="section-label mt-3">Label</label>
          <input className="mt-1.5 w-full" aria-label="Label"
                 data-testid="category-label" placeholder="Data Protection"
                 value={label} onChange={(e) => setLabel(e.target.value)} />
          <div className="caption mt-1">
            {labelClash
              ? <span style={{ color: 'var(--danger)' }}>
                  Another category already uses that label, and a label names exactly one.
                </span>
              : 'What a person reads, and the exact string an intake manifest has to match.'}
          </div>

          <label className="section-label mt-3">Two-letter code</label>
          <input className="mt-1.5 font-mono" aria-label="Two-letter code"
                 data-testid="category-short" placeholder="DP" maxLength={2}
                 style={{ width: 90, textTransform: 'uppercase' }}
                 value={short} onChange={(e) => setShort(e.target.value)} />
          <div className="caption mt-1">
            {shortMalformed
              ? <span style={{ color: 'var(--danger)' }}>
                  Exactly two letters, A–Z.
                </span>
              : shortClash
                ? <span style={{ color: 'var(--danger)' }}>
                    <span className="font-mono">{s}</span> already belongs to{' '}
                    {rows.find((c) => String(c.short).toUpperCase() === s).label}.
                    Two categories sharing a code is what made a clause ID stop
                    naming one category.
                  </span>
                : 'It is embedded in every clause ID in this category, so it must belong to this one alone.'}
          </div>

          {rows.length > 0 && (
            <div className="caption mt-2" data-testid="codes-taken">
              Already taken:{' '}
              <span className="font-mono">
                {rows.map((c) => String(c.short).toUpperCase()).sort().join(' · ')}
              </span>
            </div>
          )}

          <div className="flex gap-2 mt-4">
            <button className="btn" onClick={() => setOpen(false)}>cancel</button>
            <ActButton className="btn btn-primary" disabled={!ready}
              data-testid="add-category-submit"
              onClick={async () => {
                setBusy(true); onError(null);
                const r = await API.addCategory({ key: k, label: l, short: s });
                setBusy(false);
                if (!r.ok) { onError(r.reason); return; }
                setOpen(false); setKey(''); setLabel(''); setShort('');
                onChanged();
              }}>
              {busy ? 'saving…' : '✓ add this category'}
            </ActButton>
          </div>
        </div>
      )}
    </div>
  );
}

function GovernancePane() {
  const pane = usePane(() => API.settings());
  // THE CATEGORIES, READ BESIDE THE SETTINGS. Kept OUT of the early returns
  // below on purpose: the owner decisions are this pane's subject and must
  // render whatever the category read does. The section says what happened to
  // it instead, which is why it takes the pane object rather than the rows.
  const categories = usePane(() => API.categories());
  const [editing, setEditing] = useState(null); // key
  const [value, setValue] = useState('');
  const [rationale, setRationale] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  const decisions = pane.rows.filter((s) => s.is_owner_decision);
  const operational = pane.rows.filter((s) => !s.is_owner_decision);

  return (
    <div>
      <PaneHead title="Owner decisions"
                 sub="What the owner settled, held as data the schema enforces." />
      {decisions.length === 0 ? (
        <Empty kicker="governance" line="No owner decision is recorded." />
      ) : (
        <div className="panel">
          {decisions.map((s) => (
            <div className="waiting-row setting-row" key={s.key} style={{ alignItems: 'flex-start' }}>
              <div className="min-w-0">
                <div className="text-[13px] font-mono setting-key" style={{ color: 'var(--ink)' }}>
                  {s.key} = {s.value}
                </div>
                <div className="caption mt-0.5" style={{ whiteSpace: 'pre-wrap' }}>
                  {s.rationale || 'No rationale recorded.'}
                </div>
                {editing === s.key && (
                  <div className="panel p-3 mt-2" data-testid="decide-form">
                    <div className="flex gap-3">
                      <div>
                        <label className="section-label">New value</label>
                        <input aria-label="New value" className="mt-1.5 font-mono" value={value}
                               onChange={(e) => setValue(e.target.value)} />
                      </div>
                      <div className="grow">
                        <label className="section-label">Why it changes</label>
                        <input aria-label="Why it changes" className="mt-1.5 w-full" value={rationale}
                               placeholder="required — a decision with no reasoning cannot be reviewed"
                               onChange={(e) => setRationale(e.target.value)} />
                      </div>
                    </div>
                    <div className="flex gap-2 mt-3">
                      <button className="btn" onClick={() => setEditing(null)}>cancel</button>
                      <ActButton className="btn btn-primary" data-testid="confirm-decide"
                              disabled={busy || !value.trim() || !rationale.trim()}
                              onClick={async () => {
                                setBusy(true); setError(null);
                                const r = await API.decideSetting({
                                  key: s.key, value: value.trim(),
                                  rationale: rationale.trim(),
                                });
                                setBusy(false);
                                if (!r.ok) { setError(r.reason); return; }
                                setEditing(null); pane.reload();
                              }}>
                        {busy ? 'recording…' : '✓ record the decision'}
                      </ActButton>
                    </div>
                    <ActError error={error} />
                  </div>
                )}
              </div>
              <div className="flex items-center gap-3 shrink-0">
                <span className={`chip ${s.decided ? 'chip-ok' : 'chip-pending'}`}>
                  {s.decided ? `decided by ${s.decided_by}` : 'undecided'}
                </span>
                <button className="btn btn-sm" data-testid="open-decide"
                        onClick={() => {
                          setEditing(editing === s.key ? null : s.key);
                          setValue(s.value ?? ''); setRationale(''); setError(null);
                        }}>
                  decide…
                </button>
              </div>
            </div>
          ))}
        </div>
      )}

      {operational.length > 0 && (
        <div className="caption mt-4">
          {operational.length} operational settings are the Administrator's and
          live in the console, not here — the split is the control, both ways.
        </div>
      )}
      <CategorySection categories={categories}
                       onError={setError} onChanged={categories.reload} />
      <AssignedAttorneys />
      <RequiredApprovers />
      <ComplianceConcernsSection categories={categories} />
    </div>
  );
}

// ── The declared compliance concerns (0120) ────────────────────────────────
// The register the compliance check reads against: what regulation or
// obligation this company cares about, per clause category (or every
// category), in Legal's own words. CUSTOMER CONTENT — the system knows no
// law of its own, and a category with no declared concern is answered as
// exactly that. Declared and retired, never edited: a changed wording is a
// new declaration, so what any past check was read against stays answerable.
function ComplianceConcernsSection({ categories }) {
  const pane = usePane(() => API.complianceConcerns());
  const [declaring, setDeclaring] = useState(false);
  const [category, setCategory] = useState('');
  const [title, setTitle] = useState('');
  const [concern, setConcern] = useState('');
  const [source, setSource] = useState('');
  const [retiring, setRetiring] = useState(null); // concern_id
  const [reason, setReason] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  const live = pane.rows.filter((c) => !c.retired_at);
  const retired = pane.rows.filter((c) => c.retired_at);

  const act = async (call) => {
    setBusy(true); setError(null);
    const r = await call();
    setBusy(false);
    if (!r.ok) { setError(r.reason); return; }
    setDeclaring(false); setRetiring(null);
    setCategory(''); setTitle(''); setConcern(''); setSource('');
    setReason('');
    pane.reload();
  };

  return (
    <div className="mt-6" data-testid="compliance-concerns">
      <PanelHead
        title="Declared compliance concerns"
        sub="What this company has declared it legally cares about, per clause category. The compliance check on the deal room's workbench reads a changed paragraph against exactly these — a category with nothing declared is answered as exactly that, never as a clean bill."
        right={!declaring && (
          <ActButton className="btn btn-sm" data-testid="declare-concern"
                     onClick={() => setDeclaring(true)}>
            + declare a concern
          </ActButton>
        )} />

      {declaring && (
        <div className="panel p-3 mb-3" data-testid="declare-form">
          <div className="flex gap-3 flex-wrap">
            <div>
              <label className="section-label">Category</label>
              <select className="mt-1.5 font-mono" value={category}
                      aria-label="Which clause category the concern attaches to"
                      onChange={(e) => setCategory(e.target.value)}>
                <option value="">every category</option>
                {(categories.rows ?? []).map((c) => (
                  <option key={c.key} value={c.key}>{c.key}</option>
                ))}
              </select>
            </div>
            <div className="grow" style={{ minWidth: 180 }}>
              <label className="section-label">Title</label>
              <input aria-label="The concern's title" className="mt-1.5 w-full"
                     value={title} onChange={(e) => setTitle(e.target.value)} />
            </div>
            <div style={{ minWidth: 160 }}>
              <label className="section-label">Source (optional)</label>
              <input aria-label="The instrument it comes from"
                     className="mt-1.5 w-full" placeholder="e.g. GDPR art. 28"
                     value={source} onChange={(e) => setSource(e.target.value)} />
            </div>
          </div>
          <div className="mt-3">
            <label className="section-label">The concern, in your words</label>
            <textarea aria-label="The concern, in your words"
                      className="mt-1.5 w-full" rows={3} value={concern}
                      onChange={(e) => setConcern(e.target.value)} />
          </div>
          <div className="caption mt-2">
            A declaration is never edited — a changed wording is a new
            declaration — so what any past check was read against stays
            answerable.
          </div>
          <div className="flex gap-2 mt-3">
            <button className="btn" onClick={() => setDeclaring(false)}>cancel</button>
            <ActButton className="btn btn-primary" data-testid="confirm-declare"
                       disabled={busy || !title.trim() || !concern.trim()}
                       onClick={() => act(() => API.declareComplianceConcern({
                         category_key: category || null,
                         title: title.trim(),
                         concern: concern.trim(),
                         source: source.trim() || null,
                       }))}>
              {busy ? 'declaring…' : '✓ declare it'}
            </ActButton>
          </div>
        </div>
      )}

      <ActError error={error} />

      {live.length === 0 ? (
        <Empty kicker="compliance"
               line="Nothing is declared."
               sub="Until Legal declares what this company cares about, the compliance check has nothing to read against, and it says so rather than inventing law of its own." />
      ) : (
        <div className="panel">
          {live.map((c) => (
            <div className="waiting-row" key={c.concern_id}
                 style={{ alignItems: 'flex-start' }} data-testid="concern-row">
              <div className="min-w-0">
                <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                  {c.title}
                  <span className="caption ml-2 font-mono">
                    {c.category_key || 'every category'}
                    {c.source ? ` · ${c.source}` : ''}
                  </span>
                </div>
                <div className="caption mt-0.5" style={{ whiteSpace: 'pre-wrap' }}>
                  {c.concern}
                </div>
                <div className="caption mt-0.5 font-mono">
                  declared by {c.declared_by}
                </div>
                {retiring === c.concern_id && (
                  <div className="panel p-3 mt-2" data-testid="retire-form">
                    <label className="section-label">Why it retires</label>
                    <input aria-label="Why this concern retires"
                           className="mt-1.5 w-full" value={reason}
                           placeholder="required — a retirement with no reason cannot be reviewed"
                           onChange={(e) => setReason(e.target.value)} />
                    <div className="flex gap-2 mt-3">
                      <button className="btn" onClick={() => setRetiring(null)}>cancel</button>
                      <ActButton className="btn btn-primary" data-testid="confirm-retire"
                                 disabled={busy || !reason.trim()}
                                 onClick={() => act(() => API.retireComplianceConcern({
                                   concern_id: c.concern_id,
                                   reason: reason.trim(),
                                 }))}>
                        {busy ? 'retiring…' : '✓ retire it'}
                      </ActButton>
                    </div>
                  </div>
                )}
              </div>
              {retiring !== c.concern_id && (
                <ActButton className="btn btn-sm" data-testid="retire-concern"
                           onClick={() => { setError(null); setRetiring(c.concern_id); }}>
                  retire
                </ActButton>
              )}
            </div>
          ))}
        </div>
      )}

      {retired.length > 0 && (
        <div className="mt-3">
          <div className="section-label mb-1">Retired</div>
          {retired.map((c) => (
            <div className="caption py-1 border-b hair" key={c.concern_id}>
              <span style={{ textDecoration: 'line-through' }}>{c.title}</span>
              {' '}· retired by {c.retired_by}: {c.retire_reason}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Holds & retention: the Legal admin's half of disposal ─────────────────
// Opening a hold is Legal's act (the reviewer desk has it too); RELEASING one
// is the Legal admin's alone, and it is here. Destruction, redaction and the
// purge are the Administrator's and live in the console — a greyed-out
// destroy button here would say "you could, but not now"; the truth is that
// it is somebody else's act entirely.
function RetentionPane() {
  const pane = usePane(() => API.holds());
  const disposal = usePane(() => API.redactionState());
  const [releasing, setReleasing] = useState(null); // hold_id
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  const open = pane.rows.filter((h) => !h.released_on);
  const released = pane.rows.filter((h) => h.released_on);

  return (
    <div>
      <PaneHead title="Holds & retention"
                 sub="A hold blocks disposal while a matter is live. Releasing it is your act, on the record." />

      {open.length === 0 ? (
        <Empty kicker="holds" line="No hold is open."
               sub="A hold stops destruction for a dispute. Open ones appear here
                    with their matter, and releasing them is recorded." />
      ) : (
        <div className="panel">
          {open.map((h) => (
            <div className="waiting-row" key={h.hold_id} style={{ alignItems: 'flex-start' }}>
              <div className="min-w-0">
                <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                  {h.agreement_id}
                  <span className="caption"> · {h.matter_ref} · opened by {h.opened_by} on {h.opened_on}</span>
                </div>
                {releasing === h.hold_id && (
                  <div className="panel p-3 mt-2" data-testid="release-form">
                    <div className="caption">
                      Releasing this hold lets the retention clock reach
                      {' '}{h.agreement_id} again. The release is recorded under
                      your name, and a released hold can never reopen — a new
                      matter is a new hold.
                    </div>
                    <div className="flex gap-2 mt-3">
                      <button className="btn" onClick={() => setReleasing(null)}>← back</button>
                      <ActButton className="btn btn-primary" disabled={busy}
                              data-testid="confirm-release"
                              onClick={async () => {
                                setBusy(true); setError(null);
                                const r = await API.releaseHold({ hold_id: h.hold_id });
                                setBusy(false);
                                if (!r.ok) { setError(r.reason); return; }
                                setReleasing(null); pane.reload();
                              }}>
                        {busy ? 'releasing…' : '✓ release this hold'}
                      </ActButton>
                    </div>
                    <ActError error={error} />
                  </div>
                )}
              </div>
              <div className="flex items-center gap-3 shrink-0">
                <span className="chip chip-high">open</span>
                <button className="btn btn-sm" data-testid="open-release"
                        onClick={() => { setReleasing(
                          releasing === h.hold_id ? null : h.hold_id);
                          setError(null); }}>
                  release…
                </button>
              </div>
            </div>
          ))}
        </div>
      )}

      {released.length > 0 && (
        <div className="caption mt-3" data-testid="released-holds">
          {released.length} released hold{released.length === 1 ? '' : 's'} on the
          record: {released.map((h) =>
            `${h.agreement_id} (${h.matter_ref}, released by ${h.released_by} ${h.released_on})`
          ).join(' · ')}
        </div>
      )}

      <div className="mt-6">
        <PanelHead title="Disposal, as it stands"
                   sub="Destroy, then redact, then purge — each a separate recorded act, none of them yours." />
        {disposal.status === 'loading' ? <Loading /> :
         disposal.status === 'failed' ? <LoadFailed reason={disposal.reason} /> :
         disposal.rows.length === 0 ? (
          <Empty kicker="disposal" line="Nothing is in the disposal pipeline." />
        ) : (
          <div className="panel">
            {disposal.rows.map((d) => (
              <div className="waiting-row" key={d.agreement_id}>
                <div className="min-w-0">
                  <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                    {d.agreement_id}
                    <span className="caption">
                      {d.retention_until ? ` · retention until ${d.retention_until}` : ''}
                      {d.destroyed_on ? ` · destroyed ${d.destroyed_on} by ${d.destroyed_by}` : ''}
                      {d.redacted_on ? ` · redacted ${d.redacted_on} by ${d.redacted_by}` : ''}
                      {d.purged_on ? ` · purged ${d.purged_on} by ${d.purged_by}` : ''}
                    </span>
                  </div>
                  {d.external_bytes_pending && (
                    // THE HONEST CAVEAT, rendered rather than smoothed over.
                    <div className="caption mt-0.5" data-testid="external-bytes">
                      <strong>Bytes may survive outside.</strong> Redaction severed
                      this system's link to the stored file; it does not reach into
                      an external store. Only the purge closes that.
                    </div>
                  )}
                </div>
                <span className={`chip ${d.state === 'live' ? 'chip-ok'
                  : d.state === 'purged' ? 'chip-err' : 'chip-pending'}`}>
                  {d.state}
                </span>
              </div>
            ))}
          </div>
        )}
        <div className="caption mt-3">
          Destruction under retention, redaction and the purge belong to the
          Administrator (owner decisions U9 and U12) and are performed in the
          console. This page is Legal's window onto where each record stands.
        </div>
      </div>
    </div>
  );
}
