// The expert panel (0090, ADR-0013): consultation is evidence, never a veto.
//
// TWO PANES, THREE AUDIENCES.
//
//   PanelDeskPane   the panel member's desk AND Legal's referral board. One
//                   pane, because the scoping is cw.consultation's own read
//                   policy — a seat holder is answered the rows their live
//                   seat admits, Legal is answered the whole queue, from the
//                   same render. The deal room does exactly this for the same
//                   reason (0079). What differs by role is the AFFORDANCES:
//                   an expert gets an answer form, Legal gets ask and waive.
//                   Neither is a permission; the database refuses regardless.
//
//   PanelSeatsPane  who sits on the panel. The Administrator's, because a seat
//                   is an access grant (U5, ADR-0011) — and it decides nothing,
//                   which is why the routing RULES are not on this screen.
//
// THE ONE THING THIS SCREEN MUST NOT DO is make an expert's answer look like a
// decision. 'unsound' does not block the ticket, and a screen that drew it as a
// blocker would teach people a rule the system does not have. So the strip
// beside a required consultation says what it means in words — waiting, or
// answered, or waived — and the advice is a separate mark that gates nothing.

const { useState: usePanelState, useMemo: usePanelMemo } = React;

// `ADVICE` and `ConsultationMark` MOVED TO `common.jsx` on 2026-08-25, when the
// review desk became the second screen to draw a consultation. Both are used
// below exactly as before; what changed is that there is one copy of them.

// ── The desk ───────────────────────────────────────────────────────────────
// THE CATALOGUE OF NARROWINGS THIS LIST OFFERS (0110), declared once and read
// twice — by the figures that raise them, and by the saved views that put them
// back. A focus is a `test` function and no store holds one, so what a saved
// view keeps is the KEY; this is where a key becomes a test again. Declared
// outside the component so it is the same array on every render.
const CONSULTATION_FOCUSES = [
  { key: 'open',     label: 'waiting',  test: (r) => r.state === 'open' },
  { key: 'answered', label: 'answered', test: (r) => r.state === 'answered' },
  { key: 'waived',   label: 'waived',   test: (r) => r.state === 'waived' },
];
const consultationFocus = (k) => CONSULTATION_FOCUSES.find((f) => f.key === k);

