// TODAY — the desk every role opens on.
//
// WHY THIS PANE EXISTS. Until now every workspace opened straight into a
// working list: the requester into forty-eight deals, Legal into seventy-two
// tickets, the auditor into a chain of several hundred acts. Each of those is
// the right screen for the job it names and none of them is the right screen
// for the first ten seconds of a working day, which asks four questions in
// this order:
//
//   1. What is my job, and where do I begin? — one role-valid way in
//   2. What is waiting on ME?                 — the queue, and only mine
//   3. Where is the work sitting?             — my workspace, counted
//   4. How does the whole thing look?         — one composition at a glance
//
// The four bands below are those four questions. The Administrator alone gets
// one extra band between the map and the queue: a setup checklist made from
// the SAME reads as People, Settings and Health. It is not a second admin
// console and performs no act; every measured row opens the pane that owns the
// evidence. The one deliberately unmeasured row says so instead of turning
// absent evidence into a green mark.
//
// THE RULE THIS PANE IS BUILT TO KEEP. Every figure on it is either a control
// that reaches the exact set it counted, or it is not drawn at all. There is no
// third state — `PRODUCT.md` §4 item 7 is the register of what happens when a
// desk states numbers and leads nowhere, and this is the one page in the
// application where that failure would be the whole page.
//
// AND EVERY NUMBER IS SOMEBODY ELSE'S DERIVATION. This pane computes no count
// of its own. The area cards read `useRailCounts` — the same hook, the same
// endpoints, the same narrowing the navigation rack above them prints — so the
// card and the tab cannot disagree. A second count here, however carefully
// written, is a second count that will eventually be wrong on one of the two
// screens and right on the other, with nothing to say which.
//
// NO NEW ENDPOINT AND NO NEW GRANT. Everything drawn here is read through a
// role-scoped endpoint the role already holds; a role that cannot read a thing
// simply has no card for it, because its tab set has no tab for it either.

const { useState, useMemo } = React;

// ── What each area of a workspace is FOR, in one line ────────────────────
//
// The tab rail gives an area a two-word label and a number. That is enough
// once you know the product and useless on your first morning, which is
// exactly who a home page is for. This is the sentence that turns
// `holds · 3` into something a person can decide to click.
//
// ONE ENTRY PER TAB KEY, and `db/test/the-desk-you-open-on.test.mjs` fails by
// name when a workspace gains a tab this table cannot describe. That guard is
// the point of the table: a rule with one site per tab is a rule this
// repository has repeatedly kept at SOME of its sites.
const AREA_NOTE = {
  // Requester
  'my-deals':      'Your engagements, their stage, and the paper on each.',
  'intake':        'Describe a purchase and get a manifest the forge can build from.',
  'negotiate':     'Supplier paper, round by round, against approved positions.',
  'deal-room':     'The negotiation record with the conversation beside it.',
  'vendors':       'Which suppliers argue about what, and how long it costs.',
  'suppliers':     'Who we hold paper with, and which deals are attached to each.',
  'my-record':     'Every act you took, as the record holds it.',
  // Shared working areas
  'obligations':   'What was promised, when it falls due, and who owes it.',
  'sourcing':      'RFPs and RFQs, and the draft terms that ride along with them.',
  'competitions':  'Going to market: who is invited to bid, and which version of the paper each of them holds.',
  'departures':    'Where a statement of work is allowed to differ from its master.',
  'notices':       'Messages you raised, and messages raised to you.',
  // Legal
  'review-desk':   'Everything waiting on Legal judgement, oldest first.',
  'tickets':       'Quarantined text, badged by where it came from.',
  'builder':       'What the machine proposed, and the lawyer who decides it.',
  'routing':       'Which lawyer a ticket goes to, and why.',
  'consultations': 'Which discipline was asked, what they said, what was waived.',
  'approvals':     'Override requests, decided finding by finding.',
  'negotiations':  'Every live negotiation, and the points contested on each.',
  'holds':         'Work stopped, and what is stopping it.',
  'library':       'The approved wording, its versions and its provenance.',
  'ladders':       'Fallback ladders and the rules that check a build.',
  'governance':    'Categories, owner decisions, and the switches behind them.',
  'retention':     'Legal holds, and records whose retention has run.',
  'reporting':     'How the desk is performing, and where it is loaded.',
  'portfolio':     'How many of our contracts stand on each clause, and where the library had no answer.',
  'ai-use':        'Every question put to a model, what came back, and what it cost.',
  // Auditor
  'the-record':    'The hash-chained record of every governed act.',
  'quality':       'How much of a draft survived to approval, and where.',
  'origin-mix':    'Where the approved wording came from, by character.',
  'access-history':'Who granted whom what, and who countersigned it.',
  // Viewer
  'reading-room':  'The agreements shared with you, and nothing else.',
  // Administrator
  'people':        'Accounts, grants, and the countersign that makes one real.',
  'settings':      'The operational settings this company runs on.',
  'health':        'Whether the record is intact, and the checks that say so.',
  'watchers':      'Who is told when something is socialised.',
  'panel':         'Who holds an expert seat, in which discipline.',
  'workflow':      'The switches that decide how this company works.',
  'onboarding':    'Load, verify and apply this company’s workflow setup.',
  'contract-onboarding': 'Bring existing templates and signed contracts into the system.',
};

