// The negotiation surfaces (NG-1 … NG-4).
//
// FOUR ROLES, ONE SET OF COMPONENTS, THREE COMPOSITIONS. The requester works
// their own deals, the Legal reviewer and the Legal admin work every deal from
// the same desk, and the auditor reads a negotiation inside the record and acts
// on nothing. What differs between them is the ORDER things appear in and which
// acts are offered — never the shape of a position row, because this product
// has already paid once for four near-identical status tiles drifting apart.
//
// WHAT THE SCREEN DOES NOT DO, and this is the load-bearing half:
//
//   · It never checks a permission. The round sequence (cw.round_is_next), the
//     concession floor, who may approve a concession, and who may write against
//     which deal are all the schema's sentences. This offers the act and shows
//     the refusal in the database's own words — a pre-flight in JavaScript is a
//     second copy of a rule, and two copies stop agreeing.
//
//   · It never fetches broadly and filters for permission. Every read here
//     takes no parameter: the scoping is "these deals, this person" and it
//     arrives on the connection. Filtering by negotiation_id on screen is
//     narrowing what the rule ALREADY returned, which is a different act
//     entirely from asking for somebody else's rows and hiding them.
//
//   · It never dresses a position state as a status mark. A position is
//     opened, held, conceded, withdrawn, escalated or settled — six domain
//     states, not the five in common.jsx's vocabulary. They render as plain
//     words with the rung beside them. The one thing here that IS a status
//     mark is a concession's approval state, because that is a thing being in
//     force or not yet in force.

const { useState } = React;

// ── Reading the record ────────────────────────────────────────────────────
// Every negotiation surface needs the same six answers, so they are fetched
// once, here, and handed down. Six calls rather than one wide one: each is its
// own rule, and an endpoint that returned "everything about a negotiation"
// would be one rule standing in for six.
function useNegotiationRecord() {
  const negotiations = usePane(() => API.negotiations());
  const rounds       = usePane(() => API.negotiationRounds());
  const positions    = usePane(() => API.positions());
  const movements    = usePane(() => API.positionMovements());
  const revivals     = usePane(() => API.revivals());
  const drift        = usePane(() => API.renewalDrift());
  const concessions  = usePane(() => API.concessions());
  // The deal list, for counterparty names. Fetched HERE rather than in each
  // pane so the two compositions cannot end up asking different questions
  // about the same deals.
  const deals        = usePane(() => API.deals());

  // The authored playbook beside each open position (0081). Fetched once
  // like everything above, and DELIBERATELY kept out of the loading and
  // failure aggregation below: these moves are advice — they gate nothing —
  // and a refusal to show advice must not take the record down with it. The
  // components that render them say what happened instead.
  const positionMoves = usePane(() => API.positionMoves());

  // THE BUILDS, so an issued round can name the one that produced the paper.
  // `0011` carries a constraint written for exactly this — `only_our_rounds
  // _have_a_run` — and nothing has ever been able to set the column it
  // protects. Kept OUT of the loading and failure aggregation below, for the
  // same reason the playbook is: naming the build is optional on the record,
  // so a refusal to list builds must not take the whole negotiation down. The
  // form says what happened instead and still records the round.
  const runs = usePane(() => API.runs());

  // THE LADDERS, so a concession can be recorded against the retreat path
  // rather than against a number somebody typed. Kept OUT of the loading and
  // failure aggregation for the same reason the builds are: naming a rung is
  // optional on the record, so a refusal to list ladders must not take the
  // whole negotiation down. The form says what happened instead.
  const ladders = usePane(() => API.ladders());

  const panes = [negotiations, rounds, positions, movements, revivals, drift,
                 concessions, deals];
  return {
    negotiations, rounds, positions, movements, revivals, drift, concessions,
    deals, positionMoves, runs, ladders,
    loading: panes.some((p) => p.status === 'loading'),
    // The FIRST failure, with its sentence. A screen that renders half a
    // negotiation because one read refused is a screen quietly deciding which
    // half of a record is worth showing.
    failed: panes.find((p) => p.status === 'failed') || null,
    reloadAll: () => { panes.forEach((p) => p.reload()); positionMoves.reload(); runs.reload(); ladders.reload(); },
  };
}

// ── One negotiation, in a list ────────────────────────────────────────────
function NegotiationRow({ negotiation, deals, positions, rounds }) {
  const deal = deals.find((d) => d.agreement_id === negotiation.agreement_id);
  const mine = positions.filter((p) => p.negotiation_id === negotiation.negotiation_id);
  const open = mine.filter((p) => !['settled', 'withdrawn'].includes(p.state));
  const escalated = mine.filter((p) => p.state === 'escalated');
  const lastRound = rounds
    .filter((r) => r.negotiation_id === negotiation.negotiation_id)
    .reduce((n, r) => Math.max(n, r.round_no), 0);

  return {
    key: String(negotiation.negotiation_id),
    title: deal ? deal.counterparty : negotiation.agreement_id,
    sub: `${negotiation.agreement_id} · ${negotiation.paper === 'ours' ? 'our paper' : 'their paper'}`
         + ` · round ${lastRound || 'none yet'}`,
    // Oldest first is the desk's rule, and the date a negotiation opened is
    // the honest thing to age it by until something is waiting on somebody.
    at: negotiation.opened_on,
    chips: (
      <span className="flex items-center gap-2">
        {negotiation.renews_agreement_id && (
          <span className="chip chip-std" title={`renews ${negotiation.renews_agreement_id}`}>renewal</span>
        )}
        {escalated.length > 0 && (
          <span className="chip chip-pending">{escalated.length} with Legal</span>
        )}
        {/* WHETHER THE NEGOTIATION ITSELF IS STILL RUNNING. Thirty-six rows,
            TWENTY-FOUR of them closed, and not one said so — a requester could
            not tell a live negotiation from one that finished, and the age
            beside it ("43d") reads as a wait either way.

            Named from the row rather than derived from "not open", so a third
            state added to cw.negotiation.state shows up here as itself instead
            of being folded silently into one of these two. The vocabulary is
            two today; that is a fact about today. */}
        {negotiation.state === 'open'
          ? <span className="chip chip-std">open</span>
          : <span className="chip chip-gone">{negotiation.state}</span>}
        {/* SAYS WHAT IT COUNTS. This read "0 open" directly beside the chip
            above, so one row carried two different senses of the same word:
            the negotiation's state, and how many POINTS are still contested. */}
        <span className="chip chip-std">{open.length} points open</span>
      </span>
    ),
  };
}

// ── Recording a round whose document we do not hold ───────────────────────
//
// WHAT WAS MISSING, AND IT WAS HALF THE CONVERSATION. `cw.negotiation_round`
// (`0011`) has always allowed two directions — `issued`, the paper WE sent
// them, and `received`, what came back. The only path any person could drive
// is the redline upload, and `doorway/redlines.py` hardcodes `'received'` and
// a null run. `recordRound` — which takes the direction as a field — sat on
// the `REACHED_BY_NO_SCREEN` ledger with no caller. The only other writers
// were a seed script and two tests, which is the shape that hid
// `cw.agreement_attorney` for a month.
//
// So Clausewerk recorded what the supplier sent us and had no way to record
// what we sent them. The strip below already draws `out, to them` for a state
// nothing could produce.
//
// AND THE PROVENANCE CHAIN WAS BROKEN AT ITS LAST LINK. `0011` carries a
// constraint written for this and nothing else —
// `only_our_rounds_have_a_run check (direction = 'issued' or run_id is null)`
// — so an issued round MAY name the assembly run that built the paper. With
// no way to write an issued round at all, "which build did we actually send
// them" had no answer, in a product whose promise is that the origin of every
// clause is recorded permanently.
//
// ── THE FINGERPRINT IS COMPUTED HERE, FROM THE REAL BYTES ─────────────────
//
// A round is EVIDENCE rather than a note because of `document_sha256`, and
// `0011` requires 64 hex characters. There were two honest ways to get one for
// a document this system does not hold, and only one of them is evidence:
//
//   · TYPED IN BY HAND. Rejected. A person pasting a fingerprint can paste the
//     wrong one, or a plausible one, and nothing anywhere could tell. That
//     turns the one column that makes a round evidence into a claim.
//
//   · COMPUTED FROM THE FILE ITSELF, in the browser, with the bytes never
//     leaving it. This is what happens: the person points at the document
//     wherever it lives, `crypto.subtle` reads it and produces the digest, and
//     the file is discarded. The record gets a fingerprint that is true of a
//     real document, and this system still does not hold a copy — which is the
//     whole point of "held elsewhere".
//
// `crypto.subtle` EXISTS ONLY IN A SECURE CONTEXT (https, or localhost). On a
// deployment served over plain http it is absent, and the form says so and
// refuses rather than falling back to a typed fingerprint — a fabricated
// digest is worse than a missing round.
async function fingerprintOf(file) {
  const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
  return [...new Uint8Array(digest)]
    .map((b) => b.toString(16).padStart(2, '0')).join('');
}

// The `cw://received-document/` scheme IS NOT A POINTER SOMEBODY MAY TYPE. The
// strip below reads that exact prefix to decide whether to offer the document
// back, so a hand-recorded round carrying one would draw a download button for
// bytes this system does not have. The scheme belongs to the upload path;
// anything recorded here is held somewhere else by definition.
const CW_HELD = 'cw://received-document/';

function RecordRoundByHand({ negotiation, nextRound, runs, onError, onRecorded }) {
  const [open, setOpen] = useState(false);
  const [direction, setDirection] = useState('issued');
  const [sentOn, setSentOn] = useState('');
  const [where, setWhere] = useState('');
  const [runId, setRunId] = useState('');
  const [file, setFile] = useState(null);
  const [sha, setSha] = useState('');
  const [hashing, setHashing] = useState(false);
  const [busy, setBusy] = useState(false);

  // THE BUILDS OF THIS DEAL, and only this deal's. A run belongs to an
  // agreement; offering another deal's would let somebody attach the wrong
  // provenance to the right round.
  const mine = (runs.rows || [])
    .filter((r) => r.agreement_id === negotiation.agreement_id);

  const canHash = typeof crypto !== 'undefined' && crypto.subtle;
  const ready = sha && where.trim() && sentOn && !hashing
                && !where.trim().startsWith(CW_HELD);

  const take = async (chosen) => {
    setFile(chosen); setSha(''); onError(null);
    if (!chosen) return;
    setHashing(true);
    try { setSha(await fingerprintOf(chosen)); }
    catch (e) { onError('the document could not be read: ' + e.message); }
    finally { setHashing(false); }
  };

  if (!open) {
    return (
      <button className="btn btn-sm mt-3" data-testid="record-round-open"
              onClick={() => setOpen(true)}>
        record a round held elsewhere
      </button>
    );
  }

  return (
    <div className="panel-2 p-3 mt-3" data-testid="record-round-form">
      <div className="section-label">Record round {nextRound}</div>
      <div className="caption mt-1">
        For paper this system does not hold — what we sent them, or something
        that arrived outside the upload. The document stays where it is; only
        its fingerprint is recorded, and it is computed here from the file
        itself rather than typed.
      </div>

      {!canHash && (
        <div className="panel p-3 mt-3" style={{ borderColor: 'var(--danger)' }}>
          <div className="tag" style={{ color: 'var(--danger)' }}>cannot record</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>
            This browser will not fingerprint a file on this connection — it
            needs https, or localhost. A round can only be recorded with a
            fingerprint taken from the real bytes, and a typed one would not be
            evidence of anything.
          </div>
        </div>
      )}

      {canHash && (
        <>
          <label className="section-label mt-3">Which way</label>
          <select className="mt-1.5 w-full" aria-label="Which way"
                  data-testid="round-direction"
                  value={direction}
                  onChange={(e) => {
                    setDirection(e.target.value);
                    // A RECEIVED ROUND CANNOT NAME A BUILD. `0011`'s
                    // `only_our_rounds_have_a_run` refuses it, so the choice is
                    // dropped when it stops being legal rather than sent and
                    // refused.
                    if (e.target.value !== 'issued') setRunId('');
                  }}>
            <option value="issued">out, to them — paper we sent</option>
            <option value="received">in, from them — arrived outside the upload</option>
          </select>

          <label className="section-label mt-3">The document</label>
          <input type="file" className="mt-1.5" data-testid="round-file"
                 aria-label="Point at the document, which is not uploaded"
                 onChange={(e) => {
                   const chosen = e.target.files && e.target.files[0];
                   e.target.value = '';
                   take(chosen);
                 }} />
          <div className="caption mt-1.5" data-testid="round-fingerprint">
            {hashing ? 'reading…'
              : sha
                ? <>fingerprinted <span className="font-mono">{sha.slice(0, 16)}…</span>
                    {file ? ' · ' + file.name : ''} · <strong>not uploaded</strong></>
                : 'The file is read in this browser and discarded. Nothing is sent.'}
          </div>

          <label className="section-label mt-3">Where it is kept</label>
          <input className="mt-1.5 w-full" aria-label="Where it is kept"
                 data-testid="round-where"
                 placeholder="the system of record, and where in it"
                 value={where} onChange={(e) => setWhere(e.target.value)} />
          {where.trim().startsWith(CW_HELD) && (
            <div className="caption mt-1" style={{ color: 'var(--danger)' }}>
              That prefix belongs to documents this system holds. A round
              recorded here is held somewhere else — say where.
            </div>
          )}

          <label className="section-label mt-3">Sent on</label>
          <input type="date" className="mt-1.5" aria-label="Sent on"
                 data-testid="round-sent-on"
                 value={sentOn} onChange={(e) => setSentOn(e.target.value)} />

          {direction === 'issued' && (
            <>
              <label className="section-label mt-3">Which build produced it</label>
              {runs.status === 'failed' ? (
                <div className="caption mt-1.5">
                  The builds could not be read, so this round cannot name one.
                  It can still be recorded — naming the build is optional on the
                  record.
                </div>
              ) : mine.length === 0 ? (
                <div className="caption mt-1.5">
                  This deal has no recorded build to name.
                </div>
              ) : (
                <select className="mt-1.5 w-full" aria-label="Which build produced it"
                        data-testid="round-run"
                        value={runId} onChange={(e) => setRunId(e.target.value)}>
                  <option value="">not recorded</option>
                  {mine.map((r) => (
                    <option key={r.run_id} value={r.run_id}>
                      {r.created_at} · {r.decisions} decisions
                      {r.overridden ? ' · overridden' : ''}
                    </option>
                  ))}
                </select>
              )}
              <div className="caption mt-1">
                This is the link that says which assembled contract the
                counterparty was actually sent. Optional, and only an issued
                round may carry one.
              </div>
            </>
          )}

          <div className="flex gap-2 mt-4">
            <button className="btn" onClick={() => setOpen(false)}>cancel</button>
            <ActButton className="btn btn-primary" disabled={busy || !ready}
              data-testid="record-round-submit"
              onClick={async () => {
                setBusy(true); onError(null);
                const body = {
                  negotiation_id: negotiation.negotiation_id,
                  round_no: nextRound,
                  direction,
                  document_sha256: sha,
                  storage_uri: where.trim(),
                  sent_on: sentOn,
                };
                // OMITTED RATHER THAN SENT EMPTY when nothing was chosen: the
                // column is a foreign key to cw.run and an empty string is not
                // a run. And never on a received round, which `0011` refuses.
                if (direction === 'issued' && runId) body.run_id = runId;
                const r = await API.recordRound(body);
                setBusy(false);
                if (!r.ok) { onError(r.reason); return; }
                setOpen(false); setSha(''); setFile(null);
                setWhere(''); setSentOn(''); setRunId('');
                onRecorded();
              }}>
              {busy ? 'recording…' : '✓ record round ' + nextRound}
            </ActButton>
          </div>
        </>
      )}
    </div>
  );
}