function PanelDeskPane({ me }) {
  const isLegal = me.role === 'legal_reviewer' || me.role === 'legal_admin';
  const pane = usePane(() => API.panelConsultations());
  const acts = useActs();
  const [refused, setRefused] = usePanelState(null);
  // Which consultation's form is open. Held here rather than in the address,
  // because a half-written opinion is not a place you send a colleague to.
  const [drafting, setDrafting] = usePanelState(null);
  const [advice, setAdvice] = usePanelState('sound');
  const [reasoning, setReasoning] = usePanelState('');
  const [waiverReason, setWaiverReason] = usePanelState('');

  const filter = useListFilter(pane.rows, {
    view: 'consultations:all',
    focuses: CONSULTATION_FOCUSES,
    fields: ['consultation_id', 'ticket_id', 'discipline_key', 'category_key',
             'question', 'opened_by', 'answered_by'],
    facet: 'state',
  });

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

  const open = pane.rows.filter((r) => r.state === 'open');
  const answered = pane.rows.filter((r) => r.state === 'answered');
  const waived = pane.rows.filter((r) => r.state === 'waived');

  const finish = (r) => {
    if (!r.ok) { setRefused(r.reason); return; }
    setRefused(null);
    setDrafting(null);
    setReasoning('');
    setWaiverReason('');
    pane.reload();
  };

  const answer = (row) => acts.run(`answer-${row.consultation_id}`, async () =>
    finish(await API.answerConsultation({
      consultation_id: row.consultation_id,
      advice,
      reasoning: reasoning.trim() || null,
    })));

  const waive = (row) => acts.run(`waive-${row.consultation_id}`, async () =>
    finish(await API.waiveConsultation({
      consultation_id: row.consultation_id,
      waiver_reason: waiverReason.trim(),
    })));

  return (
    <div>
      <PaneHead
        title={isLegal ? 'Expert consultation' : 'Consultations'}
        sub={isLegal
          ? 'What each ticket was referred to, and what came back. An expert saying “unsound” does not block the ticket — you read it and decide, and the record keeps both.'
          : 'Questions referred to your discipline. You are giving an opinion, not a decision: what you say is recorded on the ticket permanently, and Legal decides.'}
        right={<FilterCount filter={filter} />} />

      {/* Measured facts only. Every one of these three counts rows that are on
          this screen — no tile here reports a stage the derivation cannot
          reach, which is the mistake S312 found on the pipeline strip.
          AND EVERY ONE OF THEM IS NOW A CONTROL (S333), which they were not
          until 2026-08-23: three figures sat directly over the list they
          counted and could not be pressed. `focusOn` narrows to exactly the
          set the tile counted, puts a chip in the filter row saying which, and
          clears the search on the way — so the number and the rows below it
          are the same set by construction. The fourth tile is the way back. */}
      <TileStrip tiles={[
        { label: 'all', n: pane.rows.length,
          to: () => filter.setFocus(null),
          on: filter.focus === null,
          describe: `show all ${pane.rows.length} consultations` },
        { label: 'waiting',  n: open.length,
          to: () => filter.focusOn(consultationFocus('open')),
          on: filter.focus && filter.focus.key === 'open' },
        { label: 'answered', n: answered.length,
          to: () => filter.focusOn(consultationFocus('answered')),
          on: filter.focus && filter.focus.key === 'answered' },
        { label: 'waived',   n: waived.length,
          to: () => filter.focusOn(consultationFocus('waived')),
          on: filter.focus && filter.focus.key === 'waived' },
      ]} />

      <ListFilter filter={filter} testid="panel-consultations"
                  placeholder="ticket, discipline, category, or who asked"
                  facetLabel="every state" />

      {refused && <Refused what="That was refused." reason={refused} />}

      {!filter.shown.length && (
        pane.rows.length
          ? <NoMatch kicker="consultations" noun="consultation" />
          : <Empty
              kicker="consultations"
              line={isLegal
                ? 'No ticket has been referred to a discipline yet.'
                : 'Nothing has been referred to your discipline.'}
              sub="An empty desk, not a failed read." />
      )}

      {filter.shown.map((row) => {
        const busyAnswer = acts.busy === `answer-${row.consultation_id}`;
        const busyWaive = acts.busy === `waive-${row.consultation_id}`;
        const drafted = drafting === `a-${row.consultation_id}`;
        const waiving = drafting === `w-${row.consultation_id}`;
        return (
          <div className="panel mb-3" key={row.consultation_id}
               data-testid={`consultation-${row.consultation_id}`}>
            <div className="flex items-start justify-between gap-3">
              <div>
                <div className="font-mono" style={{ fontSize: 12, color: 'var(--mute-2)' }}>
                  {/* SAY WHICH KIND OF REFERENCE THIS IS. One column carrying
                      several kinds of id is S316's finding; a bare number under
                      a heading is not a reference anybody can act on. */}
                  ticket {row.ticket_id} · {row.category_key} · {row.severity}
                </div>
                <div className="mt-1" style={{ fontSize: 15 }}>{row.question}</div>
              </div>
              <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center',
                             flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                {row.necessity === 'required'
                  ? <Status state="pending"
                      title="The ticket cannot be verified until this is answered or waived in writing.">
                      required
                    </Status>
                  : <Status state="neutral"
                      title="Asked, and gating nothing. The ticket moves whether or not anybody answers.">
                      advisory
                    </Status>}
                <ConsultationMark row={row} />
              </span>
            </div>

            {/* The words the expert is actually being asked about, frozen when
                the consultation opened. An expert reads THIS, never the ticket
                — which is why a panel member needs no grant on cw.review_ticket
                and the viewer's fence stayed one table wide. */}
            {row.passage && (
              <blockquote className="mt-2" style={{
                borderLeft: '2px solid var(--hair)', paddingLeft: 10,
                fontSize: 13, color: 'var(--mute)' }}>
                {row.passage}
              </blockquote>
            )}

            {row.state === 'answered' && row.reasoning && (
              <p className="mt-2" style={{ fontSize: 13 }}>
                <strong>{row.answered_by}</strong> — {row.reasoning}
              </p>
            )}
            {row.state === 'waived' && (
              <p className="mt-2" style={{ fontSize: 13 }}>
                <strong>{row.waived_by}</strong> waived this: {row.waiver_reason}
              </p>
            )}

            {row.state === 'open' && (
              <div className="mt-3 flex items-center gap-2 flex-wrap">
                {/* AFFORDANCES, NOT PERMISSIONS. Both buttons are offered to
                    whoever the endpoint might accept, and cw.consultation's
                    policies and triggers refuse everybody else in their own
                    words — including a lawyer who holds a seat but asked this
                    very question. */}
                {!isLegal && !drafted && (
                  <button className="btn btn-sm"
                          onClick={() => { setDrafting(`a-${row.consultation_id}`);
                                           setAdvice('sound'); setReasoning(''); }}>
                    give an opinion
                  </button>
                )}
                {isLegal && !waiving && (
                  <button className="btn btn-sm"
                          onClick={() => { setDrafting(`w-${row.consultation_id}`);
                                           setWaiverReason(''); }}>
                    waive with a reason
                  </button>
                )}
              </div>
            )}

            {drafted && (
              <div className="mt-3">
                <div className="flex gap-2 flex-wrap">
                  {/* THE NAME IS THE VISIBLE WORD, and it is spelled from the
                      SAME expression the button renders (S315). Without the
                      aria-label the `title` won the accessible name, so a
                      screen reader announced the help sentence while the eye
                      read "unsound" — a name and its label drifting apart at
                      the one control where the word is the whole point. */}
                  {ADVICE.map((a) => (
                    <button key={a.key}
                            className={`btn btn-sm${advice === a.key ? ' active' : ''}`}
                            aria-label={a.label}
                            title={a.help}
                            aria-pressed={advice === a.key}
                            onClick={() => setAdvice(a.key)}>
                      {a.label}
                    </button>
                  ))}
                </div>
                <p className="caption mt-2">
                  {ADVICE.find((a) => a.key === advice).help}
                </p>
                <textarea
                  className="w-full mt-2"
                  rows={3}
                  aria-label="Your reasoning"
                  placeholder="Your reasoning — this is recorded on the ticket permanently."
                  value={reasoning}
                  onChange={(e) => setReasoning(e.target.value)} />
                <div className="mt-2 flex gap-2">
                  <button className="btn btn-sm" disabled={busyAnswer}
                          onClick={() => answer(row)}>
                    {busyAnswer ? 'recording…' : 'record this opinion'}
                  </button>
                  <button className="btn btn-sm" onClick={() => setDrafting(null)}>
                    cancel
                  </button>
                </div>
              </div>
            )}

            {waiving && (
              <div className="mt-3">
                <p className="caption">
                  A waiver is on the record for ever, with your name on it. Say
                  why the ticket cannot wait for an answer.
                </p>
                <textarea
                  className="w-full mt-2"
                  rows={2}
                  aria-label="Why this consultation is being waived"
                  placeholder="Why this cannot wait — required."
                  value={waiverReason}
                  onChange={(e) => setWaiverReason(e.target.value)} />
                <div className="mt-2 flex gap-2">
                  <button className="btn btn-sm"
                          disabled={busyWaive || !waiverReason.trim()}
                          onClick={() => waive(row)}>
                    {busyWaive ? 'recording…' : 'waive, on the record'}
                  </button>
                  <button className="btn btn-sm" onClick={() => setDrafting(null)}>
                    cancel
                  </button>
                </div>
              </div>
            )}
          </div>
        );
      })}

      {isLegal && <ReferTicket onReferred={() => pane.reload()} />}
      {isLegal && <WhatTheModelSuggested />}

      <p className="caption mt-3">
        What is gated is whether somebody was <em>asked</em>, never what they
        said. A required consultation must be answered or waived before its
        ticket can be verified; an answer of “unsound” passes that gate exactly
        as “sound” does, and a decision taken against expert advice stays
        visible on the record.
      </p>
    </div>
  );
}