// The job each role came to finish. The workspace map below answers "where
// can I go?" This answers the two questions that come first on somebody's
// first morning: "what is my job here?" and "where do I begin?" It remains a
// map, not a second place to take a governed act. Every destination is a tab
// the role already holds, and the test derives that fact from WORKSPACES.
//
// These are journey LANDMARKS, not completion marks. A static screen cannot
// honestly say that a particular deal has completed Intake without choosing a
// deal and reading its runs. The record-specific panes own that progress.
const ROLE_GUIDE = {
  requester: {
    name: 'Requester',
    purpose: 'Describe what you need to buy, build it from approved terms, and carry it through signature and its promises.',
    primary: { label: 'Open your engagements', to: 'my-deals' },
    steps: [
      { label: 'Open', to: 'my-deals' },
      { label: 'Describe', to: 'intake' },
      { label: 'Assemble', to: 'my-deals' },
      { label: 'Negotiate', to: 'deal-room' },
      { label: 'Sign', to: 'deal-room' },
      { label: 'Keep promises', to: 'obligations' },
    ],
  },
  procurement: {
    name: 'Procurement',
    purpose: 'Curate what suppliers are asked, run the competition, compare the replies, and record the award.',
    primary: { label: 'Open competitions', to: 'competitions' },
    steps: [
      { label: 'Suppliers', to: 'competitions' },
      { label: 'Build the paper', to: 'sourcing' },
      { label: 'Issue & receive', to: 'competitions' },
      { label: 'Evaluate & award', to: 'competitions' },
    ],
  },
  legal_reviewer: {
    name: 'Legal reviewer',
    purpose: 'Decide whether proposed wording becomes company language and whether a recorded risk may be accepted.',
    primary: { label: 'Open the review desk', to: 'review-desk' },
    steps: [
      { label: 'Triage', to: 'review-desk' },
      { label: 'Review wording', to: 'tickets' },
      { label: 'Ask an expert', to: 'consultations' },
      { label: 'Decide risk', to: 'approvals' },
      { label: 'Negotiate', to: 'negotiations' },
    ],
  },
  legal_admin: {
    name: 'Legal administrator',
    purpose: 'Own the company’s approved language, its fallback positions, and the rules that decide what may be assembled.',
    primary: { label: 'Open the review desk', to: 'review-desk' },
    steps: [
      { label: 'Triage', to: 'review-desk' },
      { label: 'Decide', to: 'approvals' },
      { label: 'Maintain language', to: 'library' },
      { label: 'Maintain rules', to: 'ladders' },
      { label: 'Watch outcomes', to: 'reporting' },
    ],
  },
  administrator: {
    name: 'Administrator',
    purpose: 'Run access and operations without deciding the legal or commercial answer inside a workflow.',
    primary: { label: 'Open people and access', to: 'people' },
    steps: [
      { label: 'Create people', to: 'people' },
      { label: 'Grant access', to: 'people' },
      { label: 'Design workflow', to: 'workflow' },
      { label: 'Set operations', to: 'settings' },
      { label: 'Check health', to: 'health' },
    ],
  },
  auditor: {
    name: 'Auditor',
    purpose: 'Read the complete record, change nothing, and prove who did what and whether the chain still verifies.',
    primary: { label: 'Open the governed record', to: 'the-record' },
    steps: [
      { label: 'Read the chain', to: 'the-record' },
      { label: 'Check provenance', to: 'origin-mix' },
      { label: 'Check access', to: 'access-history' },
      { label: 'Take evidence', to: 'the-record' },
    ],
  },
  viewer: {
    name: 'Viewer',
    purpose: 'Read only the agreements and consultations shared with you, without changing the company’s record.',
    primary: { label: 'Open the reading room', to: 'reading-room' },
    steps: [
      { label: 'Read shared paper', to: 'reading-room' },
      { label: 'Read a consultation', to: 'consultations' },
      { label: 'Read messages', to: 'notices' },
    ],
  },
};

function RoleGuideBand({ me }) {
  const guide = ROLE_GUIDE[me.role];
  if (!guide) return null;
  const go = (to) => { window.location.hash = `#/${to}`; };

  return (
    <section className="desk-band" data-testid="role-guide">
      <div className="role-guide">
        <div className="role-guide-purpose">
          <div className="section-label">Your job as {guide.name}</div>
          <p>{guide.purpose}</p>
          <button type="button" className="btn btn-primary role-guide-primary"
                  onClick={() => go(guide.primary.to)}>
            {guide.primary.label} <span aria-hidden="true">→</span>
          </button>
        </div>
        <div>
          <div className="section-label">How your work moves</div>
          <ol className="journey-guide" aria-label={`${guide.name} journey`}>
            {guide.steps.map((step, i) => (
              <li key={`${step.to}-${i}`}>
                <button type="button" onClick={() => go(step.to)}>
                  <span className="journey-number" aria-hidden="true">{i + 1}</span>
                  <span>{step.label}</span>
                </button>
              </li>
            ))}
          </ol>
          <p className="caption mt-2">
            Open a landmark to see where a particular record stands. The
            permanent record, not this map, decides what is complete.
          </p>
        </div>
      </div>
    </section>
  );
}