// ── The rounds strip ──────────────────────────────────────────────────────
// What was exchanged, in order, with the fingerprint of each document. The
// fingerprint is shown because it is the only thing that makes the round
// EVIDENCE rather than a note that something was sent.
function Rounds({ negotiation, rounds, runs, onError, onRecorded, canAct }) {
  const [busy, setBusy] = useState(false);
  const mine = rounds
    .filter((r) => r.negotiation_id === negotiation.negotiation_id)
    .sort((a, b) => a.round_no - b.round_no);
  const next = mine.reduce((n, r) => Math.max(n, r.round_no), 0) + 1;

  const take = async (file) => {
    setBusy(true); onError(null);
    const r = await API.recordRedline(negotiation.agreement_id, file);
    setBusy(false);
    if (!r.ok) { onError(r.reason); return; }
    onRecorded();
  };

  return (
    <div className="panel p-4 mt-6">
      <PanelHead
        title="Rounds"
        sub="What was exchanged, in order. The record is append-only and gapless — the database refuses a round that skips one." />

      {mine.length === 0
        ? <Empty
            kicker="rounds"
            line="Nothing has been exchanged yet."
            sub="A negotiation with no rounds is a negotiation that has been opened and not yet started. That is a real state, not a missing screen." />
        : mine.map((r) => (
            <div className="flex items-baseline justify-between py-2 border-b hair"
                 key={r.round_no} data-testid="negotiation-round">
              <div className="min-w-0">
                <span className="font-mono text-[12.5px]">round {r.round_no}</span>
                <span className="ml-3 text-[12.5px]" style={{ color: 'var(--mute)' }}>
                  {r.direction === 'received' ? 'in, from them' : 'out, to them'}
                  {r.sent_on && ` · ${r.sent_on}`}
                </span>
                <div className="caption font-mono mt-0.5 truncate" title={r.document_sha256}>
                  {r.document_sha256}
                </div>
              </div>
              {/* THE DOCUMENT, BACK OUT (owner decision NI-4). Offered only for
                  a round whose bytes this system actually holds — a round
                  recorded for a document kept elsewhere says so instead of
                  offering a button that will refuse. */}
              {String(r.storage_uri || '').startsWith('cw://received-document/')
                ? <button className="btn btn-sm" data-testid="fetch-paper"
                          onClick={async () => {
                            onError(null);
                            const got = await API.supplierPaper(
                              r.negotiation_id, r.round_no);
                            if (!got.ok) { onError(got.reason); return; }
                            saveBlob(got.blob, got.filename);
                          }}>
                    their document
                  </button>
                : <span className="caption">held elsewhere</span>}
            </div>
          ))}

      {canAct && (
        <div className="mt-4 pt-3 border-t hair">
          <div className="section-label">Record what they sent back</div>
          {/* WRAPS AT A NARROW WIDTH. Measured at 375px: the caption beside
              this control could not shrink and hung 56px outside the panel,
              taking the strip 51px over. It is a flex row of a file input with
              its own intrinsic width and a two-sentence caption, and neither
              could give. This was true before a second control was added to
              this strip — a repair, not a precaution. */}
          <div className="flex items-center gap-3 mt-2 flex-wrap">
            <input type="file" data-testid="redline-upload" disabled={busy}
                   aria-label="Choose the redline the counterparty sent back"
                   onChange={(e) => {
                     const file = e.target.files && e.target.files[0];
                     e.target.value = '';
                     if (file) take(file);
                   }} />
            <span className="caption">
              It lands as round {next}. The fingerprint is the database's own
              arithmetic over the bytes — nothing here can supply one.
            </span>
          </div>
          {/* THE OTHER HALF OF THE RECORD. The upload above takes the bytes and
              can only ever produce a `received` round; this records a round
              whose document lives somewhere else, in either direction — which
              is the only way an `issued` round has ever been recordable. */}
          <RecordRoundByHand
            negotiation={negotiation} nextRound={next} runs={runs}
            onError={onError} onRecorded={onRecorded} />
        </div>
      )}
    </div>
  );
}

// ── One authored move, as advice ──────────────────────────────────────────
// The playbook's row, wherever it appears. Kind, title, guidance, the
// fallback as a REFERENCE, and U14d's estimate — or its honest absence,
// because an estimate nobody computed is not zero and must not read as one.
function MoveAdvice({ move }) {
  return (
    <div className="py-1.5 border-b hair" data-testid="authored-move">
      <div className="text-[12.5px]">
        <span className="font-mono">{move.seq}</span>
        <span className="chip chip-std ml-2">{move.kind}</span>
        <span className="ml-2" style={{ color: 'var(--ink)' }}>{move.title}</span>
      </div>
      <div className="font-serif italic mt-1"
           style={{ fontSize: 13.5, color: 'var(--mute)' }}>
        {move.guidance}
      </div>
      <div className="caption mt-0.5">
        {move.fallback_clause_id
          ? `falls back to ${move.fallback_clause_id}@v${move.fallback_version} — approved wording, by reference`
          : 'proposes no wording'}
        {' · '}
        {move.risk_transfer_estimate !== null && move.risk_transfer_estimate !== undefined
          ? `risk transfer est. ${move.risk_transfer_estimate}${move.model
              ? ` (${move.model}${move.model_version ? ' ' + move.model_version : ''})` : ''}`
          : 'no risk estimate was asked'}
      </div>
    </div>
  );
}