// ── Referring a ticket, with the model's suggestion beside the rules ───────
//
// THE RULES ANSWER WHETHER OR NOT THE MODEL DOES, and this component is where
// that shows. `rule_routes` comes back on every path — spent budget, dead
// provider, no key installed — so there is no state in which this screen has
// nothing to say. The model's suggestions are drawn SEPARATELY and labelled as
// suggestions, because merging the two lists would be the screen quietly
// promoting an opinion to a rule.
function ReferTicket({ onReferred }) {
  const tickets = usePane(() => API.waitingTickets());
  const acts = useActs();
  const [ticketId, setTicketId] = usePanelState('');
  const [routing, setRouting] = usePanelState(null);
  const [refused, setRefused] = usePanelState(null);

  const pending = usePanelMemo(
    () => (tickets.rows || []).filter((t) => t.state === 'pending'),
    [tickets.rows]);

  const look = () => acts.run('route', async () => {
    setRefused(null);
    const r = await API.panelRoute({ ticket_id: ticketId });
    if (!r.ok) { setRouting(null); setRefused(r.reason); return; }
    setRouting(r.body);
  });

  const ask = (discipline, question) => acts.run(`ask-${discipline}`, async () => {
    const r = await API.consult({
      ticket_id: ticketId, discipline_key: discipline, question,
    });
    if (!r.ok) { setRefused(r.reason); return; }
    setRefused(null);
    // The routing answer is already retained by the service. Asking the model
    // again merely to repaint "already asked" spent twice on one referral and
    // could give the same unchanged ticket a different suggestion. The act we
    // just completed is enough to update that one local fact.
    setRouting((current) => current ? {
      ...current,
      already_asked: [...new Set([...(current.already_asked || []), discipline])],
    } : current);
    onReferred();
  });

  return (
    <div className="panel mt-4">
      <PanelHead
        title="Refer a ticket"
        sub="Which experts should see this — from the routing rules, and from a model reading the words." />

      {tickets.status === 'failed' && <LoadFailed reason={tickets.reason} />}

      <div className="flex items-end gap-2 flex-wrap">
        <label style={{ display: 'block' }}>
          <span className="caption">Pending ticket</span>
          <select className="block" aria-label="Pending ticket to refer"
                  value={ticketId}
                  onChange={(e) => { setTicketId(e.target.value); setRouting(null); }}>
            <option value="">choose a ticket…</option>
            {/* #153. A referral is legitimate for all three shapes — a
                proposed predicate can need an expert exactly as wording can —
                so nothing is filtered out here. What was missing is that the
                picker could not say which was which, and an option list has no
                room for a chip. Wording draws no suffix, as it draws no chip
                on the desk: it is what this queue is mostly made of. */}
            {pending.map((t) => {
              const kind = proposalKind(t);
              return (
                <option key={t.ticket_id} value={t.ticket_id}>
                  {t.ticket_id} · {t.category_key} · {t.severity}
                  {kind.key === 'clause' ? '' : ` · ${kind.label}`}
                </option>
              );
            })}
          </select>
        </label>
        <button className="btn btn-sm" disabled={!ticketId || acts.busy === 'route'}
                onClick={look}>
          {acts.busy === 'route' ? 'asking…' : 'who should see this?'}
        </button>
      </div>

      {!pending.length && tickets.status === 'loaded' && (
        <p className="caption mt-2">No ticket is pending — nothing to refer.</p>
      )}

      {refused && <Refused what="That was refused." reason={refused} />}

      {routing && (
        <div className="mt-3">
          <div className="section-label">The routing rules say</div>
          {routing.rule_routes.length ? (
            <ul className="mt-1">
              {routing.rule_routes.map((r) => {
                const asked = routing.already_asked.includes(r.discipline);
                return (
                  <li key={r.discipline} className="flex items-center gap-2 mt-1">
                    <span style={{ minWidth: 220 }}>{r.label}</span>
                    <Status state={r.necessity === 'required' ? 'pending' : 'neutral'}>
                      {r.necessity}
                    </Status>
                    {asked
                      ? <span className="caption">already asked</span>
                      : <button className="btn btn-sm"
                          disabled={acts.busy === `ask-${r.discipline}`}
                          onClick={() => ask(r.discipline,
                            `${r.label} review — referred by routing rule `
                            + `(${routing.ticket_id ? `ticket ${routing.ticket_id}, ` : ''}`
                            + `${r.necessity}). Please read the passage below.`)}>
                          ask {r.label}
                        </button>}
                  </li>
                );
              })}
            </ul>
          ) : (
            <p className="caption mt-1">
              No rule refers this category and severity anywhere. That is a
              statement about the rules, not about the clause.
            </p>
          )}

          <div className="section-label mt-4">A model also suggests</div>
          {/* AN ABSENCE IS AN OUTCOME, NOT A FAILURE. The reason is printed in
              the model's own place, so nobody reads a quiet panel as "the model
              had nothing to add" when the truth is "nobody asked it". */}
          {routing.outcome === 'absent' ? (
            <p className="caption mt-1">
              No suggestion: {routing.absent_reason}. The rules above answered
              on their own — they always do.
            </p>
          ) : routing.model_routes.length ? (
            <ul className="mt-1">
              {routing.model_routes.map((r) => {
                const asked = routing.already_asked.includes(r.discipline);
                return (
                  <li key={r.discipline} className="mt-2">
                    <div className="flex items-center gap-2">
                      <span style={{ minWidth: 220 }}>{r.discipline}</span>
                      <Status state="never" title="A suggestion. It gates nothing, and only a lawyer opens a consultation.">
                        suggestion
                      </Status>
                      {asked
                        ? <span className="caption">already asked</span>
                        : <button className="btn btn-sm"
                            disabled={acts.busy === `ask-${r.discipline}`}
                            onClick={() => ask(r.discipline, r.justification)}>
                            ask {r.discipline}
                          </button>}
                    </div>
                    <p className="caption">{r.justification}</p>
                  </li>
                );
              })}
            </ul>
          ) : (
            <p className="caption mt-1">
              The model was asked and suggested nothing beyond the rules.
            </p>
          )}

          <p className="caption mt-3">
            The model proposes; it opens nothing and decides nothing. A
            suggestion is always <em>advisory</em> — only the rules above make a
            consultation required, and only a lawyer asks.
          </p>
        </div>
      )}
    </div>
  );
}