// ── The Administrator's first-customer setup ─────────────────────────────
//
// These seven areas come directly from the customer-readiness plan. Keep the
// list explicit: silently dropping an area because no read exists is the exact
// opposite of a readiness checklist. Integrations therefore remains present
// as UNVERIFIED until functional provider probes exist.
const ADMIN_SETUP_KEYS = [
  'people', 'grants', 'addresses', 'workflow', 'sourcing', 'integrations', 'health',
];
const ADMIN_SETUP_META = {
  people:       { label: 'Named people', owner: 'Administrator', to: 'people' },
  grants:       { label: 'Role grants', owner: 'Administrator · Legal countersigns', to: 'people' },
  addresses:    { label: 'Notification addresses', owner: 'Administrator', to: 'people' },
  workflow:     { label: 'Workflow settings', owner: 'Legal administrator', to: 'settings' },
  sourcing:     { label: 'Sourcing library', owner: 'Procurement', to: 'sourcing' },
  integrations: { label: 'Integrations', owner: 'Administrator' },
  health:       { label: 'System health', owner: 'Administrator', to: 'health' },
};

function SetupMark({ state }) {
  if (state === 'ready') return <span className="chip chip-ok">ready</span>;
  if (state === 'attention') return <span className="chip chip-pending">needs attention</span>;
  if (state === 'reading') return <span className="chip chip-unknown">checking</span>;
  return <span className="chip chip-unknown">not verified</span>;
}

function AdministratorSetupBand({ people }) {
  // Six existing, role-scoped reads. The people evidence is the SAME pane
  // Home already loaded for the Administrator composition below; the other
  // four are the owning panes' reads. No endpoint, grant or count has been
  // invented for this checklist.
  const grants = usePane(() => API.countersignQueue());
  const addresses = usePane(() => API.notificationAddresses());
  const settings = usePane(() => API.settings());
  const sourcing = usePane(() => API.sourcingSections());
  const health = usePane(() => API.health());

  const reading = [people, grants, addresses, settings, sourcing, health]
    .some((pane) => pane.status === 'loading');

  const measured = (pane, answer) => {
    if (pane.status === 'loading') return { state: 'reading' };
    if (pane.status === 'failed') {
      return {
        state: 'unknown',
        detail: `Clausewerk could not read this evidence: ${pane.reason}`,
      };
    }
    return answer(pane.rows);
  };

  const livePeople = people.status === 'loaded'
    ? people.rows.filter((person) => person.effective_role)
    : [];

  const rows = {
    people: measured(people, (items) => {
      const active = items.filter((person) => person.effective_role);
      const roles = new Set(active.map((person) => person.effective_role));
      return {
        state: active.length > 0 ? 'ready' : 'attention',
        detail: active.length > 0
          ? `${active.length} named ${active.length === 1 ? 'person has' : 'people have'} access across ${roles.size === 7 ? 'all seven roles' : `${roles.size} of 7 roles`}.`
          : 'No named person has effective access.',
      };
    }),
    grants: measured(grants, (items) => ({
      state: items.length === 0 ? 'ready' : 'attention',
      detail: items.length === 0
        ? 'No Legal grant is waiting for its required countersign.'
        : `${items.length} Legal ${items.length === 1 ? 'grant is' : 'grants are'} still waiting for countersign.`,
    })),
    addresses: measured(addresses, (items) => {
      const reachable = new Set(items.filter((item) => !item.removed_at)
        .map((item) => item.person));
      const missing = livePeople.filter((person) => !reachable.has(person.person));
      return {
        state: missing.length === 0 && livePeople.length > 0 ? 'ready' : 'attention',
        detail: livePeople.length === 0
          ? 'People must be set up before delivery addresses can be checked.'
          : missing.length === 0
            ? `All ${livePeople.length} people with access have a live delivery address.`
            : `${missing.length} of ${livePeople.length} people with access cannot receive notices.`,
      };
    }),
    workflow: measured(settings, (items) => {
      const decisions = items.filter((item) => item.kind === 'owner_decision');
      const open = decisions.filter((item) => !item.decided);
      return {
        state: decisions.length > 0 && open.length === 0 ? 'ready' : 'attention',
        detail: decisions.length === 0
          ? 'No workflow decisions were returned, so setup cannot be verified.'
          : open.length === 0
          ? `All ${decisions.length} workflow owner decisions are recorded.`
          : `${open.length} workflow ${open.length === 1 ? 'decision is' : 'decisions are'} still provisional; Legal admin owns the answer.`,
      };
    }),
    sourcing: measured(sourcing, (items) => ({
      // The selectable-section read is already the product's definition of
      // what can be assembled today. One row is therefore evidence that the
      // library can supply a build; zero is the existing pane's honest gap.
      state: items.length > 0 ? 'ready' : 'attention',
      detail: items.length > 0
        ? `${items.length} approved sourcing ${items.length === 1 ? 'section is' : 'sections are'} available to assemble today.`
        : 'No sourcing wording can be assembled yet; Procurement owns this library.',
    })),
    integrations: {
      state: 'unknown',
      detail: 'Clausewerk does not yet expose a functional probe for every provider. Configuration alone is not proof that an integration works.',
    },
    health: measured(health, (items) => {
      const attention = items.filter((item) =>
        !['pass', 'none due'].includes(item.state));
      return {
        state: items.length > 0 && attention.length === 0 ? 'ready' : 'attention',
        detail: items.length === 0
          ? 'No health evidence was returned.'
          : attention.length === 0
            ? `All ${items.length} health readings currently pass.`
            : `${attention.length} of ${items.length} health ${attention.length === 1 ? 'reading needs' : 'readings need'} attention.`,
      };
    }),
  };

  const ready = ADMIN_SETUP_KEYS.filter((key) => rows[key].state === 'ready').length;
  const reload = () => Promise.all([
    people.reload(), grants.reload(), addresses.reload(), settings.reload(),
    sourcing.reload(), health.reload(),
  ]);

  return (
    <section className="desk-band" data-testid="administrator-setup">
      <PanelHead
        title="Customer setup"
        sub="The evidence needed before people can rely on this instance. Nothing is marked ready unless Clausewerk can read the proof."
        right={(
          <button type="button" className="kind-chip" onClick={reload}
                  data-testid="setup-read-again">
            read setup again
          </button>
        )} />

      <div className="setup-summary" aria-live="polite">
        {reading
          ? 'Checking the six kinds of evidence this account can read…'
          : `${ready} of ${ADMIN_SETUP_KEYS.length} areas have verifiable ready evidence here.`}
      </div>

      <div className="setup-grid">
        {ADMIN_SETUP_KEYS.map((key) => {
          const meta = ADMIN_SETUP_META[key];
          const row = rows[key];
          return (
            <div className="setup-card" key={key} data-setup={key}>
              <div className="setup-card-head">
                <div>
                  <div className="area-label">{meta.label}</div>
                  <div className="caption mt-0.5">Owner · {meta.owner}</div>
                </div>
                <SetupMark state={row.state} />
              </div>
              <p>{row.detail || 'Clausewerk is reading this evidence.'}</p>
              {meta.to && (
                <button type="button" className="kind-chip"
                        onClick={() => { window.location.hash = `#/${meta.to}`; }}>
                  open {meta.label.toLowerCase()}
                </button>
              )}
            </div>
          );
        })}
      </div>
    </section>
  );
}

