// The sourcing screens — SRC-1 through SRC-4, given somewhere to be seen.
//
// WHAT WAS MISSING. Four packages landed between 2026-08-04 and 2026-08-22:
// the approved section library (`0094`), the forge that assembles a document
// from it (`0096`), the record of whether an engagement goes to market at all
// (`0097`), and the terms preview (`0099`) — the piece Mike named the
// differentiator. Every one of them was built, tested and reachable through
// the API. Not one had a screen.
//
// ── ONE PANE, FIVE ROLES, and the scoping is the database's ────────────────
//
// The deal room's rule and the obligations book's: a requester is answered the
// documents they built and the ones on deals they own, Legal and Audit every
// document, from the SAME render. Nothing here filters by role and nothing
// branches on one. What differs is the AFFORDANCES — the forge is offered to
// whoever can reach it, and a refusal renders as the database's own sentence.
//
// ── THE THREE THINGS THIS PANE HAS TO GET RIGHT ───────────────────────────
//
//   1. A GAP IS DRAWN AS A GAP. `cw.sourcing_dossier` marks a previewed term
//      the assembly could not paper with `provenance = 'unpapered'` and no
//      body. Drawing it as an ordinary clause, or dropping it, would tell a
//      supplier the paper is finished when it is not — which is the product's
//      one job, inverted.
//
//   2. "NOT ASKED" IS NOT "DECIDED AGAINST". An engagement with no row in
//      `cw.current_sourcing_intent` has not been asked the question. That is a
//      third answer, and merging it into either of the other two is S312's
//      defect and S330's, both of which shipped once already.
//
//   3. EVERY FIGURE LEADS TO WHAT IT COUNTED. S333: 63 summary figures in this
//      application and 3 of them were controls. `StatBox` takes a `to`, and a
//      figure with nowhere to go stays completely inert rather than pretending.

const { useMemo, useRef, useState } = React;

// ── The two addresses that are not a record ───────────────────────────────
//
// A sourcing run id is 32 hex characters, so neither of these can ever be one.
// Held in the address for the reason every other record here is (S321): a
// colleague can be sent to the form, Back closes it instead of leaving the
// application, and a reload does not lose your place.
const FORGE_ADDRESS = 'new';
const DECIDE_PREFIX = 'decide:';

// What each event type is called on screen. CONTENT and placeholder, but
// declared once here rather than at four call sites, because a vocabulary
// written out by hand drifts (S325).
const EVENT_LABEL = {
  rfp: 'request for proposal',
  rfq: 'request for quotation',
  none: 'no sourcing event',
};

// The parts a built document is made of. A NOUN PHRASE EACH, NOT A SENTENCE:
// these label a column and a section head, and they must read the same way on
// both event types — "how responses are evaluated" sits oddly over a
// quotation, where nothing is being proposed.
const PART_LABEL = {
  instructions: 'Instructions to bidders',
  submission: 'Submission rules',
  evaluation: 'Evaluation',
  pricing: 'Pricing',
  questions: 'Questions for suppliers',
  deliverables: 'Deliverables',
  terms_preview: 'Terms preview',
};

function partLabel(part) {
  return PART_LABEL[part] || part;
}

// Where a span came from, in the words a reader needs. `provenance` carries
// four values across the dossier's three arms, and the difference between them
// is the entire point of the record.
const PROVENANCE = {
  approved: { label: 'approved wording', state: 'effective' },
  ai_drafted: { label: 'AI-drafted', state: 'pending' },
  human_authored: { label: 'written by hand', state: 'neutral' },
  unpapered: { label: 'no approved language', state: 'never' },
};

// The two roles named by the live sourcing-section write policies. Legal kept
// the act while Procurement was deferred; 0119 added the specialist curator
// without taking Legal's existing doorway away.
const SOURCING_CURATORS = ['legal_admin', 'procurement'];

function shortRef(id) {
  return id ? String(id).slice(0, 8) : '';
}

// ── The pane ────────────────────────────────────────────────────────────────