// ── One position ──────────────────────────────────────────────────────────
// The same component wherever a position appears. `acts` decides what is
// offered; it never decides what is permitted.
function Position({ position, movements, roundNow, onError, onMoved, acts,
                    positionMoves }) {
  const [note, setNote] = useState('');
  const [rung, setRung] = useState('');
  const [busy, setBusy] = useState(false);
  const [showing, setShowing] = useState(false);

  const history = movements
    .filter((m) => m.position_id === position.position_id)
    .sort((a, b) => a.movement_id - b.movement_id);

  // The SECOND advisory stream, narrowed from what cw.position_move already
  // returned for this connection — never fetched wider. Live moves only; the
  // view leaves retired ones out because this is the playbook a negotiator
  // may act on today, not the history.
  const advice = positionMoves && positionMoves.status === 'loaded'
    ? positionMoves.rows.filter((m) => m.position_id === position.position_id)
    : [];

  const act = async (call) => {
    setBusy(true); onError(null);
    const r = await call();
    setBusy(false);
    if (!r.ok) { onError(r.reason); return; }
    setNote(''); setRung('');
    onMoved();
  };

  return (
    <div className="panel-2 p-3 mb-2" data-testid="position">
      <div className="flex items-baseline justify-between gap-3">
        <div className="min-w-0">
          <span className="font-mono text-[12.5px]">{position.category_key}</span>
          {/* A DOMAIN STATE, AS A WORD. Not a Status chip: these six are not
              the five-state vocabulary, and borrowing its inks would make
              "held" look like a governance state it is not. */}
          <span className="ml-3 text-[12.5px]" style={{ color: 'var(--ink)' }}>
            {position.state}
          </span>
          {position.current_rung !== null && position.current_rung !== undefined && (
            <span className="ml-2 caption">at rung {position.current_rung}</span>
          )}
        </div>
        <div className="caption">
          raised round {position.round_raised} · from {position.opened_from.replace('_', ' ')}
        </div>
      </div>

      {position.our_clause_id && (
        <div className="caption font-mono mt-1">
          {position.our_clause_id} v{position.our_version}
        </div>
      )}

      {advice.length > 0 && (
        <div className="mt-2" data-testid="position-moves">
          <div className="section-label">authored moves — advice, gates nothing</div>
          {advice.map((m) => <MoveAdvice key={m.move_id} move={m} />)}
        </div>
      )}

      <button className="btn btn-sm mt-2" onClick={() => setShowing(!showing)}>
        {showing ? 'hide' : `how it got here (${history.length})`}
      </button>

      {showing && (
        <div className="mt-2">
          {history.length === 0
            ? <div className="caption">No movement is recorded on this position.</div>
            : history.map((m) => (
                <div className="py-1.5 border-b hair" key={m.movement_id}>
                  <div className="text-[12.5px]">
                    <span className="font-mono">round {m.round_no}</span>
                    <span className="ml-2">{m.to_state}</span>
                    {m.current_rung !== null && m.current_rung !== undefined && (
                      <span className="ml-2 caption">rung {m.current_rung}</span>
                    )}
                  </div>
                  <div className="caption font-mono">{m.actor} · {since(m.moved_at)}</div>
                  {m.note && (
                    <div className="font-serif italic mt-1"
                         style={{ fontSize: 13.5, color: 'var(--mute)' }}>
                      {m.note}
                    </div>
                  )}
                </div>
              ))}
        </div>
      )}

      {acts && (
        <div className="mt-3 pt-3 border-t hair">
          <div className="flex gap-2 items-end">
            <div style={{ width: 90 }}>
              <label className="caption">Rung</label>
              <input aria-label="Rung" className="mt-1 w-full font-mono" style={{ padding: '4px 8px' }}
                     value={rung} onChange={(e) => setRung(e.target.value)} />
            </div>
            <div className="flex-1">
              <label className="caption">Why</label>
              <input aria-label="Why" className="mt-1 w-full" style={{ padding: '4px 8px' }}
                     data-testid="position-note"
                     value={note} onChange={(e) => setNote(e.target.value)} />
            </div>
          </div>

          <div className="flex gap-2 mt-2 flex-wrap">
            {['held', 'conceded', 'withdrawn', 'settled'].map((to) => (
              <ActButton className="btn btn-sm" key={to} disabled={busy}
                      data-testid={`move-${to}`}
                      onClick={() => act(() => API.movePosition({
                        position_id: position.position_id,
                        round_no: roundNow,
                        to_state: to,
                        current_rung: rung.trim() === '' ? null : Number(rung),
                        note: note.trim() || null,
                      }))}>
                {to}
              </ActButton>
            ))}
            {/* ESCALATION IS ITS OWN BUTTON, and it is not in the row above.
                Nobody should reach Legal by typing a string into a state
                field — the endpoint agrees, which is why 'escalated' is not
                the caller's to choose there either. */}
            <ActButton className="btn btn-sm" disabled={busy}
                    data-testid="escalate"
                    style={{ borderColor: 'var(--accent)' }}
                    onClick={() => act(() => API.escalatePosition({
                      position_id: position.position_id,
                      round_no: roundNow,
                      current_rung: rung.trim() === '' ? null : Number(rung),
                      note: note.trim() || null,
                    }))}>
              hand to Legal
            </ActButton>
          </div>

          {/* TWO ACTS, TWO BUTTONS — owner decision NI-3. Escalating records
              the movement. Opening a ticket is a second governed act with its
              own reason, and it is performed knowingly or not at all. */}
          {position.state === 'escalated' && (
            <div className="caption mt-2">
              This is with Legal. Opening a review ticket for the wording is a
              separate act, on the review desk — escalating does not open one,
              deliberately.
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Positions, with the act that opens one ────────────────────────────────
function Positions({ negotiation, positions, movements, roundNow, categories,
                    onError, onChanged, acts, positionMoves }) {
  const [adding, setAdding] = useState(false);
  const [fresh, setFresh] = useRetainedState('negotiation-point:' + negotiation.negotiation_id,
    { category_key: '', opened_from: 'library_standard', clause_ref: '' });
  const library = usePane(() => API.library());
  const [busy, setBusy] = useState(false);
  const sources = (library.rows || []).filter(c => c.category_key === fresh.category_key);
  const source = sources.find(c => c.clause_id + '@' + c.version === fresh.clause_ref);

  const mine = positions
    .filter((p) => p.negotiation_id === negotiation.negotiation_id)
    .sort((a, b) => a.position_id - b.position_id);

  return (
    <div className="panel p-4 mt-6">
      <PanelHead
        title="Positions"
        sub="One row per contested point, from first raised to finally settled. The rung is where the retreat has reached."
        right={acts && (
          <button className="btn btn-sm" onClick={() => setAdding(!adding)}>
            {adding ? 'cancel' : '+ raise a point'}
          </button>
        )} />

      {adding && (
        <div className="panel-2 p-3 mb-3 new-negotiation-point">
          <div className="flex flex-wrap gap-2 items-end">
            <div className="concession-field">
              <label className="caption">Category</label>
              <select aria-label="Category" className="mt-1 w-full font-mono" style={{ padding: '5px 8px' }}
                      data-testid="new-position-category"
                      value={fresh.category_key}
                      disabled={busy}
                      onChange={(e) => setFresh({ ...fresh, category_key: e.target.value, clause_ref: '' })}>
                <option value="">choose…</option>
                {categories.map((c) => (
                  <option key={c.key} value={c.key}>{c.key} — {c.label}</option>
                ))}
              </select>
            </div>
            <div className="concession-field">
              <label className="caption">Opened from</label>
              <select aria-label="Opened from" className="mt-1 w-full font-mono" style={{ padding: '5px 8px' }}
                      value={fresh.opened_from}
                      disabled={busy}
                      onChange={(e) => setFresh({ ...fresh, opened_from: e.target.value })}>
                <option value="library_standard">library standard</option>
                <option value="executed_agreement">last term's position</option>
                <option value="their_paper">their paper</option>
              </select>
            </div>
            <div className="concession-field">
              <label className="caption">Library clause on the paper · optional</label>
              <select aria-label="Library clause on the paper" data-testid="new-position-source"
                value={fresh.clause_ref} disabled={busy || library.status !== 'loaded'}
                onChange={e => setFresh({ ...fresh, clause_ref: e.target.value })}>
                <option value="">no library clause identified</option>
                {sources.map(c => <option key={c.clause_id + '@' + c.version} value={c.clause_id + '@' + c.version}>
                  {c.clause_id} v{c.version} · {c.title} · {c.state}
                </option>)}
              </select>
            </div>
            <ActButton className="btn btn-primary" disabled={busy || !fresh.category_key || (fresh.clause_ref && !source)}
                    data-testid="open-position"
                    onClick={async () => {
                      setBusy(true); onError(null);
                      const r = await API.openPosition({
                        negotiation_id: negotiation.negotiation_id,
                        category_key: fresh.category_key,
                        round_raised: roundNow,
                        opened_from: fresh.opened_from,
                        our_clause_id: source?.clause_id ?? null,
                        our_version: source?.version ?? null,
                      });
                      setBusy(false);
                      if (!r.ok) { onError(r.reason); return; }
                      setFresh({ category_key: '', opened_from: 'library_standard', clause_ref: '' });
                      discardDraft('negotiation-point:' + negotiation.negotiation_id);
                      setAdding(false);
                      onChanged();
                    }}>✓ raise it</ActButton>
          </div>
          {library.status === 'failed' && <LoadFailed reason={library.reason} />}
          {source && <div className="concession-wording mt-3" data-testid="new-position-source-wording">{source.body}</div>}
          <div className="caption mt-2">Naming the exact version records the position being negotiated; it does not change the contract.
            A concession can then name this position as its starting point.</div>
        </div>
      )}

      {mine.length === 0
        ? <Empty
            kicker="positions"
            line="No point is contested."
            sub="Nothing has been raised on this negotiation. That is the whole answer — there is no filter hiding anything." />
        : mine.map((p) => (
            <Position key={p.position_id} position={p} movements={movements}
                      roundNow={roundNow} onError={onError} onMoved={onChanged}
                      acts={acts} positionMoves={positionMoves} />
          ))}
    </div>
  );
}

// ── The same argument, reopening ──────────────────────────────────────────
function Revivals({ negotiation, revivals }) {
  const mine = revivals.filter((r) => r.negotiation_id === negotiation.negotiation_id);
  return (
    <div className="panel p-4 mt-6">
      <PanelHead
        title="Points being reopened"
        sub="A position that was settled and is being argued again. Live from the first round — that is what the record is for." />
      {/* EMPTY IS GOOD NEWS AND MUST SAY SO. A blank area here reads as a
          screen that failed to load, and the two facts are opposites. */}
      {mine.length === 0
        ? <div className="caption">None. No settled point is being renegotiated.</div>
        : mine.map((r) => (
            <div className="flex items-baseline justify-between py-2 border-b hair"
                 key={r.position_id}>
              <span className="font-mono text-[12.5px]">{r.category_key}</span>
              <span className="caption">
                held {r.times_held} times · first round {r.first_held_round} ·
                last round {r.last_round}
              </span>
            </div>
          ))}
    </div>
  );
}

// ── What a renewal is carrying ────────────────────────────────────────────
// U1's control. It stays in front of whoever opened the renewal rather than
// behind a link, because the whole hazard is starting from stale language
// without noticing.
function Drift({ negotiation, drift }) {
  if (!negotiation.renews_agreement_id) return null;
  const mine = drift.filter((d) => d.negotiation_id === negotiation.negotiation_id);
  return (
    <div className="panel p-4 mt-6" data-testid="renewal-drift">
      <PanelHead
        title="What this renewal opened from"
        sub={`Opened from ${negotiation.baseline === 'executed_agreement'
          ? "last term's executed positions" : 'current library standard'}. This report rewrites nothing.`} />
      {mine.length === 0
        ? <div className="caption">
            Nothing has moved in the library since the positions this renewal
            opened from.
          </div>
        : mine.map((d) => (
            <div className="flex items-baseline justify-between py-2 border-b hair"
                 key={`${d.clause_id}-${d.executed_version}`}>
              <div>
                <span className="font-mono text-[12.5px]">{d.clause_id}</span>
                <span className="ml-2 caption">
                  in force v{d.executed_version}
                  {d.successor_version && ` · library is now v${d.successor_version}`}
                </span>
              </div>
              <Status state={d.current_state === 'superseded' ? 'superseded' : 'effective'}>
                {d.current_state}
              </Status>
            </div>
          ))}
    </div>
  );
}

// ── Concessions on this deal ──────────────────────────────────────────────
// Settling at a fallback needs BOTH the requester and the assigned attorney.
// The screen offers the half the person in front of it can perform; the
// database decides whether they are one of the two named.
// ── Recording a concession, which nothing could create ────────────────────
//
// THE GATE WAS REAL AND THE DOOR DID NOT EXIST, for the second time in this
// same family. `POST /concessions` has been served since `0003` with a policy,
// an authority trigger and a floor lookup, and `api.jsx` has offered `concede`
// for months. No pane ever called it: it sat on `REACHED_BY_NO_SCREEN`. The
// only other writers are `seed_library.py` and one test.
//
// So FOUR ACTS OPERATED ON A RECORD NOBODY COULD CREATE — approve, settle,
// withdraw and promote-into-the-library are all reached from a control, and
// the thing they act on could only arrive by seed. The panel above this form
// has said "Nothing has been conceded on this deal" on every real deal since
// the beginning, and it was telling the truth about a table only a seed could
// fill.
//
// AND MARKING A POSITION `conceded` IS NOT THIS. That writes
// `cw.position_movement` — the negotiation's blow-by-blow, one row per move.
// A concession is the GOVERNED RECORD of what the company gave up: it carries
// the ladder and its floor, needs a second person to approve it, can be put in
// force or withdrawn, and is what the Library Builder reads (`cw.concession_
// rate`) when it drafts proposed language out of what negotiation taught us.
// A person could mark every point on a deal conceded and leave that record
// empty, which is exactly what has been happening.
//
// ── WHY THIS FORM SHOWS THE LADDER ────────────────────────────────────────
//
// Match 0154's live, floored ladder by the actual source clause's severity,
// not merely a ladder that happens to contain that clause. Several ladders
// may contain it, including retired history. Preserve the database's lowest-ID
// tie break. A below-floor proposal is recordable, but needs its own override
// before settlement; a number with no published rung remains unavailable.
function laddersFor(ladders, position, library) {
  const source = (library.rows || []).find(r => r.clause_id === position.our_clause_id
    && r.version === position.our_version);
  if (!source) return null;
  const rows = (ladders.rows || []).filter(
    (r) => r.category_key === position.category_key
        && r.severity === source.severity && !r.retired_on && r.is_floor)
    .sort((a, b) => Number(a.ladder_id) - Number(b.ladder_id));
  if (rows.length === 0) return null;
  const id = rows[0].ladder_id;
  const rungs = (ladders.rows || [])
    .filter((r) => r.ladder_id === id && r.rung !== null && r.rung !== undefined)
    .sort((a, b) => a.rung - b.rung);
  const floor = rungs.find((r) => r.is_floor);
  return { ladder_id: id, rungs, floor: floor ? floor.rung : null,
           status: rows[0].ladder_status };
}

function concessionFallbackRead(ladders, library) {
  const failure = [ladders, library].find(read => read.status === 'failed');
  if (failure) return failure;
  return {status: ladders.status === 'loaded' && library.status === 'loaded' ? 'loaded' : 'loading'};
}

function RecordConcession({ negotiation, positions, ladders, onError, onChanged }) {
  const library = usePane(() => API.library());
  const fallbackRead = concessionFallbackRead(ladders, library);
  const [open, setOpen] = useState(false);
  const [positionId, setPositionId] = useRetainedState(
    'concession-position:' + negotiation.negotiation_id, '');
  const [busy, setBusy] = useState(false);
  const usable = positions
    .filter(p => p.negotiation_id === negotiation.negotiation_id && p.our_clause_id && p.our_version)
    .sort((a, b) => a.position_id - b.position_id);
  const chosen = usable.find(p => String(p.position_id) === String(positionId)) || null;

  if (!open) return (
    <button className="btn btn-sm mt-3" data-testid="record-concession-open"
            onClick={() => setOpen(true)}>propose a concession</button>
  );
  return (
    <div className="panel-2 p-3 mt-3 concession-proposal" data-testid="record-concession-form">
      <div className="section-label">Propose a different position</div>
      <div className="caption mt-1">
        Record what is proposed, not what has been approved. The buyer, assigned attorney
        and any required approvers must approve before it can take effect.
      </div>
      {usable.length === 0 ? (
        <div className="caption mt-3" data-testid="no-position-to-concede">
          First record a negotiation point that names the library clause this proposal would replace.
        </div>
      ) : (
        <label className="concession-field mt-3">
          <span className="section-label">Which point</span>
          <select className="w-full font-mono" aria-label="Which point"
                  data-testid="concession-position" value={positionId} disabled={busy}
                  onChange={e => setPositionId(e.target.value)}>
            <option value="">choose…</option>
            {usable.map(p => <option key={p.position_id} value={p.position_id}>
              {p.category_key} · {p.our_clause_id} v{p.our_version} · {p.state}
            </option>)}
          </select>
        </label>
      )}
      {chosen && (
        <ConcessionProposalEditor key={negotiation.negotiation_id + ':' + chosen.position_id}
          negotiation={negotiation} chosen={chosen}
          ladder={fallbackRead.status === 'loaded' ? laddersFor(ladders, chosen, library) : null}
          fallbackRead={fallbackRead} onRetryFallback={() => { ladders.reload(); library.reload(); }}
          busy={busy} setBusy={setBusy} onError={onError} onChanged={onChanged} />
      )}
      <button className="btn btn-sm mt-3" disabled={busy} onClick={() => setOpen(false)}>
        close · keep draft
      </button>
    </div>
  );
}

function ConcessionProposalEditor({ negotiation, chosen, ladder, fallbackRead, onRetryFallback,
                                   busy, setBusy, onError, onChanged }) {
  const draftKey = 'concession-proposal:' + negotiation.negotiation_id + ':' + chosen.position_id;
  const empty = { kind: 'rung', rung: '', vendor_text: '', reason: '' };
  const [draft, setDraft] = useRetainedState(draftKey, empty);
  const [confirming, setConfirming] = useState(false);
  const [receipt, setReceipt] = useState(null);
  const offerable = ladder && ladder.floor !== null ? ladder.rungs : [];
  const selectedRung = offerable.find(r => String(r.rung) === String(draft.rung));
  const supplier = draft.kind === 'supplier';
  const needsOverride = supplier || (selectedRung && selectedRung.rung > ladder.floor);
  const ready = (supplier ? draft.vendor_text.trim() : selectedRung) && draft.reason.trim() && !busy;
  const update = (field, value) => {
    setConfirming(false); setReceipt(null);
    setDraft(previous => ({ ...previous, [field]: value }));
  };

  return (
    <div className="concession-proposal-editor" data-testid="concession-proposal-editor">
      {receipt && <div className="panel-2 p-3 mt-3" role="status">
        Proposal {receipt} recorded. It remains pending until its approvals are complete.
      </div>}
      <div className="caption mt-3">
        {negotiation.agreement_id} · replacing {chosen.our_clause_id} v{chosen.our_version}
      </div>
      <label className="concession-field mt-3">
        <span className="section-label">What is proposed</span>
        <select aria-label="What is proposed" data-testid="concession-kind"
                value={draft.kind} disabled={busy} onChange={e => update('kind', e.target.value)}>
          <option value="rung">A published fallback position</option>
          <option value="supplier">The supplier's exact wording</option>
        </select>
      </label>
      {supplier ? (
        <label className="concession-field mt-3">
          <span className="section-label">Exact supplier wording</span>
          <textarea rows={7} aria-label="Exact supplier wording" data-testid="concession-vendor-text"
            value={draft.vendor_text} disabled={busy} onChange={e => update('vendor_text', e.target.value)} />
          <span className="caption">Stored exactly as entered, including paragraph breaks. This does not add it to the library.</span>
        </label>
      ) : fallbackRead.status !== 'loaded' ? (
        <div className="mt-3" data-testid="concession-fallback-unavailable">
          {fallbackRead.status === 'failed' ? <>
            <LoadFailed reason={fallbackRead.reason} />
            <button className="btn btn-sm mt-2" disabled={busy} onClick={onRetryFallback}>retry fallback reads</button>
          </> : <Loading />}
        </div>
      ) : !ladder || ladder.floor === null ? (
        <div className="caption mt-3" data-testid={!ladder ? 'no-ladder' : 'no-floor'}>
          No published fallback with a floor is available for this point. Legal can publish one.
          You can still propose the supplier's exact wording for separate approval.
        </div>
      ) : (
        <label className="concession-field mt-3">
          <span className="section-label">Proposed fallback</span>
          <select aria-label="Proposed fallback" data-testid="concession-rung"
                  value={draft.rung} disabled={busy} onChange={e => update('rung', e.target.value)}>
            <option value="">choose…</option>
            {offerable.map(r => <option key={r.rung} value={r.rung}>
              rung {r.rung} · {r.clause_id} v{r.version}
              {r.rung_state && r.rung_state !== 'effective' ? ' · ' + r.rung_state : ''}
              {r.is_floor ? ' · the floor' : r.rung > ladder.floor ? ' · below floor: override required' : ''}
            </option>)}
          </select>
        </label>
      )}
      <label className="concession-field mt-3">
        <span className="section-label">Why this is proposed</span>
        <textarea rows={3} aria-label="Why this is proposed" data-testid="concession-reason"
          value={draft.reason} disabled={busy} onChange={e => update('reason', e.target.value)} />
      </label>
      {needsOverride && <div className="concession-approval-note mt-3" data-testid="concession-override-required">
        Before settlement: the buyer requests an override for this proposal, stakeholders are told,
        the review window runs, and Legal decides. A request is not an approval.
      </div>}
      {confirming ? (
        <div className="panel-2 p-3 mt-3" data-testid="concession-proposal-confirm">
          <div className="section-label">Review this proposal · {negotiation.agreement_id}</div>
          <div className="caption mt-1">{chosen.our_clause_id} v{chosen.our_version}
            {!supplier && selectedRung && (' → rung ' + selectedRung.rung)}</div>
          {supplier && <div className="concession-wording mt-3" data-testid="concession-wording-review">{draft.vendor_text}</div>}
          <div className="concession-wording mt-3">{draft.reason}</div>
          <div className="flex flex-wrap gap-2 mt-3">
            <ActButton className="btn btn-primary" disabled={!ready} data-testid="record-concession-submit"
              onClick={async () => {
                setBusy(true); onError(null);
                const r = await API.concede({
                  agreement_id: negotiation.agreement_id, category_key: chosen.category_key,
                  standard_clause_id: chosen.our_clause_id, standard_version: chosen.our_version,
                  conceded_rung: supplier ? null : Number(draft.rung),
                  vendor_text: supplier ? draft.vendor_text : null, reason: draft.reason.trim(),
                });
                setBusy(false);
                if (!r.ok) { onError(r.reason); return; }
                setDraft(empty); discardDraft(draftKey); setConfirming(false);
                setReceipt(r.rows[0].concession_id);
                onChanged();
              }}>{busy ? 'recording…' : 'record proposal'}</ActButton>
            <button className="btn" disabled={busy} onClick={() => setConfirming(false)}>back to editing</button>
          </div>
        </div>
      ) : (
        <button className="btn btn-primary mt-3" disabled={!ready} data-testid="review-concession-proposal"
                onClick={() => setConfirming(true)}>review proposal</button>
      )}
    </div>
  );
}

function Concessions({ negotiation, concessions, positions, ladders, acts,
                     onError, onChanged, approverKind }) {
  const [busy, setBusy] = useState(false);
  const overrides = usePane(() => API.overrides());
  const mine = concessions.filter((c) => c.agreement_id === negotiation.agreement_id);

  return (
    <div className="panel p-4 mt-6">
      <PanelHead
        title="Concessions"
        sub="Proposed and agreed departures from the standard. A proposal is not yet binding." />
      {mine.length === 0
        ? <div className="caption">
            Nothing has been conceded on this deal.
          </div>
        : mine.map((c) => (
            <div className="panel-2 p-3 mb-2" key={c.concession_id}
                 data-testid="concession">
              <div className="flex flex-wrap items-baseline justify-between gap-3">
                <div className="min-w-0">
                  <span className="font-mono text-[12.5px]">{c.category_key} · proposal {c.concession_id}</span>
                  <span className="ml-2 caption">
                    {c.standard_clause_id} v{c.standard_version}
                    {c.conceded_rung !== null && c.conceded_rung !== undefined
                      && ` · to rung ${c.conceded_rung}`}
                  </span>
                </div>
                {/* THIS one is a status mark, and correctly: an approved
                    concession is in force and a proposed one confers nothing.
                    Amber, never green, while it waits. */}
                <Status state={c.state === 'approved' ? 'effective'
                             : c.state === 'withdrawn' ? 'superseded' : 'pending'}>
                  {c.state}
                </Status>
              </div>
              {c.vendor_text != null && (
                <div className="concession-wording mt-3" data-testid="concession-recorded-wording">{c.vendor_text}</div>
              )}
              {c.reason && (
                <div className="panel-2 p-3 mt-2 relative">
                  <span className="font-serif" style={{
                    position: 'absolute', left: 6, top: -6, fontSize: 34,
                    color: 'var(--accent)', opacity: .55, lineHeight: 1 }}>“</span>
                  <div className="font-serif italic"
                       style={{ fontSize: 15, lineHeight: 1.6, paddingLeft: 22 }}>
                    {c.reason}
                  </div>
                </div>
              )}
              {/* WHAT BECAME OF IT, where the record has an answer (0010,
                  reached 2026-08-24). Until then cw.concession_state's
                  'approved' and 'withdrawn' branches were unreachable — both
                  need a row nothing could write — so every concession was
                  permanently 'proposed' and these two lines could never draw. */}
              {c.state === 'approved' && c.settled_by && (
                <div className="caption mt-2" data-testid="concession-settled">
                  Settled by {c.settled_by} on {c.settled_on}.
                  {c.override_request_id && <> Override approval {c.override_request_id} recorded with this settlement.</>}
                  {c.requires_override && !c.override_request_id && <> No linked override approval was stored with this historical settlement.</>}
                </div>
              )}
              {c.state === 'withdrawn' && (
                <div className="caption mt-2" data-testid="concession-withdrawn">
                  Withdrawn by {c.withdrawn_by || '—'} on {c.withdrawn_on || '—'}.
                </div>
              )}
              {approverKind && c.state === 'proposed' && (
                <ApproveConcession concession={c} approverKind={approverKind}
                                   busy={busy} setBusy={setBusy}
                                   onError={onError} onChanged={onChanged} />
              )}
              {approverKind && c.state === 'proposed' && (
                <ConcessionOverride concession={c} requests={overrides}
                  requester={approverKind === 'requester'} busy={busy} setBusy={setBusy}
                  onError={onError} onChanged={() => { overrides.reload(); onChanged(); }} />
              )}
              {approverKind && c.state === 'proposed' && (
                <SettleOrWithdraw concession={c} busy={busy} setBusy={setBusy}
                                  approvedOverride={overrides.status === 'loaded'
                                    ? approvedConcessionOverride(overrides.rows, c) : null}
                                  onError={onError} onChanged={onChanged} />
              )}
            </div>
          ))}

      {/* THE ACT THAT CREATES THE THING THE FOUR ACTS ABOVE OPERATE ON.
          Offered on the same condition the position acts are — `0003`'s own
          policy admits the two Legal roles and a requester on a deal they own,
          which is what `acts` already carries. */}
      {acts && (
        <RecordConcession key={negotiation.negotiation_id}
          negotiation={negotiation} positions={positions} ladders={ladders}
          onError={onError} onChanged={onChanged} />
      )}
    </div>
  );
}

function approvedConcessionOverride(requests, concession) {
  return requests.filter(r => String(r.concession_id) === String(concession.concession_id)
    && r.agreement_id === concession.agreement_id && r.state === 'approved'
    && Number(r.findings) === 1 && Number(r.approved) === 1)
    .sort((a, b) => Number(b.request_id) - Number(a.request_id))[0] || null;
}

function ConcessionOverride({ concession, requests, requester, busy, setBusy, onError, onChanged }) {
  const draftKey = 'concession-override:' + concession.concession_id;
  const [draft, setDraft] = useRetainedState(draftKey, { justification: '', pressure: '' });
  const [confirming, setConfirming] = useState(false);
  if (!concession.requires_override) return null;
  if (requests.status === 'failed') return <LoadFailed reason={requests.reason} />;
  if (requests.status === 'loading') return <div className="caption mt-3">Checking this proposal's override requests…</div>;
  const linked = requests.rows.filter(r => String(r.concession_id) === String(concession.concession_id)
    && r.agreement_id === concession.agreement_id);
  const complete = draft.justification.trim().length >= 20;
  const update = (field, value) => {
    setConfirming(false); setDraft(previous => ({ ...previous, [field]: value }));
  };
  return (
    <div className="concession-approval-note mt-3" data-testid="concession-override">
      <div className="section-label">Separate override approval required</div>
      <div className="caption mt-1">This exact proposal needs Legal's override approval as well as the deal's named approvals.</div>
      {linked.map(r => <div className="panel-2 p-3 mt-3" key={r.request_id} data-testid="concession-override-request">
        <div className="caption">Request {r.request_id} · {r.state === 'requested' ? 'stakeholders not yet told'
          : r.state === 'socialised' ? (r.window_closed ? 'waiting for Legal'
            : 'review window closes ' + new Date(r.window_closes).toLocaleString()) : r.state}</div>
        <div className="concession-wording mt-2">{r.justification}</div>
        {r.state === 'requested' && <ActButton className="btn btn-sm mt-2" disabled={busy}
          data-testid="socialise-concession-override" onClick={async () => {
            setBusy(true); onError(null);
            const result = await API.socialiseOverride({ request_id: r.request_id });
            setBusy(false);
            if (!result.ok) { onError(result.reason); return; }
            onChanged();
          }}>tell the stakeholders · start review window</ActButton>}
        {r.state === 'socialised' && !requester && <a className="btn btn-sm mt-2" href="#/approvals">
          open Legal's approval desk</a>}
        {r.state === 'rejected' && <div className="caption mt-2">This request does not authorize settlement. The proposal can be withdrawn.</div>}
      </div>)}
      {linked.length === 0 && !requester && <div className="caption mt-2">
        The buyer on this deal must request the override. Legal decides it after the review window.
      </div>}
      {linked.length === 0 && requester && <>
        <label className="concession-field mt-3"><span className="section-label">Why accept this exception?</span>
          <textarea rows={3} aria-label="Override justification" data-testid="concession-override-justification"
            disabled={busy || confirming} value={draft.justification} onChange={e => update('justification', e.target.value)} />
        </label>
        <label className="concession-field mt-3"><span className="section-label">Commercial pressure · optional</span>
          <textarea rows={2} aria-label="Commercial pressure" data-testid="concession-override-pressure"
            disabled={busy || confirming} value={draft.pressure} onChange={e => update('pressure', e.target.value)} />
        </label>
        {confirming ? <div className="mt-3" data-testid="concession-override-confirm">
          <div className="caption">Request approval for proposal {concession.concession_id} on {concession.agreement_id}.
            This asks; it does not approve or settle the proposal.</div>
          <div className="flex flex-wrap gap-2 mt-2">
            <ActButton className="btn btn-primary" disabled={busy || !complete} data-testid="request-concession-override"
              onClick={async () => {
                setBusy(true); onError(null);
                const result = await API.requestConcessionOverride({ concession_id: concession.concession_id,
                  justification: draft.justification.trim(), commercial_pressure: draft.pressure.trim() || null });
                setBusy(false);
                if (!result.ok) { onError(result.reason); return; }
                discardDraft(draftKey); setConfirming(false); onChanged();
              }}>{busy ? 'requesting…' : 'record override request'}</ActButton>
            <button className="btn" disabled={busy} onClick={() => setConfirming(false)}>back to details</button>
          </div>
        </div> : <button className="btn mt-3" disabled={busy || !complete}
          data-testid="review-concession-override" onClick={() => setConfirming(true)}>review override request</button>}
      </>}
    </div>
  );
}

// Putting a concession in force, or taking it back (0010/0057, issue #156).
//
// THE GATE WAS REAL AND THE DOOR DID NOT EXIST. `0010` built both tables,
// `cw.concession_settlement_gate()` and the trigger that binds the actor;
// `0057` set the write policies; `analysis.py` reads the settlement to answer
// whether a concession is in force. **No endpoint wrote to either**, so a
// concession could be proposed and approved and then neither put in force nor
// taken back. `writes.py` carries the same sentence about the statement-of-work
// arc, which had the identical shape and was fixed a day earlier.
//
// SETTLING IS NOT A SECOND APPROVAL, and this screen must not read as one. The
// gate decides: it refuses a withdrawn concession, fails CLOSED on a deal with
// no assigned attorney, and refuses while any named approver is still
// outstanding — NAMING WHO. Its own message is the rule this product turns on:
// *"A machine may propose; only named people settle."* So the control is drawn
// whenever the concession is still open, and the gate's sentence is shown
// verbatim when it says no. Drawing it only when the approvals happen to be
// complete would mean this screen carrying a second copy of the gate's rule,
// which would then drift from it.
//
// A WITHDRAWAL DEMANDS A REASON and the table's own check refuses a blank one.
// It is one of only two free-text fields in this family, and a withdrawal
// nobody justified is indistinguishable from a mistake. There is no un-withdraw
// and none is offered: the settlement gate refuses a withdrawn concession
// outright, so changing your mind again means recording a new concession that
// somebody has to justify afresh.
function SettleOrWithdraw({ concession, approvedOverride, busy, setBusy, onError, onChanged }) {
  const [taking, setTaking] = useState(null);
  const [reason, setReason] = useState('');

  const run = async (what, body) => {
    setBusy(true); onError(null);
    const r = await what(body);
    setBusy(false);
    if (!r.ok) { onError(r.reason); return; }
    setTaking(null); setReason('');
    onChanged();
  };

  if (taking === 'withdraw') {
    return (
      <div className="panel-2 p-3 mt-2" data-testid="withdraw-concession">
        <div className="caption mb-2">
          Withdrawing is not undone — a settled concession cannot follow a
          withdrawn one. Say why, so the record is not just an absence.
        </div>
        <input className="w-full" data-testid="withdraw-reason"
               placeholder="why it is being taken back"
               value={reason} onChange={(e) => setReason(e.target.value)} />
        <div className="flex gap-2 mt-3">
          <ActButton className="btn btn-sm btn-primary"
                     disabled={busy || !reason.trim()}
                     data-testid="withdraw-go"
                     onClick={() => run(API.withdrawConcession, {
                       concession_id: concession.concession_id,
                       reason: reason.trim(),
                     })}>
            withdraw it
          </ActButton>
          <button className="btn btn-sm" onClick={() => { setTaking(null); setReason(''); }}>
            cancel
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex flex-wrap gap-2 mt-2" data-testid="settle-or-withdraw">
      <ActButton className="btn btn-sm" disabled={busy || (concession.requires_override && !approvedOverride)}
                 data-testid="settle-concession"
                 title="records it as agreed and binding, if everyone it needs has approved"
                 onClick={() => run(API.settleConcession,
                                    { concession_id: concession.concession_id,
                                      override_request_id: approvedOverride?.request_id ?? null })}>
        record it as settled
      </ActButton>
      <button className="btn btn-sm" disabled={busy}
              data-testid="withdraw-concession-open"
              onClick={() => setTaking('withdraw')}>
        withdraw it
      </button>
    </div>
  );
}

// Approving a concession, and saying why (0106).
//
// UNTIL 0106 THIS WAS A BUTTON. An approval named a person and recorded
// nothing — so the only account of why anything was acceptable was
// `cw.concession.reason`, written when the concession was first proposed, by
// somebody who was often not the person approving it.
//
// TWO DIFFERENT THINGS ARE ASKED HERE, and keeping them apart is the whole
// point. Why it is acceptable HERE is about this deal. Whether we should ever
// do it again is about the LIBRARY — and that judgment is what
// `cw.concession_rate` splits on and what the Library Builder drafts proposed
// standard wording from. Collapsed into one free-text box, "strategic
// customer, one-off" and "our standard position is wrong" are the same field.
//
// ONLY AN ATTORNEY IS ASKED THE SECOND QUESTION, and the database refuses it
// from anybody else (`only_an_attorney_judges_reuse`). A requester's reasoning
// is still worth having, so they get the first box and not the second.
//
// LEAVING IT BLANK IS A REAL ANSWER. Unjudged is not a synonym for one-off:
// the concession counts toward the patterns exactly as it did before 0106.
// Nothing here pre-selects a judgment, because a default would put an opinion
// on the record that nobody held.
function ApproveConcession({ concession, approverKind, busy, setBusy,
                             onError, onChanged }) {
  const [open, setOpen] = useState(false);
  const [rationale, setRationale] = useState('');
  const [reuse, setReuse] = useState('');

  if (!open) {
    return (
      <ActButton className="btn btn-sm mt-2" disabled={busy}
                 data-testid="approve-concession"
                 onClick={() => { onError(null); setOpen(true); }}>
        approve as {approverKind}
      </ActButton>
    );
  }

  return (
    <div className="panel-2 p-3 mt-2" data-testid="approve-form">
      <label className="block">
        <span className="caption">why is this acceptable here?</span>
        <textarea className="input mt-1" rows={2} value={rationale}
                  data-testid="approval-rationale"
                  onChange={(e) => setRationale(e.target.value)} />
      </label>

      {approverKind === 'attorney' && (
        <fieldset className="mt-3" style={{ border: 0, padding: 0, margin: 0 }}
                  data-testid="reuse-judgment">
          <legend className="caption" style={{ padding: 0 }}>
            should this apply again?
          </legend>
          {[['one_off', 'One-off — do not read this as a pattern'],
            ['similar_deals', 'Acceptable again in similar deals'],
            ['standard_is_wrong', 'Our standard position is wrong']].map(
            ([value, label]) => (
              <label key={value} className="flex items-center gap-2 mt-1"
                     style={{ fontSize: 14 }}>
                <input type="radio" name={`reuse-${concession.concession_id}`}
                       value={value} checked={reuse === value}
                       onChange={() => setReuse(value)} />
                {label}
              </label>
            ))}
          {/* THE ABSENCE, OFFERED AS A CHOICE rather than left as an oversight.
              It is also the state the form opens in. */}
          <label className="flex items-center gap-2 mt-1" style={{ fontSize: 14 }}>
            <input type="radio" name={`reuse-${concession.concession_id}`}
                   value="" checked={reuse === ''}
                   onChange={() => setReuse('')} />
            <span style={{ color: 'var(--mute)' }}>
              Not deciding that now — counts as it does today
            </span>
          </label>
        </fieldset>
      )}

      <div className="flex gap-2 mt-3">
        <ActButton className="btn btn-sm btn-primary" disabled={busy}
                   data-testid="record-approval"
                   onClick={async () => {
                     setBusy(true); onError(null);
                     const r = await API.approveConcession({
                       concession_id: concession.concession_id,
                       approver_kind: approverKind,
                       // Sent as null rather than '' so the record says NOT
                       // RECORDED rather than storing an empty opinion.
                       rationale: rationale.trim() || null,
                       reuse: reuse || null,
                     });
                     setBusy(false);
                     if (!r.ok) { onError(r.reason); return; }
                     setOpen(false);
                     onChanged();
                   }}>
          record approval
        </ActButton>
        <button className="btn btn-sm" onClick={() => setOpen(false)}>
          cancel
        </button>
      </div>

      <div className="caption mt-2" style={{ lineHeight: 1.7 }}>
        Both answers are written into the record permanently and cannot be
        revised — changing your mind about a concession is a withdrawal, which
        is its own record with its own reason.
      </div>
    </div>
  );
}

// ── One negotiation, opened ───────────────────────────────────────────────
function OpenNegotiation({ negotiation, record, deals, categories, onBack,
                          acts, approverKind }) {
  const deal = deals.find((d) => d.agreement_id === negotiation.agreement_id);
  const roundNow = record.rounds.rows
    .filter((r) => r.negotiation_id === negotiation.negotiation_id)
    .reduce((n, r) => Math.max(n, r.round_no), 0);

  return (
    <div>
      <button className="btn btn-sm mb-4" onClick={onBack}>← negotiations</button>

      <div className="panel p-4">
        <div className="flex items-baseline justify-between">
          <div>
            {/* THE RECORD'S OWN TITLE IS A HEADING. #14 gave all 37 panes a
                page title and measured them in their LIST state; a record
                that REPLACES its pane was never in that census, and these
                views had no heading at all — the big line naming the
                counterparty was a styled div. The deal room already did this
                correctly, which is what showed the other two up.

                Tailwind's preflight resets heading size, weight and margin,
                so the element changes and nothing moves. */}
            <h1 className="display-sm">{deal ? deal.counterparty : negotiation.agreement_id}</h1>
            <div className="font-mono caption mt-1">
              {negotiation.agreement_id} · negotiation {negotiation.negotiation_id}
            </div>
          </div>
          <div className="text-right">
            <span className="chip chip-std">
              {negotiation.paper === 'ours' ? 'our paper' : 'their paper'}
            </span>
            <div className="caption mt-1">
              opened {negotiation.opened_on} by {negotiation.opened_by}
            </div>
          </div>
        </div>
      </div>

      <Drift negotiation={negotiation} drift={record.drift.rows} />
      <ReviewActionFeedback label="Recording a round">
        {(report) => <Rounds negotiation={negotiation} rounds={record.rounds.rows}
              runs={record.runs}
              onError={report} onRecorded={record.reloadAll} canAct={acts} />}
      </ReviewActionFeedback>
      <ReviewActionFeedback label="Negotiation points">
        {(report) => <Positions key={negotiation.negotiation_id} negotiation={negotiation} positions={record.positions.rows}
                 movements={record.movements.rows} roundNow={roundNow}
                 categories={categories} onError={report}
                 onChanged={record.reloadAll} acts={acts}
                 positionMoves={record.positionMoves} />}
      </ReviewActionFeedback>
      <Revivals negotiation={negotiation} revivals={record.revivals.rows} />
      <ReviewActionFeedback label="Concession decisions">
        {(report) => <Concessions negotiation={negotiation} concessions={record.concessions.rows}
                   positions={record.positions.rows} ladders={record.ladders}
                   acts={acts}
                   onError={report} onChanged={record.reloadAll}
                   approverKind={approverKind} />}
      </ReviewActionFeedback>

      {/* WHICH OTHER CONTRACTS WITH THIS SUPPLIER DOES THIS CHANGE TOUCH
          (0114). Last on the page and not first: it is context for the moves
          above it, never a step before them.

          IT WARNS AND IT NEVER GATES. Mike's decision, and it is absolute —
          nothing here disables a control, delays a round or refuses a
          concession. Legal owns the decision; this is a list.

          NO ROLE CHECK, deliberately. `cw.cross_contract_echo` is scoped
          through `cw.contract_paragraph` on both sides, so a requester sees
          echoes only among their own deals. The same component on Legal's
          ticket desk returns more rows from the same statement, which is
          exactly what "the same warning in two places" means. */}
      <CrossContractEchoes agreementId={negotiation.agreement_id} />
    </div>
  );
}

// ── Opening one ───────────────────────────────────────────────────────────
// Two acts, not one act with a switch: opening on fresh paper and opening a
// renewal from last term's executed positions are different commercial
// decisions, and six months later somebody needs to know which happened.
function OpenANegotiation({ deals, negotiations, onError, onOpened }) {
  const [fresh, setFresh] = useState({
    agreement_id: '', paper: 'ours', baseline: 'library_standard',
    renews_agreement_id: '', note: '',
  });
  const [busy, setBusy] = useState(false);

  const already = new Set(negotiations.map((n) => n.agreement_id));
  // LIVE DEALS ONLY. `status !== 'executed'` also let through every
  // TERMINATED deal, so the picker offered to open a fresh negotiation on a
  // contract that had already ended. isLive is stageOf's own answer.
  const available = deals.filter((d) => !already.has(d.agreement_id) && isLive(d));
  const renewal = fresh.renews_agreement_id.trim() !== '';

  return (
    <div className="panel p-4 mt-6">
      <PanelHead
        title="Open a negotiation"
        sub="One negotiation per deal. Which positions it starts from is recorded, because starting from last term's compromises is a different act from starting from standard." />

      {available.length === 0
        ? <div className="caption">
            Every deal of yours that could carry a negotiation already has one.
          </div>
        : (
          <>
            <div className="flex gap-2 items-end flex-wrap">
              <div style={{ width: 240 }}>
                <label className="section-label">Deal</label>
                <select aria-label="Deal" className="mt-1.5 w-full font-mono" style={{ padding: '5px 8px' }}
                        data-testid="negotiation-deal"
                        value={fresh.agreement_id}
                        onChange={(e) => setFresh({ ...fresh, agreement_id: e.target.value })}>
                  <option value="">choose…</option>
                  {available.map((d) => (
                    <option key={d.agreement_id} value={d.agreement_id}>
                      {d.agreement_id} — {d.counterparty}
                    </option>
                  ))}
                </select>
              </div>
              <div style={{ width: 140 }}>
                <label className="section-label">Whose paper</label>
                <select aria-label="Whose paper" className="mt-1.5 w-full font-mono" style={{ padding: '5px 8px' }}
                        value={fresh.paper}
                        onChange={(e) => setFresh({ ...fresh, paper: e.target.value })}>
                  <option value="ours">ours</option>
                  <option value="theirs">theirs</option>
                </select>
              </div>
              <div style={{ width: 200 }}>
                <label className="section-label">Starting from</label>
                <select aria-label="Starting from" className="mt-1.5 w-full font-mono" style={{ padding: '5px 8px' }}
                        value={fresh.baseline}
                        onChange={(e) => setFresh({ ...fresh, baseline: e.target.value })}>
                  <option value="library_standard">library standard</option>
                  <option value="executed_agreement">last term's positions</option>
                </select>
              </div>
              <div style={{ width: 200 }}>
                <label className="section-label">Renews (optional)</label>
                <input aria-label="Renews (optional)" className="mt-1.5 w-full font-mono" style={{ padding: '5px 8px' }}
                       placeholder="AG-001"
                       value={fresh.renews_agreement_id}
                       onChange={(e) => setFresh({ ...fresh, renews_agreement_id: e.target.value })} />
              </div>
            </div>

            <div className="mt-3">
              <label className="section-label">Why this starting point</label>
              <input aria-label="Why this starting point" className="mt-1.5 w-full" value={fresh.note}
                     onChange={(e) => setFresh({ ...fresh, note: e.target.value })} />
            </div>

            <ActButton className="btn btn-primary mt-3" disabled={busy || !fresh.agreement_id}
                    data-testid="open-negotiation"
                    onClick={async () => {
                      setBusy(true); onError(null);
                      const body = renewal
                        ? {
                            agreement_id: fresh.agreement_id,
                            renews_agreement_id: fresh.renews_agreement_id.trim(),
                            paper: fresh.paper,
                            baseline: fresh.baseline,
                            note: fresh.note.trim() || null,
                          }
                        : {
                            agreement_id: fresh.agreement_id,
                            paper: fresh.paper,
                            baseline: fresh.baseline,
                            baseline_note: fresh.note.trim() || null,
                          };
                      const r = renewal ? await API.openRenewal(body)
                                        : await API.openNegotiation(body);
                      setBusy(false);
                      if (!r.ok) { onError(r.reason); return; }
                      setFresh({ agreement_id: '', paper: 'ours',
                                 baseline: 'library_standard',
                                 renews_agreement_id: '', note: '' });
                      onOpened();
                    }}>
              ✓ {renewal ? 'open the renewal' : 'open it'}
            </ActButton>
            <div className="caption mt-2">
              {renewal
                ? 'A renewal seeds its positions from the agreement it renews, and the drift report opens with it.'
                : 'Naming an agreement above turns this into a renewal, which is a different recorded act.'}
            </div>
          </>
        )}
    </div>
  );
}

// ── The points register, and the concessions register ─────────────────────
//
// A NEGOTIATION IS THE FOLDER; A POINT IS THE WORK. Both panes below carried
// figures counting POINTS over a list holding NEGOTIATIONS, and this file's own
// comment gave the right reason for leaving them inert: a figure whose unit is
// not the list's unit has nowhere honest to go, and making it narrow the
// negotiation list would be a control that leads somewhere other than where it
// says it leads. The remedy was never to break that rule at two more sites. It
// is to give the points a list whose unit is a point.
//
// ONE SET OF COMPONENTS, TWO PANES, TWO DIFFERENT ANSWERS — and the difference
// is the database's, not this screen's. `cw.position_current` scopes itself in
// its own WHERE clause (0027), so a requester's register holds the points on
// deals they own and Legal's holds every one, for exactly the reason the two
// negotiation lists already differ. Nothing here filters for permission.
//
// THE PREDICATES ARE NAMED ONCE AND USED TWICE — as the figure's count and as
// the focus's test. That is what makes the number on the tile and the rows you
// land on ONE SET by construction rather than by coincidence, which is the rule
// `a-figure-and-its-drill-are-one-set` exists to hold still. Two copies of a
// predicate stop agreeing; there is one copy of each of these.
const POINT_IS_OPEN = (p) => !['settled', 'withdrawn'].includes(p.state);
const POINT_IS_WITH_LEGAL = (p) => p.state === 'escalated';
const CONCESSION_IS_PROPOSED = (c) => c.state === 'proposed';

// The rows the register runs over: a position with the deal it belongs to
// joined in at render time, the same way the negotiation lists join the
// counterparty in. A HOOK — so it is called above every early return, with the
// others, or React blanks the pane.
//
// The joined-in columns are what a person searches by. `state` is the facet
// because a position has SIX domain states (0027) and which one a point is in
// is the question this list is for.
function usePointsRegister(record) {
  const rows = (record.positions?.rows ?? []).map((p) => {
    const n = (record.negotiations?.rows ?? [])
      .find((x) => x.negotiation_id === p.negotiation_id) || {};
    const d = (record.deals?.rows ?? [])
      .find((x) => x.agreement_id === n.agreement_id) || {};
    return {
      ...p,
      agreement_id: n.agreement_id || '',
      counterparty: d.counterparty || '',
      negotiation_state: n.state || '',
    };
  });
  return useListFilter(rows, {
    view: 'negotiation:points',
    fields: ['agreement_id', 'counterparty', 'category_key'],
    facet: 'state',
  });
}

// The same, for concessions. A concession is recorded against the AGREEMENT
// (0028), not against the negotiation, so the negotiation is looked up by
// agreement_id — and where there is none, the row still shows and simply has
// nothing to open. A row that vanished because its folder is missing would be
// the register quietly deciding which concessions are worth showing.
function useConcessionsRegister(record) {
  const rows = (record.concessions?.rows ?? []).map((c) => {
    const n = (record.negotiations?.rows ?? [])
      .find((x) => x.agreement_id === c.agreement_id) || {};
    const d = (record.deals?.rows ?? [])
      .find((x) => x.agreement_id === c.agreement_id) || {};
    return {
      ...c,
      counterparty: d.counterparty || '',
      negotiation_id: n.negotiation_id ?? null,
    };
  });
  return useListFilter(rows, {
    view: 'negotiation:concessions',
    fields: ['agreement_id', 'counterparty', 'category_key'],
    facet: 'state',
  });
}

// One row of the points register. The state is drawn as a plain word with the
// rung beside it, never as a status mark — the six position states are not the
// five in common.jsx's vocabulary, and this file has said so since it was
// written. The one visual weight is on a point that is with Legal, because
// that is a person waiting for an answer.
function PointsRegister({ filter, onOpen, title, sub }) {
  return (
    <div className="mt-6" data-testid="points-register">
      <PanelHead title={title} sub={sub} right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="points"
                  placeholder="deal, counterparty or category"
                  facetLabel="any state" />
      {filter.shown.length === 0 ? (
        filter.filtering
          ? <NoMatch kicker="points" noun="point" />
          : <Empty kicker="points"
                   line="No point has been raised."
                   sub="A point is raised inside a negotiation, against a category the library already names. Open a negotiation to raise one." />
      ) : (
        <div className="panel">
          <table className="ledger">
            <thead>
              <tr>
                <th>Deal ref.</th><th>Counterparty</th><th>Category</th>
                <th>State</th><th>Rung</th><th>Raised</th><th>Last moved</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((p) => (
                <tr key={p.position_id}
                    {...(p.negotiation_id !== null && p.negotiation_id !== undefined
                      ? openableRow(() => onOpen(String(p.negotiation_id)),
                          `open the negotiation holding the ${p.category_key} point on ${p.agreement_id || 'this deal'}`)
                      : {})}>
                  <td className="mono">{p.agreement_id || '—'}</td>
                  <td>{p.counterparty || '—'}</td>
                  <td className="mono">{p.category_key}</td>
                  <td>
                    {POINT_IS_WITH_LEGAL(p)
                      ? <span className="chip chip-pending">{p.state}</span>
                      : <span className="caption">{p.state}</span>}
                  </td>
                  <td className="mono">
                    {p.current_rung !== null && p.current_rung !== undefined
                      ? p.current_rung : '—'}
                  </td>
                  <td className="mono">{p.round_raised ?? '—'}</td>
                  <td className="caption">
                    {p.round_last_moved === null || p.round_last_moved === undefined
                      ? 'not moved'
                      : `round ${p.round_last_moved} by ${p.moved_by}`}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// The concessions register. A concession's approval state IS a status mark —
// an approved one is in force and a proposed one confers nothing — which is
// the distinction `Concessions` above already draws, kept identical here so
// the same fact does not read two ways on two screens.
function ConcessionsRegister({ filter, onOpen }) {
  return (
    <div className="mt-6" data-testid="concessions-register">
      <PanelHead
        title="Concessions, across every deal"
        sub="Where the company has settled below standard, or been asked to. A proposal confers nothing until a second person approves it."
        right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="concessions"
                  placeholder="deal, counterparty or category"
                  facetLabel="any state" />
      {filter.shown.length === 0 ? (
        filter.filtering
          ? <NoMatch kicker="concessions" noun="concession" />
          : <Empty kicker="concessions"
                   line="Nothing has been conceded anywhere."
                   sub="A concession is recorded inside a negotiation, against the standard wording it settles below." />
      ) : (
        <div className="panel">
          <table className="ledger">
            <thead>
              <tr>
                <th>Deal ref.</th><th>Counterparty</th><th>Category</th>
                <th>Standard</th><th>To rung</th><th>Proposed by</th><th>State</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((c) => (
                <tr key={c.concession_id}
                    {...(c.negotiation_id !== null && c.negotiation_id !== undefined
                      ? openableRow(() => onOpen(String(c.negotiation_id)),
                          `open the negotiation holding the ${c.category_key} concession on ${c.agreement_id}`)
                      : {})}>
                  <td className="mono">{c.agreement_id}</td>
                  <td>{c.counterparty || '—'}</td>
                  <td className="mono">{c.category_key}</td>
                  <td className="mono">
                    {c.standard_clause_id ? `${c.standard_clause_id} v${c.standard_version}` : '—'}
                  </td>
                  <td className="mono">
                    {c.conceded_rung !== null && c.conceded_rung !== undefined
                      ? c.conceded_rung : '—'}
                  </td>
                  <td className="caption">{c.proposer_kind || '—'}</td>
                  <td>
                    <Status state={c.state === 'approved' ? 'effective'
                                 : c.state === 'withdrawn' ? 'superseded' : 'pending'}>
                      {c.state}
                    </Status>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── NG-1 · the requester's negotiate tab ──────────────────────────────────
function NegotiatePane({ me }) {
  const record = useNegotiationRecord();
  const cats  = usePane(() => API.library());
  // The same two reads Legal's desk makes. 0052 and 0053 admit a requester on
  // deals they own -- for the analysis, the WRITE as well as the read -- so
  // the difference between the two compositions was never a rule, and the
  // person who has to live with what the counterparty sent was the one who
  // could not ask about it. ABOVE THE EARLY RETURNS, with every other hook.
  const analysis = usePane(() => API.paragraphReadings());
  const risk = usePane(() => API.riskAssessments());
  // AI-6 (0104). Read here rather than inside the panel so the pane owns
  // one read and hands it down — two reads of the same question can
  // disagree with each other.
  const drafts = usePane(() => API.negotiationDrafts());
  const [showing, setShowing] = useAddressedRecord('negotiate');
  const [error, setError] = useState(null);

  // The same pair the desk below carries. Thirty-six negotiations is not a
  // list you read top to bottom, and the requester's copy was the one left
  // without a way through — the fourth site of the same idiom.
  //
  // ABOVE EVERY EARLY RETURN: a hook after a conditional `return` runs on some
  // renders and not others, and React blanks the pane with "Rendered more
  // hooks than during the previous render". That happened here, in this file
  // and two others, before it was caught by walking the panes.
  const withParty = (record.negotiations?.rows ?? []).map((n) => ({
    ...n,
    counterparty: ((record.deals?.rows ?? [])
      .find((d) => d.agreement_id === n.agreement_id) || {}).counterparty || '',
  }));
  const filter = useListFilter(withParty, {
    view: 'negotiate:mine',
    fields: ['agreement_id', 'counterparty'],
    facet: 'paper',
  });
  // The points on those same deals, as their own list. ALSO ABOVE THE EARLY
  // RETURNS, and for the same reason as everything else here.
  const points = usePointsRegister(record);
  // Registered at render, so a saved view can put each focus back (handoff 43 §7).
  const openFocus = filter.focusable('open', 'open negotiations',
    (n) => n.state === 'open');
  const contestedFocus = points.focusable('contested', 'points still contested',
    POINT_IS_OPEN);
  const withLegalFocus = points.focusable('with-legal', 'points handed to Legal',
    POINT_IS_WITH_LEGAL);

  if (record.loading) return <Loading />;
  if (record.failed) return <LoadFailed reason={record.failed.reason} />;
  const deals = record.deals;

  const categories = categoriesFrom(cats.rows);
  const negotiation = record.negotiations.rows
    .find((n) => String(n.negotiation_id) === String(showing));

  if (showing && negotiation) {
    return (
      <div data-testid="requester-negotiation" key={negotiation.negotiation_id}>
        <OpenNegotiation
          negotiation={negotiation} record={record} deals={deals.rows}
          categories={categories} onBack={() => setShowing(null)}
          acts approverKind="requester" />
        <ReviewActionFeedback label="Filing supplier paper">
          {(report) => <TheirOwnPaper negotiation={negotiation} onError={report}
                       onFiled={() => record.reloadAll()} />}
        </ReviewActionFeedback>
        <ReviewActionFeedback label="Asking for analysis">
          {(report) => <AskTheMachine negotiation={negotiation} onError={report}
                       onAsked={() => { analysis.reload(); risk.reload(); }} />}
        </ReviewActionFeedback>
        <RoundAnalysis negotiation={negotiation}
                       analysis={analysis} risk={risk} />
        <TheyProposedForYou negotiation={negotiation} drafts={drafts} />
      </div>
    );
  }

  return (
    <div>
      {/* THREE FIGURES, TWO LISTS, AND EACH ONE LEADS TO THE LIST WHOSE UNIT IT
          COUNTS. The first counts NEGOTIATIONS and narrows the negotiation
          list. The other two count POINTS — which is not what that list holds,
          and this file left them inert rather than pretend they could narrow
          it. They now narrow the points register below, whose rows ARE points.

          Every one of the three is counted off the WHOLE record, never off
          `shown`: a figure computed from the narrowed set becomes its own
          total the moment it is pressed. */}
      <TileStrip tiles={[
        { label: 'negotiations open',
          n: record.negotiations.rows.filter((n) => n.state === 'open').length,
          to: () => filter.focusOn(openFocus),
          on: filter.focus && filter.focus.key === 'open' },
        { label: 'points contested',
          n: record.positions.rows.filter(POINT_IS_OPEN).length,
          to: () => points.focusOn(contestedFocus),
          on: points.focus && points.focus.key === 'contested' },
        { label: 'with Legal', n: record.positions.rows.filter(POINT_IS_WITH_LEGAL).length,
          to: () => points.focusOn(withLegalFocus),
          on: points.focus && points.focus.key === 'with-legal' },
      ]} />

      {error && <RefusalNote reason={error} />}

      <div className="mt-6">
        <PaneHead title="My negotiations"
                   sub="Only deals of yours. Nobody else's reaches this browser — the database scopes it, not this screen."
                   right={<FilterCount filter={filter} />} />
        <ListFilter filter={filter} testid="my-negotiations"
                    placeholder="deal or counterparty"
                    facetLabel="either paper" />
        <WaitingList
          order="oldest"
          items={filter.shown.map((n) => NegotiationRow({
            negotiation: n, deals: deals.rows,
            positions: record.positions.rows, rounds: record.rounds.rows }))}
          onOpen={(it) => setShowing(it.key)}
          empty={filter.filtering
            ? <NoMatch kicker="negotiations" noun="negotiation" />
            : <Empty
                kicker="negotiations"
                line="You have nothing under negotiation."
                sub="A negotiation is opened against a deal you already hold. Open one below when the counterparty starts marking up the paper." />}
        />
      </div>

      <PointsRegister
        filter={points} onOpen={setShowing}
        title="Every point on your deals"
        sub="One row per contested point, across all of your negotiations — because a point, not a negotiation, is the thing somebody is waiting on. Only your deals: the database scopes this list, not this screen." />

      <OpenANegotiation deals={deals.rows} negotiations={record.negotiations.rows}
                        onError={setError} onOpened={record.reloadAll} />
    </div>
  );
}

// ── NG-2 / NG-3 · the Legal desk ──────────────────────────────────────────
// One pane, held by the Legal reviewer and the Legal admin. Ordered by what is
// waiting on Legal rather than by what is newest: an escalated point is a
// person waiting for an answer.
function NegotiationsDeskPane({ me }) {
  const record = useNegotiationRecord();
  const cats  = usePane(() => API.library());
  // THE PAIRING VIEW, not the raw analysis list (0103). Same rows, same
  // analysis_id — so the risk estimate still joins — plus the model's opinion
  // where one was asked for. `API.roundAnalysis()` still answers the scorer's
  // readings alone and is what the deal room reads.
  const analysis = usePane(() => API.paragraphReadings());
  const risk = usePane(() => API.riskAssessments());
  // AI-6 (0104). Read here rather than inside the panel so the pane owns
  // one read and hands it down — two reads of the same question can
  // disagree with each other.
  const drafts = usePane(() => API.negotiationDrafts());
  const [showing, setShowing] = useAddressedRecord('negotiations');

  // Same enrichment as the deal room, for the same reason: the counterparty is
  // what a person searches by, and it is joined in at render time.
  // ABOVE EVERY EARLY RETURN. A hook called after a conditional `return` runs
  // on some renders and not others, and React refuses the second one with
  // "Rendered more hooks than during the previous render" — the pane goes
  // blank. It reads `.rows` defensively because the record is still empty on
  // the render before the read lands.
  const withParty = (record.negotiations?.rows ?? []).map((n) => ({
    ...n,
    counterparty: ((record.deals?.rows ?? [])
      .find((d) => d.agreement_id === n.agreement_id) || {}).counterparty || '',
  }));
  const filter = useListFilter(withParty, {
    view: 'negotiations:legal',
    fields: ['agreement_id', 'counterparty'],
    facet: 'paper',
  });
  // The two registers this desk's figures narrow. ABOVE THE EARLY RETURNS.
  const points = usePointsRegister(record);
  const concessions = useConcessionsRegister(record);
  // Registered at render, so a saved view can put each focus back (handoff 43 §7).
  const openFocus = filter.focusable('open', 'open negotiations',
    (n) => n.state === 'open');
  const withLegalFocus = points.focusable('with-legal', 'points handed to Legal',
    POINT_IS_WITH_LEGAL);
  const proposedFocus = concessions.focusable('proposed',
    'concessions waiting on approval', CONCESSION_IS_PROPOSED);

  if (record.loading) return <Loading />;
  if (record.failed) return <LoadFailed reason={record.failed.reason} />;
  const deals = record.deals;

  const categories = categoriesFrom(cats.rows);
  // The open negotiation is found in the WHOLE record, never the filtered
  // view — one on screen must not vanish because somebody typed in a search
  // box.
  const negotiation = record.negotiations.rows
    .find((n) => String(n.negotiation_id) === String(showing));

  if (showing && negotiation) {
    return (
      <div data-testid="legal-negotiation" key={negotiation.negotiation_id}>
        <OpenNegotiation
          negotiation={negotiation} record={record} deals={deals.rows}
          categories={categories} onBack={() => setShowing(null)}
          acts approverKind="attorney" />
        <ReviewActionFeedback label="Filing supplier paper">
          {(report) => <TheirOwnPaper negotiation={negotiation} onError={report}
                       onFiled={() => record.reloadAll()} />}
        </ReviewActionFeedback>
        <ReviewActionFeedback label="Asking for analysis">
          {(report) => <AskTheMachine negotiation={negotiation} onError={report}
                       onAsked={() => { analysis.reload(); risk.reload(); }} />}
        </ReviewActionFeedback>
        <RoundAnalysis negotiation={negotiation}
                       analysis={analysis} risk={risk} />
        <ReviewActionFeedback label="Drafting a proposed reply">
          {(report) => <WhatWeMightSendBack negotiation={negotiation} analysis={analysis}
                             drafts={drafts} onError={report}
                             onDrafted={() => drafts.reload()} />}
        </ReviewActionFeedback>
        <MovesAside negotiation={negotiation}
                    positionMoves={record.positionMoves} />
      </div>
    );
  }

  // THE SAME PREDICATES THE FIGURES AND THE FOCUSES USE. Written once, above,
  // so the queue below, the figure over it and the rows it lands on cannot
  // drift into three slightly different ideas of "with Legal".
  const escalated = record.positions.rows.filter(POINT_IS_WITH_LEGAL);
  const proposed = record.concessions.rows.filter(CONCESSION_IS_PROPOSED);

  return (
    <div>
      <div className="sheet-head">
        <h1 className="sheet-title">Legal Deals</h1>
        <div className="flex items-center gap-4">
          {escalated.length > 0 && (
            <span className="stamp stamp-pending" style={{ '--rot': '1.2deg' }}>
              {escalated.length} waiting on Legal
            </span>
          )}
          <span className="sheet-note">
            {record.negotiations.rows.length} negotiations · every deal — Legal's
            read is not scoped to one
          </span>
        </div>
      </div>
      <div className="font-serif italic mt-2" style={{ fontSize: 14, color: 'var(--mute)' }}>
        The active negotiations and what waits on Legal.
      </div>

      <div className="mt-5 grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))' }}>
        {/* BOTH OF THESE LED NOWHERE, and the brief's account of why was wrong:
            it said they "already carry a `to`" and were drawn inert only
            because the seeded count is nought. Read against the code, neither
            carried one at all — `StatBox` draws the drill only when a `to` is
            passed, whatever the number is. Counted against the file, not
            against the paragraph. */}
        <StatBox label="waiting on Legal" n={escalated.length}
                 describe={`show the ${escalated.length} points handed to Legal`}
                 to={() => points.focusOn(withLegalFocus)}
                 on={points.focus && points.focus.key === 'with-legal'} />
        <StatBox label="concessions to approve" n={proposed.length}
                 describe={`show the ${proposed.length} concessions waiting on approval`}
                 to={() => concessions.focusOn(proposedFocus)}
                 on={concessions.focus && concessions.focus.key === 'proposed'} />
        <StatBox label="negotiations open"
                 n={record.negotiations.rows.filter((n) => n.state === 'open').length}
                 describe={`show the ${record.negotiations.rows.filter((n) => n.state === 'open').length} open negotiations`}
                 to={() => filter.focusOn(openFocus)} />
      </div>

      <div className="mt-6">
        <PanelHead title="Handed to Legal"
                   sub="Points a requester escalated, oldest first. This is the queue; everything below it is context." />
        {escalated.length === 0
          ? <Empty kicker="escalations"
                   line="Nothing has been handed to Legal."
                   sub="No requester is waiting on a judgement about a negotiating position." />
          : escalated.map((p) => {
              const n = record.negotiations.rows
                .find((x) => x.negotiation_id === p.negotiation_id);
              return (
                <div className="panel-2 p-3 mb-2 flex items-baseline justify-between"
                     key={p.position_id} data-testid="escalated-position">
                  <div>
                    <span className="font-mono text-[12.5px]">{p.category_key}</span>
                    <span className="ml-3 caption">
                      {n ? n.agreement_id : `negotiation ${p.negotiation_id}`}
                      {p.current_rung !== null && p.current_rung !== undefined
                        && ` · at rung ${p.current_rung}`}
                      {' '}· moved round {p.round_last_moved} by {p.moved_by}
                    </span>
                  </div>
                  <button className="btn btn-sm"
                          onClick={() => setShowing(String(p.negotiation_id))}>
                    open
                  </button>
                </div>
              );
            })}
      </div>

      <div className="mt-6">
        <PanelHead title="Every negotiation"
                   sub="All of them, because Legal's read is not scoped to a deal they own."
                   right={<FilterCount filter={filter} />} />
        <ListFilter filter={filter} testid="negotiations"
                    placeholder="deal or counterparty"
                    facetLabel="either paper" />
        {filter.shown.length === 0 ? (
          filter.filtering
            ? <NoMatch kicker="negotiations" noun="negotiation" />
            : <Empty kicker="negotiations"
                     line="No negotiation is open anywhere."
                     sub="Nothing is being negotiated in the system at all — not a filter, the whole record." />
        ) : (
          <div className="panel">
            <table className="ledger">
              <thead>
                <tr>
                  <th>Deal ref.</th><th>Counterparty</th><th>Paper</th>
                  {/* WHETHER IT IS STILL RUNNING. This desk listed thirty-six
                      negotiations of which twenty-four were closed, and no
                      column said which — Legal triaged live and finished work
                      from the same table with nothing to tell them apart. */}
                  <th>State</th>
                  <th>Round</th><th>Points</th><th>With Legal</th>
                </tr>
              </thead>
              <tbody>
                {filter.shown.map((n) => {
                  const deal = deals.rows.find((d) => d.agreement_id === n.agreement_id);
                  const mine = record.positions.rows
                    .filter((p) => p.negotiation_id === n.negotiation_id);
                  const open = mine.filter((p) => !['settled', 'withdrawn'].includes(p.state));
                  const withLegal = mine.filter((p) => p.state === 'escalated');
                  const lastRound = record.rounds.rows
                    .filter((r) => r.negotiation_id === n.negotiation_id)
                    .reduce((x, r) => Math.max(x, r.round_no), 0);
                  return (
                    <tr key={n.negotiation_id}
                        {...openableRow(() => setShowing(String(n.negotiation_id)),
                          `open the negotiation on ${n.agreement_id}`)}>
                      <td className="mono">
                        {n.agreement_id}
                        {n.renews_agreement_id && (
                          <span className="chip chip-std ml-2"
                                title={`renews ${n.renews_agreement_id}`}>renewal</span>
                        )}
                      </td>
                      <td>{deal ? deal.counterparty : '—'}</td>
                      <td className="mono">{n.paper === 'ours' ? 'ours' : 'theirs'}</td>
                      <td>
                        {n.state === 'open'
                          ? <span className="chip chip-std">open</span>
                          : <span className="chip chip-gone">{n.state}</span>}
                      </td>
                      <td className="mono">{lastRound || 'none yet'}</td>
                      {/* "0 open" under a heading reading "Positions" sat one
                          column from the negotiation's own state. It counts
                          POINTS, and now says so in its heading. */}
                      <td className="mono">{open.length} open</td>
                      <td>
                        {withLegal.length > 0
                          ? <span className="chip chip-pending">{withLegal.length} escalated</span>
                          : <span className="caption">—</span>}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </div>

      <PointsRegister
        filter={points} onOpen={setShowing}
        title="Every point, across every deal"
        sub="The panel above is the queue — what a requester has handed to Legal and is waiting on. This is the whole argument: every point on every negotiation, in whatever state it is in." />

      <ConcessionsRegister filter={concessions} onOpen={setShowing} />
    </div>
  );
}

// ── Advice on the record ──────────────────────────────────────────────────
// Everything here GATES NOTHING and says so. It is the model's opinion beside
// the position it touched, kept because it was given, not because it decided.
// -- Their own paper, in ---------------------------------------------------
//
// A DIFFERENT ACT FROM RECORDING A REDLINE, and the difference is worth
// saying: a redline is the counterparty marking up paper we sent. This is the
// counterparty's own agreement arriving whole -- their standard terms, their
// order form, the document they would rather we signed.
//
// WHAT HAPPENS TO IT (RP-05). It is parsed, every nonblank paragraph is
// classified against the category vocabulary by arithmetic, and each unit
// lands as a QUARANTINED review ticket. Quarantined is the schema's word, not
// a description: no selectable view reaches that text, no assembly can draw
// on it, and it becomes language this company will sign only when a named
// lawyer approves wording for it and the database mints a clause version.
//
// SO THE MACHINE'S WHOLE CONTRIBUTION HERE IS SORTING THE POST. It reads a
// document nobody has read yet and puts each part of it in front of the
// person who has to decide about it. It decides nothing.
//
// Served by the doorway since RP-05 and reachable from no screen until now.
function TheirOwnPaper({ negotiation, onFiled, onError }) {
  const [busy, setBusy] = useState(false);
  const [filed, setFiled] = useState(null);

  return (
    <div className="panel p-4 mt-6" data-testid="their-own-paper">
      <PanelHead
        title="Their own paper"
        sub="Their agreement, arriving whole rather than as a markup of ours. Every paragraph is classified and filed as a quarantined ticket for Legal — none of it can reach a contract before a lawyer approves wording for it." />

      <div className="flex items-center gap-3 mt-3">
        <input type="file" data-testid="paper-upload" disabled={busy}
               aria-label="Choose the counterparty's own agreement"
               onChange={async (e) => {
                 const file = e.target.files && e.target.files[0];
                 e.target.value = '';
                 if (!file) return;
                 setBusy(true); setFiled(null); onError(null);
                 const answer = await API.ingestPaper(
                   negotiation.agreement_id, file);
                 setBusy(false);
                 if (!answer.ok) { onError(answer.reason); return; }
                 setFiled(answer.body || {});
                 onFiled();
               }} />
        <span className="caption">
          A .docx. The fingerprint is the database's own arithmetic over the
          bytes — nothing on this screen can supply one.
        </span>
      </div>

      {filed && (
        <div className="panel-2 p-3 mt-3" data-testid="paper-filed">
          <div className="font-mono text-[12.5px]">
            {filed.paragraphs} paragraphs read · {filed.tickets_opened} filed
            for Legal · {filed.unclassified_paragraphs} matched no category
          </div>
          <div className="caption mt-1" style={{ lineHeight: 1.7 }}>
            Classified by {filed.classifier}. Nothing was approved, and nothing
            became language this company will sign.
          </div>
          {/* THE SAME THIRD STATE THE ROUND ANALYSIS HAS. `paper.py` files a
              ticket per classified unit and NONE for a paragraph it could not
              classify — a ticket names a category (0008) and there is none. So
              those paragraphs are in the document, in nobody's queue, and this
              is the only place that can say so. */}
          {(filed.unclassified_paragraphs || 0) > 0 && (
            <div className="caption mt-1" data-testid="paper-unplaced"
                 style={{ lineHeight: 1.7, color: 'var(--accent-2)' }}>
              The {filed.unclassified_paragraphs} that matched no category
              opened no ticket, so <strong>nobody has been told</strong> about
              them. They are in their document and in no queue.
            </div>
          )}
          {/* WHERE THE LIBRARY HAD NOTHING TO COMPARE AGAINST. A category with
              no baseline is not a defect in their paper; it is a gap in ours,
              and it is said rather than left as silence.

              ROWS, NOT STRINGS. `paper.py` selects `category_key, label` and
              hands the rows over whole, so joining the array itself prints
              "[object Object]" once per gap. Read what produces the reply
              before rendering it — the sibling defect on this branch was the
              same mistake about `outcome`. The label is preferred and the key
              is the fallback, because the key is what the record is filed
              under and a label can be blank. */}
          {(filed.missing_baseline_categories ?? []).length > 0 && (
            <div className="caption mt-1">
              no approved baseline exists for:{' '}
              {(filed.missing_baseline_categories ?? [])
                .map((c) => c.label || c.category_key).join(', ')}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// -- Asking the machine, and what its answer is allowed to do --------------
//
// THREE QUESTIONS, and this is the first screen in Clausewerk from which any
// of them can be asked. All three have been answerable by the doorway since
// 0052 and 0053 and askable by nobody at a keyboard: the panel below this one
// has drawn their answers all along, and there was no control anywhere in the
// application that could produce an answer for it to draw.
//
// WHAT THE ANSWER MAY DO, which is the only part that matters:
//
//   - It may put something in front of a person. A counterparty paragraph
//     that matches no position we hold is escalated, and escalated means a
//     QUARANTINED REVIEW TICKET in Legal's queue that a named lawyer decides.
//     That is the whole of the machine's authority in this product.
//   - It may write an estimate on a record, labelled an estimate.
//   - It may do nothing else. It cannot move a position, approve a
//     concession, open a gate or mint a word of contract language, and there
//     is no policy in the database that would let it -- which is why these
//     three are offered without a confirmation step. There is nothing here
//     that would have to be undone.
//
// WHICH INSTRUMENT ANSWERED IS PRINTED, NEVER IMPLIED. Matching a paragraph
// to a position is arithmetic over category words today (AI-5, the
// model-assisted matcher, is not built); the transfer estimate beside it is a
// model's opinion. Two different kinds of thing arrive in one reply, so the
// reply says which is which in the instruments' own recorded names rather
// than in an adjective this file chose.
//
// AN ABSENT ANSWER IS AN ANSWER. When no model is reachable or the day's
// budget is spent, the record stores the absence WITH ITS REASON and this
// panel prints the reason. A screen that showed a blank instead would teach
// people to press the button again until a number appeared.
// ── AI-6 / U14e: what we might send back ───────────────────────────────────
//
// A supplier changed a paragraph and the scorer could not place it. Today the
// deal stops there while somebody writes new language by hand. This panel asks
// a model to PROPOSE a reply — and then does nothing with it except put it in
// front of a lawyer, badged, with the counterparty's ask beside it.
//
// THE PROPOSAL IS NOT AN ANSWER. It arrives as a review ticket like any other,
// and becomes language only when a named lawyer verifies that ticket. Nothing
// here sends anything to the other side.
//
// WHY THE ASK IS DRAWN BESIDE THE REPLY. A reviewer judging counter-language
// has to see what was asked for; a proposal read on its own cannot be judged
// too generous or too thin. The register returns both in one row for exactly
// that reason.
function WhatWeMightSendBack({ negotiation, analysis, drafts, onDrafted, onError }) {
  const [answering, setAnswering] = useState(null);
  const [severity, setSeverity] = useState('Standard');
  const [purpose, setPurpose] = useState('');
  const [limits, setLimits] = useState('');
  const [said, setSaid] = useState(null);

  const mine = (analysis.rows ?? [])
    .filter((r) => String(r.negotiation_id) === String(negotiation.negotiation_id));
  // THE PARAGRAPHS A REPLY IS FOR: the ones the scorer placed against no
  // position we hold. A paragraph it DID place already has our answer.
  const unplaced = mine.filter((r) => !r.decided_position);

  const proposals = (drafts.rows ?? [])
    .filter((r) => String(r.negotiation_id) === String(negotiation.negotiation_id));

  const ask = async () => {
    onError(null);
    setSaid(null);
    const answer = await API.draftCounter({
      analysis_id: answering,
      severity,
      intended_purpose: purpose,
      known_limitations: limits,
    });
    if (!answer.ok) { onError(answer.reason); return; }
    setSaid(answer.body || {});
    setAnswering(null);
    setPurpose('');
    setLimits('');
    onDrafted();
  };

  return (
    <div className="panel p-4 mt-6" data-testid="what-we-might-send-back">
      <PanelHead
        title="What we might send back"
        sub="A proposed reply to one paragraph they changed. It lands in the review queue badged AI CANDIDATE, against this deal, naming the paragraph it answers — and a named attorney approves, edits or rejects it. Nothing here reaches the other side." />

      {unplaced.length === 0 && (
        <div className="caption mt-3">
          Every paragraph the scorer read on this negotiation matched a position
          we already hold, so there is nothing here that needs new language.
        </div>
      )}

      {unplaced.length > 0 && (
        <div className="mt-3">
          <div className="caption">
            {unplaced.length} paragraph{unplaced.length === 1 ? '' : 's'} the
            scorer could not place against any position we hold.
          </div>
          <table className="table mt-2" data-testid="unplaced-paragraphs">
            <thead>
              <tr>
                <th>round</th><th>¶</th><th>what they asked for</th><th></th>
              </tr>
            </thead>
            <tbody>
              {unplaced.map((r) => (
                <tr key={r.analysis_id}>
                  <td className="font-mono">{r.round_no}</td>
                  <td className="font-mono">{r.paragraph_index}</td>
                  <td style={{ maxWidth: 420 }}>
                    <span className="quarantined">{r.proposed_text}</span>
                  </td>
                  <td>
                    <ActButton className="btn" data-testid="ask-for-a-reply"
                               onClick={() => {
                                 setSaid(null);
                                 setAnswering(r.analysis_id);
                               }}>
                      draft a reply
                    </ActButton>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {answering && (
        <div className="panel-2 p-3 mt-3" data-testid="drafting-form">
          <div className="caption" style={{ lineHeight: 1.7 }}>
            Both answers below are written into the record permanently and
            cannot be added or corrected afterwards — they are what somebody
            knew at the moment the machine was asked.
          </div>
          <label className="block mt-3">
            <span className="caption">how serious this point is</span>
            <select className="input mt-1" value={severity}
                    data-testid="reply-severity"
                    onChange={(e) => setSeverity(e.target.value)}>
              <option value="Standard">Standard</option>
              <option value="High">High</option>
            </select>
          </label>
          <label className="block mt-3">
            <span className="caption">what this draft is for</span>
            <textarea className="input mt-1" rows={2} value={purpose}
                      data-testid="reply-purpose"
                      onChange={(e) => setPurpose(e.target.value)} />
          </label>
          <label className="block mt-3">
            <span className="caption">
              what is known to be unreliable about it
            </span>
            <textarea className="input mt-1" rows={2} value={limits}
                      data-testid="reply-limitations"
                      onChange={(e) => setLimits(e.target.value)} />
          </label>
          <div className="flex gap-2 mt-3">
            <ActButton className="btn btn-primary" data-testid="draft-the-reply"
                       onClick={ask}>
              ask for a draft
            </ActButton>
            <button className="btn" onClick={() => setAnswering(null)}>
              cancel
            </button>
          </div>
        </div>
      )}

      {said && <WhatItDrafted said={said} />}

      <WhatItProposed proposals={proposals} />
    </div>
  );
}

// The reply to the ask, in its own words. Branches on the STORED word, never on
// whether `text` happens to be set: 'drafted' and 'absent' are the two answers
// the record can hold, and a screen reading truthiness would draw an absence
// with a reason as though nothing had happened at all.
function WhatItDrafted({ said }) {
  if (said.outcome !== 'drafted') {
    return (
      <div className="panel-2 p-3 mt-3" data-testid="drafting-absent">
        <div className="font-mono text-[12.5px]">no reply was drafted</div>
        <div className="caption mt-1">{said.absent_reason}</div>
        {/* THE FALLBACK IS A PERSON, and the answer says so rather than
            leaving a screen to invent one. This is the one capability with no
            deterministic substitute: no arithmetic writes a sentence. */}
        <div className="caption mt-2" style={{ lineHeight: 1.7 }}>
          {said.fallback}.
        </div>
      </div>
    );
  }
  return (
    <div className="panel-2 p-3 mt-3" data-testid="drafting-drafted">
      <div className="font-mono text-[12.5px]">
        a reply was drafted · ticket {said.ticket_id} · badged AI CANDIDATE
      </div>
      <div className="caption mt-1">
        It is a proposal, waiting on an attorney. {said.model}
        {said.model_version ? ` (${said.model_version})` : ''} wrote it from{' '}
        {said.material?.our_open_positions} open position
        {said.material?.our_open_positions === 1 ? '' : 's'},{' '}
        {said.material?.approved_wording_in_this_category} approved wording
        {said.material?.approved_wording_in_this_category === 1 ? '' : 's'} and{' '}
        {said.material?.fallback_ladder} ladder rung
        {said.material?.fallback_ladder === 1 ? '' : 's'}.
      </div>
      {said.basis && (
        <div className="caption mt-2" style={{ lineHeight: 1.7 }}>
          What it says it followed: {said.basis}
        </div>
      )}
    </div>
  );
}

// What has been proposed on this negotiation and what became of it. Drawn from
// the READ, never from the reply above, so the panel and the record cannot
// disagree.
function WhatItProposed({ proposals }) {
  if (!proposals.length) return null;
  return (
    <div className="mt-4" data-testid="proposals-made">
      <div className="caption">
        {proposals.length} propos{proposals.length === 1 ? 'al' : 'als'} on this
        negotiation, with the paragraph each one answers.
      </div>
      <table className="table mt-2">
        <thead>
          <tr>
            <th>¶</th><th>what they asked for</th><th>what we might send</th>
            <th>state</th><th>decided by</th>
          </tr>
        </thead>
        <tbody>
          {proposals.map((p) => (
            <tr key={p.ticket_id}>
              <td className="font-mono">{p.paragraph_index}</td>
              <td style={{ maxWidth: 300 }}>
                <span className="quarantined">{p.their_ask}</span>
              </td>
              <td style={{ maxWidth: 300 }}>{p.proposed_reply}</td>
              <td>
                {/* THE STORED WORD, and 'pending' is not a failure — it is a
                    lawyer who has not got to it yet. */}
                <span className={p.state === 'verified' ? 'stamp stamp-decided'
                                 : p.state === 'rejected' ? 'stamp' : 'stamp stamp-pending'}
                      style={{ '--rot': '-0.8deg' }}>
                  {p.state}
                </span>
              </td>
              {/* ATTRIBUTED OR VISIBLY NOT YET. An empty cell would read as
                  "nobody", which is a different fact from "not decided". */}
              <td className="font-mono">
                {p.decided_by || <span className="caption">not yet decided</span>}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// The requester's half of AI-6: what Legal is being asked to approve on THEIR
// deal, and nothing else. No ask control — who may open a review ticket is
// 0008's insert policy, and a button the database refuses teaches a person a
// rule the system does not have.
function TheyProposedForYou({ negotiation, drafts }) {
  const mine = (drafts.rows ?? [])
    .filter((r) => String(r.negotiation_id) === String(negotiation.negotiation_id));
  if (!mine.length) return null;
  return (
    <div className="panel p-4 mt-6" data-testid="proposed-on-your-deal">
      <PanelHead
        title="Proposed replies on this deal"
        sub="Language a model proposed answering something the other side changed. Every one is waiting on an attorney, or has been decided by a named one — none of it has been sent." />
      <WhatItProposed proposals={mine} />
    </div>
  );
}

function AskTheMachine({ negotiation, onAsked, onError }) {
  const [said, setSaid] = useState(null);

  // One shape for all three: clear the last answer, ask, show the refusal in
  // the database's own words or show what came back, and tell the caller to
  // re-read the record -- the panel below draws from the READ, never from this
  // reply, so a screen that showed the reply and left the record stale would
  // be showing two different answers to the same question.
  const ask = (kind, run) => async () => {
    onError(null);
    setSaid(null);
    const answer = await run();
    if (!answer.ok) { onError(answer.reason); return; }
    setSaid({ kind, ...(answer.body || {}) });
    onAsked();
  };

  return (
    <div className="panel p-4 mt-6" data-testid="ask-the-machine">
      <PanelHead
        title="Ask the machine"
        sub="Three opinions, on the record, labelled. None of them moves a position, approves a concession or mints a word — what matches nothing becomes a ticket, and a named lawyer decides it." />

      <div className="flex flex-wrap gap-2 mt-3">
        <ActButton className="btn" data-testid="analyse-round"
                   onClick={ask('round',
                     () => API.analyseRound(negotiation.agreement_id))}>
          read what they sent
        </ActButton>
        <ActButton className="btn" data-testid="analyse-supplier"
                   onClick={ask('supplier',
                     () => API.analyseSupplierUnits(negotiation.agreement_id))}>
          read their own paper
        </ActButton>
        <ActButton className="btn" data-testid="assess-concessions"
                   onClick={ask('concessions',
                     () => API.assessConcessions(negotiation.agreement_id))}>
          estimate what we gave away
        </ActButton>
      </div>

      {said && <WhatItSaid said={said} />}

      <div className="caption mt-3" style={{ lineHeight: 1.7 }}>
        The first two read the counterparty's latest received round. If none has
        been recorded the system says so and asks for the redline first — it does
        not guess at a document.
      </div>
    </div>
  );
}

// What came back, in the reply's own numbers. Rendered from the reply rather
// than summarised into a sentence: "12 read, 9 matched, 3 escalated" is four
// facts a person can check against the panel below, and "the analysis went
// well" is none.
function WhatItSaid({ said }) {
  if (said.kind === 'concessions') {
    // The four counters account for every settled concession the run walked,
    // and they are separate on purpose: already estimated, being estimated by
    // somebody else this second, and left for the next run are three
    // different facts that a single "skipped" would bury.
    return (
      <div className="panel-2 p-3 mt-3" data-testid="machine-said">
        <div className="font-mono text-[12.5px]">
          {said.concessions_assessed} concession
          {said.concessions_assessed === 1 ? '' : 's'} estimated
        </div>
        <div className="caption mt-1">
          {said.concessions_already_assessed} already carried an estimate ·{' '}
          {said.assessment_in_progress} being estimated by somebody else right
          now · {said.concessions_deferred} left for the next run
        </div>
        <AbsencesAmong rows={said.risk_assessments} />
      </div>
    );
  }

  // THREE STATES, NOT TWO, and the third is the one that matters most.
  // `analysis._analyse_one` opens a ticket only where the paragraph HAS a
  // category and no single open position fits it. A paragraph the classifier
  // could not place at all gets no category, no position and NO TICKET — its
  // own comment in that file says the row lands "visibly unanswered, never
  // guessed at, and the screen's job is to show exactly that".
  //
  // Folding it into "matched nothing" would say those paragraphs are waiting
  // on Legal when nobody has been told about them at all. That is this
  // repository's standing rule in a new place: "nobody acted on it" is not
  // "somebody decided not to" (docs/traps.md, S330).
  //
  // DERIVED BY SUBTRACTION, and said so, because the reply does not carry it.
  // If it ever does, read it instead of computing it here.
  const analysed = said.paragraphs_analysed || 0;
  const matched = said.matched || 0;
  const escalated = said.escalated || 0;
  const unplaced = Math.max(0, analysed - matched - escalated);
  return (
    <div className="panel-2 p-3 mt-3" data-testid="machine-said">
      <div className="font-mono text-[12.5px]">
        round {said.round_no} · {analysed} read · {matched} matched a position
        we hold · {escalated} handed to Legal · {unplaced} it could not place
      </div>
      {/* THE SENTENCE THIS PANEL EXISTS FOR. Escalation is the one thing the
          machine may do that has a consequence, so it is said in full rather
          than left as a number a reader has to interpret. */}
      <div className="caption mt-1" style={{ lineHeight: 1.7 }}>
        {escalated > 0
          ? <>Those {escalated} are now quarantined review tickets waiting on
              Legal. Nothing about them is decided, and none of that text can
              reach a contract until a named lawyer approves wording for it.</>
          : <>Nothing was handed to Legal: every paragraph the machine could
              place matched a position this negotiation already holds.</>}
      </div>
      {unplaced > 0 && (
        // THE ONES WITH NO OWNER. No category, so no ticket a ticket could
        // name, so nobody has been told. Drawn in the ink an unanswered thing
        // wears, because this is the only place it is ever said.
        <div className="caption mt-1" data-testid="machine-unplaced"
             style={{ lineHeight: 1.7, color: 'var(--accent-2)' }}>
          {unplaced} paragraph{unplaced === 1 ? '' : 's'} could not be placed in
          any category at all, so no ticket was opened and <strong>nobody has
          been told</strong>. They are on the record below, unanswered. Somebody
          has to read them.
        </div>
      )}
      <div className="caption mt-1">
        matched by {said.matcher} · classified by {said.classifier}
      </div>
      <AbsencesAmong rows={said.risk_assessments} />
    </div>
  );
}

// The estimates that could not be obtained, counted and given their reason.
// One line per DISTINCT reason rather than one per row: forty concessions
// hitting a spent budget is one fact, and printing it forty times would hide
// the one that failed differently.
//
// BRANCHED ON THE VALUE THE RECORD STORES, NOT ON TRUTHINESS. `0053` constrains
// `outcome` to 'recorded' or 'absent' and NEVER null — an absence is a word,
// not a missing one. The first version of this filtered on `!r.outcome`, which
// is never true, so the panel whose whole job is printing an absent estimate's
// reason could not render at all. Written positively so that a third outcome
// value added tomorrow reads as "not a recorded estimate" and shows its reason,
// rather than silently vanishing.
function AbsencesAmong({ rows }) {
  const absent = (rows ?? [])
    .filter((r) => r && r.outcome !== 'recorded' && r.absent_reason);
  if (absent.length === 0) return null;
  const byReason = new Map();
  for (const row of absent) {
    byReason.set(row.absent_reason, (byReason.get(row.absent_reason) || 0) + 1);
  }
  return (
    <div className="caption mt-2" data-testid="machine-absences"
         style={{ lineHeight: 1.7 }}>
      {[...byReason.entries()].map(([reason, n]) => (
        <div key={reason}>
          {n} estimate{n === 1 ? '' : 's'} could not be obtained — "{reason}"
        </div>
      ))}
    </div>
  );
}

function RoundAnalysis({ negotiation, analysis, risk }) {
  if (analysis.status === 'failed') return null;
  const mine = (analysis.rows ?? [])
    .filter((a) => a.negotiation_id === negotiation.negotiation_id);
  const estimates = risk.rows ?? [];
  // How many of these paragraphs a model was asked about at all. Nought for
  // every company that has not switched AI-5 on, which is every company by
  // default — so the panel says nothing about it rather than drawing an
  // absence for a feature nobody enabled.
  const asked = mine.filter((a) => a.opinion_id).length;

  return (
    <div className="panel p-4 mt-6" data-testid="round-analysis">
      <PanelHead
        title="Analysis of what they sent"
        sub="Advice on the record. Nothing here moved a position, opened a ticket, or gated anything — and nothing here can."
        right={asked > 0 && (
          <span className="caption">a model was asked about {asked} of these</span>
        )} />
      {mine.length === 0
        ? <div className="caption">
            No round of this negotiation has been analysed.
          </div>
        : mine.map((a) => {
            const estimate = estimates.find((e) => e.analysis_id === a.analysis_id);
            return (
              <div className="panel-2 p-3 mb-2" key={a.analysis_id}>
                <div className="flex items-baseline justify-between">
                  <span className="font-mono text-[12.5px]">
                    round {a.round_no} · paragraph {a.paragraph_index}
                  </span>
                  {/* THREE OUTCOMES, NOT TWO. "matched no position" was one
                      sentence covering two different facts: a paragraph
                      HANDED TO LEGAL as a quarantined ticket, and one the
                      classifier could not place at all — which opens no
                      ticket, because a ticket names a category (0008) and
                      there is none. Nobody has been told about the second
                      kind, and this row was the only place that could say so.
                      `no_match_ticket` is on the read already. */}
                  <span className="caption">
                    {a.decided_position
                      ? `matched position ${a.decided_position}`
                      : a.decided_ticket
                        ? `handed to Legal · ticket ${a.decided_ticket}`
                        : 'could not be placed — no ticket, nobody told'}
                    {a.decided_score !== null && a.decided_score !== undefined
                      && ` · score ${a.decided_score}`}
                  </span>
                </div>
                {a.category_key && (
                  <div className="caption mt-1">
                    classified {a.category_key} by {a.classifier}
                  </div>
                )}
                <SecondReader reading={a} />
                {estimate && (
                  <div className="caption mt-1">
                    {/* AN ESTIMATE, LABELLED. An absent outcome carries its
                        recorded reason rather than showing as a blank.

                        THE TEST IS THE STORED WORD, not whether the field is
                        set. `0053` constrains outcome to 'recorded' or
                        'absent' and never leaves it null, so the old
                        truthiness test took the branch for a present estimate
                        on every absent row and printed "absent (null)" — the
                        recorded reason, which is the entire point of storing
                        one, was on the row and unread. */}
                    estimate: {estimate.outcome === 'recorded'
                      ? `${estimate.transfer_estimate}${estimate.basis ? ` — ${estimate.basis}` : ''}`
                      : `none obtained — ${estimate.absent_reason}`}
                  </div>
                )}
              </div>
            );
          })}
    </div>
  );
}

// ── The second reader, beside the first (AI-5, 0103) ─────────────────────
//
// WHAT THIS DRAWS AND WHY IT IS SEPARATE. The line above it is what the system
// DID: matched, handed to Legal, or could not be placed. This is what a model
// made of the same paragraph, and it did none of those things — it cannot open
// a ticket and cannot keep one out, and the database refuses the row that would
// (`0103`, only_the_deterministic_path_escalates).
//
// BOTH LABELS COME OUT OF THE ROW. `cw.paragraph_readings` carries
// `decided_label` and `opinion_label` as columns for cw.ticket_metrics' reason
// (0030): the one way this feature does harm is a reader taking a keyword
// score and a model's confidence for the same kind of number, and a label a
// screen remembers is a label a screen can forget.
//
// AND THE SCORES ARE NEVER PUT SIDE BY SIDE AS IF COMPARABLE. AI-5's own
// ruling: "the two paths do NOT share a threshold. Each carries its own, each
// answer names which path produced it, and the UI says so." So each score is
// printed with the instrument that produced it, and nothing here subtracts one
// from the other.
//
// NOTHING AT ALL WHERE NOBODY ASKED. A company that has not switched AI-5 on
// gets the panel exactly as it was — not an empty box explaining a feature it
// does not use.
function SecondReader({ reading }) {
  if (!reading.opinion_id) return null;

  return (
    <div className="mt-2 pt-2 border-t hair" data-testid="second-reader">
      <div className="caption" style={{ color: 'var(--accent-2)' }}>
        {reading.opinion_label}
      </div>
      {reading.opinion_absent_reason
        ? (
          // AN ABSENCE IS AN OUTCOME. The reading above stands, the ticket it
          // opened stands, and this says why there is no opinion rather than
          // leaving a space that reads as "the machine had nothing to add".
          <div className="caption mt-1" style={{ lineHeight: 1.7 }}>
            No opinion was obtainable — “{reading.opinion_absent_reason}”.
            Nothing about the reading above depends on it.
          </div>
        )
        : (
          <div className="caption mt-1" style={{ lineHeight: 1.7 }}>
            {reading.opinion_position
              ? <>Reads it as position {reading.opinion_position}</>
              : <>Reads it as none of the positions this deal holds</>}
            {reading.opinion_score !== null && reading.opinion_score !== undefined
              && <> · confidence {reading.opinion_score} on{' '}
                   {reading.opinion_instrument}’s own scale, which is not the
                   scorer’s</>}
            {reading.opinion_basis && <> — “{reading.opinion_basis}”</>}
            <div className="mt-1">
              {reading.opinion_model} {reading.opinion_model_version || ''}
              {/* THE THIRD ANSWER, SAID. `readers_agree` is null where nothing
                  was asked or nothing came back, and folding that into
                  "disagrees" would report a dispute the machine never had. */}
              {reading.readers_agree === true && ' · agrees with the scorer'}
              {reading.readers_agree === false && ' · differs from the scorer'}
            </div>
          </div>
        )}
    </div>
  );
}

// ── The authored playbook, beside the analysis ────────────────────────────
// The SECOND labeled advisory stream (0081), kept beside the model's
// analysis and never mixed into it: these are moves Legal WROTE for the
// clauses this deal's open positions stand on. Advice on the record — gates
// nothing, moves nothing, and no resolution consults it. cw.position_move
// already scoped what arrives to this connection; this narrows to one deal.
function MovesAside({ negotiation, positionMoves }) {
  if (positionMoves.status === 'loading') return null;
  if (positionMoves.status === 'failed') {
    // Said, not swallowed. "No moves are authored" and "we could not ask"
    // are different facts, and on an advisory panel the second must still
    // not wear the clothes of the first.
    return (
      <div className="panel p-4 mt-6" data-testid="moves-aside">
        <PanelHead title="Authored moves"
                   sub="Advice on the record — gates nothing, moves nothing." />
        <div className="caption">“{positionMoves.reason}”</div>
      </div>
    );
  }

  const mine = positionMoves.rows
    .filter((m) => m.negotiation_id === negotiation.negotiation_id);
  const byPosition = new Map();
  for (const m of mine) {
    if (!byPosition.has(m.position_id)) {
      byPosition.set(m.position_id, {
        position_id: m.position_id, category_key: m.category_key,
        our_clause_id: m.our_clause_id, our_version: m.our_version, moves: [],
      });
    }
    byPosition.get(m.position_id).moves.push(m);
  }

  return (
    <div className="panel p-4 mt-6" data-testid="moves-aside">
      <PanelHead
        title="Authored moves — advice, gates nothing"
        sub="What Legal wrote in advance for the clauses these open positions stand on. The second advisory stream, beside the analysis above and never mixed into it." />
      {byPosition.size === 0
        ? <div className="caption">
            No live move is authored for the clauses standing open here.
          </div>
        : [...byPosition.values()].map((g) => (
            <div className="panel-2 p-3 mb-2" key={g.position_id}>
              <div className="flex items-baseline justify-between">
                <span className="font-mono text-[12.5px]">{g.category_key}</span>
                <span className="caption">{g.our_clause_id} v{g.our_version}</span>
              </div>
              {g.moves.map((m) => <MoveAdvice key={m.move_id} move={m} />)}
            </div>
          ))}
    </div>
  );
}

// ── The shared oddments ───────────────────────────────────────────────────

// The library's categories as {key, label}. A POSITION names the KEY (it is a
// foreign key into cw.category); a MANIFEST names the label. Sending the wrong
// one is refused, and the two are kept apart here rather than at each call.
function categoriesFrom(rows) {
  const seen = new Map();
  for (const row of rows ?? []) {
    if (row.category_key && !seen.has(row.category_key)) {
      seen.set(row.category_key, row.category_label || row.category_key);
    }
  }
  return [...seen.entries()].map(([key, label]) => ({ key, label }))
    .sort((a, b) => a.key.localeCompare(b.key));
}

// The database's own sentence, shown where the person acted. Red, because a
// refused act IS an error state for the person who attempted it — unlike a
// refusal to SHOW something, which is the system working.
function RefusalNote({ reason }) {
  return (
    <div className="panel p-3 mt-4" style={{ borderColor: 'var(--danger)' }}>
      <div className="tag" style={{ color: 'var(--danger)' }}>refused</div>
      <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{reason}</div>
    </div>
  );
}

// Saving a file is the ONE screen's job, never the transport's — the same
// split GET /runs/contract established, and db/test/shell.test.mjs asserts
// api.jsx contains no createElement('a') precisely so this stays here.
function saveBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = filename;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  URL.revokeObjectURL(url);
}