// ── The one composition each role can read at a glance ────────────────────
//
// A dashboard's third band is where invention usually gets in: a chart looks
// like insight whether or not anything behind it was measured. So there is
// exactly ONE per role, it is drawn from a read that role provably holds
// (because the role also holds a tab onto it), and a role with no honest
// composition gets NO BAND rather than a decorated one. The viewer is that
// role, deliberately — their desk is three tabs wide and a chart of it would
// be furniture.
//
// `by` names the field the bar is grouped on and the grouping is DYNAMIC. A
// value the record gains appears as its own segment the day it arrives, rather
// than being silently swept into an "other" the reader cannot see. That is the
// same lesson as the audit record's machine figure: an allow-list stops
// counting the moment somebody adds a kind, and nothing says so.
//
// `weigh` is for the one composition that is not about how MANY: the library's
// origin mix is about how MUCH TEXT, because one long AI-drafted clause and one
// short one are not the same amount of machine authorship. The auditor's own
// pane already says exactly that, and this reads the same column.
const COMPOSITION = {
  requester: {
    title: 'Your portfolio',
    sub: 'Every deal you own, by where the record says it stands.',
    read: () => API.deals(),
    by: (d) => d.status,
    noun: 'deals',
    to: 'my-deals',
    empty: 'You own no deals yet, so there is nothing to compose.',
  },
  legal_reviewer: {
    title: 'The review queue',
    sub: 'Every ticket you can see, by what has been decided about it.',
    read: () => API.tickets(),
    by: (t) => t.state,
    noun: 'tickets',
    to: 'tickets',
    empty: 'No tickets have been raised, so there is nothing to compose.',
  },
  legal_admin: {
    title: 'The review queue',
    sub: 'Every ticket in the company, by what has been decided about it.',
    read: () => API.tickets(),
    by: (t) => t.state,
    noun: 'tickets',
    // The Legal admin holds no `tickets` tab; the desk is where they read it.
    to: 'review-desk',
    empty: 'No tickets have been raised, so there is nothing to compose.',
  },
  auditor: {
    title: 'Where the library’s language came from',
    sub: 'By character, not by clause — the AI-provenance figure this product exists to keep checkable.',
    read: () => API.originMix(),
    by: (r) => r.origin,
    weigh: (r) => Number(r.characters ?? 0),
    noun: 'characters',
    to: 'origin-mix',
    empty: 'The library holds no clause versions, so there is no origin to report.',
  },
  administrator: {
    title: 'The people on this instance',
    sub: 'Every account, by whether it is being used.',
    read: () => API.peopleActivity(),
    by: (p) => p.activity_state,
    noun: 'accounts',
    to: 'people',
    empty: 'There are no accounts on this instance.',
  },
};

