// The Legal reviewer's workspace (WP-U11).
//
// The review desk: everything waiting on Legal judgement, oldest first.
//
// THREE RULES THIS SCREEN HAS TO GET RIGHT, and two of them are the difference
// between a review and a rubber stamp:
//
//   1. THE AI CANDIDATE IS NEVER PRE-FILLED INTO THE APPROVAL BOX. The reviewer
//      chooses; the screen must not have chosen already. A box arriving with the
//      proposal in it turns "approve" into "confirm", and the
//      unedited-approval-rate measurement exists precisely to watch that
//      pressure — a screen that creates it is measuring its own defect.
//
//   2. VERIFY SHOWS WHAT WILL BE MINTED, BEFORE IT IS MINTED. The wording
//      approved is the wording that exists forever, and a clause version is
//      immutable the moment it lands. There is no undo to fall back on.
//
//   3. THE OLDEST WAIT IS VISIBLE. Sort order is a control here, not a
//      preference: the desk exists to surface the longest wait, and a
//      newest-first list buries exactly what it was built to show.

const { useState, useRef } = React;

// Provenance badges travel with the text, so a reviewer always knows whose words
// they are reading (ADR-0003). Rendered in the established chip idiom, and
// deliberately NOT colour-coded by severity — red means error, never
// classification.
function ProvenanceBadge({ badge }) {
  return <span className="chip chip-std" title="whose words these are">{badge}</span>;
}