// ── What became of what the model suggested (0091) ─────────────────────────
//
// THE GAP 0090 SHIPPED WITH, closed. A suggestion nobody acted on used to
// leave no trace at all, so the one figure that says whether the model is
// earning its keep could not be computed.
//
// THREE OUTCOMES, NOT TWO, and this component's whole job is to keep them
// apart on screen. "No consultation" means the lawyer may yet act (the ticket
// is still pending) OR that the moment passed (the ticket is decided). Drawing
// the first as declined would accuse somebody of a decision they never took —
// which is S312's defect, one screen over.
//
// COUNTS, NEVER A RATE. U4 settled how this system treats a number nobody has
// enough data for: measure it, show it, and let a person decide what it should
// be. A percentage printed beside four suggestions reads as a measurement when
// it is noise.
const SUGGESTION_OUTCOME = {
  taken:       { ink: 'effective', word: 'acted on',
                 help: 'A consultation was opened with this discipline.' },
  outstanding: { ink: 'pending', word: 'not yet',
                 help: 'The ticket is still pending — the lawyer may yet open one. '
                     + 'This is NOT a decision.' },
  declined:    { ink: 'superseded', word: 'not taken',
                 help: 'The ticket was decided without consulting this discipline.' },
};

function WhatTheModelSuggested() {
  const pane = usePane(() => API.panelSuggestions());
  const uptake = usePane(() => API.panelUptake());

  const filter = useListFilter(pane.rows, {
    view: 'routing:referrals',
    fields: ['ticket_id', 'discipline_key', 'discipline', 'justification',
             'proposed_by'],
    facet: 'outcome',
  });

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

  return (
    <div className="panel mt-4">
      <PanelHead
        title="What the model suggested"
        sub="Every routing suggestion, and whether a lawyer acted on it. A suggestion gates nothing — this is the record of what was offered and what was taken."
        right={<FilterCount filter={filter} />} />

      {/* Counts per discipline. Only disciplines that were actually suggested
          appear: a zero printed for a discipline the model has never mentioned
          would be stating a fact nobody measured — shell.jsx's rule about the
          navigation rack, kept here. */}
      {uptake.status === 'loaded' && uptake.rows.length > 0 && (
        <table className="ledger mt-2">
          <thead>
            <tr>
              <th>discipline</th><th>suggested</th><th>acted on</th>
              <th>not taken</th><th>not yet</th>
            </tr>
          </thead>
          <tbody>
            {uptake.rows.map((r) => (
              <tr key={r.discipline_key}>
                <td>{r.discipline}</td>
                <td>{r.suggested}</td>
                <td>{r.taken}</td>
                <td>{r.declined}</td>
                <td>{r.outstanding}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}

      <ListFilter filter={filter} testid="panel-suggestions"
                  placeholder="ticket, discipline, or reason"
                  facetLabel="every outcome" />

      {!filter.shown.length && (
        pane.rows.length
          ? <NoMatch kicker="suggestions" noun="suggestion" />
          : <Empty
              kicker="suggestions"
              line="No model has suggested a routing yet."
              sub="An empty record, not a failed read — the routing rules answer on their own." />
      )}

      <WaitingList
        order="oldest"
        items={filter.shown.map((r) => {
          const mark = SUGGESTION_OUTCOME[r.outcome] || SUGGESTION_OUTCOME.outstanding;
          return {
            key: r.suggestion_id,
            // SAY WHICH KIND OF REFERENCE THIS IS — one column carrying several
            // kinds of id is what made a bare `9` unreadable in S316.
            title: `ticket ${r.ticket_id} · ${r.discipline}`,
            sub: r.justification,
            at: r.proposed_at,
            chips: (
              <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}>
                <Status state={mark.ink} title={mark.help}>{mark.word}</Status>
              </span>
            ),
          };
        })}
        empty={null} />

      <p className="caption mt-3">
        “Not yet” and “not taken” are different facts: a ticket still pending
        may yet be referred, and only a ticket that was <em>decided</em> without
        the consultation records a choice. Nothing here is written when a lawyer
        declines — it is read from whether a consultation exists, so opening one
        later corrects the record on its own.
      </p>
    </div>
  );
}

// ── Who sits on the panel ──────────────────────────────────────────────────
function PanelSeatsPane({ me }) {
  const seats = usePane(() => API.panelSeats());
  const disciplines = usePane(() => API.panelDisciplines());
  const acts = useActs();
  const [person, setPerson] = usePanelState('');
  const [discipline, setDiscipline] = usePanelState('');
  const [refused, setRefused] = usePanelState(null);

  const filter = useListFilter(seats.rows, {
    view: 'panel:seats',
    fields: ['person', 'discipline', 'discipline_key', 'seated_by'],
    facet: 'discipline',
  });

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

  const done = (r) => {
    if (!r.ok) { setRefused(r.reason); return; }
    setRefused(null); setPerson('');
    seats.reload();
  };

  const seat = () => acts.run('seat', async () =>
    done(await API.seatExpert({ discipline_key: discipline, person: person.trim() })));

  const close = (row) => acts.run(`close-${row.seat_id}`, async () =>
    done(await API.closeSeat({ seat_id: row.seat_id })));

  const live = filter.shown.filter((s) => !s.closed_at);
  const closed = filter.shown.filter((s) => s.closed_at);

  return (
    <div>
      <PaneHead
        title="The expert panel"
        sub="Who may be asked, and about what. Seating somebody is an access grant — it says who may be asked, and confers no vote on any ticket."
        right={<FilterCount filter={filter} />} />

      <ListFilter filter={filter} testid="panel-seats"
                  placeholder="person or discipline"
                  facetLabel="every discipline" />

      {refused && <Refused what="That was refused." reason={refused} />}

      <div className="panel mb-3">
        <div className="flex items-end gap-2 flex-wrap">
          <label>
            <span className="caption">Person</span>
            <input className="block" aria-label="Person to seat"
                   placeholder="name@company.com"
                   value={person} onChange={(e) => setPerson(e.target.value)} />
          </label>
          <label>
            <span className="caption">Discipline</span>
            <select className="block" aria-label="Discipline for this seat"
                    value={discipline}
                    onChange={(e) => setDiscipline(e.target.value)}>
              <option value="">choose a discipline…</option>
              {(disciplines.rows || []).map((d) => (
                <option key={d.discipline_key} value={d.discipline_key}>{d.label}</option>
              ))}
            </select>
          </label>
          <button className="btn btn-sm"
                  disabled={!person.trim() || !discipline || acts.busy === 'seat'}
                  onClick={seat}>
            {acts.busy === 'seat' ? 'seating…' : 'seat this person'}
          </button>
        </div>
        {discipline && (disciplines.rows || []).length > 0 && (
          <p className="caption mt-2">
            {(disciplines.rows.find((d) => d.discipline_key === discipline) || {}).purpose}
          </p>
        )}
      </div>

      <WaitingList
        order="given"
        items={live.map((s) => ({
          key: s.seat_id,
          title: `${s.person} — ${s.discipline}`,
          sub: `seated by ${s.seated_by}`,
          at: s.seated_at,
          chips: (
            <button className="btn btn-sm"
                    disabled={acts.busy === `close-${s.seat_id}`}
                    onClick={() => close(s)}>
              {acts.busy === `close-${s.seat_id}` ? 'closing…' : 'close seat'}
            </button>
          ),
        }))}
        empty={<Empty kicker="the panel"
          line="Nobody sits on the panel yet."
          sub="Until somebody does, a required consultation has nobody to answer it — and a lawyer must waive it in writing to verify the ticket." />}
      />

      {/* CLOSED SEATS ARE KEPT AND SHOWN. That somebody could answer security
          questions last March does not stop being true when they leave, and a
          panel that quietly forgot its own history would break the provenance
          this whole feature exists to add. */}
      {closed.length > 0 && (
        <div className="mt-4">
          <div className="section-label">Closed seats</div>
          <WaitingList
            order="newest"
            items={closed.map((s) => ({
              key: s.seat_id,
              title: `${s.person} — ${s.discipline}`,
              sub: `closed by ${s.closed_by}`,
              at: s.closed_at,
              chips: <Status state="superseded">closed</Status>,
            }))}
            empty={null} />
        </div>
      )}

      <p className="caption mt-3">
        A seat says who may be <em>asked</em>. Which categories must be referred
        at all is a routing rule, and those belong to Legal — not to this
        console.
      </p>
    </div>
  );
}