// Which ink a segment wears. Three tones exist on this desk and no more — a
// fourth would be a colour carrying a meaning nothing else on the screen
// carries. Anything this map does not name is `wait`: neutral, and never
// green, because reading "fine" into a value nobody classified is the way a
// composition starts lying.
const BAND_TONE = {
  executed: 'ok', verified: 'ok', legal_authored: 'ok', active: 'ok',
  negotiating: 'warn', pending: 'warn', dormant: 'warn', model_drafted: 'warn',
  terminated: 'wait', rejected: 'wait', revoked: 'wait', external: 'wait',
};

// ── Band one: what is waiting on you ─────────────────────────────────────
//
// The same `cw.waiting_for()` derivation the notification digest reads, so the
// screen and the email cannot disagree about what somebody owes. The
// obligations book already renders it; this renders it NARROWABLE, because the
// question a home page is asked is "what is waiting on me" and the question it
// is asked ten seconds later is "show me just the overdue ones".
//
// THE CHIP COUNTS ARE TAKEN OFF THE WHOLE QUEUE, never off what is shown. A
// count computed from the narrowed set becomes its own total the moment it is
// pressed — press "an obligation is due · 3" and it reads 3 of 3 rather than
// 3 of 19, and pressing it again cannot get you back. That is a real bug this
// application has had before, in the audit record's machine figure.
// How many waiting rows a HOME page shows before it stops being a home page.
// Measured on the demonstration database: the requester's queue is 22 rows and
// drew a first band 1,435px tall, which put the other two questions this page
// exists to answer entirely below the fold. Eight is what fits above it beside
// the chip row on a 900px-tall window.
//
// THE CAP IS NOT A FILTER AND MUST NOT BE SILENT. It is said in words under the
// list, with the control that lifts it, because a list that quietly stops at
// eight is a list somebody trusts to be everything — and this is the one page
// in the application where that trust is the whole design.
const FIRST_FEW = 8;

// ── THE THREE WAYS A DESK IS READ, and why the default is not "oldest" ────
//
// `cw.waiting_for()` answers TWO different quantities — a DEADLINE on two of
// its twelve kinds, a START on the rest — and `WaitingList`'s `oldest` sorts on
// the start alone, putting every row that has only a deadline at the back. On
// the demonstration database that is the wrong way up: the requester's queue is
// three envelopes with a start date and nineteen obligations with a due date,
// SEVENTEEN OF THEM ALREADY PAST IT, and "longest waiting first" showed the
// three envelopes and pushed every overdue obligation below the cut.
//
// So the default is PRESSING, and pressing is defined rather than felt:
//
//   0  past its date        — most days overdue first
//   1  a date still to come — soonest first
//   2  no date, just waiting — longest waiting first
//   3  no date at all        — last, in the order the record gave them
//
// It is passed to `WaitingList` as `given`, which is exactly what that order
// means — "a domain order that is not time: ranked by share, by friction, BY
// DUE DATE, by round" — and nothing is marked as the longest wait, because
// under this ranking the top row is not the longest wait and a mark saying so
// would be false.
//
// THE OTHER TWO ARE KEPT AND ARE NOT DECORATION. "Longest waiting first" is the
// question a queue is normally read with, and "most recent first" is how
// somebody checks what has just landed. Which one a person wants is theirs, and
// the answer is on screen in words rather than implied by the order of rows.
const HOME_ORDERS = {
  pressing: { label: 'most pressing first', list: 'given' },
  oldest:   { label: 'longest waiting first', list: 'oldest' },
  newest:   { label: 'most recent first',   list: 'newest' },
};

// Read THROUGH the shared derivation — `waitingWhen` is what decides whether a
// row carries a deadline or a start, and a second opinion about that here is
// how the home page and the obligations book come to disagree about what a date
// means. What this adds that the shared one does not expose is the SIGNED
// number of days against a deadline: `waitingWhen` clamps it at zero because
// its job is to say "overdue" or not, and a ranking needs to tell a thing due
// tomorrow from a thing due next quarter.
function pressureOf(w) {
  const when = waitingWhen(w);
  if (when.sort === 'due') {
    const at = Date.parse(`${when.exact}T00:00:00Z`);
    const now = new Date();
    const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
    const days = Number.isFinite(at) ? Math.round((today - at) / 86400000) : 0;
    return { tier: days > 0 ? 0 : 1, n: days };
  }
  if (when.sort === 'since') {
    const t = Date.parse(when.exact);
    return { tier: 2, n: Number.isFinite(t) ? Date.now() - t : 0 };
  }
  return { tier: 3, n: 0 };
}

