// The obligations surfaces — OB-11 (the in-app inbox) and OB-15 (the
// lifecycle surfaces): the waiting-on-you panel, the calendar, the
// per-agreement obligations panel with its envelope strip, and the inbox.
//
// ONE SOURCE, TWO RENDERINGS. The panel renders cw.waiting_on_you — the same
// derivation the notification digest reads — so the screen and the email
// cannot disagree about what is waiting. The inbox renders the outbox record:
// what was actually sent, not what should have been.
//
// ENTITLEMENTS AS PROMINENT AS DUTIES (OA §8): what the counterparty owes us
// renders with the same weight as what we owe them. NOTHING CLOSES SILENTLY:
// the closed states here are recorded acts with a name on them, and the pane
// says who.
//
// The scoping is the database's: a requester's book is their own deals, by the
// obligation table's own policy — nothing here filters or widens.

// `me` because a row that names a deal should OPEN it — and only where this
// role has somewhere to open it. The requester holds `my-deals`; the Legal
// admin, who reads the same book, does not, and a link that lands on a refusal
// is worse than no link. Asked of the role's own tab set, which is the same
// question Workspace asks before it renders anything at all.
function WaitingOnYouPanel({ me }) {
  const pane = usePane(() => API.waiting());
  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  // THE SHARED VOCABULARY, not a second copy. This file held its own, and the
  // two had already drifted: the buyer home names what kind of reference each
  // row carries and this one did not, so the same row read "· AG-26-041" there
  // and "· 9" here.
  const KINDS = WAITING_KINDS;

  // Whether a row can be opened at all depends on this role having the pane it
  // would open. `renewal_window` is the kind whose subject_ref IS a deal.
  const canOpenDeals = (WORKSPACES[me.role] || { tabs: [] })
    .tabs.some((t) => t.key === 'my-deals');

  return (
    <div>
      <PanelHead title="Waiting on you"
                 sub="The same derivation the notification digest reads — screen and email cannot disagree." />
      {pane.rows.length === 0 ? (
        <Empty kicker="waiting" line="Nothing is waiting on you."
               sub="When something is — a due obligation, a socialised override, a
                    renewal window — it appears here and in the digest, from one
                    derivation." />
      ) : (
        <div className="panel">
          {pane.rows.map((w, i) => {
            const noun = WAITING_REF_KINDS[w.kind];
            // A ROW NAMING A DEAL OPENS IT. The buyer home was given this and
            // the reason holds here word for word: a list of what is waiting
            // on you should be a list you can act FROM. A row naming an
            // ENVELOPE does not pretend to — a control that looks pressable
            // and does nothing is worse than one that plainly does not.
            const deal = w.kind === 'renewal_window' && canOpenDeals
              ? String(w.subject_ref) : null;
            const rowProps = deal
              ? openableRow(() => { window.location.hash = `#/my-deals/${encodeURIComponent(deal)}`; },
                  `open ${deal}`)
              : {};
            return (
              <div className="waiting-row" key={`${w.kind}-${w.subject_ref}-${i}`} {...rowProps}>
                <div className="min-w-0">
                  <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                    {KINDS[w.kind] ?? w.kind}
                    <span className="caption">
                      {' · '}{noun ? `${noun} ` : ''}{w.subject_ref}
                    </span>
                  </div>
                    <WaitingRecordLink row={w} me={me} />
                </div>
                {/* THIS PANE ALREADY TOLD THE TWO DATES APART and the buyer
                    home did not — one rule, two sites. It now reads through
                    the shared helper rather than its own copy, so the next
                    change reaches both. What it gains: an age instead of a
                    start date somebody has to do arithmetic on, and the word
                    "overdue" on a deadline that has passed. */}
                <span className="caption shrink-0">
                  <WaitingWhen row={w} />
                </span>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ── The calendar: obligations and entitlements over time ──────────────────
function ObligationCalendar({ rows, onOpen }) {
  // Grouped by due month; the unanchored (no due date) get their own honest
  // bucket rather than disappearing — a termination-anchored duty with no
  // termination date yet is a fact, not a blank.
  const months = new Map();
  const undated = [];
  for (const o of rows) {
    if (!o.due_on) { undated.push(o); continue; }
    const m = String(o.due_on).slice(0, 7);
    if (!months.has(m)) months.set(m, []);
    months.get(m).push(o);
  }
  const ordered = [...months.keys()].sort();

  return (
    <div className="mt-6">
      <PanelHead title="The calendar"
                 sub="Duties and entitlements over time, from the recorded book." />
      {ordered.length === 0 && undated.length === 0 ? (
        <Empty kicker="calendar" line="No obligation is on the book." />
      ) : (
        <>
          {ordered.map((m) => (
            <div className="panel p-3 mb-3" key={m} data-testid="calendar-month">
              <div className="section-label">{m}</div>
              {months.get(m).map((o) => (
                <ObligationLine o={o} onOpen={onOpen} key={o.obligation_id} />))}
            </div>
          ))}
          {undated.length > 0 && (
            <div className="panel p-3 mb-3" data-testid="calendar-unanchored">
              <div className="section-label">no date yet</div>
              <div className="caption mt-1">
                Anchored to an event that has not happened — a termination date,
                usually. Visible here rather than invented or hidden.
              </div>
              {undated.map((o) => (
                <ObligationLine o={o} onOpen={onOpen} key={o.obligation_id} />))}
            </div>
          )}
        </>
      )}
    </div>
  );
}

// WRAPPING, NOT CLIPPING, and measured rather than assumed. At 375px each
// calendar month panel reported scrollWidth 294 against clientWidth 254 with
// `overflow:hidden` above it — the state chip, the due date and the owner were
// simply off the right edge, unreachable. The defect predates this change and
// was INVISIBLE until the book had rows in it: with an empty calendar there is
// nothing to overflow. S323's rule — a fix sized in pixels is a fix for the
// widths you tried; a wrapping box holds an arbitrarily long value at every
// width.
//
// AND THE COMMENT LIVES HERE RATHER THAN INSIDE THE RETURN. Written as a
// `{/* … */}` above the root element it is a SECOND root, which is a parse
// error, which blanks every component in this file — the whole obligations
// workspace went white for exactly that reason while this was being measured.
function ObligationLine({ o, onOpen }) {
  return (
    <div className="flex gap-3 items-center mt-1.5 flex-wrap" data-testid="obligation-line"
         {...(onOpen
           ? openableRow(() => onOpen(o.obligation_id),
                         `open obligation ${o.obligation_id}`)
           : {})}>
      <span className="text-[13px] min-w-0 truncate" style={{ color: 'var(--ink)' }}>
        {o.summary}
        <span className="caption"> · {o.agreement_id} · {o.clause_id}@v{o.version}
          {o.occurrence > 0 ? ` · #${o.occurrence}` : ''}</span>
      </span>
      {/* `shrink-0` WAS THE THING THAT COULD NOT FIT. The row already wrapped;
          this group could not, so it sat at its own max-content width of 431px
          inside a 230px line and the chips ran off the right edge with nothing
          to scroll them back. Wrapping and allowed to shrink, it stacks
          instead. */}
      <span className="flex gap-2 items-center flex-wrap ml-auto">
        {/* An entitlement — the counterparty owes US — carries the same weight
            as a duty, deliberately (OA §8). */}
        {o.entitlement
          ? <span className="chip chip-std" data-testid="entitlement">owed to us</span>
          : <span className="caption">ours · {o.owner_person ?? 'UNOWNED'}</span>}
        {o.due_on && <span className="caption">{o.due_on}</span>}
        <span className={`chip ${o.state === 'overdue' ? 'chip-err'
          : o.state === 'due' ? 'chip-pending'
          : o.closed_as ? 'chip-std' : 'chip-ok'}`}>
          {o.closed_as ? `${o.closed_as} by ${o.closed_by}` : o.state}
        </span>
      </span>
    </div>
  );
}

// ── The book, as the design's master-detail ───────────────────────────────
// The 2026-08-10 obligations design: agreements listed at the left with
// their counts, the chosen agreement's duties as a straight table at the
// right, the envelope strip above the table. Same rows, same policies —
// only the arrangement changed. An entitlement still renders with the same
// weight as a duty (OA §8), and nothing closes silently.
function stateChip(o) {
  if (o.closed_as) {
    return <span className="chip chip-std">{o.closed_as} by {o.closed_by}</span>;
  }
  if (o.state === 'overdue') return <span className="chip chip-err">overdue</span>;
  if (o.state === 'due') return <span className="chip chip-pending">due</span>;
  return <span className="chip chip-ok">{o.state}</span>;
}

function AgreementObligations({ rows, envelopes, recipients, onOpen }) {
  const byDeal = new Map();
  for (const o of rows) {
    if (!byDeal.has(o.agreement_id)) byDeal.set(o.agreement_id, []);
    byDeal.get(o.agreement_id).push(o);
  }
  const agreements = [...byDeal.keys()].sort();
  const [chosen, setChosen] = React.useState(null);
  const shown = chosen && byDeal.has(chosen) ? chosen : agreements[0];
  const mine = shown ? byDeal.get(shown) : [];
  const envs = (envelopes ?? []).filter((e) => e.agreement_id === shown);

  if (agreements.length === 0) return null;

  return (
    <div className="mt-6">
      <PanelHead title="By agreement"
                 sub="Each deal's duties with the wording that created them adjacent, and its envelopes." />
      {/* The two tracks live in registry.css (.by-agreement-grid), because a
          grid that must become ONE column on a phone needs a media query, and
          an inline style cannot carry one. Measured at 375px on 2026-09-05:
          the deal's duties were a 26px sliver beside the 230px list (S481). */}
      <div className="by-agreement-grid">
        <div>
          {agreements.map((ag) => {
            const os = byDeal.get(ag);
            const overdue = os.filter((o) => !o.closed_as && o.state === 'overdue').length;
            const due = os.filter((o) => !o.closed_as && o.state === 'due').length;
            return (
              <button key={ag}
                      className="panel p-3 w-full text-left mb-2"
                      style={shown === ag
                        ? { borderColor: 'var(--ink)', boxShadow: '0 2px 0 rgba(0,0,0,.25)' }
                        : { cursor: 'pointer' }}
                      onClick={() => setChosen(ag)}>
                <div className="font-mono text-[12px]" style={{ color: 'var(--ink)' }}>{ag}</div>
                <div className="caption mt-1">
                  {os.length} on the book
                  {due > 0 && ` · ${due} due`}
                  {overdue > 0 && ` · ${overdue} overdue`}
                </div>
              </button>
            );
          })}
        </div>

        <div className="panel p-3 min-w-0" data-testid="agreement-obligations">
          <div className="flex items-baseline justify-between gap-3 flex-wrap">
            <div className="section-label">{shown}</div>
            {envs.length > 0 && (
              <div className="flex gap-2" data-testid="envelope-strip">
                {envs.map((e) => (
                  <span key={e.envelope_id}
                        className={`chip ${e.state === 'completed' ? 'chip-ok'
                          : e.state === 'sent' ? 'chip-pending' : 'chip-err'}`}>
                    envelope · {e.provider} · {e.state}
                  </span>
                ))}
              </div>
            )}
          </div>
          {/* Who each envelope went to (0040) — the strip said an envelope
              existed; this says who it is in front of. Drawn from the
              recipient rows, which are fenced through the envelope, so a
              requester reads their own deals' and nobody else's. */}
          {envs.length > 0 && (recipients ?? []).length > 0 && (
            <div data-testid="envelope-recipients">
              {envs.map((e) => {
                const to = (recipients ?? [])
                  .filter((r) => r.envelope_id === e.envelope_id)
                  .sort((a, b) => a.ordinal - b.ordinal);
                if (to.length === 0) return null;
                return (
                  <div className="caption mt-1" key={e.envelope_id}>
                    Envelope {e.envelope_id} went to{' '}
                    {to.map((r) => `${r.name} (${r.party})`).join(', ')}.
                  </div>
                );
              })}
            </div>
          )}
          <table className="ledger mt-2">
            <thead>
              <tr>
                <th>Obligation</th><th>Source (clause)</th><th>Due</th>
                <th>Owner</th><th>Status</th>
              </tr>
            </thead>
            <tbody>
              {mine.map((o) => (
                <tr key={o.obligation_id} data-testid="obligation-line"
                    {...openableRow(() => onOpen(o.obligation_id),
                                    `open obligation ${o.obligation_id}`)}>
                  <td>{o.summary}</td>
                  <td className="mono">
                    {o.clause_id}@v{o.version}{o.occurrence > 0 ? ` · #${o.occurrence}` : ''}
                  </td>
                  <td className="mono">{o.due_on ?? '—'}</td>
                  <td>
                    {/* An entitlement — the counterparty owes US — carries the
                        same weight as a duty, deliberately (OA §8). */}
                    {o.entitlement
                      ? <span className="chip chip-std" data-testid="entitlement">owed to us</span>
                      : <span className="caption">ours · {o.owner_person ?? 'UNOWNED'}</span>}
                  </td>
                  <td>{stateChip(o)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

// ── The inbox: what was actually sent (OB-11) ─────────────────────────────
function NotificationInbox() {
  const pane = usePane(() => API.outbox());
  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  return (
    <div className="mt-6">
      <PanelHead title="Inbox"
                 sub="What was actually sent — the delivery record, not the intention." />
      {pane.rows.length === 0 ? (
        <Empty kicker="inbox" line="Nothing has been sent."
               sub="Deliveries are recorded here as facts once they happen; a row
                    that never appears is itself the honest answer." />
      ) : (
        <div className="panel">
          {pane.rows.map((n, i) => (
            <div className="waiting-row" key={i}>
              <div className="min-w-0">
                <div className="text-[13px]" style={{ color: 'var(--ink)' }}>
                  {n.kind ?? 'digest'}
                  <span className="caption"> · to {n.person}
                    {n.channel ? ` · ${n.channel}` : ''}</span>
                </div>
                {n.subject_refs && (
                  <div className="caption mt-0.5">{String(n.subject_refs)}</div>
                )}
              </div>
              <span className="caption shrink-0">
                {n.sent_on ?? (n.sent_at ? String(n.sent_at).slice(0, 10) : '')}
                {n.outcome ? ` · ${n.outcome}` : ''}
              </span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── The pane the tab renders ──────────────────────────────────────────────
// Composed to the 2026-08-10 design: the headline figures first, the book
// as master-detail, the key stating what each mark means, then the waiting
// panel, the calendar and the delivery record. Every count is counted from
// the rows the policy returned — a figure the system did not measure is
// not on this screen.
// ── What a clause obliges (0035, on screen 2026-08-24) ───────────────────
//
// THE MOST COMPLETE UNREACHABLE THING IN THIS SCHEMA. 0035 built a whole
// governed content type — born proposed, approved by a named Legal admin who is
// never the proposer, immutable once approved, retirement terminal and reasoned,
// nothing ever deleted, every transition on the audit chain, a read to six roles
// and writes to two — and no endpoint ever named the table. Every template in
// every demonstration came from a seed script.
//
// WHAT IT COSTS WHEN IT IS EMPTY IS EVERYTHING BELOW IT. `cw.derive_obligations()`
// (0036) builds an executed agreement's duties by joining approved templates to
// the clauses that agreement's run selected, pinned to the templates approved ON
// the execution date. No approved template means no obligation, ever — so the
// book, the acts, the waivers, the digest and the coverage gaps were all
// machinery over a set only a seed script could fill.
//
// AND THE READ CENSUS COULD NOT SEE IT, which is worth saying where somebody
// will read it: that census counts a MENTION anywhere in `doorway/`, and a seed
// script and a test file both mention this table. A seed is not a person. Only
// the write census draws that line, and it is the line that found this.
//
// THE CONTROLS FOLLOW THE POLICIES, one role at a time. A reviewer or the Legal
// admin may propose; the Legal admin alone may approve or retire; everybody
// signed in may read, because what a clause obliges is not more secret than the
// clause. A role holding no grant is drawn no control rather than a control
// that always fails.
const TEMPLATE_KINDS = ['deliver', 'pay', 'notify', 'maintain', 'refrain', 'permit'];
const TEMPLATE_EVIDENCE = ['document', 'attestation', 'system', 'counterparty_ack'];
const TEMPLATE_ANCHORS = ['effective_on', 'executed_on', 'term_end', 'termination'];

const MAY_PROPOSE_TEMPLATE = ['legal_reviewer', 'legal_admin'];
const MAY_DECIDE_TEMPLATE = ['legal_admin'];

function TemplateProposeForm({ onDone }) {
  const acts = useActs();
  const [f, setF] = useRetainedState('obligation-template', {
    clause_id: '', version: '1', kind: 'deliver', obliged: 'vendor', summary: '',
    schedule_kind: 'once', anchor: 'effective_on', offset_days: '0',
    every_months: '', evidence: 'attestation', lead_days: '30',
    survives: false, entitlement: false,
  });
  const [error, setError] = useState(null);

  // THE THREE PAIRED CONSTRAINTS ARE THE TABLE'S, and this form keeps them
  // rather than restating them: 0035 says a recurring schedule has a month
  // interval and nothing else does, and that `on_event` and the `termination`
  // anchor imply each other. Moving one control moves the other so a person
  // cannot build a row the database will refuse — the database is still the
  // authority, and its sentence is what comes back if this is ever wrong.
  const setSchedule = (schedule_kind) => setF({
    ...f, schedule_kind,
    anchor: schedule_kind === 'on_event' ? 'termination'
            : f.anchor === 'termination' ? 'effective_on' : f.anchor,
    every_months: schedule_kind === 'recurring' ? (f.every_months || '12') : '',
  });
  const setAnchor = (anchor) => setF({
    ...f, anchor,
    schedule_kind: anchor === 'termination' ? 'on_event'
                   : f.schedule_kind === 'on_event' ? 'once' : f.schedule_kind,
    every_months: anchor === 'termination' ? '' : f.every_months,
  });

  const ready = f.clause_id.trim() && String(f.version).trim() && f.summary.trim();

  const propose = () => acts.run('propose-template', async () => {
    setError(null);
    const r = await API.proposeObligationTemplate({
      clause_id: f.clause_id.trim(),
      version: Number(f.version),
      kind: f.kind,
      obliged: f.obliged,
      summary: f.summary.trim(),
      schedule_kind: f.schedule_kind,
      anchor: f.anchor,
      offset_days: Number(f.offset_days || 0),
      every_months: f.schedule_kind === 'recurring' ? Number(f.every_months || 12) : null,
      evidence: f.evidence,
      lead_days: Number(f.lead_days || 30),
      // AS WORDS, not booleans. The doorway refuses a boolean in a request
      // body outright — a body carrying a JSON type the driver adapts its own
      // way is a body whose meaning depends on the client — and the statement
      // casts these instead.
      survives: String(!!f.survives),
      entitlement: String(!!f.entitlement),
    });
    if (!r.ok) { setError(r.reason); return r; }
    discardDraft('obligation-template');
    setF({ ...f, clause_id: '', summary: '' });
    onDone();
    return r;
  });

  return (
    <div className="panel p-4 mt-3" data-testid="propose-template">
      <PanelHead
        title="Declare what a clause obliges"
        sub="A declaration on one clause version, not a reading of its prose. It is born proposed and confers nothing until a Legal admin who is not you approves it." />

      <div className="flex gap-2 flex-wrap mt-3">
        <input className="font-mono" style={{ padding: '6px 9px', minWidth: 140 }}
               placeholder="DP-H-014" aria-label="Which clause"
               data-testid="template-clause" value={f.clause_id}
               onChange={(e) => setF({ ...f, clause_id: e.target.value })} />
        <input className="font-mono" style={{ padding: '6px 9px', width: 80 }}
               placeholder="1" aria-label="Which version"
               data-testid="template-version" value={f.version}
               onChange={(e) => setF({ ...f, version: e.target.value })} />
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="What kind of duty" data-testid="template-kind"
                value={f.kind} onChange={(e) => setF({ ...f, kind: e.target.value })}>
          {TEMPLATE_KINDS.map((k) => <option key={k} value={k}>{k}</option>)}
        </select>
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="Which party owes it" data-testid="template-obliged"
                value={f.obliged} onChange={(e) => setF({ ...f, obliged: e.target.value })}>
          <option value="vendor">vendor owes it</option>
          <option value="customer">we owe it</option>
        </select>
      </div>

      <input className="w-full mt-2" style={{ padding: '6px 9px' }}
             placeholder="what the duty is, in one sentence"
             aria-label="What the duty is" data-testid="template-summary"
             value={f.summary} onChange={(e) => setF({ ...f, summary: e.target.value })} />

      <div className="flex gap-2 flex-wrap mt-2 items-center">
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="How often" data-testid="template-schedule"
                value={f.schedule_kind} onChange={(e) => setSchedule(e.target.value)}>
          <option value="once">once</option>
          <option value="recurring">recurring</option>
          <option value="on_event">on an event</option>
        </select>
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="Measured from" data-testid="template-anchor"
                value={f.anchor} onChange={(e) => setAnchor(e.target.value)}>
          {TEMPLATE_ANCHORS.map((a) => <option key={a} value={a}>from {a}</option>)}
        </select>
        <input className="font-mono" style={{ padding: '6px 9px', width: 90 }}
               aria-label="Days after the anchor" data-testid="template-offset"
               value={f.offset_days}
               onChange={(e) => setF({ ...f, offset_days: e.target.value })} />
        <span className="caption">days after</span>
        {f.schedule_kind === 'recurring' && (
          <>
            <input className="font-mono" style={{ padding: '6px 9px', width: 90 }}
                   aria-label="Every how many months" data-testid="template-months"
                   value={f.every_months}
                   onChange={(e) => setF({ ...f, every_months: e.target.value })} />
            <span className="caption">months apart</span>
          </>
        )}
      </div>

      <div className="flex gap-2 flex-wrap mt-2 items-center">
        <select className="font-mono" style={{ padding: '6px 9px' }}
                aria-label="What satisfies it" data-testid="template-evidence"
                value={f.evidence} onChange={(e) => setF({ ...f, evidence: e.target.value })}>
          {TEMPLATE_EVIDENCE.map((e2) => <option key={e2} value={e2}>{e2}</option>)}
        </select>
        <input className="font-mono" style={{ padding: '6px 9px', width: 90 }}
               aria-label="Days of warning" data-testid="template-lead"
               value={f.lead_days}
               onChange={(e) => setF({ ...f, lead_days: e.target.value })} />
        <span className="caption">days of warning</span>
        <label className="caption flex items-center gap-1.5">
          <input type="checkbox" data-testid="template-survives"
                 checked={!!f.survives}
                 onChange={(e) => setF({ ...f, survives: e.target.checked })} />
          survives termination
        </label>
        <label className="caption flex items-center gap-1.5">
          <input type="checkbox" data-testid="template-entitlement"
                 checked={!!f.entitlement}
                 onChange={(e) => setF({ ...f, entitlement: e.target.checked })} />
          it is an entitlement, not a duty
        </label>
      </div>

      <div className="flex gap-2 mt-3 items-center">
        <ActButton className="btn btn-primary" data-testid="template-propose"
                   disabled={!ready} onClick={propose}>
          propose this declaration
        </ActButton>
        <span className="caption">
          Proposing confers nothing. Somebody else approves it, and only then
          does an agreement executed afterwards register the duty.
        </span>
      </div>
      {/* Spelled out here rather than reaching for library.jsx's ActError:
          a helper defined in another pane file is a load-order dependency
          nothing in this repository checks. */}
      {error && (
        <div className="panel-2 p-3 mt-3" data-testid="propose-template-error">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>refused</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{error}</div>
        </div>
      )}
    </div>
  );
}

function ObligationTemplates({ me }) {
  const acts = useActs();
  const pane = usePane(() => API.obligationTemplates());
  const [proposing, setProposing] = useState(false);
  const [retiring, setRetiring] = useState(null); // template_id
  const [reason, setReason] = useState('');
  const [error, setError] = useState(null);
  const filter = useListFilter(pane.rows, {
    view: 'obligations:templates',
    fields: ['clause_id', 'summary', 'kind', 'proposed_by', 'approved_by'],
    facet: 'state',
  });

  const mayPropose = MAY_PROPOSE_TEMPLATE.includes(me.role);
  const mayDecide = MAY_DECIDE_TEMPLATE.includes(me.role);

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

  const retire = (row) => acts.run(`retire-${row.template_id}`, async () => {
    setError(null);
    const r = await API.retireObligationTemplate({
      template_id: row.template_id, reason: reason.trim() });
    if (!r.ok) { setError(r.reason); return r; }
    setRetiring(null); setReason('');
    pane.reload();
    return r;
  });

  const schedule = (t) =>
    t.schedule_kind === 'recurring'
      ? `every ${t.every_months} month(s) from ${t.anchor} +${t.offset_days}d`
      : t.schedule_kind === 'on_event'
        ? `on ${t.anchor}`
        : `once, ${t.anchor} +${t.offset_days}d`;

  return (
    <div className="mt-8" data-testid="obligation-templates">
      <PanelHead
        title="What our clauses oblige"
        sub="Every duty in the book above is derived from one of these. A declaration is made on the clause record — never read out of the wording — and an agreement registers the declarations that stood approved on the day it executed, which is why an approved one is never edited and retirement is terminal."
        right={<FilterCount filter={filter} />} />

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

      {mayPropose && !proposing && (
        <button className="btn btn-sm mb-3" data-testid="open-propose-template"
                onClick={() => setProposing(true)}>
          declare a duty…
        </button>
      )}
      {mayPropose && proposing && (
        <TemplateProposeForm onDone={() => { setProposing(false); pane.reload(); }} />
      )}

      {pane.status === 'loading' ? <Loading />
        : pane.status === 'failed' ? <LoadFailed reason={pane.reason} />
        : pane.rows.length === 0 ? (
          <Empty
            kicker="declarations"
            line="No clause declares any obligation."
            sub="Nothing an agreement executes from here will register a duty, and
                 that is arithmetic rather than an oversight: the book is derived
                 from these declarations and there are none." />
        ) : (
          <>
            <ListFilter filter={filter} testid="templates"
                        placeholder="clause, duty or who proposed it"
                        facetLabel="state" />
            {filter.shown.length === 0
              ? <NoMatch kicker="declarations" noun="declaration" />
              : (
                <div className="panel">
                  <table className="ledger">
                    <thead><tr><th>Clause</th><th>Duty</th><th>When</th>
                               <th>Standing</th>
                               <th style={{ textAlign: 'right' }}>Act</th></tr></thead>
                    <tbody>
                      {filter.shown.map((t) => (
                        <tr key={t.template_id}>
                          <td className="mono">{t.clause_id}@v{t.version}</td>
                          <td>
                            <span className="chip chip-std">{t.kind}</span>
                            <span className="caption"> {t.obliged} owes</span>
                            <div className="text-[12.5px] mt-0.5">{t.summary}</div>
                          </td>
                          <td className="caption">
                            {schedule(t)}
                            <div>{t.evidence} · {t.lead_days}d warning
                              {t.survives ? ' · survives' : ''}
                              {t.entitlement ? ' · entitlement' : ''}</div>
                          </td>
                          <td>
                            <span className={`chip ${t.state === 'approved' ? 'chip-ok'
                              : t.state === 'retired' ? 'chip-err' : 'chip-pending'}`}>
                              {t.state}
                            </span>
                            <div className="caption mt-0.5">
                              {t.state === 'proposed'
                                ? `proposed by ${t.proposed_by}`
                                : t.state === 'approved'
                                  ? `approved by ${t.approved_by} on ${String(t.approved_on ?? '').slice(0, 10)}`
                                  : `retired on ${String(t.retired_on ?? '').slice(0, 10)} — ${t.retired_reason}`}
                            </div>
                          </td>
                          <td style={{ textAlign: 'right' }}>
                            {/* NOBODY APPROVES THEIR OWN, and 0035 checks that
                                against the RECORDED proposer rather than the
                                connection — so holding two roles does not open a
                                way round it. The control is hidden for the same
                                reason it is refused, and says which. */}
                            {mayDecide && t.state === 'proposed' && t.proposed_by !== me.person && (
                              <ActButton className="btn btn-sm"
                                         data-testid={`template-approve-${t.template_id}`}
                                         onClick={() => approve(t)}>
                                approve
                              </ActButton>
                            )}
                            {mayDecide && t.state === 'proposed' && t.proposed_by === me.person && (
                              <span className="caption">yours to propose, somebody else&rsquo;s to approve</span>
                            )}
                            {mayDecide && t.state === 'approved' && retiring !== t.template_id && (
                              <button className="btn btn-sm"
                                      data-testid={`template-retire-${t.template_id}`}
                                      onClick={() => { setRetiring(t.template_id); setReason(''); }}>
                                retire…
                              </button>
                            )}
                            {mayDecide && retiring === t.template_id && (
                              <div className="flex gap-2 items-center justify-end">
                                <input style={{ padding: '5px 9px', minWidth: 180 }}
                                       placeholder="why it is being retired"
                                       aria-label="Why it is being retired"
                                       data-testid="template-retire-reason"
                                       value={reason}
                                       onChange={(e) => setReason(e.target.value)} />
                                <ActButton className="btn btn-sm btn-primary"
                                           data-testid="template-retire-confirm"
                                           disabled={!reason.trim()}
                                           onClick={() => retire(t)}>
                                  retire
                                </ActButton>
                                <button className="btn btn-sm"
                                        onClick={() => setRetiring(null)}>cancel</button>
                              </div>
                            )}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
          </>
        )}

      <div className="caption mt-3" style={{ lineHeight: 1.6 }}>
        {/* `{' '}` after the <em>: JSX drops the whitespace on a line break
            between an element and the text after it, and the sentence rendered
            as "afterwardsregister" in the running application. Seen by walking
            it; nothing in this repository would have caught it. */}
        Retiring a declaration changes what agreements executed <em>afterwards</em>{' '}
        register, and changes nothing about the duties already on the book —
        registration reads these by date, so rewriting an approved one would
        rewrite what past registrations meant.
      </div>
    </div>
  );
}

// ── What a signed agreement carries as evidence, and what it is missing ────
// Three reads of the executed family (0006), each granted since 0006 and
// served by nothing until 2026-08-24. REPORTING, all three — a gap here is a
// filing task for a named person, and drift is input to a renewal
// conversation; nothing on this panel changes a signed contract, because
// nothing can.
function ExecutionEvidence({ certificates, gaps, drift }) {
  const short = (gaps ?? []).filter((g) =>
    g.missing_our_signatory || g.missing_their_signatory
    || g.missing_completion_certificate);
  const gapWords = (g) => [
    g.missing_our_signatory && 'no signatory of ours on record',
    g.missing_their_signatory && 'no signatory of theirs on record',
    g.missing_completion_certificate && 'no completion certificate on file',
  ].filter(Boolean).join(', ');

  return (
    <div className="mt-8" data-testid="execution-evidence">
      <PanelHead title="Execution evidence"
                 sub="What each signed agreement carries on the record, and what it is still missing." />

      {gaps !== null && (
        short.length === 0 ? (
          <div className="caption" data-testid="evidence-gaps">
            {gaps.length === 0
              ? 'Nothing is executed, so there is no evidence to be short of.'
              : `Every signed agreement (${gaps.length}) carries both signatories and its completion certificate.`}
          </div>
        ) : (
          <div data-testid="evidence-gaps">
            {short.map((g) => (
              <div className="panel-2 p-3 mb-2" key={g.agreement_id}>
                <span className="font-mono text-[12px]" style={{ color: 'var(--ink)' }}>
                  {g.agreement_id}
                </span>
                <span className="chip chip-pending" style={{ marginLeft: 8 }}>evidence incomplete</span>
                <div className="caption mt-1">
                  {gapWords(g)}. Filing it is Legal&apos;s — the system&apos;s part is
                  saying so here rather than resolving it on anybody&apos;s behalf.
                </div>
              </div>
            ))}
          </div>
        )
      )}

      {certificates !== null && certificates.length > 0 && (
        <div className="mt-4" data-testid="signature-certificates">
          <div className="section-label mb-2">Completion certificates on file</div>
          {certificates.map((c) => (
            <div className="caption mb-1" key={c.agreement_id}>
              <span className="font-mono">{c.agreement_id}</span>
              {' — '}{c.provider}, ceremony completed{' '}
              {new Date(c.completed_at).toLocaleDateString()},{' '}
              {Number(c.byte_size).toLocaleString()} bytes held,{' '}
              sha256 <span className="font-mono">{String(c.sha256).slice(0, 12)}…</span>
            </div>
          ))}
          <div className="caption mt-1" style={{ color: 'var(--mute-2)' }}>
            The bytes themselves stay on the record — what a dispute verifies
            them against is the hash above.
          </div>
        </div>
      )}

      {drift !== null && drift.length > 0 && (
        <div className="mt-4" data-testid="agreement-drift">
          <div className="section-label mb-2">Where the library has moved on</div>
          {drift.map((d) => (
            <div className="caption mb-1" key={`${d.agreement_id}·${d.clause_id}`}>
              <span className="font-mono">{d.agreement_id}</span>
              {' carries '}<span className="font-mono">{d.clause_id}@v{d.executed_version}</span>
              {d.successor_version
                ? <> — superseded by v{d.successor_version}{d.superseded_reason ? ` (${d.superseded_reason})` : ''}</>
                : <> — now {d.current_state ?? 'no longer active'}</>}
            </div>
          ))}
          <div className="caption mt-1" style={{ color: 'var(--mute-2)' }}>
            Input to a renewal conversation, nothing more. The signed contract
            has not changed, and cannot.
          </div>
        </div>
      )}
    </div>
  );
}

function ObligationsPane({ me }) {
  // THE OBLIGATION YOU HAVE OPEN LIVES IN THE ADDRESS (S321). A duty is a
  // thing somebody links a colleague to — "look at this one" was a sentence
  // people had to follow by hand — Back should close it rather than leave the
  // application, and a reload should not lose your place.
  const [openId, openObligation] = useAddressedRecord('obligations');

  const book = usePane(() => API.obligationsBook());
  const envelopes = usePane(() => API.envelopes());
  // WHO EACH ENVELOPE WENT TO (0040). The strip said an envelope existed and
  // its state, and could not say who it was in front of — the recipient rows
  // were granted to every role this pane is on and served by nothing.
  const recipients = usePane(() => API.envelopeRecipients());
  // THE EVIDENCE A SIGNED AGREEMENT CARRIES, AND WHAT IT IS STILL MISSING
  // (0006). All three had grants since 0006 and no read until 2026-08-24:
  // the completion certificate on record (metadata, never the bytes), the
  // signed agreements short of the evidence they should carry, and how far
  // the library has moved on from what an executed contract holds.
  const certificates = usePane(() => API.signatureCertificates());
  const evidenceGaps = usePane(() => API.evidenceGaps());
  const drift = usePane(() => API.agreementDrift());
  const gaps = usePane(() => API.obligationGaps());
  // THE DUTIES NOBODY HOLDS, counted separately rather than left to be spotted
  // in a list of hundreds. `cw.obligation_unowned` has had a grant and a read
  // since 0037 and no screen — the row most likely to pass its date in silence
  // was the one nothing pointed at.
  const unowned = usePane(() => API.obligationsUnowned());
  // WHAT THE RECORD SAYS IS FINISHED (0038). Derived, never declared, and
  // fail-closed: an obligation that survives termination with no anchor date
  // blocks. Also had a read and no screen.
  const closeable = usePane(() => API.closeableAgreements());
  // WHAT ARRIVED, so an act can cite evidence. Three of the four acts touch
  // this — one requires it — and until this read nothing anywhere told a
  // person the document_id the endpoint demands.
  const documents = usePane(() => API.receivedDocuments());
  // WHO A DUTY MAY BE HANDED TO. Every signed-in role reads the account
  // register (0013's read_all); a role that is refused falls back to typing
  // the name rather than being unable to hand anything over.
  const people = usePane(() => API.people());
  // THE APPROVALS THAT AUTHORISE A WAIVER. A waiver rides the override
  // machinery (0039) rather than having a second approval system, so the
  // authority is an override FINDING naming `obligation:<id>` and approved.
  const approvals = usePane(() => API.overrideFindings());
  const unownedRef = React.useRef(null);

  // Narrows the BOOK, so the stat boxes, the grouped list and the calendar
  // below all speak about the same set — a filter that moved the list and left
  // the totals alone would be two answers on one screen.
  // ABOVE EVERY EARLY RETURN. A hook called after a conditional `return` runs
  // on some renders and not others, and React refuses the second one with
  // "Rendered more hooks than during the previous render" — the pane goes
  // blank. It reads `.rows` defensively because the record is still empty on
  // the render before the read lands.
  const filterRows = book.rows;
  const filter = useListFilter(filterRows, {
    view: 'obligations:book',
    fields: ['agreement_id', 'obligation_id', 'owner_person', 'summary', 'clause_id'],
    facet: 'state',
  });

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

  // ── One obligation, open ────────────────────────────────────────────────
  // EVERY HOOK IS ABOVE THIS LINE. A hook after an early return blanks the
  // pane on the render AFTER the data lands, which is the one nobody watches
  // (S318) — and this file is in `hook-order.test.mjs`'s scope.
  if (openId) {
    return (
      <ObligationRecord
        me={me}
        obligation={book.rows.find(
          (o) => String(o.obligation_id) === String(openId))}
        documents={documents.status === 'loaded' ? documents.rows : []}
        documentsFailed={documents.status === 'failed' ? documents.reason : null}
        people={people.status === 'loaded' ? people.rows : []}
        approvals={approvals.status === 'loaded' ? approvals.rows : []}
        onClose={() => openObligation(null)}
        onActed={() => {
          // Four surfaces behind this record are now out of date.
          book.reload(); unowned.reload(); closeable.reload();
        }} />
    );
  }

  const rows = filter.shown;

  // ── THE FIGURES COUNT THE WHOLE BOOK, AND PRESSING ONE NARROWS TO IT ─────
  //
  // THIS REVERSES AN EARLIER DECISION, and the earlier argument deserves an
  // answer rather than silence. It ran: a filter that moved the list and left
  // the totals alone would be two answers on one screen — so the figures were
  // computed from `filter.shown` and moved with it.
  //
  // What that cost was the whole of S333. A figure computed from the narrowed
  // set cannot be a CONTROL: press "overdue 17" and the narrowing makes the
  // tile beside it read "total on the book 17", so every number on the strip
  // collapses onto the one you pressed. Six figures on the pane with the most
  // rows in the application were inert for that reason, measured 2026-08-23.
  //
  // The two-answers worry is answered by what the library pane already does:
  // the narrowing is VISIBLE. `focusOn` puts a chip in the filter row saying
  // which subset is showing, `FilterCount` reads `N of M`, and pressing the
  // same figure again clears it. A list quietly showing a subset is the trap;
  // a list saying which subset, with a way out, is a control.
  // `wholeBook`, not `book` — that name is already the pane's `usePane`
  // handle, and shadowing it is a parse error that blanks every component in
  // this file. Caught by re-parsing after the edit.
  const wholeBook = filterRows;
  const openBook = wholeBook.filter((o) => !o.closed_as);
  const counts = {
    agreements: new Set(wholeBook.map((o) => o.agreement_id)).size,
    total: wholeBook.length,
    overdue: openBook.filter((o) => o.state === 'overdue').length,
    due: openBook.filter((o) => o.state === 'due').length,
    closed: wholeBook.filter((o) => o.closed_as).length,
  };
  counts.standing = openBook.length - counts.overdue - counts.due;

  // Each figure's own predicate, written once so the number and the narrowing
  // cannot come apart — the tile counts what the test selects, by construction.
  const isOpen = (o) => !o.closed_as;
  const drill = (key, label, test) => {
    // Registered as the tile renders, so a saved view can put the focus back.
    const f = filter.focusable(key, label, test);
    return {
      to: () => filter.focusOn(f),
      on: filter.focus && filter.focus.key === key,
      describe: `show only ${label}`,
    };
  };

  return (
    <div>
      <div className="sheet-head">
        <h1 className="sheet-title">Obligations</h1>
        <div className="flex items-center gap-4">
          {counts.overdue > 0 && (
            <span className="stamp stamp-err" style={{ '--rot': '1.5deg' }}>
              {counts.overdue} overdue
            </span>
          )}
          <span className="sheet-note">
            {filter.filtering
              ? `${filter.shown.length} of ${filter.total} on the book`
              : `${counts.total} on the book`}
            {' · '}{counts.agreements}
            {counts.agreements === 1 ? ' agreement' : ' agreements'}
          </span>
        </div>
      </div>
      <div className="font-serif italic mt-2" style={{ fontSize: 14, color: 'var(--mute)' }}>
        Duties and entitlements that bind, lifted from the recorded book.
      </div>

      <HistoricalObligations me={me} />
      <ListFilter filter={filter} testid="obligations"
                  placeholder="agreement, owner, clause or wording"
                  facetLabel="every state" />

      <div className="mt-5 grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
        {/* AGREEMENTS IS THE ONE THAT STAYS INERT, and deliberately: it
            counts DEALS while every list below it counts OBLIGATIONS, so there
            is no set of rows a press could narrow to. A figure whose unit is
            not the list's unit has nowhere to go, and offering one anyway is
            the affordance-for-nothing this whole strip was repaired for. */}
        <StatBox label="agreements with obligations" n={counts.agreements} />
        <StatBox label="total on the book" n={counts.total}
                 to={() => filter.setFocus(null)}
                 on={filter.focus === null}
                 describe={`show all ${counts.total} on the book`} />
        <StatBox label="standing" n={counts.standing}
                 {...drill('standing', 'standing',
                           (o) => isOpen(o) && o.state !== 'overdue' && o.state !== 'due')} />
        <StatBox label="due" n={counts.due}
                 nStyle={counts.due > 0 ? { color: 'var(--accent-2)' } : undefined}
                 {...drill('due', 'due', (o) => isOpen(o) && o.state === 'due')} />
        <StatBox label="overdue" n={counts.overdue}
                 nStyle={counts.overdue > 0 ? { color: 'var(--danger)' } : undefined}
                 {...drill('overdue', 'overdue', (o) => isOpen(o) && o.state === 'overdue')} />
        <StatBox label="closed on the record" n={counts.closed}
                 {...drill('closed', 'closed on the record', (o) => Boolean(o.closed_as))} />
        {/* THE ABSENCE OF AN OWNER, COUNTED — never left to be noticed. A
            measured zero still drills, because "every duty is held" is a real
            answer and the panel that says so is a real destination; an
            UNMEASURED figure draws an em-dash and stays inert (S333). */}
        <StatBox label="nobody holds" n={unowned.status === 'loaded' ? unowned.rows.length : null}
                 to={unowned.status === 'loaded' ? showSection(unownedRef) : null}
                 describe={`show the ${unowned.rows.length} obligations nobody holds`}
                 nStyle={unowned.rows.length > 0 ? { color: 'var(--danger)' } : undefined} />
      </div>

      {rows.length === 0 && (
        <div className="mt-6">
          {/* An empty book and a filter that matches nothing are different
              news, and the second must not wear the first's clothes. */}
          {filter.filtering
            ? <NoMatch kicker="the book" noun="obligation" />
            : <Empty kicker="the book" line="No obligation is on the book."
                     sub="Duties arrive when executed clauses declare them. An empty
                          book is the honest state of a system with nothing executed
                          — not a screen waiting to be filled." />}
        </div>
      )}

      {/* A LIST OF WHAT BINDS YOU SHOULD BE A LIST YOU CAN ACT FROM. Every
          row here now opens the obligation, where the four acts live — the
          rule the requester's deal list and the buyer home were repaired for,
          reaching the book that has the most rows of any of them. */}
      <AgreementObligations rows={rows} onOpen={openObligation}
        envelopes={envelopes.status === 'loaded' ? envelopes.rows : []}
        recipients={recipients.status === 'loaded' ? recipients.rows : []} />

      {/* Drawn only when the reads answered, for the unowned panel's reason:
          a role refused the evidence reads must not be shown "every signed
          agreement carries its evidence", a claim this screen would have no
          standing to make. */}
      {(certificates.status === 'loaded' || evidenceGaps.status === 'loaded'
        || drift.status === 'loaded') && (
        <ExecutionEvidence
          certificates={certificates.status === 'loaded' ? certificates.rows : null}
          gaps={evidenceGaps.status === 'loaded' ? evidenceGaps.rows : null}
          drift={drift.status === 'loaded' ? drift.rows : null} />
      )}

      {rows.length > 0 && (
        <div className="note-card key-card mt-5" style={{ maxWidth: 420, '--rot': '-.4deg' }}>
          <div className="note-title">Key</div>
          <div className="key-row"><span className="chip chip-ok">active</span>
            <span className="key-what">in force, and confers what it says</span></div>
          <div className="key-row"><span className="chip chip-pending">due</span>
            <span className="key-what">falling due — amber, never green</span></div>
          <div className="key-row"><span className="chip chip-err">overdue</span>
            <span className="key-what">past due with nothing recorded against it</span></div>
          <div className="key-row"><span className="chip chip-std">closed</span>
            <span className="key-what">closed as a recorded act, with a name on it</span></div>
        </div>
      )}

      {gaps.status === 'loaded' && gaps.rows.length > 0 && (
        // A clause in force declaring no obligations — reported, not guessed
        // at, and never a defect in development (content is placeholder).
        <div className="caption mt-4" data-testid="obligation-gaps">
          {gaps.rows.length} in-force clause{gaps.rows.length === 1 ? ' declares' : 's declare'}
          {' '}no obligations: {gaps.rows.map((g) => `${g.clause_id}@v${g.version}`).join(' · ')}.
          Declaring them is Legal's, through the declarations below — the
          system's part is showing the gap here and giving Legal somewhere to
          close it.
        </div>
      )}

      {/* ── THE GAP, DRAWN AS A GAP AND ACTED ON FROM ───────────────────
          Drawn only when the read answered. A role refused it would otherwise
          be shown "every obligation is held", which is a claim this screen
          would have no standing to make. */}
      {unowned.status === 'loaded' && (
        <div className="mt-8" ref={unownedRef}>
          <UnownedObligations me={me} rows={unowned.rows} onOpen={openObligation} />
        </div>
      )}

      {closeable.status === 'loaded' && (
        <div className="mt-8">
          <CloseableAgreements rows={closeable.rows} />
        </div>
      )}

      <div className="mt-8">
        <WaitingOnYouPanel me={me} />
      </div>
      <ObligationTemplates me={me} />
      <ObligationCalendar rows={rows} onOpen={openObligation} />
      <NotificationInbox />
    </div>
  );
}