function SourcingPane({ me }) {
  // THE RECORD YOU HAVE OPEN LIVES IN THE ADDRESS (S321). A built document is
  // a thing somebody links a colleague to, Back should close it rather than
  // leave the application, and a reload should not lose your place.
  const [openId, openDoc] = useAddressedRecord('sourcing');

  const runs = usePane(() => API.sourcingRuns());
  const dossier = usePane(() => API.sourcingDossier());
  const intents = usePane(() => API.sourcingIntents());
  // THE DEALS ARE READ HERE AND NOT INSIDE THE FORGE, because three surfaces
  // want the same answer — the form's engagement list, the decision screen's
  // one line about the engagement, and the panel of engagements nobody has
  // asked about — and three reads of one endpoint is how they come to
  // disagree with each other about what is live.
  //
  // A ROLE REFUSED THIS READ IS NOT A BROKEN PANE. `cw.agreement`'s policy
  // answers each caller their own deals; a role it answers with nothing, or
  // refuses outright, still sees every built document. What that role does NOT
  // see is the unasked panel, because "no engagement is unasked" and "I cannot
  // read the engagements" are different sentences and only one of them is true.
  const deals = usePane(() => API.deals());

  // The receipt of a build that has just happened, held here rather than in
  // the form so that pressing "build another" cannot leave a stale one behind.
  const [built, setBuilt] = useState(null);
  // Which engagement the person arrived at the form FROM. It fills the empty
  // form and never overwrites retained work — the form says which of the two
  // won rather than silently choosing.
  const [startOn, setStartOn] = useState(null);

  if (runs.status === 'loading' || dossier.status === 'loading'
      || intents.status === 'loading' || deals.status === 'loading') return <Loading />;
  if (runs.status === 'failed') return <LoadFailed reason={runs.reason} />;

  // ── The form ────────────────────────────────────────────────────────────
  if (openId === FORGE_ADDRESS) {
    return (
      <SourcingForge
        me={me}
        deals={deals.rows}
        intents={intents.rows}
        startOn={startOn}
        built={built}
        onBuilt={(receipt) => {
          setBuilt(receipt);
          // The desk behind this form is now out of date in three places.
          runs.reload(); dossier.reload(); intents.reload();
        }}
        onOpenBuilt={(id) => { setBuilt(null); openDoc(id); }}
        onAnother={() => setBuilt(null)}
        onClose={() => { setBuilt(null); setStartOn(null); openDoc(null); }} />
    );
  }

  // ── The decision ────────────────────────────────────────────────────────
  if (openId && openId.startsWith(DECIDE_PREFIX)) {
    const agreementId = openId.slice(DECIDE_PREFIX.length);
    return (
      <DecideIntent
        me={me}
        agreementId={agreementId}
        deals={deals.rows}
        intents={intents.rows}
        onDone={(choice) => {
          intents.reload();
          setStartOn(agreementId);
          // A DECISION TO COMPETE LEADS SOMEWHERE; A DECISION NOT TO DOES NOT.
          // Sending somebody who has just recorded a direct award to a form
          // that builds a request for proposal would be the screen arguing
          // with the decision they just made.
          openDoc(choice === 'none' ? null : FORGE_ADDRESS);
        }}
        onClose={() => openDoc(null)} />
    );
  }

  if (openId) {
    const doc = runs.rows.find((r) => String(r.sourcing_run_id) === String(openId));
    return (
      <BuiltDocument
        doc={doc}
        openId={openId}
        spans={dossier.rows.filter(
          (d) => String(d.sourcing_run_id) === String(openId))}
        dossierFailed={dossier.status === 'failed' ? dossier.reason : null}
        onClose={() => openDoc(null)}
      />
    );
  }

  // `onBuildFor` IS THE SAME FORGE, OPENED ON A NAMED ENGAGEMENT. `startOn`
  // fills the form only while the field is empty and never overwrites retained
  // work — the forge says which of the two won rather than choosing silently,
  // which is the rule it was built with in S356.
  //
  // AND THIS COMMENT SITS ABOVE THE RETURN, not between the attributes. Babel
  // accepts a `//` line inside an attribute list; the convention in this
  // repository is that reasoning goes above the element, and a comment only
  // some parsers accept is a trap for whoever changes the toolchain.
  return (
    <SourcingDesk
      me={me} runs={runs} intents={intents} deals={deals}
      onOpen={openDoc}
      onBuild={() => { setStartOn(null); openDoc(FORGE_ADDRESS); }}
      onBuildFor={(agreementId) => { setStartOn(agreementId); openDoc(FORGE_ADDRESS); }}
      onDecide={(agreementId) => openDoc(DECIDE_PREFIX + agreementId)} />
  );
}

// ── The desk ────────────────────────────────────────────────────────────────