function WaitingBand({ me, waiting }) {
  const [kind, setKind] = useState(null);
  const [order, setOrder] = useState('pressing');
  const [all, setAll] = useState(false);

  const rows = waiting.rows ?? [];

  // Grouped over the WHOLE queue. Sorted by how many are waiting, because a
  // chip row is read left to right and the biggest pile is the one somebody
  // came here to find.
  const kinds = useMemo(() => {
    const n = new Map();
    for (const w of rows) n.set(w.kind, (n.get(w.kind) ?? 0) + 1);
    return [...n.entries()].sort((a, b) => b[1] - a[1]);
  }, [rows]);

  // Overdue is a MEASURED fact — `waitingWhen` says so from the record's own
  // due date — and it is the one narrowing worth offering beside the kinds,
  // because it crosses them. It is drawn only when there is something overdue;
  // a chip reading `0 overdue` is a control that leads to an empty list.
  const overdue = rows.filter((w) => waitingWhen(w).overdueDays > 0);

  const shown = kind === 'overdue'
    ? overdue
    : (kind ? rows.filter((w) => w.kind === kind) : rows);

  // Whether a row can be opened at all depends on this role holding the pane
  // it would open. Affordances, not permissions — the database refuses
  // regardless, and a link landing on a refusal is worse than no link.
  const canOpenDeals = (WORKSPACES[me.role] || { tabs: [] })
    .tabs.some((t) => t.key === 'my-deals');

  // ORDERED FIRST, THEN CUT. "The first eight" has to mean the eight at the top
  // of the order somebody chose, not the eight the endpoint happened to return
  // first — and `WaitingList` orders internally, so cutting before handing it
  // over would cut the wrong eight and then sort the remainder into a
  // thoroughly convincing order. Two of the three orders are the SHARED
  // derivation, asked here for the same answer the list will reach on its own;
  // the third is `pressing`, which this pane ranks and then hands over as
  // `given` so the list leaves it alone.
  const capped = (items) => {
    const ordered = order === 'pressing'
      ? [...items].sort((a, b) => a.press.tier - b.press.tier || b.press.n - a.press.n)
      : orderWaiting(items, HOME_ORDERS[order].list).rows;
    return all ? ordered : ordered.slice(0, FIRST_FEW);
  };

  if (waiting.status === 'failed') {
    return (
      <section className="desk-band">
        <PanelHead title="Waiting on you"
                   sub="The same derivation the notification digest reads." />
        <LoadFailed reason={waiting.reason} />
      </section>
    );
  }

  return (
    <section className="desk-band">
      <PanelHead
        title="Waiting on you"
        sub="The same derivation the notification digest reads — the screen and the email cannot disagree."
        right={rows.length > 1 && (
          <label className="flex items-center gap-2">
            <span className="section-label">order</span>
            <select value={order} onChange={(e) => setOrder(e.target.value)}
                    data-testid="waiting-order">
              {Object.entries(HOME_ORDERS).map(([k, o]) => (
                <option key={k} value={k}>{o.label}</option>
              ))}
            </select>
          </label>
        )} />

      {rows.length === 0 ? (
        <Empty
          kicker="clear"
          line="Nothing is waiting on you."
          sub="When something is — a due obligation, a socialised override, an
               expert opinion somebody asked of you — it appears here and in the
               digest, from one derivation." />
      ) : (
        <>
          <div className="kind-row" data-testid="waiting-kinds">
            <button type="button"
                    className={`kind-chip${kind === null ? ' on' : ''}`}
                    aria-pressed={kind === null}
                    onClick={() => setKind(null)}>
              everything <b>{rows.length}</b>
            </button>
            {overdue.length > 0 && (
              <button type="button"
                      className={`kind-chip kind-chip--late${kind === 'overdue' ? ' on' : ''}`}
                      aria-pressed={kind === 'overdue'}
                      onClick={() => setKind('overdue')}>
                past its date <b>{overdue.length}</b>
              </button>
            )}
            {kinds.map(([k, n]) => (
              <button type="button" key={k}
                      className={`kind-chip${kind === k ? ' on' : ''}`}
                      aria-pressed={kind === k}
                      onClick={() => setKind(kind === k ? null : k)}>
                {WAITING_KINDS[k] ?? k} <b>{n}</b>
              </button>
            ))}
          </div>

          {/* SAID OUT LOUD WHEN IT IS NARROWED. A list showing a subset
              without saying which subset is the defect the filter row exists
              to close, and it is worse on a home page than anywhere else —
              this is the list somebody trusts to be everything. */}
          {kind !== null && (
            <div className="caption mt-2" data-testid="waiting-narrowed">
              Showing {shown.length} of {rows.length}
              {kind === 'overdue' ? ' — past their date only'
                : ` — ${WAITING_KINDS[kind] ?? kind} only`}.
            </div>
          )}

          <div className="mt-3">
            <WaitingList
              order={HOME_ORDERS[order].list}
              items={capped(shown.map((w, i) => {
                const noun = WAITING_REF_KINDS[w.kind];
                // THE TWO DATES, TOLD APART BY THE SHARED DERIVATION and each
                // drawn ONCE. `cw.waiting_for()` answers a DEADLINE on two of
                // its twelve kinds and a START on the rest, and `WaitingList`
                // already renders a start as an age in its own column. So a
                // deadline goes in the chips, where the overdue mark belongs,
                // and a start does not — drawn in both places it reads
                // "waiting 3d … 3d", which is this pane telling somebody the
                // same thing twice and looking like two facts.
                const when = waitingWhen(w);
                // ONE KIND CARRIES A DEAL REFERENCE — `renewal_window` — and
                // it opens, where this role holds the pane that would open it.
                // Drawn as its own control rather than by making the whole row
                // pressable: obligations and review tickets share the control
                // below; remaining kinds have no destination. An inert row must
                // not look like a control.
                const deal = w.kind === 'renewal_window' && canOpenDeals
                  ? String(w.subject_ref) : null;
                return {
                  key: `${w.kind}-${w.subject_ref}-${i}`,
                  press: pressureOf(w),
                  title: WAITING_KINDS[w.kind] ?? w.kind,
                  sub: `${noun ? `${noun} ` : ''}${w.subject_ref}`,
                  at: when.sort === 'since' ? when.exact : null,
                  chips: (
                    <>
                      {when.sort === 'due' && <WaitingWhen row={w} />}
                      <WaitingRecordLink row={w} me={me} />
                      {deal && (
                        <button type="button" className="kind-chip"
                                onClick={() => {
                                  window.location.hash =
                                    `#/my-deals/${encodeURIComponent(deal)}`;
                                }}>
                          open {deal}
                        </button>
                      )}
                    </>
                  ),
                };
              }))}
              empty={<NoMatch kicker="waiting" noun="waiting rows" />} />
          </div>

          {/* THE CAP, SAID OUT LOUD, with the control that lifts it. */}
          {shown.length > FIRST_FEW && (
            <div className="flex items-center gap-3 mt-2">
              <button type="button" className="kind-chip"
                      data-testid="waiting-show-all"
                      aria-pressed={all}
                      onClick={() => setAll(!all)}>
                {all ? `show the first ${FIRST_FEW}` : `show all ${shown.length}`}
              </button>
              <span className="caption" data-testid="waiting-capped">
                {all
                  ? `All ${shown.length}, ${HOME_ORDERS[order].label}.`
                  : `Showing ${FIRST_FEW} of ${shown.length}, `
                    + `${HOME_ORDERS[order].label} — the whole queue is one `
                    + 'press away.'}
              </span>
            </div>
          )}
        </>
      )}
    </section>
  );
}