// ── What the machine was shown, where the decision is taken (0102) ────────
//
// ADR-0010 let a model draft candidate wording on one condition: that "the
// provenance chain reaches back past the lawyer to what the lawyer was shown".
// Every part of that has been recorded since 0008 and 0029 — the prompt, the
// model and its version, what it was for, what was known to be unreliable
// about it — and until 0102 nothing served it, so the reviewer deciding an AI
// CANDIDATE could see the words and nothing about where they came from.
//
// IT IS DRAWN CLOSED. The decision this desk exists for is about the WORDING,
// which is already on screen above in the model's own words; how it was made
// is context somebody asks for. Open by default it would push a reviewer to
// read the prompt instead of the clause.
//
// THE READ IS ROLE-SCOPED AND NARROWED FOR PRESENTATION. cw.library_draft_
// register is granted to this role in full; picking the row for the ticket on
// screen is the same shape GET /library/proposal-evidence uses, and no row
// arrives here that this role could not already read.
//
// NOTHING HERE IS PRE-FILLED ANYWHERE. This panel renders provenance only —
// rule 1 at the top of this file stands untouched.
function DraftProvenance({ ticket }) {
  const [open, setOpen] = useState(false);
  const register = usePane(() => API.libraryDrafts());
  if (ticket.provenance_badge !== 'AI CANDIDATE') return null;

  const row = (register.rows || []).find(
    (r) => String(r.ticket_id) === String(ticket.ticket_id));

  return (
    <div className="mt-3" data-testid="ticket-draft-provenance">
      <button type="button" className="btn btn-sm"
              data-testid="open-draft-provenance"
              onClick={() => setOpen(!open)}>
        {open ? 'close' : 'what the machine was shown'}
      </button>
      {open && (
        <div className="panel-2 p-3 mt-2">
          {register.status === 'failed' ? (
            <div className="text-[12.5px]" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
              “{register.reason}”
            </div>
          ) : !row ? (
            /* AN HONEST THIRD ANSWER. A ticket can be badged AI CANDIDATE and
               carry no draft record — 0008 lets a ticket be opened with the
               badge and no draft_id — and saying "no draft record" is not the
               same as saying nothing was recorded. */
            <div className="text-[12.5px]" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
              This ticket is badged AI CANDIDATE and carries no draft record, so
              there is no prompt, model or stated purpose behind it. The wording
              above is what was proposed; where it came from was never recorded.
            </div>
          ) : (
            <div className="text-[12.5px]" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
              <div><strong>Model:</strong> <span className="font-mono">{row.model}</span>
                   {' · '}<span className="font-mono">{row.model_version}</span></div>
              <div className="mt-1"><strong>What it was for:</strong> {row.intended_purpose}</div>
              <div className="mt-1"><strong>Known limitations at the time:</strong> {row.known_limitations}</div>
              <div className="mt-1"><strong>Material it was given:</strong>{' '}
                {(row.inputs || []).length
                  ? (row.inputs || []).map((i) => `${i.name} (${i.characters} chars)`).join(' · ')
                  : 'nothing was recorded'}
              </div>
              <div className="mt-2"><strong>The prompt, verbatim:</strong></div>
              <div className="text-[12px] mt-1 font-mono" style={{ whiteSpace: 'pre-wrap' }}>
                {row.prompt}
              </div>
              <div className="mt-2">
                Whatever you approve is compared with the model’s own words above,
                and the difference is recorded on this ticket.
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── A lawyer proposes wording of their own authorship (0118) ─────────────
//
// WHAT WAS MISSING, and it is the largest thing this desk did not have.
// Outside seed scripts and tests, the ONLY way a clause version enters the
// library is 0008/0009 minting one from a VERIFIED review ticket — and every
// review ticket anybody could actually cause was opened by one of four machine
// or supplier paths (analysis.py, builder.py, drafting.py, paper.py). So every
// word in the library arrived because a model drafted it or a counterparty
// sent it, and Legal could only approve, edit or reject what was put in front
// of them. `POST /tickets` — the human-authored path — has been served since
// 0008 and offered by api.jsx for months with no control anywhere.
//
// NO ROLE CHECK HERE, AND THAT IS DELIBERATE. `review-desk` is on the Legal
// reviewer's and the Legal admin's rails and nobody else's, and 0118's
// `open_ticket` policy refuses the badge below to every other role by name. So
// the screen is fenced on both sides and a check in this component would be a
// third copy of a decision the database already takes.
//
// THE READS ARE INSIDE THE FORM, not beside it. This desk already makes six.
// Drawing the control costs one button; the categories and the deal list are
// fetched when somebody opens it, which is why the open state lives in the
// wrapper below and the hooks live here.
function ProposeWordingForm({ onClose, onOpened, onError }) {
  const categories = usePane(() => API.categories());
  // NAMED `dealList` RATHER THAN `deals`, and that is not a style choice. The
  // mutation harness anchors "canned example rows are carried into a pane" on
  // the exact source line that declares a pane's deal read, and its
  // preflight refuses to run at all when a pattern matches twice in one file —
  // `HoldsPane` below already has that exact line. A second identical copy
  // would have left the harness mutating whichever came first and silently
  // watching the wrong pane. The preflight caught it, which is what it is for.
  const dealList = usePane(() => API.deals());
  const [categoryKey, setCategoryKey] = useState('');
  const [severity, setSeverity] = useState('Standard');
  const [agreementId, setAgreementId] = useState('');
  const [text, setText] = useState('');
  const [busy, setBusy] = useState(false);

  const body = text.trim();
  const ready = categoryKey && severity && body && !busy;

  return (
    <div className="panel-2 p-3 mt-3" data-testid="propose-wording-form">
      <div className="section-label">Wording of your own authorship</div>

      {/* SAID BEFORE THE FIELDS, NOT AFTER, the way the category form says it.
          0008 makes proposed_text immutable from the moment the ticket is
          opened — it is the baseline an edit is measured against — and no role
          holds DELETE on cw.review_ticket. So this cannot be corrected or
          taken back; it can only be decided. */}
      <div className="panel p-3 mt-2" style={{ borderColor: 'var(--danger)' }}>
        <div className="tag" style={{ color: 'var(--danger)' }}>permanent</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
          What you type here is recorded as <strong>your own words</strong>,
          badged <span className="font-mono">DRAFTED BY LEGAL</span>, against
          your name. You cannot edit it afterwards and you cannot take it back:
          a ticket is verified, rejected or left to expire, and no role can
          delete one. It is not library language until somebody verifies it.
        </div>
      </div>

      <label className="section-label mt-3" htmlFor="propose-category">Risk category</label>
      {categories.status === 'failed' ? (
        <LoadFailed reason={categories.reason} />
      ) : (
        <select id="propose-category" className="mt-1.5 w-full font-mono"
                aria-label="Risk category"
                data-testid="propose-category" value={categoryKey}
                onChange={(e) => setCategoryKey(e.target.value)}>
          <option value="">choose a category…</option>
          {(categories.rows || []).map((c) => (
            <option key={c.key} value={c.key}>{c.label} ({c.key})</option>
          ))}
        </select>
      )}
      <div className="caption mt-1">
        Every clause belongs to one. If the risk you are writing about has no
        category, a Legal admin names one on the governance pane first.
      </div>

      <label className="section-label mt-3" htmlFor="propose-severity">Severity</label>
      <select id="propose-severity" className="mt-1.5 w-full font-mono"
              aria-label="Severity"
              data-testid="propose-severity" value={severity}
              onChange={(e) => setSeverity(e.target.value)}>
        <option value="Standard">Standard</option>
        <option value="High">High</option>
      </select>

      {/* OPTIONAL, AND THE EMPTY CHOICE IS THE HONEST ONE. Wording proposed to
          fill a gap in the library belongs to no contract; wording proposed
          because a particular deal exposed the gap belongs to that one, and
          naming it is what lets the desk draw this supplier's other paper
          beside it when the ticket is decided. */}
      <label className="section-label mt-3" htmlFor="propose-deal">Deal this came from (optional)</label>
      {dealList.status === 'failed' ? (
        <LoadFailed reason={dealList.reason} />
      ) : (
        <select id="propose-deal" className="mt-1.5 w-full font-mono"
                aria-label="Deal this came from (optional)"
                data-testid="propose-deal" value={agreementId}
                onChange={(e) => setAgreementId(e.target.value)}>
          <option value="">no deal — this is library language</option>
          {(dealList.rows || []).map((d) => (
            <option key={d.agreement_id} value={d.agreement_id}>
              {d.agreement_id} · {d.counterparty}
            </option>
          ))}
        </select>
      )}

      <label className="section-label mt-3" htmlFor="propose-text">The wording you propose</label>
      <textarea id="propose-text" className="mt-1.5 w-full font-serif" rows={5}
                aria-label="The wording you propose"
                data-testid="propose-text"
                placeholder="Write the clause language you are proposing."
                value={text} onChange={(e) => setText(e.target.value)} />

      {/* THE CARVE-OUT, SAID RATHER THAN DISCOVERED. cw.verify_review_ticket()
          refuses the opener as decider only when the opener is NOT Legal
          (0028's deliberate carve-out for Legal's own queue). So a lawyer may
          verify their own proposal. That is the existing rule and this screen
          does not change it — but a form that let somebody find it out by
          accident would be teaching a rule the system does not have. Whether
          it should still hold now that this door exists is a question for the
          owner, recorded in PRODUCT.md §6 rather than answered here. */}
      <div className="caption mt-2" style={{ lineHeight: 1.7 }}>
        This goes to the bottom of the queue above and waits like anything else.
        A colleague deciding it is the norm; Legal's own queue is carved out of
        the “nobody decides their own request” rule, so you can also verify it
        yourself — and the record names you at both ends when you do.
      </div>

      <div className="flex gap-2 mt-3">
        <button className="btn" onClick={onClose}>cancel</button>
        <ActButton className="btn btn-primary" disabled={!ready}
          data-testid="propose-submit"
          onClick={async () => {
            setBusy(true); onError(null);
            // THE BADGE AND THE REASON ARE NOT OFFERED AS A CHOICE. This form
            // produces one kind of ticket and the pair is what says so. 0118
            // refuses the badge to any role but Legal, refuses it on a ticket
            // citing a machine's draft, and refuses either of the two values
            // without the other — so the screen and the database agree, and
            // the database is what decides.
            const r = await API.openTicket({
              agreement_id: agreementId || null,
              category_key: categoryKey,
              severity,
              reason_code: 'legal-authored',
              provenance_badge: 'DRAFTED BY LEGAL',
              proposed_text: body,
            });
            setBusy(false);
            if (!r.ok) { onError(r.reason); return; }
            setText(''); setCategoryKey(''); setAgreementId('');
            onOpened(r.rows && r.rows[0]);
          }}>
          {busy ? 'opening…' : '✓ open this ticket'}
        </ActButton>
      </div>
    </div>
  );
}

// The wrapper holds only the open state, so the form's reads happen when
// somebody asks for the form rather than on every load of this desk.
//
// `filed` IS THE PANE'S STATE, NOT THIS COMPONENT'S, AND THAT IS A REPAIR.
// It lived here first, and the confirmation never appeared once — found by
// driving the act in the running application, where the ticket reached the
// database and the screen said nothing about it. Filing reloads the queue;
// `usePane`'s reload sets the pane back to `loading`; `ReviewDeskPane` returns
// `<Loading />` on `loading`; so everything below that early return is
// UNMOUNTED, and state held down here dies with it — destroyed by the reload
// the act itself asked for. The pane component stays mounted across its own
// early return, so state held up there survives.
function ProposeWording({ filed, onFiled, onError }) {
  const [open, setOpen] = useState(false);

  return (
    <div className="mt-4" data-testid="propose-wording">
      {!open ? (
        <button className="btn btn-sm" data-testid="propose-wording-open"
                onClick={() => { setOpen(true); onFiled(null); }}>
          propose wording of your own
        </button>
      ) : (
        <ProposeWordingForm
          onClose={() => setOpen(false)}
          onError={onError}
          onOpened={(row) => { setOpen(false); onFiled(row); }}
        />
      )}
      {/* NAMES THE TICKET IT MADE. "Saved" is not an answer on a desk holding
          seventy-two rows; the number is how somebody finds it again. */}
      {filed && (
        <div className="caption mt-2" data-testid="propose-wording-filed">
          Ticket <span className="font-mono">{filed.ticket_id}</span> is open and
          waiting on Legal, badged{' '}
          <span className="font-mono">DRAFTED BY LEGAL</span> and opened by{' '}
          <span className="font-mono">{filed.opened_by}</span>.
        </div>
      )}
    </div>
  );
}

// ── The expert opinions on this ticket (0090, ADR-0013; on screen 2026-08-25) ──
//
// WHAT WAS MISSING. `cw.ticket_consultation` and `GET /tickets/consultations`
// have existed since 0090 and no screen has ever called them. So the lawyer
// DECIDING a ticket could not see the expert consultations against it — while
// the verify trigger in that same migration REFUSES to mint the wording until
// every required one is answered or waived. The person who pays for the wait
// could not see what was being waited on, or ask anybody about it, or read the
// opinion once it arrived. They learned by being refused.
//
// IT IS NOT `GET /panel/consultations`, WHICH THE EXPERT PANEL PANE ALREADY
// DRAWS, and the difference is the scoping rather than the columns. That one
// reads `cw.consultation` under its `read_scoped` policy — a seat holder's own
// desk, answering the rows their live seat admits. This reads a view scoped in
// its own WHERE clause: Legal and Audit see every ticket, a requester sees the
// tickets they opened or own the agreement for, and a viewer holds no grant on
// it at all. That was measured across five roles rather than assumed — the
// counts are dated in `docs/audits/VERIFIED-CLEAN-REGISTER.md`, because they
// belong to the database they were taken on and not to this file. The one that
// matters: a requester who owns nothing is answered ZERO rows, and a viewer is
// refused outright.
//
// AND IT SHOWS WHAT THE RULES SAY WAS OWED, not only what was asked. The view
// unions the routing rules with the consultations, so a required discipline
// nobody has referred the ticket to appears as `not asked` rather than as
// nothing. That row is the one this panel exists for: it is the commonest
// reason a mint is refused, and it is invisible everywhere else.
//
// ── THREE RULES IT WOULD BE EASY TO BREAK HERE, ALL OF THEM THE PRODUCT'S ──
//
//   AN ADVISORY CONSULTATION IS NEVER DRAWN AS A BLOCKER. Only a required one
//   gates, `consultationHolds()` in common.jsx is the single expression of
//   that, and the standing answer below says in words that the advisory rows
//   hold nothing. The waiver report counts required consultations for the same
//   reason (panel-measures.jsx).
//
//   A WAIVER PASSES EXACTLY AS AN ANSWER DOES, and the reason is the
//   interesting part, so it is rendered in full rather than hidden behind the
//   chip's tooltip. Without a door that is not a lie people route around the
//   system and it records nothing.
//
//   AN ANSWER OF `unsound` PASSES EXACTLY AS `sound` DOES. The panel says so
//   in those words wherever an opinion has been given. Drawing an expert's
//   opinion as a veto would teach a rule this system does not have.
//
// IT STATES THE GATE; IT DOES NOT KEEP IT. Nothing below disables the mint
// control, and `ready` does not consult this panel. The gate belongs to
// `cw.review_ticket_transition()`, and a screen holding a second copy of it is
// how two expressions of one rule come to disagree — this repository's most
// repeated defect. What the screen owes is that nobody is refused by a rule
// they were never shown.
function TicketConsultations({ ticketId }) {
  const pane = usePane(() => API.ticketConsultations());

  // FILTERED, NEVER SCOPED — the same sentence TicketEvidence carries above.
  // The view's own WHERE clause decides what may be seen; this picks the
  // ticket on screen out of what was already answered.
  const rows = (pane.rows ?? [])
    .filter((r) => String(r.ticket_id) === String(ticketId));
  const required = rows.filter((r) => r.necessity === 'required');
  const holding = rows.filter(consultationHolds);
  const answered = rows.filter((r) => r.state === 'answered');

  return (
    <div className="mt-4" data-testid="ticket-consultations">
      <PanelHead
        title="What the experts were asked"
        sub="Every discipline this ticket is routed to, and what came back."
      />

      {pane.status === 'loading' && <div className="caption mt-1.5">reading…</div>}
      {pane.status === 'failed' && (
        <div className="caption mt-1.5">
          the consultations could not be read: {pane.reason}
        </div>
      )}

      {pane.status === 'loaded' && rows.length === 0 && (
        /* AN ANSWER, NOT AN EMPTY BOX. No discipline routed and none asked is
           a fact about this ticket, and a panel that vanished would read as
           "nobody looked" — which is the one thing evidence must never do. */
        <div className="panel-2 p-3 mt-2 caption" style={{ lineHeight: 1.7 }}
             data-testid="consultations-none">
          No routing rule sends a ticket of this category and severity to any
          discipline, and nobody has referred this one to an expert. There is no
          expert opinion to read here, and nothing on this ticket is waiting on
          one.
        </div>
      )}

      {pane.status === 'loaded' && rows.length > 0 && (
        <React.Fragment>
          {/* THE STANDING ANSWER, above the rows, because it is the thing the
              person deciding actually needs. It says which way the gate is
              currently pointing and — either way — what does NOT affect it. */}
          <div className="panel-2 p-3 mt-2" data-testid="consultations-gate">
            {holding.length > 0 ? (
              <div className="text-[12.5px]" style={{ lineHeight: 1.7 }}>
                <strong>This wording cannot be minted yet.</strong>{' '}
                {holding.map((r) => r.discipline || r.discipline_key).join(', ')}
                {holding.length === 1
                  ? <> is a required consultation that has not been answered or
                      waived, and the record refuses to mint until it is.</>
                  : <> are required consultations that have not been answered or
                      waived, and the record refuses to mint until each of them
                      is.</>}
                {' '}Get an answer, or waive it with a reason on the expert
                panel. <strong>Rejecting this ticket needs no expert answer at
                all</strong> — the gate protects what enters the library, and an
                answer could not change a rejection.
              </div>
            ) : required.length > 0 ? (
              <div className="text-[12.5px]" style={{ lineHeight: 1.7 }}>
                <strong>Nothing here holds this ticket.</strong> All{' '}
                {required.length} required{' '}
                {required.length === 1 ? 'consultation has' : 'consultations have'}{' '}
                been answered or waived, so the record will not refuse the mint
                on this ground.
              </div>
            ) : (
              <div className="text-[12.5px]" style={{ lineHeight: 1.7 }}>
                <strong>Nothing here holds this ticket.</strong> Every
                consultation below is advisory: it was asked because somebody
                wanted the opinion, and the ticket moves whether or not anybody
                answers.
              </div>
            )}

            {/* SAID WHEREVER AN OPINION EXISTS, not only where it was
                unwelcome. A sentence that appeared beside `unsound` alone
                would itself be drawing that answer as the special one. */}
            {answered.length > 0 && (
              <div className="caption mt-2" style={{ lineHeight: 1.7 }}>
                An expert's answer is evidence, never a veto. <em>Unsound</em>{' '}
                passes this gate exactly as <em>sound</em> does; you decide, and
                a decision taken against advice stays visible on the panel's own
                report.
              </div>
            )}
          </div>

          {rows.map((r) => (
            <div className="panel-2 p-3 mt-2" key={`${r.ticket_id}-${r.discipline_key}`}
                 data-testid={`ticket-consultation-${r.discipline_key}`}>
              {/* WRAPS, and the aside does not shrink. Measured at 375px:
                  the discipline name, the necessity chip and a mark reading
                  "waived by r.vance@clausewerk" are four unbreakable runs on
                  one row. */}
              <div className="flex items-start justify-between gap-3 flex-wrap">
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 14 }}>
                    {r.discipline || r.discipline_key}
                  </div>
                  {r.question && (
                    <div className="caption mt-1" style={{ lineHeight: 1.6 }}>
                      {r.question}
                    </div>
                  )}
                </div>
                <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center',
                               flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                  {r.necessity === 'required'
                    ? <Status state="pending"
                        title="This ticket cannot be verified until this is answered or waived in writing.">
                        required
                      </Status>
                    : <Status state="neutral"
                        title="Asked, and gating nothing. The ticket moves whether or not anybody answers.">
                        advisory
                      </Status>}
                  <ConsultationMark row={r} />
                </span>
              </div>

              {r.state === 'answered' && r.reasoning && (
                <p className="mt-2 text-[13px]" style={{ lineHeight: 1.6 }}>
                  <strong>{r.answered_by}</strong> — {r.reasoning}
                </p>
              )}

              {/* THE WAIVER REASON IN FULL. It is the whole value of the door:
                  the gate is on whether somebody was ASKED, and the recorded
                  reason is what a waiver leaves behind for the auditor. */}
              {r.state === 'waived' && (
                <p className="mt-2 text-[13px]" style={{ lineHeight: 1.6 }}>
                  <strong>{r.waived_by}</strong> waived this: {r.waiver_reason}
                </p>
              )}

              {/* THE THIRD ANSWER, said rather than left blank. A required
                  discipline nobody has referred the ticket to is the commonest
                  reason a mint is refused, and "no consultation" and "asked and
                  waiting" are different facts about different desks. */}
              {r.state === 'not_asked' && (
                <p className="caption mt-2" style={{ lineHeight: 1.6 }}>
                  The routing rules send a ticket of this category and severity
                  to this discipline, and nobody has asked it. This one is
                  waiting on Legal to refer it, not on an expert to answer.
                </p>
              )}

              {r.state === 'open' && (
                <p className="caption mt-2" style={{ lineHeight: 1.6 }}>
                  Asked by {r.opened_by} on {String(r.opened_at || '').slice(0, 10)}.
                  Waiting for an answer.
                </p>
              )}
            </div>
          ))}
        </React.Fragment>
      )}
    </div>
  );
}

// ── Adjudicating a ticket ────────────────────────────────────────────────
// ── A proposal this desk does not decide (0107; #153) ─────────────────────
//
// WHAT THIS REPLACES. Opening a rung or a rule ticket used to draw the mint
// form below — an approve control whose only possible outcome was 0107's
// refusal. This says what the ticket is, why the decision is taken elsewhere,
// and goes there. It offers no act, because this desk holds none for it.
//
// EVERY ROLE THAT HOLDS THIS DESK HOLDS THAT TAB. `builder` is on the Legal
// reviewer's, the Legal admin's and the auditor's rails, so the destination is
// never a tab the reader cannot open. Who may DECIDE there is a narrower
// question — 0107 grants both landing acts to a Legal admin alone — and it is
// said rather than discovered, because a reviewer arriving at a screen that
// will not let them act has been sent somewhere useless.
function ProposalDecidedElsewhere({ ticket, onClose, me }) {
  const kind = proposalKind(ticket);
  return (
    <div className="panel p-4 mt-4" data-testid="not-decided-here">
      <div className="flex items-start justify-between">
        <PanelHead
          title={`Ticket ${ticket.ticket_id}`}
          sub={`${ticket.category_key} · ${ticket.severity} · ${kind.label}`} />
        <button className="btn btn-sm" onClick={onClose}>close</button>
      </div>

      {/* WRAPS. Measured at 375px: the badge, the kind chip and "opened by
          <person>" are three unbreakable words on one row, and without this
          they ran 19px past the panel. */}
      <div className="flex items-center gap-2 mt-2 flex-wrap">
        <ProvenanceBadge badge={ticket.provenance_badge} />
        <ProposalKindChip ticket={ticket} />
        <span className="caption">opened by {ticket.opened_by}</span>
      </div>

      <div className="panel-2 p-3 mt-3">
        <div className="font-serif" style={{ fontSize: 15, lineHeight: 1.6 }}>
          {ticket.proposed_text}
        </div>
      </div>

      <div className="caption mt-3" style={{ lineHeight: 1.7 }}>{kind.why}</div>

      {kind.hash && (
        <div className="flex items-center gap-2 mt-4 flex-wrap">
          <button className="btn btn-primary" data-testid="go-to-decision"
                  onClick={() => { window.location.hash = kind.hash; }}>
            open {kind.where} →
          </button>
          {me && me.role !== 'legal_admin' && (
            <span className="caption">
              A Legal admin decides this one. You can read it there.
            </span>
          )}
        </div>
      )}
    </div>
  );
}

// ── The evidence behind a quarantined ticket (0008, on screen 2026-08-24) ──
//
// TWO TABLES NOBODY COULD WRITE AND NOTHING SERVED, for sixteen migrations.
// 0008 built `cw.review_segment` and `cw.review_candidate` as the two halves of
// what a reviewer is shown — its own words: "what Legal is shown is the
// evidence, not a summary of it" — and until now the desk showed the
// quarantined paragraph alone. A reviewer decided whether a vendor's wording
// was acceptable without being shown WHAT THEY CHANGED, or what the company
// already holds in that category.
//
// IT DRAWS ABSENCE RATHER THAN HIDING IT. A ticket with no segments says so and
// says why — an ingested paragraph is the vendor's entire, so there is no
// change to display — and a category with no ladder says that instead of
// showing an empty box. A panel that vanished when it had nothing would teach a
// reviewer that silence means "nothing to see", which is the one thing evidence
// must never do.
//
// FILTERED, NEVER SCOPED. Both reads are whole-set and carry 0008's own
// read_scoped policy, which admits a row exactly when its TICKET is visible. So
// this filter narrows what is drawn; it is not what decides what may be seen.
function TicketEvidence({ ticketId }) {
  const segments   = usePane(() => API.ticketSegments());
  const candidates = usePane(() => API.ticketCandidates());

  const mine = (pane) => (pane.rows ?? [])
    .filter((r) => String(r.ticket_id) === String(ticketId));
  const segs = mine(segments);
  const cands = mine(candidates);

  // A LITTLE AIR ROUND A CHANGED RUN, and nothing else. A deletion followed
  // immediately by an insertion is two runs with no whitespace between them —
  // that is what the document said, and putting a space INTO the text would be
  // this screen editing the evidence. Padding is presentation; the characters
  // are untouched.
  const paint = (kind) =>
    kind === 'ins' ? { color: 'var(--accent-2)', textDecoration: 'underline',
                       padding: '0 2px' }
    : kind === 'del' ? { color: 'var(--mute)', textDecoration: 'line-through',
                         padding: '0 2px' }
    : {};

  return (
    <div className="mt-4">
      <PanelHead
        title="The evidence behind this ticket"
        sub="What the vendor changed, and what we already hold in this category."
      />

      <div className="panel-2 p-3 mt-2">
        <div className="section-label">What the vendor changed</div>
        {segments.status === 'loading' && <div className="caption mt-1.5">reading…</div>}
        {segments.status === 'failed' && (
          <div className="caption mt-1.5">
            the segments could not be read: {segments.reason}
          </div>
        )}
        {segments.status === 'loaded' && segs.length === 0 && (
        <div className="caption mt-1.5" style={{ lineHeight: 1.6 }}>
            No change is recorded for this ticket. That is expected when the
            paragraph arrived on the counterparty's <em>own</em> paper — there is
            no wording of ours for it to differ from, and the paragraph above is
            the whole of what was received.
          </div>
        )}
        {segments.status === 'loaded' && segs.length > 0 && (
          <React.Fragment>
            {/* IN THE DOCUMENT'S ORDER, which the read enforces and this does
                not re-sort. keep/ins/del out of order is a different change. */}
            <div className="font-serif mt-2" style={{ fontSize: 15, lineHeight: 1.7 }}>
              {segs.map((s) => (
                <span key={s.seq} style={paint(s.kind)} title={s.kind}>{s.text}</span>
              ))}
            </div>
            <div className="caption mt-2">
              <span style={paint('ins')}>underlined</span> is what they added ·{' '}
              <span style={paint('del')}>struck</span> is what they removed ·{' '}
              {segs.filter((s) => s.kind === 'ins').length} insertion(s),{' '}
              {segs.filter((s) => s.kind === 'del').length} deletion(s)
            </div>
          </React.Fragment>
        )}
      </div>

      <div className="panel-2 p-3 mt-2">
        <div className="section-label">What we hold in this category</div>
        {candidates.status === 'loading' && <div className="caption mt-1.5">reading…</div>}
        {candidates.status === 'failed' && (
          <div className="caption mt-1.5">
            the candidates could not be read: {candidates.reason}
          </div>
        )}
        {candidates.status === 'loaded' && cands.length === 0 && (
          <div className="caption mt-1.5" style={{ lineHeight: 1.6 }}>
            No ladder was recorded for this ticket's category, so there were no
            alternatives to offer. That is the answer, not a gap in this panel —
            a category with no retreat path escalates by design.
          </div>
        )}
        {candidates.status === 'loaded' && cands.length > 0 && (
          <React.Fragment>
            <table className="w-full mt-2" style={{ fontSize: 13 }}>
              <thead>
                <tr>
                  <th className="text-left">position</th>
                  <th className="text-left">wording</th>
                  <th className="text-left">standing</th>
                </tr>
              </thead>
              <tbody>
                {cands.map((c) => (
                  <tr key={c.seq}>
                    <td>
                      <span className="chip chip-std" title={c.reason}>
                        {c.disposition}
                      </span>
                    </td>
                    <td>
                      <span className="font-mono">{c.clause_id}@v{c.version}</span>
                      {c.title && <span className="caption"> · {c.title}</span>}
                    </td>
                    {/* THE COLUMN THAT MADE THE JOIN WORTH MAKING. A candidate
                        is stored as a REFERENCE (ADR-0004), so what it points
                        at can have been retired since it was offered. Drawing
                        it as available would offer language the library has
                        withdrawn. */}
                    <td>
                      {c.selectable
                        ? <span className="caption">{c.state}</span>
                        : <span className="stamp stamp-sm" title={c.state}>
                            not selectable now
                          </span>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            <div className="caption mt-2" style={{ lineHeight: 1.6 }}>
              <strong>suppressed</strong> means recorded below the ladder's floor
              — kept so the question "why not that one?" stays answerable, and
              never offered as a retreat (ADR-0004).
            </div>
          </React.Fragment>
        )}
      </div>    </div>
  );
}

function ticketDraftKey(ticketId, field) {
  return `review-ticket:${ticketId}:${field}`;
}

function discardTicketDraft(ticketId) {
  for (const field of ['approved', 'clauseId', 'title', 'rationale', 'note']) {
    discardDraft(ticketDraftKey(ticketId, field));
  }
}

// Keyed by the ticket below: changing an address starts a fresh component,
// not a new ticket inside the previous ticket's confirmation or draft state.
// Focus and scroll are both deliberate; a 71-row queue can put this panel
// several screens below the button that opened it.
function SelectedReview({ ticket, onClose, focusRequest, children }) {
  const panel = useRef(null);
  React.useEffect(() => {
    panel.current?.focus({ preventScroll: true });
    panel.current?.scrollIntoView({ block: 'start', behavior: 'auto' });
  }, [focusRequest]);
  return (
    <section ref={panel} tabIndex={-1} aria-label={`Review ticket ${ticket.ticket_id}`}
             data-testid="selected-review" className="mt-4">
      <div className="flex items-center justify-between gap-2 flex-wrap">
        <div className="font-mono caption">
          Ticket {ticket.ticket_id} · {ticket.agreement_id || 'library proposal'} · {ticket.category_key}
        </div>
        <button className="btn btn-sm" onClick={onClose}>← back to queue</button>
      </div>
      {children}
    </section>
  );
}

function TicketDesk({ ticket, onDone, onError, onClose }) {
  // A NEW draft is EMPTY, never seeded from the proposal. Only the lawyer's
  // own unsent fields return, under this ticket's key, in this session only.
  const [approved, setApproved] = useRetainedState(ticketDraftKey(ticket.ticket_id, 'approved'), '');
  const [clauseId, setClauseId] = useRetainedState(ticketDraftKey(ticket.ticket_id, 'clauseId'), '');
  const [title, setTitle] = useRetainedState(ticketDraftKey(ticket.ticket_id, 'title'), '');
  const [rationale, setRationale] = useRetainedState(ticketDraftKey(ticket.ticket_id, 'rationale'), '');
  const [note, setNote] = useRetainedState(ticketDraftKey(ticket.ticket_id, 'note'), '');
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const finish = () => {
    discardTicketDraft(ticket.ticket_id);
    onDone(ticket.ticket_id);
  };

  const edited = approved.trim() !== ticket.proposed_text.trim();
  const ready = approved.trim() && clauseId.trim() && title.trim() && rationale.trim();

  if (confirming) {
    return (
      <div className="panel p-4 mt-4" style={{ borderColor: 'var(--accent)' }}>
        <PanelHead
          title="This is what will be minted"
          sub="A clause version cannot be edited once it exists. Read it before you agree."
        />
        {/* The paper surface, because this is contract language. */}
        <div className="paper p-5 mt-2" style={{ color: '#1A1714' }}>
          <div className="font-mono" style={{ fontSize: 11, letterSpacing: '.06em' }}>
            {clauseId.trim()} · v1 · {title.trim()}
          </div>
          <div className="font-serif mt-3 review-literal" style={{ fontSize: 16, lineHeight: 1.6 }}>
            {approved.trim()}
          </div>
        </div>

        <div className="mt-3 caption" style={{ lineHeight: 1.7 }}>
          Approved by you, on the record, permanently. The origin is{' '}
          <strong>derived from where this text came from</strong> — you do not
          choose it, and it cannot be changed afterwards.
          {edited
            ? <> This text <strong>differs</strong> from what was proposed, so it
                will be recorded as edited before approval.</>
            : <> This is the proposed text unchanged, so it will be recorded as
                approved without edit — which is the figure Legal watches.</>}
        </div>

        <div className="flex gap-2 mt-4">
          <button className="btn" onClick={() => setConfirming(false)}>← back</button>
          <ActButton
            className="btn btn-primary" disabled={busy}
            data-testid="confirm-verify"
            onClick={async () => {
              setBusy(true); onError(null);
              const r = await API.verifyTicket({
                ticket_id: ticket.ticket_id,
                approved_text: approved.trim(),
                new_clause_id: clauseId.trim(),
                title: title.trim(),
                rationale: rationale.trim(),
                note: note.trim() || null,
              });
              setBusy(false);
              if (!r.ok) { onError(r.reason); setConfirming(false); return; }
              finish();
            }}
          >{busy ? 'minting…' : '✓ mint this wording'}</ActButton>
        </div>
      </div>
    );
  }

  return (
    <div className="panel p-4 mt-4 review-ticket-desk">
      <div className="flex items-start justify-between gap-2 flex-wrap">
        <div>
          <PanelHead
            title={`Ticket ${ticket.ticket_id}`}
            sub={`${ticket.category_key} · ${ticket.severity} · ${ticket.reason_code}`}
          />
        </div>
        <button className="btn btn-sm" onClick={onClose}>close</button>
      </div>

      {/* What is being proposed, badged, and quarantined. This text is not
          selectable by anything and cannot reach a contract from here. */}
      <div className="review-workbench">
      <section className="review-reference" aria-label="Proposal and supporting evidence">
      <div className="mt-2">
        <div className="section-label mb-2">Proposed wording · not approved</div>
        <div className="flex items-center gap-2 mb-2 flex-wrap">
          <ProvenanceBadge badge={ticket.provenance_badge} />
          <span className="caption">opened by {ticket.opened_by}</span>
        </div>
        <div className="panel-2 p-3 review-source" tabIndex={0}
             role="region" aria-label="Proposed wording, read only" data-testid="proposed-wording">
          <div className="font-serif review-literal" style={{ fontSize: 15, lineHeight: 1.6 }}>
            {ticket.proposed_text}
          </div>
        </div>
        {ticket.provenance_badge === 'AI CANDIDATE'
          && <DraftProvenance ticket={ticket} />}
        {ticket.provenance_badge === 'CUSTOMER PAPER' && <ContractTicketSource ticket={ticket} />}
      </div>

      <TicketEvidence ticketId={ticket.ticket_id} />

      <TicketConsultations ticketId={ticket.ticket_id} />

      {/* WHAT ELSE THIS SUPPLIER'S PAPER SAYS ABOUT THE SAME THING (0114).
          Beside the approval form (above it on narrow screens): a reviewer deciding whether this
          wording is acceptable should see what the company has already agreed
          with the same company, while they are deciding rather than after.

          IT WARNS AND IT NEVER GATES. No control below is disabled by
          anything here, `ready` does not consult it, and a reviewer may verify
          or reject with every echo on the screen unread. A screen that refused
          to proceed because of one of these would be the defect.

          ONLY WHERE THERE IS A DEAL. A library ticket carries no
          `agreement_id` — it belongs to no contract, so there is no portfolio
          to compare it against, and drawing an empty panel there would suggest
          somebody looked. */}
      {ticket.agreement_id && (
        <CrossContractEchoes agreementId={ticket.agreement_id} />
      )}
      </section>

      <section className="review-decision" aria-label="Your review draft">
      <div className="mt-2">
        <label className="section-label">The wording you approve</label>
        <textarea aria-label="The wording you approve"
          className="mt-1.5 w-full font-serif review-editor" rows={10}
          data-testid="approved-text"
          placeholder="Type or paste the wording you are approving."
          value={approved} onChange={(e) => setApproved(e.target.value)}
        />
        {/* Said out loud, because an empty box looks like an oversight until
            somebody explains that it is the point. */}
          <div className="caption mt-1.5" style={{ lineHeight: 1.6 }}>
          New drafts start empty. Your unsent wording stays with this ticket
          while you move around the desk, until you sign out or reload the page.
          It is not an approval and has not been filed.
          {approved.trim() && (
            edited
              ? <> <span style={{ color: 'var(--accent-2)' }}>This differs from the proposal.</span></>
              : <> <span style={{ color: 'var(--mute)' }}>This matches the proposal exactly.</span></>
          )}
        </div>
      </div>

      <div className="review-identifiers grid grid-cols-2 gap-3 mt-3">
        <div>
          <label className="section-label">New clause id</label>
          <input aria-label="New clause id" className="mt-1.5 w-full font-mono" placeholder="DP-H-020"
                 value={clauseId} onChange={(e) => setClauseId(e.target.value)} />
        </div>
        <div>
          <label className="section-label">Title</label>
          <input aria-label="Title" className="mt-1.5 w-full" placeholder="72-hour notification"
                 value={title} onChange={(e) => setTitle(e.target.value)} />
        </div>
      </div>

      <div className="mt-3">
        <label className="section-label">Rationale</label>
        <input aria-label="Rationale" className="mt-1.5 w-full" placeholder="Why this wording is acceptable"
               value={rationale} onChange={(e) => setRationale(e.target.value)} />
      </div>

      <div className="mt-3">
        <label className="section-label">Note (optional on approval, required on rejection)</label>
        <input aria-label="Note (optional on approval, required on rejection)" className="mt-1.5 w-full" value={note}
               onChange={(e) => setNote(e.target.value)} />
      </div>

      <div className="flex gap-2 mt-4 flex-wrap">
        {/* Verify goes through a confirmation. Reject does not need one: it
            mints nothing and is reversible by opening a new ticket. Friction
            belongs where the irreversibility is. */}
        <button className="btn btn-primary" disabled={!ready}
                data-testid="verify"
                onClick={() => setConfirming(true)}>
          ✓ verify…
        </button>
        <ActButton className="btn" disabled={busy || !note.trim()}
                data-testid="reject"
                onClick={async () => {
                  setBusy(true); onError(null);
                  const r = await API.rejectTicket({
                    ticket_id: ticket.ticket_id, note: note.trim(),
                  });
                  setBusy(false);
                  if (!r.ok) { onError(r.reason); return; }
                  finish();
                }}>
          ✕ reject
        </ActButton>
        {!note.trim() && (
          <span className="caption self-center">A rejection needs a note.</span>
        )}
      </div>
      </section>
      </div>
    </div>
  );
}

// ── The override decision surface ────────────────────────────────────────
// PER FINDING. There is no "approve all" button here, and adding one would not
// merely be a shortcut — the deciding person would not have seen each finding,
// which is the entire reason the workflow is per-finding.
function overrideNoteKey(requestId, findingRef) {
  return JSON.stringify([String(requestId), findingRef]);
}

function OverrideDecisions({ me, onError }) {
  // One act at a time. useActs guards with a ref, so a second click in the
  // same tick never reaches the network — `disabled` alone cannot, because it
  // only takes effect after a render.
  const acts = useActs();

  const requests = usePane(() => API.overrides());
  const findings = usePane(() => API.overrideFindings());
  const notified = usePane(() => API.overrideNotified());
  // The socialisation RECORD (0015): the one fact no view ever carried out is
  // window_setting — the value in force when the window opened, stored at the
  // time precisely so shortening the setting later cannot retrospectively
  // close a window. A reviewer deciding after the window should see which
  // rule the window actually ran under.
  const socialisation = usePane(() => API.overrideSocialisation());
  const [note, setNote] = useRetainedState('override-finding-notes', {});

  if (requests.status === 'loading') return <Loading />;
  if (requests.status === 'failed') return <LoadFailed reason={requests.reason} />;
  // The findings and the socialisation list are checked too, and this is not
  // belt-and-braces. `(findings.rows ?? [])` turns a FAILED read into an empty
  // list, so a request would render "Each finding, decided on its own" with
  // nothing under it — "we could not ask" wearing the clothes of "there is
  // nothing here", on the one screen where the whole point is deciding each
  // finding. A refusal is not an empty list, here as everywhere.
  if (findings.status === 'failed') return <LoadFailed reason={findings.reason} />;
  if (notified.status === 'failed') return <LoadFailed reason={notified.reason} />;
  if (findings.status === 'loading' || notified.status === 'loading') return <Loading />;

  const open = requests.rows.filter((r) => r.state === 'socialised');
  const reload = () => { requests.reload(); findings.reload(); notified.reload(); };
  const finishFinding = (requestId, findingRef) => {
    const key = overrideNoteKey(requestId, findingRef);
    setNote((previous) => {
      const next = { ...previous };
      delete next[key];
      return next;
    });
    reload();
  };

  if (open.length === 0) {
    return <Empty
      kicker="override requests"
      line="Nothing is waiting on an override decision."
      sub="A request appears here once it has been socialised to its watchers. Nothing can be decided before that, or before its review window closes." />;
  }

  return (
    <div>
      <div className="caption mb-3">
        Unsent notes stay with their request and finding until you sign out or reload.
        A note is filed only with its decision.
      </div>
      {open.map((r) => {
        const mine = (findings.rows ?? []).filter((f) => f.request_id === r.request_id);
        const told = (notified.rows ?? []).filter((n) => n.request_id === r.request_id);
        return (
          <div className="panel p-4 mb-4" key={r.request_id}>
            <PanelHead
              title={`Request ${r.request_id} · ${r.agreement_id ?? 'no deal'}`}
              sub={`asked for by ${r.requested_by}`}
              right={r.window_closed
                ? <span className="chip chip-ok">window closed</span>
                // The window state is visible, always. A reviewer who cannot see
                // it would try, be refused, and learn the rule from an error.
                : <span className="chip chip-pending" title={r.window_closes}>
                    window open until {new Date(r.window_closes).toLocaleString()}
                  </span>}
            />

            {/* The justification, in the established idiom: oversized teal
                quotation marks around every human justification. */}
            <div className="panel-2 p-3 mt-1 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 }}>
                {r.justification}
              </div>
              {r.commercial_pressure && (
                <div className="caption mt-2" style={{ paddingLeft: 22 }}>
                  Commercial pressure cited: {r.commercial_pressure}
                </div>
              )}
            </div>

            <div className="caption mt-2">
              Socialised to {told.length} {told.length === 1 ? 'person' : 'people'}
              {told.length > 0 && <> — {told.map((t) => `${t.person} (${t.reason})`).join(', ')}</>}
            </div>
            {/* The record's own account of the window, drawn only when the
                read answered — the caption above lists who; this says under
                which rule, from the value FROZEN when the window opened. */}
            {socialisation.status === 'loaded' && (() => {
              const s = socialisation.rows.find((x) => x.request_id === r.request_id);
              if (!s) return null;
              return (
                <div className="caption mt-1" data-testid="socialisation-record">
                  The record: socialised {new Date(s.socialised_at).toLocaleString()},
                  {' '}window ran under <span className="font-mono">{s.window_setting}</span>
                  {' '}(the value in force at the time), {s.notified_count} notified.
                </div>
              );
            })()}

            <div className="mt-4">
              <div className="section-label mb-2">Each finding, decided on its own</div>
              {mine.map((f) => {
                const findingKey = overrideNoteKey(r.request_id, f.finding_ref);
                return (
                <div className="panel-2 p-3 mb-2" key={f.finding_ref}>
                  <div className="flex items-start justify-between gap-4">
                    <div className="min-w-0">
                      <div className="font-mono" style={{ fontSize: 11, color: 'var(--mute-2)' }}>
                        {f.finding_ref}
                      </div>
                      <div className="text-[13px] mt-1" style={{ color: 'var(--ink)', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
                        {f.summary}
                      </div>
                    </div>
                    <span className={`chip ${f.severity === 'High' ? 'chip-high' : 'chip-std'}`}>
                      {f.severity}
                    </span>
                  </div>

                  {f.decision ? (
                    <div className="mt-2 flex items-center gap-2">
                      <span className={`chip ${f.decision === 'approved' ? 'chip-ok' : 'chip-err'}`}>
                        {f.decision}
                      </span>
                      <span className="caption">
                        by {f.decided_by}{f.note ? ` — ${f.note}` : ''}
                      </span>
                    </div>
                  ) : (
                    <div className="flex gap-2 mt-3 override-finding-actions">
                      <textarea
                        className="override-finding-note" rows={3} style={{ padding: '4px 8px' }}
                        aria-label={`Note on ${f.finding_ref}, request ${r.request_id} (required to reject)`}
                        data-testid={`override-note-${r.request_id}-${f.finding_ref}`}
                        placeholder="note (required to reject)"
                        value={note[findingKey] ?? ''}
                        onChange={(e) => {
                          const value = e.target.value;
                          setNote((previous) => ({ ...previous, [findingKey]: value }));
                        }}
                      />
                      {/* Legal deciding a finding is permanent — 0085 binds
                          decided_by on the first write and refuses the second.
                          The refusal is the record's guard; this is the
                          screen's, so a double-click does not send a person a
                          refusal for an act they performed once. */}
                      <button
                        className="btn btn-sm"
                        disabled={!r.window_closed || acts.busy !== null}
                        data-testid={`approve-${r.request_id}-${f.finding_ref}`}
                        onClick={() => acts.run(`approve-${findingKey}`, async () => {
                          onError(null);
                          const x = await API.decideOverride({
                            request_id: r.request_id, finding_ref: f.finding_ref,
                            decision: 'approved', note: note[findingKey] || null,
                          });
                          if (!x.ok) onError(x.reason); else finishFinding(r.request_id, f.finding_ref);
                        })}
                      >✓ approve this finding</button>
                      <button
                        className="btn btn-sm"
                        data-testid={`reject-${r.request_id}-${f.finding_ref}`}
                        disabled={!r.window_closed || !(note[findingKey] ?? '').trim()
                                  || acts.busy !== null}
                        onClick={() => acts.run(`reject-${findingKey}`, async () => {
                          onError(null);
                          const x = await API.decideOverride({
                            request_id: r.request_id, finding_ref: f.finding_ref,
                            decision: 'rejected', note: note[findingKey].trim(),
                          });
                          if (!x.ok) onError(x.reason); else finishFinding(r.request_id, f.finding_ref);
                        })}
                      >✕ reject</button>
                    </div>
                  )}
                </div>
              );
              })}
              {/* No approve-all. Deliberately, and said so, because its absence
                  looks like an omission to anybody who has not read ADR-0008. */}
              <div className="caption mt-2">
                Each finding is decided on its own. There is no approve-all —
                accepting a governing-law conflict does not accept an uncapped
                indemnity, and one button for both would mean nobody looked at
                the second.
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── The desk ─────────────────────────────────────────────────────────────
// ── Filing an executed agreement ─────────────────────────────────────────
//
// The one act in this system that cannot be taken back. Everything filed here
// is frozen the moment it lands — there is no correcting it, only superseding
// it — which is why the last step is a confirmation showing exactly what will
// be written rather than a button that does it.
//
// NO PERMISSION DECISION IS MADE ON THIS SCREEN. The button is offered to
// whoever is looking at the pane. A requester, a viewer and an auditor are
// refused by the database, and what they see is the database's own sentence.
// A screen that hid the button would be a control-shaped decoration: it would
// look like a rule while enforcing nothing, and the rule that does the work
// would stop being the one anybody could point at.
function executionDraftKey(runId, field) {
  return `execution-run:${runId}:${field}`;
}

function discardExecutionDraft(runId) {
  for (const field of ['form', 'signatories']) discardDraft(executionDraftKey(runId, field));
}

function FileExecution({ run, onDone, onError }) {
  const [form, setForm] = useRetainedState(executionDraftKey(run.run_id, 'form'), {
    executed_on: '', effective_on: '', term_end: '',
    filename: '', byte_size: '', sha256: '', storage_uri: '', signed_on: '',
    signature_evidence: '',
  });
  const [signatories, setSignatories] = useRetainedState(executionDraftKey(run.run_id, 'signatories'), [
    { name: '', party: 'ours', method: 'electronic', signed_on: '', title: '' },
  ]);
  const [confirming, setConfirming] = useState(false);
  const [busy, setBusy] = useState(false);
  const filingPanel = useRef(null);
  React.useEffect(() => {
    filingPanel.current?.focus({ preventScroll: true });
    filingPanel.current?.scrollIntoView({ block: 'start', behavior: 'auto' });
  }, []);

  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
  const named = signatories.filter((s) => s.name.trim() && s.signed_on.trim());
  const complete = ['executed_on', 'effective_on', 'filename', 'byte_size',
                    'sha256', 'storage_uri', 'signed_on']
    .every((k) => String(form[k]).trim()) && named.length > 0;

  const field = (k, label, extra) => (
    <div className="flex-1">
      <label className="caption">{label}</label>
      {/* Named from the same `label` the caption renders — this helper builds
          every field on the execution form, so one line names all of them. */}
      <input className="mt-1 w-full" style={{ padding: '4px 8px' }} aria-label={label}
             data-testid={`exec-${k}`} value={form[k]} onChange={set(k)} {...extra} />
    </div>
  );

  return (
    <section className="panel p-4 mt-4 execution-filing" data-testid="execution-filing"
             ref={filingPanel} tabIndex={-1} aria-label={`Execution draft for ${run.agreement_id}, assembly ${run.run_id}`}>
      <PanelHead
        title="File the signed agreement"
        sub="Frozen the moment it lands. There is no correcting a filing, only superseding it."
      />

      <div className="font-mono caption">{run.agreement_id} · {run.vendor} · assembly {run.run_id}</div>
      <div className="caption mt-2">
        Unfiled evidence stays with this assembly while you move around the desk,
        until you sign out or reload. It is not a signed-agreement record yet.
      </div>

      <div className="panel-2 p-3 mt-1">
        <div className="tag" style={{ color: 'var(--accent-2)' }}>what is checked first</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
          The assembly must belong to <strong>this</strong> deal, every clause in
          it must still be one the library would choose, and any finding that
          blocked it must have been individually approved. All three are checked
          before anything is written.
        </div>
      </div>

      <div className="mt-4">
        <div className="section-label mb-2">The signed document</div>
        <div className="flex gap-2 mb-2 items-end">
          {field('filename', 'File name')}
          {field('byte_size', 'Size in bytes', { type: 'number' })}
        </div>
        <div className="flex gap-2 mb-2 items-end">
          {field('sha256', 'Fingerprint (SHA-256)')}
        </div>
        <div className="flex gap-2 mb-2 items-end">
          {field('storage_uri', 'Where it is kept')}
        </div>
        {/* SAID PLAINLY RATHER THAN DRESSED UP. There is no document store yet,
            so whatever is typed here is written down exactly as given and is
            not resolvable to anything. Inventing a location on the filer's
            behalf would look real to every later reader, including the report
            that exists to find missing evidence. */}
        <div className="caption mt-1">
          Written down exactly as you give it. There is no document store yet,
          so this is a reference somebody will have to follow by hand.
        </div>

        {/* NO CERTIFICATE FIELD, AND THE ABSENCE IS SHOWN RATHER THAN HIDDEN —
            the same way the system already reports evidence it does not have.
            The endpoint refuses a certificate outright, so a field here would
            collect something that could not be filed. */}
        <div className="panel-2 p-3 mt-3">
          <div className="tag" style={{ color: 'var(--mute-2)' }}>known gap</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            The signature certificate itself cannot be attached yet — there is
            nowhere to keep the file. The fingerprint and the signatories are
            filed now; the certificate is attached when the document store
            exists.
          </div>
        </div>
      </div>

      <div className="mt-4">
        <div className="section-label mb-2">Dates</div>
        <div className="flex gap-2 items-end">
          {field('executed_on', 'Signed on', { type: 'date' })}
          {field('effective_on', 'Effective from', { type: 'date' })}
          {field('term_end', 'Term ends (optional)', { type: 'date' })}
          {field('signed_on', 'Document dated', { type: 'date' })}
        </div>
      </div>

      <div className="mt-4">
        <div className="section-label mb-2">Who signed</div>
        {signatories.map((s, i) => (
          <div className="flex gap-2 mb-2 items-end" key={i}>
            <div className="flex-1">
              <label className="caption">Name</label>
              <input aria-label="Name" className="mt-1 w-full" style={{ padding: '4px 8px' }}
                     data-testid={`signatory-name-${i}`} value={s.name}
                     onChange={(e) => setSignatories(signatories.map((x, j) =>
                       j === i ? { ...x, name: e.target.value } : x))} />
            </div>
            <div style={{ width: 150 }}>
              <label className="caption">Title (optional)</label>
              <input aria-label="Title (optional)" className="mt-1 w-full" style={{ padding: '4px 8px' }} value={s.title}
                     data-testid={`signatory-title-${i}`}
                     onChange={(e) => setSignatories(signatories.map((x, j) =>
                       j === i ? { ...x, title: e.target.value } : x))} />
            </div>
            <div style={{ width: 130 }}>
              <label className="caption">Signed on</label>
              <input aria-label="Signed on" className="mt-1 w-full" type="date" style={{ padding: '4px 8px' }}
                     value={s.signed_on} data-testid={`signatory-date-${i}`}
                     onChange={(e) => setSignatories(signatories.map((x, j) =>
                       j === i ? { ...x, signed_on: e.target.value } : x))} />
            </div>
            <select className="font-mono" style={{ padding: '5px 8px' }} value={s.party}
                    data-testid={`signatory-party-${i}`}
                    aria-label="Which side this signatory signs for"
                    onChange={(e) => setSignatories(signatories.map((x, j) =>
                      j === i ? { ...x, party: e.target.value } : x))}>
              <option value="ours">ours</option>
              <option value="theirs">theirs</option>
            </select>
            <select className="font-mono" style={{ padding: '5px 8px' }} value={s.method}
                    data-testid={`signatory-method-${i}`}
                    aria-label="How this signatory signed"
                    onChange={(e) => setSignatories(signatories.map((x, j) =>
                      j === i ? { ...x, method: e.target.value } : x))}>
              <option value="electronic">electronic</option>
              <option value="wet_ink">wet ink</option>
            </select>
            {signatories.length > 1 && (
              <button className="btn btn-sm"
                      onClick={() => setSignatories(signatories.filter((_, j) => j !== i))}>−</button>
            )}
          </div>
        ))}
        <button className="btn btn-sm"
                onClick={() => setSignatories([...signatories,
                  { name: '', party: 'theirs', method: 'electronic', signed_on: '', title: '' }])}>
          + another signatory
        </button>
      </div>

      {confirming && (
        <div className="panel-2 p-3 mt-4" style={{ borderColor: 'var(--warn)' }}>
          <div className="tag" style={{ color: 'var(--warn)' }}>this cannot be undone</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.8 }}>
            Filing <span className="font-mono">{form.filename}</span>{' '}
            ({form.byte_size} bytes) against{' '}
            <span className="font-mono">{run.agreement_id}</span>, citing
            assembly <span className="font-mono">{run.run_id}</span>, signed{' '}
            {form.executed_on} and effective {form.effective_on}, by{' '}
            {named.map((s) => s.name).join(', ')}.
          </div>
        </div>
      )}

      <div className="flex gap-2 mt-4">
        {confirming && (
          <button className="btn" onClick={() => setConfirming(false)}>back</button>
        )}
        <ActButton className={confirming ? 'btn btn-primary' : 'btn'}
                disabled={busy || !complete}
                data-testid={confirming ? 'confirm-execution' : 'review-execution'}
                onClick={async () => {
                  if (!confirming) { setConfirming(true); return; }
                  setBusy(true); onError(null);
                  const r = await API.executeAgreement({
                    agreement_id: run.agreement_id,
                    run_id: run.run_id,
                    executed_on: form.executed_on,
                    effective_on: form.effective_on,
                    term_end: form.term_end || null,
                    filename: form.filename,
                    byte_size: Number(form.byte_size),
                    sha256: form.sha256.trim(),
                    storage_uri: form.storage_uri.trim(),
                    signed_on: form.signed_on,
                    signature_evidence: form.signature_evidence.trim() || null,
                    signatories: named.map((s) => ({
                      name: s.name.trim(), party: s.party, method: s.method,
                      signed_on: s.signed_on, title: s.title.trim() || null,
                    })),
                  });
                  setBusy(false);
                  // The refusal's own sentence, unchanged. It names the deal the
                  // assembly actually belongs to, or the clause that is no
                  // longer current, or the finding nobody approved — and every
                  // one of those is the only part anybody can act on.
                  if (!r.ok) { setConfirming(false); onError(r.reason); return; }
                  discardExecutionDraft(run.run_id);
                  onDone(run.run_id);
                }}>
          {confirming ? '✓ file it' : 'review what will be filed…'}
        </ActButton>
      </div>
    </section>
  );
}

// ── The assemblies Legal can see ─────────────────────────────────────────
function RunsForLegal({ onError, onFiled }) {
  const runs = usePane(() => API.runs());
  const decisions = usePane(() => API.runDecisions());
  const findings = usePane(() => API.runFindings());
  // Named for what it holds rather than reusing the shorter name below, and
  // not for taste: the review desk's own state declaration is the exact line a
  // standing check keys on, and a second identical copy in this file would
  // leave that check silently watching the wrong one. The name is also the
  // reason this comment does not quote the line.
  const [openRun, setOpenRun] = useState(null);
  const currentRun = useRef(openRun);
  currentRun.current = openRun;
  React.useEffect(() => () => { currentRun.current = null; }, []);
  const chooseRun = (runId) => {
    const next = currentRun.current === runId ? null : runId;
    currentRun.current = next;
    setOpenRun(next);
    onError(null);
  };
  const finishExecution = (runId) => {
    if (currentRun.current === runId) {
      currentRun.current = null;
      setOpenRun(null);
    }
    runs.reload();
    onFiled();
  };
  const reportExecution = (runId, reason) => {
    if (currentRun.current === runId) onError(reason);
  };

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

  const chosen = (runs.rows ?? []).find((r) => r.run_id === openRun);

  return (
    <div>
      <PanelHead
        title="Assembled contracts"
        sub="What the engine selected, what the rules found, and whether it may be signed."
      />
      {(runs.rows ?? []).length === 0 ? (
        <Empty
          kicker="assemblies"
          line="No contract has been assembled yet."
          sub="A requester assembles one from their deal; it appears here when they do." />
      ) : (runs.rows ?? []).map((r) => (
        <div className="panel p-3 mb-2" key={r.run_id}>
          <div className="flex items-baseline justify-between gap-2">
            <div className="min-w-0">
              <span className="text-[13px]">{r.vendor}</span>
              <span className="font-mono ml-2" style={{ fontSize: 11, color: 'var(--mute-2)' }}>
                {r.agreement_id}
              </span>
              <div className="caption mt-0.5">
                {new Date(r.created_at).toLocaleString()} · {r.decisions} clauses ·{' '}
                {r.findings} findings
              </div>
            </div>
            <div className="flex gap-2 items-center">
              {r.gate_open
                ? <span className="chip chip-ok">nothing blocking</span>
                : <span className="chip chip-err">{r.blocking} blocking</span>}
              <button className="btn btn-sm" data-testid={`open-run-${r.run_id}`}
                      onClick={() => chooseRun(r.run_id)}>
                {openRun === r.run_id ? 'close' : 'open'}
              </button>
            </div>
          </div>

          {openRun === r.run_id && (
            <div className="mt-3 pt-3 border-t hair">
              {(decisions.rows ?? []).filter((d) => d.run_id === r.run_id).map((d) => (
                <div className="py-1" key={d.seq}>
                  <span className="text-[13px]">{d.category}</span>
                  {d.clause_id
                    ? <span className="font-mono ml-2" style={{ fontSize: 11, color: 'var(--mute-2)' }}>
                        {d.clause_id}@v{d.version}
                      </span>
                    : <span className="chip chip-unknown ml-2">no clause</span>}
                  <div className="caption mt-0.5">{d.reason}</div>
                </div>
              ))}
              {(findings.rows ?? []).filter((f) => f.run_id === r.run_id).map((f) => (
                <div className="py-1" key={`f${f.seq}`}
                     style={{ borderTop: '1px solid var(--line)' }}>
                  <span className={f.severity === 'High' ? 'chip chip-err' : 'chip chip-pending'}>
                    {f.severity}
                  </span>
                  <span className="text-[13px] ml-2">{f.title}</span>
                  <span className="font-mono ml-2" style={{ fontSize: 11, color: 'var(--mute-2)' }}>
                    {f.rule_id}@v{f.rule_version}
                  </span>
                  <div className="caption mt-0.5">{f.detail}</div>
                </div>
              ))}
            </div>
          )}
        </div>
      ))}

      {chosen && (
        <FileExecution
          key={chosen.run_id}
          run={chosen}
          onError={(reason) => reportExecution(chosen.run_id, reason)}
          onDone={finishExecution} />
      )}
    </div>
  );
}

// The legal home of the 2026-08-10 design set: the desk's headline figures
// as framed counters with their stamps, then the queue itself. Every figure
// is counted from rows a role-scoped read returned; a read that failed or
// has not landed shows a dash, never a zero — "none are waiting" and "we
// could not ask" are different facts.
// -- What survived the reviewer's pen -------------------------------------
//
// THE QUESTION THIS PANEL ASKS, and it is the one the whole AI story turns
// on. Every word of contract language in this system was approved by a named
// lawyer; some of those words were first proposed by a machine. So: when the
// lawyer rewrote the proposal, did they change the WORDS, or did they change
// the MEANING?
//
// TWO NUMBERS, TWO KINDS OF THING, AND THE DIFFERENCE IS THE POINT.
//
//   - The MEASUREMENT is textual distance. The database computes it at the
//     moment of approval, over the two texts, with arithmetic. It is exact,
//     it is reproducible, and it says nothing whatever about meaning: a
//     lawyer who changes "shall" to "shall not" moves it barely at all.
//
//   - The ESTIMATE is a model's opinion about how far the meaning moved. No
//     arithmetic answers that question, which is why this one place asks for
//     an opinion at all (ADR-0005's one deliberate bend, argued in
//     doorway/advisory.py).
//
// BOTH LABELS COME OUT OF THE ROW, never out of this file. `cw.ticket_metrics`
// (0030) carries `measurement_label` and `judgment_label` as columns for
// exactly this reason: the single way this feature does harm is a screen
// printing two numbers side by side and letting a reader take them for the
// same kind of thing, and a label a screen remembers is a label a screen can
// forget. db/test/what-survived-the-pen.test.mjs holds this file to it.
//
// NOTHING HERE GATES ANYTHING. An estimate saying the meaning moved a long
// way does not reopen a ticket, unmint a clause or block anything: the
// wording is already approved and a named lawyer already approved it. What
// this is for is the next decision -- whether that lawyer wants to look
// again, and whether Legal wants to keep letting a machine draft.
//
// ASKED ONLY WHERE THERE IS SOMETHING TO COMPARE. A ticket is offered the
// question when it has been VERIFIED, because the comparison is between what
// was proposed and what was approved and there is no approved text before
// then. That is reading the row's own state, not a second copy of the rule --
// the doorway refuses it in its own words either way, and this panel prints
// that refusal when it comes.
// THE READ COMES IN AS A PROP, and that is not tidiness. The desk's figure
// "wordings never asked about" counts this panel's rows. Asked twice, the two
// copies reload independently — so pressing "ask" here refreshed the list and
// left the figure above it counting a wording that had just been asked about.
// A figure and the list it leads to are ONE SET, which is this repository's
// standing rule (S319, S333, db/test/a-figure-and-its-drill-are-one-set), and
// the cheapest way to keep it is to have one read rather than two agreeing.
function WhatSurvivedThePen({ metrics, onError }) {
  const [busy, setBusy] = useState(null);

  // ABOVE THE EARLY RETURNS, with `?? []` behind it, because the read has not
  // landed on the first render and a hook reached on some renders and not
  // others blanks the pane (the standing trap, and db/test/hook-order).
  //
  // `asked` IS DERIVED FOR THE FACET, not stored. The view leaves
  // judgment_outcome null for a ticket nobody asked about, and a facet drops
  // empty values from its options — so "never asked", which is the state this
  // panel exists to make visible, would have been the one thing you could not
  // select. Naming it turns it into a value like any other.
  const decided = (metrics.rows ?? [])
    .filter((m) => m.state === 'verified')
    .map((m) => ({ ...m, asked: m.judgment_outcome || 'not asked' }));
  const filter = useListFilter(decided, {
    view: 'review-desk:decided',
    fields: ['ticket_id', 'agreement_id', 'category_key'],
    facet: 'asked',
  });

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

  return (
    <div data-testid="what-survived-the-pen">
      <PanelHead
        title="What survived your pen"
        sub="Wording you approved, with the distance the database measured and — where one was asked for — a model's opinion about how far the meaning moved. Two different kinds of number, each wearing its own label."
        right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="survived-the-pen"
                  placeholder="ticket, deal or category"
                  facetLabel="asked or not" />

      {filter.shown.length === 0
        ? (filter.filtering
            ? <NoMatch kicker="approved wording" noun="approval" />
            : <Empty
                kicker="approved wording"
                line="Nothing has been approved yet."
                sub="A ticket appears here once it has been verified, because the comparison is between the wording proposed and the wording you approved." />)
        : filter.shown.map((m) => (
            <div className="panel-2 p-3 mb-2" key={m.ticket_id}
                 data-testid="pen-row">
              <div className="flex items-baseline justify-between gap-3">
                <span className="font-mono text-[12.5px]">
                  ticket {m.ticket_id} · {m.agreement_id || 'no deal'} ·{' '}
                  {m.category_key}
                </span>
                <ActButton className="btn btn-sm"
                           data-testid="ask-semantic-difference"
                           disabled={busy === m.ticket_id}
                           onClick={async () => {
                             setBusy(m.ticket_id);
                             onError(null);
                             const answer = await API.judgeSemanticDifference({
                               ticket_id: m.ticket_id,
                             });
                             setBusy(null);
                             if (!answer.ok) { onError(answer.reason); return; }
                             // Reloads the read the DESK holds, so the figure
                             // above and these rows move together.
                             metrics.reload();
                           }}>
                  {m.judgments_recorded > 0
                    ? 'ask again' : 'ask how far the meaning moved'}
                </ActButton>
              </div>

              {/* THE ARITHMETIC. Printed with the label the view carries, and
                  a null is said rather than drawn as a blank: a ticket
                  verified before 0029 was grandfathered in without one, and
                  "not measured" and "measured as nought" are different
                  sentences. */}
              <div className="caption mt-1.5">
                {m.measurement_label}:{' '}
                {m.measured_edit_similarity === null
                  || m.measured_edit_similarity === undefined
                  ? 'not measured — this ticket was verified before the figure existed'
                  : m.measured_edit_similarity}
              </div>

              {/* THE OPINION. Three states, all of them said out loud: never
                  asked, asked and answered, asked and honestly absent. */}
              <div className="caption mt-1" style={{ lineHeight: 1.7 }}>
                {!m.judgment_outcome
                  ? <>{m.judgment_label}: not asked for.</>
                  : m.judgment_outcome === 'absent'
                    ? <>{m.judgment_label}: none was obtainable —{' '}
                        <q>{m.judgment_absent_reason}</q>. The request was
                        honoured; the record says so rather than showing a
                        number nobody produced.</>
                    : <>
                        {m.judgment_label}: {m.judgment_outcome}
                        {m.estimated_semantic_difference !== null
                          && m.estimated_semantic_difference !== undefined
                          && <> ({m.estimated_semantic_difference})</>}
                        {m.judgment_basis && <> — {m.judgment_basis}</>}
                      </>}
              </div>

              {m.judgment_outcome && (
                <div className="caption mt-1">
                  asked by {m.judgment_requested_by} ·{' '}
                  {m.judgment_model} {m.judgment_model_version || ''}
                  {m.judgments_recorded > 1
                    && ` · ${m.judgments_recorded} opinions on record`}
                </div>
              )}
            </div>
          ))}
    </div>
  );
}

function ReviewDeskPane({ me }) {
  const tickets = usePane(() => API.waitingTickets());
  const queue   = usePane(() => API.countersignQueue());
  const overrides = usePane(() => API.overrides());
  // The assemblies, read here for the headline count of what still blocks.
  // The same read RunsForLegal below makes for its list — one rule, asked
  // twice, cannot answer differently.
  const runsAbove = usePane(() => API.runs());
  const [open, setOpen] = useAddressedRecord('review-desk');
  const [selectionRequest, setSelectionRequest] = useState(0);
  const currentTicket = useRef(open);
  currentTicket.current = open;
  // The same pair the deal list, the library, the audit record and the access
  // history carry. Seventy-two rows is the longest queue in the application
  // and it belonged to the role that lives in it, with no way through.
  const [q, setQ] = useState('');
  const [severity, setSeverity] = useState('');
  // #153. A third narrowing, drawn only when the queue actually holds more
  // than one shape — a picker offering one option is a control that cannot
  // change anything. It is a CONTROL rather than a figure, so it owes nothing
  // a drill would: it narrows the list it sits above and nothing else.
  const [kind, setKind] = useState('');
  // WHERE EACH FIGURE ABOVE LEADS. Three of this desk's four headline counts
  // are counting a section further down this same page; the fourth counts a
  // pane on another tab. Declared here with the other hooks and above the
  // early returns, because a hook after `if (…) return <Loading />` blanks the
  // pane on the render after the data lands (S318).
  const queueSection = useRef(null);
  const runsSection = useRef(null);
  const countersignSection = useRef(null);
  const penSection = useRef(null);
  // Read ONCE, here, and handed to the panel below. The figure and the rows it
  // counts are then the same set by construction rather than by two reads
  // happening to agree — and pressing "ask" down there reloads what the figure
  // above is counting.
  const metrics = usePane(() => API.ticketMetrics());
  // THE TICKET THIS DESK JUST OPENED, held HERE rather than in the control
  // that made it — see ProposeWording above. Filing reloads the queue, the
  // reload puts this pane back into `loading`, and the early return below
  // unmounts everything under it. This component survives that; the control
  // does not.
  const [filed, setFiled] = useState(null);

  const closeReview = () => {
    setOpen(null);
    queueSection.current?.focus({ preventScroll: true });
    queueSection.current?.scrollIntoView({ block: 'start', behavior: 'auto' });
  };
  const finishReview = (ticketId) => {
    // A slow reply for A must not close B if the lawyer changed tickets.
    if (String(currentTicket.current) === String(ticketId)) closeReview();
    tickets.reload();
  };

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

  // Narrows what is SHOWN. The read already answered only what this role may
  // see, so there is no wider queue behind this to leak.
  const needle = q.trim().toLowerCase();
  const shownTickets = tickets.rows.filter((t) =>
    (!severity || t.severity === severity) &&
    (!kind || proposalKind(t).key === kind) &&
    (!needle
      || String(t.agreement_id || '').toLowerCase().includes(needle)
      || String(t.category_key || '').toLowerCase().includes(needle)
      || String(t.opened_by || '').toLowerCase().includes(needle)));
  const severities = [...new Set(tickets.rows.map((t) => t.severity))].filter(Boolean).sort();
  // The shapes ACTUALLY IN THE QUEUE, derived from the rows rather than from a
  // list of the three the schema allows — a filter offering "validation rule"
  // on a queue holding none is a control that only ever empties the list.
  const kinds = [...new Map(tickets.rows.map((t) => {
    const k = proposalKind(t);
    return [k.key, k.label];
  })).entries()].sort((a, b) => a[1].localeCompare(b[1]));

  const waitingOverrides = (overrides.rows ?? []).filter((r) => r.state === 'socialised');
  const blocked = (runsAbove.rows ?? []).filter((r) => !r.gate_open);
  // Found in the WHOLE queue, never in the filtered view: a ticket opened and
  // then filtered out from behind must not vanish mid-decision.
  // COMPARED AS STRINGS ON BOTH SIDES. A ticket id is a number and an address
  // is text, so `t.ticket_id === open` matched nothing at all once the open
  // ticket started coming from the hash — a deep link that quietly opens
  // nothing is worse than no deep link. The negotiation panes already compared
  // this way; this one did not.
  const full = open && tickets.rows.find((t) => String(t.ticket_id) === String(open));

  return (
    <div>
      <div className="sheet-head">
        <div>
          <div className="sheet-kicker">The desk of {me.display_name || me.person}</div>
          <h1 className="sheet-title mt-1">Legal Home</h1>
        </div>
        <span className="sheet-note">
          oldest wait first — that order is a control, not a preference
        </span>
      </div>
      <div className="font-serif italic mt-2" style={{ fontSize: 14, color: 'var(--mute)' }}>
        Everything waiting on Legal judgement. Select a paper to open.
      </div>

      <div className="mt-5 grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(185px, 1fr))' }}>
        {/* PRESSING A FIGURE GOES TO WHAT IT COUNTED. The queue this one
            counts is the list below, so it clears the search and the severity
            filter on the way — a stale needle would answer a figure promising
            72 with three rows while the figure still said 72 (S319). */}
        <StatBox label="awaiting review" n={tickets.rows.length}
                 mark={tickets.rows.length > 0 &&
                   <span className="stamp stamp-pending stamp-sm">pending</span>}
                 describe={`show all ${tickets.rows.length} tickets waiting on Legal`}
                 to={() => { setQ(''); setSeverity(''); setKind('');
                             requestAnimationFrame(showSection(queueSection)); }} />
        {/* THE ONE THAT IS NOT ON THIS PAGE. Override decisions are the
            approvals tab, and both roles that open this desk hold the deciding
            grant (0015). Holding the grant was not enough on its own: until
            2026-08-24 `approvals` sat on the reviewer's rail only, so this
            press answered a legal_admin — who reaches this desk through
            `review desk` — with "not part of your workspace". The rail now
            carries the tab for both roles; the destination needs no role
            switch here. */}
        <StatBox label="override decisions"
                 n={overrides.status === 'loaded' ? waitingOverrides.length : null}
                 mark={overrides.status === 'loaded' && waitingOverrides.length > 0 &&
                   <span className="stamp stamp-pending stamp-sm">pending</span>}
                 describe={`open the approvals tab (${waitingOverrides.length} waiting)`}
                 to={() => { window.location.hash = '#/approvals'; }} />
        {/* Drawn only when there are any, so the figure leads somewhere only
            then — offering a press to a section that is not rendered would be
            an affordance for nothing. */}
        <StatBox label="grants to countersign"
                 n={queue.status === 'loaded' ? queue.rows.length : null}
                 mark={queue.status === 'loaded' && queue.rows.length > 0 &&
                   <span className="stamp stamp-pending stamp-sm">pending</span>}
                 describe={`show the ${queue.rows.length} grants waiting on your countersign`}
                 to={queue.status === 'loaded' && queue.rows.length > 0
                   ? showSection(countersignSection) : null} />
        {/* HOW MUCH APPROVED WORDING HAS NEVER BEEN ASKED THE QUESTION.
            Counts verified tickets carrying no judgment at all -- not the
            ones whose judgment came back absent, which WERE asked and are a
            different fact. Presses to the panel that holds them. */}
        <StatBox label="wordings never asked about"
                 n={metrics.status === 'loaded'
                   ? metrics.rows.filter((m) => m.state === 'verified'
                       && !m.judgment_outcome).length
                   : null}
                 describe="show the wording you approved, measured and estimated"
                 to={showSection(penSection)} />
        <StatBox label="blocked assemblies"
                 n={runsAbove.status === 'loaded' ? blocked.length : null}
                 mark={runsAbove.status === 'loaded' && blocked.length > 0 &&
                   /* The same ink the blocked state already wears everywhere: a
                      refused gate is an error state for the deal that hit it. */
                   <span className="stamp stamp-err stamp-sm">still blocks</span>}
                 describe={`show the assemblies above the line (${blocked.length} still blocked)`}
                 to={showSection(runsSection)} />
      </div>

      <div className="note-card note-card--rule mt-5" style={{ maxWidth: 430, '--rot': '.5deg' }}>
        <div className="note-title">The rule of this desk</div>
        <div className="rule-text">
          The AI's candidate is never pre-filled into the approval box. The
          reviewer chooses; the screen must not have chosen already.
        </div>
      </div>

      <div className="mt-6" ref={queueSection} tabIndex={-1}
           role="region" aria-label="Review queue" data-testid="review-queue">
        <PanelHead
          title="Waiting on Legal"
          sub="Oldest first. The longest wait is the one this desk exists to show."
          right={<span className="caption">
            {shownTickets.length === tickets.rows.length
              ? `${tickets.rows.length}`
              : `${shownTickets.length} of ${tickets.rows.length}`}
          </span>} />
        {tickets.rows.length > 0 && (
          <div className="list-filter flex gap-2 mb-3 mt-2">
            <input className="font-mono min-w-0 grow" style={{ padding: '5px 9px' }}
                   placeholder="deal, category or who opened it" value={q}
                   aria-label="Search the queue by deal, category or who opened it"
                   onChange={(e) => setQ(e.target.value)} data-testid="desk-search" />
            <select className="font-mono" style={{ padding: '5px 9px' }} value={severity}
                    aria-label="Filter the queue by severity"
                    onChange={(e) => setSeverity(e.target.value)} data-testid="desk-severity">
              <option value="">every severity</option>
              {severities.map((sv) => <option key={sv} value={sv}>{sv}</option>)}
            </select>
            {kinds.length > 1 && (
              <select className="font-mono" style={{ padding: '5px 9px' }} value={kind}
                      aria-label="Filter the queue by what kind of proposal it is"
                      onChange={(e) => setKind(e.target.value)} data-testid="desk-kind">
                <option value="">every kind</option>
                {kinds.map(([key, label]) =>
                  <option key={key} value={key}>{label}</option>)}
              </select>
            )}
          </div>
        )}
        <WaitingList
          order="oldest"
          items={shownTickets.map((t) => ({
            key: t.ticket_id,
            // LEADS WITH THE AGREEMENT. Seventy-two rows read
            // "service · Standard / human-escalated / VENDOR LANGUAGE / 11d"
            // and not one said which contract it was about — while
            // `agreement_id` sat in the very row the screen was drawing from.
            // Legal triages by matter; this queue could not be triaged at all.
            // `|| 'no deal'` BECAUSE A TICKET NEED NOT HAVE ONE, and until
            // 2026-08-25 this row printed the literal string `null` when it
            // did not. `cw.review_ticket.agreement_id` is nullable — a
            // library proposal belongs to no contract — and the panel below
            // already knows it, drawing this desk's cross-contract echoes
            // only `{ticket.agreement_id && …}`. It was reachable before
            // today (builder.py drafts library tickets), and the form added
            // on 2026-08-25 is the first control that lets a PERSON make one,
            // so it was found by driving that form. Both of the two sites
            // that carry a ticket's deal into a template literal are fixed,
            // counted against the code: this one and TicketsPane's below.
            // The words are the ones this file already uses two hundred
            // lines up, rather than a third phrasing for one fact.
            title: `${t.agreement_id || 'no deal'} · ${t.category_key} · ${t.severity}`,
            sub: `${t.reason_code} · opened by ${t.opened_by}`,
            at: t.created_at,
            // TWO BADGES, ANSWERING TWO DIFFERENT QUESTIONS. The provenance
            // badge says WHOSE WORDS these are (ADR-0003); the kind chip says
            // WHAT SORT OF THING is being proposed, and whether this desk is
            // where it is decided. An AI CANDIDATE can be any of the three.
            chips: (
              <>
                <ProvenanceBadge badge={t.provenance_badge} />
                <ProposalKindChip ticket={t} />
                <OverdueChip ticket={t} />
              </>
            ),
          }))}
          onOpen={(it) => { setSelectionRequest((n) => n + 1); setOpen(it.key); }}
          empty={tickets.rows.length > 0
            /* A filter matching nothing is not an empty desk, and must not
               wear its clothes — the difference between "Legal is clear" and
               "you typed something that matches nothing" is the whole point. */
            ? <Empty
                kicker="review desk"
                line="No ticket matches that."
                sub="Clear the search, the severity and the kind filter to see the whole queue again." />
            : <Empty
                kicker="review desk"
                line="Nothing is waiting on Legal."
                sub="Tickets, override decisions, concession approvals and holds all land here, oldest first." />}
        />

        {/* BELOW THE QUEUE, NOT ABOVE IT. Rule 3 at the top of this file: the
            oldest wait is what this desk exists to show, and a form above the
            list would push it down the page. Same placement and same idiom as
            the category form on the governance pane — a button until somebody
            wants it. */}
        <ReviewActionFeedback label="Proposing wording">
          {(report) => <ProposeWording
            filed={filed} onError={report}
            onFiled={(row) => { setFiled(row); if (row) tickets.reload(); }} />}
        </ReviewActionFeedback>
      </div>

      {/* WHICH PANEL OPENS IS THE TICKET'S OWN ANSWER (#153). The mint form is
          drawn only for a proposal this desk decides; a rung, a rule, or a
          draft this reader cannot see gets the panel that says so and goes
          where it IS decided. Offering the form to the other three would be an
          affordance whose only outcome is 0107's refusal. */}
      {full && <SelectedReview key={full.ticket_id} ticket={full} onClose={closeReview}
                              focusRequest={selectionRequest}>
        {proposalKind(full).decidedHere
        ? (
          <ReviewActionFeedback label={`Review ticket ${full.ticket_id}`}>
            {(report) => <TicketDesk
              ticket={full}
              onClose={closeReview}
              onError={report}
              onDone={finishReview}
            />}
          </ReviewActionFeedback>
        ) : (
          <ProposalDecidedElsewhere
            ticket={full} me={me}
            onClose={closeReview}
          />
        )}
      </SelectedReview>}

      <div className="mt-8" ref={runsSection}>
        <ReviewActionFeedback label="Filing execution evidence">
          {(report) => <RunsForLegal onError={report} onFiled={() => overrides.reload()} />}
        </ReviewActionFeedback>
      </div>

      <div className="mt-8" ref={penSection}>
        <ReviewActionFeedback label="Asking about approved wording">
          {(report) => <WhatSurvivedThePen metrics={metrics} onError={report} />}
        </ReviewActionFeedback>
      </div>

      {queue.status === 'loaded' && queue.rows.length > 0 && (
        <div className="mt-8" ref={countersignSection}>
          <ReviewActionFeedback label="Countersigning a Legal role">
          {(report) => <CountersignQueue
            me={me} rows={queue.rows}
            onDone={() => queue.reload()} onError={report}
            // A Legal reviewer holds no route for an account notice, so the
            // raise control renders nothing here. The shared component asks
            // the route table rather than being told which screen it is on.
            routes={[]}
          />}
          </ReviewActionFeedback>
        </div>
      )}
    </div>
  );
}

function OverridesPane({ me }) {
  const [error, setError] = useState(null);
  return (
    <div>
      {error && (
        <div className="panel p-3 mb-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)' }}>{error}</div>
        </div>
      )}
      <PaneHead
        title="Override decisions"
        sub="Each finding on its own. Nothing can be decided before its window closes."
      />
      <OverrideDecisions me={me} onError={setError} />
    </div>
  );
}

// Tickets pane — the whole queue including decided ones, so a reviewer can see
// what they and their colleagues have done.
function TicketsPane({ me }) {
  const pane = usePane(() => API.tickets());
  const filter = useListFilter(pane.rows, {
    view: 'tickets:all',
    fields: ['agreement_id', 'category_key', 'severity', 'opened_by', 'decided_by'],
    facet: 'state',
  });
  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;
  return (
    <div>
      <PaneHead
        title="Every ticket"
        sub="Pending and decided. What was approved, by whom, and whether it was edited first."
        right={<FilterCount filter={filter} />}
      />
      <ListFilter filter={filter} testid="tickets"
                  placeholder="deal, category, severity or who"
                  facetLabel="every state" />
      <WaitingList
        order="oldest"
        items={filter.shown.map((t) => ({
          key: t.ticket_id,
          // The second of the two sites. See the review desk's note above.
          title: `${t.agreement_id || 'no deal'} · ${t.category_key} · ${t.severity}`,
          sub: t.state === 'pending'
            ? `${t.reason_code} · opened by ${t.opened_by}`
            : `${t.decided_by} · ${t.minted_clause_id
                ? `minted ${t.minted_clause_id}@v${t.minted_version}` : 'nothing minted'}`,
          at: t.created_at,
          chips: (
            <>
              <ProvenanceBadge badge={t.provenance_badge} />
              {/* #153, the same label as the desk's queue. A DECIDED ticket
                  already says what it became through minted_rule_id and
                  placed_ladder_id in its sub-line; a PENDING one said nothing
                  at all, and this register lists both. */}
              <ProposalKindChip ticket={t} />
              <OverdueChip ticket={t} />
              {t.state === 'pending'
                ? <span className="chip chip-pending">pending</span>
                : t.state === 'verified'
                  ? <span className="chip chip-ok">verified</span>
                  : <span className="chip chip-err">{t.state}</span>}
              {/* DERIVED, never chosen. The badge exists because the fact does,
                  not because anybody ticked a box. */}
              {t.edited_before_approval === true &&
                <span className="chip chip-std" title="the approved wording differed from what was proposed">
                  edited first
                </span>}
              {t.edited_before_approval === false &&
                <span className="chip chip-std" title="approved exactly as proposed">
                  unedited
                </span>}
            </>
          ),
        }))}
        empty={<Empty kicker="tickets" line="No tickets have been opened." />}
      />
    </div>
  );
}

// ── Holds ────────────────────────────────────────────────────────────────
// Opening a hold is a reviewer's act; RELEASING one is legal admin's, and this
// pane does not offer it. A greyed-out release button would be the read-only
// editor the guides refuse elsewhere — a reviewer simply does not have that act,
// and the screen should say so in words rather than in a disabled control.
function HoldsPane({ me }) {
  // One act at a time. useActs guards with a ref, so a second click in the
  // same tick never reaches the network — `disabled` alone cannot, because it
  // only takes effect after a render.
  const acts = useActs();

  const pane = usePane(() => API.holds());
  const deals = usePane(() => API.deals());
  const [error, setError] = useState(null);
  const [agreement, setAgreement] = useState('');
  const [matter, setMatter] = useState('');

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

  return (
    <div>
      {error && (
        <div className="panel p-3 mb-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)' }}>{error}</div>
        </div>
      )}

      <PaneHead
        title="Legal holds"
        sub="While a hold is open, nothing about that agreement is destroyed, however far past its retention date."
      />
      <WaitingList
        order="oldest"
        items={pane.rows.map((h) => ({
          key: h.hold_id,
          title: `${h.agreement_id} · ${h.matter_ref}`,
          sub: `opened by ${h.opened_by}${h.released_on ? ` · released by ${h.released_by}` : ''}`,
          at: h.opened_on,
          chips: h.released_on
            ? <span className="chip chip-std">released</span>
            : <span className="chip chip-high">open</span>,
        }))}
        empty={<Empty kicker="holds" line="No holds are open."
                      sub="A hold stops the retention clock for a dispute." />}
      />

      <div className="panel p-4 mt-6">
        <PanelHead title="Open a hold" sub="Name the matter. It is what the retention monitor will show." />
        <div className="flex gap-2 items-end">
          <div style={{ width: 220 }}>
            <label className="section-label">Agreement</label>
            <select aria-label="Agreement" className="mt-1.5 w-full font-mono" value={agreement}
                    onChange={(e) => setAgreement(e.target.value)}>
              <option value="">choose a deal</option>
              {(deals.rows ?? []).map((d) => (
                <option key={d.agreement_id} value={d.agreement_id}>{d.agreement_id}</option>
              ))}
            </select>
          </div>
          <div style={{ width: 240 }}>
            <label className="section-label">Matter</label>
            <input aria-label="Matter" className="mt-1.5 w-full" placeholder="LIT-2026-014"
                   value={matter} onChange={(e) => setMatter(e.target.value)} />
          </div>
          {/* cw.legal_hold keys on a surrogate hold_id, so a second click does
              not collide — it opens a SECOND hold on the same matter. */}
          <button className="btn btn-primary"
                  disabled={!agreement || !matter.trim() || acts.busy !== null}
                  onClick={() => acts.run('open-hold', async () => {
                    setError(null);
                    const r = await API.openHold({
                      agreement_id: agreement, matter_ref: matter.trim(),
                    });
                    if (!r.ok) { setError(r.reason); return; }
                    setMatter(''); pane.reload();
                  })}>✓ open hold</button>
        </div>
      </div>

      <div className="caption mt-4">
        Releasing a hold is a Legal admin's act, not a reviewer's — so there is
        no release button here rather than a greyed-out one. A disabled control
        would say "you could, but not now"; the truth is that this is somebody
        else's decision.
      </div>
    </div>
  );
}