function SourcingDesk({ me, runs, intents, deals, onOpen, onBuild, onBuildFor,
                       onDecide }) {
  const built = useRef(null);
  const register = useRef(null);
  const unasked = useRef(null);
  const market = useRef(null);

  // Faceted on the LABEL rather than the key, for the reason the section
  // library is: `rfp` in a dropdown is the database's word, not a buyer's.
  const rows = useMemo(
    () => runs.rows.map((r) => ({
      ...r, event_label: EVENT_LABEL[r.event_type] || r.event_type })),
    [runs.rows]);
  const filter = useListFilter(rows, {
    view: 'sourcing:runs',
    fields: ['sourcing_run_id', 'agreement_id', 'need', 'counterparty_hint',
             'built_by'],
    facet: 'event_label',
  });

  // WHAT THE FIGURES COUNT, and each is measured off rows this role was
  // actually answered — never off a paragraph.
  const withTerms = runs.rows.filter((r) => r.preview_run_id).length;
  const goingToMarket = intents.status === 'loaded'
    ? intents.rows.filter((i) => i.produces_document).length : null;
  const directAwards = intents.status === 'loaded'
    ? intents.rows.filter((i) => !i.produces_document).length : null;

  // THE THIRD ANSWER, COUNTED. Measured only when BOTH reads answered: an
  // unasked engagement is a live deal with no intent row, so a refused deals
  // read and a refused intents read each make the figure a guess rather than a
  // measurement. `n` of null draws an em-dash and stays inert (S333) — which is
  // the difference between "none" and "I could not tell".
  const canMeasureUnasked = deals.status === 'loaded' && intents.status === 'loaded';
  const notYetAsked = canMeasureUnasked
    ? unaskedDeals(deals.rows, intents.rows).length : null;

  return (
    <div>
      <PaneHead
        title="Sourcing"
        sub="What we asked the market for, what every document was made of, and which engagements are not going to market at all."
        right={
          <div className="flex items-center gap-3">
            <span className="caption">{runs.rows.length} built</span>
            {/* THE ACT THIS PANE EXISTED WITHOUT — offered to the three roles
                `0096`'s builders_write policy names, and to nobody else.
                `maySourceOn` is that list, stated once beside the migration it
                comes from. An auditor reading this desk is not shown a button
                whose only possible outcome is a refusal; the refusal still
                renders for anybody who reaches the address. */}
            {maySourceOn(me) && (
              <button type="button" className="btn btn-primary"
                      data-testid="open-sourcing-forge"
                      onClick={onBuild}>build a sourcing document</button>
            )}
          </div>}
      />

      <div className="tile-strip mt-4">
        <StatBox
          label="documents built" n={runs.rows.length}
          to={showSection(built)} describe="show the documents that have been built" />
        {/* A MEASURED ZERO STILL DRILLS — nought documents carrying the terms
            is a fact, and the list that says so is a real destination. What
            must never drill is an unmeasured figure, which is why the two
            below go inert when the intents read was refused. */}
        <StatBox
          label="carrying the draft terms" n={withTerms}
          to={showSection(built)}
          describe={`show the ${withTerms} carrying the draft terms`} />
        {/* THE LAST INERT FIGURE ON THIS DESK, and the last of the five it
            was inert on. It counts every engagement decided to compete, so
            it leads to a panel listing every one of them — with the ones
            nothing was built for marked, and a focus to narrow to those.
            A figure leading to a SHORTER list than it counted would be the
            same defect it was repaired for, wearing the fix's clothes. */}
        <StatBox
          label="going to market" n={goingToMarket}
          to={goingToMarket === null ? null : showSection(market)}
          describe={`show the ${goingToMarket} going to market`} />
        <StatBox
          label="direct awards" n={directAwards}
          to={directAwards ? showSection(register) : null}
          describe={`show the ${directAwards} direct awards`} />
        {/* A MEASURED ZERO STILL DRILLS — "every live engagement has been
            asked" is a real answer and the panel that says so is a real
            destination. What must never drill is the unmeasured figure. */}
        <StatBox
          label="not yet asked" n={notYetAsked}
          to={canMeasureUnasked ? showSection(unasked) : null}
          describe={`show the ${notYetAsked} engagements nobody has asked about`} />
      </div>

      <div className="mt-6" ref={built}>
        <PanelHead
          title="Documents built"
          sub="Each one says where every character in it came from."
          right={<FilterCount filter={filter} />} />
        <ListFilter
          filter={filter} testid="sourcing-built"
          placeholder="search by reference, deal, or what is being bought"
          facetLabel="any event type" />
        {runs.rows.length === 0 ? (
          <Empty
            kicker="nothing built"
            line="No sourcing document has been assembled yet."
            sub="A request for proposal or quotation is assembled from the approved
                 section library, and the record of what it was made of appears
                 here the moment one is." />
        ) : filter.shown.length === 0 ? (
          <NoMatch kicker="no match" noun="documents" />
        ) : (
          <div className="panel">
          {/* IN A `.panel`, and that is not decoration. `registry.css`
              carries `.panel:has(table.ledger){overflow-x:auto}` — the
              repair for S324, where a six-column ledger pushed the whole
              PAGE sideways at a narrow width and dragged the masthead and
              the rack with it. A ledger written outside a panel silently
              opts out of that, which is what these three did until the
              width sweep measured the workspace rather than the table. */}
            <table className="ledger w-full">
              <thead>
                <tr>
                  <th>reference</th><th>engagement</th><th>type</th>
                  <th>what is being bought</th><th>terms</th><th>built</th>
                </tr>
              </thead>
              <tbody>
                {filter.shown.map((r) => (
                  <tr key={r.sourcing_run_id}
                      {...openableRow(() => onOpen(r.sourcing_run_id),
                                      `open the document ${shortRef(r.sourcing_run_id)}`)}>
                    <td className="font-mono">{shortRef(r.sourcing_run_id)}</td>
                    <td className="font-mono">{r.agreement_id || '—'}</td>
                    <td>{r.event_label}</td>
                    <td className="truncate" style={{ maxWidth: 320 }}
                        title={r.need}>{r.need}</td>
                    <td>
                      {/* THE DIFFERENTIATOR, ON THE LIST. Whether the draft
                          contract terms went out with the document is the
                          single most interesting fact about it, and a reader
                          should not have to open the record to learn it. */}
                      {r.preview_run_id
                        ? <Status state="effective" title={'from assembly ' + r.preview_run_id}>attached</Status>
                        : <Status state="neutral">not attached</Status>}
                    </td>
                    <td className="caption" title={r.built_at}>{since(r.built_at)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>

      {/* ── THE THIRD ANSWER, GIVEN A PANEL ─────────────────────────────
          Drawn only when the deals read answered. A role that cannot read the
          engagements would otherwise be shown "every live engagement has been
          asked", which is a claim this screen would have no standing to make
          — S326's rule: never describe a filtered sample as the population. */}
      {deals.status === 'loaded' && (
        <div className="mt-8" ref={unasked}>
          <UnaskedEngagements
            me={me} deals={deals.rows} intents={intents.rows}
            onDecide={onDecide} />
        </div>
      )}

      {/* ── DECIDED TO COMPETE, AND WHAT CAME OF IT ────────────────────
          Drawn only when BOTH reads answered. The panel's whole content is
          a comparison of two populations — decisions and documents — so a
          role answered one and refused the other would be shown a gap that
          is really a missing half. "I could not read the documents" and
          "no document exists" are different sentences and only one of them
          would be true. */}
      {intents.status === 'loaded' && runs.status === 'loaded' && (
        <div className="mt-8" ref={market}>
          <GoingToMarket
            me={me} intents={intents.rows} runs={runs.rows}
            deals={deals.status === 'loaded' ? deals.rows : []}
            onOpen={onOpen} onBuildFor={onBuildFor} />
        </div>
      )}

      <div className="mt-8" ref={register}>
        <UncompetedRegister />
      </div>

      <div className="mt-8">
        <SectionLibrary me={me} />
      </div>
    </div>
  );
}

// ── Saving the file, in the screen that downloads it ──────────────────────
//
// THE THIRD COPY IN THIS APPLICATION, and it is deliberate. Saving a file is
// one screen's job and never the transport's — `db/test/shell.test.mjs`
// asserts `api.jsx` builds no anchor, and pins WHICH screen calls each
// download method. `requester.jsx` has one for the contract and
// `negotiate.jsx` one for the supplier paper.
//
// MOVING ALL THREE INTO `common.jsx` WOULD BREAK A REAL GUARD, which is why
// the extraction rule does not apply here. The reading room is fenced by a
// test reading ITS OWN SOURCE for `createElement('a')`, `createObjectURL`
// and `.download` — ADR-0008 gives a viewer no export path of any kind. A
// shared `saveBlob()` would let that pane save a file while the test that
// exists to stop it went on passing. Three copies of six lines is the price
// of a fence that reads the pane rather than trusting it, and the count is
// written down here rather than left to be rediscovered.
function saveSourcingFile(blob, filename) {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = filename;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  URL.revokeObjectURL(url);
}

// ── One built document ──────────────────────────────────────────────────────

function BuiltDocument({ doc, openId, spans, dossierFailed, onClose }) {
  // ABOVE THE EARLY RETURN BELOW, because a hook after one blanks the pane on
  // the render AFTER the data lands — which is the render nobody watches
  // (S318). `hook-order.test.mjs` covers this file.
  const [downloadRefused, setDownloadRefused] = useState(null);

  // A RECORD VIEW OPENS ON ITS OWN PAGE TITLE (the heading rule, 2026-08-22).
  if (!doc) {
    return (
      <div>
        <PaneHead title="Sourcing document" kicker="sourcing"
                  sub="Nothing here answers to that reference." />
        <Empty
          kicker="not found"
          line={`No sourcing document of yours has the reference ${shortRef(openId)}.`}
          sub="It may belong to somebody else's engagement, or the address may be
               stale. Nothing was hidden from this screen — the record simply did
               not answer with it."
          action={<button type="button" onClick={onClose}>back to sourcing</button>} />
      </div>
    );
  }

  // WHICH ARM, ASKED OF THE RECORD (0100). `part` cannot answer this: the
  // sourcing library holds a skeleton PREAMBLE whose part is also
  // `terms_preview` — the paragraph that introduces the clauses below it — so
  // filtering on `part` counted the preamble as one of the terms the supplier
  // will be asked to sign. The alternative was to teach this screen the view's
  // `seq + 2000` offset, which would make an ordering detail load-bearing.
  const preview = spans.filter((s) => s.source === 'preview');
  const gaps = preview.filter((s) => s.provenance === 'unpapered').length;
  const body = spans.filter((s) => s.source !== 'preview');

  return (
    <div>
      {/* A SHORT TITLE AND THE SENTENCE UNDERNEATH IT. The first draft made the
          whole `need` the h1, which produced a hundred-character heading and
          read nothing like the rest of the application — every other pane's
          title is a short noun phrase. `sub` is exactly the place for the
          sentence: it is the one line under the rule that says what this is. */}
      <PaneHead
        kicker={(EVENT_LABEL[doc.event_type] || doc.event_type)
                + ' · ' + shortRef(doc.sourcing_run_id)}
        title={doc.agreement_id || 'Not yet tied to an engagement'}
        sub={doc.need}
        right={
          <div className="flex items-center gap-2">
            {/* THE DOCUMENT ITSELF, which until now could not leave the
                building. The service REBUILDS it from the record and refuses
                to serve anything that does not fingerprint to what is stored,
                so a file that arrives is provably the document these figures
                describe. */}
            <ActButton className="btn btn-primary" data-testid="download-sourcing"
                       onClick={async () => {
                         setDownloadRefused(null);
                         const r = await API.sourcingDocument(doc.sourcing_run_id);
                         // THE REPLY IS CHECKED BEFORE ANYTHING IS SAVED. A
                         // refusal saved to somebody's desktop is a broken
                         // document with our name on it; shell.test.mjs asserts
                         // this order on the contract download for that reason.
                         if (!r.ok) { setDownloadRefused(r.reason); return; }
                         saveSourcingFile(r.blob, r.filename);
                       }}>
              download the document
            </ActButton>
            <button type="button" className="btn" onClick={onClose}>back to sourcing</button>
          </div>} />
      <div className="caption mt-2">
        built by <span className="font-mono">{doc.built_by}</span>
        {doc.counterparty_hint && <> · {doc.counterparty_hint}</>}
      </div>

      {/* A REFUSAL IS A SENTENCE AND NEVER A FILE. The interesting one is the
          fingerprint mismatch: it names both hashes, because "this document no
          longer rebuilds to its record" is something somebody has to go and
          investigate, and a screen that said only "could not download" would
          have thrown away the only part they can act on. */}
      {downloadRefused && (
        <div className="panel p-3 mt-4" style={{ borderColor: 'var(--danger)' }}
             data-testid="download-refused" role="alert">
          <div className="tag" style={{ color: 'var(--danger)' }}>nothing was served</div>
          <div className="text-[12.5px] mt-1.5"
               style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            {downloadRefused}
          </div>
        </div>
      )}

      {/* ── WHERE EVERY CHARACTER CAME FROM ────────────────────────────────
          The three figures, on the screen as well as in the record. A contract
          asserts zero AUTHORED characters; a sourcing document asserts zero
          UNATTRIBUTED ones, and that difference is the whole of Mike's
          decision 1. A reader who never opens the dossier still sees it. */}
      <div className="tile-strip mt-4">
        <StatBox label="from approved wording" n={doc.approved_chars} />
        <StatBox label="written for this engagement" n={doc.engagement_chars} />
        <StatBox label="unattributed" n={doc.unaccounted_chars} />
        <StatBox label="terms previewed" n={preview.length} />
      </div>

      <div className="caption mt-2">
        Every character is approved wording, a declared engagement span, or
        declared structure — <span className="font-mono">{doc.engine_version}</span>,
        document <span className="font-mono">{String(doc.document_sha256).slice(0, 12)}</span>.
      </div>

      {dossierFailed && (
        <div className="mt-6"><LoadFailed reason={dossierFailed} /></div>
      )}

      <div className="mt-8">
        <TermsPreview doc={doc} preview={preview} gaps={gaps} />
      </div>

      <div className="mt-8">
        <PanelHead
          title="The rest of the document"
          sub="Every span in order, saying where it came from and who is answerable for it." />
        {body.length === 0 ? (
          <Empty kicker="nothing recorded" line="This document has no other spans on the record." />
        ) : (
          <div className="panel">
            {body.map((s) => <Span key={s.part + '-' + s.seq} span={s} />)}
          </div>
        )}
      </div>
    </div>
  );
}

// ── The terms preview — the differentiator ─────────────────────────────────

function TermsPreview({ doc, preview, gaps }) {
  if (!doc.preview_run_id) {
    // AN ABSENCE WITH ITS REASON, never a blank. "The build did not ask for
    // them" and "there was no assembled contract to show" are different facts
    // about this document, and a screen that rendered one blank for both would
    // be stating neither.
    return (
      <div>
        <PanelHead title="The terms the winner will be asked to sign"
                   sub="The draft contract terms did not go out with this document." />
        <Empty
          kicker="not attached"
          line="This document went out without the draft terms."
          sub="Suppliers responding to it have not seen the paper they would be
               asked to sign. Whether the terms must ride along is an
               Administrator setting; whether they did is a fact about this
               document and cannot be changed after it was built." />
      </div>
    );
  }

  return (
    <div>
      <PanelHead
        title="The terms the winner will be asked to sign"
        sub="Referenced, never copied — each clause is the exact version a contract assembly resolved for this engagement."
        right={
          <span className="caption">
            {preview.length} {preview.length === 1 ? 'term' : 'terms'}
            {/* THE GAPS, COUNTED IN THE HEADER. "12 terms" and "12 terms, four
                of which we have no approved language for" are different
                documents, and the header is where somebody reads the shape of
                it before reading the rows. */}
            {gaps > 0 && <> · <span style={{ color: 'var(--warn, var(--mute))' }}>
              {gaps} with no approved language</span></>}
          </span>} />

      {preview.length === 0 ? (
        <Empty kicker="nothing to show"
               line="The record names an assembly, and none of its terms came back."
               sub="This is a record you may not read in full rather than an empty
                    preview — the dossier is scoped to the engagement, and it
                    answered with nothing." />
      ) : (
        <div className="panel">
          {preview.map((s) => <PreviewTerm key={s.seq} term={s} />)}
        </div>
      )}
    </div>
  );
}

// One previewed term. Two shapes, and telling them apart is the point.
function PreviewTerm({ term }) {
  const gap = term.provenance === 'unpapered';
  const mark = PROVENANCE[term.provenance] || PROVENANCE.approved;
  return (
    <div className="waiting-row" style={{ alignItems: 'flex-start' }}>
      <div className="min-w-0">
        <div className="flex items-center gap-2">
          {/* NAMED WITH ITS VERSION. A supplier holding the document and a
              lawyer holding the record must be able to agree on which words
              are meant, and a title alone cannot do that. A gap has no clause
              to name, so it is named by the category it is a gap in. */}
          {term.section_id && (
            <span className="font-mono text-[12px]" style={{ color: 'var(--mute)' }}>
              {term.section_id} v{term.version}
            </span>
          )}
          <span className="text-[13px]" style={{ color: 'var(--ink)' }}>
            {term.title}
          </span>
        </div>
        {gap ? (
          <div className="font-serif italic mt-1" style={{ fontSize: 13, color: 'var(--mute)' }}>
            No approved language has been selected for this term. It will be
            settled during negotiation.
          </div>
        ) : (
          <div className="mt-1 text-[13px]" style={{ color: 'var(--mute)' }}>
            {term.body}
          </div>
        )}
      </div>
      <div className="flex items-center gap-3 shrink-0">
        <Status state={mark.state}>{mark.label}</Status>
        {/* WHO IS ANSWERABLE. For approved wording it is the person who
            approved that clause version — the whole point of the dossier. For
            a gap it is nobody, and saying so is more honest than naming
            whoever ran the assembly. */}
        <span className="caption font-mono">
          {term.answerable || 'nobody yet'}
        </span>
      </div>
    </div>
  );
}

// One span of the rest of the document.
function Span({ span }) {
  const mark = PROVENANCE[span.provenance] || PROVENANCE.approved;
  return (
    <div className="waiting-row" style={{ alignItems: 'flex-start' }}>
      <div className="min-w-0">
        <div className="flex items-center gap-2">
          <span className="caption">{partLabel(span.part)}</span>
          {span.section_id && (
            <span className="font-mono text-[12px]" style={{ color: 'var(--mute)' }}>
              {span.section_id} v{span.version}
            </span>
          )}
        </div>
        {span.title && (
          <div className="text-[13px] mt-0.5" style={{ color: 'var(--ink)' }}>
            {span.title}
          </div>
        )}
        <div className="mt-1 text-[13px]" style={{ color: 'var(--mute)' }}>
          {span.body}
        </div>
      </div>
      <div className="flex items-center gap-3 shrink-0">
        <Status state={mark.state}>{mark.label}</Status>
        {/* A MODEL-WRITTEN SPAN NAMES ITS MODEL. `cw.sourcing_run_span`
            refuses one that does not, and that refusal is the whole value of
            Mike's carve-out: the AI gets to write, and the record always says
            that it did. */}
        <span className="caption font-mono">
          {span.model || span.answerable || ''}
        </span>
      </div>
    </div>
  );
}

// ── The register of what we did not compete ────────────────────────────────

function UncompetedRegister() {
  const pane = usePane(() => API.sourcingUncompeted());
  const filter = useListFilter(pane.rows, {
    view: 'sourcing:not-competed',
    fields: ['agreement_id', 'counterparty', 'reason', 'decided_by'],
  });

  // A ROLE THAT HOLDS NO GRANT HERE SEES NOTHING AT ALL, not an empty table.
  // The register crosses every deal, so it is Legal, Audit and the
  // Administrator; a requester reads their own decision through the intents
  // endpoint and is refused this one. Drawing an empty register for them would
  // say "no engagement was handed over without competition", which is a claim
  // this screen has no standing to make.
  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return null;

  return (
    <div>
      <PanelHead
        title="Handed over without competition"
        sub="Every engagement going to a supplier without a sourcing event, and the reason somebody gave for it."
        right={<FilterCount filter={filter} />} />
      {/* NOT A QUEUE OF THINGS TO APPROVE, and the subtitle must not imply
          one. Mike's standing rule: warn stakeholders, do not gate on
          approval. Nobody signs a direct award off; this is the surface that
          makes the choice visible, which is where this system's
          responsibility ends and the buyer's begins. */}
      <ListFilter filter={filter} testid="sourcing-uncompeted"
                  placeholder="search by deal, counterparty, or reason" />
      {pane.rows.length === 0 ? (
        <Empty
          kicker="none"
          line="Every engagement that has been asked is going to market."
          sub="A renewal or a direct award is a real and useful answer — when one
               is recorded, it appears here with its reason, so the choice is
               visible without anybody having to approve it." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="engagements" />
      ) : (
        <div className="panel">
          {/* IN A `.panel`, and that is not decoration. `registry.css`
              carries `.panel:has(table.ledger){overflow-x:auto}` — the
              repair for S324, where a six-column ledger pushed the whole
              PAGE sideways at a narrow width and dragged the masthead and
              the rack with it. A ledger written outside a panel silently
              opts out of that, which is what these three did until the
              width sweep measured the workspace rather than the table. */}
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>engagement</th><th>counterparty</th><th>reason given</th>
                <th>decided by</th><th>standing</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((r) => (
                <tr key={r.agreement_id}>
                  <td className="font-mono">{r.agreement_id}</td>
                  <td>{r.counterparty}</td>
                  <td style={{ maxWidth: 380 }}>{r.reason}</td>
                  <td className="font-mono caption">{r.decided_by}</td>
                  <td className="caption">
                    {r.days_standing} {r.days_standing === 1 ? 'day' : 'days'}
                    {/* CHANGED THEIR MIND, AND IT IS WORTH SEEING. More than one
                        decision recorded against an engagement means somebody
                        revisited it, which the append-only record keeps and this
                        column surfaces without anybody opening the history. */}
                    {r.decisions_recorded > 1 && (
                      <> · {r.decisions_recorded} decisions</>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── What may be assembled today ────────────────────────────────────────────

function SectionLibrary({ me }) {
  const pane = usePane(() => API.sourcingSections());
  // THE CURATING READ, beside the assembling one (0111). cw.selectable_section
  // hides everything retired or lapsed, which is right for assembling a
  // document and wrong for deciding whether to publish a new version.
  const history = usePane(() => API.sectionHistory());
  // WHO HOLDS THE DOOR: 0094's Legal-admin policy plus 0119's Procurement
  // policy. The screen follows both live policies; it does not preserve the
  // temporary arrangement from before the seventh role existed.
  const mayCurate = SOURCING_CURATORS.includes(me && me.role);
  // THE FACET SHOWS WHAT A PERSON WOULD SAY, not the column's value. Faceting
  // on `part` put `terms_preview` in the dropdown of a screen a buyer reads —
  // Mike's standing rule is plain language, and an underscored key is the
  // database's word rather than anybody's.
  const rows = useMemo(
    () => pane.rows.map((r) => ({ ...r, part_label: partLabel(r.part) })),
    [pane.rows]);
  const filter = useListFilter(rows, {
    view: 'sourcing:sections',
    fields: ['section_id', 'title', 'body'],
    facet: 'part_label',
  });

  // ONLY ON THE FIRST LOAD, and this was a real defect found by walking the
  // screen rather than by any test. Every curating act calls `pane.reload()`,
  // which puts this pane back into `loading` — and an early return here
  // UNMOUNTS the whole subtree, including the form that had just recorded
  // `said`. So the act reached the database, the audit row landed, and the
  // person was shown nothing at all: the confirmation was destroyed by the
  // refresh it asked for.
  //
  // Keeping the rows on screen while they are refetched is also the better
  // behaviour on its own merits — a list that blanks itself on every write
  // loses the reader's place.
  // `rows.length` cannot identify the first load: adding the very first
  // section leaves the selectable view empty until wording is published, so
  // the refresh used to unmount this form and erase its success receipt. The
  // response body is null only before the first answer; use that lifecycle
  // fact instead of treating an honest empty list as "not loaded".
  if (pane.status === 'loading' && pane.body === null) return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  const skeleton = rows.filter((r) => r.authorship === 'skeleton').length;
  const engagement = rows.length - skeleton;

  return (
    <div>
      <PanelHead
        title="The approved wording"
        sub="Computed, never stored: the latest version of every section, nothing retired and nothing lapsed."
        right={<FilterCount filter={filter} />} />
      {/* THE DISTINCTION MIKE'S DECISION 1 TURNS ON, said on the screen that
          shows the library. `skeleton` assembles deterministically exactly as
          a contract clause does; `engagement` is the span the AI may author
          for one deal, and it is NEVER assembled from the library row. A
          screen that listed both without saying which was which would make
          the looser rule invisible. */}
      <div className="caption mb-3">
        {skeleton} assemble into every document of their type ·{' '}
        {engagement} are written for one engagement and never assembled
      </div>
      <ListFilter filter={filter} testid="sourcing-sections"
                  placeholder="search the approved wording"
                  facetLabel="any part" />
      {rows.length === 0 ? (
        <Empty
          kicker="empty library"
          line="No sourcing wording has been approved yet."
          sub="Until there is, nothing can be assembled — a request for proposal is
               built from approved sections the same deterministic way a contract
               is built from approved clauses. Legal and Procurement curate that wording." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="sections" />
      ) : (
        <div className="panel">
          {/* IN A `.panel`, and that is not decoration. `registry.css`
              carries `.panel:has(table.ledger){overflow-x:auto}` — the
              repair for S324, where a six-column ledger pushed the whole
              PAGE sideways at a narrow width and dragged the masthead and
              the rack with it. A ledger written outside a panel silently
              opts out of that, which is what these three did until the
              width sweep measured the workspace rather than the table. */}
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>section</th><th>part</th><th>title</th>
                <th>how it is written</th><th>appears in</th><th>approved by</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((r) => (
                <tr key={r.section_id}>
                  <td className="font-mono">{r.section_id} v{r.version}</td>
                  <td className="caption">{r.part_label}</td>
                  <td style={{ maxWidth: 260 }}>{r.title}</td>
                  <td>
                    {r.authorship === 'skeleton'
                      ? <Status state="effective">assembled</Status>
                      : <Status state="pending">written per engagement</Status>}
                  </td>
                  <td className="caption">
                    {(r.applies_to || []).map((t) => EVENT_LABEL[t] || t).join(', ')}
                  </td>
                  <td className="font-mono caption">{r.approved_by || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      <SectionHistory history={history} />
      <CurateTheLibrary
        mayCurate={mayCurate}
        sections={history.status === 'loaded' ? history.rows : []}
        onChanged={() => { pane.reload(); history.reload(); }} />
    </div>
  );
}

// ── What every version said, including the ones nobody may assemble ─────────
//
// `cw.selectable_section` is the ASSEMBLING view and hides everything retired
// or lapsed. This is the CURATING one, and until 0111 nothing served it: a
// lawyer deciding whether to publish new wording could not see what the last
// version said, or why it was withdrawn.
//
// COLLAPSED BY DEFAULT, because the common case on this screen is reading the
// library rather than its history, and a second ledger opened underneath the
// first is the width problem S324 found.
function SectionHistory({ history }) {
  const [open, setOpen] = useState(false);
  if (history.status !== 'loaded') return null;

  // A SECTION WITH NO WORDING IS NOT A SUPERSEDED VERSION. The view left joins
  // (0111) so a section awaiting its first wording appears with a null version;
  // counting those as "superseded" would state something false about wording
  // that has never existed.
  const rows = (history.rows || []).filter((r) => r.version != null);
  const unwritten = (history.rows || []).filter((r) => r.version == null).length;
  const superseded = rows.filter((r) => !r.is_current && !r.retired).length;
  const retired = rows.filter((r) => r.retired).length;
  // NO NUMBER WHERE NOTHING WAS MEASURED — the navigation rack's rule, kept at
  // a third site. A library with one version of everything has no history, and
  // a nought here would state a fact about withdrawal that nobody measured.
  if (!superseded && !retired && !unwritten) return null;

  return (
    <div className="mt-4">
      <ActButton className="btn" data-testid="section-history-toggle"
                 onClick={() => setOpen(!open)}>
        {open ? 'hide' : 'show'} what earlier versions said
        {' · '}{superseded} superseded, {retired} withdrawn
        {unwritten ? `, ${unwritten} awaiting their first wording` : ''}
      </ActButton>
      {open && (
        <div className="panel mt-3">
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>section</th><th>state</th><th>approved by</th>
                <th>approved</th><th>why it went</th>
              </tr>
            </thead>
            <tbody>
              {rows.filter((r) => !r.is_current).map((r) => (
                <tr key={`${r.section_id}-${r.version}`}>
                  <td className="font-mono">{r.section_id} v{r.version}</td>
                  <td>
                    {r.retired
                      ? <Status state="retired">withdrawn</Status>
                      : <Status state="pending">superseded</Status>}
                  </td>
                  <td className="font-mono caption">{r.approved_by || '—'}</td>
                  <td className="caption">{r.approved_on || '—'}</td>
                  <td className="caption" style={{ maxWidth: 320 }}>
                    {r.retired_reason || '—'}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── The three acts 0094 permitted and nothing offered ───────────────────────
//
// 0094 granted the Legal admin insert and update on this library and NO
// ENDPOINT EVER WROTE TO IT (issue #155, found by the write census). Every
// section in a demo came from `seed_sourcing.py`, while ADR-0014 says the
// Legal admin curates it until the `procurement` role exists.
//
// WHAT IS NOT OFFERED HERE, deliberately: **nothing renames a section.**
// `cw.sourcing_section.title` sits on the SECTION rather than on the version,
// so retitling changes what a REBUILD of a past sourcing document produces and
// that document stops matching its own fingerprint — `sourcing_document.py`'s
// header records the hazard. Publishing a new VERSION is the act that exists,
// and it touches no title.
//
// AND NOTHING NAMES AN APPROVER. It is bound from the connection at the
// database, so the field is not on this form to be filled in wrongly.
function CurateTheLibrary({ mayCurate, sections, onChanged }) {
  const [tab, setTab] = useState(null);
  const [busy, setBusy] = useState(false);
  const [said, setSaid] = useState(null);

  // A CONTROL IS NOT DRAWN FOR A ROLE THE DATABASE WOULD REFUSE. Everybody
  // else reads the library, which is what 0094 grants them, and is told whose
  // act this is rather than shown a button that answers "permission denied".
  if (!mayCurate) {
    return (
      <div className="caption mt-4" data-testid="section-library-read-only">
        The approved wording is curated by Legal and Procurement.
      </div>
    );
  }

  const run = async (what, body) => {
    setBusy(true); setSaid(null);
    try {
      await what(body);
      setSaid({ ok: true });
      onChanged();
    } catch (e) {
      // THE DATABASE'S OWN SENTENCE, not one composed here. Every refusal on
      // this surface is 0094's policy or 0111's triggers, and re-wording one
      // would teach a rule the system does not have.
      setSaid({ ok: false, reason: (e && e.reason) || String(e) });
    } finally { setBusy(false); }
  };

  return (
    <div className="mt-6" data-testid="curate-sections">
      <div className="flex gap-2">
        {[['add', 'add a section'], ['publish', 'publish wording'],
          ['retire', 'withdraw wording']].map(([k, label]) => (
          <ActButton key={k} className={tab === k ? 'btn btn-primary' : 'btn'}
                     data-testid={`curate-${k}`}
                     onClick={() => { setTab(tab === k ? null : k); setSaid(null); }}>
            {label}
          </ActButton>
        ))}
      </div>

      {tab === 'add' && <AddSection busy={busy} onRun={run} />}
      {tab === 'publish' && <PublishWording busy={busy} sections={sections} onRun={run} />}
      {tab === 'retire' && <RetireWording busy={busy} sections={sections} onRun={run} />}

      {said && (
        <div className="mt-3" data-testid="curate-said">
          {said.ok
            ? <Status state="effective">recorded</Status>
            : <><Status state="blocked">refused</Status>{' '}
                <span className="caption">{said.reason}</span></>}
        </div>
      )}
    </div>
  );
}

// The vocabularies are the TABLE's own CHECK constraints (0094), listed here so
// the form cannot offer a value the database will refuse. A free-text box would
// turn a data-entry slip into a refusal somebody has to decode.
const SECTION_PARTS = [
  ['instructions', 'How to respond'],
  ['submission', 'The rules a response must satisfy'],
  ['evaluation', 'How responses are scored'],
  ['terms_preview', 'The terms the winner will sign'],
  ['pricing', 'The pricing table'],
  ['questions', 'Supplier questions for one engagement'],
  ['deliverables', 'Deliverables for one engagement'],
];

function AddSection({ busy, onRun }) {
  const [f, setF] = useState({
    section_id: '', part: 'instructions', title: '',
    authorship: 'skeleton', rfp: true, rfq: false,
  });
  const set = (k, v) => setF({ ...f, [k]: v });
  const applies = [f.rfp && 'rfp', f.rfq && 'rfq'].filter(Boolean);

  return (
    <div className="panel mt-3">
      <div className="caption mb-3">
        A section with no wording yet is a real state — it is a gap somebody can
        see, rather than a row invented with placeholder text in it. Publish the
        wording separately.
      </div>
      <div className="grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))' }}>
        <label className="block">
          <span className="tag">section id</span>
          <input className="mt-1 w-full" data-testid="new-section-id"
                 value={f.section_id} onChange={(e) => set('section_id', e.target.value)} />
        </label>
        <label className="block">
          <span className="tag">what part of the document</span>
          <select className="mt-1 w-full" data-testid="new-section-part"
                  value={f.part} onChange={(e) => set('part', e.target.value)}>
            {SECTION_PARTS.map(([k, label]) => <option key={k} value={k}>{label}</option>)}
          </select>
        </label>
        <label className="block">
          <span className="tag">title</span>
          <input className="mt-1 w-full" data-testid="new-section-title"
                 value={f.title} onChange={(e) => set('title', e.target.value)} />
        </label>
        <label className="block">
          {/* MIKE'S DECISION 1, ON THE FORM THAT CREATES THE ROW. `skeleton`
              assembles deterministically exactly as a contract clause does;
              `engagement` is the span a model may author for one deal. It is a
              property of the section KIND, which is why it is chosen here and
              never per document. */}
          <span className="tag">how it is written</span>
          <select className="mt-1 w-full" data-testid="new-section-authorship"
                  value={f.authorship} onChange={(e) => set('authorship', e.target.value)}>
            <option value="skeleton">assembled from approved wording</option>
            <option value="engagement">written for one engagement</option>
          </select>
        </label>
      </div>
      <div className="mt-3">
        <span className="tag">appears in</span>{' '}
        <label className="caption mr-3">
          <input type="checkbox" checked={f.rfp} data-testid="new-section-rfp"
                 onChange={(e) => set('rfp', e.target.checked)} /> request for proposal
        </label>
        <label className="caption">
          <input type="checkbox" checked={f.rfq} data-testid="new-section-rfq"
                 onChange={(e) => set('rfq', e.target.checked)} /> request for quotation
        </label>
      </div>
      <ActButton className="btn btn-primary mt-3" data-testid="add-section-go"
                 disabled={busy || !f.section_id.trim() || !f.title.trim() || !applies.length}
                 onClick={() => onRun(API.addSection, {
                   section_id: f.section_id.trim(), part: f.part,
                   title: f.title.trim(), authorship: f.authorship,
                   applies_to: applies,
                 })}>
        {busy ? 'recording…' : 'add the section'}
      </ActButton>
    </div>
  );
}

function PublishWording({ busy, sections, onRun }) {
  // EVERY SECTION, INCLUDING ONE WITH NO WORDING YET — which is the one this
  // form most needs to offer, and the one an inner join hid. Walking the screen
  // found it: a section added a moment earlier could not be given its first
  // wording, because the list came from a view that only had rows for sections
  // that already had a version. 0111's view left joins for this reason.
  const ids = useMemo(
    () => [...new Set((sections || []).map((r) => r.section_id))].sort(), [sections]);
  const unwritten = useMemo(
    () => new Set((sections || []).filter((r) => r.version == null)
                                  .map((r) => r.section_id)), [sections]);
  const [f, setF] = useState({ section_id: '', body: '', rationale: '', expires_on: '' });
  const set = (k, v) => setF({ ...f, [k]: v });

  return (
    <div className="panel mt-3">
      <div className="caption mb-3">
        Your name goes on this wording — it is written from your session, not
        from this form, and it cannot be somebody else’s. The version number is
        the database’s. Published wording is never edited afterwards: publish
        another version, or withdraw this one.
      </div>
      <label className="block">
        <span className="tag">section</span>
        <select className="mt-1 w-full" data-testid="publish-section-id"
                value={f.section_id} onChange={(e) => set('section_id', e.target.value)}>
          <option value="">choose a section</option>
          {ids.map((id) => (
            <option key={id} value={id}>
              {id}{unwritten.has(id) ? ' — no wording yet' : ''}
            </option>
          ))}
        </select>
      </label>
      <label className="block mt-3">
        <span className="tag">the wording</span>
        <textarea className="mt-1 w-full" rows={6} data-testid="publish-body"
                  value={f.body} onChange={(e) => set('body', e.target.value)} />
      </label>
      <div className="grid gap-3 mt-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))' }}>
        <label className="block">
          <span className="tag">why this wording (never printed in a document)</span>
          <input className="mt-1 w-full" data-testid="publish-rationale"
                 value={f.rationale} onChange={(e) => set('rationale', e.target.value)} />
        </label>
        <label className="block">
          <span className="tag">expires on, if it should</span>
          <input className="mt-1 w-full" type="date" data-testid="publish-expires"
                 value={f.expires_on} onChange={(e) => set('expires_on', e.target.value)} />
        </label>
      </div>
      <ActButton className="btn btn-primary mt-3" data-testid="publish-section-go"
                 disabled={busy || !f.section_id || !f.body.trim()}
                 onClick={() => onRun(API.publishSection, {
                   section_id: f.section_id, body: f.body,
                   rationale: f.rationale.trim() || undefined,
                   expires_on: f.expires_on || undefined,
                 })}>
        {busy ? 'publishing…' : 'publish this wording'}
      </ActButton>
    </div>
  );
}

function RetireWording({ busy, sections, onRun }) {
  // ONLY WHAT CAN ACTUALLY BE WITHDRAWN. A version already retired is refused
  // by the endpoint's own `and not retired`, and offering it would be a control
  // leading to a refusal the person could have been spared.
  const live = (sections || []).filter((r) => r.version != null && !r.retired);
  const [pick, setPick] = useState('');
  const [reason, setReason] = useState('');

  return (
    <div className="panel mt-3">
      <div className="caption mb-3">
        Withdrawing is not replacing, and it is not undone — a withdrawn version
        stays on the record, so a document built from it still says what it
        said. Publish a new version to replace wording.
      </div>
      <label className="block">
        <span className="tag">which version</span>
        <select className="mt-1 w-full" data-testid="retire-pick"
                value={pick} onChange={(e) => setPick(e.target.value)}>
          <option value="">choose a version</option>
          {live.map((r) => (
            <option key={`${r.section_id}-${r.version}`} value={`${r.section_id}|${r.version}`}>
              {r.section_id} v{r.version}{r.is_current ? ' — in use today' : ''}
            </option>
          ))}
        </select>
      </label>
      <label className="block mt-3">
        <span className="tag">why it is being withdrawn</span>
        <input className="mt-1 w-full" data-testid="retire-reason"
               value={reason} onChange={(e) => setReason(e.target.value)} />
      </label>
      <ActButton className="btn btn-primary mt-3" data-testid="retire-section-go"
                 disabled={busy || !pick || !reason.trim()}
                 onClick={() => onRun(API.retireSection, {
                   section_id: pick.split('|')[0],
                   version: Number(pick.split('|')[1]),
                   reason: reason.trim(),
                 })}>
        {busy ? 'withdrawing…' : 'withdraw this wording'}
      </ActButton>
    </div>
  );
}