// ── Band two: the areas of this workspace, counted ───────────────────────
//
// THE MAP OF THE PRODUCT, for the role looking at it. Twelve tabs is past what
// a rail reads well at — `shell.jsx` has said so in a comment since 2026-08-05
// and the rail has grown twice since — and a rail cannot say what an area is
// for in any case. This is the same set of destinations with the sentence
// attached and the number beside it.
//
// THE NUMBER IS THE RAIL'S OWN. `useRailCounts` is the hook the navigation
// rack calls; this calls it too rather than counting anything itself, so the
// figure on the card and the figure on the tab are one derivation. An area the
// application has not measured shows NO NUMBER — never a zero, which would be
// a fact this page does not hold.
function AreasBand({ me, counts }) {
  // A DESK SOMEBODY CAN NARROW. Fourteen cards is the widest workspace and
  // most mornings only three of them have anything in them. Off by default,
  // because a map with things missing from it is not a map until you asked
  // for it to be.
  const [onlyWork, setOnlyWork] = useState(false);

  const tabs = (WORKSPACES[me.role] || { tabs: [] }).tabs
    .filter((t) => t.key !== 'home');
  // An area is "carrying work" only where a count was actually measured. An
  // unmeasured area is neither carrying work nor clear of it, so narrowing to
  // the ones with work must not sweep it away silently — it is kept, and the
  // count beside the control says how many are in that state.
  const measured = tabs.filter((t) => counts[t.key] !== undefined);
  const carrying = measured.filter((t) => counts[t.key] > 0);
  const unmeasured = tabs.length - measured.length;
  const shown = onlyWork
    ? tabs.filter((t) => counts[t.key] === undefined || counts[t.key] > 0)
    : tabs;

  return (
    <section className="desk-band">
      <PanelHead
        title="Your workspace"
        sub="Every area you hold, what it is for, and how much is in it."
        right={carrying.length > 0 && measured.length > carrying.length && (
          <button type="button"
                  className={`kind-chip${onlyWork ? ' on' : ''}`}
                  aria-pressed={onlyWork}
                  data-testid="areas-only-work"
                  onClick={() => setOnlyWork(!onlyWork)}>
            only areas carrying work <b>{carrying.length}</b>
          </button>
        )} />

      <div className="desk-grid" data-testid="area-grid">
        {shown.map((t) => {
          const n = counts[t.key];
          return (
            <div className="area-card" key={t.key} data-area={t.key}
                 {...openableRow(
                   () => { window.location.hash = `#/${t.key}`; },
                   `open ${t.label}`)}>
              <div className="area-head">
                <span className="area-label">{t.label}</span>
                {/* NO NUMBER IS NOT ZERO. Six of the areas below have no
                    endpoint whose row count is the honest answer to "how many
                    are waiting here", and printing 0 for them would be this
                    page stating a fact it never measured. */}
                <span className={`area-n${n === 0 ? ' none' : ''}`}>
                  {n === undefined ? '' : n}
                </span>
              </div>
              <div className="area-note">{AREA_NOTE[t.key]}</div>
            </div>
          );
        })}
      </div>

      {/* WHAT THE NARROWING ACTUALLY DID, and it is not what its label says on
          its own. The chip counts the areas CARRYING WORK; the grid also keeps
          every area nothing has measured, because an unmeasured area is neither
          carrying work nor clear of it and dropping it would be this control
          deciding a question the application has not answered. So the two
          numbers differ on purpose, and the difference is stated rather than
          left for somebody to notice. */}
      {onlyWork && (
        <div className="caption mt-2" data-testid="areas-narrowed">
          Showing {shown.length} of {tabs.length} — the {carrying.length} carrying
          work{unmeasured > 0 && <>, and the {unmeasured} nothing has measured</>}.
        </div>
      )}

      <p className="caption mt-3">
        A number here is the same count the tab above it prints, from the same
        read. An area with no number is one nothing has measured — which is not
        the same as an area with nothing in it.
      </p>
    </section>
  );
}

// ── Band three: one composition, or none ─────────────────────────────────
//
// Drawn as segments of a MEASURED TOTAL rather than as percentages, on the
// shared `BandBar` — each segment's width is its own count over the sum of the
// counts given, which is arithmetic the reader can check by looking. A role
// with no honest composition renders nothing at all.
function CompositionBand({ me, pane }) {
  const spec = COMPOSITION[me.role];
  if (!spec) return null;
  if (pane.status === 'loading') return <section className="desk-band"><Loading /></section>;
  if (pane.status === 'failed') {
    return (
      <section className="desk-band">
        <PanelHead title={spec.title} sub={spec.sub} />
        <LoadFailed reason={pane.reason} />
      </section>
    );
  }

  const weigh = spec.weigh ?? (() => 1);
  const tally = new Map();
  for (const row of pane.rows) {
    const k = spec.by(row);
    const key = k === null || k === undefined || k === '' ? 'unrecorded' : String(k);
    tally.set(key, (tally.get(key) ?? 0) + weigh(row));
  }
  const bands = [...tally.entries()]
    .sort((a, b) => b[1] - a[1])
    .map(([key, n]) => ({
      key, n,
      label: key.replace(/_/g, ' '),
      tone: BAND_TONE[key] ?? 'wait',
    }));
  const total = bands.reduce((n, b) => n + b.n, 0);

  return (
    <section className="desk-band">
      <PanelHead
        title={spec.title}
        sub={spec.sub}
        right={total > 0 && (
          <button type="button" className="kind-chip"
                  data-testid="composition-drill"
                  onClick={() => { window.location.hash = `#/${spec.to}`; }}>
            open the {spec.noun} <b>{total.toLocaleString()}</b>
          </button>
        )} />
      <BandBar bands={bands} total={total} empty={spec.empty} />
    </section>
  );
}

// ── The pane ─────────────────────────────────────────────────────────────
function HomePane({ me }) {
  const waiting = usePane(() => API.waiting());
  // THE RAIL'S OWN COUNTS, not a second set. See AreasBand.
  const counts = useRailCounts(me);
  const spec = COMPOSITION[me.role];
  // Called unconditionally, because a hook behind an `if` is the fault that
  // blanks a pane on the render after the data lands — the one render nobody
  // watches (S318). A role with no composition fetches nothing and the band
  // draws nothing.
  const composition = usePane(
    () => (spec ? spec.read() : Promise.resolve({ ok: true, rows: [] })),
    [me.role]);

  if (waiting.status === 'loading') return <Loading />;

  // THE DATE, AS THE KICKER. It was the workspace's name — "My deals" over a
  // page headed "Today", six inches under a masthead already saying "My deals",
  // which told a reader nothing twice. The date is what makes the word "today"
  // mean something, and it is the reader's own clock, which is the same clock
  // every age on this desk is measured against.
  const today = new Date().toLocaleDateString(undefined,
    { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });

  return (
    <div>
      <PaneHead
        title="Today"
        kicker={today}
        /* SAYS WHAT THE PAGE IS, not what the role is. `opensOn` sat here
           first and is written in the third person for the masthead — "Their
           engagements only…" — which on somebody's own desk reads as though it
           were about a colleague. */
        sub="What is waiting on you, where the work is sitting, and one picture of the whole."
        right={
          <ActButton className="btn btn-sm" data-testid="home-refresh"
                     onClick={async () => {
                       await Promise.all([waiting.reload(), composition.reload()]);
                     }}>
            read the desk again
          </ActButton>
        } />

      <RoleGuideBand me={me} />
      {me.role === 'administrator' && <AdministratorSetupBand people={composition} />}
      <WaitingBand me={me} waiting={waiting} />
      <AreasBand me={me} counts={counts} />
      <CompositionBand me={me} pane={composition} />
    </div>
  );
}
