<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recruitment desk — Larkspur Health</title>
<meta name="ext-resource-dependency" content="./assets/img/avatar.jpg" data-resource-id="avatar.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-01.jpg" data-resource-id="avatar-80-01.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-02.jpg" data-resource-id="avatar-80-02.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-03.jpg" data-resource-id="avatar-80-03.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-04.jpg" data-resource-id="avatar-80-04.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-05.jpg" data-resource-id="avatar-80-05.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-80-06.jpg" data-resource-id="avatar-80-06.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-1.jpg" data-resource-id="avatar-f-1.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-2.png" data-resource-id="avatar-f-2.png">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-3.jpg" data-resource-id="avatar-f-3.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-4.jpg" data-resource-id="avatar-f-4.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-5.jpg" data-resource-id="avatar-f-5.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-6.jpg" data-resource-id="avatar-f-6.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-7.jpg" data-resource-id="avatar-f-7.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-8.jpg" data-resource-id="avatar-f-8.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-9.jpg" data-resource-id="avatar-f-9.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-10.jpg" data-resource-id="avatar-f-10.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-11.jpg" data-resource-id="avatar-f-11.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-12.jpg" data-resource-id="avatar-f-12.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-13.jpg" data-resource-id="avatar-f-13.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-14.jpg" data-resource-id="avatar-f-14.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-15.jpg" data-resource-id="avatar-f-15.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-16.jpg" data-resource-id="avatar-f-16.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-17.jpg" data-resource-id="avatar-f-17.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-f-18.jpg" data-resource-id="avatar-f-18.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-1.jpg" data-resource-id="avatar-m-1.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-2.jpg" data-resource-id="avatar-m-2.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-3.jpg" data-resource-id="avatar-m-3.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-4.jpg" data-resource-id="avatar-m-4.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-5.jpg" data-resource-id="avatar-m-5.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-6.jpg" data-resource-id="avatar-m-6.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-7.jpg" data-resource-id="avatar-m-7.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-9.jpg" data-resource-id="avatar-m-9.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-10.jpg" data-resource-id="avatar-m-10.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-12.jpg" data-resource-id="avatar-m-12.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-13.jpg" data-resource-id="avatar-m-13.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-14.jpg" data-resource-id="avatar-m-14.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-15.jpg" data-resource-id="avatar-m-15.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-16.jpg" data-resource-id="avatar-m-16.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-19.jpg" data-resource-id="avatar-m-19.jpg">
<meta name="ext-resource-dependency" content="./assets/img/avatar-m-20.jpg" data-resource-id="avatar-m-20.jpg">
<meta name="ext-resource-dependency" content="../../assets/templates/logos/12-logo-demo.svg" data-resource-id="brandMark">
<link rel="stylesheet" href="../../tokens/fonts.css">
<link rel="stylesheet" href="../../tokens/colors.css">
<link rel="stylesheet" href="../../tokens/typography.css">
<link rel="stylesheet" href="../../tokens/spacing.css">
<link rel="stylesheet" href="../../tokens/radius.css">
<link rel="stylesheet" href="../../tokens/elevation.css">
<link rel="stylesheet" href="../../tokens/motion.css">
<link rel="stylesheet" href="../../css/base.css">
<link rel="stylesheet" href="../../css/controls.css">
<link rel="stylesheet" href="../../css/surfaces.css">
<link rel="stylesheet" href="../../css/overlays.css">
<link rel="stylesheet" href="../../css/navigation.css">
<link rel="stylesheet" href="../../css/data.css">
<link rel="stylesheet" href="../../styles.css">
<link rel="stylesheet" href="./kit.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="https://unpkg.com/lucide@0.544.0/dist/umd/lucide.min.js" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<style>html,body{margin:0;padding:0;height:100%}#rd-root{height:100dvh}#rd-boot{position:fixed;inset:0;display:grid;place-items:center;font:500 14px/1.5 system-ui,sans-serif;color:#5b6b66;background:#f6f7f6}</style>
</head>
<body>
<template id="__bundler_thumbnail" data-bg-color="#0d5c47">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg"><rect width="1200" height="800" fill="#0d5c47"/><g transform="translate(360,300)"><rect x="0" y="0" width="60" height="60" rx="30" fill="#e8f3ef"/><rect x="0" y="96" width="60" height="60" rx="30" fill="#e8f3ef" opacity=".55"/><rect x="110" y="14" width="370" height="32" rx="16" fill="#e8f3ef" opacity=".9"/><rect x="110" y="110" width="250" height="32" rx="16" fill="#e8f3ef" opacity=".45"/></g></svg>
</template>
<div id="rd-root"><div id="rd-boot">Opening the desk…</div></div>
<script type="text/plain" data-rd-file="lib/contracts.js">
/* Contract only — permanent machinery, copied into every product folder so that
   deleting the product deletes it too. No domain value may appear in this file:
   if an unrelated product could not use a value with the same meaning, it
   belongs in enums.js or nav.js beside it. These copies start byte-identical
   between products and stay that way — a fix here is a fix everywhere, never a
   local edit.

   Shapes
     Tone          "neutral" | "info" | "success" | "warning" | "danger"
     StatusValue   { id, label, tone }
     StatusField   { entity, field, means, values: StatusValue[] }
     LabelledValue { value, label }
     Route         { path, label, icon?, group?, parent?, badge?,
                     offSidebar?, menuOnly?, roles? }
*/

const TONES = ["neutral", "info", "success", "warning", "danger"];

/* Classification values render through this — never through a Badge. */
function labelOf(list, value) {
  const hit = (list || []).find((i) => i.value === value);
  return hit ? hit.label : value;
}

/* The one place a status id becomes a label + tone. */
function statusValue(values, id) {
  return (values || []).find((v) => v.id === id) || null;
}

function indexRoutes(nav) {
  const index = {};
  (nav || []).forEach((r) => { index[r.path] = r; });
  return index;
}

function matchRoute(path, index) {
  if (index[path]) return index[path];
  return Object.values(index).find((r) =>
    r.path.indexOf(":") > -1 &&
    new RegExp("^" + r.path.replace(/:[^/]+/g, "[^/]+") + "$").test(path)
  ) || null;
}

/* Breadcrumbs read the index, never the nav tree, and never invent a segment:
   an unresolved path returns [] so the screen — not the route table — is fixed. */
function breadcrumb(path, index) {
  const route = matchRoute(path, index);
  if (!route) return [];
  const trail = [route];
  let parent = route.parent;
  while (parent && index[parent]) { trail.unshift(index[parent]); parent = index[parent].parent; }
  return trail.map((s) => ({ label: s.label, path: s.path, isNamespace: !!s.menuOnly }));
}

Object.assign(window, { TONES, labelOf, statusValue, indexRoutes, matchRoute, breadcrumb });

</script>
<script type="text/plain" data-rd-file="enums.js">
/* Recruitment Desk — this product's status vocabulary.
   A value not listed here does not exist. A screen that wants one stops and
   asks; it never passes a string literal into a badge, and it never adds a
   value because a fixture would look better with it.

   Larkspur Health runs one hiring process for every requisition — that is the
   point of the desk. The seven ordered stages below ARE the stepper, in this
   order, everywhere an application appears.

   Shape matters here: a status field is `{id, label, tone}` and is read with
   `statusValue(ENUM, id)` — or `statusWords` for the same label mid-sentence.
   A classification list is `{value, label}` and is read with `labelOf`. Passing
   a status enum to `labelOf` matches nothing and hands back the raw id, which
   is how "DBS not_started" once reached a reader. */

/* ── Application stage ────────────────────────────────────────────────────
   Means: how far this application has got, and — because the tone grouping
   answers the only question a reviewer asks at a queue — who owes the next
   move. Grouped that way deliberately:
     info    · ours to move. The desk is the blocker.
     warning · out of our hands but on the clock: referees, the DBS service,
               the candidate holding an offer. Deliberate states needing
               attention, never failures.
     success · hired.
     neutral · settled, nothing owed by anybody.
   Deliberately ABSENT because they are derived, never stored: stalled (days in
   stage against the stage's own service level), unopened (no first review),
   overdue references, offer expiring. A record carries one stage and any number
   of derived flags — see APPLICATION_FLAGS. */
const APPLICATION_STAGE = [
  { id: "applied", label: "Applied", tone: "info" },
  { id: "screening", label: "Screening", tone: "info" },
  { id: "assessment", label: "Assessment", tone: "info" },
  { id: "panel", label: "Panel", tone: "info" },
  { id: "checks", label: "Checks", tone: "warning" },
  { id: "offer", label: "Offer", tone: "warning" },
  { id: "hired", label: "Hired", tone: "success" },
  { id: "declined", label: "Declined", tone: "neutral" },
  { id: "withdrawn", label: "Withdrawn", tone: "neutral" },
];
/* The route through the process, in order — the stepper reads this, not the
   whole enum, because declined and withdrawn are ends rather than steps. */
const APPLICATION_PATH = ["applied", "screening", "assessment", "panel", "checks", "offer", "hired"];
const STAGE_OURS = ["applied", "screening", "assessment", "panel"];
const STAGE_ELSEWHERE = ["checks", "offer"];
const STAGE_SETTLED = ["hired", "declined", "withdrawn"];
/* What each step is for, in the words a reviewer would use. The stepper and the
   stage note both read these; nothing describes a stage twice. */
const STAGE_MEANS = {
  applied: "Application in, nobody has read it yet",
  screening: "CV and answers read, screening call booked",
  assessment: "Written exercise or clinical scenario with the candidate",
  panel: "Panel interview and scorecards",
  checks: "Two references, right to work and DBS",
  offer: "Offer out, waiting on the candidate",
  hired: "Accepted and handed to onboarding",
};

/* ── Requisition status ───────────────────────────────────────────────────
   Means: whether this seat may be filled right now. `on_hold` is warning
   because it is a deliberate pause somebody has to come back to; `closed` is
   neutral because it is settled. Aging and under-supplied are derived. */
const REQUISITION_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "open", label: "Open", tone: "info" },
  { id: "on_hold", label: "On hold", tone: "warning" },
  { id: "filled", label: "Filled", tone: "success" },
  { id: "closed", label: "Closed", tone: "neutral" },
];

/* ── Interview status ─────────────────────────────────────────────────────
   Means: the state of one booked conversation. `no_show` is the only danger in
   the product: it is a failure event, not a state anybody chose. */
const INTERVIEW_STATUS = [
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "awaiting_feedback", label: "Awaiting feedback", tone: "warning" },
  { id: "complete", label: "Complete", tone: "success" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
  { id: "no_show", label: "No show", tone: "danger" },
];

/* ── Right to work ────────────────────────────────────────────────────────
   Means: what we hold on this candidate's right to work in the UK. Nothing
   here is a judgement on the person; it is the state of our paperwork. */
const RIGHT_TO_WORK = [
  { id: "verified", label: "Verified", tone: "success" },
  { id: "documents_seen", label: "Documents seen", tone: "info" },
  { id: "outstanding", label: "Outstanding", tone: "warning" },
  { id: "needs_sponsorship", label: "Needs sponsorship", tone: "warning" },
];

/* ── DBS ──────────────────────────────────────────────────────────────────
   Means: where this candidate's disclosure check has got to. It is a status
   field like any other, so it lives here rather than as a loose string on the
   record: `flagged` is warning, not danger, because a disclosure is a
   conversation to have and not a failure event. */
const DBS_STATUS = [
  { id: "not_started", label: "Not started", tone: "neutral" },
  { id: "submitted", label: "With the service", tone: "info" },
  { id: "clear", label: "Clear", tone: "success" },
  { id: "flagged", label: "Disclosure to discuss", tone: "warning" },
];

/* ── Reference status ─────────────────────────────────────────────────────
   Means: the state of one reference request. `refused` is danger — a referee
   declining to answer stops the hire. */
const REFERENCE_STATUS = [
  { id: "requested", label: "Requested", tone: "info" },
  { id: "chased", label: "Chased", tone: "warning" },
  { id: "received", label: "Received", tone: "success" },
  { id: "refused", label: "Refused", tone: "danger" },
];

/* Id unions, for the reader: an application's stage is one of
   APPLICATION_STAGE[].id; a requisition's status one of REQUISITION_STATUS[].id;
   and so on. Nothing in this product carries a status not in this file. */

/* ── Classification lists — read through labelOf, never through a Badge ── */
const DEPARTMENTS = [
  { value: "nursing", label: "Nursing" },
  { value: "therapies", label: "Therapies" },
  { value: "medical", label: "Medical" },
  { value: "digital", label: "Digital" },
  { value: "operations", label: "Operations" },
];
const SITES = [
  { value: "riverside", label: "Riverside Hospital" },
  { value: "eastgate", label: "Eastgate Clinic" },
  { value: "community", label: "Community teams" },
  { value: "hq", label: "Larkspur House" },
];
const SOURCES = [
  { value: "careers_site", label: "Careers site" },
  { value: "nhs_jobs", label: "Jobs board" },
  { value: "referral", label: "Colleague referral" },
  { value: "agency", label: "Agency" },
  { value: "pool", label: "Talent pool" },
  { value: "event", label: "Careers event" },
];
const EMPLOYMENT_TYPES = [
  { value: "permanent", label: "Permanent" },
  { value: "fixed_term", label: "Fixed term" },
  { value: "bank", label: "Bank" },
];
/* Which professional register a SEAT requires — a property of the requisition,
   not of the person, and the reason compliance can tell a nurse with no NMC
   number from a ward clerk who needs none. `none` stores as null. */
const REGISTERS = [
  { value: "none", label: "No professional register" },
  { value: "NMC", label: "NMC — nursing and midwifery" },
  { value: "HCPC", label: "HCPC — allied health professions" },
];
const DESK_ROLES = [
  { value: "recruiter", label: "Recruiter" },
  { value: "coordinator", label: "Coordinator" },
  { value: "hiring_manager", label: "Hiring manager" },
  { value: "panel", label: "Panel member" },
  { value: "lead", label: "Head of resourcing" },
];
const DECLINE_REASONS = [
  { value: "experience", label: "Not enough relevant experience" },
  { value: "registration", label: "Registration or qualification missing" },
  { value: "assessment", label: "Assessment below the bar" },
  { value: "panel", label: "Panel scored below the bar" },
  { value: "stronger_field", label: "Stronger candidates for the same seat" },
  { value: "money", label: "Expectation outside the band" },
];
const WITHDRAW_REASONS = [
  { value: "another_offer", label: "Took another offer" },
  { value: "timing", label: "Timing no longer works" },
  { value: "travel", label: "Travel or site" },
  { value: "money", label: "Money" },
  { value: "no_contact", label: "Stopped responding" },
];
/* What a panel scores. Four areas, the same four for every requisition, so two
   scorecards can sit side by side on one grid. */
const SCORE_AREAS = [
  { value: "clinical", label: "Clinical or technical" },
  { value: "people", label: "Working with people" },
  { value: "judgement", label: "Judgement under pressure" },
  { value: "values", label: "Larkspur values" },
];
/* Social. Named reactions, not free emoji: three things a reviewer actually
   means, so a count is readable ("4 agree") rather than decorative. */
const REACTIONS = [
  { value: "agree", label: "Agree", icon: "thumbs-up" },
  { value: "watching", label: "Watching", icon: "eye" },
  { value: "question", label: "Question", icon: "circle-help" },
];
const NOTE_KINDS = [
  { value: "note", label: "Note" },
  { value: "decision", label: "Decision" },
  { value: "candidate", label: "From the candidate" },
];
/* What a notification is about. A classification, not a status: a notification
   has one kind for its whole life, and the icon comes from here so no screen
   picks one. */
const NOTIFICATION_KINDS = [
  { value: "mention", label: "Mentions", icon: "at-sign" },
  { value: "panel", label: "Panel and scorecards", icon: "clipboard-pen" },
  { value: "offer", label: "Offers", icon: "file-signature" },
  { value: "compliance", label: "Compliance", icon: "shield-check" },
  { value: "application", label: "New applications", icon: "inbox" },
  { value: "interview", label: "Interviews", icon: "calendar-clock" },
];

/* ── Policy — the constants the arithmetic and the copy both read ─────────
   Editable on /settings/process. It is one object and every screen reads it, so
   changing a service level here moves the queue, the stepper, the badges and the
   compliance screen at once — which is the point of it being one object. */
const RD_POLICY = {
  /* Service level per stage, in whole days. isStalled compares against the
     stage the application is in, never against one number for all of them. */
  stageDays: { applied: 3, screening: 5, assessment: 7, panel: 5, checks: 10, offer: 7 },
  panelScorecards: 2,      // a panel decision needs two, from different people
  referencesRequired: 2,   // NHS standard: two, one the current employer
  offerValidDays: 7,
  agingDays: 45,           // an open requisition older than this is aging
  thinPipeline: 3,         // fewer live applications than this and the seat is at risk
  registrationWarnDays: 60, // NMC or HCPC registration inside this is flagged on /compliance
  offerExpiryWarnDays: 3,   // an offer inside this is on the sidebar badge
};

/* ── Derived flags — computed at render, never stored, never in an enum.
   The clock is injected so a screen cannot quietly use a different now. ──── */
const APPLICATION_FLAGS = {
  isLive: (a) => STAGE_SETTLED.indexOf(a.stage) === -1,
  /* THE act-ability predicate. The row's actions, the section count, the tab
     count and the sidebar badge all call this one function. */
  awaitsUs: (a) => STAGE_OURS.indexOf(a.stage) > -1,
  awaitsThem: (a) => STAGE_ELSEWHERE.indexOf(a.stage) > -1,
  isUnread: (a) => a.stage === "applied" && !a.firstReadAt,
  isStalled: (a, now) => {
    const allowed = RD_POLICY.stageDays[a.stage];
    if (!allowed || STAGE_SETTLED.indexOf(a.stage) > -1) return false;
    return now - a.stageEnteredAt > allowed * 864e5;
  },
  offerExpiring: (a, now) => a.stage === "offer" && !!a.offer && a.offer.expiresAt - now < 3 * 864e5,
};
const INTERVIEW_FLAGS = {
  isBooked: (iv) => iv.status === "scheduled",
  feedbackOutstanding: (iv) => iv.status === "awaiting_feedback",
  isToday: (iv, now) => new Date(iv.at).toDateString() === new Date(now).toDateString(),
  isAhead: (iv, now) => iv.at >= now && iv.status === "scheduled",
};
const REQUISITION_FLAGS = {
  isOpen: (r) => r.status === "open",
  isAging: (r, now) => r.status === "open" && now - r.openedAt > RD_POLICY.agingDays * 864e5,
  isPosted: (advert) => !!advert && advert.postedTo.length > 0,
};
/* Compliance — the three things that have to be true before somebody starts,
   all derived, none of them a status of their own. Registration expiry is the
   only one with a clock, and the clock is injected.

   Registration is the one check that is not owed by everybody: whether a
   registration is required at all is a property of the SEAT (requisition.
   registration), so these predicates take the requisition rather than assuming
   every hire is a registered clinician — a ward clerk with no NMC number is not
   a blocked hire, and reading it off the application alone said he was. */
const COMPLIANCE_FLAGS = {
  rtwOutstanding: (a) => a.rightToWork !== "verified",
  needsSponsorship: (a) => a.rightToWork === "needs_sponsorship",
  dbsOutstanding: (a) => a.dbs !== "clear",
  registrationRequired: (requisition) => !!requisition && !!requisition.registration,
  registrationMissing: (a, requisition) => COMPLIANCE_FLAGS.registrationRequired(requisition) && !a.registrationExpiresAt,
  registrationExpired: (a, now) => !!a.registrationExpiresAt && a.registrationExpiresAt < now,
  registrationExpiring: (a, now) => !!a.registrationExpiresAt && a.registrationExpiresAt >= now
    && a.registrationExpiresAt - now <= RD_POLICY.registrationWarnDays * 864e5,
  /* Is the register this SEAT asks for something somebody still has to act on —
     missing, lapsed, or renewing inside the warn window. A register the seat does
     not ask for is never outstanding, whatever the candidate happens to carry. */
  registrationOutstanding: (a, requisition, now) => COMPLIANCE_FLAGS.registrationRequired(requisition)
    && (COMPLIANCE_FLAGS.registrationMissing(a, requisition)
      || COMPLIANCE_FLAGS.registrationExpired(a, now)
      || COMPLIANCE_FLAGS.registrationExpiring(a, now)),
  /* THE predicate the compliance screen, its recession, its counts and the nav
     badge all share: nothing outstanding on this person. It is the negation of
     the three flags above and nothing else, so a row can never be counted as
     clear and shown as blocked at the same time — which is what once recessed
     the one row on the screen with a live action on it. */
  clearToStart: (a, requisition, now) => !COMPLIANCE_FLAGS.rtwOutstanding(a)
    && !COMPLIANCE_FLAGS.dbsOutstanding(a)
    && !COMPLIANCE_FLAGS.registrationOutstanding(a, requisition, now),
};

/* ── Transitions ──────────────────────────────────────────────────────────
   Named once, after the move, and offered by ONE function per entity that every
   surface calls — the row menu, the detail header, the board card and the bulk
   bar. A transition that does not apply is omitted, never disabled; a
   transition that exists but this reviewer cannot take yet is disabled with the
   reason stated. Exactly one is primary: the record's normal path. */
const STAGE_AFTER = { applied: "screening", screening: "assessment", assessment: "panel", panel: "checks", checks: "offer", offer: "hired" };
const ADVANCE_LABEL = {
  screening: "Move to screening",
  assessment: "Send the assessment",
  panel: "Book the panel",
  checks: "Start the checks",
  offer: "Send the offer",
  hired: "Record acceptance",
};

function applicationTransitions(application, context) {
  const ctx = context || {};
  const next = STAGE_AFTER[application.stage];
  const out = [];
  if (next) {
    const gate = advanceGate(application, ctx);
    out.push({
      id: "advance", to: next, label: ADVANCE_LABEL[next], primary: true,
      disabled: !!gate, disabledReason: gate,
    });
  }
  if (application.stage === "checks" || application.stage === "panel") {
    out.push({ id: "book_interview", label: "Book an interview" });
  }
  if (APPLICATION_FLAGS.isLive(application)) {
    out.push({ id: "decline", label: "Decline", destructive: true });
    out.push({ id: "withdraw", label: "Record a withdrawal" });
  }
  if (application.stage === "declined" && application.wouldReconsider) {
    out.push({ id: "reopen", label: "Reopen for another seat", primary: true });
  }
  return out;
}

/* Why the primary move is not available yet — a visible precondition, stated on
   the disabled control rather than left for the reviewer to guess. */
function advanceGate(application, ctx) {
  const scorecards = ctx.scorecards === undefined ? (application.scorecards || []).length : ctx.scorecards;
  if (application.stage === "panel" && scorecards < RD_POLICY.panelScorecards) {
    return RD_POLICY.panelScorecards + " scorecards are needed before checks start — " + scorecards + " in so far.";
  }
  if (application.stage === "checks") {
    /* One list, one spelling. checksMissing (metrics.js) is what the checks card
       on the record renders, so the reason on the disabled control and the card
       beside it cannot disagree — this branch used to omit the DBS the card
       named, which let an offer look available while a check was outstanding. */
    const missing = checksMissing(application);
    if (missing.length) return "An offer waits on the checks: " + missing.join(" · ") + ".";
  }
  if (application.stage === "offer" && application.offer && !application.offer.acceptedAt) {
    return "Nothing to record until the candidate accepts — the offer is with them until " + new Date(application.offer.expiresAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" }) + ".";
  }
  return null;
}

function requisitionTransitions(requisition) {
  const out = [];
  if (requisition.status === "draft") out.push({ id: "open_req", label: "Open the requisition", primary: true });
  if (requisition.status === "open") {
    out.push({ id: "hold_req", label: "Put on hold" });
    out.push({ id: "close_req", label: "Close without hiring", destructive: true });
  }
  if (requisition.status === "on_hold") out.push({ id: "resume_req", label: "Reopen the requisition", primary: true });
  return out;
}

function interviewTransitions(interview) {
  const out = [];
  if (interview.status === "scheduled") {
    out.push({ id: "record_held", label: "Mark it held", primary: true });
    out.push({ id: "reschedule", label: "Reschedule" });
    out.push({ id: "record_no_show", label: "Record a no show" });
    out.push({ id: "cancel_interview", label: "Cancel the interview", destructive: true });
  }
  if (interview.status === "awaiting_feedback") out.push({ id: "add_scorecard", label: "Add your scorecard", primary: true });
  return out;
}

/* ── The registry, and what the fixture still does not cover ───────────── */
const STATUS_FIELDS = [
  { entity: "application", field: "stage", means: "How far the application has got, and who owes the next move", values: APPLICATION_STAGE },
  { entity: "requisition", field: "status", means: "Whether this seat may be filled right now", values: REQUISITION_STATUS },
  { entity: "interview", field: "status", means: "The state of one booked conversation", values: INTERVIEW_STATUS },
  { entity: "application", field: "rightToWork", means: "What we hold on this candidate's right to work", values: RIGHT_TO_WORK },
  { entity: "reference", field: "status", means: "The state of one reference request", values: REFERENCE_STATUS },
  { entity: "application", field: "dbs", means: "Where the disclosure check has got to", values: DBS_STATUS },
];

/* Coverage. Every value above has at least one fixture row except:
   · requisition.draft — reachable only by creating one on /requisitions/new,
     which is how the fixture stays honest about a state the desk makes itself.
   · interview.cancelled — reachable through the cancel transition.
   · reference.refused — no fixture row; the copy for it exists on the checks
     panel and it is reachable by chasing a refusal in a real system, so it is
     recorded here as uncovered rather than faked.
   · dbs.flagged — no fixture row. A disclosure to discuss is a real state with a
     real screen behind it on /compliance, and inventing one for a demo candidate
     would put an untrue thing on a person's record.
   dbs.submitted sits on one row (Priya Raghunathan, whose checks pack went out
   on the 27th) — the state where the desk is waiting on somebody else, which the
   screen would otherwise never show. */

Object.assign(window, {
  APPLICATION_STAGE, APPLICATION_PATH, STAGE_OURS, STAGE_ELSEWHERE, STAGE_SETTLED, STAGE_MEANS,
  REQUISITION_STATUS, INTERVIEW_STATUS, RIGHT_TO_WORK, REFERENCE_STATUS, DBS_STATUS,
  DEPARTMENTS, SITES, SOURCES, EMPLOYMENT_TYPES, REGISTERS, DESK_ROLES, DECLINE_REASONS, WITHDRAW_REASONS,
  SCORE_AREAS, REACTIONS, NOTE_KINDS, NOTIFICATION_KINDS, RD_POLICY,
  APPLICATION_FLAGS, INTERVIEW_FLAGS, REQUISITION_FLAGS, COMPLIANCE_FLAGS,
  STAGE_AFTER, ADVANCE_LABEL, applicationTransitions, advanceGate, requisitionTransitions, interviewTransitions,
  STATUS_FIELDS,
});

</script>
<script type="text/plain" data-rd-file="nav.js">
/* Recruitment Desk — the route table.

   Deliberately NOT routes: the ⌘K palette, the notifications panel, the sidebar
   fold, row menus, every confirm and staged dialog, the applications list's
   filters and tabs, the stepper's step selection, the comment composer and its
   replies, the scorecard drawer. They are states of a screen; a breadcrumb that
   wants one of them is a bug in the screen, not a missing route.

   Nine top-level destinations, dense record work, four per-destination counts:
   this product runs the SIDEBAR shell, labelled mode, in TWO groups — the
   reviewing a recruiter does all day, and the running of the hiring itself. Two
   groups, so both carry a label (AGENTS §6.3).

   One application is one record with one route: /applications/:id is reached
   from the desk, the queue, the interview ladder, the requisition board, the
   talent pool and the palette alike. There is no second candidate screen. */

const RD_NAV = [
  { path: "/", label: "Your desk", icon: "layout-dashboard", group: "review" },
  { path: "/applications", label: "Applications", icon: "inbox", badge: "awaitingUs", group: "review" },
  { path: "/interviews", label: "Interviews", icon: "calendar-clock", badge: "feedbackOutstanding", group: "review" },
  { path: "/offers", label: "Offers", icon: "file-signature", badge: "offersExpiring", group: "review" },

  { path: "/requisitions", label: "Requisitions", icon: "briefcase", group: "hiring" },
  { path: "/compliance", label: "Compliance", icon: "shield-check", badge: "complianceUrgent", group: "hiring" },
  { path: "/pool", label: "Talent pool", icon: "users-round", group: "hiring" },
  { path: "/insights", label: "Insights", icon: "chart-no-axes-column", group: "hiring" },
  { path: "/team", label: "Desk team", icon: "user-round-cog", group: "hiring" },

  /* Registered, not in the nav. */
  { path: "/applications/new", label: "New application", parent: "/applications", offSidebar: true, group: "Create" },
  { path: "/applications/:id", label: "Application", parent: "/applications", offSidebar: true },
  { path: "/requisitions/new", label: "New requisition", parent: "/requisitions", offSidebar: true, group: "Create" },
  { path: "/requisitions/:id", label: "Requisition", parent: "/requisitions", offSidebar: true },
  /* The advert is a view WITHIN a requisition, so it is a registered child
     reached by the record's own tabs — not a destination of its own. */
  { path: "/requisitions/:id/advert", label: "Advert", parent: "/requisitions/:id", offSidebar: true },
  { path: "/me/profile", label: "Your profile", offSidebar: true },
  /* The notifications inbox is app-scope, not a destination: it is opened from
     the bell the count sits on (AGENTS §6.2), so it is registered off the nav. */
  { path: "/notifications", label: "Notifications", offSidebar: true },

  /* Settings is a NAMESPACE with no screen of its own: clicking it in the
     account menu opens the first child, and it supplies the first breadcrumb
     segment. Its children are the four things a desk actually configures. */
  { path: "/settings", label: "Settings", menuOnly: true, offSidebar: true },
  { path: "/settings/desk", label: "The desk", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/process", label: "Process and service levels", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/channels", label: "Adverts and channels", parent: "/settings", offSidebar: true, group: "Settings" },
  { path: "/settings/people", label: "People and access", parent: "/settings", offSidebar: true, group: "Settings" },

  /* Auth namespace — no navigation owner, no product chrome (AGENTS §7.1). */
  { path: "/signin", label: "Sign in", offSidebar: true },
  { path: "/signout", label: "Sign out", offSidebar: true },
];

const RD_ROUTES = indexRoutes(RD_NAV);

/* Projections the shell renders — derived by filtering the tree, never retyped. */
const destinations = () => RD_NAV.filter((r) => !r.offSidebar);
const NAV_GROUPS = [
  { key: "review", label: "Reviewing" },
  { key: "hiring", label: "Running the hiring" },
];
const sidebarGroups = () => NAV_GROUPS
  .map((group) => ({ label: group.label, items: destinations().filter((r) => r.group === group.key) }))
  .filter((group) => group.items.length > 0);

const accountMenu = () => [
  RD_ROUTES["/me/profile"], RD_ROUTES["/notifications"],
  /* Labelled by where it goes, not by the child that happens to be first. */
  Object.assign({}, RD_ROUTES["/settings/desk"], { label: "Settings" }),
  RD_ROUTES["/signout"],
];
/* The settings sub-nav: the route table filtered by parent, never retyped. */
const settingsPages = () => RD_NAV.filter((route) => route.parent === "/settings");

/* Paths that render without product chrome. */
const RD_AUTH_PATHS = ["/signin", "/signout"];

const resolvePath = (pattern, params) => pattern.replace(/:([^/]+)/g, (_, key) => params[key]);

/* The preview menu's page list — this table read whole and grouped, with the
   parameterised routes resolved against the fixture so a link lands on a real
   record rather than on ":id". A pattern with no fixture example is left out. */
const RD_PAGES = (data) => {
  const application = data.applications[0], requisition = data.requisitions[0];
  return templatePages(RD_NAV, RD_AUTH_PATHS, {
    "/applications/:id": application ? resolvePath("/applications/:id", { id: application.id }) : null,
    "/requisitions/:id": requisition ? resolvePath("/requisitions/:id", { id: requisition.id }) : null,
    "/requisitions/:id/advert": requisition ? resolvePath("/requisitions/:id/advert", { id: requisition.id }) : null,
  });
};

Object.assign(window, {
  RD_NAV, RD_ROUTES, RD_AUTH_PATHS, destinations, sidebarGroups, accountMenu, settingsPages, resolvePath, RD_PAGES,
});

</script>
<script type="text/plain" data-rd-file="fixtures.js">
/* Recruitment Desk — the fixture. One record, one entry: every screen reads
   these arrays, and no screen writes a second literal to match. Nothing is
   stored that can be derived — days in stage, funnel rates, panel averages,
   pipeline health, the talent pool itself and every count in the product are
   computed in metrics.js from what is here.

   Three things deliberately NOT stored:
   · Whether an application is stalled. That is days in its current stage
     against that stage's own service level (APPLICATION_FLAGS.isStalled).
   · The talent pool. It is the declined and withdrawn people we said we would
     talk to again — a predicate over this same array, not a second list that
     could disagree with it.
   · A panel average. It is the mean of the scorecards on the record, and it
     changes the moment one is added.

   The narrative agrees with the records: a note that names a panel, a reference
   or an offer is dated after the thing it names, and says what the record says. */

const DAY_MS = 864e5;
const d0 = (iso) => new Date(iso + "T00:00:00").getTime();
const d1 = (iso) => new Date(iso + "T23:59:59").getTime();
const at = (iso) => new Date(iso).getTime();

const RD_NOW = at("2026-08-28T09:20:00");

const RD_ORG = {
  name: "Larkspur Health",
  desk: "Recruitment Desk",
  what: "Community and hospital group, 4,100 people",
  domain: "larkspurhealth.uk",
};

/* ── The desk and the panel ───────────────────────────────────────────────
   Nine people: two recruiters, a coordinator, the head of resourcing, three
   hiring managers and two clinicians who sit on panels.

   Faces are cast, not hashed. PORTRAITS names one photograph per person on this
   fixture — thirty-three people, thirty-three different photographs — chosen so
   the face agrees with the name and the role: nobody is shown as the wrong sex,
   the desk skews older than the newly qualified applicants, and the register
   numbers sit with faces of the right professional age. A person therefore keeps
   the same face on every screen because there is one entry for her, not because
   a hash happens to be stable.

   SPARE_WOMEN and SPARE_MEN are the photographs no fixture person holds; they
   dress candidates created in the running product, where the only thing known
   about the name is what the recruiter typed. MEN lists who the men are, which
   is what lets a spare be allocated without guessing from the name. */
const img = (file) => (window.__resources && window.__resources[file]) || "./assets/img/" + file;
const PORTRAITS = {
  /* The desk */
  "Nadia Ferreira": "avatar-f-17.jpg",
  "Joel Whitcombe": "avatar-m-13.jpg",
  "Marek Zielinski": "avatar-m-1.jpg",
  "Amara Boateng": "avatar-f-9.jpg",
  "Esme Okonkwo": "avatar-80-06.jpg",
  "Fern Adeyemi": "avatar-f-6.jpg",
  "David Aliyev": "avatar-m-10.jpg",
  "Ruth Calloway": "avatar-f-7.jpg",
  "Tomas Iglesias": "avatar-m-20.jpg",
  /* Applied */
  "Aisha Farrow": "avatar-f-1.jpg",
  "Callum Beattie": "avatar-m-2.jpg",
  "Dilnoza Rakhimova": "avatar-80-02.jpg",
  "Michael Osei-Bonsu": "avatar-m-14.jpg",
  "Yasmin Qureshi": "avatar-f-18.jpg",
  /* Screening */
  "Grace Lindqvist": "avatar-f-14.jpg",
  "Tobias Vale": "avatar-m-15.jpg",
  "Nkechi Ibrahim": "avatar-f-16.jpg",
  "Samuel Doherty": "avatar-m-19.jpg",
  /* Assessment */
  "Helena Marquez": "avatar-80-05.jpg",
  "Ravi Chandran": "avatar-m-9.jpg",
  "Bea Whitlock": "avatar-f-11.jpg",
  /* Panel */
  "Priya Raghunathan": "avatar-f-13.jpg",
  "Jonah Okwu": "avatar-80-03.jpg",
  "Marta Kaczmarek": "avatar-f-5.jpg",
  /* Checks */
  "Elias Nordahl": "avatar-m-4.jpg",
  "Sonia Bhattacharya": "avatar-f-2.png",
  /* Offer */
  "Freya Lindsay": "avatar-80-04.jpg",
  "Damian Oyelaran": "avatar-m-6.jpg",
  /* Settled */
  "Adaeze Nwankwo": "avatar-f-12.jpg",
  "Peter Halloran": "avatar-m-12.jpg",
  "Ines Duarte": "avatar-f-3.jpg",
  "Tom Sefton": "avatar-m-16.jpg",
  "Lila Farid": "avatar-f-8.jpg",
};
const SPARE_WOMEN = ["avatar-f-4.jpg", "avatar-f-10.jpg", "avatar-f-15.jpg"];
const SPARE_MEN = ["avatar-m-3.jpg", "avatar-m-5.jpg", "avatar-m-7.jpg", "avatar-80-01.jpg", "avatar.jpg"];
const MEN = new Set([
  "Joel Whitcombe", "Marek Zielinski", "David Aliyev", "Tomas Iglesias",
  "Callum Beattie", "Michael Osei-Bonsu", "Tobias Vale", "Samuel Doherty",
  "Ravi Chandran", "Jonah Okwu", "Elias Nordahl", "Damian Oyelaran",
  "Peter Halloran", "Tom Sefton",
]);
const allocated = new Map();
const nextSpare = { women: 0, men: 0 };
const portraitFor = (name) => {
  if (PORTRAITS[name]) return img(PORTRAITS[name]);
  if (allocated.has(name)) return allocated.get(name);
  const man = MEN.has(name);
  const list = man ? SPARE_MEN : SPARE_WOMEN;
  const key = man ? "men" : "women";
  const file = img(list[nextSpare[key]++ % list.length]);
  allocated.set(name, file);
  return file;
};
const initialsOf = (name) => name.split(" ").map((w) => w[0]).join("").slice(0, 2);

const S = (slug, name, title, role, department, site) => ({
  id: "p-" + slug, name, initials: initialsOf(name), photo: portraitFor(name),
  email: name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/ /g, ".") + "@" + RD_ORG.domain,
  title, role, department, site,
});

const STAFF = [
  S("nadia", "Nadia Ferreira", "Recruiter, clinical", "recruiter", "nursing", "hq"),
  S("joel", "Joel Whitcombe", "Recruiter, corporate and digital", "recruiter", "digital", "hq"),
  S("marek", "Marek Zielinski", "Resourcing coordinator", "coordinator", "operations", "hq"),
  S("amara", "Amara Boateng", "Head of resourcing", "lead", "operations", "hq"),
  S("esme", "Esme Okonkwo", "Community nursing lead", "hiring_manager", "nursing", "community"),
  S("fern", "Fern Adeyemi", "Therapies service manager", "hiring_manager", "therapies", "eastgate"),
  S("david", "David Aliyev", "Head of clinical systems", "hiring_manager", "digital", "hq"),
  S("ruth", "Ruth Calloway", "Matron, community teams", "panel", "nursing", "community"),
  S("tomas", "Tomas Iglesias", "Principal engineer", "panel", "digital", "hq"),
];

const RD_SESSION = { personId: "p-nadia", role: "recruiter" };

/* ── Requisitions ─────────────────────────────────────────────────────────
   `headcount` is how many seats this requisition carries, so a hire does not
   necessarily fill it — /requisitions counts hires against headcount rather
   than assuming one each. */
const R = (ref, title, department, site, type, bandMajor, hiringManagerId, recruiterId, headcount, openedISO, targetISO, status, extra) => Object.assign({
  id: "rq-" + ref, ref: "LH-" + ref, title, department, site, employmentType: type,
  bandMinor: [bandMajor[0] * 100, bandMajor[1] * 100],
  hiringManagerId, recruiterId, headcount,
  openedAt: d0(openedISO), targetStartAt: d0(targetISO), status,
  /* Which professional register this SEAT requires, null where none does. It is
     the requisition that knows — the advert's essentials say the same thing in
     words — so compliance can tell a nurse with no NMC number from a ward clerk
     who needs none. */
  registration: null,
  holdNote: null, closedNote: null, closedAt: null,
}, extra || {});

const REQUISITIONS = [
  R("1042", "Community staff nurse", "nursing", "community", "permanent", [31049, 37796], "p-esme", "p-nadia", 3, "2026-06-30", "2026-10-05", "open",
    { registration: "NMC", brief: "Three seats across the north and west community teams. Band 5, NMC registration essential, driving licence essential." }),
  R("1051", "Senior physiotherapist", "therapies", "eastgate", "permanent", [38682, 46580], "p-fern", "p-nadia", 1, "2026-07-27", "2026-11-02", "open",
    { registration: "HCPC", brief: "Musculoskeletal outpatients at Eastgate. HCPC registration essential, first-contact practitioner experience desirable." }),
  R("1063", "Clinical systems engineer", "digital", "hq", "permanent", [46148, 52809], "p-david", "p-joel", 1, "2026-08-10", "2026-10-26", "open",
    { brief: "Integration work on the shared care record. On-call rota one week in six." }),
  R("1067", "Ward clerk", "operations", "riverside", "fixed_term", [24071, 25674], "p-david", "p-joel", 1, "2026-08-17", "2026-09-28", "open",
    { brief: "Twelve months, Bramble ward. Cover for a secondment." }),
  R("1058", "Occupational therapist", "therapies", "community", "permanent", [35392, 42618], "p-fern", "p-nadia", 1, "2026-07-13", "2026-10-19", "on_hold",
    { registration: "HCPC", holdNote: "Establishment review in the community division — Amara is taking it to the October finance meeting. Two live applicants have been told." }),
  R("1039", "Night nurse practitioner", "nursing", "riverside", "permanent", [43742, 50056], "p-esme", "p-nadia", 1, "2026-05-18", "2026-09-07", "filled",
    { registration: "NMC", brief: "Nights, Riverside acute floor." }),
];

/* ── Applications ─────────────────────────────────────────────────────────
   `stageEnteredAt` is what the stepper and every clock read; `firstReadAt` is
   null until somebody opens the application, which is the only thing that makes
   an application unread. Documents, scorecards and references are sub-records on
   the application because they are meaningless without it. */
const doc = (kind, name, iso) => ({ kind, name, at: at(iso) });
const card = (byId, iso, scores, note, recommend) => ({ byId, at: at(iso), scores, note, recommend });
const ref = (name, relationship, status, requestedISO, extra) => Object.assign({
  id: "rf-" + name.toLowerCase().replace(/[^a-z]/g, "").slice(0, 8), name, relationship, status,
  requestedAt: d0(requestedISO), receivedAt: null, note: null,
}, extra || {});

const A = (id, name, headline, location, requisitionId, stage, source, appliedISO, stageISO, extra) => Object.assign({
  id: "ap-" + id, ref: "APP-" + id,
  candidate: { name, initials: initialsOf(name), photo: portraitFor(name), headline, location },
  requisitionId, stage, source,
  appliedAt: at(appliedISO), stageEnteredAt: at(stageISO), firstReadAt: null,
  ownerId: null, rightToWork: "documents_seen", salaryExpectationMinor: null,
  /* DBS is a status like any other (DBS_STATUS in enums.js), so it is present on
     every application rather than only on the ones that have reached it. */
  dbs: "not_started", dbsAt: null, registration: null, registrationExpiresAt: null,
  documents: [], scorecards: [], references: [], offer: null, lastLiveStage: null,
  watcherIds: [], declineReason: null, declineNote: null, wouldReconsider: false, decidedAt: null, decidedById: null,
  withdrawReason: null, withdrawNote: null,
}, extra || {});

const APPLICATIONS = [
  /* ── Applied: nobody has read three of these five ───────────────────── */
  A("3101", "Aisha Farrow", "Band 5 nurse, district team, three years", "Eastgate", "rq-1042", "applied", "careers_site", "2026-08-27T18:42", "2026-08-27T18:42",
    { ownerId: "p-nadia", salaryExpectationMinor: 3400000, registration: "NMC 21F4478", registrationExpiresAt: d1("2027-03-31"), documents: [doc("cv", "Aisha-Farrow-CV.pdf", "2026-08-27T18:42"), doc("statement", "Supporting statement", "2026-08-27T18:42")] }),
  A("3102", "Callum Beattie", "Newly qualified nurse, September cohort", "Riverside", "rq-1042", "applied", "event", "2026-08-27T09:05", "2026-08-27T09:05",
    { ownerId: "p-nadia", salaryExpectationMinor: 3104900, registration: "NMC pending — September", documents: [doc("cv", "C-Beattie-CV.pdf", "2026-08-27T09:05")] }),
  A("3103", "Dilnoza Rakhimova", "Integration engineer, HL7 and FHIR", "Remote, Manchester", "rq-1063", "applied", "nhs_jobs", "2026-08-26T21:14", "2026-08-26T21:14",
    { ownerId: "p-joel", rightToWork: "needs_sponsorship", salaryExpectationMinor: 5500000, documents: [doc("cv", "Rakhimova-CV.pdf", "2026-08-26T21:14")] }),
  A("3104", "Michael Osei-Bonsu", "Ward administrator, six years", "Riverside", "rq-1067", "applied", "referral", "2026-08-25T11:30", "2026-08-25T11:30",
    { ownerId: "p-joel", firstReadAt: at("2026-08-26T08:40"), salaryExpectationMinor: 2500000, referredById: "p-marek",
      documents: [doc("cv", "MOB-CV.pdf", "2026-08-25T11:30")] }),
  A("3105", "Yasmin Qureshi", "MSK physiotherapist, rotational band 6", "Eastgate", "rq-1051", "applied", "nhs_jobs", "2026-08-21T16:02", "2026-08-21T16:02",
    { ownerId: "p-nadia", firstReadAt: at("2026-08-24T09:12"), salaryExpectationMinor: 4200000, registration: "HCPC PH108842", registrationExpiresAt: d1("2026-10-31"),
      documents: [doc("cv", "Y-Qureshi-CV.pdf", "2026-08-21T16:02"), doc("statement", "Supporting statement", "2026-08-21T16:02")] }),

  /* ── Screening ──────────────────────────────────────────────────────── */
  A("3088", "Grace Lindqvist", "Community nurse, returning after two years abroad", "Community, north", "rq-1042", "screening", "careers_site", "2026-08-14T10:20", "2026-08-24T11:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-08-17T09:30"), rightToWork: "verified", salaryExpectationMinor: 3500000, registration: "NMC 18L2210", registrationExpiresAt: d1("2027-05-31"),
      watcherIds: ["p-esme"], documents: [doc("cv", "G-Lindqvist-CV.pdf", "2026-08-14T10:20")] }),
  A("3089", "Tobias Vale", "Integration developer, private sector", "Larkspur House", "rq-1063", "screening", "agency", "2026-08-13T14:45", "2026-08-25T15:30",
    { ownerId: "p-joel", firstReadAt: at("2026-08-14T08:05"), rightToWork: "verified", salaryExpectationMinor: 5800000,
      documents: [doc("cv", "TVale.pdf", "2026-08-13T14:45")] }),
  A("3090", "Nkechi Ibrahim", "Band 5 nurse, acute to community move", "Riverside", "rq-1042", "screening", "referral", "2026-08-12T08:50", "2026-08-18T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-08-12T15:20"), rightToWork: "verified", salaryExpectationMinor: 3300000, registration: "NMC 20I8830", registrationExpiresAt: d1("2026-09-30"), referredById: "p-ruth",
      watcherIds: ["p-esme", "p-ruth"], documents: [doc("cv", "N-Ibrahim-CV.pdf", "2026-08-12T08:50")] }),
  A("3091", "Samuel Doherty", "Ward clerk, bank staff with us since March", "Riverside", "rq-1067", "screening", "pool", "2026-08-19T12:10", "2026-08-26T10:15",
    { ownerId: "p-joel", firstReadAt: at("2026-08-19T16:40"), rightToWork: "verified", salaryExpectationMinor: 2450000,
      documents: [doc("cv", "S-Doherty-CV.pdf", "2026-08-19T12:10")] }),

  /* ── Assessment ─────────────────────────────────────────────────────── */
  A("3072", "Helena Marquez", "Senior physiotherapist, first-contact practitioner", "Eastgate", "rq-1051", "assessment", "nhs_jobs", "2026-08-04T19:30", "2026-08-20T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-08-05T08:15"), rightToWork: "verified", salaryExpectationMinor: 4500000, registration: "HCPC PH094117", registrationExpiresAt: d1("2027-01-31"),
      watcherIds: ["p-fern"], documents: [doc("cv", "HMarquez-CV.pdf", "2026-08-04T19:30"), doc("case", "MSK case study response", "2026-08-24T22:05")] }),
  A("3073", "Ravi Chandran", "Systems engineer, shared care records", "Larkspur House", "rq-1063", "assessment", "careers_site", "2026-08-11T13:05", "2026-08-21T14:00",
    { ownerId: "p-joel", firstReadAt: at("2026-08-11T17:50"), rightToWork: "verified", salaryExpectationMinor: 5100000,
      watcherIds: ["p-david", "p-tomas"], documents: [doc("cv", "R-Chandran-CV.pdf", "2026-08-11T13:05"), doc("case", "Integration exercise", "2026-08-26T20:40")] }),
  A("3074", "Bea Whitlock", "Community nurse, band 5, rural caseload", "Community, west", "rq-1042", "assessment", "careers_site", "2026-08-06T09:40", "2026-08-13T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-08-06T11:20"), rightToWork: "verified", salaryExpectationMinor: 3600000, registration: "NMC 17W6642", registrationExpiresAt: d1("2026-11-30"),
      watcherIds: ["p-esme"], documents: [doc("cv", "B-Whitlock-CV.pdf", "2026-08-06T09:40")] }),

  /* ── Panel ──────────────────────────────────────────────────────────── */
  A("3051", "Priya Raghunathan", "Band 6 community nurse, eight years, prescriber", "Community, north", "rq-1042", "panel", "referral", "2026-07-24T20:15", "2026-08-25T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-25T08:30"), rightToWork: "verified", salaryExpectationMinor: 3750000, registration: "NMC 14R1187", registrationExpiresAt: d1("2027-07-31"), referredById: "p-esme",
      /* The checks pack went out on the 27th with the references — the disclosure
         is with the service and nobody here can move it along. */
      dbs: "submitted", dbsAt: at("2026-08-27T09:15"),
      watcherIds: ["p-esme", "p-ruth", "p-amara"],
      documents: [doc("cv", "Priya-Raghunathan-CV.pdf", "2026-07-24T20:15"), doc("statement", "Supporting statement", "2026-07-24T20:15"), doc("case", "Caseload scenario response", "2026-08-18T21:10")],
      scorecards: [
        card("p-ruth", "2026-08-26T17:40", { clinical: 4, people: 5, judgement: 4, values: 5 },
          "Talked through a deteriorating patient on a Friday afternoon with no GP cover and got to the right answer without drama. Would have her on my team.", "yes"),
        card("p-esme", "2026-08-26T19:05", { clinical: 4, people: 4, judgement: 5, values: 4 },
          "Strong on prioritising a mixed caseload. Prescribing is current. Asked good questions about the north team's vacancy gap — she has read the job, not the advert.", "yes"),
      ],
      references: [
        ref("Hannah Iversen", "Current line manager, Meadowvale Trust", "requested", "2026-08-27"),
        ref("Dr Owen Marsh", "GP, north locality", "requested", "2026-08-27"),
      ] }),
  A("3052", "Jonah Okwu", "Band 5 nurse, urgent care", "Riverside", "rq-1042", "panel", "nhs_jobs", "2026-07-29T11:00", "2026-08-27T14:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-29T14:10"), rightToWork: "verified", salaryExpectationMinor: 3200000, registration: "NMC 19O4520", registrationExpiresAt: d1("2026-12-31"),
      watcherIds: ["p-esme"],
      documents: [doc("cv", "J-Okwu-CV.pdf", "2026-07-29T11:00")],
      scorecards: [
        card("p-ruth", "2026-08-27T18:20", { clinical: 3, people: 4, judgement: 3, values: 4 },
          "Sound, a bit early in his career for a lone-working caseload. Esme's card will decide it — she saw the second half.", "maybe"),
      ] }),
  A("3053", "Marta Kaczmarek", "Clinical systems analyst, integration lead", "Larkspur House", "rq-1063", "panel", "agency", "2026-07-30T16:20", "2026-08-24T10:00",
    { ownerId: "p-joel", firstReadAt: at("2026-07-31T09:00"), rightToWork: "verified", salaryExpectationMinor: 5300000,
      watcherIds: ["p-david", "p-tomas"],
      documents: [doc("cv", "M-Kaczmarek-CV.pdf", "2026-07-30T16:20"), doc("case", "Integration exercise", "2026-08-19T20:00")],
      scorecards: [
        card("p-tomas", "2026-08-25T12:15", { clinical: 5, people: 4, judgement: 4, values: 4 },
          "Best answer on the shared care record we have had. Knew the failure modes of the message queue before I described them.", "yes"),
        card("p-david", "2026-08-25T16:45", { clinical: 5, people: 3, judgement: 4, values: 4 },
          "Technically the strongest in the field. Will need support presenting to the clinical safety group — not a blocker.", "yes"),
      ] }),

  /* ── Checks: one waiting on a referee, one ready for the offer ───────── */
  A("3020", "Elias Nordahl", "Senior physiotherapist, spinal service", "Eastgate", "rq-1051", "checks", "careers_site", "2026-07-10T10:00", "2026-08-19T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-10T14:30"), rightToWork: "verified", salaryExpectationMinor: 4400000, registration: "HCPC PH077310", registrationExpiresAt: d1("2026-09-14"),
      watcherIds: ["p-fern"], dbs: "clear", dbsAt: d1("2026-08-20"),
      documents: [doc("cv", "E-Nordahl-CV.pdf", "2026-07-10T10:00"), doc("case", "MSK case study response", "2026-07-28T12:00")],
      scorecards: [
        card("p-fern", "2026-08-18T15:00", { clinical: 5, people: 4, judgement: 4, values: 5 }, "Ready for the senior post today. Clear about supervision and happy to run the Friday clinic.", "yes"),
        card("p-ruth", "2026-08-18T16:10", { clinical: 4, people: 5, judgement: 4, values: 5 }, "Patients will like him. Gave a straight answer about the incident he was involved in two years ago.", "yes"),
      ],
      references: [
        ref("Marianne Voss", "Current line manager, Eastgate private clinic", "received", "2026-08-19", { receivedAt: at("2026-08-21T11:20"), note: "Six years, no concerns, would re-employ." }),
        ref("Sarah Beeching", "Clinical supervisor", "chased", "2026-08-19", { note: "Out of office until Monday 31st — auto-reply. Chased 26 Aug." }),
      ] }),
  A("3021", "Sonia Bhattacharya", "Band 5 nurse, community rotation", "Community, west", "rq-1042", "checks", "careers_site", "2026-07-06T09:15", "2026-08-14T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-06T13:00"), rightToWork: "verified", salaryExpectationMinor: 3250000, registration: "NMC 20B3391", registrationExpiresAt: d1("2027-02-28"),
      watcherIds: ["p-esme"], dbs: "clear", dbsAt: d1("2026-08-12"),
      documents: [doc("cv", "S-Bhattacharya-CV.pdf", "2026-07-06T09:15")],
      scorecards: [
        card("p-ruth", "2026-08-13T16:00", { clinical: 4, people: 4, judgement: 4, values: 4 }, "Good fit for the west team. Knows the patch.", "yes"),
        card("p-esme", "2026-08-13T17:30", { clinical: 4, people: 4, judgement: 3, values: 5 }, "Yes from me. Would want her paired with Ruth for the first month.", "yes"),
      ],
      references: [
        ref("Priti Raval", "Ward manager, Riverside", "received", "2026-08-14", { receivedAt: at("2026-08-17T09:40"), note: "Two years on Bramble, reliable, left to travel." }),
        ref("Alan Kerr", "Practice educator", "received", "2026-08-14", { receivedAt: at("2026-08-20T14:15"), note: "Supported her preceptorship. No concerns." }),
      ] }),

  /* ── Offer: one out with the candidate, one accepted ─────────────────── */
  A("2990", "Freya Lindsay", "Band 6 community nurse, prescriber", "Community, north", "rq-1042", "offer", "nhs_jobs", "2026-06-29T12:00", "2026-08-24T10:30",
    { ownerId: "p-nadia", firstReadAt: at("2026-06-29T15:30"), rightToWork: "verified", salaryExpectationMinor: 3700000, registration: "NMC 15L9902", registrationExpiresAt: d1("2027-06-30"),
      watcherIds: ["p-esme", "p-amara"], dbs: "clear", dbsAt: d1("2026-08-19"),
      documents: [doc("cv", "F-Lindsay-CV.pdf", "2026-06-29T12:00")],
      scorecards: [
        card("p-esme", "2026-08-11T15:20", { clinical: 4, people: 5, judgement: 4, values: 4 }, "Strong. Wants the north patch specifically, which is the seat we are struggling to fill.", "yes"),
        card("p-ruth", "2026-08-11T17:00", { clinical: 4, people: 4, judgement: 4, values: 5 }, "No hesitation.", "yes"),
      ],
      references: [
        ref("Gillian Frayne", "Current line manager", "received", "2026-08-12", { receivedAt: at("2026-08-14T10:05"), note: "Would re-employ without question." }),
        ref("Dr Neil Ashworth", "GP partner", "received", "2026-08-12", { receivedAt: at("2026-08-18T16:30"), note: "Confident lone worker." }),
      ],
      offer: { salaryMinor: 3679600, startAt: d0("2026-10-05"), sentAt: at("2026-08-24T10:30"), expiresAt: d1("2026-08-31"), acceptedAt: null, note: "Band 6 bottom point plus recruitment premium agreed with Amara." } }),
  A("2991", "Damian Oyelaran", "Ward clerk, twelve months' NHS admin", "Riverside", "rq-1067", "offer", "referral", "2026-07-20T08:30", "2026-08-21T11:00",
    { ownerId: "p-joel", firstReadAt: at("2026-07-20T10:00"), rightToWork: "verified", salaryExpectationMinor: 2480000,
      watcherIds: ["p-marek"], dbs: "clear", dbsAt: d1("2026-08-15"),
      documents: [doc("cv", "D-Oyelaran-CV.pdf", "2026-07-20T08:30")],
      scorecards: [card("p-david", "2026-08-14T14:00", { clinical: 4, people: 5, judgement: 4, values: 4 }, "Exactly what Bramble needs while Kemi is seconded.", "yes")],
      references: [
        ref("Tessa Marlowe", "Ward manager", "received", "2026-08-14", { receivedAt: at("2026-08-17T13:20"), note: "No concerns." }),
        ref("Ben Adeyeye", "Team leader, bank office", "received", "2026-08-14", { receivedAt: at("2026-08-18T09:10"), note: "Reliable on late notice." }),
      ],
      offer: { salaryMinor: 2407100, startAt: d0("2026-09-28"), sentAt: at("2026-08-21T11:00"), expiresAt: d1("2026-08-28"), acceptedAt: at("2026-08-27T19:40"), note: "Fixed term to 27 September 2027." } }),

  /* ── Settled ────────────────────────────────────────────────────────── */
  A("2870", "Adaeze Nwankwo", "Night nurse practitioner", "Riverside", "rq-1039", "hired", "nhs_jobs", "2026-05-26T18:00", "2026-08-07T09:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-05-27T08:40"), rightToWork: "verified", registration: "NMC 12N7745", registrationExpiresAt: d1("2026-10-15"), dbs: "clear", dbsAt: d1("2026-07-20"),
      documents: [doc("cv", "A-Nwankwo-CV.pdf", "2026-05-26T18:00")],
      offer: { salaryMinor: 4650000, startAt: d0("2026-09-07"), sentAt: at("2026-07-28T10:00"), expiresAt: d1("2026-08-04"), acceptedAt: at("2026-08-03T12:15"), note: "Nights premium in the contract." } }),
  A("3010", "Peter Halloran", "Physiotherapist, band 5", "Eastgate", "rq-1051", "declined", "nhs_jobs", "2026-07-16T09:00", "2026-08-12T16:00",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-16T12:00"), rightToWork: "verified", registration: "HCPC PH120664", registrationExpiresAt: d1("2027-04-30"),
      lastLiveStage: "assessment", declineReason: "experience", wouldReconsider: true, declineNote: "A year off the senior bar rather than the wrong person — no rotational MSK yet. Fern would see him for the band 5 seat when it comes back in the autumn.",
      decidedAt: at("2026-08-12T16:00"), decidedById: "p-nadia",
      documents: [doc("cv", "P-Halloran-CV.pdf", "2026-07-16T09:00")] }),
  A("3011", "Ines Duarte", "Integration engineer", "Remote, Porto", "rq-1063", "declined", "agency", "2026-08-05T22:10", "2026-08-20T10:00",
    { ownerId: "p-joel", firstReadAt: at("2026-08-06T09:00"), rightToWork: "needs_sponsorship",
      lastLiveStage: "screening", declineReason: "money", wouldReconsider: true, declineNote: "Expectation £68k against a band 7 top of £52,809. Told her plainly. Worth a call if the band 8a architecture post is funded.",
      decidedAt: at("2026-08-20T10:00"), decidedById: "p-joel",
      documents: [doc("cv", "I-Duarte-CV.pdf", "2026-08-05T22:10")] }),
  A("3012", "Tom Sefton", "Ward administrator", "Riverside", "rq-1067", "declined", "careers_site", "2026-08-18T13:40", "2026-08-24T09:30",
    { ownerId: "p-joel", firstReadAt: at("2026-08-19T08:30"), rightToWork: "documents_seen",
      lastLiveStage: "screening", declineReason: "stronger_field", wouldReconsider: false, declineNote: "Four stronger applications for one twelve-month seat. Nothing against him; the field was simply better.",
      decidedAt: at("2026-08-24T09:30"), decidedById: "p-joel",
      documents: [doc("cv", "T-Sefton-CV.pdf", "2026-08-18T13:40")] }),
  A("3013", "Lila Farid", "Community nurse, band 5", "Community, west", "rq-1042", "withdrawn", "careers_site", "2026-07-27T10:20", "2026-08-26T08:15",
    { ownerId: "p-nadia", firstReadAt: at("2026-07-27T14:00"), rightToWork: "verified", registration: "NMC 21F0093", registrationExpiresAt: d1("2027-08-31"),
      lastLiveStage: "panel", withdrawReason: "another_offer", wouldReconsider: true,
      withdrawNote: "Took a band 5 post nearer home before our panel date. Asked to be kept on file for the west patch — she lives on it.",
      decidedAt: at("2026-08-26T08:15"), decidedById: "p-nadia",
      documents: [doc("cv", "L-Farid-CV.pdf", "2026-07-27T10:20")] }),
];

/* ── Interviews ───────────────────────────────────────────────────────────
   One booked conversation, one row. `kind` names what it is for and matches the
   stage that books it, so the ladder reads against the stepper. */
const IV = (id, applicationId, kind, iso, minutes, panelIds, status, extra) => Object.assign({
  id: "iv-" + id, applicationId, kind, at: at(iso), minutes, panelIds, status,
  where: "Larkspur House, room 2.4", note: null,
}, extra || {});

const INTERVIEWS = [
  IV("501", "ap-3088", "screening", "2026-08-28T11:30", 30, ["p-nadia"], "scheduled", { where: "Telephone" }),
  IV("502", "ap-3089", "screening", "2026-08-28T15:00", 30, ["p-joel"], "scheduled", { where: "Telephone" }),
  IV("503", "ap-3090", "screening", "2026-08-31T09:30", 30, ["p-nadia"], "scheduled", { where: "Telephone" }),
  IV("504", "ap-3072", "assessment", "2026-09-01T13:00", 90, ["p-fern"], "scheduled", { where: "Eastgate Clinic, therapies office" }),
  IV("505", "ap-3073", "assessment", "2026-09-02T10:00", 90, ["p-tomas", "p-david"], "scheduled", { where: "Larkspur House, room 1.2" }),
  IV("506", "ap-3051", "panel", "2026-08-26T14:00", 60, ["p-esme", "p-ruth"], "complete", { where: "Community hub, north" }),
  IV("507", "ap-3053", "panel", "2026-08-25T11:00", 60, ["p-david", "p-tomas"], "complete", { where: "Larkspur House, room 2.4" }),
  IV("508", "ap-3052", "panel", "2026-08-27T13:30", 60, ["p-esme", "p-ruth"], "awaiting_feedback", { where: "Community hub, north" }),
  IV("509", "ap-3074", "assessment", "2026-08-26T09:30", 90, ["p-esme"], "awaiting_feedback", { where: "Community hub, north" }),
  IV("510", "ap-3091", "screening", "2026-08-25T16:00", 30, ["p-joel"], "no_show", { where: "Telephone", note: "Rang twice, no answer, voicemail left. He is bank staff on shift — Marek is finding another slot." }),
];

/* ── The thread ───────────────────────────────────────────────────────────
   Notes on an application, in the order they were written. This is where the
   desk actually talks: a mention pulls somebody in, a reaction is how the rest
   of the panel agrees without writing "agreed", and `seenBy` is who has read it.
   Every note here is dated after what it names and says what the record says. */
const N = (id, applicationId, authorId, iso, body, extra) => Object.assign({
  id: "nt-" + id, applicationId, authorId, at: at(iso), body, kind: "note", parentId: null,
  reactions: {}, seenBy: [],
}, extra || {});

const NOTES = [
  /* Priya Raghunathan — the application at panel, and the one being decided. */
  N("801", "ap-3051", "p-nadia", "2026-07-25T08:52",
    "Referred by @p-esme. Eight years community, independent prescriber, currently at Meadowvale on the same band. Lives on the north patch — that is the seat we have had open since April.",
    { seenBy: ["p-esme", "p-ruth", "p-amara"], reactions: { watching: ["p-esme", "p-ruth"] } }),
  N("802", "ap-3051", "p-esme", "2026-07-25T12:10",
    "Worth saying out loud: I worked with her at Meadowvale for two years, so I will sit on the panel but I am not scoring the values section on my own. @p-ruth to chair.",
    { kind: "decision", seenBy: ["p-nadia", "p-ruth", "p-amara"], reactions: { agree: ["p-nadia", "p-ruth", "p-amara"] } }),
  N("803", "ap-3051", "p-marek", "2026-08-18T16:30",
    "Caseload scenario back in, attached to the file. She sent it two days early.",
    { seenBy: ["p-nadia"], reactions: { agree: ["p-nadia"] } }),
  N("804", "ap-3051", "p-ruth", "2026-08-26T17:52",
    "Scorecard in — 4.5 average, yes from me. The Friday afternoon question is the one I always ask and she is the first this year to say she would ring the out-of-hours GP before the family.",
    { kind: "decision", seenBy: ["p-nadia", "p-esme", "p-amara"], reactions: { agree: ["p-esme", "p-nadia"], watching: ["p-amara"] } }),
  N("805", "ap-3051", "p-nadia", "2026-08-27T09:15",
    "Both scorecards are in, so checks can start. References went out this morning to Hannah Iversen and Dr Marsh, and the disclosure is with the DBS service. Nothing moves to offer until both references are back — @p-amara, the band 6 premium is the same shape as Freya Lindsay's if we get there.",
    { seenBy: ["p-esme", "p-amara"], reactions: { agree: ["p-esme"], question: ["p-amara"] } }),
  N("806", "ap-3051", "p-amara", "2026-08-27T13:40",
    "Same shape is fine. Two band 6 premiums on one requisition is the limit for this year, so if the third seat needs one it comes to me first.",
    { parentId: "nt-805", kind: "decision", seenBy: ["p-nadia", "p-esme"], reactions: { agree: ["p-nadia"] } }),
  N("807", "ap-3051", "p-esme", "2026-08-28T08:05",
    "She rang me directly — wants to know whether the north team covers the Thornbury villages, because it changes her drive. It does. Told her nothing about where we are with the decision.",
    { kind: "candidate", seenBy: ["p-nadia"], reactions: { agree: ["p-nadia"] } }),

  /* Jonah Okwu — the panel that is one scorecard short. */
  N("810", "ap-3052", "p-ruth", "2026-08-27T18:26",
    "Mine is in but I would not move him on my card alone — @p-esme saw the second half of the interview and the lone-working question is hers.",
    { seenBy: ["p-esme", "p-nadia"], reactions: { agree: ["p-nadia"] } }),
  N("811", "ap-3052", "p-nadia", "2026-08-28T08:40",
    "Chased the second scorecard. He is a strong band 5 for the acute floor if the community caseload is too early for him — that is a reopen for another seat, not a decline.",
    { seenBy: ["p-esme"], reactions: { watching: ["p-esme"] } }),

  /* Elias Nordahl — checks, waiting on one referee. */
  N("815", "ap-3020", "p-marek", "2026-08-26T11:05",
    "Second referee is out of office until Monday 31st. Chased today; Marianne's reference came back clean on the 21st. Right to work and DBS both done.",
    { seenBy: ["p-nadia", "p-fern"], reactions: { agree: ["p-fern"] } }),
  N("816", "ap-3020", "p-fern", "2026-08-26T15:20",
    "He has asked twice about a start date. Can we tell him the week of the 12th October assuming the reference lands Monday? @p-nadia",
    { kind: "candidate", seenBy: ["p-nadia"], reactions: { question: ["p-nadia"] } }),
  N("817", "ap-3020", "p-nadia", "2026-08-27T09:50",
    "I will tell him the week of the 12th is what we are aiming at and that it depends on one outstanding reference. No offer letter until it is in — that is the rule and it has saved us twice.",
    { parentId: "nt-816", kind: "decision", seenBy: ["p-fern", "p-marek"], reactions: { agree: ["p-fern", "p-marek"] } }),

  /* Freya Lindsay — offer out, expiring. */
  N("820", "ap-2990", "p-nadia", "2026-08-24T10:44",
    "Offer sent: £36,796 to start 5 October, valid to 31 August. She asked for the north patch and that is what is in the letter.",
    { kind: "decision", seenBy: ["p-esme", "p-amara"], reactions: { agree: ["p-esme", "p-amara"] } }),
  N("821", "ap-2990", "p-esme", "2026-08-27T16:15",
    "Three days left on it. She was straight with me that she has one other conversation running. Worth a call tomorrow rather than an email.",
    { seenBy: ["p-nadia"], reactions: { agree: ["p-nadia"], watching: ["p-amara"] } }),

  /* Dilnoza Rakhimova — unread, and there is a sponsorship question. */
  N("825", "ap-3103", "p-joel", "2026-08-27T08:20",
    "Flagging before anyone reads it: sponsorship needed and we have no certificate left on the digital establishment this year. Do not screen her in without @p-amara.",
    { seenBy: ["p-amara"], reactions: { watching: ["p-amara"] } }),
];

/* ── Job adverts ──────────────────────────────────────────────────────────
   One advert per requisition: the wording that produces the pipeline, and where
   it is posted. `views` is the only figure held here, because it comes from
   outside the product — the applications a channel produced are counted from the
   applications array by `source`, never stored twice. */
const AD = (requisitionId, headline, body, essentials, desirables, updatedISO, updatedById, postedTo) => ({
  requisitionId, headline, body, essentials, desirables,
  updatedAt: at(updatedISO), updatedById, postedTo,
});
const post = (channel, postedISO, views) => ({ channel, postedAt: d0(postedISO), views });

const ADVERTS = [
  AD("rq-1042", "Community staff nurse — north and west teams",
    "Three band 5 nurses to join the community teams covering the north and west of the county. You will hold your own caseload of around eighteen patients, work alongside a matron and a locality GP, and spend most of your week in people's homes rather than on a ward.\n\nWe are honest about the drive: the north patch reaches the Thornbury villages and a car is essential. In return the rota is nine to five with no nights, and every nurse gets a protected study afternoon a fortnight.",
    ["Current NMC registration", "Full UK driving licence and use of a car", "At least a year post-preceptorship"],
    ["Community or district nursing experience", "Independent prescriber", "Wound care or diabetes special interest"],
    "2026-07-02T11:20", "p-nadia",
    [post("careers_site", "2026-06-30", 1840), post("nhs_jobs", "2026-06-30", 2610), post("event", "2026-07-18", 96)]),
  AD("rq-1051", "Senior physiotherapist — MSK outpatients, Eastgate",
    "A band 7 physiotherapist for the musculoskeletal service at Eastgate Clinic. Mixed caseload, first-contact clinics two days a week, and supervision of two band 5s and a rotational band 6.\n\nThe service runs a Friday injection clinic and we would want you comfortable taking that on within six months, with the training paid for.",
    ["Current HCPC registration", "Postgraduate MSK experience", "Experience supervising junior staff"],
    ["First-contact practitioner qualification", "Injection therapy", "Research or audit experience"],
    "2026-07-27T09:05", "p-nadia",
    [post("careers_site", "2026-07-27", 640), post("nhs_jobs", "2026-07-27", 1180)]),
  AD("rq-1063", "Clinical systems engineer — shared care record",
    "Integration work on the shared care record that joins our hospital, the community teams and forty GP practices. HL7 and FHIR, a message queue that has to be right, and clinicians who will tell you plainly when it is not.\n\nOn-call is one week in six and paid as such. Hybrid: two days at Larkspur House, three wherever you work best.",
    ["Integration experience with HL7 v2 or FHIR", "Comfortable being the person who owns an incident", "Right to work in the UK without sponsorship"],
    ["NHS or healthcare experience", "Clinical safety (DCB0129) awareness", "Python or Java"],
    "2026-08-11T16:40", "p-joel",
    [post("careers_site", "2026-08-10", 410), post("nhs_jobs", "2026-08-10", 760), post("agency", "2026-08-14", 0)]),
  AD("rq-1067", "Ward clerk — Bramble ward, twelve months",
    "Twelve-month cover for a secondment on Bramble ward. You are the person who knows where everything is: notes, discharge letters, the consultant's clinic list and which porter to ring.\n\nEarly shifts, no weekends, and the ward team eats lunch together.",
    ["Administrative experience in a busy office", "Confident on the phone with anxious relatives"],
    ["NHS ward or clinic experience", "Familiar with electronic patient records"],
    "2026-08-17T10:15", "p-joel",
    [post("careers_site", "2026-08-17", 220), post("nhs_jobs", "2026-08-17", 340)]),
  AD("rq-1058", "Occupational therapist — community, band 6",
    "Community occupational therapy across the county, with a caseload weighted towards falls prevention and discharge to assess.\n\nThis advert is unpublished while the establishment review runs. It is kept here so the wording is not written twice when it comes back.",
    ["Current HCPC registration", "Community or intermediate care experience"],
    ["Falls prevention", "Moving and handling trainer"],
    "2026-07-13T14:00", "p-nadia", []),
  AD("rq-1039", "Night nurse practitioner — Riverside acute floor",
    "Nights on the acute floor, leading a team of four and working alongside the on-call registrar. Filled in August 2026.",
    ["Current NMC registration", "Advanced clinical practice qualification or in training", "Acute experience"],
    ["Non-medical prescribing", "Simulation teaching"],
    "2026-05-18T09:30", "p-nadia",
    [post("nhs_jobs", "2026-05-18", 1420), post("careers_site", "2026-05-18", 510)]),
];

/* ── Notifications ────────────────────────────────────────────────────────
   Each one names a record in the arrays above, routes to it, and carries who
   caused it. `readAt` is per notification rather than one "all read" flag, so
   the bell's count and the inbox cannot disagree — both count the same array. */
const NOTE_ROUTE = (id) => "/applications/" + id;
const NOTIFICATIONS = [
  { id: "n1", kind: "mention", actorId: "p-amara", at: at("2026-08-27T13:40"), readAt: null,
    title: "Amara Boateng replied to your note",
    body: "Two band 6 premiums on one requisition is the limit for this year — a third comes to me first.",
    route: NOTE_ROUTE("ap-3051") },
  { id: "n2", kind: "offer", actorId: null, at: at("2026-08-27T19:40"), readAt: null,
    title: "Damian Oyelaran accepted",
    body: "Ward clerk, £24,071, starting 28 September. Record the hire to close the seat.",
    route: NOTE_ROUTE("ap-2991") },
  { id: "n3", kind: "offer", actorId: null, at: at("2026-08-28T07:00"), readAt: null,
    title: "Freya Lindsay's offer runs out on Monday",
    body: "Sent 24 August, valid to 31 August. No answer yet, and Esme thinks a call beats an email.",
    route: NOTE_ROUTE("ap-2990") },
  { id: "n4", kind: "compliance", actorId: null, at: at("2026-08-28T06:05"), readAt: null,
    title: "Elias Nordahl's HCPC registration renews 14 September",
    body: "He is at checks with one reference outstanding. Nobody starts on a lapsed registration.",
    route: NOTE_ROUTE("ap-3020") },
  { id: "n5", kind: "panel", actorId: "p-esme", at: at("2026-08-28T06:05"), readAt: null,
    title: "A scorecard is outstanding for Jonah Okwu",
    body: "Panel was 27 August. Ruth has filed hers; the second card is what the decision waits on.",
    route: NOTE_ROUTE("ap-3052") },
  { id: "n6", kind: "application", actorId: null, at: at("2026-08-28T06:00"), readAt: null,
    title: "Three applications arrived overnight",
    body: "Aisha Farrow and Callum Beattie for the community nurse seats, Dilnoza Rakhimova for the systems engineer.",
    route: "/applications" },
  { id: "n7", kind: "interview", actorId: null, at: at("2026-08-28T08:00"), readAt: null,
    title: "Screening call with Grace Lindqvist at 11:30",
    body: "Telephone, 30 minutes, yours. She is returning to community nursing after two years abroad.",
    route: "/interviews" },
  { id: "n8", kind: "panel", actorId: "p-ruth", at: at("2026-08-26T17:52"), readAt: at("2026-08-27T08:10"),
    title: "Ruth Calloway scored Priya Raghunathan",
    body: "4.5 average, recommends yes. Both cards are now in, so checks can start.",
    route: NOTE_ROUTE("ap-3051") },
  { id: "n9", kind: "mention", actorId: "p-fern", at: at("2026-08-26T15:20"), readAt: at("2026-08-27T08:11"),
    title: "Fern Adeyemi asked you about a start date",
    body: "Can we tell Elias the week of the 12th October, assuming the reference lands Monday?",
    route: NOTE_ROUTE("ap-3020") },
  { id: "n10", kind: "compliance", actorId: null, at: at("2026-08-25T06:05"), readAt: at("2026-08-25T09:30"),
    title: "Nkechi Ibrahim's NMC registration renews 30 September",
    body: "She is at screening, so nothing is blocked yet. It will be if she reaches checks.",
    route: NOTE_ROUTE("ap-3090") },
];

Object.assign(window, {
  DAY_MS, d0, d1, at, RD_NOW, RD_ORG, STAFF, RD_SESSION,
  REQUISITIONS, APPLICATIONS, INTERVIEWS, NOTES, ADVERTS, NOTIFICATIONS,
  portraitFor, initialsOf,
});

</script>
<script type="text/plain" data-rd-file="metrics.js">
/* Recruitment Desk — every figure in the product. If a screen shows a number,
   it came from a function here, called with the array the screen is displaying.

   Four declarations this product makes once, and everywhere:
   · Time in stage. Counted in whole days from `stageEnteredAt` against that
     stage's own service level in RD_POLICY.stageDays — never one number for
     every stage, and never stored.
   · Money. Sterling only, held in minor units, formatted by one formatter. A
     band is a pair; an offer is a point inside it, and the product says where.
   · Progress. A step in the stepper is `done`, `current`, `todo` or `stopped`,
     decided by the record's stage against APPLICATION_PATH — never by counting
     how far down a list a step sits.
   · Evidence. A completed step shows the sub-record that completed it: the
     screening call, the returned exercise, the last scorecard, the references,
     the offer letter. Nothing is asserted that is not in the fixture. */

const DAY = 864e5;
const byId = (list, id) => (list || []).find((r) => r.id === id) || null;
const staffById = (staff, id) => byId(staff, id);
const applicationById = (applications, id) => byId(applications, id);
const requisitionById = (requisitions, id) => byId(requisitions, id);
const firstNameOf = (person) => (person ? person.name.split(" ")[0] : "somebody");

const sumBy = (list, fn) => list.reduce((total, item) => total + fn(item), 0);
const plural = (n, one, many) => n + " " + (n === 1 ? one : many);
const share = (part, whole) => (whole ? part / whole : 0);
const avg = (list, fn) => (list.length ? sumBy(list, fn) / list.length : null);
/* A status field's own words, mid-sentence. Status enums are `{id, label, tone}`
   and are read with statusValue — labelOf matches on `.value` and would silently
   hand back the raw id, which is how "DBS not_started" once reached a reader. */
const statusWords = (values, id) => statusValue(values, id).label.toLowerCase();
/* ONE percent spelling, product-wide. Everything here takes a ratio, never a
   pre-multiplied number, so no screen can round it its own way. Whole numbers,
   because every share in this product is over dozens of records — except a
   conversion rate against channel views, which is over thousands and would
   round to 0%, and asks for `precise`. */
const percent = (ratio, precise) => (ratio === null || ratio === undefined ? "—"
  : (precise ? (ratio * 100).toFixed(1) : String(Math.round(ratio * 100))) + "%");

/* ── Money ───────────────────────────────────────────────────────────────── */
const GBP = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", maximumFractionDigits: 0 });
const money = (minor) => GBP.format(Math.round(minor / 100));
const moneyShort = (minor) => {
  const major = Math.round(minor / 100);
  return Math.abs(major) >= 1000 ? "£" + (major / 1000).toFixed(major >= 10000 ? 0 : 1) + "k" : money(minor);
};
const bandLabel = (requisition) => money(requisition.bandMinor[0]) + " – " + money(requisition.bandMinor[1]);
/* Where an offer sits inside the band it came from, as a share of the band. */
const bandPosition = (requisition, minor) => {
  const [low, high] = requisition.bandMinor;
  return high === low ? 1 : Math.max(0, Math.min(1, (minor - low) / (high - low)));
};

/* ── Dates. One absolute spelling for calendar dates; the distance rules in
   lib/ui-runtime.js govern event times — a note, a scorecard, an offer. ───── */
const formatDay = (ts) => (ts ? new Date(ts).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : "—");
const formatDayShort = (ts) => (ts ? new Date(ts).toLocaleDateString("en-GB", { day: "numeric", month: "short" }) : "—");
const formatWeekday = (ts) => new Date(ts).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
const formatTime = (ts) => new Date(ts).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
const dayKey = (ts) => { const d = new Date(ts); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); };
const startOfDay = (ts) => { const d = new Date(ts); d.setHours(0, 0, 0, 0); return d.getTime(); };
/* "Today", "Tomorrow", then the weekday — one spelling, product-wide. */
const dayLabel = (ts, now) => {
  const days = Math.round((startOfDay(ts) - startOfDay(now)) / DAY);
  if (days === 0) return "Today";
  if (days === 1) return "Tomorrow";
  if (days === -1) return "Yesterday";
  return formatWeekday(ts);
};
/* One spelling for every duration in the product. Direction is in the words. */
const daysBetween = (a, b) => Math.round((startOfDay(b) - startOfDay(a)) / DAY);
const durationWords = (days) => Math.abs(days) + " " + (Math.abs(days) === 1 ? "day" : "days") + (days < 0 ? " over" : " left");

/* ── One application, read properly ─────────────────────────────────────── */
const daysInStage = (application, now) => Math.max(0, daysBetween(application.stageEnteredAt, now));
const stageAllowance = (application) => RD_POLICY.stageDays[application.stage] || null;
const stageOverBy = (application, now) => {
  const allowed = stageAllowance(application);
  return allowed === null ? 0 : daysInStage(application, now) - allowed;
};
const daysSinceApplied = (application, now) => Math.max(0, daysBetween(application.appliedAt, now));
const stageIndex = (stage) => APPLICATION_PATH.indexOf(stage);

/* The state of one step for one application. `stopped` is where a declined or
   withdrawn application came to rest, which is why the stepper still reads. */
function stepState(application, stage) {
  const here = stageIndex(application.stage);
  const there = stageIndex(stage);
  if (here > -1) return there < here ? "done" : there === here ? "current" : "todo";
  const stopped = stageIndex(application.lastLiveStage || "applied");
  return there < stopped ? "done" : there === stopped ? "stopped" : "todo";
}

/* What actually completed a step, from the sub-records — never a claim the
   fixture cannot support. A step with no evidence says what it is for instead. */
function stepEvidence(application, stage, data, now) {
  const interviews = interviewsFor(data.interviews, application.id);
  if (stage === "applied") return "Applied " + formatDayShort(application.appliedAt) + " · " + labelOf(SOURCES, application.source);
  if (stage === "screening") {
    const call = interviews.filter((iv) => iv.kind === "screening")[0];
    return call ? "Screening call " + formatDayShort(call.at) + " with " + firstNameOf(staffById(data.staff, call.panelIds[0])) : null;
  }
  if (stage === "assessment") {
    const returned = application.documents.filter((d) => d.kind === "case")[0];
    const booked = interviews.filter((iv) => iv.kind === "assessment")[0];
    if (returned) return "Exercise returned " + formatDayShort(returned.at);
    return booked ? "Booked " + formatWeekday(booked.at) : null;
  }
  if (stage === "panel") {
    const score = scoresOf(application);
    if (!score.count) return null;
    return plural(score.count, "scorecard", "scorecards") + " · " + score.average.toFixed(1) + " average";
  }
  if (stage === "checks") {
    const refs = referenceState(application);
    if (!refs.requested) return null;
    return refs.received + " of " + refs.required + " references back · right to work " + statusWords(RIGHT_TO_WORK, application.rightToWork);
  }
  if (stage === "offer" && application.offer) {
    return application.offer.acceptedAt
      ? "Accepted " + formatDayShort(application.offer.acceptedAt)
      : money(application.offer.salaryMinor) + " sent " + formatDayShort(application.offer.sentAt) + " · " + durationWords(daysBetween(now, application.offer.expiresAt));
  }
  if (stage === "hired" && application.offer && application.offer.acceptedAt) {
    return "Starting " + formatDay(application.offer.startAt);
  }
  return null;
}

/* ── Panel scorecards ───────────────────────────────────────────────────── */
function scoresOf(application) {
  const cards = application.scorecards || [];
  const areaAverage = {};
  SCORE_AREAS.forEach((area) => { areaAverage[area.value] = avg(cards, (c) => c.scores[area.value]); });
  const cardAverage = (c) => avg(SCORE_AREAS, (a) => c.scores[a.value]);
  return {
    count: cards.length,
    cards,
    required: RD_POLICY.panelScorecards,
    average: cards.length ? avg(cards, cardAverage) : null,
    areaAverage,
    cardAverage,
    yeses: cards.filter((c) => c.recommend === "yes").length,
    complete: cards.length >= RD_POLICY.panelScorecards,
  };
}

/* ── References and checks ──────────────────────────────────────────────── */
function referenceState(application) {
  const refs = application.references || [];
  const received = refs.filter((r) => r.status === "received");
  return {
    refs,
    requested: refs.length,
    required: RD_POLICY.referencesRequired,
    received: received.length,
    outstanding: refs.filter((r) => r.status !== "received"),
    complete: received.length >= RD_POLICY.referencesRequired,
  };
}

/* What an offer is waiting on, as a list of named things — the same list the
   disabled primary action states as its reason. */
function checksMissing(application) {
  const refs = referenceState(application);
  const missing = [];
  if (!refs.complete) missing.push(refs.received + " of " + refs.required + " references back");
  if (application.rightToWork !== "verified") missing.push("right to work " + statusWords(RIGHT_TO_WORK, application.rightToWork));
  if (application.dbs !== "clear") missing.push("DBS " + statusWords(DBS_STATUS, application.dbs));
  return missing;
}

/* ── Offers: the queue, and the clock on each one ────────────────────────── */
const offersOut = (applications) => applications
  .filter((a) => a.stage === "offer" && a.offer)
  .sort((a, b) => a.offer.expiresAt - b.offer.expiresAt);
const offersAwaitingAnswer = (applications) => offersOut(applications).filter((a) => !a.offer.acceptedAt);
const offersExpiringSoon = (applications, now) => offersAwaitingAnswer(applications)
  .filter((a) => a.offer.expiresAt - now <= RD_POLICY.offerExpiryWarnDays * 864e5);
/* One row shape for the offer queue, so the table, the tiles and the toast copy
   read the same numbers. */
function offerRow(application, requisitions, now) {
  const requisition = requisitionById(requisitions, application.requisitionId);
  const offer = application.offer;
  return {
    application, requisition, offer,
    daysLeft: daysBetween(now, offer.expiresAt),
    position: bandPosition(requisition, offer.salaryMinor),
    metExpectation: !application.salaryExpectationMinor || offer.salaryMinor >= application.salaryExpectationMinor,
    daysToStart: daysBetween(now, offer.startAt),
    answered: !!offer.acceptedAt,
  };
}

/* ── Compliance: three things, one predicate, one row per live application ──
   The row takes the requisitions array because whether a registration is owed
   at all belongs to the seat, not to the person. */
function complianceRow(application, requisitions, now) {
  const requisition = requisitionById(requisitions, application.requisitionId);
  const blockers = [];
  if (COMPLIANCE_FLAGS.rtwOutstanding(application)) blockers.push("right to work " + statusWords(RIGHT_TO_WORK, application.rightToWork));
  if (COMPLIANCE_FLAGS.dbsOutstanding(application)) blockers.push("DBS " + statusWords(DBS_STATUS, application.dbs));
  /* One guard, so the blockers a reader sees and the clear/blocked split the
     screen recesses on are the same three flags read once. */
  if (COMPLIANCE_FLAGS.registrationOutstanding(application, requisition, now)) {
    if (COMPLIANCE_FLAGS.registrationMissing(application, requisition)) blockers.push("no " + requisition.registration + " registration on file");
    else if (COMPLIANCE_FLAGS.registrationExpired(application, now)) blockers.push("registration expired " + formatDayShort(application.registrationExpiresAt));
    else blockers.push("registration renews " + formatDayShort(application.registrationExpiresAt));
  }
  return {
    application, requisition, blockers,
    /* What this seat asks for, so the cell can say "not required" and mean it. */
    registerNeeded: COMPLIANCE_FLAGS.registrationRequired(requisition) ? requisition.registration : null,
    clear: COMPLIANCE_FLAGS.clearToStart(application, requisition, now),
    registrationDays: application.registrationExpiresAt ? daysBetween(now, application.registrationExpiresAt) : null,
    /* Urgency is the stage, not the number of blockers: a blocker on somebody at
       offer stops a start date, the same blocker at applied stops nothing yet. */
    urgent: stageIndex(application.stage) >= stageIndex("checks") && blockers.length > 0,
  };
}
const complianceRows = (applications, requisitions, now) => liveApplications(applications)
  .map((a) => complianceRow(a, requisitions, now))
  .sort((a, b) => (b.urgent - a.urgent) || (b.blockers.length - a.blockers.length) || stageIndex(b.application.stage) - stageIndex(a.application.stage));

/* ── Adverts: the wording, and what each channel actually produced ────────
   Views are the only stored figure (they come from outside the product); the
   applications a channel produced are counted from the applications array. */
const advertFor = (adverts, requisitionId) => adverts.find((ad) => ad.requisitionId === requisitionId) || null;
function channelRows(advert, applications, now) {
  const all = applicationsFor(applications, advert.requisitionId);
  return advert.postedTo.map((posting) => {
    const rows = all.filter((a) => a.source === posting.channel && a.appliedAt >= posting.postedAt);
    const offered = rows.filter((a) => a.offer && a.offer.sentAt);
    return {
      posting,
      source: SOURCES.find((s) => s.value === posting.channel),
      rows,
      applications: rows.length,
      views: posting.views,
      rate: posting.views ? rows.length / posting.views : null,
      live: rows.filter(APPLICATION_FLAGS.isLive).length,
      offered: offered.length,
      days: Math.max(1, daysBetween(posting.postedAt, now)),
    };
  }).sort((a, b) => b.applications - a.applications);
}
/* The advert's own totals, summed over the rows it renders — and the
   applications that arrived from a channel the advert is not posted to, named
   rather than quietly dropped. */
function advertTotals(advert, applications, now) {
  const rows = channelRows(advert, applications, now);
  const all = applicationsFor(applications, advert.requisitionId);
  const counted = sumBy(rows, (r) => r.applications);
  return {
    rows,
    views: sumBy(rows, (r) => r.views),
    applications: counted,
    rate: sumBy(rows, (r) => r.views) ? counted / sumBy(rows, (r) => r.views) : null,
    elsewhere: all.length - counted,
    posted: advert.postedTo.length,
  };
}

/* The context advanceGate needs, derived rather than passed around by hand. */
const advanceContext = (application) => ({ scorecards: scoresOf(application).count });
const transitionsFor = (application) => applicationTransitions(application, advanceContext(application));

/* ── Predicates over the applications array ─────────────────────────────── */
const liveApplications = (applications) => applications.filter(APPLICATION_FLAGS.isLive);
/* THE act-ability predicate at desk scale — the queue rows, the tab counts, the
   desk figures and the sidebar badge all end up here. */
const awaitingUs = (applications) => liveApplications(applications).filter(APPLICATION_FLAGS.awaitsUs);
const awaitingThem = (applications) => liveApplications(applications).filter(APPLICATION_FLAGS.awaitsThem);
const unreadApplications = (applications) => applications.filter(APPLICATION_FLAGS.isUnread);
const stalledApplications = (applications, now) => liveApplications(applications).filter((a) => APPLICATION_FLAGS.isStalled(a, now));
const applicationsFor = (applications, requisitionId) => applications.filter((a) => a.requisitionId === requisitionId);
const ownedBy = (applications, personId) => applications.filter((a) => a.ownerId === personId);
const watchedBy = (applications, personId) => applications.filter((a) => (a.watcherIds || []).indexOf(personId) > -1);
const applicationsAtStage = (applications, stage) => applications.filter((a) => a.stage === stage);

/* The pool is a predicate, not a list: people we said we would talk to again. */
const talentPool = (applications) => applications.filter((a) => !APPLICATION_FLAGS.isLive(a) && a.stage !== "hired" && a.wouldReconsider);

/* ── Interviews ─────────────────────────────────────────────────────────── */
const interviewsFor = (interviews, applicationId) => interviews
  .filter((iv) => iv.applicationId === applicationId)
  .sort((a, b) => a.at - b.at);
const feedbackOutstanding = (interviews) => interviews.filter(INTERVIEW_FLAGS.feedbackOutstanding);
const interviewsAhead = (interviews, now) => interviews.filter((iv) => INTERVIEW_FLAGS.isAhead(iv, now)).sort((a, b) => a.at - b.at);
const interviewsToday = (interviews, now) => interviews.filter((iv) => INTERVIEW_FLAGS.isToday(iv, now) && iv.status !== "cancelled").sort((a, b) => a.at - b.at);
/* The ladder groups the same array by day; nothing is filtered twice. */
function interviewDays(interviews) {
  const days = [];
  const index = {};
  interviews.slice().sort((a, b) => a.at - b.at).forEach((iv) => {
    const key = dayKey(iv.at);
    if (!index[key]) { index[key] = { key, at: iv.at, items: [] }; days.push(index[key]); }
    index[key].items.push(iv);
  });
  return days;
}
const panelOf = (staff, interview) => interview.panelIds.map((id) => staffById(staff, id)).filter(Boolean);

/* ── Requisitions ───────────────────────────────────────────────────────── */
function requisitionHealth(applications, requisition, now) {
  const all = applicationsFor(applications, requisition.id);
  const live = liveApplications(all);
  const hires = all.filter((a) => a.stage === "hired").length;
  const offersOut = all.filter((a) => a.stage === "offer" && a.offer && !a.offer.acceptedAt).length;
  const accepted = all.filter((a) => a.offer && a.offer.acceptedAt && a.stage !== "declined" && a.stage !== "withdrawn").length;
  return {
    all, live, hires, offersOut, accepted,
    seats: requisition.headcount,
    seatsLeft: Math.max(0, requisition.headcount - accepted),
    awaitingUs: live.filter(APPLICATION_FLAGS.awaitsUs).length,
    stalled: live.filter((a) => APPLICATION_FLAGS.isStalled(a, now)).length,
    thin: live.length < RD_POLICY.thinPipeline && requisition.status === "open",
    openDays: daysBetween(requisition.openedAt, now),
    aging: REQUISITION_FLAGS.isAging(requisition, now),
    daysToTarget: daysBetween(now, requisition.targetStartAt),
  };
}

/* The stage strip: one segment per path stage, proportional to the live
   applications sitting in it. The strip's own numbers are figures too. */
function stageStrip(applications, requisitionId) {
  const live = liveApplications(applicationsFor(applications, requisitionId));
  const segments = APPLICATION_PATH.filter((s) => s !== "hired").map((stage) => ({
    stage, label: statusValue(APPLICATION_STAGE, stage).label,
    count: live.filter((a) => a.stage === stage).length,
  }));
  return { segments, total: live.length };
}

/* The board: one column per path stage, for one requisition. */
function pipelineBoard(applications, requisitionId) {
  const all = applicationsFor(applications, requisitionId);
  return APPLICATION_PATH.map((stage) => ({
    stage,
    label: statusValue(APPLICATION_STAGE, stage).label,
    means: STAGE_MEANS[stage],
    items: all.filter((a) => a.stage === stage).sort((a, b) => a.stageEnteredAt - b.stageEnteredAt),
  }));
}

/* ── Insights: the funnel over one array, and the rates between its steps ── */
function funnel(applications) {
  const reached = (stage) => applications.filter((a) => {
    const here = stageIndex(a.stage);
    const rest = stageIndex(a.lastLiveStage || a.stage);
    const depth = here > -1 ? here : rest;
    return depth >= stageIndex(stage);
  }).length;
  const rows = APPLICATION_PATH.map((stage, i) => {
    const count = reached(stage);
    const previous = i === 0 ? count : reached(APPLICATION_PATH[i - 1]);
    return {
      stage, label: statusValue(APPLICATION_STAGE, stage).label,
      count, share: share(count, applications.length),
      fromPrevious: i === 0 ? null : share(count, previous),
    };
  });
  return { rows, total: applications.length };
}

/* Time to offer, over the applications that actually reached an offer. */
function timeToOffer(applications) {
  const offered = applications.filter((a) => a.offer && a.offer.sentAt);
  const days = offered.map((a) => daysBetween(a.appliedAt, a.offer.sentAt)).sort((a, b) => a - b);
  return {
    applications: offered,
    count: offered.length,
    median: days.length ? days[Math.floor(days.length / 2)] : null,
    fastest: days.length ? days[0] : null,
    slowest: days.length ? days[days.length - 1] : null,
  };
}

/* Where applications come from, and how far each source gets. */
function sourceMix(applications) {
  return SOURCES.map((source) => {
    const rows = applications.filter((a) => a.source === source.value);
    const offered = rows.filter((a) => a.offer && a.offer.sentAt);
    return { source, rows, count: rows.length, share: share(rows.length, applications.length), offered: offered.length };
  }).filter((row) => row.count > 0).sort((a, b) => b.count - a.count);
}

/* Acceptance: offers sent against offers accepted, over the same array. */
function offerOutcome(applications) {
  const sent = applications.filter((a) => a.offer && a.offer.sentAt);
  const accepted = sent.filter((a) => a.offer.acceptedAt);
  return { sent, accepted, rate: sent.length ? share(accepted.length, sent.length) : null };
}

/* ── The desk itself ────────────────────────────────────────────────────── */
function deskLoad(staff, applications, interviews, now) {
  return staff.filter((person) => person.role === "recruiter" || person.role === "coordinator" || person.role === "lead")
    .map((person) => {
      const owned = liveApplications(ownedBy(applications, person.id));
      return {
        person,
        owned,
        awaitingUs: owned.filter(APPLICATION_FLAGS.awaitsUs).length,
        stalled: owned.filter((a) => APPLICATION_FLAGS.isStalled(a, now)).length,
        interviews: interviews.filter((iv) => iv.panelIds.indexOf(person.id) > -1 && INTERVIEW_FLAGS.isAhead(iv, now)).length,
      };
    });
}
/* Panel load is a different question over a different array, so it is a
   different function — never the same figure relabelled. */
function panelLoad(staff, applications, interviews, now) {
  return staff.filter((person) => person.role === "hiring_manager" || person.role === "panel")
    .map((person) => ({
      person,
      booked: interviews.filter((iv) => iv.panelIds.indexOf(person.id) > -1 && INTERVIEW_FLAGS.isAhead(iv, now)).length,
      outstanding: feedbackOutstanding(interviews).filter((iv) => iv.panelIds.indexOf(person.id) > -1).length,
      watching: watchedBy(applications, person.id).filter(APPLICATION_FLAGS.isLive).length,
    }));
}

/* ── The thread ─────────────────────────────────────────────────────────── */
const notesFor = (notes, applicationId) => notes.filter((n) => n.applicationId === applicationId).sort((a, b) => a.at - b.at);
/* Top-level notes with their replies attached, so the thread renders in one
   pass and a reply can never appear without its parent. */
function threadFor(notes, applicationId) {
  const rows = notesFor(notes, applicationId);
  return rows.filter((n) => !n.parentId).map((n) => ({ note: n, replies: rows.filter((r) => r.parentId === n.id) }));
}
const reactionsOf = (note) => REACTIONS
  .map((reaction) => ({ reaction, who: (note.reactions || {})[reaction.value] || [] }))
  .filter((row) => row.who.length > 0);
const mentionIds = (body) => (body.match(/@p-[a-z]+/g) || []).map((m) => m.slice(1));
/* Everybody who is following this application: the owner, the watchers and
   anybody who has written on it. One list, so the faces and the count agree. */
function followersOf(application, notes, staff) {
  const ids = [];
  const add = (id) => { if (id && ids.indexOf(id) === -1) ids.push(id); };
  add(application.ownerId);
  (application.watcherIds || []).forEach(add);
  notesFor(notes, application.id).forEach((n) => { add(n.authorId); mentionIds(n.body).forEach(add); });
  return ids.map((id) => staffById(staff, id)).filter(Boolean);
}

/* ── Notifications ────────────────────────────────────────────────────────
   One array, read three ways: the bell's count, the inbox's tabs and its day
   groups. Unread is the absence of `readAt`, never a second flag. */
const unreadNotifications = (notifications) => notifications.filter((n) => !n.readAt);
const notificationsOfKind = (notifications, kind) => notifications.filter((n) => n.kind === kind);
function notificationDays(notifications, now) {
  const days = [];
  const index = {};
  notifications.slice().sort((a, b) => b.at - a.at).forEach((n) => {
    const key = dayKey(n.at);
    if (!index[key]) { index[key] = { key, at: n.at, items: [] }; days.push(index[key]); }
    index[key].items.push(n);
  });
  return days;
}
const notificationKind = (n) => NOTIFICATION_KINDS.find((k) => k.value === n.kind) || NOTIFICATION_KINDS[0];

/* ── The sidebar's badges. Each one is a predicate over one array, and the
   label says what it counts, so a reader can recover the number. ─────────── */const NAV_BADGES = {
  awaitingUs: { label: "waiting on the desk", count: (data) => awaitingUs(data.applications).length },
  feedbackOutstanding: { label: "scorecards outstanding", count: (data) => feedbackOutstanding(data.interviews).length },
  offersExpiring: { label: "offers inside " + RD_POLICY.offerExpiryWarnDays + " days of running out", count: (data, now) => offersExpiringSoon(data.applications, now).length },
  complianceUrgent: { label: "blockers on people close to starting", count: (data, now) => complianceRows(data.applications, data.requisitions, now).filter((row) => row.urgent).length },
};

Object.assign(window, {
  DAY, byId, staffById, applicationById, requisitionById, firstNameOf,
  sumBy, plural, share, percent, avg, statusWords, money, moneyShort, bandLabel, bandPosition,
  formatDay, formatDayShort, formatWeekday, formatTime, dayKey, startOfDay, dayLabel, daysBetween, durationWords,
  daysInStage, stageAllowance, stageOverBy, daysSinceApplied, stageIndex, stepState, stepEvidence,
  scoresOf, referenceState, checksMissing, advanceContext, transitionsFor,
  liveApplications, awaitingUs, awaitingThem, unreadApplications, stalledApplications,
  applicationsFor, ownedBy, watchedBy, applicationsAtStage, talentPool,
  interviewsFor, feedbackOutstanding, interviewsAhead, interviewsToday, interviewDays, panelOf,
  requisitionHealth, stageStrip, pipelineBoard, funnel, timeToOffer, sourceMix, offerOutcome,
  offersOut, offersAwaitingAnswer, offersExpiringSoon, offerRow,
  complianceRow, complianceRows, advertFor, channelRows, advertTotals,
  unreadNotifications, notificationsOfKind, notificationDays, notificationKind,
  deskLoad, panelLoad, notesFor, threadFor, reactionsOf, mentionIds, followersOf, NAV_BADGES,
});

</script>
<script type="text/plain" data-rd-file="lib/ui-runtime.js">
/* Shared render runtime — copied per product, domain-free. Status tone, money,
   time and the floating layer live here so no screen assigns a tone or spells a
   duration its own way.

   Local addition, reported in README: Floating / FloatingTip. These portal to the
   document in viewport coordinates, reposition on scroll and resize, flip when
   there is no room, and clamp to the viewport. The design system's own Menu,
   Popover and Tooltip now do all of that too — these remain because they carry a
   different API the products lean on: one element as the trigger, a render
   callback that receives `close`, and no compound Trigger/Popup pair. Reach for
   the design system's overlays in new work. */

const { Badge } = window.OriginDesignSystem_7c998b;

const TONE_VARIANT = { neutral: "secondary", info: "info", success: "success", warning: "warning", danger: "destructive" };

/* The only place a status becomes a label and a colour. */
function StatusBadge({ values, value, size = "sm" }) {
  const found = statusValue(values, value);
  if (!found) { console.warn("[ui-runtime] value not in enum:", value); return null; }
  return <Badge size={size} variant={TONE_VARIANT[found.tone]}>{found.label}</Badge>;
}

const moneyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const formatMoney = (cents) => moneyFormatter.format(cents / 100);
const formatPercent = (ratio) => (ratio === null ? "—" : (ratio * 100).toFixed(1) + "%");
const formatCount = (n) => new Intl.NumberFormat("en-US").format(n);

/* One format rule per column. Distance is absolute: three days ago and three
   days ahead format alike. Direction is never carried by the format. */
function formatWhen(ts, now) {
  if (!ts) return "Never";
  const d = Math.abs(ts - now);
  const date = new Date(ts);
  if (d < 24 * 3600e3) return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  if (d < 7 * 24 * 3600e3) return date.toLocaleDateString("en-GB", { weekday: "short" }) + " " + date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
  return date.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
}
const formatAbsolute = (ts) => (ts ? new Date(ts).toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "");

/* One spelling for every countdown in the product. Direction is in the words. */
function formatCountdown(ts, now) {
  if (!ts) return "No deadline";
  const diff = ts - now;
  const abs = Math.abs(diff);
  const unit = abs >= 36 * 3600e3 ? Math.round(abs / 864e5) : Math.round(abs / 3600e3);
  const noun = abs >= 36 * 3600e3 ? (unit === 1 ? "day" : "days") : (unit === 1 ? "hour" : "hours");
  return unit + " " + noun + (diff >= 0 ? " left" : " over");
}

function Icon({ name, size = 16 }) {
  const host = React.useRef(null);
  React.useEffect(() => {
    if (!host.current || !window.lucide) return;
    host.current.innerHTML = "";
    const el = document.createElement("i");
    el.setAttribute("data-lucide", name);
    host.current.appendChild(el);
    window.lucide.createIcons({ attrs: { width: size, height: size } });
  }, [name, size]);
  return <span className="fd-icon" ref={host} aria-hidden="true" style={{ width: size, height: size }} />;
}

function useFloating(open, anchorRef, panelRef, align, side) {
  const [pos, setPos] = React.useState({ top: -9999, left: -9999 });
  const place = React.useCallback(() => {
    const host = anchorRef.current, panel = panelRef.current;
    if (!host || !panel) return;
    /* Measure the TRIGGER, not the wrapper. The wrapper is inline-flex and static,
       so an absolutely-positioned trigger inside it — a chart bar, a pinned badge —
       does not sit where the wrapper does, and measuring the wrapper puts every
       overlay in the same wrong place. */
    const anchor = host.firstElementChild || host;
    const a = anchor.getBoundingClientRect(), p = panel.getBoundingClientRect();
    const m = 8;
    let top, left;
    if (side === "right") {
      left = a.right + 6;
      top = a.top + (a.height - p.height) / 2;
    } else {
      top = a.bottom + 4;
      if (top + p.height > window.innerHeight - m) {
        const above = a.top - 4 - p.height;
        top = above > m ? above : Math.max(m, window.innerHeight - m - p.height);
      }
      left = align === "end" ? a.right - p.width : align === "center" ? a.left + (a.width - p.width) / 2 : a.left;
    }
    left = Math.min(Math.max(m, left), Math.max(m, window.innerWidth - m - p.width));
    top = Math.min(Math.max(m, top), Math.max(m, window.innerHeight - m - p.height));
    setPos({ top: top, left: left });
  }, [align, side, anchorRef, panelRef]);
  React.useLayoutEffect(() => { if (open) place(); }, [open, place]);
  React.useEffect(() => {
    if (!open) return undefined;
    const onMove = () => place();
    window.addEventListener("scroll", onMove, true);
    window.addEventListener("resize", onMove);
    return () => { window.removeEventListener("scroll", onMove, true); window.removeEventListener("resize", onMove); };
  }, [open, place]);
  return pos;
}

function Floating({ trigger, children, align = "end", width, role = "menu", panelClass = "o-menu", label }) {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null), panelRef = React.useRef(null);
  const pos = useFloating(open, anchorRef, panelRef, align, "bottom");
  React.useEffect(() => {
    if (!open) return undefined;
    const away = (e) => {
      if (anchorRef.current && anchorRef.current.contains(e.target)) return;
      if (panelRef.current && panelRef.current.contains(e.target)) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", away);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", onKey); };
  }, [open]);
  const close = () => setOpen(false);
  return (
    <span ref={anchorRef} className="fd-floating-anchor">
      {React.cloneElement(trigger, { onClick: () => setOpen(!open), "aria-expanded": open, "aria-haspopup": role === "menu" ? "menu" : "dialog" })}
      {open ? ReactDOM.createPortal(
        <div ref={panelRef} role={role} aria-label={label} className={"o-popup " + panelClass} style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 60, minWidth: width || "12rem" }}>
          {typeof children === "function" ? children(close) : children}
        </div>, document.body) : null}
    </span>
  );
}

function FloatingTip({ label, side = "right", children }) {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null), panelRef = React.useRef(null);
  const pos = useFloating(open, anchorRef, panelRef, "center", side);
  return (
    <span ref={anchorRef} className="fd-floating-anchor" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)} onFocus={() => setOpen(true)} onBlur={() => setOpen(false)}>
      {children}
      {open ? ReactDOM.createPortal(
        <span ref={panelRef} role="tooltip" className="o-tooltip" style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 70, whiteSpace: "nowrap" }}>{label}</span>,
        document.body) : null}
    </span>
  );
}

Object.assign(window, { StatusBadge, TONE_VARIANT, formatMoney, formatPercent, formatCount, formatWhen, formatAbsolute, formatCountdown, Icon, Floating, FloatingTip });

</script>
<script type="text/plain" data-rd-file="lib/command-palette.js">
/* App-scope search — copied per product, domain-free. This file owns the
   shortcut, the query, the empty state and the route check; the shell supplies
   the record groups. The field in the toolbar is read-only and opens this same
   overlay, so search is a palette, not a filter.

   Optional `footer`: one persistent row under the list, given the palette's own
   close function. It is not filtered by the query, so it is for a destination
   rather than a record — "see every result", "open the full list". Products
   without one pass nothing and get no row. */

const { Command } = window.OriginDesignSystem_7c998b;

function useCommandPalette() {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((v) => !v); }
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []);
  return { open: open, setOpen: setOpen, bound: true };
}

function CommandPalette({ open, onClose, routes, recordGroups = [], onNavigate, placeholder, label, emptyMessage, footer }) {
  /* Route check: an item that leads nowhere is a bug in the caller, not a
     silent dead end. */
  React.useEffect(() => {
    if (!open) return;
    recordGroups.forEach((g) => g.items.forEach((i) => {
      if (i.route && !matchRoute(i.route, routes)) console.warn("[command-palette] item points at an unregistered route:", i.route);
    }));
  }, [open, recordGroups, routes]);

  if (!open) return null;

  const destinations = Object.values(routes)
    .filter((r) => !r.offSidebar)
    .map((r) => ({ value: r.path, label: r.label, route: r.path, shortcut: r.path === "/" ? null : null }));
  const groups = [{ label: "Go to", items: destinations }].concat(recordGroups);

  return (
    <>
      <div className="o-backdrop" onClick={onClose} />
      <div className="o-viewport">
        <div className="o-dialog fd-command" role="dialog" aria-modal="true" aria-label={label || "Search"}>
          <Command
            groups={groups}
            placeholder={placeholder || "Search…"}
            emptyMessage={emptyMessage || "Nothing matches that. Try a record or a destination."}
            onSelect={(item) => { if (item.route) onNavigate(item.route); onClose(); }}
          />
          {footer ? <div className="fd-command__foot">{typeof footer === "function" ? footer(onClose) : footer}</div> : null}
        </div>
      </div>
    </>
  );
}

Object.assign(window, { useCommandPalette, CommandPalette });

</script>
<script type="text/plain" data-rd-file="lib/template-menu.js">
/* Preview page menu — copied per product, domain-free, byte-identical across
   products. This is the template's own harness chrome, not part of any product
   shell: it exists so a reader who opens a template can reach every screen the
   route table registers, including the ones a running product would only reach
   from a real session — a record, an account screen, an expired reset link.

   Two things it deliberately owns, and why:
   · The page list is a projection of the product's route table (nav.js passes
     it through templatePages), so a route added anywhere shows up here without
     being retyped, and a record route with no fixture example is not offered.
   · The theme control. Auth screens render no shell (AGENTS §7.1), so on those
     screens the product has no theme control at all — and §7.3 wants dark mode
     verified on them too. Outside auth this sits beside the shell's own theme
     button on purpose: one belongs to the product, one to the harness, and the
     harness panel says "Template" at the top so the two do not read as a
     duplicated duty.

   It injects its own stylesheet once rather than adding rules to each product's
   kit.css, which is what keeps the eight copies identical. The one rule it
   reaches outside itself lifts the toast region so toasts stack above the
   button instead of burying it. */

const { Input } = window.OriginDesignSystem_7c998b;

const TEMPLATE_MENU_CSS = `
.tm-root{position:fixed;inset-block-end:var(--space-5);inset-inline-end:var(--space-5);z-index:80;display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-2);pointer-events:none}
.tm-root>*{pointer-events:auto}
.tm-fab{display:inline-flex;width:44px;height:44px;flex:none;align-items:center;justify-content:center;border:0;border-radius:var(--radius-full);background:var(--foreground);color:var(--background);box-shadow:var(--shadow-lg);cursor:pointer;opacity:0;transition:transform var(--duration-fast) ease,opacity var(--duration-normal) ease}
/* The fab is preview chrome, not product UI: it stays out of the way until the
   pointer is in the app, and is always present for the keyboard and while open. */
html:hover .tm-fab,.tm-root:focus-within .tm-fab,.tm-fab[aria-expanded="true"]{opacity:1}
.tm-fab:hover{transform:translateY(-1px)}
.tm-fab:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
.tm-panel{display:flex;width:21rem;max-width:calc(100vw - var(--space-6));max-height:min(70vh,32rem);flex-direction:column;overflow:hidden;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--popover);color:var(--popover-foreground);box-shadow:var(--shadow-lg)}
.tm-head{display:flex;align-items:center;gap:var(--space-2);border-bottom:1px solid var(--border);padding:var(--space-3)}
.tm-chip{border:1px solid var(--border);border-radius:var(--radius-full);padding:1px var(--space-1-5);font-family:var(--font-mono);font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-foreground)}
.tm-head__title{font-family:var(--font-heading);font-size:var(--text-sm);font-weight:var(--font-weight-semibold)}
.tm-head__count{margin-inline-start:auto;font-size:var(--text-xs);color:var(--muted-foreground)}
.tm-filter{padding:var(--space-2) var(--space-3)}
.tm-body{flex:1;min-height:0;overflow-y:auto;padding:0 var(--space-2) var(--space-2)}
.tm-group+.tm-group{margin-block-start:var(--space-2)}
.tm-group__label{padding:var(--space-1-5) var(--space-2);font-size:var(--text-xs);font-weight:var(--font-weight-medium);color:var(--muted-foreground)}
.tm-item{display:flex;width:100%;align-items:baseline;gap:var(--space-2);border:0;border-radius:var(--radius-lg);background:transparent;padding:var(--space-1-5) var(--space-2);text-align:left;font:inherit;color:inherit;cursor:pointer}
.tm-item:hover{background:var(--accent)}
.tm-item:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}
.tm-item.is-active{background:var(--accent);color:var(--accent-foreground);font-weight:var(--font-weight-medium)}
.tm-item__label{flex:1;min-width:0;font-size:var(--text-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tm-item__path{flex:none;max-width:9rem;font-family:var(--font-mono);font-size:10px;color:var(--muted-foreground);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tm-empty{padding:var(--space-4) var(--space-2);font-size:var(--text-sm);color:var(--muted-foreground)}
.tm-foot{display:flex;align-items:center;gap:var(--space-2);border-top:1px solid var(--border);padding:var(--space-2-5) var(--space-3)}
.tm-foot__label{font-size:var(--text-xs);color:var(--muted-foreground)}
.tm-seg{display:flex;margin-inline-start:auto;gap:2px;border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}
.tm-seg__btn{border:0;border-radius:calc(var(--radius-md) - 3px);background:transparent;padding:2px var(--space-2);font:inherit;font-size:var(--text-xs);color:var(--muted-foreground);cursor:pointer}
.tm-seg__btn.is-on{background:var(--accent);color:var(--accent-foreground)}
.o-toast-region{inset-block-end:calc(var(--space-5) + 44px + var(--space-2))}
@media (max-width:600px){.tm-root{inset-inline:var(--space-3);inset-block-end:var(--space-3)}.tm-panel{width:100%;max-width:none;max-height:64vh}.o-toast-region{inset-block-end:calc(var(--space-3) + 44px + var(--space-2))}}
`;

/* The route table read whole and grouped. `examples` maps a parameterised
   pattern to one concrete path from the fixture; a pattern with no example is
   left out rather than offered as a link that resolves nowhere. */
function templatePages(nav, authPaths, examples) {
  const destinations = [], records = [], settings = [], auth = [];
  /* An off-nav route may name its own group ("Create", "Reports"); a route in
     the nav may not, because there `group` is the sidebar's own sectioning. */
  const named = {}, order = [];
  const put = (label, item) => { if (!named[label]) { named[label] = []; order.push(label); } named[label].push(item); };
  nav.forEach((route) => {
    const parameterised = route.path.indexOf(":") > -1;
    const to = parameterised ? (examples || {})[route.path] : route.path;
    if (!to) return;
    const item = { to: to, label: route.label };
    if (authPaths.indexOf(route.path) > -1) auth.push(item);
    else if (!route.offSidebar) destinations.push(item);
    else if (route.group) put(route.group, item);
    else if (parameterised) records.push(item);
    else settings.push(item);
  });
  return [{ label: "Destinations", items: destinations }]
    .concat(order.map((label) => ({ label: label, items: named[label] })))
    .concat([
      { label: "Records", items: records },
      { label: "Account and settings", items: settings },
      { label: "Auth", items: auth },
    ])
    .filter((group) => group.items.length > 0);
}

function TemplateMenu({ product, path, onNavigate, groups, dark, onToggleTheme }) {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const rootRef = React.useRef(null);

  React.useEffect(() => {
    if (document.getElementById("tm-styles")) return;
    const style = document.createElement("style");
    style.id = "tm-styles";
    style.textContent = TEMPLATE_MENU_CSS;
    document.head.appendChild(style);
  }, []);

  React.useEffect(() => {
    if (!open) return undefined;
    const away = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", away);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("keydown", onKey); };
  }, [open]);

  const total = groups.reduce((n, group) => n + group.items.length, 0);
  const q = query.trim().toLowerCase();
  const shown = groups
    .map((group) => ({ label: group.label, items: group.items.filter((item) => !q || (item.label + " " + item.to).toLowerCase().indexOf(q) > -1) }))
    .filter((group) => group.items.length > 0);
  const go = (to) => { onNavigate(to); setOpen(false); setQuery(""); };

  return (
    <div className="tm-root" ref={rootRef}>
      {open ? (
        <div className="tm-panel" role="dialog" aria-label={product + " — template pages"}>
          <div className="tm-head">
            <span className="tm-chip">Template</span>
            <span className="tm-head__title">{product}</span>
            <span className="tm-head__count">{total} pages</span>
          </div>
          <div className="tm-filter">
            <Input size="sm" value={query} onChange={(e) => setQuery(e.target.value)}
              aria-label="Filter pages" placeholder="Filter pages…" startAddon={<Icon name="search" size={14} />} />
          </div>
          <div className="tm-body">
            {shown.length ? shown.map((group) => (
              <div className="tm-group" key={group.label}>
                <div className="tm-group__label">{group.label}</div>
                {group.items.map((item) => {
                  const active = item.to === path;
                  return (
                    <button type="button" key={item.to} className={"tm-item" + (active ? " is-active" : "")}
                      aria-current={active ? "page" : undefined} onClick={() => go(item.to)}>
                      <span className="tm-item__label">{item.label}</span>
                      <span className="tm-item__path">{item.to}</span>
                    </button>
                  );
                })}
              </div>
            )) : <p className="tm-empty">No page matches “{query.trim()}”.</p>}
          </div>
          {onToggleTheme ? (
            <div className="tm-foot">
              <span className="tm-foot__label">Theme</span>
              <div className="tm-seg" role="group" aria-label="Theme">
                <button type="button" className={"tm-seg__btn" + (dark ? "" : " is-on")} aria-pressed={!dark} onClick={() => { if (dark) onToggleTheme(); }}>Light</button>
                <button type="button" className={"tm-seg__btn" + (dark ? " is-on" : "")} aria-pressed={!!dark} onClick={() => { if (!dark) onToggleTheme(); }}>Dark</button>
              </div>
            </div>
          ) : null}
        </div>
      ) : null}
      <button type="button" className="tm-fab" aria-expanded={open}
        aria-label={open ? "Close the template page menu" : "Template pages — " + total + " screens in " + product}
        onClick={() => setOpen(!open)}>
        <Icon name={open ? "x" : "layout-grid"} size={18} />
      </button>
    </div>
  );
}

Object.assign(window, { TemplateMenu, templatePages });

</script>
<script type="text/plain" data-rd-file="RdParts.jsx">
/* Recruitment Desk — the product's own parts. Nothing here holds data or
   assigns a status tone: StatusBadge does the tone, metrics.js does the
   arithmetic, enums.js names the transitions. Reported in the README as local
   additions (AGENTS §9): the stepper, the note thread and its reaction bar, the
   follower stack, the scorecard matrix, the stage strip and the board card. */
(() => {
  const {
    Button, Input, Textarea, Select, Checkbox, RadioGroup, Switch,
    Field, FieldLabel, FieldDescription, FieldError,
    Card, CardPanel, Badge, Avatar, MenuItem, MenuGroupLabel, Progress, Separator,
    Alert, AlertTitle, AlertDescription,
    Dialog, DialogPopup, DialogHeader, DialogTitle, DialogDescription, DialogPanel, DialogFooter,
    AlertDialog, AlertDialogPopup, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* ── Figures ──────────────────────────────────────────────────────────── */

  /* A figure is checkable three ways (AGENTS §2): the rows sit beside it, it
     drills through to them, or it names a predicate. `onDrill` is the second. */
  function StatTile({ label, value, qualifier, onDrill, drillLabel }) {
    return (
      <Card>
        <div className="rd-tile">
          <span className="rd-tile__label">{label}</span>
          {onDrill
            ? <button type="button" className="rd-tile__value rd-link" onClick={onDrill} aria-label={drillLabel || ("Show the " + label.toLowerCase())}>{value}</button>
            : <span className="rd-tile__value">{value}</span>}
          {qualifier ? <span className="rd-tile__qual">{qualifier}</span> : null}
        </div>
      </Card>
    );
  }

  function Bar({ ratio, tone }) {
    return (
      <span className={"rd-bar" + (tone ? " rd-bar--" + tone : "")}>
        <span className="rd-bar__fill" style={{ width: Math.max(0, Math.min(1, ratio)) * 100 + "%" }} />
      </span>
    );
  }

  /* A clock cell. Whether "past" is a problem is a property of the column,
     declared by the caller once — never guessed per row. */
  function Clock({ at, now, problem, absolute }) {
    if (!at) return <span className="rd-muted">Never</span>;
    const late = problem && at < now;
    const soon = problem && !late && at - now < 3 * DAY;
    return (
      <span className={"rd-clock" + (late ? " rd-clock--danger" : soon ? " rd-clock--warning" : "")} title={formatAbsolute(at)}>
        {absolute ? formatDay(at) : formatWhen(at, now)}
      </span>
    );
  }

  function KeyValues({ items }) {
    return (
      <div className="rd-kv">
        {items.map((item) => (
          <span className="rd-part" key={item.label}>
            <span className="rd-kv__label">{item.label}</span>
            <span className="rd-kv__value">{item.value}</span>
            {item.note ? <span className="rd-foot">{item.note}</span> : null}
          </span>
        ))}
      </div>
    );
  }

  /* ── People ───────────────────────────────────────────────────────────── */

  function StaffCell({ person, meta, size = 26 }) {
    if (!person) return <span className="rd-muted">Unassigned</span>;
    return (
      <span className="rd-person">
        <Avatar size={size} src={person.photo} alt="" fallback={person.initials} />
        <span className="rd-record">
          <span className="rd-record__name">{person.name}</span>
          <span className="rd-record__meta">{meta || person.title}</span>
        </span>
      </span>
    );
  }

  /* The candidate, with their face, at every scale. The name is the link to the
     one application route; nothing else in the cell navigates. */
  function CandidateCell({ application, meta, onNavigate, size = 32 }) {
    const c = application.candidate;
    return (
      <span className="rd-person">
        <Avatar size={size} src={c.photo} alt="" fallback={c.initials} />
        <span className="rd-record">
          {onNavigate
            ? <button type="button" className="rd-link rd-record__name" onClick={() => onNavigate("/applications/" + application.id)}>{c.name}</button>
            : <span className="rd-record__name">{c.name}</span>}
          <span className="rd-record__meta">{meta === undefined ? c.headline : meta}</span>
        </span>
      </span>
    );
  }

  /* Everybody following this application — the owner, the watchers and anybody
     who has written on it. One list, so the faces and the count agree. */
  function Followers({ people, size = 24, label = "Following this application" }) {
    if (!people.length) return null;
    return (
      <span className="rd-watch">
        <span className="rd-faces" aria-hidden="true">
          {people.slice(0, 5).map((person) => <Avatar key={person.id} size={size} src={person.photo} alt="" fallback={person.initials} />)}
        </span>
        <span className="rd-foot">
          {label}: {people.slice(0, 3).map(firstNameOf).join(", ")}
          {people.length > 3 ? " and " + plural(people.length - 3, "other", "others") : ""}
        </span>
      </span>
    );
  }

  /* ── The stepper: this product's signature ────────────────────────────────
     The same seven steps, in APPLICATION_PATH order, wherever an application
     appears. A step's state comes from the record (stepState) and a completed
     step names the sub-record that completed it (stepEvidence) — never a claim
     the fixture cannot support. */
  function Stepper({ application, data, now, mini }) {
    const currentLabel = statusValue(APPLICATION_STAGE, application.stage).label;
    const stopped = STAGE_SETTLED.indexOf(application.stage) > -1 && application.stage !== "hired";
    return (
      <div className={"rd-stepper" + (mini ? " rd-stepper--mini" : "")} role="group"
        aria-label={stopped ? currentLabel + " at " + statusValue(APPLICATION_STAGE, application.lastLiveStage || "applied").label.toLowerCase() : "Stage: " + currentLabel}>
        {APPLICATION_PATH.map((stage, i) => {
          const state = stepState(application, stage);
          const value = statusValue(APPLICATION_STAGE, stage);
          const over = state === "current" ? stageOverBy(application, now) : 0;
          const evidence = stepEvidence(application, stage, data, now);
          const meta = state === "current" && !stopped
            ? plural(daysInStage(application, now), "day", "days") + " in this step"
              + (stageAllowance(application) ? " of " + stageAllowance(application) : "")
              + (over > 0 ? " — " + plural(over, "day", "days") + " over" : "")
            : state === "done" ? evidence
              : state === "stopped" ? (application.stage === "declined" ? "Declined here " + formatDayShort(application.decidedAt) : "Withdrew here " + formatDayShort(application.decidedAt))
                : STAGE_MEANS[stage];
          const pip = (
            <span className="rd-step__pip">{state === "done" ? <Icon name="check" size={12} /> : state === "stopped" ? <Icon name="x" size={12} /> : i + 1}</span>
          );
          return (
            <div className="rd-step" data-state={state} key={stage}>
              <span className="rd-step__track">
                <span className="rd-step__line" data-done={state === "done"} />
                {mini ? <FloatingTip side="bottom" label={value.label + (meta ? " — " + meta : "")}>{pip}</FloatingTip> : pip}
              </span>
              {mini ? null : (
                <span className="rd-step__text">
                  <span className="rd-step__label">{value.label}</span>
                  {meta ? <span className="rd-step__meta">{meta}</span> : null}
                </span>
              )}
            </div>
          );
        })}
      </div>
    );
  }

  /* One line under the stepper saying what the step is waiting on and who owes
     it — recession is named, never implied (AGENTS §5.4). */
  function StageNote({ application, data, now }) {
    const stage = application.stage;
    const owner = staffById(data.staff, application.ownerId);
    if (stage === "declined" || stage === "withdrawn") {
      return (
        <p className="rd-stage-note">
          <Icon name="circle-slash" size={14} />
          {stage === "declined"
            ? labelOf(DECLINE_REASONS, application.declineReason) + " · decided " + formatDay(application.decidedAt) + " by " + firstNameOf(staffById(data.staff, application.decidedById))
            : labelOf(WITHDRAW_REASONS, application.withdrawReason) + " · " + formatDay(application.decidedAt)}
          {application.wouldReconsider ? " · in the talent pool" : ""}
        </p>
      );
    }
    if (stage === "hired") {
      return <p className="rd-stage-note"><Icon name="circle-check" size={14} />Accepted {formatDay(application.offer.acceptedAt)} · starting {formatDay(application.offer.startAt)} · handed to onboarding</p>;
    }
    const waiting = APPLICATION_FLAGS.awaitsUs(application)
      ? "Waiting on the desk" + (owner ? " — " + owner.name : "")
      : stage === "offer" ? "Waiting on " + application.candidate.name.split(" ")[0] + " until " + formatDay(application.offer.expiresAt)
        : "Waiting on referees and the DBS service";
    return (
      <p className="rd-stage-note">
        <Icon name={APPLICATION_FLAGS.awaitsUs(application) ? "hand" : "clock" } size={14} />
        {waiting} · {STAGE_MEANS[stage]}
      </p>
    );
  }

  /* ── Scorecards: two cards side by side on one grid, header and rows on the
     same tracks, so a wrapped area label cannot shift a column. ──────────── */
  function Pips({ value, max = 5 }) {
    return (
      <span className="rd-pips" aria-hidden="true">
        {Array.from({ length: max }).map((_, i) => <span className="rd-pip" key={i} data-on={i < value} />)}
      </span>
    );
  }

  function ScoreMatrix({ application, data }) {
    const score = scoresOf(application);
    if (!score.count) return null;
    const columns = "minmax(0,1.2fr) " + score.cards.map(() => "minmax(0,1fr)").join(" ");
    return (
      <div className="rd-matrix" style={{ gridTemplateColumns: columns }}>
        <span className="rd-matrix__head">Area</span>
        {score.cards.map((card) => (
          <span className="rd-matrix__head" key={card.byId}>{firstNameOf(staffById(data.staff, card.byId))} · {formatDayShort(card.at)}</span>
        ))}
        {SCORE_AREAS.map((area) => (
          <React.Fragment key={area.value}>
            <span>{area.label}</span>
            {score.cards.map((card) => (
              <span className="rd-matrix__cell" key={card.byId + area.value}><Pips value={card.scores[area.value]} />{card.scores[area.value]}</span>
            ))}
          </React.Fragment>
        ))}
        <span className="rd-matrix__head">Average</span>
        {score.cards.map((card) => <span className="rd-matrix__cell" key={"avg" + card.byId}>{score.cardAverage(card).toFixed(2)}</span>)}
      </div>
    );
  }

  /* ── The thread: how the desk actually talks ──────────────────────────────
     A mention pulls somebody in, a reaction is how the rest of the panel agrees
     without writing "agreed", and the seen line is who has read it. */
  function NoteBody({ body, staff }) {
    const parts = body.split(/(@p-[a-z]+)/g);
    return (
      <p className="rd-note__text">
        {parts.map((part, i) => {
          if (part.indexOf("@p-") !== 0) return <React.Fragment key={i}>{part}</React.Fragment>;
          const person = staffById(staff, part.slice(1));
          return <span className="rd-mention" key={i}>@{person ? person.name : part.slice(1)}</span>;
        })}
      </p>
    );
  }

  function ReactionBar({ note, me, onReact }) {
    const rows = reactionsOf(note);
    return (
      <span className="rd-react">
        {rows.map(({ reaction, who }) => {
          const mine = who.indexOf(me.id) > -1;
          const names = who.map((id) => id.replace("p-", "")).length;
          return (
            <FloatingTip key={reaction.value} side="bottom" label={reaction.label + " · " + plural(names, "person", "people")}>
              <button type="button" className="rd-react__btn" data-mine={mine}
                aria-label={(mine ? "Remove your " : "Add ") + reaction.label.toLowerCase() + " — " + who.length + " so far"}
                onClick={() => onReact(note, reaction.value)}>
                <Icon name={reaction.icon} size={12} />{who.length}
              </button>
            </FloatingTip>
          );
        })}
        <Floating align="start" width="12rem" label="React"
          trigger={<button type="button" className="rd-react__btn" aria-label="React to this note"><Icon name="smile-plus" size={12} /></button>}>
          {(close) => REACTIONS.map((reaction) => (
            <MenuItem key={reaction.value} onClick={() => { close(); onReact(note, reaction.value); }}>{reaction.label}</MenuItem>
          ))}
        </Floating>
      </span>
    );
  }

  function Note({ note, staff, me, now, reply, onReact, onReply }) {
    const author = staffById(staff, note.authorId);
    const seen = (note.seenBy || []).length;
    return (
      <div className={"rd-note" + (reply ? " rd-note--reply" : "")}>
        <Avatar size={reply ? 26 : 32} src={author.photo} alt="" fallback={author.initials} />
        <div className="rd-note__body">
          <div className="rd-note__head">
            <span className="rd-note__who">{author.name}</span>
            <span className="rd-note__role">{labelOf(DESK_ROLES, author.role)}</span>
            {note.kind !== "note" ? <Badge size="sm" variant="secondary">{labelOf(NOTE_KINDS, note.kind)}</Badge> : null}
            <span className="rd-note__at" title={formatAbsolute(note.at)}>{formatWhen(note.at, now)}</span>
          </div>
          <NoteBody body={note.body} staff={staff} />
          <div className="rd-note__foot">
            <ReactionBar note={note} me={me} onReact={onReact} />
            {onReply ? <Button variant="ghost" size="xs" onClick={() => onReply(note)}>Reply</Button> : null}
            {seen ? <span className="rd-note__seen"><Icon name="check-check" size={12} />Read by {seen}</span> : null}
          </div>
        </div>
      </div>
    );
  }

  /* Instant model, and consequential: a note posts on send and raises an undo
     toast. Its own control is not a form — there is no Save on this screen. */
  function Composer({ me, onPost, replyingTo, staff, onCancelReply }) {
    const [body, setBody] = React.useState("");
    const [kind, setKind] = React.useState("note");
    React.useEffect(() => { if (replyingTo) setBody("@" + replyingTo.authorId + " "); }, [replyingTo]);
    const send = () => { if (!body.trim()) return; onPost(body.trim(), kind, replyingTo); setBody(""); setKind("note"); };
    return (
      <div className="rd-composer">
        <Avatar size={32} src={me.photo} alt="" fallback={me.initials} />
        <div className="rd-composer__body">
          <Textarea rows={3} value={body} onChange={(e) => setBody(e.target.value)}
            aria-label={replyingTo ? "Reply to " + staffById(staff, replyingTo.authorId).name : "Write a note on this application"}
            placeholder={replyingTo ? "Reply to " + firstNameOf(staffById(staff, replyingTo.authorId)) + "…" : "What did you find? Type @ and a colleague's id to pull them in."} />
          <div className="rd-composer__foot">
            <span className="rd-composer__hint">
              {replyingTo ? "Replying to " + staffById(staff, replyingTo.authorId).name + "'s note." : "Notes are visible to everybody on the desk and to the hiring manager. The candidate never sees them."}
            </span>
            <span className="rd-filters__select">
              <label className="rd-sr" htmlFor="rd-note-kind">Kind of note</label>
              <Select id="rd-note-kind" size="sm" items={NOTE_KINDS.map((k) => ({ value: k.value, label: k.label }))} value={kind} onValueChange={setKind} />
            </span>
            {replyingTo ? <Button variant="ghost" size="sm" onClick={onCancelReply}>Cancel</Button> : null}
            <Button size="sm" disabled={!body.trim()} onClick={send}>Post the note</Button>
          </div>
        </div>
      </div>
    );
  }

  /* ── The stage strip and the board card ───────────────────────────────── */
  function StageStrip({ applications, requisitionId }) {
    const strip = stageStrip(applications, requisitionId);
    if (!strip.total) return <p className="rd-foot">No live applications on this requisition.</p>;
    return (
      <span className="rd-part">
        <span className="rd-strip" role="img" aria-label={strip.segments.filter((s) => s.count).map((s) => s.count + " " + s.label.toLowerCase()).join(", ")}>
          {strip.segments.filter((s) => s.count).map((segment) => (
            <span className="rd-strip__seg" key={segment.stage} data-stage={segment.stage} style={{ width: (segment.count / strip.total) * 100 + "%" }} />
          ))}
        </span>
        <span className="rd-striplegend">
          {strip.segments.filter((s) => s.count).map((segment) => <span key={segment.stage}>{segment.label} {segment.count}</span>)}
        </span>
      </span>
    );
  }

  function AppCard({ application, data, now, onNavigate }) {
    const stalled = APPLICATION_FLAGS.isStalled(application, now);
    return (
      <button type="button" className="rd-appcard" onClick={() => onNavigate("/applications/" + application.id)}>
        <span className="rd-appcard__head">
          <Avatar size={32} src={application.candidate.photo} alt="" fallback={application.candidate.initials} />
          <span className="rd-record">
            <span className="rd-record__name">{application.candidate.name}</span>
            <span className="rd-record__meta">{application.candidate.headline}</span>
          </span>
        </span>
        <Stepper application={application} data={data} now={now} mini />
        <span className="rd-appcard__meta">
          <span className={stalled ? "rd-clock rd-clock--danger" : "rd-clock"}>{plural(daysInStage(application, now), "day", "days")} in step</span>
          {application.watcherIds.length ? <span>· {plural(application.watcherIds.length, "watcher", "watchers")}</span> : null}
          {stalled ? <Badge size="sm" variant="warning">Over its {stageAllowance(application)} days</Badge> : null}
        </span>
      </button>
    );
  }

  /* ── Actions ──────────────────────────────────────────────────────────── */

  function Transitions({ transitions, onRun, size = "xs" }) {
    if (!transitions.length) return null;
    return (
      <span className="rd-actions__row">
        {transitions.map((t) => {
          const button = (
            <Button key={t.id} size={size} disabled={!!t.disabled}
              variant={t.destructive ? "destructive-outline" : t.primary ? "default" : "outline"}
              onClick={() => onRun(t)}>{t.label}</Button>
          );
          return t.disabled && t.disabledReason
            ? <FloatingTip key={t.id} label={t.disabledReason} side="bottom">{button}</FloatingTip>
            : button;
        })}
      </span>
    );
  }

  function RowActions({ transitions, onRun, label, extra }) {
    const primary = transitions.filter((t) => !t.destructive && t.primary);
    const rest = transitions.filter((t) => !t.primary);
    if (!primary.length && !rest.length && !extra) return <span className="rd-muted">—</span>;
    return (
      <span className="rd-actions__row">
        {primary.length ? <Transitions transitions={primary} onRun={onRun} /> : null}
        {rest.length || extra ? (
          <Floating align="end" width="16rem" label={label}
            trigger={<Button variant="ghost" size="icon-xs" aria-label={label}><Icon name="ellipsis" size={14} /></Button>}>
            {(close) => (
              <>
                {extra ? extra(close) : null}
                {rest.map((t) => (
                  <MenuItem key={t.id} variant={t.destructive ? "destructive" : undefined} disabled={!!t.disabled}
                    onClick={() => { close(); onRun(t); }}>{t.label}</MenuItem>
                ))}
              </>
            )}
          </Floating>
        ) : null}
      </span>
    );
  }

  function ExportMenu({ scope, onExport }) {
    return (
      <Floating align="end" width="11rem" label="Export"
        trigger={<Button variant="outline" size="sm"><Icon name="download" size={14} />Export</Button>}>
        {(close) => (
          <>
            <MenuGroupLabel>{scope}</MenuGroupLabel>
            {["CSV", "XLSX"].map((format) => <MenuItem key={format} onClick={() => { close(); onExport(format); }}>{format}</MenuItem>)}
          </>
        )}
      </Floating>
    );
  }

  /* ── List furniture: same order on every list screen ──────────────────── */

  function ScopeBar({ controls, note, dirty, onClear, switches }) {
    return (
      <div className="rd-scope">
        <span className="rd-scope__label">Scope</span>
        {(controls || []).map((c) => (
          <span className="rd-scope__group" key={c.id}>
            <label className="rd-foot" htmlFor={"rd-scope-" + c.id}>{c.label}</label>
            <span className="rd-scope__select"><Select id={"rd-scope-" + c.id} size="sm" items={c.items} value={c.value} onValueChange={c.onChange} /></span>
          </span>
        ))}
        {(switches || []).map((s) => (
          <span className="rd-scope__group" key={s.id}>
            <Switch checked={s.checked} onCheckedChange={s.onChange} aria-label={s.label} id={"rd-sw-" + s.id} />
            <label className="rd-foot" htmlFor={"rd-sw-" + s.id}>{s.label}</label>
          </span>
        ))}
        {dirty ? <Button variant="ghost" size="sm" onClick={onClear}>Reset scope</Button> : null}
        {note ? <p className="rd-scope__note">{note}</p> : null}
      </div>
    );
  }

  function FilterBar({ query, onQuery, placeholder, selects = [], onClear, dirty, trailing }) {
    return (
      <div className="rd-filters">
        <div className="rd-filters__text">
          <Input size="sm" value={query} placeholder={placeholder} aria-label={placeholder} startAddon={<Icon name="search" size={14} />} onChange={(e) => onQuery(e.target.value)} />
        </div>
        {selects.map((s) => (
          <span className="rd-filters__select" key={s.id}>
            <label className="rd-sr" htmlFor={"rd-flt-" + s.id}>{s.label}</label>
            <Select id={"rd-flt-" + s.id} size="sm" items={s.items} value={s.value} onValueChange={s.onChange} />
          </span>
        ))}
        {dirty ? <Button variant="ghost" size="sm" onClick={onClear}>Clear filters</Button> : null}
        {trailing}
      </div>
    );
  }

  function BulkBar({ count, note, children, onClear }) {
    if (!count) return null;
    return (
      <div className="rd-bulk">
        <span className="rd-bulk__count">{count} selected</span>
        {note ? <span className="rd-foot">{note}</span> : null}
        <span className="rd-bulk__actions">
          {children}
          <Button variant="ghost" size="sm" onClick={onClear}>Clear selection</Button>
        </span>
      </div>
    );
  }

  function NoResults({ title, description, onClear, action }) {
    return (
      <CardPanel>
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
            <EmptyTitle>{title}</EmptyTitle>
            <EmptyDescription>{description}</EmptyDescription>
          </EmptyHeader>
          <EmptyContent>{onClear ? <Button onClick={onClear}>Clear filters</Button> : action}</EmptyContent>
        </Empty>
      </CardPanel>
    );
  }

  /* ── Commit surfaces ──────────────────────────────────────────────────── */

  function ConfirmDialog({ request, onCancel, onConfirm }) {
    if (!request) return null;
    return (
      <AlertDialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>{request.title}</AlertDialogTitle>
            <AlertDialogDescription>{request.description}</AlertDialogDescription>
          </AlertDialogHeader>
          {request.rows ? (
            <div className="rd-confirm__rows">
              {request.rows.map((row) => <div className="rd-confirm__row" key={row.key}><span>{row.label}</span><span className="rd-muted">{row.value}</span></div>)}
            </div>
          ) : null}
          <AlertDialogFooter>
            <Button variant="outline" onClick={onCancel}>Cancel</Button>
            <Button variant={request.destructive ? "destructive" : "default"} onClick={onConfirm}>{request.confirmLabel}</Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    );
  }

  /* A staged dialog — its own commit model, so an instant screen may open one.
     Submit is disabled until dirty; errors render on submit, never on paint. */
  function FormDialog({ request, onCancel, onSubmit }) {
    const [values, setValues] = React.useState({});
    const [touched, setTouched] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    React.useEffect(() => { setValues(request ? request.initial || {} : {}); setTouched(false); setSubmitted(false); }, [request]);
    if (!request) return null;
    const errors = request.validate ? request.validate(values) : {};
    const valid = Object.keys(errors).length === 0;
    const set = (id, value) => { setTouched(true); setValues((v) => Object.assign({}, v, { [id]: value })); };
    const fields = request.fields.filter((f) => !f.when || f.when(values));
    return (
      <Dialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>{request.title}</DialogTitle>
            <DialogDescription>{request.description}</DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <div className="rd-fields">
              {request.preview ? request.preview(values) : null}
              {fields.map((field) => {
                const id = "rd-f-" + field.id;
                const invalid = submitted && !!errors[field.id];
                return (
                  <Field key={field.id}>
                    <FieldLabel htmlFor={id}>{field.label}</FieldLabel>
                    {field.kind === "radio" ? (
                      <RadioGroup name={id} items={field.options} value={values[field.id] || ""} onValueChange={(v) => set(field.id, v)} />
                    ) : field.kind === "select" ? (
                      <Select id={id} items={field.options} value={values[field.id] || ""} placeholder={field.placeholder} invalid={invalid} onValueChange={(v) => set(field.id, v)} />
                    ) : field.kind === "checks" ? (
                      <div className="rd-checks">
                        {field.options.map((option) => {
                          const on = (values[field.id] || []).indexOf(option.value) > -1;
                          return (
                            <label className="rd-check" key={option.value}>
                              <Checkbox checked={on} onCheckedChange={() => set(field.id, on ? (values[field.id] || []).filter((v) => v !== option.value) : (values[field.id] || []).concat([option.value]))} />
                              <span className="rd-check__text"><span>{option.label}</span>{option.note ? <span className="rd-foot">{option.note}</span> : null}</span>
                            </label>
                          );
                        })}
                      </div>
                    ) : field.kind === "toggle" ? (
                      <label className="rd-check"><Switch checked={!!values[field.id]} onCheckedChange={(v) => set(field.id, v)} /><span className="rd-check__text"><span>{field.toggleLabel}</span></span></label>
                    ) : field.kind === "area" ? (
                      <Textarea id={id} rows={3} placeholder={field.placeholder} value={values[field.id] || ""} invalid={invalid} onChange={(e) => set(field.id, e.target.value)} />
                    ) : field.kind === "day" ? (
                      <Input id={id} type="date" value={values[field.id] || ""} invalid={invalid} onChange={(e) => set(field.id, e.target.value)} />
                    ) : field.kind === "time" ? (
                      <Input id={id} type="time" value={values[field.id] || ""} invalid={invalid} onChange={(e) => set(field.id, e.target.value)} />
                    ) : field.kind === "scores" ? (
                      <div className="rd-checks">
                        {SCORE_AREAS.map((area) => (
                          <span className="rd-scope__group" key={area.value}>
                            <span className="rd-filters__select">
                              <label className="rd-sr" htmlFor={id + "-" + area.value}>{area.label}</label>
                              <Select id={id + "-" + area.value} size="sm"
                                items={[1, 2, 3, 4, 5].map((n) => ({ value: String(n), label: n + " of 5" }))}
                                value={String((values[field.id] || {})[area.value] || "")} placeholder="Score"
                                onValueChange={(v) => set(field.id, Object.assign({}, values[field.id] || {}, { [area.value]: Number(v) }))} />
                            </span>
                            <span className="rd-foot">{area.label}</span>
                          </span>
                        ))}
                      </div>
                    ) : (
                      <Input id={id} placeholder={field.placeholder} value={values[field.id] || ""} invalid={invalid} onChange={(e) => set(field.id, e.target.value)} />
                    )}
                    {invalid ? <FieldError>{errors[field.id]}</FieldError> : field.hint ? <FieldDescription>{field.hint}</FieldDescription> : null}
                  </Field>
                );
              })}
            </div>
          </DialogPanel>
          <DialogFooter>
            {touched ? <Button variant="outline" onClick={onCancel}>Cancel</Button> : null}
            <Button disabled={!touched} onClick={() => { setSubmitted(true); if (valid) onSubmit(values); }}>{request.submitLabel}</Button>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    );
  }

  function FormBar({ dirty, note, onCancel, onSave, label }) {
    return (
      <div className="rd-formbar">
        <span className="rd-formbar__note">{note}</span>
        {dirty ? <Button variant="outline" size="sm" onClick={onCancel}>Cancel</Button> : null}
        <Button size="sm" disabled={!dirty} onClick={onSave}>{label}</Button>
      </div>
    );
  }

  function RouteNotBuilt({ path, onNavigate }) {
    return (
      <div className="rd-page">
        <Card>
          <CardPanel>
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="signpost" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing is registered at {path}</EmptyTitle>
                <EmptyDescription>Every screen in the Recruitment Desk has a route in nav.js. This path is not one of them.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent><Button onClick={() => onNavigate("/")}>Back to your desk</Button></EmptyContent>
            </Empty>
          </CardPanel>
        </Card>
      </div>
    );
  }

  Object.assign(window, {
    StatTile, Bar, Clock, KeyValues, StaffCell, CandidateCell, Followers,
    Stepper, StageNote, Pips, ScoreMatrix, NoteBody, ReactionBar, Note, Composer,
    StageStrip, AppCard, Transitions, RowActions, ExportMenu,
    ScopeBar, FilterBar, BulkBar, NoResults, ConfirmDialog, FormDialog, FormBar, RouteNotBuilt,
  });
})();

</script>
<script type="text/plain" data-rd-file="RdShell.jsx">
/* Recruitment Desk shell — SIDEBAR shell, labelled mode, one navigation owner
   (the sidebar owns every destination; the toolbar is utility only).
   Density: roomy desk — 32px nav rows, 32px page gutters, 36px table rows.
   Separation: hairlines on the tinted --sidebar plane; cards are the only
   raised surface, one elevation step, never nested.
   Active state: --sidebar-accent wash + foreground token + medium weight,
   derived from the route.
   App-scope duties: search → the toolbar's palette field · session and account
   → the sidebar footer · utility → the toolbar (notifications, theme) · page
   context → the page header's breadcrumb and title.
   Two nav groups, both labelled: reviewing, and running the hiring.
   The fold: the sidebar collapses to an icon rail on the chevron button in the
   toolbar's leading slot — beside the drawer's menu button, before the search
   field — and unfolds again from the same button, at every width where a rail
   and a labelled sidebar are both possible. It is a utility action on the
   content side rather than a footer control, so it sits where the reader's eye
   already is when they want the width back. The reader's choice wins over the
   viewport's default once made, and holds for the session. Below 900px the whole
   thing becomes a drawer and the button is not offered, because the menu button
   owns the nav there and there is no rail to fold to.
   Primary content per destination:
     Your desk      — the day's work waiting on you, over the same arrays
     Applications   — table
     Interviews     — the day ladder, one row per booked conversation
     Offers         — table, the offers out, soonest to run out first
     Requisitions   — table, each seat with its pipeline as a stage strip
     Compliance     — table, one row per live application, three checks on each
     Talent pool    — a grid of faces, people we said we would talk to again
     Insights       — the funnel, chart-led
     Desk team      — table, who owns what
   Interviews is the destination that would most plausibly have been a table and
   is not: it answers "what is happening today", which is a question about time,
   so the days are the display and the conversations sit in them.
   Auth renders no shell (AGENTS §7.1); /me/profile is registered off the
   sidebar and reached from the account menu. */
(() => {
  const {
    Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel,
    SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuBadge, SidebarInset, SidebarSeparator,
    Button, Input, Kbd, Avatar, MenuItem, MenuGroupLabel, Tabs, TabsList, TabsTab,
    Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator,
    Toast, ToastRegion,
  } = window.OriginDesignSystem_7c998b;

  const BRAND_MARK = (window.__resources && window.__resources.brandMark) || "../../assets/templates/logos/12-logo-demo.svg";

  /* Declared once for the product: 1280 expanded · 1024 icon rail with the
     labels as tooltips · below 900 an off-canvas drawer. Nothing is lost on the
     way down; the account menu never collapses into an overflow. This is the
     DEFAULT only — the reader's fold overrides it either way above 900px. */
  function useViewportMode() {
    const read = () => (window.innerWidth >= 1280 ? "expanded" : window.innerWidth >= 900 ? "rail" : "drawer");
    const [mode, setMode] = React.useState(read);
    React.useEffect(() => {
      const onResize = () => setMode(read());
      window.addEventListener("resize", onResize);
      return () => window.removeEventListener("resize", onResize);
    }, []);
    return mode;
  }

  /* PageHead is rendered by every screen and none of them pass the router, so
     the shell registers it once per render and the breadcrumb reads it here. */
  let breadcrumbNavigate = null;

  const isActive = (route, path) =>
    route.path === path || (route.path !== "/" && path.indexOf(route.path + "/") === 0);

  function BrandBlock({ rail }) {
    return (
      <div className="rd-brand">
        <img className="rd-brand__mark" src={BRAND_MARK} alt="" width="28" height="28" />
        {rail ? null : (
          <span className="rd-brand__text">
            <span className="rd-brand__name">{RD_ORG.desk}</span>
            <span className="rd-brand__sub">{RD_ORG.name}</span>
          </span>
        )}
      </div>
    );
  }

  function NavRow({ route, active, rail, badge, onNavigate }) {
    const button = (
      <SidebarMenuButton isActive={active} aria-current={active ? "page" : undefined} aria-label={rail ? route.label : undefined} onClick={() => onNavigate(route.path)}>
        <Icon name={route.icon} size={16} />
        {rail ? null : <span>{route.label}</span>}
      </SidebarMenuButton>
    );
    const tip = badge && badge.count > 0 ? route.label + " — " + badge.count + " " + badge.label : route.label;
    return (
      <SidebarMenuItem>
        {rail ? <FloatingTip label={tip}>{button}</FloatingTip> : button}
        {badge && badge.count > 0 ? (
          rail ? <span className="rd-rail-dot" aria-hidden="true" />
            : (
              <span className="rd-nav__badge">
                <FloatingTip label={badge.count + " " + badge.label} side="bottom">
                  <SidebarMenuBadge aria-label={badge.count + " " + badge.label}>{badge.count}</SidebarMenuBadge>
                </FloatingTip>
              </span>
            )
        ) : null}
      </SidebarMenuItem>
    );
  }

  function AccountMenu({ rail, onNavigate, person }) {
    const trigger = (
      <button type="button" className={"rd-account" + (rail ? " rd-account--rail" : "")} aria-label={"Account — " + person.name + ", " + labelOf(DESK_ROLES, person.role)}>
        <Avatar size={rail ? 24 : 28} src={person.photo} alt="" fallback={person.initials} />
        {rail ? null : (
          <span className="rd-account__text">
            <span className="rd-account__name">{person.name}</span>
            <span className="rd-account__role">{labelOf(DESK_ROLES, person.role)} · {labelOf(SITES, person.site)}</span>
          </span>
        )}
        {rail ? null : <Icon name="chevrons-up-down" size={14} />}
      </button>
    );
    return (
      <Floating trigger={trigger} align="start" width="16rem" label="Account">
        {(close) => (
          <>
            <MenuGroupLabel>{person.name} · {labelOf(DESK_ROLES, person.role)}</MenuGroupLabel>
            {accountMenu().map((route) => (
              <MenuItem key={route.path} onClick={() => { onNavigate(route.path); close(); }}>{route.label}</MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  function NotificationsButton({ items, unread, onRead, onOpen, onNavigate, now }) {
    const trigger = (
      <span className="rd-utility">
        <Button variant="ghost" size="icon-sm" aria-label={unread ? unread + " unread notifications" : "Notifications"}><Icon name="bell" size={16} /></Button>
        {unread ? <span className="rd-badge-dot" aria-hidden="true">{unread}</span> : null}
      </span>
    );
    /* The panel shows the newest few; the count and the screen behind it read
       the same array, so “3 unread” and the inbox cannot disagree. */
    const shown = items.slice().sort((a, b) => b.at - a.at).slice(0, 5);
    return (
      <Floating trigger={trigger} align="end" width="24rem" role="dialog" panelClass="o-popover rd-notify" label="Notifications">
        {(close) => (
          <>
            <div className="rd-notify__head">
              <span className="o-popover__title">{unread ? unread + " unread" : "Notifications"}</span>
              <Button variant="ghost" size="xs" disabled={!unread} onClick={onRead}>Mark all read</Button>
            </div>
            <div className="rd-notify__list">
              {shown.map((n) => (
                <button type="button" key={n.id} className="rd-notify__item" data-unread={!n.readAt} onClick={() => { onNavigate(n.route); close(); }}>
                  <span className="rd-notify__title">{n.title}</span>
                  <span className="rd-notify__body">{n.body}</span>
                  <span className="rd-notify__time" title={formatAbsolute(n.at)}>{formatWhen(n.at, now)}</span>
                </button>
              ))}
            </div>
            <div className="rd-notify__foot">
              <span className="rd-foot">Showing {shown.length} of {items.length}.</span>
              <Button variant="outline" size="xs" onClick={() => { onOpen(); close(); }}>See all notifications</Button>
            </div>
          </>
        )}
      </Floating>
    );
  }

  /* Breadcrumb and title. Every segment before the last navigates to its own
     route; the last is never a link. */
  function PageHead({ path, title, note, actions, params, labels, avatar }) {
    const trail = breadcrumb(path, RD_ROUTES);
    return (
      <header className="rd-head">
        <div className="rd-head__id">
          {avatar ? <Avatar size={48} src={avatar.photo} alt="" fallback={avatar.initials} /> : null}
          <div className="rd-head__text">
            <Breadcrumb>
              <BreadcrumbList>
                {trail.map((seg, i) => (
                  <React.Fragment key={seg.path}>
                    {i > 0 ? <BreadcrumbSeparator /> : null}
                    <BreadcrumbItem>
                      {(() => {
                        const label = (labels && labels[seg.path]) || seg.label;
                        const href = resolvePath(seg.path, params || {});
                        if (i === trail.length - 1) return <BreadcrumbPage>{label}</BreadcrumbPage>;
                        if (seg.isNamespace) return <span>{label}</span>;
                        return <BreadcrumbLink href={"#" + href} onSelect={() => breadcrumbNavigate && breadcrumbNavigate(href)}>{label}</BreadcrumbLink>;
                      })()}
                    </BreadcrumbItem>
                  </React.Fragment>
                ))}
              </BreadcrumbList>
            </Breadcrumb>
            <h1 className="rd-title">{title}</h1>
            {note ? <p className="rd-head__note">{note}</p> : null}
          </div>
        </div>
        {actions ? <div className="rd-head__actions">{actions}</div> : null}
      </header>
    );
  }

  /* Views inside a record or a list: tabs narrow what is shown, never a second
     set of top-level destinations. */
  function ViewTabs({ views, value, onSelect }) {
    return (
      <Tabs value={value} onValueChange={onSelect}>
        <TabsList variant="underline">
          {views.map((v) => (
            <TabsTab key={v.value} value={v.value}>
              {v.label}{v.count === undefined ? null : <span className="rd-tab-count">{v.count}</span>}
            </TabsTab>
          ))}
        </TabsList>
      </Tabs>
    );
  }

  function RdShell({ path, onNavigate, dark, onToggleTheme, data, person, notifications, unread, onRead, onOpenNotifications, palette, toasts, onDismissToast, now, fold, onSetFold, children }) {
    const viewport = useViewportMode();
    breadcrumbNavigate = onNavigate;
    const [drawer, setDrawer] = React.useState(false);
    React.useEffect(() => { if (viewport !== "drawer") setDrawer(false); }, [viewport]);
    /* One effective mode: the viewport decides what is POSSIBLE, and where both
       a rail and a labelled sidebar are, the reader's own fold decides — in
       both directions, so nobody is stuck in a rail they cannot open. `fold` is
       null until they touch the button, and then it is the answer. */
    const mode = viewport === "drawer" ? "drawer" : fold === true ? "rail" : fold === false ? "expanded" : viewport;
    const rail = mode === "rail";
    const navOpen = mode !== "drawer" || drawer;
    const canFold = viewport !== "drawer";
    const groups = sidebarGroups();

    return (
      <div className="rd-shell" data-mode={mode}>
        {mode === "drawer" && drawer ? <div className="rd-scrim" onClick={() => setDrawer(false)} /> : null}
        {navOpen ? (
          <Sidebar className="rd-nav" aria-label={RD_ORG.desk}>
            <SidebarHeader>
              <div className="rd-nav__top">
                <BrandBlock rail={rail} />
                {mode === "drawer" ? <Button variant="ghost" size="icon-sm" aria-label="Close navigation" onClick={() => setDrawer(false)}><Icon name="x" size={16} /></Button> : null}
              </div>
            </SidebarHeader>
            <SidebarContent>
              {groups.map((group) => (
                <SidebarGroup key={group.label}>
                  {rail ? null : <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
                  <SidebarMenu>
                    {group.items.map((route) => {
                      const spec = route.badge ? NAV_BADGES[route.badge] : null;
                      return (
                        <NavRow key={route.path} route={route} rail={rail} active={isActive(route, path)}
                          badge={spec ? { count: spec.count(data, now), label: spec.label } : null}
                          onNavigate={(p) => { onNavigate(p); setDrawer(false); }} />
                      );
                    })}
                  </SidebarMenu>
                </SidebarGroup>
              ))}
            </SidebarContent>
            <SidebarFooter>
              <SidebarSeparator />
              <div className={"rd-nav__foot" + (rail ? " rd-nav__foot--rail" : "")}>
                <AccountMenu rail={rail} person={person} onNavigate={(p) => { onNavigate(p); setDrawer(false); }} />
              </div>
            </SidebarFooter>
          </Sidebar>
        ) : null}

        <SidebarInset className="rd-inset">
          <div className="rd-toolbar">
            {mode === "drawer" ? <Button variant="ghost" size="icon-sm" aria-label="Open navigation" onClick={() => setDrawer(true)}><Icon name="menu" size={16} /></Button> : null}
            {canFold ? (
              <FloatingTip side="bottom" label={rail ? "Unfold the sidebar" : "Fold the sidebar to icons"}>
                <Button variant="ghost" size="icon-sm" aria-expanded={!rail} aria-label={rail ? "Unfold the sidebar" : "Fold the sidebar to icons"} onClick={() => onSetFold(!rail)}>
                  <Icon name={rail ? "chevron-right" : "chevron-left"} size={16} />
                </Button>
              </FloatingTip>
            ) : null}
            <div className="rd-search" onClick={() => palette.setOpen(true)}>
              <Input readOnly size="sm" aria-label="Search the Recruitment Desk" placeholder="Search candidates, requisitions, interviews…"
                startAddon={<Icon name="search" size={14} />} endAddon={<Kbd>⌘K</Kbd>} onFocus={() => palette.setOpen(true)} />
            </div>
            <div className="rd-toolbar__utility">
              <NotificationsButton items={notifications} unread={unread} onRead={onRead} onOpen={onOpenNotifications} onNavigate={onNavigate} now={now} />
              <FloatingTip label={dark ? "Switch to light" : "Switch to dark"} side="bottom">
                <Button variant="ghost" size="icon-sm" aria-label={dark ? "Switch to light theme" : "Switch to dark theme"} onClick={onToggleTheme}>
                  <Icon name={dark ? "sun" : "moon"} size={16} />
                </Button>
              </FloatingTip>
            </div>
          </div>
          <div className="rd-content">{children}</div>
        </SidebarInset>

        <ToastRegion>
          {toasts.map((t) => (
            <Toast key={t.id} variant={t.variant} title={t.title} description={t.description} onClose={() => onDismissToast(t.id)}
              action={t.onUndo ? <Button variant="outline" size="xs" onClick={() => { t.onUndo(); onDismissToast(t.id); }}>Undo</Button> : null} />
          ))}
        </ToastRegion>
      </div>
    );
  }

  Object.assign(window, { RdShell, PageHead, ViewTabs });
})();

</script>
<script type="text/plain" data-rd-file="DeskScreen.jsx">
/* Your desk — the day's work, over the same arrays every other screen reads.
   Instant commit model: everything here applies immediately, consequential
   moves raise an undo toast, and the two destructive ones open a dialog.
   Every figure names its array; the four anchor figures drill through to the
   rows they count. */
(() => {
  const { Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter, Button, Badge, Alert, AlertTitle, AlertDescription, Avatar } = window.OriginDesignSystem_7c998b;

  function QueueRow({ application, data, now, onNavigate, onRun }) {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const over = stageOverBy(application, now);
    return (
      <div className="rd-list__row">
        <div className="rd-list__main">
          <CandidateCell application={application} onNavigate={onNavigate} meta={requisition.title + " · " + labelOf(SITES, requisition.site)} />
          <Stepper application={application} data={data} now={now} mini />
        </div>
        <div className="rd-list__side">
          <span className="rd-status">
            <StatusBadge values={APPLICATION_STAGE} value={application.stage} />
            {APPLICATION_FLAGS.isUnread(application) ? <Badge size="sm" variant="secondary">Unread</Badge> : null}
          </span>
          <span className={"rd-clock" + (over > 0 ? " rd-clock--danger" : over === 0 ? " rd-clock--warning" : "")} title={formatAbsolute(application.stageEnteredAt)}>
            {plural(daysInStage(application, now), "day", "days")} in {statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase()}
            {over > 0 ? " · " + plural(over, "day", "days") + " over" : ""}
          </span>
          <RowActions transitions={transitionsFor(application)} onRun={(t) => onRun(application, t)} label={"Actions for " + application.candidate.name} />
        </div>
      </div>
    );
  }

  function DeskScreen({ data, now, session, onNavigate, onApplicationTransition, onInterviewTransition, onReact }) {
    const me = staffById(data.staff, session.personId);
    const live = liveApplications(data.applications);
    const queue = awaitingUs(data.applications);
    const mine = queue.filter((a) => a.ownerId === me.id).sort((a, b) => stageOverBy(b, now) - stageOverBy(a, now));
    const others = queue.filter((a) => a.ownerId !== me.id);
    const unread = unreadApplications(data.applications);
    const stalled = stalledApplications(data.applications, now);
    const outstanding = feedbackOutstanding(data.interviews);
    const today = interviewsToday(data.interviews, now);
    const offers = live.filter((a) => a.stage === "offer");
    const checks = live.filter((a) => a.stage === "checks");
    const mentions = data.notes.filter((n) => mentionIds(n.body).indexOf(me.id) > -1).sort((a, b) => b.at - a.at).slice(0, 3);

    return (
      <div className="rd-page">
        <PageHead path="/" title={"Good morning, " + firstNameOf(me)}
          note={dayLabel(now, now) + ", " + formatDay(now) + " · " + plural(live.length, "live application", "live applications") + " across " + plural(data.requisitions.filter(REQUISITION_FLAGS.isOpen).length, "open requisition", "open requisitions") + "."}
          avatar={me}
          actions={<Button size="sm" onClick={() => onNavigate("/applications/new")}><Icon name="plus" size={14} />New application</Button>} />

        {/* The four figures the desk is run on. The first is the one the day is
            judged on, so it carries the primary plane; the rest are read after
            it. Each drills to the rows it counts or names its predicate. */}
        <div className="rd-metrics">
          {/* Inline, not a class: this is the one tile whose plane inverts, and
              the token pair is the whole rule. */}
          <Card className="rd-tile--primary" style={{ background: "var(--primary)", borderColor: "var(--primary)", color: "var(--primary-foreground)" }}>
            <div className="rd-tile">
              <span className="rd-tile__label" style={{ color: "color-mix(in srgb, var(--primary-foreground) 82%, transparent)" }}>Waiting on the desk</span>
              <button type="button" className="rd-tile__value rd-link" style={{ color: "var(--primary-foreground)", textDecorationColor: "color-mix(in srgb, var(--primary-foreground) 45%, transparent)" }}
                onClick={() => onNavigate("/applications")} aria-label="Show the applications waiting on the desk">{queue.length}</button>
              <span className="rd-tile__qual" style={{ color: "color-mix(in srgb, var(--primary-foreground) 82%, transparent)" }}>{mine.length} yours, {others.length} another recruiter's</span>
            </div>
          </Card>
          <StatTile label="Not read yet" value={unread.length}
            qualifier={unread.length ? "Oldest applied " + formatDayShort(Math.min.apply(null, unread.map((a) => a.appliedAt))) : "Everything has been opened"} />
          <StatTile label="Over their service level" value={stalled.length}
            qualifier="Counted per stage, not one figure for all of them" />
          <StatTile label="Scorecards outstanding" value={outstanding.length}
            qualifier={outstanding.length ? "From " + outstanding.map((iv) => formatDayShort(iv.at)).join(" and ") : "Every panel has reported"}
            onDrill={() => onNavigate("/interviews")} drillLabel="Show the interviews awaiting feedback" />
        </div>

        <div className="rd-columns">
          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>Your queue</CardTitle>
                <CardDescription>The {Math.min(3, mine.length)} most urgent of the {mine.length} you own where the next move is ours, worst over its service level first.</CardDescription>
              </CardHeader>
              <CardPanel>
                {mine.length ? (
                  <div className="rd-list">
                    {mine.slice(0, 3).map((application) => (
                      <QueueRow key={application.id} application={application} data={data} now={now} onNavigate={onNavigate} onRun={onApplicationTransition} />
                    ))}
                  </div>
                ) : <p className="rd-foot">Nothing of yours is waiting on the desk. {others.length ? plural(others.length, "application", "applications") + " sit with another recruiter." : ""}</p>}
              </CardPanel>
              {mine.length > 3 ? (
                <CardFooter className="rd-card-foot">
                  <span className="rd-foot">{plural(mine.length - 3, "other", "others")} of yours are waiting on the desk, less urgent than these.</span>
                  <Button variant="outline" size="sm" onClick={() => onNavigate("/applications")}>Open the full queue</Button>
                </CardFooter>
              ) : null}
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Today</CardTitle>
                <CardDescription>{today.length ? plural(today.length, "conversation", "conversations") + " booked for " + formatDay(now) + "." : "Nothing booked for today."}</CardDescription>
              </CardHeader>
              <CardPanel>
                {today.length ? today.map((interview) => {
                  const application = applicationById(data.applications, interview.applicationId);
                  const panel = panelOf(data.staff, interview);
                  return (
                    <div className="rd-slot" key={interview.id}>
                      <span className="rd-slot__time">{formatTime(interview.at)}</span>
                      <span className="rd-slot__body">
                        <CandidateCell application={application} onNavigate={onNavigate} meta={statusValue(APPLICATION_STAGE, interview.kind === "screening" ? "screening" : interview.kind === "panel" ? "panel" : "assessment").label + " · " + interview.where} />
                        <span className="rd-foot">{interview.minutes} minutes · {panel.map((p) => p.name).join(", ")}</span>
                      </span>
                      <span className="rd-slot__side">
                        <StatusBadge values={INTERVIEW_STATUS} value={interview.status} />
                        <RowActions transitions={interviewTransitions(interview)} onRun={(t) => onInterviewTransition(interview, t)} label={"Actions for the " + interview.kind + " with " + application.candidate.name} />
                      </span>
                    </div>
                  );
                }) : (() => {
                  const next = interviewsAhead(data.interviews, now)[0];
                  return <p className="rd-foot">{next ? "The next one is " + formatWeekday(next.at) + " at " + formatTime(next.at) + "." : "Nothing is booked at all — the queue is where the work is."}</p>;
                })()}
              </CardPanel>
            </Card>
          </div>

          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>Offers with candidates</CardTitle>
                <CardDescription>{plural(offers.length, "offer", "offers")} out. Nothing here is ours to move.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-list">
                  {offers.map((application) => {
                    const left = daysBetween(now, application.offer.expiresAt);
                    return (
                      <div className="rd-list__row" key={application.id}>
                        <div className="rd-list__main">
                          <CandidateCell application={application} onNavigate={onNavigate} meta={money(application.offer.salaryMinor) + " · starts " + formatDayShort(application.offer.startAt)} />
                          {application.offer.acceptedAt
                            ? <span className="rd-foot">Accepted {formatWhen(application.offer.acceptedAt, now)} — record it to move to hired.</span>
                            : <span className="rd-foot">Sent {formatDayShort(application.offer.sentAt)}, valid to {formatDay(application.offer.expiresAt)}.</span>}
                        </div>
                        <div className="rd-list__side">
                          {application.offer.acceptedAt
                            ? <Badge size="sm" variant="success">Accepted</Badge>
                            : <span className={"rd-clock" + (left <= 3 ? " rd-clock--warning" : "")}>{durationWords(left)}</span>}
                          <Transitions transitions={transitionsFor(application).filter((t) => t.primary)} onRun={(t) => onApplicationTransition(application, t)} />
                        </div>
                      </div>
                    );
                  })}
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Checks outstanding</CardTitle>
                <CardDescription>{plural(checks.length, "application", "applications")} in checks. Referees and the DBS service owe these, not us.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-list">
                  {checks.map((application) => {
                    const missing = checksMissing(application);
                    return (
                      <div className={"rd-list__row" + (missing.length ? "" : " rd-row--recessed")} key={application.id}>
                        <div className="rd-list__main">
                          <CandidateCell application={application} onNavigate={onNavigate} meta={requisitionById(data.requisitions, application.requisitionId).title} />
                          <span className="rd-foot">{missing.length ? "Waiting on " + missing.join(" · ") : "Everything is in — the offer is ours to send."}</span>
                        </div>
                        <div className="rd-list__side">
                          <span className="rd-clock">{plural(daysInStage(application, now), "day", "days")} in checks</span>
                          <Transitions transitions={transitionsFor(application).filter((t) => t.primary)} onRun={(t) => onApplicationTransition(application, t)} />
                        </div>
                      </div>
                    );
                  })}
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>You were mentioned</CardTitle>
                <CardDescription>The three most recent notes naming you.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-thread">
                  {mentions.map((note) => {
                    const application = applicationById(data.applications, note.applicationId);
                    return (
                      <div className="rd-part" key={note.id}>
                        <button type="button" className="rd-link rd-foot" onClick={() => onNavigate("/applications/" + application.id)}>{application.candidate.name} · {requisitionById(data.requisitions, application.requisitionId).title}</button>
                        <Note note={note} staff={data.staff} me={me} now={now} onReact={onReact} />
                      </div>
                    );
                  })}
                </div>
              </CardPanel>
            </Card>
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { DeskScreen });
})();

</script>
<script type="text/plain" data-rd-file="ApplicationsScreens.jsx">
/* Applications — the queue, and the form that makes one.
   Instant commit model on the list (each control confirms itself, moves raise
   an undo toast); /applications/new is Staged, as every form in this product is.
   List architecture, the same on every list screen in the desk: filter bar,
   view tabs, bulk bar, table, pagination. The tabs narrow the list only — there
   are no figures above them to move. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Checkbox, Alert, AlertTitle, AlertDescription, Separator,
    Field, FieldLabel, FieldDescription, FieldError, Input, Textarea, Select, RadioGroup,
    Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext,
  } = window.OriginDesignSystem_7c998b;

  const PAGE = 8;
  const ALL = "all";

  /* One tab, one predicate — and the count on the tab is that predicate's own
     length, over the same array the table renders. */
  const VIEWS = [
    { value: "ours", label: "Waiting on us", predicate: (rows) => rows.filter(APPLICATION_FLAGS.awaitsUs) },
    { value: "theirs", label: "Waiting on others", predicate: (rows) => rows.filter(APPLICATION_FLAGS.awaitsThem) },
    { value: "settled", label: "Settled", predicate: (rows) => rows.filter((a) => !APPLICATION_FLAGS.isLive(a)) },
    { value: ALL, label: "Everything", predicate: (rows) => rows },
  ];

  function ApplicationsScreen({ data, now, session, onNavigate, onApplicationTransition, onBulkAdvance, onBulkDecline, pushToast }) {
    const [view, setView] = React.useState("ours");
    const [query, setQuery] = React.useState("");
    const [requisition, setRequisition] = React.useState(ALL);
    const [owner, setOwner] = React.useState(ALL);
    const [page, setPage] = React.useState(1);
    const [selected, setSelected] = React.useState([]);

    const dirty = !!query || requisition !== ALL || owner !== ALL;
    const clear = () => { setQuery(""); setRequisition(ALL); setOwner(ALL); setPage(1); };

    /* One filter chain, in one place: the text filter, the two selects, then the
       tab's predicate. Every count on this screen reads a stage of this chain. */
    const filtered = data.applications.filter((application) => {
      const req = requisitionById(data.requisitions, application.requisitionId);
      if (requisition !== ALL && application.requisitionId !== requisition) return false;
      if (owner !== ALL && application.ownerId !== owner) return false;
      if (!query) return true;
      const hay = [application.candidate.name, application.candidate.headline, application.candidate.location, application.ref, req.title].join(" ").toLowerCase();
      return hay.indexOf(query.trim().toLowerCase()) > -1;
    });
    const views = VIEWS.map((v) => ({ value: v.value, label: v.label, count: v.predicate(filtered).length }));
    const rows = VIEWS.find((v) => v.value === view).predicate(filtered)
      .sort((a, b) => stageOverBy(b, now) - stageOverBy(a, now) || b.appliedAt - a.appliedAt);
    const pages = Math.max(1, Math.ceil(rows.length / PAGE));
    const current = Math.min(page, pages);
    const shown = rows.slice((current - 1) * PAGE, current * PAGE);
    const from = rows.length ? (current - 1) * PAGE + 1 : 0;
    const to = Math.min(current * PAGE, rows.length);

    const chosen = data.applications.filter((a) => selected.indexOf(a.id) > -1);
    const allChosen = shown.length > 0 && shown.every((a) => selected.indexOf(a.id) > -1);
    const toggle = (id) => setSelected((list) => (list.indexOf(id) > -1 ? list.filter((x) => x !== id) : list.concat([id])));
    /* One act-ability predicate, again: a batch may only advance the ones the
       desk owes a move on, and the note says so rather than the button lying. */
    const advanceable = chosen.filter((a) => {
      const move = transitionsFor(a).find((t) => t.id === "advance");
      return move && !move.disabled;
    });

    return (
      <div className="rd-page">
        <PageHead path="/applications" title="Applications"
          note={"Every application on the desk. " + plural(awaitingUs(data.applications).length, "one", "of them") + " waiting on us right now."}
          actions={
            <>
              <ExportMenu scope={plural(rows.length, "row", "rows") + " as filtered"} onExport={(format) => pushToast({ title: format + " export started", description: plural(rows.length, "application", "applications") + " — the file lands in your downloads." })} />
              <Button size="sm" onClick={() => onNavigate("/applications/new")}><Icon name="plus" size={14} />New application</Button>
            </>
          } />

        <FilterBar query={query} onQuery={(v) => { setQuery(v); setPage(1); }} placeholder="Search a name, a headline or a reference…"
          dirty={dirty} onClear={clear}
          selects={[
            { id: "req", label: "Requisition", value: requisition, onChange: (v) => { setRequisition(v); setPage(1); },
              items: [{ value: ALL, label: "Every requisition" }].concat(data.requisitions.map((r) => ({ value: r.id, label: r.ref + " · " + r.title }))) },
            { id: "owner", label: "Owner", value: owner, onChange: (v) => { setOwner(v); setPage(1); },
              items: [{ value: ALL, label: "Any owner" }].concat(data.staff.filter((p) => p.role === "recruiter").map((p) => ({ value: p.id, label: p.name }))) },
          ]} />

        <Card>
          <div className="rd-card-tabs"><ViewTabs views={views} value={view} onSelect={(v) => { setView(v); setPage(1); }} /></div>
          <CardPanel>
            <BulkBar count={chosen.length} onClear={() => setSelected([])}
              note={advanceable.length === chosen.length
                ? "All " + chosen.length + " can move on."
                : advanceable.length + " of " + chosen.length + " can move on — the rest are waiting on somebody outside the desk."}>
              <Button variant="outline" size="sm" disabled={!advanceable.length} onClick={() => onBulkAdvance(advanceable, () => setSelected([]))}>Move on</Button>
              <Button variant="destructive-outline" size="sm" onClick={() => onBulkDecline(chosen.filter(APPLICATION_FLAGS.isLive), () => setSelected([]))}>Decline</Button>
            </BulkBar>
          </CardPanel>
          {rows.length === 0 ? (
            <NoResults title="No application matches that"
              description={dirty ? "The filters are narrower than the data. Clearing them brings back " + plural(data.applications.length, "application", "applications") + "." : "This view is empty — nothing is sitting here at the moment."}
              onClear={dirty ? clear : null}
              action={<Button onClick={() => setView(ALL)}>Show everything</Button>} />
          ) : (
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead style={{ width: "2.5rem" }}>
                      <Checkbox checked={allChosen} aria-label={allChosen ? "Clear this page" : "Select this page"}
                        onCheckedChange={() => setSelected(allChosen ? selected.filter((id) => shown.every((a) => a.id !== id)) : selected.concat(shown.filter((a) => selected.indexOf(a.id) === -1).map((a) => a.id)))} />
                    </TableHead>
                    <TableHead>Candidate</TableHead>
                    <TableHead className="rd-drop-md">Requisition</TableHead>
                    <TableHead>Stage</TableHead>
                    <TableHead className="rd-drop-lg">Progress</TableHead>
                    <TableHead>In stage</TableHead>
                    <TableHead className="rd-drop-lg">Source</TableHead>
                    <TableHead className="rd-actions-head">Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {shown.map((application) => {
                    const req = requisitionById(data.requisitions, application.requisitionId);
                    const over = stageOverBy(application, now);
                    const settled = !APPLICATION_FLAGS.isLive(application);
                    return (
                      <TableRow key={application.id} className={settled ? "rd-row--recessed" : undefined}>
                        <TableCell>
                          <Checkbox checked={selected.indexOf(application.id) > -1} aria-label={"Select " + application.candidate.name} onCheckedChange={() => toggle(application.id)} />
                        </TableCell>
                        <TableCell>
                          <CandidateCell application={application} onNavigate={onNavigate}
                            meta={application.candidate.headline + (APPLICATION_FLAGS.isUnread(application) ? " · not read yet" : "")} />
                        </TableCell>
                        <TableCell className="rd-drop-md">
                          <button type="button" className="rd-link" onClick={() => onNavigate("/requisitions/" + req.id)}>{req.title}</button>
                          <span className="rd-sub">{req.ref} · {labelOf(SITES, req.site)}</span>
                        </TableCell>
                        <TableCell>
                          <span className="rd-status">
                            <StatusBadge values={APPLICATION_STAGE} value={application.stage} />
                            {APPLICATION_FLAGS.isStalled(application, now) ? <Badge size="sm" variant="warning">Over</Badge> : null}
                          </span>
                        </TableCell>
                        <TableCell className="rd-drop-lg" style={{ minWidth: "9rem" }}>
                          <Stepper application={application} data={data} now={now} mini />
                        </TableCell>
                        <TableCell>
                          <span className={"rd-clock" + (over > 0 ? " rd-clock--danger" : over === 0 && !settled ? " rd-clock--warning" : "")} title={formatAbsolute(application.stageEnteredAt)}>
                            {settled ? formatDayShort(application.decidedAt) : plural(daysInStage(application, now), "day", "days")}
                          </span>
                          {settled ? null : <span className="rd-sub">{stageAllowance(application) ? "of " + stageAllowance(application) : "no service level"}</span>}
                        </TableCell>
                        <TableCell className="rd-drop-lg">{labelOf(SOURCES, application.source)}</TableCell>
                        <TableCell className="rd-actions">
                          <RowActions transitions={transitionsFor(application)} onRun={(t) => onApplicationTransition(application, t)} label={"Actions for " + application.candidate.name}
                            extra={(close) => <MenuItemOpen onClick={() => { close(); onNavigate("/applications/" + application.id); }} />} />
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          {rows.length ? (
            <CardFooter className="rd-card-foot">
              <span className="rd-foot">Showing {from}–{to} of {rows.length} {view === ALL ? "applications" : views.find((v) => v.value === view).label.toLowerCase()}.</span>
              {pages > 1 ? (
                <Pagination>
                  <PaginationContent>
                    <PaginationItem><PaginationPrevious href="#" onClick={(e) => { e.preventDefault(); setPage(Math.max(1, current - 1)); }} /></PaginationItem>
                    {Array.from({ length: pages }).map((_, i) => (
                      <PaginationItem key={i}>
                        <PaginationLink href="#" isActive={current === i + 1} onClick={(e) => { e.preventDefault(); setPage(i + 1); }}>{i + 1}</PaginationLink>
                      </PaginationItem>
                    ))}
                    <PaginationItem><PaginationNext href="#" onClick={(e) => { e.preventDefault(); setPage(Math.min(pages, current + 1)); }} /></PaginationItem>
                  </PaginationContent>
                </Pagination>
              ) : null}
            </CardFooter>
          ) : null}
        </Card>
      </div>
    );
  }

  /* The row menu's one non-transition entry. */
  function MenuItemOpen({ onClick }) {
    const { MenuItem } = window.OriginDesignSystem_7c998b;
    return <MenuItem onClick={onClick}>Open the application</MenuItem>;
  }

  /* ── /applications/new — Staged. A real form, so it is its own route.
     Two legitimate ways in, and the screen offers both: key it in here, or send
     the candidate the form and let them fill it on their side. The second
     writes nothing to the desk — an application appears when they submit, not
     when we ask, because a record with no stage does not exist. ─────────────── */
  function NewApplicationScreen({ data, now, session, onNavigate, onCreateApplication, onInviteCandidate }) {
    const open = data.requisitions.filter((r) => r.status === "open");
    const [values, setValues] = React.useState({
      name: "", headline: "", location: "", requisitionId: open[0] ? open[0].id : "",
      source: "referral", rightToWork: "documents_seen", expectation: "", registration: "", note: "",
    });
    const [photo, setPhoto] = React.useState(null);
    const [photoError, setPhotoError] = React.useState(null);
    const [over, setOver] = React.useState(false);
    const [documents, setDocuments] = React.useState([]);
    const [submitted, setSubmitted] = React.useState(false);
    const [dirty, setDirty] = React.useState(false);
    const set = (id, value) => { setDirty(true); setValues((v) => Object.assign({}, v, { [id]: value })); };
    const requisition = requisitionById(data.requisitions, values.requisitionId);
    const photoInput = React.useRef(null);
    const fileInput = React.useRef(null);

    const readPhoto = (file) => {
      if (!file) return;
      if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setPhotoError("JPG, PNG or WebP only — that file is " + (file.type || "an unknown type") + "."); return; }
      if (file.size > 2 * 1024 * 1024) { setPhotoError("That is " + (file.size / 1048576).toFixed(1) + " MB. Up to 2 MB."); return; }
      setPhotoError(null);
      const reader = new FileReader();
      reader.onload = () => { setPhoto({ name: file.name, src: reader.result }); setDirty(true); };
      reader.readAsDataURL(file);
    };
    const addFiles = (files) => {
      if (!files || !files.length) return;
      const kindOf = (name) => (/cv|resume/i.test(name) ? "cv" : /state|cover|letter/i.test(name) ? "statement" : "case");
      setDocuments((list) => list.concat([].slice.call(files).map((file) => ({ kind: kindOf(file.name), name: file.name, at: now, sizeKb: Math.max(1, Math.round(file.size / 1024)) }))));
      setDirty(true);
    };

    const errors = {};
    if (!values.name.trim() || values.name.trim().indexOf(" ") === -1) errors.name = "Full name, as it appears on their documents.";
    if (!values.headline.trim()) errors.headline = "One line on who they are — it is what the queue shows.";
    if (!values.requisitionId) errors.requisitionId = "Pick the seat this application is for.";
    if (values.expectation) {
      const minor = Math.round(parseFloat(values.expectation.replace(/[^0-9.]/g, "")) * 100);
      if (!minor || isNaN(minor)) errors.expectation = "Whole pounds, or leave it empty.";
      else if (requisition && minor > requisition.bandMinor[1] * 1.25) errors.expectation = "That is well above the band's top of " + money(requisition.bandMinor[1]) + " — check the figure before you key it in.";
    }
    const valid = Object.keys(errors).length === 0;

    return (
      <div className="rd-page">
        <PageHead path="/applications/new" title="New application"
          note="For the ones that arrive by hand — a referral, a conversation at a careers event, somebody the pool turned up. Applications from the careers site and the jobs board land here on their own."
          actions={<Button variant="outline" size="sm" onClick={onInviteCandidate}><Icon name="mail" size={14} />Send the form to the candidate</Button>} />

        <div className="rd-columns">
          <Card>
            <CardHeader>
              <CardTitle>The candidate</CardTitle>
              <CardDescription>Enough to review them properly. Everything else is added as the application moves.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="rd-name">Full name</FieldLabel>
                  <Input id="rd-name" value={values.name} invalid={submitted && !!errors.name} placeholder="Rowan Ellis-Baptiste" onChange={(e) => set("name", e.target.value)} />
                  {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : null}
                </Field>
                <Field>
                  <FieldLabel>Photograph</FieldLabel>
                  <div className={"rd-upload" + (over ? " rd-upload--over" : "")}
                    onDragOver={(e) => { e.preventDefault(); setOver(true); }}
                    onDragLeave={() => setOver(false)}
                    onDrop={(e) => { e.preventDefault(); setOver(false); readPhoto(e.dataTransfer.files && e.dataTransfer.files[0]); }}>
                    <span className="rd-upload__face">
                      {photo
                        ? <img src={photo.src} alt={"Photograph of " + (values.name.trim() || "the candidate")} />
                        : <Icon name="user-round" size={26} />}
                    </span>
                    <div className="rd-upload__body">
                      <span className="rd-upload__note">
                        {photo
                          ? photo.name + " staged. It becomes their face on every screen when you save."
                          : "No photograph yet — their initials stand in wherever a face appears."}
                      </span>
                      <span className="rd-upload__actions">
                        <Button variant="outline" size="sm" onClick={() => photoInput.current.click()}>
                          <Icon name="upload" size={14} />{photo ? "Replace" : "Upload a photo"}
                        </Button>
                        {photo ? <Button variant="ghost" size="sm" onClick={() => { setPhoto(null); setPhotoError(null); setDirty(true); }}>Remove</Button> : null}
                      </span>
                      {photoError
                        ? <FieldError>{photoError}</FieldError>
                        : <span className="rd-foot">Drop an image here, or pick one. JPG, PNG or WebP; square reads best; up to 2 MB.</span>}
                    </div>
                    <input ref={photoInput} className="rd-sr" type="file" accept="image/png,image/jpeg,image/webp" aria-label="Choose a photograph of the candidate" onChange={(e) => { readPhoto(e.target.files && e.target.files[0]); e.target.value = ""; }} />
                  </div>
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-headline">Headline</FieldLabel>
                  <Input id="rd-headline" value={values.headline} invalid={submitted && !!errors.headline} placeholder="Band 5 nurse, district team, three years" onChange={(e) => set("headline", e.target.value)} />
                  {submitted && errors.headline ? <FieldError>{errors.headline}</FieldError> : <FieldDescription>Shown wherever their name appears.</FieldDescription>}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-location">Where they are</FieldLabel>
                  <Input id="rd-location" value={values.location} placeholder="Community, north" onChange={(e) => set("location", e.target.value)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-registration">Professional registration</FieldLabel>
                  <Input id="rd-registration" value={values.registration} placeholder="NMC 21F4478" onChange={(e) => set("registration", e.target.value)} />
                  <FieldDescription>{requisition && requisition.registration ? "This seat asks for " + requisition.registration + " registration — the checks will not pass without it." : requisition ? "This seat asks for no professional register." : "Leave empty for non-clinical seats."}</FieldDescription>
                </Field>
                <Field>
                  <FieldLabel>Documents</FieldLabel>
                  {documents.length ? (
                    <div className="rd-list">
                      {documents.map((document, i) => (
                        <div className="rd-list__row" key={document.name + i}>
                          <div className="rd-list__main">
                            <span className="rd-record__name">{document.name}</span>
                            <span className="rd-record__meta">{document.kind === "cv" ? "CV" : document.kind === "statement" ? "Supporting statement" : "Other"} · {document.sizeKb} KB</span>
                          </div>
                          <div className="rd-list__side">
                            <Button variant="ghost" size="xs" onClick={() => setDocuments((list) => list.filter((_, x) => x !== i))}>Remove</Button>
                          </div>
                        </div>
                      ))}
                    </div>
                  ) : null}
                  <span className="rd-actions__row" style={{ justifyContent: "flex-start" }}>
                    <Button variant="outline" size="xs" onClick={() => fileInput.current.click()}><Icon name="paperclip" size={12} />Add documents</Button>
                  </span>
                  <input ref={fileInput} className="rd-sr" type="file" multiple accept=".pdf,.doc,.docx,.png,.jpg" aria-label="Add documents to this application" onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }} />
                  <FieldDescription>
                    {documents.length
                      ? plural(documents.length, "file", "files") + " will be attached to the record. A CV is what the screening step reads."
                      : "The CV, and the supporting statement if there is one. Named by what they are, so the record reads properly later."}
                  </FieldDescription>
                </Field>
              </div>
            </CardPanel>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>The seat, and where they came from</CardTitle>
              <CardDescription>{requisition ? bandLabel(requisition) + " · target start " + formatDay(requisition.targetStartAt) : "Pick a requisition to see its band."}</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="rd-req">Requisition</FieldLabel>
                  <Select id="rd-req" value={values.requisitionId} invalid={submitted && !!errors.requisitionId}
                    items={open.map((r) => ({ value: r.id, label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site) }))}
                    onValueChange={(v) => set("requisitionId", v)} />
                  {submitted && errors.requisitionId ? <FieldError>{errors.requisitionId}</FieldError> : <FieldDescription>Only open requisitions accept applications. {data.requisitions.filter((r) => r.status === "on_hold").length} is on hold.</FieldDescription>}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-source">Where they came from</FieldLabel>
                  <Select id="rd-source" value={values.source} items={SOURCES.map((s) => ({ value: s.value, label: s.label }))} onValueChange={(v) => set("source", v)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-expectation">Salary expectation</FieldLabel>
                  <Input id="rd-expectation" value={values.expectation} invalid={submitted && !!errors.expectation} placeholder={requisition ? String(Math.round(requisition.bandMinor[0] / 100)) : "34000"} onChange={(e) => set("expectation", e.target.value)} />
                  {submitted && errors.expectation ? <FieldError>{errors.expectation}</FieldError> : <FieldDescription>Optional. Whole pounds. {requisition ? "Band is " + bandLabel(requisition) + "." : ""}</FieldDescription>}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-rtw">Right to work</FieldLabel>
                  <RadioGroup name="rd-rtw" value={values.rightToWork} onValueChange={(v) => set("rightToWork", v)}
                    items={RIGHT_TO_WORK.map((r) => ({ value: r.id, label: r.label }))} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rd-note">First note</FieldLabel>
                  <Textarea id="rd-note" rows={3} value={values.note} placeholder="Who referred them, what they said, anything the reviewer should know before they read the CV." onChange={(e) => set("note", e.target.value)} />
                  <FieldDescription>Posted on the thread in your name. Type @ and a colleague's id to pull them in.</FieldDescription>
                </Field>
              </div>
            </CardPanel>
          </Card>
        </div>

        <FormBar dirty={dirty} label="Save application" onCancel={() => onNavigate("/applications")}
          note={dirty
            ? "It enters at applied, unread, owned by you" + (documents.length ? ", with " + plural(documents.length, "document", "documents") : "") + ", and appears in every count on the desk."
            : "Nothing is saved until you fill this in — Applications is unchanged. Or send the candidate the form and let them fill it themselves."}
          onSave={() => { setSubmitted(true); if (valid) onCreateApplication(Object.assign({}, values, { photo: photo ? photo.src : null, documents })); }} />
      </div>
    );
  }

  Object.assign(window, { ApplicationsScreen, NewApplicationScreen });
})();

</script>
<script type="text/plain" data-rd-file="ApplicationScreen.jsx">
/* One application, read properly — the screen this product exists for.
   Instant commit model: the stage moves, the watch toggle and the notes apply
   immediately; consequential ones raise an undo toast; declining opens a staged
   dialog and then a confirm. The header's actions are whatever the record's
   stage allows (transitionsFor) — never a fixed set.
   The panel, checks and offer sections render only when the record has reached
   them or holds their sub-records: one data state per screen. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, Progress, MenuItem,
  } = window.OriginDesignSystem_7c998b;

  function OfferPanel({ application, requisition, now }) {
    const offer = application.offer;
    const position = bandPosition(requisition, offer.salaryMinor);
    const left = daysBetween(now, offer.expiresAt);
    return (
      <Card>
        <CardHeader>
          <CardTitle>Offer</CardTitle>
          <CardDescription>
            {offer.acceptedAt
              ? "Accepted " + formatDay(offer.acceptedAt) + ", starting " + formatDay(offer.startAt) + "."
              : "Sent " + formatDay(offer.sentAt) + " and valid to " + formatDay(offer.expiresAt) + " — " + durationWords(left) + "."}
          </CardDescription>
        </CardHeader>
        <CardPanel>
          <div className="rd-stack">
            <span className="rd-part">
              <span className="rd-spread">
                <span className="rd-anchor__figure" style={{ fontSize: "var(--text-2xl)" }}>{money(offer.salaryMinor)}</span>
                <span className="rd-foot">{percent(position)} up the band</span>
              </span>
              <Bar ratio={position} />
              <span className="rd-foot">Band {bandLabel(requisition)} on {requisition.ref}.</span>
            </span>
            <KeyValues items={[
              { label: "Start date", value: formatDay(offer.startAt) },
              { label: "Expires", value: formatDay(offer.expiresAt), note: offer.acceptedAt ? "Answered before it ran out" : durationWords(left) },
              { label: "Expectation", value: application.salaryExpectationMinor ? money(application.salaryExpectationMinor) : "Not stated",
                note: application.salaryExpectationMinor ? (offer.salaryMinor >= application.salaryExpectationMinor ? "Met" : money(application.salaryExpectationMinor - offer.salaryMinor) + " below what they asked") : null },
            ]} />
            {offer.note ? <p className="rd-quote">{offer.note}</p> : null}
          </div>
        </CardPanel>
      </Card>
    );
  }

  function ChecksPanel({ application, data, now, onRun }) {
    const refs = referenceState(application);
    const missing = checksMissing(application);
    return (
      <Card>
        <CardHeader>
          <CardTitle>Checks</CardTitle>
          <CardDescription>
            {refs.received} of {refs.required} references back · right to work {statusWords(RIGHT_TO_WORK, application.rightToWork)} · DBS {statusWords(DBS_STATUS, application.dbs)}.
          </CardDescription>
        </CardHeader>
        <CardPanel>
          <div className="rd-stack">
            {missing.length ? (
              <Alert variant="warning">
                <Icon name="clock" size={16} />
                <AlertTitle>An offer waits on {plural(missing.length, "thing", "things")}</AlertTitle>
                <AlertDescription>{missing.join(" · ")}. Nothing here is ours to hurry except the chasing.</AlertDescription>
              </Alert>
            ) : (
              <Alert variant="success">
                <Icon name="circle-check" size={16} />
                <AlertTitle>Everything is in</AlertTitle>
                <AlertDescription>Both references, right to work verified and a clear DBS. The offer is the desk's to send.</AlertDescription>
              </Alert>
            )}
            <div className="rd-list">
              {refs.refs.map((reference) => (
                <div className="rd-list__row" key={reference.id}>
                  <div className="rd-list__main">
                    <span className="rd-record__name">{reference.name}</span>
                    <span className="rd-record__meta">{reference.relationship}</span>
                    {reference.note ? <p className="rd-quote">{reference.note}</p> : null}
                  </div>
                  <div className="rd-list__side">
                    <StatusBadge values={REFERENCE_STATUS} value={reference.status} />
                    <span className="rd-clock" title={formatAbsolute(reference.requestedAt)}>
                      {reference.receivedAt ? "Back " + formatDayShort(reference.receivedAt) : "Asked " + formatDayShort(reference.requestedAt)}
                    </span>
                    {reference.status !== "received" ? <Button variant="outline" size="xs" onClick={() => onRun(reference)}>Chase</Button> : null}
                  </div>
                </div>
              ))}
            </div>
          </div>
        </CardPanel>
      </Card>
    );
  }

  function ApplicationScreen({ application, data, now, session, onNavigate, onApplicationTransition, onInterviewTransition, onPostNote, onReact, onToggleWatch, onChaseReference }) {
    const me = staffById(data.staff, session.personId);
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const owner = staffById(data.staff, application.ownerId);
    const score = scoresOf(application);
    const interviews = interviewsFor(data.interviews, application.id);
    const thread = threadFor(data.notes, application.id);
    const followers = followersOf(application, data.notes, data.staff);
    const watching = (application.watcherIds || []).indexOf(me.id) > -1;
    const [replyingTo, setReplyingTo] = React.useState(null);
    const transitions = transitionsFor(application);
    const reachedChecks = stageIndex(application.stage) >= stageIndex("checks") || (application.references || []).length > 0;
    const c = application.candidate;

    return (
      <div className="rd-page">
        <PageHead path="/applications/:id" params={{ id: application.id }} labels={{ "/applications/:id": c.name }}
          title={c.name} note={requisition.title + " · " + requisition.ref + " · applied " + formatDay(application.appliedAt) + " through " + labelOf(SOURCES, application.source).toLowerCase()} />

        <Card>
          <div className="rd-cover">
            <div className="rd-cover__body">
              <div className="rd-cover__id">
                <Avatar size={88} src={c.photo} alt="" fallback={c.initials} />
                <div className="rd-cover__text">
                  <span className="rd-cover__name">{c.name}</span>
                  <span className="rd-cover__meta">
                    <span>{c.headline}</span>
                    <span>· {c.location}</span>
                    {application.registration ? <span>· {application.registration}</span> : null}
                  </span>
                  <span className="rd-status">
                    <StatusBadge values={APPLICATION_STAGE} value={application.stage} />
                    <StatusBadge values={RIGHT_TO_WORK} value={application.rightToWork} />
                    {APPLICATION_FLAGS.isStalled(application, now) ? <Badge size="sm" variant="warning">{plural(stageOverBy(application, now), "day", "days")} over its service level</Badge> : null}
                  </span>
                </div>
              </div>
              <div className="rd-cover__actions">
                <Button variant={watching ? "outline" : "ghost"} size="sm" aria-pressed={watching} onClick={() => onToggleWatch(application)}>
                  <Icon name={watching ? "eye-off" : "eye"} size={14} />{watching ? "Stop watching" : "Watch"}
                </Button>
                <RowActions transitions={transitions} onRun={(t) => onApplicationTransition(application, t)} label={"Actions for " + c.name}
                  extra={(close) => <MenuItem onClick={() => { close(); onNavigate("/requisitions/" + requisition.id); }}>Open the requisition</MenuItem>} />
              </div>
            </div>
          </div>
          <CardPanel>
            <div className="rd-stack">
              <Stepper application={application} data={data} now={now} />
              <StageNote application={application} data={data} now={now} />
              <Followers people={followers} />
            </div>
          </CardPanel>
        </Card>

        <div className="rd-columns">
          <div className="rd-col">
            {score.count ? (
              <Card>
                <CardHeader>
                  <CardTitle>Panel scorecards</CardTitle>
                  <CardDescription>
                    {plural(score.count, "scorecard", "scorecards")} of {score.required} · {score.average.toFixed(2)} average · {score.yeses} recommending yes.
                  </CardDescription>
                </CardHeader>
                <CardPanel>
                  <div className="rd-stack">
                    {score.complete ? null : (
                      <Alert variant="warning">
                        <Icon name="triangle-alert" size={16} />
                        <AlertTitle>One more scorecard before this can move</AlertTitle>
                        <AlertDescription>
                          {score.required} are needed from different people. {firstNameOf(staffById(data.staff, score.cards[0].byId))} has filed hers; the panel's other seat has not.
                        </AlertDescription>
                      </Alert>
                    )}
                    <ScoreMatrix application={application} data={data} />
                    <Separator />
                    {score.cards.map((card) => {
                      const author = staffById(data.staff, card.byId);
                      return (
                        <div className="rd-note" key={card.byId}>
                          <Avatar size={32} src={author.photo} alt="" fallback={author.initials} />
                          <div className="rd-note__body">
                            <div className="rd-note__head">
                              <span className="rd-note__who">{author.name}</span>
                              <span className="rd-note__role">{author.title}</span>
                              <Badge size="sm" variant={card.recommend === "yes" ? "success" : "warning"}>{card.recommend === "yes" ? "Recommends" : "Not sure"}</Badge>
                              <span className="rd-note__at" title={formatAbsolute(card.at)}>{formatWhen(card.at, now)}</span>
                            </div>
                            <p className="rd-note__text">{card.note}</p>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </CardPanel>
              </Card>
            ) : null}

            {reachedChecks ? <ChecksPanel application={application} data={data} now={now} onRun={onChaseReference} /> : null}

            <Card>
              <CardHeader>
                <CardTitle>The thread</CardTitle>
                <CardDescription>
                  {plural(notesFor(data.notes, application.id).length, "note", "notes")} from {plural(followers.length, "person", "people")}. The candidate never sees any of it.
                </CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-thread">
                  {thread.length ? null : (
                    <p className="rd-foot">
                      Nothing on the thread yet. The first note is usually why this one is worth reading —
                      who referred them, what stood out in the CV, or what somebody else should check.
                    </p>
                  )}
                  {thread.map(({ note, replies }) => (
                    <React.Fragment key={note.id}>
                      <Note note={note} staff={data.staff} me={me} now={now} onReact={onReact} onReply={setReplyingTo} />
                      {replies.map((reply) => (
                        <Note key={reply.id} note={reply} staff={data.staff} me={me} now={now} reply onReact={onReact} />
                      ))}
                    </React.Fragment>
                  ))}
                  <Composer me={me} staff={data.staff} replyingTo={replyingTo} onCancelReply={() => setReplyingTo(null)}
                    onPost={(body, kind, parent) => { onPostNote(application, body, kind, parent); setReplyingTo(null); }} />
                </div>
              </CardPanel>
            </Card>
          </div>

          <div className="rd-col">
            <Card>
              <CardHeader><CardTitle>The application</CardTitle></CardHeader>
              <CardPanel>
                <div className="rd-stack">
                  <KeyValues items={[
                    { label: "Applied", value: formatDay(application.appliedAt), note: plural(daysSinceApplied(application, now), "day", "days") + " with us" },
                    { label: "Source", value: labelOf(SOURCES, application.source), note: application.referredById ? "Referred by " + staffById(data.staff, application.referredById).name : null },
                    { label: "Owner", value: owner ? owner.name : "Unassigned" },
                    { label: "First read", value: application.firstReadAt ? formatWhen(application.firstReadAt, now) : "Not yet", note: application.firstReadAt ? null : "It counts as unread on the desk" },
                  ]} />
                  <Separator />
                  <span className="rd-part">
                    <span className="rd-kv__label">Expectation against the band</span>
                    {application.salaryExpectationMinor ? (
                      <>
                        <span className="rd-spread">
                          <span className="rd-kv__value">{money(application.salaryExpectationMinor)}</span>
                          <span className="rd-foot">{bandLabel(requisition)}</span>
                        </span>
                        <Bar ratio={bandPosition(requisition, application.salaryExpectationMinor)}
                          tone={application.salaryExpectationMinor > requisition.bandMinor[1] ? "warn" : null} />
                        <span className="rd-foot">
                          {application.salaryExpectationMinor > requisition.bandMinor[1]
                            ? money(application.salaryExpectationMinor - requisition.bandMinor[1]) + " above the top of the band — it has to be settled before an offer."
                            : percent(bandPosition(requisition, application.salaryExpectationMinor)) + " up the band, which the seat can pay."}
                        </span>
                      </>
                    ) : <span className="rd-foot">Not stated on the application.</span>}
                  </span>
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Documents</CardTitle>
                <CardDescription>{plural(application.documents.length, "file", "files")} on the record.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-list">
                  {application.documents.map((document) => (
                    <div className="rd-list__row" key={document.name}>
                      <div className="rd-list__main">
                        <span className="rd-record__name">{document.name}</span>
                        <span className="rd-record__meta">{document.kind === "cv" ? "CV" : document.kind === "case" ? "Assessment response" : "Supporting statement"}</span>
                      </div>
                      <div className="rd-list__side">
                        <span className="rd-clock" title={formatAbsolute(document.at)}>{formatWhen(document.at, now)}</span>
                        <Button variant="outline" size="xs"><Icon name="download" size={12} />Download</Button>
                      </div>
                    </div>
                  ))}
                </div>
              </CardPanel>
            </Card>

            {interviews.length ? (
              <Card>
                <CardHeader>
                  <CardTitle>Interviews</CardTitle>
                  <CardDescription>{plural(interviews.length, "conversation", "conversations")} booked on this application.</CardDescription>
                </CardHeader>
                <CardPanel>
                  <div className="rd-list">
                    {interviews.map((interview) => (
                      <div className="rd-list__row" key={interview.id}>
                        <div className="rd-list__main">
                          <span className="rd-record__name">{statusValue(APPLICATION_STAGE, interview.kind === "screening" ? "screening" : interview.kind === "panel" ? "panel" : "assessment").label} · {formatWeekday(interview.at)} {formatTime(interview.at)}</span>
                          <span className="rd-record__meta">{interview.where} · {panelOf(data.staff, interview).map((p) => p.name).join(", ")}</span>
                          {interview.note ? <p className="rd-quote">{interview.note}</p> : null}
                        </div>
                        <div className="rd-list__side">
                          <StatusBadge values={INTERVIEW_STATUS} value={interview.status} />
                          <RowActions transitions={interviewTransitions(interview)} onRun={(t) => onInterviewTransition(interview, t)} label={"Actions for the " + interview.kind} />
                        </div>
                      </div>
                    ))}
                  </div>
                </CardPanel>
              </Card>
            ) : null}

            {application.offer ? <OfferPanel application={application} requisition={requisition} now={now} /> : null}
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { ApplicationScreen });
})();

</script>
<script type="text/plain" data-rd-file="RequisitionScreens.jsx">
/* Requisitions — the seats, the board behind one of them, and the form that
   raises a new one. Instant on the list and the record; /requisitions/new is
   Staged. Every figure derives from applicationsFor(...) on the same array. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, Progress, MenuItem,
    Field, FieldLabel, FieldDescription, FieldError, Input, Textarea, Select, NumberField,
  } = window.OriginDesignSystem_7c998b;

  const ALL = "all";

  function RequisitionsScreen({ data, now, session, onNavigate, onRequisitionTransition, pushToast }) {
    const [query, setQuery] = React.useState("");
    const [status, setStatus] = React.useState(ALL);
    const [site, setSite] = React.useState(ALL);
    const dirty = !!query || status !== ALL || site !== ALL;
    const clear = () => { setQuery(""); setStatus(ALL); setSite(ALL); };

    const rows = data.requisitions.filter((requisition) => {
      if (status !== ALL && requisition.status !== status) return false;
      if (site !== ALL && requisition.site !== site) return false;
      if (!query) return true;
      return (requisition.title + " " + requisition.ref + " " + labelOf(DEPARTMENTS, requisition.department)).toLowerCase().indexOf(query.trim().toLowerCase()) > -1;
    }).sort((a, b) => a.openedAt - b.openedAt);
    const openRows = rows.filter(REQUISITION_FLAGS.isOpen);
    const seats = sumBy(openRows, (r) => requisitionHealth(data.applications, r, now).seatsLeft);

    return (
      <div className="rd-page">
        <PageHead path="/requisitions" title="Requisitions"
          note={plural(openRows.length, "requisition", "requisitions") + " open in this view, " + plural(seats, "seat", "seats") + " still unfilled."}
          actions={<Button size="sm" onClick={() => onNavigate("/requisitions/new")}><Icon name="plus" size={14} />New requisition</Button>} />

        <FilterBar query={query} onQuery={setQuery} placeholder="Search a title, a reference or a department…" dirty={dirty} onClear={clear}
          selects={[
            { id: "status", label: "Status", value: status, onChange: setStatus,
              items: [{ value: ALL, label: "Any status" }].concat(REQUISITION_STATUS.map((s) => ({ value: s.id, label: s.label }))) },
            { id: "site", label: "Site", value: site, onChange: setSite,
              items: [{ value: ALL, label: "Every site" }].concat(SITES.map((s) => ({ value: s.value, label: s.label }))) },
          ]} />

        <Card>
          {rows.length === 0 ? (
            <NoResults title="No requisition matches that" description={"Clearing the filters brings back " + plural(data.requisitions.length, "requisition", "requisitions") + "."} onClear={clear} />
          ) : (
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead>Requisition</TableHead>
                    <TableHead className="rd-drop-md">Manager</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead className="rd-num">Seats</TableHead>
                    <TableHead style={{ minWidth: "12rem" }}>Live pipeline</TableHead>
                    <TableHead className="rd-drop-lg">Open</TableHead>
                    <TableHead className="rd-drop-lg">Target start</TableHead>
                    <TableHead className="rd-actions-head">Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((requisition) => {
                    const health = requisitionHealth(data.applications, requisition, now);
                    const manager = staffById(data.staff, requisition.hiringManagerId);
                    const settled = requisition.status === "closed" || requisition.status === "filled";
                    return (
                      <TableRow key={requisition.id} className={settled ? "rd-row--recessed" : undefined}>
                        <TableCell>
                          <button type="button" className="rd-link rd-record__name" onClick={() => onNavigate("/requisitions/" + requisition.id)}>{requisition.title}</button>
                          <span className="rd-sub">{requisition.ref} · {labelOf(SITES, requisition.site)} · {labelOf(EMPLOYMENT_TYPES, requisition.employmentType)}</span>
                        </TableCell>
                        <TableCell className="rd-drop-md"><StaffCell person={manager} meta={labelOf(DEPARTMENTS, requisition.department)} /></TableCell>
                        <TableCell>
                          <span className="rd-status">
                            <StatusBadge values={REQUISITION_STATUS} value={requisition.status} />
                            {health.thin ? <Badge size="sm" variant="warning">Thin</Badge> : null}
                          </span>
                        </TableCell>
                        <TableCell className="rd-num">{health.accepted} of {health.seats}</TableCell>
                        <TableCell><StageStrip applications={data.applications} requisitionId={requisition.id} /></TableCell>
                        <TableCell className="rd-drop-lg">
                          <span className={health.aging ? "rd-clock rd-clock--warning" : "rd-clock"} title={formatAbsolute(requisition.openedAt)}>
                            {requisition.status === "open" ? plural(health.openDays, "day", "days") : formatDayShort(requisition.openedAt)}
                          </span>
                          {health.aging ? <span className="rd-sub">past {RD_POLICY.agingDays} days</span> : null}
                        </TableCell>
                        <TableCell className="rd-drop-lg">{formatDayShort(requisition.targetStartAt)}</TableCell>
                        <TableCell className="rd-actions">
                          <RowActions transitions={requisitionTransitions(requisition)} onRun={(t) => onRequisitionTransition(requisition, t)} label={"Actions for " + requisition.title}
                            extra={(close) => <MenuItem onClick={() => { close(); onNavigate("/requisitions/" + requisition.id); }}>Open the pipeline</MenuItem>} />
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          {rows.length ? (
            <CardFooter className="rd-card-foot">
              <span className="rd-foot">Showing all {rows.length} of {data.requisitions.length} requisitions. Seats count accepted offers against headcount, not applications.</span>
              <ExportMenu scope={plural(rows.length, "requisition", "requisitions")} onExport={(format) => pushToast({ title: format + " export started", description: "The file lands in your downloads." })} />
            </CardFooter>
          ) : null}
        </Card>
      </div>
    );
  }

  /* ── One requisition: the board is the display, because the question here is
     "where is everybody", not "list them". ─────────────────────────────────── */
  function RequisitionScreen({ requisition, data, now, session, onNavigate, onRequisitionTransition, onApplicationTransition }) {
    const health = requisitionHealth(data.applications, requisition, now);
    const advert = advertFor(data.adverts, requisition.id);
    const board = pipelineBoard(data.applications, requisition.id);
    const manager = staffById(data.staff, requisition.hiringManagerId);
    const recruiter = staffById(data.staff, requisition.recruiterId);

    return (
      <div className="rd-page">
        <PageHead path="/requisitions/:id" params={{ id: requisition.id }} labels={{ "/requisitions/:id": requisition.title }}
          title={requisition.title}
          note={requisition.ref + " · " + labelOf(SITES, requisition.site) + " · " + labelOf(EMPLOYMENT_TYPES, requisition.employmentType) + " · " + bandLabel(requisition)}
          actions={<Transitions size="sm" transitions={requisitionTransitions(requisition)} onRun={(t) => onRequisitionTransition(requisition, t)} />} />

        {/* Views within the record, both registered routes — never a second set
            of top-level destinations. */}
        <div className="rd-card-tabs">
          <ViewTabs value="pipeline" onSelect={(v) => { if (v === "advert") onNavigate("/requisitions/" + requisition.id + "/advert"); }}
            views={[
              { value: "pipeline", label: "Pipeline", count: health.live.length },
              { value: "advert", label: "Advert", count: advert ? advert.postedTo.length : 0 },
            ]} />
        </div>

        {requisition.status === "on_hold" ? (
          <Alert variant="warning">
            <Icon name="pause" size={16} />
            <AlertTitle>On hold</AlertTitle>
            <AlertDescription>{requisition.holdNote}</AlertDescription>
          </Alert>
        ) : null}
        {health.thin ? (
          <Alert variant="warning">
            <Icon name="triangle-alert" size={16} />
            <AlertTitle>{plural(health.live.length, "live application", "live applications")} for {plural(health.seatsLeft, "unfilled seat", "unfilled seats")}</AlertTitle>
            <AlertDescription>Under {RD_POLICY.thinPipeline} live applications per requisition is where seats start slipping. The target start is {formatDay(requisition.targetStartAt)}, {durationWords(health.daysToTarget)}.</AlertDescription>
          </Alert>
        ) : null}

        <div className="rd-anchor">
          <span className="rd-part">
            <span className="rd-anchor__label">Seats filled</span>
            <span className="rd-anchor__figure">{health.accepted} of {health.seats}</span>
            <span className="rd-tile__qual">Accepted offers, not applications</span>
          </span>
          <span className="rd-part">
            <span className="rd-anchor__label">Live applications</span>
            <span className="rd-anchor__figure">{health.live.length}</span>
            <span className="rd-tile__qual">{health.awaitingUs} waiting on the desk</span>
          </span>
          <span className="rd-part">
            <span className="rd-anchor__label">Over their service level</span>
            <span className="rd-anchor__figure">{health.stalled}</span>
            <span className="rd-tile__qual">Per stage, on this requisition only</span>
          </span>
          <span className="rd-part">
            <span className="rd-anchor__label">Open</span>
            <span className="rd-anchor__figure">{health.openDays}</span>
            <span className="rd-tile__qual">days since {formatDay(requisition.openedAt)}{health.aging ? " — past " + RD_POLICY.agingDays : ""}</span>
          </span>
        </div>

        <Card>
          <CardHeader>
            <CardTitle>The brief</CardTitle>
            <CardDescription>Target start {formatDay(requisition.targetStartAt)} · {plural(requisition.headcount, "seat", "seats")}{requisition.registration ? " · " + requisition.registration + " registration required" : " · no professional register required"}</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-stack">
              <p className="rd-note__text">{requisition.brief || requisition.holdNote}</p>
              <Separator />
              <div className="rd-kv">
                <span className="rd-part"><span className="rd-kv__label">Hiring manager</span><StaffCell person={manager} /></span>
                <span className="rd-part"><span className="rd-kv__label">Recruiter</span><StaffCell person={recruiter} /></span>
                <span className="rd-part"><span className="rd-kv__label">Band</span><span className="rd-kv__value">{bandLabel(requisition)}</span></span>
                <span className="rd-part"><span className="rd-kv__label">Department</span><span className="rd-kv__value">{labelOf(DEPARTMENTS, requisition.department)}</span></span>
              </div>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>The pipeline</CardTitle>
            <CardDescription>{plural(health.all.length, "application", "applications")} in all, {health.live.length} of them live. One column per step of the process.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-board">
              {board.map((column) => (
                <div className="rd-lane" key={column.stage}>
                  <div className="rd-lane__head">
                    <span className="rd-lane__title">{column.label}<span className="rd-lane__count">{column.items.length}</span></span>
                    <span className="rd-lane__figure">{column.means}</span>
                  </div>
                  <div className="rd-lane__list">
                    {column.items.length
                      ? column.items.map((application) => <AppCard key={application.id} application={application} data={data} now={now} onNavigate={onNavigate} />)
                      : <p className="rd-lane__empty">Nobody at this step.</p>}
                  </div>
                </div>
              ))}
            </div>
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">Settled applications are not on the board. {plural(health.all.filter((a) => !APPLICATION_FLAGS.isLive(a)).length, "application", "applications")} declined, withdrawn or hired.</span>
            <Button variant="outline" size="sm" onClick={() => onNavigate("/applications")}>See them in the queue</Button>
          </CardFooter>
        </Card>
      </div>
    );
  }

  /* ── /requisitions/new — Staged, its own route, opens as a draft. ──────── */
  function NewRequisitionScreen({ data, now, session, onNavigate, onCreateRequisition }) {
    const [values, setValues] = React.useState({
      title: "", department: "nursing", site: "community", employmentType: "permanent",
      headcount: "1", bandLow: "", bandHigh: "", registration: "NMC", hiringManagerId: "p-esme", targetStart: "", brief: "",
    });
    const [dirty, setDirty] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    const set = (id, value) => { setDirty(true); setValues((v) => Object.assign({}, v, { [id]: value })); };

    const low = Math.round(parseFloat(values.bandLow.replace(/[^0-9.]/g, "")) * 100);
    const high = Math.round(parseFloat(values.bandHigh.replace(/[^0-9.]/g, "")) * 100);
    const errors = {};
    if (!values.title.trim()) errors.title = "What is the seat called? It is what candidates search for.";
    if (!values.bandLow || isNaN(low)) errors.bandLow = "Bottom of the band, whole pounds.";
    if (!values.bandHigh || isNaN(high)) errors.bandHigh = "Top of the band, whole pounds.";
    else if (low && high && high < low) errors.bandHigh = "The top of the band cannot be below the bottom.";
    if (!values.targetStart) errors.targetStart = "When does this seat need somebody in it?";
    else if (new Date(values.targetStart + "T00:00:00").getTime() < now) errors.targetStart = "That date has passed.";
    if (!Number(values.headcount) || Number(values.headcount) < 1) errors.headcount = "At least one seat.";
    const valid = Object.keys(errors).length === 0;

    return (
      <div className="rd-page">
        <PageHead path="/requisitions/new" title="New requisition"
          note="It opens as a draft. Nothing can be applied to a draft — opening it is a second, deliberate action from the requisition itself." />
        <div className="rd-columns">
          <Card>
            <CardHeader><CardTitle>The seat</CardTitle></CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="rq-title">Title</FieldLabel>
                  <Input id="rq-title" value={values.title} invalid={submitted && !!errors.title} placeholder="Community staff nurse" onChange={(e) => set("title", e.target.value)} />
                  {submitted && errors.title ? <FieldError>{errors.title}</FieldError> : null}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-dept">Department</FieldLabel>
                  <Select id="rq-dept" value={values.department} items={DEPARTMENTS.map((d) => ({ value: d.value, label: d.label }))} onValueChange={(v) => set("department", v)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-site">Site</FieldLabel>
                  <Select id="rq-site" value={values.site} items={SITES.map((s) => ({ value: s.value, label: s.label }))} onValueChange={(v) => set("site", v)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-type">Employment type</FieldLabel>
                  <Select id="rq-type" value={values.employmentType} items={EMPLOYMENT_TYPES.map((t) => ({ value: t.value, label: t.label }))} onValueChange={(v) => set("employmentType", v)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-register">Professional register</FieldLabel>
                  <Select id="rq-register" value={values.registration} items={REGISTERS.map((r) => ({ value: r.value, label: r.label }))} onValueChange={(v) => set("registration", v)} />
                  <FieldDescription>Whether this seat may only be filled by somebody on a register. Compliance holds a start date on it; a seat that asks for none is not blocked by it.</FieldDescription>
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-heads">Seats</FieldLabel>
                  <Input id="rq-heads" value={values.headcount} invalid={submitted && !!errors.headcount} onChange={(e) => set("headcount", e.target.value)} />
                  {submitted && errors.headcount ? <FieldError>{errors.headcount}</FieldError> : <FieldDescription>How many people this requisition may hire. Seats fill on accepted offers.</FieldDescription>}
                </Field>
              </div>
            </CardPanel>
          </Card>
          <Card>
            <CardHeader><CardTitle>Money, dates and the brief</CardTitle></CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="rq-low">Band, bottom</FieldLabel>
                  <Input id="rq-low" value={values.bandLow} invalid={submitted && !!errors.bandLow} placeholder="31049" onChange={(e) => set("bandLow", e.target.value)} />
                  {submitted && errors.bandLow ? <FieldError>{errors.bandLow}</FieldError> : null}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-high">Band, top</FieldLabel>
                  <Input id="rq-high" value={values.bandHigh} invalid={submitted && !!errors.bandHigh} placeholder="37796" onChange={(e) => set("bandHigh", e.target.value)} />
                  {submitted && errors.bandHigh ? <FieldError>{errors.bandHigh}</FieldError> : <FieldDescription>Every offer is shown as a position inside this band.</FieldDescription>}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-manager">Hiring manager</FieldLabel>
                  <Select id="rq-manager" value={values.hiringManagerId}
                    items={data.staff.filter((p) => p.role === "hiring_manager").map((p) => ({ value: p.id, label: p.name + " · " + p.title }))}
                    onValueChange={(v) => set("hiringManagerId", v)} />
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-start">Target start</FieldLabel>
                  <Input id="rq-start" type="date" value={values.targetStart} invalid={submitted && !!errors.targetStart} onChange={(e) => set("targetStart", e.target.value)} />
                  {submitted && errors.targetStart ? <FieldError>{errors.targetStart}</FieldError> : null}
                </Field>
                <Field>
                  <FieldLabel htmlFor="rq-brief">Brief</FieldLabel>
                  <Textarea id="rq-brief" rows={4} value={values.brief} placeholder="What the seat is, what is essential, what is desirable, anything unusual about the rota." onChange={(e) => set("brief", e.target.value)} />
                </Field>
              </div>
            </CardPanel>
          </Card>
        </div>
        <FormBar dirty={dirty} label="Save requisition" onCancel={() => onNavigate("/requisitions")}
          note={dirty ? "Saved as a draft, owned by you. It appears on Requisitions immediately and accepts nothing until it is opened." : "Nothing is saved yet."}
          onSave={() => { setSubmitted(true); if (valid) onCreateRequisition(Object.assign({}, values, { bandMinor: [low, high] })); }} />
      </div>
    );
  }

  Object.assign(window, { RequisitionsScreen, RequisitionScreen, NewRequisitionScreen });
})();

</script>
<script type="text/plain" data-rd-file="AdvertScreen.jsx">
/* The advert behind one requisition — a view WITHIN the record, reached by the
   requisition's own tabs, not a destination.
   Instant commit model: posting and unpublishing apply immediately with an undo
   toast; editing the wording opens a staged dialog, because it is a real piece
   of writing and nobody wants it half-saved.
   Views are the only figure that comes from outside the desk. Every other number
   here counts the applications array by `source`, which is why the channel table
   and the funnel on /insights cannot disagree. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Alert, AlertTitle, AlertDescription, Separator, MenuItem,
  } = window.OriginDesignSystem_7c998b;

  function AdvertScreen({ requisition, advert, data, now, session, onNavigate, onEditAdvert, onPostAdvert, onUnpostAdvert }) {
    const totals = advertTotals(advert, data.applications, now);
    const health = requisitionHealth(data.applications, requisition, now);
    const author = staffById(data.staff, advert.updatedById);
    const unposted = SOURCES.filter((source) => source.value !== "pool" && !advert.postedTo.some((p) => p.channel === source.value));
    const best = totals.rows.filter((row) => row.rate !== null).sort((a, b) => b.rate - a.rate)[0];

    return (
      <div className="rd-page">
        <PageHead path="/requisitions/:id/advert" params={{ id: requisition.id }}
          labels={{ "/requisitions/:id": requisition.title }} title={requisition.title + " — advert"}
          note={requisition.ref + " · " + labelOf(SITES, requisition.site) + " · " + bandLabel(requisition) + " · last edited " + formatDay(advert.updatedAt) + " by " + author.name}
          actions={
            <>
              <Button variant="outline" size="sm" onClick={() => onEditAdvert(requisition, advert)}><Icon name="pencil" size={14} />Edit the wording</Button>
              <Button size="sm" disabled={!unposted.length || requisition.status !== "open"} onClick={() => onPostAdvert(requisition, advert)}>
                <Icon name="megaphone" size={14} />Post to a channel
              </Button>
            </>
          } />

        <div className="rd-card-tabs">
          <ViewTabs value="advert" onSelect={(v) => { if (v === "pipeline") onNavigate("/requisitions/" + requisition.id); }}
            views={[
              { value: "pipeline", label: "Pipeline", count: health.live.length },
              { value: "advert", label: "Advert", count: totals.posted },
            ]} />
        </div>

        {requisition.status !== "open" ? (
          <Alert variant="warning">
            <Icon name="eye-off" size={16} />
            <AlertTitle>Not live anywhere</AlertTitle>
            <AlertDescription>
              {requisition.ref} is {statusValue(REQUISITION_STATUS, requisition.status).label.toLowerCase()}, so the advert cannot be posted.
              {requisition.holdNote ? " " + requisition.holdNote : ""} The wording is kept so it is not written twice.
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="rd-metrics">
          <StatTile label="Views" value={formatCount(totals.views)}
            qualifier={totals.posted ? "Across " + plural(totals.posted, "channel", "channels") + ", since the earliest posting" : "Not posted anywhere"} />
          <StatTile label="Applications from the advert" value={totals.applications}
            qualifier={totals.elsewhere ? totals.elsewhere + " more arrived another way — referral, the pool, an event" : "Every application on this seat came through a posted channel"} />
          <StatTile label="Views to application" value={percent(totals.rate, true)}
            qualifier={totals.views ? totals.applications + " of " + formatCount(totals.views) + " people who read it applied" : "No views yet"} />
          <StatTile label="Best channel" value={best ? best.source.label : "—"}
            qualifier={best ? percent(best.rate, true) + " of its views applied — " + plural(best.applications, "application", "applications") : "Nothing to compare yet"} />
        </div>

        <div className="rd-columns">
          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>The wording</CardTitle>
                <CardDescription>What a candidate reads. The essentials are what the screening step actually filters on.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-stack">
                  <h2 className="rd-advert__headline">{advert.headline}</h2>
                  {advert.body.split("\n\n").map((paragraph, i) => <p className="rd-advert__body" key={i}>{paragraph}</p>)}
                  <Separator />
                  <div className="rd-columns rd-columns--even">
                    <span className="rd-part">
                      <span className="rd-kv__label">Essential</span>
                      <ul className="rd-advert__list">
                        {advert.essentials.map((item) => <li key={item}><Icon name="check" size={13} />{item}</li>)}
                      </ul>
                    </span>
                    <span className="rd-part">
                      <span className="rd-kv__label">Desirable</span>
                      <ul className="rd-advert__list rd-advert__list--soft">
                        {advert.desirables.map((item) => <li key={item}><Icon name="plus" size={13} />{item}</li>)}
                      </ul>
                    </span>
                  </div>
                </div>
              </CardPanel>
              <CardFooter className="rd-card-foot">
                <span className="rd-foot">
                  {plural(advert.essentials.length, "essential", "essentials")} and {plural(advert.desirables.length, "desirable", "desirables")}.
                  Band and target start come from the requisition, so the advert cannot state a different figure.
                </span>
              </CardFooter>
            </Card>
          </div>

          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>Where it is posted</CardTitle>
                <CardDescription>
                  {totals.posted ? plural(totals.posted, "channel", "channels") + ". Applications are counted from the applications on this requisition, by source and after the posting date." : "Not posted anywhere yet."}
                </CardDescription>
              </CardHeader>
              {totals.rows.length ? (
                <div className="rd-card-table">
                  <Table variant="card">
                    <TableHeader>
                      <TableRow>
                        <TableHead>Channel</TableHead>
                        <TableHead className="rd-num">Views</TableHead>
                        <TableHead className="rd-num">Applications</TableHead>
                        <TableHead className="rd-num rd-drop-md">Live</TableHead>
                        <TableHead className="rd-num">Rate</TableHead>
                        <TableHead className="rd-actions-head">Action</TableHead>
                      </TableRow>
                    </TableHeader>
                    <TableBody>
                      {totals.rows.map((row) => (
                        <TableRow key={row.posting.channel}>
                          <TableCell>
                            {row.source.label}
                            <span className="rd-sub">posted {formatDayShort(row.posting.postedAt)} · {plural(row.days, "day", "days")} up</span>
                          </TableCell>
                          <TableCell className="rd-num">{formatCount(row.views)}</TableCell>
                          <TableCell className="rd-num">{row.applications}</TableCell>
                          <TableCell className="rd-num rd-drop-md">{row.live || "—"}</TableCell>
                          <TableCell className="rd-num">{percent(row.rate, true)}</TableCell>
                          <TableCell className="rd-actions">
                            <Button variant="destructive-outline" size="xs" onClick={() => onUnpostAdvert(requisition, advert, row)}>Unpublish</Button>
                          </TableCell>
                        </TableRow>
                      ))}
                    </TableBody>
                  </Table>
                </div>
              ) : (
                <CardPanel>
                  <p className="rd-foot">
                    Nothing is live. {requisition.status === "open"
                      ? "Post it to a channel and applications from that channel are counted from the day it goes up."
                      : "It cannot be posted while the requisition is " + statusValue(REQUISITION_STATUS, requisition.status).label.toLowerCase() + "."}
                  </p>
                </CardPanel>
              )}
              {totals.rows.length ? (
                <CardFooter className="rd-card-foot">
                  <span className="rd-foot">
                    Rate is applications as a share of that channel's views. Views come from the channel itself; everything else on this screen counts the {plural(health.all.length, "application", "applications")} on {requisition.ref}.
                  </span>
                </CardFooter>
              ) : null}
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>What the advert produced</CardTitle>
                <CardDescription>The people behind the numbers, newest first.</CardDescription>
              </CardHeader>
              <CardPanel>
                {totals.applications ? (
                  <div className="rd-list">
                    {totals.rows.reduce((all, row) => all.concat(row.rows), []).sort((a, b) => b.appliedAt - a.appliedAt).slice(0, 6).map((application) => (
                      <div className="rd-list__row" key={application.id}>
                        <div className="rd-list__main">
                          <CandidateCell application={application} onNavigate={onNavigate}
                            meta={labelOf(SOURCES, application.source) + " · applied " + formatDayShort(application.appliedAt)} />
                        </div>
                        <div className="rd-list__side">
                          <StatusBadge values={APPLICATION_STAGE} value={application.stage} />
                        </div>
                      </div>
                    ))}
                  </div>
                ) : <p className="rd-foot">No applications from a posted channel yet.</p>}
              </CardPanel>
              {totals.applications > 6 ? (
                <CardFooter className="rd-card-foot">
                  <span className="rd-foot">Showing 6 of {totals.applications}.</span>
                  <Button variant="outline" size="sm" onClick={() => onNavigate("/requisitions/" + requisition.id)}>Open the pipeline</Button>
                </CardFooter>
              ) : null}
            </Card>
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { AdvertScreen });
})();

</script>
<script type="text/plain" data-rd-file="InterviewsScreens.jsx">
/* Interviews — the destination that is not a table. The question is "what is
   happening, and when", so the days are the display and the conversations sit
   in them. Instant commit model; feedback is a staged dialog.
   The tabs narrow the list only; the figures above them do not move. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator,
  } = window.OriginDesignSystem_7c998b;

  const KIND_LABEL = { screening: "Screening call", assessment: "Assessment", panel: "Panel interview" };

  const VIEWS = [
    { value: "ahead", label: "Booked", predicate: (rows, now) => rows.filter((iv) => INTERVIEW_FLAGS.isAhead(iv, now)) },
    { value: "feedback", label: "Awaiting feedback", predicate: (rows) => rows.filter(INTERVIEW_FLAGS.feedbackOutstanding) },
    { value: "held", label: "Held", predicate: (rows) => rows.filter((iv) => iv.status === "complete" || iv.status === "no_show") },
  ];

  function Slot({ interview, data, now, onNavigate, onInterviewTransition }) {
    const application = applicationById(data.applications, interview.applicationId);
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const panel = panelOf(data.staff, interview);
    const recessed = interview.status === "complete" || interview.status === "cancelled";
    return (
      <div className="rd-slot">
        <span className="rd-slot__time">
          {formatTime(interview.at)}
          <span className="rd-sub">{interview.minutes} min</span>
        </span>
        <span className="rd-slot__body">
          <CandidateCell application={application} onNavigate={onNavigate} meta={KIND_LABEL[interview.kind] + " · " + requisition.title} />
          <span className="rd-foot">{interview.where} · {panel.map((p) => p.name).join(", ")}</span>
          {interview.note ? <p className="rd-quote">{interview.note}</p> : null}
        </span>
        <span className="rd-slot__side">
          <span className="rd-faces" aria-hidden="true">
            {panel.map((person) => <Avatar key={person.id} size={24} src={person.photo} alt="" fallback={person.initials} />)}
          </span>
          <StatusBadge values={INTERVIEW_STATUS} value={interview.status} />
          {recessed ? null : <RowActions transitions={interviewTransitions(interview)} onRun={(t) => onInterviewTransition(interview, t)} label={"Actions for the " + interview.kind + " with " + application.candidate.name} />}
        </span>
      </div>
    );
  }

  function InterviewsScreen({ data, now, session, onNavigate, onInterviewTransition, onBookInterview }) {
    const [view, setView] = React.useState("ahead");
    const me = staffById(data.staff, session.personId);
    const outstanding = feedbackOutstanding(data.interviews);
    const today = interviewsToday(data.interviews, now);
    const ahead = interviewsAhead(data.interviews, now);
    const views = VIEWS.map((v) => ({ value: v.value, label: v.label, count: v.predicate(data.interviews, now).length }));
    const rows = VIEWS.find((v) => v.value === view).predicate(data.interviews, now);
    const days = interviewDays(rows, now);

    return (
      <div className="rd-page">
        <PageHead path="/interviews" title="Interviews"
          note={plural(today.length, "conversation", "conversations") + " today, " + plural(ahead.length, "booked", "booked") + " in all. " + (outstanding.length ? plural(outstanding.length, "panel", "panels") + " owe us a scorecard." : "Every panel has reported.")}
          actions={<Button size="sm" onClick={onBookInterview}><Icon name="calendar-plus" size={14} />Book an interview</Button>} />

        {outstanding.length ? (
          <Alert variant="warning">
            <Icon name="clipboard-pen" size={16} />
            <AlertTitle>{plural(outstanding.length, "scorecard", "scorecards")} outstanding</AlertTitle>
            <AlertDescription>
              {outstanding.map((iv) => applicationById(data.applications, iv.applicationId).candidate.name + " (" + formatDayShort(iv.at) + ")").join(" · ")}.
              A panel decision needs {RD_POLICY.panelScorecards} cards from different people, so nothing after the panel can move until they are in.
            </AlertDescription>
          </Alert>
        ) : null}

        <Card>
          <div className="rd-card-tabs"><ViewTabs views={views} value={view} onSelect={setView} /></div>
          <CardPanel>
            {days.length ? (
              <div className="rd-stack">
                {days.map((day) => (
                  <div className="rd-day" key={day.key}>
                    <div className="rd-day__head">
                      <span className="rd-day__label">{dayLabel(day.at, now)}{dayLabel(day.at, now).indexOf("day") === -1 ? "" : " · " + formatDay(day.at)}</span>
                      <span className="rd-day__count">{plural(day.items.length, "conversation", "conversations")}</span>
                    </div>
                    {day.items.map((interview) => (
                      <Slot key={interview.id} interview={interview} data={data} now={now} onNavigate={onNavigate} onInterviewTransition={onInterviewTransition} />
                    ))}
                  </div>
                ))}
              </div>
            ) : (
              <p className="rd-foot">Nothing in this view. {view === "feedback" ? "Every panel has filed its scorecard." : "Book one from an application at screening, assessment or panel."}</p>
            )}
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">
              {view === "ahead" ? "Booked conversations from " + formatDay(now) + " onwards, earliest first." : view === "feedback" ? "Held, waiting on a scorecard." : "Everything that has already happened, including the one no-show."}
            </span>
            <span className="rd-foot">{plural(rows.length, "conversation", "conversations")} across {plural(days.length, "day", "days")}.</span>
          </CardFooter>
        </Card>
      </div>
    );
  }

  /* ── Talent pool: faces first, because the question is "who do we know". ── */
  function PoolScreen({ data, now, session, onNavigate, onApplicationTransition }) {
    const [site, setSite] = React.useState("all");
    const [query, setQuery] = React.useState("");
    const pool = talentPool(data.applications);
    const dirty = !!query || site !== "all";
    const clear = () => { setQuery(""); setSite("all"); };
    const rows = pool.filter((application) => {
      const requisition = requisitionById(data.requisitions, application.requisitionId);
      if (site !== "all" && requisition.site !== site) return false;
      if (!query) return true;
      return (application.candidate.name + " " + application.candidate.headline).toLowerCase().indexOf(query.trim().toLowerCase()) > -1;
    });

    return (
      <div className="rd-page">
        <PageHead path="/pool" title="Talent pool"
          note={"The " + pool.length + " people we declined or who withdrew and said we would talk again. It is a predicate over the applications, not a separate list — reopening somebody here moves their own record."} />

        <FilterBar query={query} onQuery={setQuery} placeholder="Search a name or a headline…" dirty={dirty} onClear={clear}
          selects={[{ id: "site", label: "Site", value: site, onChange: setSite,
            items: [{ value: "all", label: "Every site" }].concat(SITES.map((s) => ({ value: s.value, label: s.label }))) }]} />

        {rows.length ? (
          <div className="rd-grid">
            {rows.map((application) => {
              const requisition = requisitionById(data.requisitions, application.requisitionId);
              const reached = statusValue(APPLICATION_STAGE, application.lastLiveStage || "applied");
              return (
                <Card key={application.id}>
                  <div className="rd-poolcard">
                    <div className="rd-poolcard__head">
                      <Avatar size={48} src={application.candidate.photo} alt="" fallback={application.candidate.initials} />
                      <span className="rd-record">
                        <button type="button" className="rd-link rd-record__name" onClick={() => onNavigate("/applications/" + application.id)}>{application.candidate.name}</button>
                        <span className="rd-record__meta">{application.candidate.headline}</span>
                      </span>
                    </div>
                    <span className="rd-status">
                      <StatusBadge values={APPLICATION_STAGE} value={application.stage} />
                      <Badge size="sm" variant="secondary">Reached {reached.label.toLowerCase()}</Badge>
                    </span>
                    <p className="rd-quote">{application.declineNote || application.withdrawNote}</p>
                    <div className="rd-poolcard__foot">
                      <span className="rd-foot">
                        {requisition.title} · {application.stage === "declined" ? "declined " : "withdrew "}{formatDayShort(application.decidedAt)} · {labelOf(SITES, requisition.site)}
                      </span>
                      <Transitions transitions={transitionsFor(application)} onRun={(t) => onApplicationTransition(application, t)} />
                    </div>
                  </div>
                </Card>
              );
            })}
          </div>
        ) : (
          <Card><NoResults title="Nobody in the pool matches that" description={"Clearing the filters brings back " + plural(pool.length, "person", "people") + "."} onClear={clear} /></Card>
        )}
      </div>
    );
  }

  Object.assign(window, { InterviewsScreen, PoolScreen });
})();

</script>
<script type="text/plain" data-rd-file="OffersComplianceScreens.jsx">
/* Offers, and compliance. Two screens over the same applications array, both
   Instant: every control here confirms itself, consequential ones raise an undo
   toast, and nothing on either screen is destructive.

   Offers exists because an offer is the only record in the desk with a hard
   deadline nobody on the desk controls. Compliance exists because a start date
   is the thing that actually slips, and it slips on three checks. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, MenuItem, Progress,
  } = window.OriginDesignSystem_7c998b;

  /* ── /offers ───────────────────────────────────────────────────────────── */
  const OFFER_VIEWS = [
    { value: "waiting", label: "With the candidate", predicate: (rows) => rows.filter((r) => !r.answered) },
    { value: "answered", label: "Answered", predicate: (rows) => rows.filter((r) => r.answered) },
    { value: "all", label: "Everything", predicate: (rows) => rows },
  ];

  function OffersScreen({ data, now, session, onNavigate, onApplicationTransition, onChaseOffer, onExtendOffer }) {
    const [view, setView] = React.useState("waiting");
    const all = offersOut(data.applications).map((a) => offerRow(a, data.requisitions, now));
    const waiting = all.filter((r) => !r.answered);
    const expiring = offersExpiringSoon(data.applications, now);
    const outcome = offerOutcome(data.applications);
    const views = OFFER_VIEWS.map((v) => ({ value: v.value, label: v.label, count: v.predicate(all).length }));
    const rows = OFFER_VIEWS.find((v) => v.value === view).predicate(all);

    return (
      <div className="rd-page">
        <PageHead path="/offers" title="Offers"
          note={plural(waiting.length, "offer", "offers") + " out with candidates and "
            + plural(all.length - waiting.length, "answered", "answered") + ". Every one of them has a date on it that we do not control, which is why they live on their own screen."} />

        {expiring.length ? (
          <Alert variant="warning">
            <Icon name="clock-alert" size={16} />
            <AlertTitle>{plural(expiring.length, "offer", "offers")} inside {RD_POLICY.offerExpiryWarnDays} days of running out</AlertTitle>
            <AlertDescription>
              {expiring.map((a) => a.candidate.name + " — " + durationWords(daysBetween(now, a.offer.expiresAt))).join(" · ")}.
              A call is worth more than an email at this point; extending is the desk's decision, not the candidate's.
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="rd-metrics">
          <StatTile label="Out with candidates" value={waiting.length}
            qualifier={waiting.length ? "Soonest runs out " + formatDay(waiting[0].offer.expiresAt) : "Nothing waiting on an answer"} />
          <StatTile label="Accepted" value={outcome.accepted.length + " of " + outcome.sent.length}
            qualifier={outcome.sent.length ? percent(outcome.rate) + " of every offer this desk has sent" : "No offer has gone out yet"} />
          <StatTile label="Committed salary" value={money(sumBy(all.filter((r) => r.answered), (r) => r.offer.salaryMinor))}
            qualifier={"Annual, over the " + plural(all.filter((r) => r.answered).length, "accepted offer", "accepted offers") + " — offers still out are not counted"} />
          <StatTile label="Starting inside 30 days" value={all.filter((r) => r.answered && r.daysToStart <= 30).length}
            qualifier="Accepted offers with a start date in the next month" />
        </div>

        <Card>
          <div className="rd-card-tabs"><ViewTabs views={views} value={view} onSelect={setView} /></div>
          {rows.length === 0 ? (
            <NoResults title="No offer in this view"
              description={view === "waiting" ? "Nothing is waiting on a candidate right now." : "Nothing has been answered yet."}
              action={<Button onClick={() => setView("all")}>Show everything</Button>} />
          ) : (
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead>Candidate</TableHead>
                    <TableHead className="rd-drop-md">Seat</TableHead>
                    <TableHead className="rd-num">Salary</TableHead>
                    <TableHead className="rd-drop-lg" style={{ minWidth: "9rem" }}>In the band</TableHead>
                    <TableHead>Runs out</TableHead>
                    <TableHead className="rd-drop-md">Starts</TableHead>
                    <TableHead className="rd-actions-head">Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((row) => (
                    <TableRow key={row.application.id} className={row.answered ? "rd-row--recessed" : undefined}>
                      <TableCell>
                        <CandidateCell application={row.application} onNavigate={onNavigate}
                          meta={row.metExpectation ? row.application.candidate.headline : "Asked for " + money(row.application.salaryExpectationMinor)} />
                      </TableCell>
                      <TableCell className="rd-drop-md">
                        <button type="button" className="rd-link" onClick={() => onNavigate("/requisitions/" + row.requisition.id)}>{row.requisition.title}</button>
                        <span className="rd-sub">{row.requisition.ref}</span>
                      </TableCell>
                      <TableCell className="rd-num">{money(row.offer.salaryMinor)}</TableCell>
                      <TableCell className="rd-drop-lg">
                        <Bar ratio={row.position} />
                        <span className="rd-sub">{percent(row.position)} up {bandLabel(row.requisition)}</span>
                      </TableCell>
                      <TableCell>
                        {row.answered
                          ? <span className="rd-status"><Badge size="sm" variant="success">Accepted</Badge></span>
                          : (
                            <span className={"rd-clock" + (row.daysLeft <= RD_POLICY.offerExpiryWarnDays ? " rd-clock--warning" : "")} title={formatAbsolute(row.offer.expiresAt)}>
                              {durationWords(row.daysLeft)}
                              <span className="rd-sub">{formatDay(row.offer.expiresAt)}</span>
                            </span>
                          )}
                      </TableCell>
                      <TableCell className="rd-drop-md">
                        {formatDayShort(row.offer.startAt)}
                        <span className="rd-sub">{durationWords(row.daysToStart)}</span>
                      </TableCell>
                      <TableCell className="rd-actions">
                        <RowActions transitions={transitionsFor(row.application)} onRun={(t) => onApplicationTransition(row.application, t)}
                          label={"Actions for " + row.application.candidate.name}
                          extra={row.answered ? null : (close) => (
                            <>
                              <MenuItem onClick={() => { close(); onChaseOffer(row.application); }}>Chase the candidate</MenuItem>
                              <MenuItem onClick={() => { close(); onExtendOffer(row.application); }}>Extend the offer</MenuItem>
                            </>
                          )} />
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </div>
          )}
          {rows.length ? (
            <CardFooter className="rd-card-foot">
              <span className="rd-foot">
                {plural(rows.length, "offer", "offers")}, soonest to run out first. Committed salary counts accepted offers only, so this table and the tile above it agree.
              </span>
            </CardFooter>
          ) : null}
        </Card>
      </div>
    );
  }

  /* ── /compliance ───────────────────────────────────────────────────────── */
  function ComplianceScreen({ data, now, session, onNavigate, onVerifyRightToWork, onSubmitDbs, onChaseRegistration }) {
    const [only, setOnly] = React.useState("blockers");
    const rows = complianceRows(data.applications, data.requisitions, now);
    const urgent = rows.filter((r) => r.urgent);
    const clear = rows.filter((r) => r.clear);
    const shown = only === "blockers" ? rows.filter((r) => r.blockers.length) : only === "urgent" ? urgent : rows;
    const views = [
      { value: "blockers", label: "Anything outstanding", count: rows.filter((r) => r.blockers.length).length },
      { value: "urgent", label: "Close to starting", count: urgent.length },
      { value: "all", label: "Every live application", count: rows.length },
    ];

    return (
      <div className="rd-page">
        <PageHead path="/compliance" title="Compliance"
          note={"Right to work, DBS and professional registration across the " + plural(rows.length, "live application", "live applications")
            + ". Nobody starts without all three, so this is the screen a start date actually depends on."} />

        {urgent.length ? (
          <Alert variant="warning">
            <Icon name="shield-alert" size={16} />
            <AlertTitle>{plural(urgent.length, "person", "people")} at checks or beyond with something outstanding</AlertTitle>
            <AlertDescription>
              {urgent.map((row) => row.application.candidate.name + " — " + row.blockers.join(", ")).join(" · ")}.
              Urgency is the stage, not the count: the same gap at applied stops nothing yet.
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="rd-metrics">
          <StatTile label="Nothing outstanding" value={clear.length + " of " + rows.length}
            qualifier="Right to work verified, DBS clear, and the seat's register in date where it asks for one" />
          <StatTile label="Right to work outstanding" value={rows.filter((r) => COMPLIANCE_FLAGS.rtwOutstanding(r.application)).length}
            qualifier={plural(rows.filter((r) => COMPLIANCE_FLAGS.needsSponsorship(r.application)).length, "of them needs", "of them need") + " sponsorship"} />
          <StatTile label="DBS not clear" value={rows.filter((r) => COMPLIANCE_FLAGS.dbsOutstanding(r.application)).length}
            qualifier={rows.filter((r) => r.application.dbs === "submitted").length
              ? rows.filter((r) => r.application.dbs === "submitted").length + " with the service, the rest not started"
              : "None with the service yet — the rest are not started"} />
          <StatTile label="Registration to chase" value={rows.filter((r) => COMPLIANCE_FLAGS.registrationOutstanding(r.application, r.requisition, now)).length}
            qualifier={"Renewing inside " + RD_POLICY.registrationWarnDays + " days, already lapsed, or none on file where the seat asks for one"} />
        </div>

        <Card>
          <div className="rd-card-tabs"><ViewTabs views={views} value={only} onSelect={setOnly} /></div>
          {shown.length === 0 ? (
            <NoResults title="Nothing outstanding" description="Every live application has its right to work verified, a clear DBS, and the register its seat asks for in date."
              action={<Button onClick={() => setOnly("all")}>Show every live application</Button>} />
          ) : (
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead>Candidate</TableHead>
                    <TableHead className="rd-drop-md">Stage</TableHead>
                    <TableHead>Right to work</TableHead>
                    <TableHead>DBS</TableHead>
                    <TableHead>Registration</TableHead>
                    <TableHead className="rd-actions-head">Action</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {shown.map((row) => {
                    const a = row.application;
                    const expiring = COMPLIANCE_FLAGS.registrationExpiring(a, now);
                    const expired = COMPLIANCE_FLAGS.registrationExpired(a, now);
                    return (
                      <TableRow key={a.id} className={row.clear ? "rd-row--recessed" : undefined}>
                        <TableCell>
                          <CandidateCell application={a} onNavigate={onNavigate}
                            meta={requisitionById(data.requisitions, a.requisitionId).title} />
                        </TableCell>
                        <TableCell className="rd-drop-md">
                          <span className="rd-status">
                            <StatusBadge values={APPLICATION_STAGE} value={a.stage} />
                            {row.urgent ? <Badge size="sm" variant="warning">Blocks a start date</Badge> : null}
                          </span>
                        </TableCell>
                        <TableCell><StatusBadge values={RIGHT_TO_WORK} value={a.rightToWork} /></TableCell>
                        <TableCell>
                          <StatusBadge values={DBS_STATUS} value={a.dbs} />
                          {a.dbsAt ? <span className="rd-sub">{formatDayShort(a.dbsAt)}</span> : null}
                        </TableCell>
                        <TableCell>
                          {a.registrationExpiresAt ? (
                            <>
                              <span className={"rd-clock" + (expired ? " rd-clock--danger" : expiring ? " rd-clock--warning" : "")} title={formatAbsolute(a.registrationExpiresAt)}>
                                {a.registration}
                              </span>
                              <span className="rd-sub">{expired ? "lapsed " + formatDayShort(a.registrationExpiresAt) : "renews " + formatDayShort(a.registrationExpiresAt) + " · " + durationWords(row.registrationDays)}</span>
                            </>
                          ) : row.registerNeeded
                            ? <span className="rd-clock rd-clock--warning">No {row.registerNeeded} number<span className="rd-sub">This seat asks for one</span></span>
                            : <span className="rd-muted">Not required<span className="rd-sub">This seat asks for no register</span></span>}
                        </TableCell>
                        <TableCell className="rd-actions">
                          <span className="rd-actions__row">
                            {COMPLIANCE_FLAGS.rtwOutstanding(a)
                              ? <Button size="xs" onClick={() => onVerifyRightToWork(a)}>Verify right to work</Button>
                              : COMPLIANCE_FLAGS.dbsOutstanding(a) && a.dbs === "not_started"
                                ? <Button size="xs" variant="outline" onClick={() => onSubmitDbs(a)}>Send to DBS</Button>
                                : expiring || expired
                                  ? <Button size="xs" variant="outline" onClick={() => onChaseRegistration(a)}>Ask for the renewal</Button>
                                  : <span className="rd-muted">—</span>}
                          </span>
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">
              {only === "all" ? "Every live application, worst first. Rows with nothing outstanding are recessed." : plural(shown.length, "application", "applications") + " of the " + rows.length + " live ones."}
              {" "}Settled applications are not here: nothing is checked on somebody who is not being hired.
            </span>
            <ExportMenu scope={plural(shown.length, "row", "rows") + " as filtered"} onExport={() => {}} />
          </CardFooter>
        </Card>
      </div>
    );
  }

  Object.assign(window, { OffersScreen, ComplianceScreen });
})();

</script>
<script type="text/plain" data-rd-file="InsightsScreen.jsx">
/* Insights — the desk read sideways. Chart-led, not a table: the funnel is the
   display, and every figure on the screen derives from the same scoped array.
   The scope control sits ABOVE the figures and moves them (AGENTS §5.3); there
   is no list on this screen to narrow separately. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Separator, Alert, AlertTitle, AlertDescription,
  } = window.OriginDesignSystem_7c998b;

  function InsightsScreen({ data, now, onNavigate }) {
    const [requisition, setRequisition] = React.useState("all");
    const scoped = requisition === "all" ? data.applications : applicationsFor(data.applications, requisition);
    const chosen = requisition === "all" ? null : requisitionById(data.requisitions, requisition);
    const shape = funnel(scoped);
    const offers = offerOutcome(scoped);
    const speed = timeToOffer(scoped);
    const sources = sourceMix(scoped);
    const oldest = scoped.length ? Math.min.apply(null, scoped.map((a) => a.appliedAt)) : now;

    return (
      <div className="rd-page">
        <PageHead path="/insights" title="Insights"
          note={"Every figure here counts the same " + plural(scoped.length, "application", "applications") + " — live and settled, applied since " + formatDay(oldest) + "."} />

        <ScopeBar
          controls={[{ id: "req", label: "Requisition", value: requisition, onChange: setRequisition,
            items: [{ value: "all", label: "Every requisition" }].concat(data.requisitions.map((r) => ({ value: r.id, label: r.ref + " · " + r.title }))) }]}
          dirty={requisition !== "all"} onClear={() => setRequisition("all")}
          note={chosen
            ? "Scoped to " + chosen.ref + " · " + chosen.title + " — " + plural(scoped.length, "application", "applications") + " ever raised against it, " + plural(liveApplications(scoped).length, "still live", "still live") + "."
            : "Nothing is filtered out: all " + data.applications.length + " applications on the desk, across " + plural(data.requisitions.length, "requisition", "requisitions") + "."} />

        <div className="rd-metrics">
          <StatTile label="Applications" value={scoped.length}
            qualifier={liveApplications(scoped).length + " live, " + (scoped.length - liveApplications(scoped).length) + " settled"}
            onDrill={() => onNavigate("/applications")} drillLabel="Show the applications" />
          <StatTile label="Offers accepted" value={offers.accepted.length + " of " + offers.sent.length}
            qualifier={offers.sent.length ? percent(offers.rate) + " of the offers we sent" : "No offer has gone out yet"} />
          <StatTile label="Days to offer, median" value={speed.median === null ? "—" : speed.median}
            qualifier={speed.count ? "Over the " + plural(speed.count, "application", "applications") + " that reached an offer — fastest " + speed.fastest + ", slowest " + speed.slowest : "Nothing has reached an offer"} />
          <StatTile label="Waiting on the desk" value={awaitingUs(scoped).length}
            qualifier={"Of " + plural(liveApplications(scoped).length, "live application", "live applications") + " — the rest sit with referees or candidates"}
            onDrill={() => onNavigate("/applications")} drillLabel="Show what is waiting on the desk" />
        </div>

        <div className="rd-columns">
          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>The funnel</CardTitle>
                <CardDescription>How many applications reached each step, including the ones that later stopped. A step counts everybody who got that far, so the numbers only fall.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-stack">
                  {shape.rows.map((row) => (
                    <span className="rd-part" key={row.stage}>
                      <span className="rd-spread">
                        <span className="rd-record__name">{row.label}</span>
                        <span className="rd-kv__value">{row.count}{row.fromPrevious === null ? "" : " · " + percent(row.fromPrevious) + " of the step before"}</span>
                      </span>
                      <Bar ratio={shape.total ? row.count / shape.total : 0} tone={row.stage === "hired" ? null : "soft"} />
                    </span>
                  ))}
                </div>
              </CardPanel>
              <CardFooter className="rd-card-foot">
                <span className="rd-foot">
                  {shape.rows[0].count} applied · {shape.rows[shape.rows.length - 1].count} hired.
                  The biggest drop is between {biggestDrop(shape).from} and {biggestDrop(shape).to}, where {biggestDrop(shape).lost} stopped.
                </span>
              </CardFooter>
            </Card>
          </div>

          <div className="rd-col">
            <Card>
              <CardHeader>
                <CardTitle>Where they come from</CardTitle>
                <CardDescription>{plural(sources.length, "source", "sources")} with at least one application in scope.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-card-table">
                  <Table variant="card">
                    <TableHeader>
                      <TableRow>
                        <TableHead>Source</TableHead>
                        <TableHead className="rd-num">Applications</TableHead>
                        <TableHead className="rd-num">Share</TableHead>
                        <TableHead className="rd-num">Reached an offer</TableHead>
                      </TableRow>
                    </TableHeader>
                    <TableBody>
                      {sources.map((row) => (
                        <TableRow key={row.source.value}>
                          <TableCell>{row.source.label}</TableCell>
                          <TableCell className="rd-num">{row.count}</TableCell>
                          <TableCell className="rd-num">{percent(row.share)}</TableCell>
                          <TableCell className="rd-num">{row.offered}</TableCell>
                        </TableRow>
                      ))}
                    </TableBody>
                  </Table>
                </div>
              </CardPanel>
              <CardFooter className="rd-card-foot">
                <span className="rd-foot">Shares are of the {scoped.length} applications in scope, so they add to 100%.</span>
              </CardFooter>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Offers, one by one</CardTitle>
                <CardDescription>{plural(offers.sent.length, "offer", "offers")} sent in scope. Small numbers, so they are listed rather than averaged.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="rd-list">
                  {offers.sent.map((application) => (
                    <div className="rd-list__row" key={application.id}>
                      <div className="rd-list__main">
                        <CandidateCell application={application} onNavigate={onNavigate} meta={requisitionById(data.requisitions, application.requisitionId).title} />
                      </div>
                      <div className="rd-list__side">
                        <span className="rd-kv__value">{money(application.offer.salaryMinor)}</span>
                        <span className="rd-foot">
                          {application.offer.acceptedAt ? "Accepted " + formatDayShort(application.offer.acceptedAt) : "Out since " + formatDayShort(application.offer.sentAt)}
                          {" · " + plural(daysBetween(application.appliedAt, application.offer.sentAt), "day", "days") + " from applying"}
                        </span>
                      </div>
                    </div>
                  ))}
                </div>
              </CardPanel>
            </Card>
          </div>
        </div>
      </div>
    );
  }

  /* The one derived claim the footer makes, computed rather than asserted. */
  function biggestDrop(shape) {
    let worst = { from: shape.rows[0].label, to: shape.rows[1] ? shape.rows[1].label : shape.rows[0].label, lost: 0 };
    for (let i = 1; i < shape.rows.length; i++) {
      const lost = shape.rows[i - 1].count - shape.rows[i].count;
      if (lost > worst.lost) worst = { from: shape.rows[i - 1].label.toLowerCase(), to: shape.rows[i].label.toLowerCase(), lost };
    }
    return worst;
  }

  Object.assign(window, { InsightsScreen });
})();

</script>
<script type="text/plain" data-rd-file="NotificationsScreen.jsx">
/* The notifications inbox — the same array the bell's count reads, laid out
   properly. Instant commit model: reading, unreading and marking the lot apply
   immediately; marking all read raises an undo toast because it is the one that
   throws information away.
   Unread is the absence of `readAt`, so the count on the bell and the count on
   the tab cannot disagree. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, MenuItem,
  } = window.OriginDesignSystem_7c998b;

  function NotificationsScreen({ data, now, session, onNavigate, onReadNotification, onUnreadNotification, onReadAll }) {
    const [view, setView] = React.useState("unread");
    const all = data.notifications;
    const unread = unreadNotifications(all);
    const views = [
      { value: "unread", label: "Unread", count: unread.length },
      { value: "mentions", label: "Mentions", count: notificationsOfKind(all, "mention").length },
      { value: "all", label: "Everything", count: all.length },
    ];
    const rows = view === "unread" ? unread : view === "mentions" ? notificationsOfKind(all, "mention") : all;
    const days = notificationDays(rows, now);

    return (
      <div className="rd-page">
        <PageHead path="/notifications" title="Notifications"
          note={unread.length
            ? plural(unread.length, "unread notification", "unread notifications") + " of " + all.length + ". Everything here points at a record — nothing is a summary of something you cannot open."
            : "Nothing unread. All " + all.length + " are kept so you can find what happened last week."}
          actions={<Button variant="outline" size="sm" disabled={!unread.length} onClick={onReadAll}>Mark all read</Button>} />

        {/* One tile per kind that has something in it — the enum read whole and
           filtered, never its first four. */}
        <div className="rd-metrics">
          {NOTIFICATION_KINDS.map((kind) => ({ kind, ofKind: notificationsOfKind(all, kind.value) }))
            .filter((row) => row.ofKind.length > 0)
            .map(({ kind, ofKind }) => {
            const unreadOfKind = ofKind.filter((n) => !n.readAt).length;
            return (
              <StatTile key={kind.value} label={kind.label} value={ofKind.length}
                qualifier={unreadOfKind ? plural(unreadOfKind, "unread", "unread") : "All read"} />
            );
          })}
        </div>

        <Card>
          <div className="rd-card-tabs"><ViewTabs views={views} value={view} onSelect={setView} /></div>
          <CardPanel>
            {days.length ? (
              <div className="rd-stack">
                {days.map((day) => (
                  <div className="rd-day" key={day.key}>
                    <div className="rd-day__head">
                      <span className="rd-day__label">{dayLabel(day.at, now)}</span>
                      <span className="rd-day__count">{plural(day.items.length, "notification", "notifications")}</span>
                    </div>
                    {day.items.map((notification) => {
                      const kind = notificationKind(notification);
                      const actor = notification.actorId ? staffById(data.staff, notification.actorId) : null;
                      return (
                        <div className={"rd-notif" + (notification.readAt ? " rd-notif--read" : "")} key={notification.id}>
                          <span className="rd-notif__mark" aria-hidden="true">
                            {actor
                              ? <Avatar size={32} src={actor.photo} alt="" fallback={actor.initials} />
                              : <span className="rd-notif__icon"><Icon name={kind.icon} size={15} /></span>}
                          </span>
                          <span className="rd-notif__body">
                            <span className="rd-notif__head">
                              <button type="button" className="rd-link rd-notif__title"
                                onClick={() => { onReadNotification(notification); onNavigate(notification.route); }}>{notification.title}</button>
                              {notification.readAt ? null : <span className="rd-notif__dot" aria-label="Unread" />}
                            </span>
                            <span className="rd-notif__text">{notification.body}</span>
                            <span className="rd-notif__meta">
                              {kind.label}
                              {actor ? " · " + actor.name : ""}
                              {" · "}
                              <span title={formatAbsolute(notification.at)}>{formatWhen(notification.at, now)}</span>
                              {notification.readAt ? " · read " + formatWhen(notification.readAt, now) : ""}
                            </span>
                          </span>
                          <span className="rd-notif__side">
                            <Button variant="ghost" size="xs"
                              onClick={() => (notification.readAt ? onUnreadNotification(notification) : onReadNotification(notification))}>
                              {notification.readAt ? "Mark unread" : "Mark read"}
                            </Button>
                          </span>
                        </div>
                      );
                    })}
                  </div>
                ))}
              </div>
            ) : (
              <p className="rd-foot">
                {view === "unread" ? "Nothing unread. " : view === "mentions" ? "Nobody has mentioned you. " : ""}
                {view === "unread" ? <button type="button" className="rd-link" onClick={() => setView("all")}>Show all {all.length}</button> : "Everything you are sent lands here."}
              </p>
            )}
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">
              What you are notified about is set on <button type="button" className="rd-link" onClick={() => onNavigate("/me/profile")}>your profile</button>.
              The desk's own defaults are in Settings.
            </span>
            <span className="rd-foot">{plural(rows.length, "notification", "notifications")} in this view.</span>
          </CardFooter>
        </Card>
      </div>
    );
  }

  Object.assign(window, { NotificationsScreen });
})();

</script>
<script type="text/plain" data-rd-file="SettingsScreens.jsx">
/* Settings — four registered screens under one namespace, reached from the
   account menu and from each other by the sub-nav, which is the route table
   filtered by parent.

   Commit model, the same on all four: STAGED. Every control is a form field,
   nothing applies until Save, Save and Discard are disabled until dirty. That is
   the opposite of the rest of the product (Instant everywhere else) and it is
   deliberate: these values change what every other screen counts, so nobody
   should change one by brushing a switch.

   The process page edits RD_POLICY itself — the object metrics.js reads. Saving
   it moves the queue, the stepper's service levels, the sidebar badges and the
   compliance screen at once, because there is only one copy of those numbers. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, Switch, Select, Checkbox,
    Field, FieldLabel, FieldDescription, FieldError, Input, Textarea,
  } = window.OriginDesignSystem_7c998b;

  function SettingsFrame({ path, title, note, onNavigate, children, actions }) {
    return (
      <div className="rd-page">
        <PageHead path={path} title={title} note={note} actions={actions} />
        <div className="rd-card-tabs">
          <ViewTabs value={path} onSelect={(next) => { if (next !== path) onNavigate(next); }}
            views={settingsPages().map((route) => ({ value: route.path, label: route.label }))} />
        </div>
        {children}
      </div>
    );
  }

  /* ── /settings/desk ────────────────────────────────────────────────────── */
  function DeskSettingsScreen({ data, now, session, onNavigate, desk, onSaveDesk }) {
    const [draft, setDraft] = React.useState(desk);
    React.useEffect(() => setDraft(desk), [desk]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(desk);
    const set = (key, value) => setDraft((d) => Object.assign({}, d, { [key]: value }));
    const errors = {};
    if (!draft.name.trim()) errors.name = "The organisation's name appears on every advert and every email.";
    if (!draft.replyTo.trim() || draft.replyTo.indexOf("@") === -1) errors.replyTo = "A real address — candidates reply to it.";
    const valid = Object.keys(errors).length === 0;

    return (
      <SettingsFrame path="/settings/desk" title="The desk" onNavigate={onNavigate}
        note="Who this desk belongs to, and what candidates see when it writes to them.">
        <div className="rd-columns">
          <Card>
            <CardHeader>
              <CardTitle>Organisation</CardTitle>
              <CardDescription>Used on adverts, offer letters and every email the desk sends.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="st-name">Name</FieldLabel>
                  <Input id="st-name" value={draft.name} invalid={!!errors.name} onChange={(e) => set("name", e.target.value)} />
                  {errors.name ? <FieldError>{errors.name}</FieldError> : null}
                </Field>
                <Field>
                  <FieldLabel htmlFor="st-reply">Reply-to address</FieldLabel>
                  <Input id="st-reply" type="email" value={draft.replyTo} invalid={!!errors.replyTo} onChange={(e) => set("replyTo", e.target.value)} />
                  {errors.replyTo ? <FieldError>{errors.replyTo}</FieldError> : <FieldDescription>Where a candidate's reply goes. It is not a person's inbox on purpose.</FieldDescription>}
                </Field>
                <Field>
                  <FieldLabel htmlFor="st-sign">How the desk signs off</FieldLabel>
                  <Textarea id="st-sign" rows={3} value={draft.signature} onChange={(e) => set("signature", e.target.value)} />
                  <FieldDescription>Goes under every message sent from an application.</FieldDescription>
                </Field>
              </div>
            </CardPanel>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>Sites and departments</CardTitle>
              <CardDescription>The lists every requisition and every application chooses from.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack">
                <span className="rd-part">
                  <span className="rd-kv__label">Sites</span>
                  <div className="rd-list">
                    {SITES.map((site) => {
                      const reqs = data.requisitions.filter((r) => r.site === site.value);
                      return (
                        <div className="rd-list__row" key={site.value}>
                          <div className="rd-list__main">
                            <span className="rd-record__name">{site.label}</span>
                            <span className="rd-record__meta">{plural(reqs.length, "requisition", "requisitions")} · {plural(reqs.filter(REQUISITION_FLAGS.isOpen).length, "open", "open")}</span>
                          </div>
                          <div className="rd-list__side">
                            <span className="rd-foot">{reqs.length ? "In use" : "Not used yet"}</span>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </span>
                <Separator />
                <span className="rd-part">
                  <span className="rd-kv__label">Departments</span>
                  <span className="rd-chips">
                    {DEPARTMENTS.map((department) => (
                      <Badge key={department.value} size="sm" variant="secondary">
                        {department.label} · {data.requisitions.filter((r) => r.department === department.value).length}
                      </Badge>
                    ))}
                  </span>
                  <span className="rd-foot">
                    A site or department in use cannot be removed — {plural(data.requisitions.length, "requisition", "requisitions")} and {plural(data.applications.length, "application", "applications")} point at these.
                    Editing the lists is a data change and belongs with the trust's own establishment records.
                  </span>
                </span>
              </div>
            </CardPanel>
          </Card>
        </div>
        <FormBar dirty={dirty} label="Save the desk" onCancel={() => setDraft(desk)}
          note={dirty ? "Three fields, applied to every advert and message the desk sends." : "Nothing has changed."}
          onSave={() => { if (valid) onSaveDesk(draft); }} />
      </SettingsFrame>
    );
  }

  /* ── /settings/process — the policy every figure reads ─────────────────── */
  function ProcessSettingsScreen({ data, now, session, onNavigate, policy, onSavePolicy }) {
    const [draft, setDraft] = React.useState(() => JSON.parse(JSON.stringify(policy)));
    React.useEffect(() => setDraft(JSON.parse(JSON.stringify(policy))), [policy]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(policy);
    const setDays = (stage, value) => setDraft((d) => Object.assign({}, d, { stageDays: Object.assign({}, d.stageDays, { [stage]: Number(value) }) }));
    const set = (key, value) => setDraft((d) => Object.assign({}, d, { [key]: Number(value) }));
    const numbers = [1, 2, 3, 4, 5, 6, 7, 10, 14, 21, 30, 45, 60, 90];
    const items = (list) => list.map((n) => ({ value: String(n), label: String(n) }));
    /* Live, over the real array, at the draft's numbers: what saving would do. */
    const wouldBeOver = liveApplications(data.applications).filter((a) => {
      const allowed = draft.stageDays[a.stage];
      return allowed && daysInStage(a, now) > allowed;
    });
    const isOver = stalledApplications(data.applications, now);

    return (
      <SettingsFrame path="/settings/process" title="Process and service levels" onNavigate={onNavigate}
        note="How long each step of the process is allowed to take, and what a decision needs before it can be made. These are the numbers every other screen counts against.">

        <Alert variant={dirty && wouldBeOver.length !== isOver.length ? "warning" : "info"}>
          <Icon name={dirty && wouldBeOver.length !== isOver.length ? "triangle-alert" : "info"} size={16} />
          <AlertTitle>
            {dirty && wouldBeOver.length !== isOver.length
              ? plural(wouldBeOver.length, "application", "applications") + " would be over its service level, against " + isOver.length + " today"
              : plural(isOver.length, "application", "applications") + " over its service level at these numbers"}
          </AlertTitle>
          <AlertDescription>
            Run against the {liveApplications(data.applications).length} live applications as they stand, so the effect of a change is visible before it is saved.
            {wouldBeOver.length ? " " + wouldBeOver.slice(0, 4).map((a) => a.candidate.name.split(" ")[0] + " (" + statusValue(APPLICATION_STAGE, a.stage).label.toLowerCase() + ")").join(", ") + (wouldBeOver.length > 4 ? " and " + (wouldBeOver.length - 4) + " more" : "") + "." : ""}
          </AlertDescription>
        </Alert>

        <div className="rd-columns">
          <Card>
            <CardHeader>
              <CardTitle>Service level per step</CardTitle>
              <CardDescription>In days. Each step gets its own, which is why "over its service level" is answered per stage and never with one number for the whole desk.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack rd-stack--tight">
                {APPLICATION_PATH.filter((stage) => draft.stageDays[stage] !== undefined).map((stage) => {
                  const here = applicationsAtStage(liveApplications(data.applications), stage);
                  return (
                    <div className="rd-setting" key={stage}>
                      <span className="rd-setting__text">
                        <span className="rd-setting__label">{statusValue(APPLICATION_STAGE, stage).label}</span>
                        <span className="rd-foot">{STAGE_MEANS[stage]} · {plural(here.length, "application", "applications")} here now</span>
                      </span>
                      <span className="rd-setting__control">
                        <label className="rd-sr" htmlFor={"st-days-" + stage}>{statusValue(APPLICATION_STAGE, stage).label} service level in days</label>
                        <Select id={"st-days-" + stage} size="sm" items={items(numbers.filter((n) => n <= 30))}
                          value={String(draft.stageDays[stage])} onValueChange={(v) => setDays(stage, v)} />
                        <span className="rd-foot">days</span>
                      </span>
                    </div>
                  );
                })}
              </div>
            </CardPanel>
            <CardFooter className="rd-card-foot">
              <span className="rd-foot">Checks is the longest on purpose: referees and the disclosure service are not ours to hurry.</span>
            </CardFooter>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>What a decision needs</CardTitle>
              <CardDescription>The preconditions the primary action on an application checks before it will move.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack rd-stack--tight">
                {[
                  { key: "panelScorecards", label: "Scorecards before checks", note: "From different people. " + data.applications.filter((a) => a.stage === "panel" && scoresOf(a).count < draft.panelScorecards).length + " application(s) at panel would be short.", list: [1, 2, 3, 4] },
                  { key: "referencesRequired", label: "References before an offer", note: "NHS standard is two, one the current employer.", list: [1, 2, 3] },
                  { key: "offerValidDays", label: "An offer is valid for", note: "Days. Extending one is a decision with a reason attached.", list: [3, 5, 7, 10, 14, 21] },
                  { key: "registrationWarnDays", label: "Flag a registration renewing inside", note: "Days. " + complianceRows(data.applications, data.requisitions, now).filter((r) => r.application.registrationExpiresAt && daysBetween(now, r.application.registrationExpiresAt) <= draft.registrationWarnDays).length + " would be flagged on Compliance.", list: [30, 45, 60, 90] },
                  { key: "offerExpiryWarnDays", label: "Badge an offer inside", note: "Days of running out. " + offersAwaitingAnswer(data.applications).filter((a) => daysBetween(now, a.offer.expiresAt) <= draft.offerExpiryWarnDays).length + " would be on the badge.", list: [1, 2, 3, 5, 7] },
                  { key: "thinPipeline", label: "A pipeline is thin under", note: "Live applications per requisition. " + data.requisitions.filter((r) => r.status === "open" && liveApplications(applicationsFor(data.applications, r.id)).length < draft.thinPipeline).length + " open requisition(s) would be flagged.", list: [2, 3, 4, 5, 6] },
                  { key: "agingDays", label: "A requisition is aging after", note: "Days open. " + data.requisitions.filter((r) => r.status === "open" && daysBetween(r.openedAt, now) > draft.agingDays).length + " would be flagged.", list: [30, 45, 60, 90] },
                ].map((row) => (
                  <div className="rd-setting" key={row.key}>
                    <span className="rd-setting__text">
                      <span className="rd-setting__label">{row.label}</span>
                      <span className="rd-foot">{row.note}</span>
                    </span>
                    <span className="rd-setting__control">
                      <label className="rd-sr" htmlFor={"st-" + row.key}>{row.label}</label>
                      <Select id={"st-" + row.key} size="sm" items={items(row.list)} value={String(draft[row.key])} onValueChange={(v) => set(row.key, v)} />
                    </span>
                  </div>
                ))}
              </div>
            </CardPanel>
          </Card>
        </div>

        <FormBar dirty={dirty} label="Save the process" onCancel={() => setDraft(JSON.parse(JSON.stringify(policy)))}
          note={dirty
            ? "There is one copy of these numbers: saving moves the queue, the stepper, the sidebar badges and Compliance at the same moment."
            : "Nothing has changed. Every screen is counting against these numbers now."}
          onSave={() => onSavePolicy(draft)} />
      </SettingsFrame>
    );
  }

  /* ── /settings/channels ────────────────────────────────────────────────── */
  function ChannelSettingsScreen({ data, now, session, onNavigate, channels, onSaveChannels }) {
    const [draft, setDraft] = React.useState(channels);
    React.useEffect(() => setDraft(channels), [channels]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(channels);
    const toggle = (value) => setDraft((d) => Object.assign({}, d, {
      enabled: d.enabled.indexOf(value) > -1 ? d.enabled.filter((v) => v !== value) : d.enabled.concat([value]),
      defaults: d.enabled.indexOf(value) > -1 ? d.defaults.filter((v) => v !== value) : d.defaults,
    }));
    const toggleDefault = (value) => setDraft((d) => Object.assign({}, d, {
      defaults: d.defaults.indexOf(value) > -1 ? d.defaults.filter((v) => v !== value) : d.defaults.concat([value]),
    }));
    /* Usage is counted from the adverts and applications, not stored here. */
    const usage = (value) => ({
      posted: data.adverts.filter((ad) => ad.postedTo.some((p) => p.channel === value)).length,
      applications: data.applications.filter((a) => a.source === value).length,
    });
    const errors = {};
    if (!draft.enabled.length) errors.enabled = "At least one channel, or a requisition cannot be advertised anywhere.";

    return (
      <SettingsFrame path="/settings/channels" title="Adverts and channels" onNavigate={onNavigate}
        note="Where adverts may be posted, what a new advert starts as, and how long a posting stays up before the desk is asked about it.">
        <div className="rd-columns">
          <Card>
            <CardHeader>
              <CardTitle>Channels</CardTitle>
              <CardDescription>A channel a requisition may be posted to. Turning one off leaves existing postings alone.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack rd-stack--tight">
                {SOURCES.filter((source) => source.value !== "pool").map((source) => {
                  const on = draft.enabled.indexOf(source.value) > -1;
                  const used = usage(source.value);
                  return (
                    <div className="rd-setting" key={source.value}>
                      <span className="rd-setting__text">
                        <span className="rd-setting__label">{source.label}</span>
                        <span className="rd-foot">
                          {used.posted ? plural(used.posted, "advert", "adverts") + " posted there" : "Not posted anywhere"}
                          {" · " + plural(used.applications, "application", "applications") + " came from it"}
                        </span>
                      </span>
                      <span className="rd-setting__control">
                        {on ? (
                          <label className="rd-check">
                            <Checkbox checked={draft.defaults.indexOf(source.value) > -1} onCheckedChange={() => toggleDefault(source.value)}
                              aria-label={"Post new adverts to " + source.label + " by default"} />
                            <span className="rd-check__text"><span className="rd-foot">By default</span></span>
                          </label>
                        ) : null}
                        <Switch checked={on} onCheckedChange={() => toggle(source.value)} aria-label={source.label + " available for adverts"} />
                      </span>
                    </div>
                  );
                })}
                {errors.enabled ? <FieldError>{errors.enabled}</FieldError> : null}
              </div>
            </CardPanel>
            <CardFooter className="rd-card-foot">
              <span className="rd-foot">
                The talent pool is not a channel: it is people we already know, and applications from it are created here rather than applied for.
              </span>
            </CardFooter>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>Postings</CardTitle>
              <CardDescription>The two rules the desk keeps about live adverts.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack rd-stack--tight">
                <div className="rd-setting">
                  <span className="rd-setting__text">
                    <span className="rd-setting__label">Ask about a posting after</span>
                    <span className="rd-foot">
                      Days up. {data.adverts.reduce((n, ad) => n + ad.postedTo.filter((p) => daysBetween(p.postedAt, now) > draft.reviewDays).length, 0)} posting(s) are older than that now.
                    </span>
                  </span>
                  <span className="rd-setting__control">
                    <label className="rd-sr" htmlFor="st-review">Ask about a posting after</label>
                    <Select id="st-review" size="sm" value={String(draft.reviewDays)} onValueChange={(v) => setDraft((d) => Object.assign({}, d, { reviewDays: Number(v) }))}
                      items={[14, 21, 28, 42, 60].map((n) => ({ value: String(n), label: n + " days" }))} />
                  </span>
                </div>
                <div className="rd-setting">
                  <span className="rd-setting__text">
                    <span className="rd-setting__label">Take adverts down when the seat is filled</span>
                    <span className="rd-foot">
                      {data.requisitions.filter((r) => r.status === "filled").length} filled requisition(s) — {data.adverts.filter((ad) => ad.postedTo.length && data.requisitions.some((r) => r.id === ad.requisitionId && r.status === "filled")).length} still has a posting on the record.
                    </span>
                  </span>
                  <span className="rd-setting__control">
                    <Switch checked={draft.autoUnpost} onCheckedChange={(v) => setDraft((d) => Object.assign({}, d, { autoUnpost: v }))}
                      aria-label="Take adverts down when the seat is filled" />
                  </span>
                </div>
              </div>
            </CardPanel>
          </Card>
        </div>
        <FormBar dirty={dirty} label="Save channels" onCancel={() => setDraft(channels)}
          note={dirty
            ? plural(draft.enabled.length, "channel", "channels") + " available, " + plural(draft.defaults.length, "on", "on") + " by default for a new advert."
            : "Nothing has changed."}
          onSave={() => { if (!Object.keys(errors).length) onSaveChannels(draft); }} />
      </SettingsFrame>
    );
  }

  /* ── /settings/people ──────────────────────────────────────────────────── */
  const CAN = [
    { key: "review", label: "Read and move applications", roles: ["recruiter", "coordinator", "lead"] },
    { key: "book", label: "Book interviews", roles: ["recruiter", "coordinator", "lead"] },
    { key: "score", label: "File scorecards", roles: ["hiring_manager", "panel", "recruiter", "lead"] },
    { key: "offer", label: "Send offers", roles: ["recruiter", "lead"] },
    { key: "premium", label: "Approve money above the band", roles: ["lead"] },
    { key: "settings", label: "Change these settings", roles: ["lead"] },
  ];

  function PeopleSettingsScreen({ data, now, session, onNavigate, onInviteMember, onChangeRole }) {
    const me = staffById(data.staff, session.personId);
    const canChange = me.role === "lead";
    return (
      <SettingsFrame path="/settings/people" title="People and access" onNavigate={onNavigate}
        note={plural(data.staff.length, "person", "people") + " on this desk. What each role may do is fixed by the role, not per person, so there is one answer to \u201cwho can send an offer\u201d."}
        actions={<Button size="sm" disabled={!canChange} onClick={onInviteMember}><Icon name="user-round-plus" size={14} />Invite somebody</Button>}>

        {canChange ? null : (
          <Alert variant="info">
            <Icon name="info" size={16} />
            <AlertTitle>You can read this, not change it</AlertTitle>
            <AlertDescription>
              Roles and invitations are the head of resourcing's — {staffById(data.staff, "p-amara").name}. You are signed in as {labelOf(DESK_ROLES, me.role).toLowerCase()}.
            </AlertDescription>
          </Alert>
        )}

        <Card>
          <CardHeader>
            <CardTitle>The desk</CardTitle>
            <CardDescription>Owned applications and outstanding scorecards are counted from the live arrays, not from a field on the person.</CardDescription>
          </CardHeader>
          <div className="rd-card-table">
            <Table variant="card">
              <TableHeader>
                <TableRow>
                  <TableHead>Person</TableHead>
                  <TableHead className="rd-drop-md">Department</TableHead>
                  <TableHead>Role</TableHead>
                  <TableHead className="rd-num">Owns</TableHead>
                  <TableHead className="rd-num rd-drop-lg">Scorecards owed</TableHead>
                  <TableHead className="rd-actions-head">Action</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {data.staff.map((person) => {
                  const owned = liveApplications(ownedBy(data.applications, person.id)).length;
                  const owed = feedbackOutstanding(data.interviews).filter((iv) => iv.panelIds.indexOf(person.id) > -1).length;
                  return (
                    <TableRow key={person.id}>
                      <TableCell><StaffCell person={person} meta={person.email} /></TableCell>
                      <TableCell className="rd-drop-md">{labelOf(DEPARTMENTS, person.department)}</TableCell>
                      <TableCell>
                        <span className="rd-status">
                          <Badge size="sm" variant={person.role === "lead" ? "info" : "secondary"}>{labelOf(DESK_ROLES, person.role)}</Badge>
                          {person.id === me.id ? <span className="rd-foot">you</span> : null}
                        </span>
                      </TableCell>
                      <TableCell className="rd-num">{owned || "—"}</TableCell>
                      <TableCell className="rd-num rd-drop-lg">{owed || "—"}</TableCell>
                      <TableCell className="rd-actions">
                        <Button variant="outline" size="xs" disabled={!canChange || person.id === me.id}
                          onClick={() => onChangeRole(person)}>Change role</Button>
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
            </Table>
          </div>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">You cannot change your own role — that is what stops the last head of resourcing locking everybody out.</span>
          </CardFooter>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>What each role may do</CardTitle>
            <CardDescription>Fixed by role. One row per permission, one column per role, on one set of tracks.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-matrix" style={{ gridTemplateColumns: "minmax(0,1.4fr) " + DESK_ROLES.map(() => "minmax(0,1fr)").join(" ") }}>
              <span className="rd-matrix__head">Permission</span>
              {DESK_ROLES.map((role) => <span className="rd-matrix__head" key={role.value}>{role.label}</span>)}
              {CAN.map((permission) => (
                <React.Fragment key={permission.key}>
                  <span>{permission.label}</span>
                  {DESK_ROLES.map((role) => (
                    <span className="rd-matrix__cell" key={role.value + permission.key}>
                      {permission.roles.indexOf(role.value) > -1
                        ? <Icon name="check" size={14} />
                        : <span className="rd-muted">—</span>}
                    </span>
                  ))}
                </React.Fragment>
              ))}
            </div>
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">
              {plural(data.staff.filter((p) => p.role === "lead").length, "person", "people")} can approve money above a band and change these settings.
              A desk with one is a desk that stops when they are on leave.
            </span>
          </CardFooter>
        </Card>
      </SettingsFrame>
    );
  }

  Object.assign(window, { DeskSettingsScreen, ProcessSettingsScreen, ChannelSettingsScreen, PeopleSettingsScreen });
})();

</script>
<script type="text/plain" data-rd-file="TeamAccountScreens.jsx">
/* Desk team, your profile, and auth.
   Team is instant (nothing on it commits); the profile is Staged with one Save;
   auth is its own shell with no navigation owner (AGENTS §7.1) and every form
   on it is Staged. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Button, Badge, Avatar, Alert, AlertTitle, AlertDescription, Separator, Switch, Select,
    Field, FieldLabel, FieldDescription, FieldError, Input, InputGroup, InputGroupInput, InputGroupAddon,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  const BRAND_MARK = (window.__resources && window.__resources.brandMark) || "../../assets/templates/logos/12-logo-demo.svg";

  /* ── /team ─────────────────────────────────────────────────────────────── */
  function TeamScreen({ data, now, session, onNavigate }) {
    const desk = deskLoad(data.staff, data.applications, data.interviews, now);
    const panel = panelLoad(data.staff, data.applications, data.interviews, now);
    const unowned = liveApplications(data.applications).filter((a) => !a.ownerId);

    return (
      <div className="rd-page">
        <PageHead path="/team" title="Desk team"
          note={"Who owns what, and what the panel owes us. Load counts live applications only — " + plural(liveApplications(data.applications).length, "application", "applications") + " in total."} />

        {unowned.length ? (
          <Alert variant="warning">
            <Icon name="user-round-x" size={16} />
            <AlertTitle>{plural(unowned.length, "application", "applications")} with no owner</AlertTitle>
            <AlertDescription>Unowned applications appear in nobody's queue. {unowned.map((a) => a.candidate.name).join(", ")}.</AlertDescription>
          </Alert>
        ) : null}

        <Card>
          <CardHeader>
            <CardTitle>Recruiters and coordination</CardTitle>
            <CardDescription>Live applications owned, and how many of them the desk owes a move on.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead>Person</TableHead>
                    <TableHead className="rd-num">Owns</TableHead>
                    <TableHead className="rd-num">Waiting on them</TableHead>
                    <TableHead className="rd-num">Over service level</TableHead>
                    <TableHead className="rd-drop-md">Load</TableHead>
                    <TableHead className="rd-num rd-drop-lg">Interviews booked</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {desk.map((row) => (
                    <TableRow key={row.person.id}>
                      <TableCell><StaffCell person={row.person} /></TableCell>
                      <TableCell className="rd-num">{row.owned.length}</TableCell>
                      <TableCell className="rd-num">{row.awaitingUs}</TableCell>
                      <TableCell className="rd-num">{row.stalled || "—"}</TableCell>
                      <TableCell className="rd-drop-md" style={{ minWidth: "8rem" }}>
                        <Bar ratio={row.owned.length / Math.max(1, liveApplications(data.applications).length)} tone={row.stalled ? "warn" : "soft"} />
                      </TableCell>
                      <TableCell className="rd-num rd-drop-lg">{row.interviews}</TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </div>
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">Load is each person's owned applications as a share of the {liveApplications(data.applications).length} live ones.</span>
          </CardFooter>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>Hiring managers and the panel</CardTitle>
            <CardDescription>A different question over a different array: what clinicians and managers owe the desk.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-card-table">
              <Table variant="card">
                <TableHeader>
                  <TableRow>
                    <TableHead>Person</TableHead>
                    <TableHead className="rd-drop-md">Department</TableHead>
                    <TableHead className="rd-num">Booked ahead</TableHead>
                    <TableHead className="rd-num">Scorecards owed</TableHead>
                    <TableHead className="rd-num rd-drop-lg">Watching</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {panel.map((row) => (
                    <TableRow key={row.person.id} className={row.outstanding ? undefined : "rd-row--recessed"}>
                      <TableCell><StaffCell person={row.person} /></TableCell>
                      <TableCell className="rd-drop-md">{labelOf(DEPARTMENTS, row.person.department)}</TableCell>
                      <TableCell className="rd-num">{row.booked || "—"}</TableCell>
                      <TableCell className="rd-num">
                        {row.outstanding ? <Badge size="sm" variant="warning">{row.outstanding}</Badge> : "—"}
                      </TableCell>
                      <TableCell className="rd-num rd-drop-lg">{row.watching || "—"}</TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </div>
          </CardPanel>
          <CardFooter className="rd-card-foot">
            <span className="rd-foot">Rows with nothing owed are recessed, not hidden — a quiet panel member is still on the panel.</span>
            <Button variant="outline" size="sm" onClick={() => onNavigate("/interviews")}>Open interviews</Button>
          </CardFooter>
        </Card>
      </div>
    );
  }

  /* ── /me/profile — Staged: nothing applies until Save. ─────────────────── */
  function ProfileScreen({ data, now, session, onNavigate, preferences, onSavePreferences, onSavePhoto }) {
    const me = staffById(data.staff, session.personId);
    const [draft, setDraft] = React.useState(preferences);
    const [photo, setPhoto] = React.useState(me.photo);
    const [photoError, setPhotoError] = React.useState(null);
    const [over, setOver] = React.useState(false);
    const photoInput = React.useRef(null);
    React.useEffect(() => setDraft(preferences), [preferences]);
    React.useEffect(() => { setPhoto(me.photo); setPhotoError(null); }, [me.photo]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(preferences) || photo !== me.photo;
    const set = (key, value) => setDraft((d) => Object.assign({}, d, { [key]: value }));
    const owned = liveApplications(ownedBy(data.applications, me.id));

    /* Same control as the create form: dashed drop zone, real preview, the file
       input driven by buttons. Staged like everything else on this screen — the
       face does not change on the rest of the product until Save. */
    const readPhoto = (file) => {
      if (!file) return;
      if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setPhotoError("JPG, PNG or WebP only — that file is " + (file.type || "an unknown type") + "."); return; }
      if (file.size > 2 * 1024 * 1024) { setPhotoError("That is " + (file.size / 1048576).toFixed(1) + " MB. Up to 2 MB."); return; }
      setPhotoError(null);
      const reader = new FileReader();
      reader.onload = () => setPhoto(reader.result);
      reader.readAsDataURL(file);
    };

    return (
      <div className="rd-page">
        <PageHead path="/me/profile" title={me.name} avatar={me}
          note={me.title + " · " + labelOf(SITES, me.site) + " · owns " + plural(owned.length, "live application", "live applications")} />

        {/* Two column stacks, never three loose cards (AGENTS §5.1): you and
            what you hear about on the left, the queue's own defaults beside it. */}
        <div className="rd-columns">
          <div className="rd-col">
          <Card>
            <CardHeader>
              <CardTitle>You</CardTitle>
              <CardDescription>Your photograph is what the desk sees on every note you write and every panel you sit on.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-stack">
                <div className={"rd-upload" + (over ? " rd-upload--over" : "")}
                  onDragOver={(e) => { e.preventDefault(); setOver(true); }}
                  onDragLeave={() => setOver(false)}
                  onDrop={(e) => { e.preventDefault(); setOver(false); readPhoto(e.dataTransfer.files && e.dataTransfer.files[0]); }}>
                  <span className="rd-upload__face">
                    {photo ? <img src={photo} alt={"Photograph of " + me.name} /> : <Icon name="user-round" size={26} />}
                  </span>
                  <div className="rd-upload__body">
                    <span className="rd-upload__note">
                      {photo === me.photo
                        ? (photo ? "The photograph on your account." : "No photograph — your initials stand in wherever a face appears.")
                        : (photo ? "New photograph staged. It is not on your account until you save." : "Your photograph will be removed when you save.")}
                    </span>
                    <span className="rd-upload__actions">
                      <Button variant="outline" size="sm" onClick={() => photoInput.current.click()}>
                        <Icon name="upload" size={14} />{photo ? "Replace" : "Upload a photo"}
                      </Button>
                      {photo ? <Button variant="ghost" size="sm" onClick={() => { setPhoto(null); setPhotoError(null); }}>Remove</Button> : null}
                      {photo !== me.photo ? <Button variant="ghost" size="sm" onClick={() => { setPhoto(me.photo); setPhotoError(null); }}>Undo</Button> : null}
                    </span>
                    {photoError
                      ? <FieldError>{photoError}</FieldError>
                      : <span className="rd-foot">Drop an image here, or pick one. JPG, PNG or WebP; square reads best; up to 2 MB.</span>}
                  </div>
                  <input ref={photoInput} className="rd-sr" type="file" accept="image/png,image/jpeg,image/webp" aria-label="Choose your photograph"
                    onChange={(e) => { readPhoto(e.target.files && e.target.files[0]); e.target.value = ""; }} />
                </div>
                <Separator />
                <KeyValues items={[
                  { label: "Role", value: labelOf(DESK_ROLES, me.role) },
                  { label: "Site", value: labelOf(SITES, me.site) },
                  { label: "Email", value: me.email, note: "Held in the staff directory, not here" },
                  { label: "Live applications", value: owned.length, note: "Counted from the queue" },
                ]} />
              </div>
            </CardPanel>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>What you hear about</CardTitle>
              <CardDescription>Notifications from the bell and by email. Staged: nothing changes until you save.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-checks">
                {[
                  { key: "notifyMentions", label: "When somebody mentions you on a thread", note: "You are mentioned on " + plural(data.notes.filter((n) => mentionIds(n.body).indexOf(me.id) > -1).length, "note", "notes") + " at the moment." },
                  { key: "notifyScorecards", label: "When a scorecard lands on an application you own" },
                  { key: "notifyOffers", label: "When an offer is accepted or runs out" },
                  { key: "notifyStalled", label: "When something you own goes over its service level", note: "Counted per stage, from the same rule the queue uses." },
                ].map((row) => (
                  <label className="rd-check" key={row.key}>
                    <Switch checked={!!draft[row.key]} onCheckedChange={(v) => set(row.key, v)} aria-label={row.label} />
                    <span className="rd-check__text"><span>{row.label}</span>{row.note ? <span className="rd-foot">{row.note}</span> : null}</span>
                  </label>
                ))}
              </div>
            </CardPanel>
          </Card>
          </div>

          <div className="rd-col">
          <Card>
            <CardHeader>
              <CardTitle>How the queue opens</CardTitle>
              <CardDescription>Your own defaults on Applications.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="rd-fields">
                <Field>
                  <FieldLabel htmlFor="pf-view">Default view</FieldLabel>
                  <Select id="pf-view" value={draft.defaultView} onValueChange={(v) => set("defaultView", v)}
                    items={[
                      { value: "ours", label: "Waiting on us" },
                      { value: "theirs", label: "Waiting on others" },
                      { value: "all", label: "Everything" },
                    ]} />
                  <FieldDescription>Waiting on us is the desk's default, and the one the sidebar badge counts.</FieldDescription>
                </Field>
                <Field>
                  <FieldLabel htmlFor="pf-mine">Only your own applications</FieldLabel>
                  <label className="rd-check">
                    <Switch checked={!!draft.onlyMine} onCheckedChange={(v) => set("onlyMine", v)} aria-label="Filter the queue to applications you own" />
                    <span className="rd-check__text">
                      <span>Open the queue filtered to you</span>
                      <span className="rd-foot">You own {owned.length} of the {liveApplications(data.applications).length} live applications.</span>
                    </span>
                  </label>
                </Field>
              </div>
            </CardPanel>
          </Card>
          </div>
        </div>

        <FormBar dirty={dirty} label="Save your profile" onCancel={() => { setDraft(preferences); setPhoto(me.photo); setPhotoError(null); }}
          note={dirty
            ? (photo !== me.photo ? "Your photograph and " : "") + "four notification switches and two queue defaults, applied to your account only."
            : "Nothing has changed."}
          onSave={() => { if (photo !== me.photo) onSavePhoto(photo); onSavePreferences(draft); }} />
      </div>
    );
  }

  /* ── Auth ──────────────────────────────────────────────────────────────────
     One layout variant for the product: centred card, 23rem column, the mark
     above the form as the only navigation. One primary credential strategy:
     password, with a reveal toggle. Two auth screens are designed here — sign
     in and signed out — and because the product does not include a reset flow,
     it does not link to one: the support line is the way out. */
  function AuthFrame({ children }) {
    return (
      <div className="rd-auth">
        <div className="rd-auth__col">
          <div className="rd-auth__mark">
            <img src={BRAND_MARK} alt="" width="32" height="32" />
            <span className="rd-auth__words">
              <span className="rd-brand__name">{RD_ORG.desk}</span>
              <span className="rd-brand__sub">{RD_ORG.name}</span>
            </span>
          </div>
          {children}
        </div>
      </div>
    );
  }

  function SignInScreen({ onAuthenticated, attempts, onFailedAttempt }) {
    const [email, setEmail] = React.useState("");
    const [password, setPassword] = React.useState("");
    const [reveal, setReveal] = React.useState(false);
    const [state, setState] = React.useState("idle");
    const [errors, setErrors] = React.useState({});
    const dirty = !!email || !!password;
    const locked = attempts >= 3;

    const submit = () => {
      const next = {};
      if (!email.trim() || email.indexOf("@") === -1) next.email = "Use your Larkspur address.";
      if (!password) next.password = "Enter your password.";
      setErrors(next);
      if (Object.keys(next).length) { setState("invalid"); return; }
      /* One state at a time: a server answer replaces field errors, never both. */
      if (password.length < 8) { setErrors({}); setState("rejected"); setPassword(""); onFailedAttempt(); return; }
      setState("submitting");
      window.setTimeout(onAuthenticated, 400);
    };

    return (
      <AuthFrame>
        <Card>
          <CardHeader>
            <CardTitle>Sign in</CardTitle>
            <CardDescription>The recruitment desk for {RD_ORG.name}. Your Larkspur account, the same one as the staff intranet.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="rd-fields">
              {locked ? (
                <Alert variant="destructive">
                  <Icon name="lock" size={16} />
                  <AlertTitle>Too many attempts</AlertTitle>
                  <AlertDescription>This account is locked for 15 minutes. The IT service desk on 2200 can unlock it sooner.</AlertDescription>
                </Alert>
              ) : state === "rejected" ? (
                <Alert variant="destructive">
                  <Icon name="triangle-alert" size={16} />
                  <AlertTitle>Email or password is incorrect</AlertTitle>
                  <AlertDescription>{plural(3 - attempts, "attempt", "attempts")} left before the account locks for 15 minutes.</AlertDescription>
                </Alert>
              ) : null}
              <Field>
                <FieldLabel htmlFor="au-email">Email</FieldLabel>
                <Input id="au-email" type="email" autoComplete="email" value={email} invalid={!!errors.email} disabled={locked}
                  placeholder={"nadia.ferreira@" + RD_ORG.domain} onChange={(e) => setEmail(e.target.value)} />
                {errors.email ? <FieldError>{errors.email}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="au-password">Password</FieldLabel>
                <InputGroup>
                  <InputGroupInput id="au-password" type={reveal ? "text" : "password"} autoComplete="current-password" value={password}
                    invalid={!!errors.password} disabled={locked} onChange={(e) => setPassword(e.target.value)} />
                  <InputGroupAddon align="inline-end">
                    <Button variant="ghost" size="icon-xs" aria-label={reveal ? "Hide the password" : "Show the password"} onClick={() => setReveal(!reveal)}>
                      <Icon name={reveal ? "eye-off" : "eye"} size={14} />
                    </Button>
                  </InputGroupAddon>
                </InputGroup>
                {errors.password ? <FieldError>{errors.password}</FieldError> : null}
              </Field>
              <Button disabled={!dirty || locked || state === "submitting"} onClick={submit}>
                {state === "submitting" ? "Signing in…" : "Sign in"}
              </Button>
            </div>
          </CardPanel>
        </Card>
        <p className="rd-auth__foot">
          Trouble getting in? The IT service desk on 2200 handles Larkspur accounts — password resets are not done in this app.
        </p>
      </AuthFrame>
    );
  }

  function SignedOutScreen({ person, onNavigate }) {
    return (
      <AuthFrame>
        <Card>
          <CardPanel>
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="door-open" size={18} /></EmptyMedia>
                <EmptyTitle>You are signed out</EmptyTitle>
                <EmptyDescription>
                  {person ? person.name.split(" ")[0] + ", your" : "Your"} session on this browser is closed. Anything you had open was saved as you went — this product has no draft to lose.
                </EmptyDescription>
              </EmptyHeader>
              <EmptyContent><Button onClick={() => onNavigate("/signin")}>Sign in</Button></EmptyContent>
            </Empty>
          </CardPanel>
        </Card>
        <p className="rd-auth__foot">Candidate data stays on Larkspur systems. Close the browser on a shared machine.</p>
      </AuthFrame>
    );
  }

  Object.assign(window, { TeamScreen, ProfileScreen, SignInScreen, SignedOutScreen });
})();

</script>
<script type="text/plain" data-rd-file="App.jsx">
/* Recruitment Desk — the product's own routing and its commit machinery. Every
   action that changes anything goes through this file, so the reversibility
   tiers are decided in one place: harmless controls (watch, react) live on their
   screens, consequential ones apply here and raise an undo toast, destructive
   ones open a confirm dialog naming the consequence, and anything needing facts
   the app does not hold opens a staged dialog first.

   The chains that matter, because they are what actually happens on a desk:
   · Booking the panel writes the interview AND moves the stage, so the ladder,
     the stepper and the badge move together.
   · Starting the checks writes the two reference requests the policy demands, so
     "1 of 2 back" is a real count from a real array rather than a label.
   · Recording an acceptance fills a seat: when the accepted offers reach the
     requisition's headcount the requisition itself becomes filled, and the
     seats figure on /requisitions moves without anything being retyped.
   · Opening an application marks it read, which is the only thing that makes
     the unread figure on the desk true. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  /* Folded by default: the desk's own screens are wide, and the destinations
     are seven icons a recruiter learns in a day. null would follow the viewport;
     true is the product's chosen default, and the reader's chevron overrides it
     either way for the session. */
  const [fold, setFold] = React.useState(true);
  const [session] = React.useState(RD_SESSION);
  const [read, setRead] = React.useState(false);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [attempts, setAttempts] = React.useState(0);
  /* Settings state. `policy` is the RD_POLICY object itself: there is one copy of
     those numbers and every metric reads it, so saving mutates it and bumps a
     counter to re-render rather than threading a second copy through the tree. */
  const [policyVersion, setPolicyVersion] = React.useState(0);
  const [desk, setDesk] = React.useState({
    name: RD_ORG.name,
    replyTo: "resourcing@" + RD_ORG.domain,
    signature: "Resourcing team\nLarkspur Health\nWe answer every application, including the ones we decline.",
  });
  const [channels, setChannels] = React.useState({
    enabled: ["careers_site", "nhs_jobs", "referral", "agency", "event"],
    defaults: ["careers_site", "nhs_jobs"],
    reviewDays: 28,
    autoUnpost: true,
  });
  const [preferences, setPreferences] = React.useState({
    notifyMentions: true, notifyScorecards: true, notifyOffers: true, notifyStalled: false,
    defaultView: "ours", onlyMine: false,
  });
  const [data, setData] = React.useState({
    staff: STAFF, requisitions: REQUISITIONS, applications: APPLICATIONS,
    interviews: INTERVIEWS, notes: NOTES, adverts: ADVERTS, notifications: NOTIFICATIONS,
  });
  const palette = useCommandPalette();
  const now = RD_NOW;
  const me = staffById(data.staff, session.personId);

  React.useEffect(() => {
    document.documentElement.classList.toggle("dark", dark);
    document.body.classList.toggle("dark", dark);
  }, [dark]);

  /* Opening an application is what makes it read. */
  React.useEffect(() => {
    const match = path.match(/^\/applications\/(ap-[^/]+)$/);
    if (!match) return;
    const application = applicationById(data.applications, match[1]);
    if (application && !application.firstReadAt) patch("applications", application.id, { firstReadAt: now });
  }, [path]);

  const dismiss = (id) => setToasts((list) => list.filter((t) => t.id !== id));
  const pushToast = (toast) => {
    const id = "t" + Date.now() + Math.random().toString(36).slice(2, 6);
    setToasts((list) => list.concat([Object.assign({ id }, toast)]));
    window.setTimeout(() => dismiss(id), 9000);
  };

  /* ── Writing into the fixture ──────────────────────────────────────────── */
  const patch = (key, id, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const patchMany = (key, ids, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (ids.indexOf(r.id) > -1 ? Object.assign({}, r, fields) : r)) }));
  const snapshot = () => data;
  const restore = (before) => setData(before);
  const parseMoney = (value) => Math.round(parseFloat(String(value).replace(/[^0-9.]/g, "")) * 100);
  const parseDay = (value) => (value ? d0(value) : null);
  const dayValue = (ts) => dayKey(ts);
  const addNote = (applicationId, body, kind) => ({
    id: "nt-" + Math.random().toString(36).slice(2, 7), applicationId, authorId: session.personId,
    at: now, body, kind: kind || "note", parentId: null, reactions: {}, seenBy: [],
  });

  /* ── Applications ─────────────────────────────────────────────────────── */
  const moveStage = (application, stage, extra) => {
    patch("applications", application.id, Object.assign({
      stage, stageEnteredAt: now, firstReadAt: application.firstReadAt || now,
    }, extra || {}));
  };

  const onApplicationTransition = (application, transition) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const first = application.candidate.name.split(" ")[0];

    if (transition.id === "advance" && transition.to === "screening") {
      const before = snapshot();
      moveStage(application, "screening");
      pushToast({
        variant: "success", title: first + " is in screening",
        description: "Read and moved on " + formatDay(now) + ". " + RD_POLICY.stageDays.screening + " days is the service level for this step, so it is due by " + formatDayShort(now + RD_POLICY.stageDays.screening * DAY) + ".",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "assessment") {
      const before = snapshot();
      moveStage(application, "assessment");
      pushToast({
        variant: "success", title: "Assessment sent to " + first,
        description: (requisition.department === "digital" ? "Integration exercise" : "Case study") + " for " + requisition.title + ". Due back inside " + RD_POLICY.stageDays.assessment + " days; the queue will show it as over after that.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "panel") {
      bookInterview(application, "panel", "Book the panel for " + application.candidate.name,
        "Booking it moves the application to panel. " + RD_POLICY.panelScorecards + " scorecards from different people are needed before checks can start.");
      return;
    }

    if (transition.id === "book_interview") {
      bookInterview(application, application.stage === "panel" ? "panel" : "assessment", "Book another conversation with " + application.candidate.name,
        "The stage does not change — this is a second conversation inside " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + ".", true);
      return;
    }

    if (transition.id === "advance" && transition.to === "checks") {
      const score = scoresOf(application);
      setForm({
        title: "Start the checks on " + application.candidate.name,
        description: score.average.toFixed(2) + " over " + plural(score.count, "scorecard", "scorecards") + ". " + RD_POLICY.referencesRequired
          + " references are required and one must be the current employer. Nothing can be offered until both are back and the right to work is verified.",
        submitLabel: "Request the references",
        initial: {},
        fields: [
          { id: "ref1", kind: "text", label: "Current employer's reference", placeholder: "Name and where they work" },
          { id: "rel1", kind: "text", label: "Their relationship to the candidate", placeholder: "Current line manager, Meadowvale Trust" },
          { id: "ref2", kind: "text", label: "Second reference", placeholder: "Name and where they work" },
          { id: "rel2", kind: "text", label: "Their relationship to the candidate", placeholder: "Clinical supervisor" },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.ref1 || v.ref1.trim().length < 4) errors.ref1 = "Who is the current employer's referee?";
          if (!v.rel1 || v.rel1.trim().length < 4) errors.rel1 = "Say how they know the candidate.";
          if (!v.ref2 || v.ref2.trim().length < 4) errors.ref2 = "A second referee is required — one is not enough to hire on.";
          if (!v.rel2 || v.rel2.trim().length < 4) errors.rel2 = "Say how they know the candidate.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const references = [
            { id: "rf-" + Math.random().toString(36).slice(2, 7), name: v.ref1.trim(), relationship: v.rel1.trim(), status: "requested", requestedAt: now, receivedAt: null, note: null },
            { id: "rf-" + Math.random().toString(36).slice(2, 7), name: v.ref2.trim(), relationship: v.rel2.trim(), status: "requested", requestedAt: now, receivedAt: null, note: null },
          ];
          moveStage(application, "checks", { references });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Checks started. References requested from " + v.ref1.trim() + " and " + v.ref2.trim() + ". No offer until both are back.", "decision")]) }));
          pushToast({
            variant: "success", title: "Checks started on " + first,
            description: "Two references requested, " + RD_POLICY.stageDays.checks + " days on the clock. Right to work is " + statusWords(RIGHT_TO_WORK, application.rightToWork) + ".",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "offer") {
      setForm({
        title: "Offer " + application.candidate.name + " the " + requisition.title.toLowerCase(),
        description: "Band is " + bandLabel(requisition) + ". They asked for "
          + (application.salaryExpectationMinor ? money(application.salaryExpectationMinor) : "nothing in particular")
          + ". Target start on the requisition is " + formatDay(requisition.targetStartAt) + ".",
        submitLabel: "Send the offer",
        initial: { salary: String(Math.round(requisition.bandMinor[0] / 100)), start: dayValue(requisition.targetStartAt), expires: dayValue(now + RD_POLICY.offerValidDays * DAY) },
        preview: (v) => {
          const minor = parseMoney(v.salary || "0");
          if (!minor || isNaN(minor)) return null;
          const { Alert, AlertTitle, AlertDescription } = window.OriginDesignSystem_7c998b;
          const position = percent(bandPosition(requisition, minor));
          const over = application.salaryExpectationMinor && minor < application.salaryExpectationMinor;
          return (
            <Alert variant={minor > requisition.bandMinor[1] ? "warning" : "info"}>
              <Icon name={minor > requisition.bandMinor[1] ? "triangle-alert" : "badge-pound-sterling"} size={16} />
              <AlertTitle>{money(minor)} — {position} up the band</AlertTitle>
              <AlertDescription>
                {minor > requisition.bandMinor[1]
                  ? "Above the top of the band. Amara Boateng signs those off before the letter goes out."
                  : over ? money(application.salaryExpectationMinor - minor) + " below what they asked for. Worth a call before the letter." : "Inside the band and at or above what they asked for."}
              </AlertDescription>
            </Alert>
          );
        },
        fields: [
          { id: "salary", kind: "text", label: "Annual salary", hint: "Whole pounds, the way the contract states it." },
          { id: "start", kind: "day", label: "Start date" },
          { id: "expires", kind: "day", label: "Valid until", hint: RD_POLICY.offerValidDays + " days is the standard on a Larkspur offer." },
          { id: "note", kind: "area", label: "Anything agreed alongside it", placeholder: "Recruitment premium, notice period, a start date they asked for." },
        ],
        validate: (v) => {
          const errors = {};
          const minor = parseMoney(v.salary || "");
          if (!minor || isNaN(minor)) errors.salary = "Enter the salary in whole pounds.";
          else if (minor < requisition.bandMinor[0] * 0.9) errors.salary = "That is below the bottom of the band — check the figure.";
          if (!parseDay(v.start)) errors.start = "Give the start date.";
          if (!parseDay(v.expires)) errors.expires = "Say when the offer runs out.";
          else if (parseDay(v.expires) < now) errors.expires = "That date has passed.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const offer = {
            salaryMinor: parseMoney(v.salary), startAt: parseDay(v.start), sentAt: now,
            expiresAt: d1(v.expires), acceptedAt: null, note: (v.note || "").trim(),
          };
          moveStage(application, "offer", { offer });
          pushToast({
            variant: "success", title: "Offer out to " + first,
            description: money(offer.salaryMinor) + " to start " + formatDay(offer.startAt) + ", valid " + durationWords(daysBetween(now, offer.expiresAt)) + ". It is with them now, not with us.",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "hired") {
      const health = requisitionHealth(data.applications, requisition, now);
      const fills = health.accepted + 1 >= requisition.headcount;
      setConfirm({
        title: "Record " + application.candidate.name + " as hired?",
        description: "The offer was accepted on " + formatDay(application.offer.acceptedAt) + ". Recording it closes the application and hands the person to onboarding.",
        confirmLabel: "Record the hire",
        rows: [
          { key: "app", label: "Application", value: "Hired · starting " + formatDay(application.offer.startAt) },
          { key: "seat", label: "Seats on " + requisition.ref, value: (health.accepted + 1) + " of " + requisition.headcount + (fills ? " — the requisition becomes filled" : " — " + plural(requisition.headcount - health.accepted - 1, "seat", "seats") + " still open") },
          { key: "pipeline", label: "Others in the pipeline", value: plural(health.live.length - 1, "application", "applications") + " stay live" },
        ],
        run: () => {
          const before = snapshot();
          moveStage(application, "hired");
          if (fills) patch("requisitions", requisition.id, { status: "filled" });
          pushToast({
            variant: "success", title: first + " is hired",
            description: "Starting " + formatDay(application.offer.startAt) + ". " + (fills ? requisition.ref + " is now filled." : plural(requisition.headcount - health.accepted - 1, "seat", "seats") + " left on " + requisition.ref + "."),
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "decline") {
      const score = scoresOf(application);
      setForm({
        title: "Decline " + application.candidate.name,
        description: (score.count ? "Averaging " + score.average.toFixed(2) + " over " + plural(score.count, "scorecard", "scorecards") + ". " : "No scorecards on file. ")
          + "The reason and whether we would talk again are what the pool keeps.",
        submitLabel: "Continue",
        initial: { again: false },
        fields: [
          { id: "reason", kind: "radio", label: "Why?", options: DECLINE_REASONS },
          { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "We would talk to them again" },
          { id: "note", kind: "area", label: "What we would say", placeholder: "Specific, so the next person reading this file knows what happened.", hint: "Never sent automatically — it is for us, and it is what the talent pool shows." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.reason) errors.reason = "Pick the reason.";
          if (!v.note || v.note.trim().length < 15) errors.note = "One sentence at least — this is the whole record.";
          return errors;
        },
        run: (v) => setConfirm({
          title: "Decline " + application.candidate.name + "?",
          description: "They come off the pipeline for " + requisition.title + ". " + (v.again ? "They stay in the talent pool and can be reopened for another seat." : "They will not appear in the talent pool, so reopening means a fresh application."),
          confirmLabel: "Decline the application",
          destructive: true,
          rows: [
            { key: "reason", label: "Reason", value: labelOf(DECLINE_REASONS, v.reason) },
            { key: "pool", label: "Talent pool", value: v.again ? "Kept" : "Not kept" },
            { key: "iv", label: "Booked interviews", value: interviewsFor(data.interviews, application.id).filter(INTERVIEW_FLAGS.isBooked).length ? "Cancelled" : "None" },
          ],
          run: () => {
            const booked = interviewsFor(data.interviews, application.id).filter(INTERVIEW_FLAGS.isBooked);
            patch("applications", application.id, {
              stage: "declined", stageEnteredAt: now, lastLiveStage: application.stage,
              declineReason: v.reason, wouldReconsider: !!v.again, declineNote: v.note.trim(),
              decidedAt: now, decidedById: session.personId,
            });
            if (booked.length) patchMany("interviews", booked.map((iv) => iv.id), { status: "cancelled" });
            setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, v.note.trim(), "decision")]) }));
            pushToast({
              title: "Declined · " + application.candidate.name,
              description: labelOf(DECLINE_REASONS, v.reason) + (v.again ? " · in the talent pool" : " · not kept") + (booked.length ? " · " + plural(booked.length, "interview", "interviews") + " cancelled" : ""),
            });
          },
        }),
      });
      return;
    }

    if (transition.id === "withdraw") {
      setForm({
        title: application.candidate.name + " has withdrawn",
        description: "Their decision, not ours. What they said is the most useful thing on this file — it is what the pool shows if we go back to them.",
        submitLabel: "Record the withdrawal",
        initial: { again: true },
        fields: [
          { id: "reason", kind: "radio", label: "Why?", options: WITHDRAW_REASONS },
          { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "They asked to be kept on file" },
          { id: "note", kind: "area", label: "What they said", placeholder: "Another offer, the travel, the rota, the money." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.reason) errors.reason = "Pick the closest reason.";
          if (!v.note || v.note.trim().length < 10) errors.note = "Write down what they told you.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          patch("applications", application.id, {
            stage: "withdrawn", stageEnteredAt: now, lastLiveStage: application.stage,
            withdrawReason: v.reason, wouldReconsider: !!v.again, withdrawNote: v.note.trim(),
            decidedAt: now, decidedById: session.personId,
          });
          pushToast({
            title: first + " has withdrawn",
            description: labelOf(WITHDRAW_REASONS, v.reason) + ". " + requisition.title + " is down to " + plural(requisitionHealth(data.applications, requisition, now).live.length - 1, "live application", "live applications") + ".",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "reopen") {
      const open = data.requisitions.filter((r) => r.status === "open");
      setForm({
        title: "Reopen " + application.candidate.name + " for another seat",
        description: "This is their own record, moved — not a copy. Their documents, notes and history come with them. Reached " + statusValue(APPLICATION_STAGE, application.lastLiveStage || "applied").label.toLowerCase() + " last time.",
        submitLabel: "Reopen the application",
        initial: { requisitionId: open[0] ? open[0].id : "", stage: "screening" },
        fields: [
          { id: "requisitionId", kind: "select", label: "Which seat?", options: open.map((r) => ({ value: r.id, label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site) })) },
          { id: "stage", kind: "radio", label: "Coming back in at", options: [{ value: "applied", label: "Applied — start again from the top" }, { value: "screening", label: "Screening — we have already read them" }] },
          { id: "note", kind: "area", label: "Why now", placeholder: "What changed: a new seat, a new band, they got the experience they were missing." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.requisitionId) errors.requisitionId = "Pick the seat.";
          if (!v.note || v.note.trim().length < 10) errors.note = "Say what changed.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          patch("applications", application.id, {
            requisitionId: v.requisitionId, stage: v.stage, stageEnteredAt: now, lastLiveStage: null,
            declineReason: null, declineNote: null, withdrawReason: null, withdrawNote: null, decidedAt: null, decidedById: null,
          });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, v.note.trim(), "decision")]) }));
          pushToast({
            variant: "success", title: first + " is live again",
            description: requisitionById(data.requisitions, v.requisitionId).title + " at " + statusValue(APPLICATION_STAGE, v.stage).label.toLowerCase() + ". They are out of the talent pool.",
            onUndo: () => restore(before),
          });
        },
      });
    }
  };

  /* One booking dialog, used by the advance-to-panel move, the "book another"
     action and the header button on /interviews. */
  const bookInterview = (application, kind, title, description, keepStage) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const candidates = data.staff.filter((p) => p.role === "hiring_manager" || p.role === "panel" || p.role === "recruiter");
    setForm({
      title, description,
      submitLabel: "Book it",
      initial: {
        day: dayValue(now + 4 * DAY), time: "10:00", minutes: "60",
        panel: kind === "screening" ? [session.personId] : [requisition.hiringManagerId],
        where: kind === "screening" ? "Telephone" : "Larkspur House, room 2.4",
      },
      fields: [
        { id: "day", kind: "day", label: "Date" },
        { id: "time", kind: "time", label: "Start time" },
        { id: "minutes", kind: "select", label: "Length", options: [{ value: "30", label: "30 minutes" }, { value: "60", label: "60 minutes" }, { value: "90", label: "90 minutes" }] },
        { id: "panel", kind: "checks", label: "Who is on it", options: candidates.map((p) => ({ value: p.id, label: p.name, note: p.title })) },
        { id: "where", kind: "text", label: "Where" },
      ],
      validate: (v) => {
        const errors = {};
        const when = v.day && v.time ? new Date(v.day + "T" + v.time).getTime() : null;
        if (!when) errors.day = "Pick a date and a time.";
        else if (when < now) errors.day = "That is in the past.";
        if (!v.panel || !v.panel.length) errors.panel = "Somebody has to be in the room.";
        else if (kind === "panel" && v.panel.length < RD_POLICY.panelScorecards) errors.panel = RD_POLICY.panelScorecards + " people are needed on a panel — that is where the two scorecards come from.";
        if (!v.where || !v.where.trim()) errors.where = "Where is it? A room, a site or the telephone.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const when = new Date(v.day + "T" + v.time).getTime();
        const interview = {
          id: "iv-" + Math.random().toString(36).slice(2, 7), applicationId: application.id, kind,
          at: when, minutes: Number(v.minutes), panelIds: v.panel, status: "scheduled", where: v.where.trim(), note: null,
        };
        setData((d) => Object.assign({}, d, { interviews: d.interviews.concat([interview]) }));
        if (!keepStage) moveStage(application, kind === "panel" ? "panel" : "assessment");
        pushToast({
          variant: "success",
          title: (kind === "panel" ? "Panel" : kind === "assessment" ? "Assessment" : "Screening call") + " booked · " + application.candidate.name,
          description: formatWeekday(when) + " at " + formatTime(when) + ", " + v.minutes + " minutes, " + v.panel.map((id) => firstNameOf(staffById(data.staff, id))).join(" and ") + ". " + (keepStage ? "The stage has not changed." : "The application is at " + (kind === "panel" ? "panel" : "assessment") + "."),
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onBookInterview = () => {
    const live = liveApplications(data.applications).filter(APPLICATION_FLAGS.awaitsUs);
    setForm({
      title: "Book an interview",
      description: "Only the " + live.length + " applications the desk owes a move on can be booked. Pick one and the right kind of conversation for its stage.",
      submitLabel: "Choose the application",
      initial: { applicationId: live[0] ? live[0].id : "" },
      fields: [{ id: "applicationId", kind: "select", label: "Application", options: live.map((a) => ({ value: a.id, label: a.candidate.name + " · " + statusValue(APPLICATION_STAGE, a.stage).label + " · " + requisitionById(data.requisitions, a.requisitionId).title })) }],
      validate: (v) => (v.applicationId ? {} : { applicationId: "Pick the application." }),
      run: (v) => {
        const application = applicationById(data.applications, v.applicationId);
        const kind = application.stage === "panel" ? "panel" : application.stage === "assessment" ? "assessment" : "screening";
        bookInterview(application, kind, "Book a " + (kind === "screening" ? "screening call" : kind) + " with " + application.candidate.name,
          "The application stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + "; booking a conversation does not move it on its own.", true);
      },
    });
  };

  /* ── Batches: they say exactly what they are about to do, and ask first ── */
  const onBulkAdvance = (applications, clear) => {
    if (!applications.length) return;
    setConfirm({
      title: "Move " + plural(applications.length, "application", "applications") + " on?",
      description: "Each one moves to the next step of its own process, dated today, in your name. Undo restores the batch.",
      confirmLabel: "Move " + applications.length + " on",
      rows: applications.map((a) => ({
        key: a.id, label: a.candidate.name,
        value: statusValue(APPLICATION_STAGE, a.stage).label + " → " + statusValue(APPLICATION_STAGE, STAGE_AFTER[a.stage]).label,
      })),
      run: () => {
        const before = snapshot();
        applications.forEach((a) => moveStage(a, STAGE_AFTER[a.stage]));
        clear();
        pushToast({
          variant: "success", title: plural(applications.length, "application", "applications") + " moved on",
          description: applications.map((a) => a.candidate.name.split(" ")[0]).join(", ") + ". Anything that needed a booking or a reference request is still waiting for it.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onBulkDecline = (applications, clear) => {
    if (!applications.length) return;
    setForm({
      title: "Decline " + plural(applications.length, "application", "applications"),
      description: "One reason and one note for all of them. If they need different reasons, decline them one at a time.",
      submitLabel: "Continue",
      initial: { again: false },
      fields: [
        { id: "reason", kind: "radio", label: "Why?", options: DECLINE_REASONS },
        { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "We would talk to all of them again" },
        { id: "note", kind: "area", label: "What we would say", placeholder: "One field was much stronger than the rest — say so plainly." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.reason) errors.reason = "Pick the reason.";
        if (!v.note || v.note.trim().length < 15) errors.note = "One sentence at least.";
        return errors;
      },
      run: (v) => setConfirm({
        title: "Decline " + plural(applications.length, "application", "applications") + "?",
        description: "All of them come off their pipelines with the same reason and note. " + (v.again ? "All stay in the talent pool." : "None are kept in the pool."),
        confirmLabel: "Decline " + applications.length,
        destructive: true,
        rows: applications.map((a) => ({ key: a.id, label: a.candidate.name, value: statusValue(APPLICATION_STAGE, a.stage).label })),
        run: () => {
          const ids = applications.map((a) => a.id);
          setData((d) => Object.assign({}, d, {
            applications: d.applications.map((a) => (ids.indexOf(a.id) > -1 ? Object.assign({}, a, {
              stage: "declined", stageEnteredAt: now, lastLiveStage: a.stage, declineReason: v.reason,
              wouldReconsider: !!v.again, declineNote: v.note.trim(), decidedAt: now, decidedById: session.personId,
            }) : a)),
          }));
          clear();
          pushToast({
            title: plural(applications.length, "application", "applications") + " declined",
            description: labelOf(DECLINE_REASONS, v.reason) + ". " + (v.again ? "All " + applications.length + " are in the talent pool." : "None kept in the pool."),
          });
        },
      }),
    });
  };

  /* ── Interviews ───────────────────────────────────────────────────────── */
  const onInterviewTransition = (interview, transition) => {
    const application = applicationById(data.applications, interview.applicationId);
    const first = application.candidate.name.split(" ")[0];

    if (transition.id === "record_held") {
      const before = snapshot();
      patch("interviews", interview.id, { status: "awaiting_feedback" });
      pushToast({
        title: "Held · " + first,
        description: interview.kind === "panel"
          ? RD_POLICY.panelScorecards + " scorecards are needed from " + panelOf(data.staff, interview).map(firstNameOf).join(" and ") + ". It is on the outstanding list until they are in."
          : "Waiting on " + panelOf(data.staff, interview).map(firstNameOf).join(" and ") + " to write it up.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "add_scorecard") {
      setForm({
        title: "Your scorecard for " + application.candidate.name,
        description: "Four areas, the same four on every requisition, so two cards can be read side by side. " + scoresOf(application).count + " of " + RD_POLICY.panelScorecards + " in so far.",
        submitLabel: "File the scorecard",
        initial: { scores: {}, recommend: "yes" },
        fields: [
          { id: "scores", kind: "scores", label: "Scores, 1 to 5" },
          { id: "recommend", kind: "radio", label: "Would you have them?", options: [{ value: "yes", label: "Yes" }, { value: "maybe", label: "Not sure" }, { value: "no", label: "No" }] },
          { id: "note", kind: "area", label: "What you saw", placeholder: "The question you asked and what they actually said. Specific beats generous.", hint: "The panel reads this, and so does whoever picks the file up in six months." },
        ],
        validate: (v) => {
          const errors = {};
          const scores = v.scores || {};
          if (SCORE_AREAS.some((area) => !scores[area.value])) errors.scores = "Score all four areas.";
          if (!v.note || v.note.trim().length < 20) errors.note = "A sentence or two — a bare score is not a scorecard.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const card = { byId: session.personId, at: now, scores: v.scores, note: v.note.trim(), recommend: v.recommend };
          const cards = (application.scorecards || []).concat([card]);
          patch("applications", application.id, { scorecards: cards });
          patch("interviews", interview.id, { status: "complete" });
          const score = scoresOf(Object.assign({}, application, { scorecards: cards }));
          pushToast({
            variant: "success", title: "Scorecard filed · " + first,
            description: score.count + " of " + RD_POLICY.panelScorecards + " in, averaging " + score.average.toFixed(2) + ". "
              + (score.complete ? "Checks can start." : "One more is needed before this can move."),
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "reschedule") {
      setForm({
        title: "Reschedule the " + interview.kind + " with " + application.candidate.name,
        description: "Currently " + formatWeekday(interview.at) + " at " + formatTime(interview.at) + " · " + interview.where + ". The panel and the length stay as they are.",
        submitLabel: "Move it",
        initial: { day: dayValue(interview.at), time: formatTime(interview.at) },
        fields: [
          { id: "day", kind: "day", label: "New date" },
          { id: "time", kind: "time", label: "New start time" },
          { id: "why", kind: "area", label: "Why", placeholder: "Who asked, and what it clashed with.", hint: "Posted on the thread so the panel sees it." },
        ],
        validate: (v) => {
          const errors = {};
          const when = v.day && v.time ? new Date(v.day + "T" + v.time).getTime() : null;
          if (!when) errors.day = "Pick a date and a time.";
          else if (when < now) errors.day = "That is in the past.";
          if (!v.why || v.why.trim().length < 8) errors.why = "Say why it moved.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const when = new Date(v.day + "T" + v.time).getTime();
          patch("interviews", interview.id, { at: when, note: v.why.trim() });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Moved the " + interview.kind + " to " + formatWeekday(when) + " at " + formatTime(when) + ". " + v.why.trim(), "note")]) }));
          pushToast({ title: "Moved · " + first, description: formatWeekday(when) + " at " + formatTime(when) + ", " + interview.where + ".", onUndo: () => restore(before) });
        },
      });
      return;
    }

    if (transition.id === "record_no_show") {
      const before = snapshot();
      patch("interviews", interview.id, { status: "no_show" });
      pushToast({
        title: "No show recorded · " + first,
        description: "The application stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + " and nothing is decided by a missed call. Book another before you draw a conclusion.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "cancel_interview") {
      setConfirm({
        title: "Cancel the " + interview.kind + " with " + application.candidate.name + "?",
        description: "The slot goes back to the panel and the candidate has to be told. The application does not move.",
        confirmLabel: "Cancel the interview",
        destructive: true,
        rows: [
          { key: "when", label: "Was booked", value: formatWeekday(interview.at) + " at " + formatTime(interview.at) },
          { key: "panel", label: "Panel", value: panelOf(data.staff, interview).map((p) => p.name).join(", ") },
          { key: "stage", label: "Application", value: "Stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() },
        ],
        run: () => {
          patch("interviews", interview.id, { status: "cancelled" });
          pushToast({ title: "Cancelled · " + first, description: "Tell them today — it was " + formatWeekday(interview.at) + "." });
        },
      });
    }
  };

  /* ── Requisitions ─────────────────────────────────────────────────────── */
  const onRequisitionTransition = (requisition, transition) => {
    const health = requisitionHealth(data.applications, requisition, now);

    if (transition.id === "open_req") {
      const before = snapshot();
      patch("requisitions", requisition.id, { status: "open", openedAt: now });
      pushToast({
        variant: "success", title: requisition.title + " is open",
        description: plural(requisition.headcount, "seat", "seats") + ", target start " + formatDay(requisition.targetStartAt) + ". It accepts applications from today.",
        onUndo: () => restore(before),
      });
      return;
    }
    if (transition.id === "hold_req") {
      setForm({
        title: "Put " + requisition.title + " on hold",
        description: plural(health.live.length, "live application", "live applications") + " stay in the pipeline and have to be told. The reason shows on the requisition.",
        submitLabel: "Put on hold",
        initial: {},
        fields: [{ id: "note", kind: "area", label: "Why, and until when", placeholder: "Waiting on the establishment review; revisit in October." }],
        validate: (v) => (v.note && v.note.trim().length > 12 ? {} : { note: "Say why, and roughly when it comes back." }),
        run: (v) => {
          const before = snapshot();
          patch("requisitions", requisition.id, { status: "on_hold", holdNote: v.note.trim() });
          pushToast({ title: requisition.title + " on hold", description: v.note.trim(), onUndo: () => restore(before) });
        },
      });
      return;
    }
    if (transition.id === "resume_req") {
      const before = snapshot();
      patch("requisitions", requisition.id, { status: "open", holdNote: null });
      pushToast({
        variant: "success", title: requisition.title + " is open again",
        description: plural(health.live.length, "application", "applications") + " still live, " + health.awaitingUs + " waiting on the desk.",
        onUndo: () => restore(before),
      });
      return;
    }
    if (transition.id === "close_req") {
      setForm({
        title: "Close " + requisition.title + " without hiring",
        description: "The reason is the only record of why this seat went away.",
        submitLabel: "Continue",
        initial: {},
        fields: [{ id: "note", kind: "area", label: "Why is it closing?", placeholder: "Absorbed into another seat, funding pulled, the need went away." }],
        validate: (v) => (v.note && v.note.trim().length > 12 ? {} : { note: "Write it down — a closed requisition with no reason is a mystery next quarter." }),
        run: (v) => setConfirm({
          title: "Close " + requisition.title + "?",
          description: plural(health.live.length, "live application", "live applications") + " will be declined and the requisition cannot be reopened — a new one would have to be raised. This is not reversible.",
          confirmLabel: "Close the requisition",
          destructive: true,
          rows: health.live.map((a) => ({ key: a.id, label: a.candidate.name, value: statusValue(APPLICATION_STAGE, a.stage).label })),
          run: () => {
            const ids = health.live.map((a) => a.id);
            setData((d) => Object.assign({}, d, {
              requisitions: d.requisitions.map((r) => (r.id === requisition.id ? Object.assign({}, r, { status: "closed", closedAt: now, closedNote: v.note.trim() }) : r)),
              applications: d.applications.map((a) => (ids.indexOf(a.id) > -1 ? Object.assign({}, a, {
                stage: "declined", stageEnteredAt: now, lastLiveStage: a.stage, declineReason: "stronger_field",
                wouldReconsider: true, declineNote: "Requisition closed: " + v.note.trim(), decidedAt: now, decidedById: session.personId,
              }) : a)),
            }));
            pushToast({
              title: requisition.title + " closed",
              description: v.note.trim() + (ids.length ? " " + plural(ids.length, "application", "applications") + " declined, all kept in the talent pool." : ""),
            });
            setPath("/requisitions");
          },
        }),
      });
    }
  };

  /* ── Social: harmless and instant, so a bare control and no confirmation ── */
  const onReact = (note, value) => {
    setData((d) => Object.assign({}, d, {
      notes: d.notes.map((n) => {
        if (n.id !== note.id) return n;
        const who = (n.reactions || {})[value] || [];
        const next = who.indexOf(session.personId) > -1 ? who.filter((id) => id !== session.personId) : who.concat([session.personId]);
        return Object.assign({}, n, { reactions: Object.assign({}, n.reactions, { [value]: next }) });
      }),
    }));
  };

  const onToggleWatch = (application) => {
    const watching = (application.watcherIds || []).indexOf(session.personId) > -1;
    patch("applications", application.id, {
      watcherIds: watching ? application.watcherIds.filter((id) => id !== session.personId) : (application.watcherIds || []).concat([session.personId]),
    });
    pushToast({
      title: watching ? "No longer watching " + application.candidate.name : "Watching " + application.candidate.name,
      description: watching ? "You will not hear about this one unless you are mentioned." : "You will hear about scorecards, offers and anything that goes over its service level.",
    });
  };

  /* Consequential: a note is visible to the desk and the hiring manager the
     moment it is posted, so it applies immediately and offers an undo. */
  const onPostNote = (application, body, kind, parent) => {
    const before = snapshot();
    const note = Object.assign(addNote(application.id, body, kind), { parentId: parent ? parent.id : null });
    setData((d) => Object.assign({}, d, { notes: d.notes.concat([note]) }));
    const mentioned = mentionIds(body).map((id) => staffById(data.staff, id)).filter(Boolean);
    pushToast({
      title: parent ? "Reply posted" : "Note posted",
      description: (mentioned.length ? mentioned.map((p) => p.name).join(" and ") + " " + (mentioned.length === 1 ? "is" : "are") + " notified. " : "")
        + "Everybody following " + application.candidate.name + " sees it — " + plural(followersOf(application, data.notes, data.staff).length, "person", "people") + ".",
      onUndo: () => restore(before),
    });
  };

  const onChaseReference = (reference) => {
    const application = data.applications.find((a) => (a.references || []).some((r) => r.id === reference.id));
    const before = snapshot();
    patch("applications", application.id, {
      references: application.references.map((r) => (r.id === reference.id ? Object.assign({}, r, { status: "chased" }) : r)),
    });
    pushToast({
      title: "Chased " + reference.name,
      description: reference.relationship + " · asked " + formatDay(reference.requestedAt) + ". Still " + referenceState(application).received + " of " + RD_POLICY.referencesRequired + " back.",
      onUndo: () => restore(before),
    });
  };

  /* ── Offers: the desk's two levers on a record it does not control ────── */
  const onChaseOffer = (application) => {
    const before = snapshot();
    const left = daysBetween(now, application.offer.expiresAt);
    setData((d) => Object.assign({}, d, {
      notes: d.notes.concat([addNote(application.id, "Chased the offer — " + durationWords(left) + " on it. " + money(application.offer.salaryMinor) + " to start " + formatDay(application.offer.startAt) + ".", "note")]),
    }));
    pushToast({
      title: "Chased " + application.candidate.name.split(" ")[0],
      description: durationWords(left) + " on the offer. The chase is on the thread so the hiring manager can see it happened.",
      onUndo: () => restore(before),
    });
  };

  const onExtendOffer = (application) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    setForm({
      title: "Extend " + application.candidate.name + "'s offer",
      description: "It runs out " + formatDay(application.offer.expiresAt) + " — " + durationWords(daysBetween(now, application.offer.expiresAt))
        + ". Extending is the desk's decision; the money and the start date do not change here.",
      submitLabel: "Extend it",
      initial: { expires: dayValue(application.offer.expiresAt + 7 * DAY) },
      fields: [
        { id: "expires", kind: "day", label: "New date it runs out" },
        { id: "why", kind: "area", label: "Why", placeholder: "They asked for the weekend to talk it over with family.", hint: "Posted on the thread. The hiring manager sees the new date on " + requisition.ref + "." },
      ],
      validate: (v) => {
        const errors = {};
        const when = parseDay(v.expires);
        if (!when) errors.expires = "Pick the new date.";
        else if (when < now) errors.expires = "That date has passed.";
        else if (when <= application.offer.expiresAt) errors.expires = "That is not later than " + formatDay(application.offer.expiresAt) + ".";
        if (!v.why || v.why.trim().length < 10) errors.why = "Say why — an extension with no reason reads as a soft deadline.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const expiresAt = d1(v.expires);
        patch("applications", application.id, { offer: Object.assign({}, application.offer, { expiresAt }) });
        setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Offer extended to " + formatDay(expiresAt) + ". " + v.why.trim(), "decision")]) }));
        pushToast({
          variant: "success", title: "Extended to " + formatDay(expiresAt),
          description: durationWords(daysBetween(now, expiresAt)) + " now. " + money(application.offer.salaryMinor) + " and the " + formatDay(application.offer.startAt) + " start are unchanged.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  /* ── Compliance: three checks, one action each ────────────────────────── */
  const onVerifyRightToWork = (application) => {
    setForm({
      title: "Verify right to work for " + application.candidate.name,
      description: application.rightToWork === "needs_sponsorship"
        ? "This candidate needs sponsorship. Verifying here records that a certificate is in place — it does not create one, and Amara Boateng holds the establishment's certificates."
        : "Currently " + statusWords(RIGHT_TO_WORK, application.rightToWork) + ". Record what you saw, not what you were told.",
      submitLabel: "Record the check",
      initial: {},
      fields: [
        { id: "document", kind: "radio", label: "What did you see?", options: [
          { value: "passport", label: "British or Irish passport" },
          { value: "share_code", label: "Home Office share code" },
          { value: "brp", label: "Biometric residence permit" },
          { value: "sponsorship", label: "Certificate of sponsorship" },
        ] },
        { id: "note", kind: "area", label: "Anything to record", placeholder: "Seen in person on 28 August, copy on file, expiry noted." },
      ],
      validate: (v) => (v.document ? {} : { document: "Say which document you saw." }),
      run: (v) => {
        const before = snapshot();
        patch("applications", application.id, { rightToWork: "verified" });
        setData((d) => Object.assign({}, d, {
          notes: d.notes.concat([addNote(application.id, "Right to work verified — " + v.document.replace("_", " ") + "." + (v.note ? " " + v.note.trim() : ""), "decision")]),
        }));
        const missing = checksMissing(Object.assign({}, application, { rightToWork: "verified" }));
        pushToast({
          variant: "success", title: "Right to work verified · " + application.candidate.name.split(" ")[0],
          description: missing.length ? "Still outstanding: " + missing.join(" · ") + "." : "Nothing outstanding — clear to start.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onSubmitDbs = (application) => {
    const before = snapshot();
    patch("applications", application.id, { dbs: "submitted", dbsAt: now });
    pushToast({
      title: "DBS sent for " + application.candidate.name.split(" ")[0],
      description: "With the service from today. They usually come back inside two weeks; the checks step allows " + RD_POLICY.stageDays.checks + " days.",
      onUndo: () => restore(before),
    });
  };

  const onChaseRegistration = (application) => {
    const before = snapshot();
    const days = daysBetween(now, application.registrationExpiresAt);
    setData((d) => Object.assign({}, d, {
      notes: d.notes.concat([addNote(application.id, "Asked for the renewed " + application.registration.split(" ")[0] + " registration — "
        + (days < 0 ? "lapsed " + formatDay(application.registrationExpiresAt) : "renews " + formatDay(application.registrationExpiresAt)) + ".", "note")]),
    }));
    pushToast({
      title: "Renewal asked for · " + application.candidate.name.split(" ")[0],
      description: application.registration + " " + (days < 0 ? "lapsed " + formatDay(application.registrationExpiresAt) + ". Nobody starts on a lapsed registration." : "renews " + formatDay(application.registrationExpiresAt) + " — " + durationWords(days) + "."),
      onUndo: () => restore(before),
    });
  };

  /* ── The advert ───────────────────────────────────────────────────────── */
  const patchAdvert = (requisitionId, fields) => setData((d) => Object.assign({}, d, {
    adverts: d.adverts.map((ad) => (ad.requisitionId === requisitionId ? Object.assign({}, ad, fields) : ad)),
  }));

  const onEditAdvert = (requisition, advert) => {
    setForm({
      title: "Edit the advert for " + requisition.title,
      description: "Staged: nothing changes on the live posting until you save. Band and target start are not editable here — they come from the requisition, so the advert cannot state a different figure.",
      submitLabel: "Save the wording",
      initial: {
        headline: advert.headline, body: advert.body,
        essentials: advert.essentials.join("\n"), desirables: advert.desirables.join("\n"),
      },
      fields: [
        { id: "headline", kind: "text", label: "Headline", hint: "What a candidate sees in a list of forty jobs." },
        { id: "body", kind: "area", label: "The advert", hint: "Blank line between paragraphs. Say the hard parts plainly — the drive, the on-call, the nights." },
        { id: "essentials", kind: "area", label: "Essential", hint: "One per line. These are what screening actually filters on." },
        { id: "desirables", kind: "area", label: "Desirable", hint: "One per line." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.headline || v.headline.trim().length < 10) errors.headline = "A headline that names the seat and the place.";
        if (!v.body || v.body.trim().length < 80) errors.body = "A paragraph at least — this is the whole advert.";
        if (!v.essentials || !v.essentials.trim()) errors.essentials = "At least one essential, or screening has nothing to filter on.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const lines = (text) => text.split("\n").map((line) => line.trim()).filter(Boolean);
        patchAdvert(requisition.id, {
          headline: v.headline.trim(), body: v.body.trim(),
          essentials: lines(v.essentials), desirables: lines(v.desirables || ""),
          updatedAt: now, updatedById: session.personId,
        });
        pushToast({
          variant: "success", title: "Advert saved",
          description: plural(lines(v.essentials).length, "essential", "essentials") + " and " + plural(lines(v.desirables || "").length, "desirable", "desirables")
            + ". " + (advert.postedTo.length ? "Live on " + plural(advert.postedTo.length, "channel", "channels") + " — the new wording is up now." : "Not posted anywhere yet."),
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onPostAdvert = (requisition, advert) => {
    const unposted = SOURCES.filter((source) => source.value !== "pool" && !advert.postedTo.some((p) => p.channel === source.value));
    setForm({
      title: "Post the advert",
      description: "Applications from a channel are counted from the day it goes up, so the rate on this screen is the advert's own doing and not somebody else's referral.",
      submitLabel: "Post it",
      initial: { channel: unposted[0] ? unposted[0].value : "" },
      fields: [{ id: "channel", kind: "radio", label: "Where?", options: unposted.map((source) => ({ value: source.value, label: source.label })) }],
      validate: (v) => (v.channel ? {} : { channel: "Pick a channel." }),
      run: (v) => {
        const before = snapshot();
        patchAdvert(requisition.id, { postedTo: advert.postedTo.concat([{ channel: v.channel, postedAt: now, views: 0 }]) });
        pushToast({
          variant: "success", title: "Posted to " + labelOf(SOURCES, v.channel),
          description: "Live from today on " + plural(advert.postedTo.length + 1, "channel", "channels") + ". Views and applications from it are counted from now.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onUnpostAdvert = (requisition, advert, row) => {
    setConfirm({
      title: "Unpublish from " + row.source.label + "?",
      description: "The advert comes down and nobody new can apply through it. What it already produced stays on the requisition.",
      confirmLabel: "Unpublish",
      destructive: true,
      rows: [
        { key: "up", label: "Up since", value: formatDay(row.posting.postedAt) + " · " + plural(row.days, "day", "days") },
        { key: "apps", label: "Applications it produced", value: plural(row.applications, "application", "applications") + (row.live ? ", " + row.live + " still live" : "") },
        { key: "other", label: "Still posted", value: advert.postedTo.length - 1 ? plural(advert.postedTo.length - 1, "channel", "channels") : "Nowhere — the seat goes dark" },
      ],
      run: () => {
        patchAdvert(requisition.id, { postedTo: advert.postedTo.filter((p) => p.channel !== row.posting.channel) });
        pushToast({
          title: "Down from " + row.source.label,
          description: advert.postedTo.length - 1
            ? "Still live on " + plural(advert.postedTo.length - 1, "channel", "channels") + "."
            : requisition.ref + " is not advertised anywhere now, and it has " + plural(requisitionHealth(data.applications, requisition, now).live.length, "live application", "live applications") + ".",
        });
      },
    });
  };

  /* ── Notifications ────────────────────────────────────────────────────── */
  const onReadNotification = (notification) => {
    if (notification.readAt) return;
    patch("notifications", notification.id, { readAt: now });
  };
  const onUnreadNotification = (notification) => patch("notifications", notification.id, { readAt: null });
  const onReadAll = () => {
    const before = snapshot();
    const unread = unreadNotifications(data.notifications);
    patchMany("notifications", unread.map((n) => n.id), { readAt: now });
    pushToast({
      title: plural(unread.length, "notification", "notifications") + " marked read",
      description: "The bell is clear. They are all still on the notifications screen, newest first.",
      onUndo: () => restore(before),
    });
  };

  /* ── Settings. Staged screens, so each one commits once, here. ────────── */
  const onSaveDesk = (draft) => {
    const before = desk;
    setDesk(draft);
    pushToast({
      variant: "success", title: "The desk is saved",
      description: draft.name + " · replies go to " + draft.replyTo + ". Every advert and message the desk sends uses these from now on.",
      onUndo: () => setDesk(before),
    });
  };

  /* One copy of the policy, mutated in place: the queue, the stepper, the
     badges and Compliance all read RD_POLICY, so they move together. */
  const onSavePolicy = (draft) => {
    const before = JSON.parse(JSON.stringify(RD_POLICY));
    const apply = (values) => { Object.assign(RD_POLICY, values); setPolicyVersion((n) => n + 1); };
    apply(draft);
    const over = stalledApplications(data.applications, now).length;
    pushToast({
      variant: "success", title: "Process saved",
      description: "Screening " + draft.stageDays.screening + " days, checks " + draft.stageDays.checks + ", offers valid " + draft.offerValidDays
        + ". " + plural(over, "application", "applications") + " now reads as over its service level.",
      onUndo: () => apply(before),
    });
  };

  const onSaveChannels = (draft) => {
    const before = channels;
    setChannels(draft);
    pushToast({
      variant: "success", title: "Channels saved",
      description: plural(draft.enabled.length, "channel", "channels") + " available, " + draft.defaults.map((v) => labelOf(SOURCES, v)).join(" and ")
        + " on by default. Postings are queried after " + draft.reviewDays + " days.",
      onUndo: () => setChannels(before),
    });
  };

  const onInviteMember = () => {
    setForm({
      title: "Invite somebody to the desk",
      description: "They get an email with a link. The role decides what they can do — see the table below the invite for exactly what.",
      submitLabel: "Send the invitation",
      initial: { role: "panel" },
      fields: [
        { id: "name", kind: "text", label: "Name", placeholder: "Ruth Calloway" },
        { id: "email", kind: "text", label: "Larkspur email", placeholder: "name@" + RD_ORG.domain },
        { id: "role", kind: "radio", label: "Role", options: DESK_ROLES.map((r) => ({ value: r.value, label: r.label })) },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.name || v.name.trim().indexOf(" ") === -1) errors.name = "Full name.";
        if (!v.email || v.email.indexOf("@" + RD_ORG.domain) === -1) errors.email = "A Larkspur address — the desk holds candidate data.";
        if (!v.role) errors.role = "Pick the role.";
        return errors;
      },
      run: (v) => {
        pushToast({
          variant: "success", title: "Invitation sent to " + v.name.trim(),
          description: v.email.trim() + " · " + labelOf(DESK_ROLES, v.role) + ". They appear on this screen when they accept, not before.",
        });
      },
    });
  };

  const onChangeRole = (person) => {
    const owned = liveApplications(ownedBy(data.applications, person.id)).length;
    setForm({
      title: "Change " + person.name + "'s role",
      description: labelOf(DESK_ROLES, person.role) + " today" + (owned ? ", owning " + plural(owned, "live application", "live applications") : "") + ". A role change takes effect the next time they open the desk.",
      submitLabel: "Change the role",
      initial: { role: person.role },
      fields: [{ id: "role", kind: "radio", label: "Role", options: DESK_ROLES.map((r) => ({ value: r.value, label: r.label })) }],
      validate: (v) => (v.role && v.role !== person.role ? {} : { role: "Pick a different role." }),
      run: (v) => {
        const before = snapshot();
        patch("staff", person.id, { role: v.role });
        pushToast({
          variant: "success", title: person.name + " is now " + labelOf(DESK_ROLES, v.role).toLowerCase(),
          description: owned ? plural(owned, "application", "applications") + " stay with them — a role change does not move work." : "They own nothing at the moment, so nothing moves.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  /* ── The other legitimate way in: we ask, they fill it, and an application
     appears when they submit — not when we send the link. So this writes
     nothing to the desk, and the toast says so plainly rather than leaving a
     phantom record in a status nobody could act on. */
  const onInviteCandidate = () => {
    const open = data.requisitions.filter((r) => r.status === "open");
    setForm({
      title: "Send the application form",
      description: "They get a link to the form for one requisition, valid for 14 days. Nothing appears on the desk until they submit it — there is no half-application sitting in the queue.",
      submitLabel: "Send the link",
      initial: { requisitionId: open[0] ? open[0].id : "", source: "referral" },
      fields: [
        { id: "email", kind: "text", label: "Their email", placeholder: "name@example.com", hint: "The only thing we keep until they submit." },
        { id: "name", kind: "text", label: "Their name", placeholder: "Rowan Ellis-Baptiste", hint: "So the email is not addressed to nobody." },
        { id: "requisitionId", kind: "select", label: "Which seat?", options: open.map((r) => ({ value: r.id, label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site) })) },
        { id: "source", kind: "select", label: "Where they came from", options: SOURCES.map((s) => ({ value: s.value, label: s.label })) },
        { id: "message", kind: "area", label: "What to say", placeholder: "We met at the Riverside open evening — here is the form for the community staff nurse post.", hint: "Goes into the email above the link. The form itself asks for the CV, the photograph and the right-to-work documents." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.email || v.email.indexOf("@") === -1 || v.email.indexOf(".") === -1) errors.email = "A real email address — the link is the whole point.";
        if (!v.name || v.name.trim().length < 2) errors.name = "Their name, as they gave it to you.";
        if (!v.requisitionId) errors.requisitionId = "Pick the seat the form is for.";
        if (!v.message || v.message.trim().length < 15) errors.message = "A sentence at least — a bare link from a hospital reads as a scam.";
        return errors;
      },
      run: (v) => {
        const requisition = requisitionById(data.requisitions, v.requisitionId);
        pushToast({
          variant: "success",
          title: "Form sent to " + v.name.trim(),
          description: v.email.trim() + " · " + requisition.ref + " " + requisition.title + ". The link runs out " + formatDay(now + 14 * DAY)
            + ". Nothing is on the desk yet: their application arrives at applied, owned by you, when they submit it.",
        });
      },
    });
  };

  /* ── Creating records ─────────────────────────────────────────────────── */
  const onCreateApplication = (values) => {
    const before = snapshot();
    const id = "ap-" + Math.random().toString(36).slice(2, 7);
    const application = {
      id, ref: "APP-" + id.slice(3).toUpperCase(),
      candidate: { name: values.name.trim(), initials: initialsOf(values.name.trim()), photo: values.photo || portraitFor(values.name.trim()), headline: values.headline.trim(), location: values.location.trim() || "Not stated" },
      requisitionId: values.requisitionId, stage: "applied", source: values.source,
      appliedAt: now, stageEnteredAt: now, firstReadAt: null, ownerId: session.personId,
      rightToWork: values.rightToWork, salaryExpectationMinor: values.expectation ? parseMoney(values.expectation) : null,
      registration: values.registration.trim() || null,
      documents: (values.documents || []).map((document) => ({ kind: document.kind, name: document.name, at: now })), scorecards: [], references: [], offer: null, lastLiveStage: null,
      watcherIds: [], declineReason: null, declineNote: null, wouldReconsider: false, decidedAt: null, decidedById: null,
      withdrawReason: null, withdrawNote: null,
    };
    const notes = values.note.trim() ? [addNote(id, values.note.trim(), "note")] : [];
    setData((d) => Object.assign({}, d, { applications: d.applications.concat([application]), notes: d.notes.concat(notes) }));
    pushToast({
      variant: "success", title: values.name.trim() + " is on the desk",
      description: "Applied today, unread, owned by you"
        + ((values.documents || []).length ? ", with " + plural(values.documents.length, "document", "documents") : "")
        + ". " + requisitionById(data.requisitions, values.requisitionId).title + " now has "
        + plural(requisitionHealth(data.applications, requisitionById(data.requisitions, values.requisitionId), now).live.length + 1, "live application", "live applications") + ".",
      onUndo: () => restore(before),
    });
    setPath("/applications/" + id);
  };

  const onCreateRequisition = (values) => {
    const before = snapshot();
    const ref = String(1070 + data.requisitions.length);
    const requisition = {
      id: "rq-" + ref, ref: "LH-" + ref, title: values.title.trim(), department: values.department, site: values.site,
      employmentType: values.employmentType, bandMinor: values.bandMinor,
      hiringManagerId: values.hiringManagerId, recruiterId: session.personId, headcount: Number(values.headcount),
      openedAt: now, targetStartAt: d0(values.targetStart), status: "draft",
      registration: values.registration === "none" ? null : values.registration,
      brief: values.brief.trim(), holdNote: null, closedNote: null, closedAt: null,
    };
    setData((d) => Object.assign({}, d, { requisitions: d.requisitions.concat([requisition]) }));
    pushToast({
      variant: "success", title: requisition.ref + " saved as a draft",
      description: values.title.trim() + " · " + plural(requisition.headcount, "seat", "seats") + " · " + bandLabel(requisition) + ". Open it to start accepting applications.",
      onUndo: () => restore(before),
    });
    setPath("/requisitions/" + requisition.id);
  };

  /* Your own photograph, saved onto the staff record — so it changes on every
     note, panel and account menu at once, because there is one copy of it. */
  const onSavePhoto = (photo) => {
    const before = snapshot();
    patch("staff", session.personId, { photo });
    pushToast({
      variant: "success",
      title: photo ? "Photograph saved" : "Photograph removed",
      description: photo
        ? "It is on every note you have written, every panel you sit on and the account menu."
        : "Your initials stand in wherever a face appears.",
      onUndo: () => restore(before),
    });
  };

  const onSavePreferences = (draft) => {
    const before = preferences;
    setPreferences(draft);
    pushToast({
      variant: "success", title: "Preferences saved",
      description: [draft.notifyMentions && "mentions", draft.notifyScorecards && "scorecards", draft.notifyOffers && "offers", draft.notifyStalled && "service levels"]
        .filter(Boolean).join(", ") + " · the queue opens on " + (draft.defaultView === "ours" ? "waiting on us" : draft.defaultView === "theirs" ? "waiting on others" : "everything")
        + (draft.onlyMine ? ", filtered to you." : "."),
      onUndo: () => setPreferences(before),
    });
  };

  /* ── Routing ───────────────────────────────────────────────────────────── */
  const preview = (
    <TemplateMenu product="Recruitment Desk" path={path} onNavigate={setPath} groups={RD_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (RD_AUTH_PATHS.indexOf(path) > -1) {
    const authScreen = () => {
      if (path === "/signout") return <SignedOutScreen person={me} onNavigate={setPath} />;
      return <SignInScreen attempts={attempts} onFailedAttempt={() => setAttempts((n) => n + 1)} onAuthenticated={() => { setAttempts(0); setPath("/"); }} />;
    };
    return <>{authScreen()}{preview}</>;
  }

  const applicationMatch = path.match(/^\/applications\/(ap-[^/]+)$/);
  const requisitionMatch = path.match(/^\/requisitions\/(rq-[^/]+)$/);
  const advertMatch = path.match(/^\/requisitions\/(rq-[^/]+)\/advert$/);

  const screen = () => {
    if (path === "/") {
      return <DeskScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onInterviewTransition={onInterviewTransition} onReact={onReact} />;
    }
    if (path === "/applications") {
      return <ApplicationsScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onBulkAdvance={onBulkAdvance} onBulkDecline={onBulkDecline} pushToast={pushToast} />;
    }
    if (path === "/applications/new") {
      return <NewApplicationScreen data={data} now={now} session={session} onNavigate={setPath} onCreateApplication={onCreateApplication} onInviteCandidate={onInviteCandidate} />;
    }
    if (path === "/interviews") {
      return <InterviewsScreen data={data} now={now} session={session} onNavigate={setPath}
        onInterviewTransition={onInterviewTransition} onBookInterview={onBookInterview} />;
    }
    if (path === "/requisitions") {
      return <RequisitionsScreen data={data} now={now} session={session} onNavigate={setPath}
        onRequisitionTransition={onRequisitionTransition} pushToast={pushToast} />;
    }
    if (path === "/requisitions/new") {
      return <NewRequisitionScreen data={data} now={now} session={session} onNavigate={setPath} onCreateRequisition={onCreateRequisition} />;
    }
    if (path === "/pool") {
      return <PoolScreen data={data} now={now} session={session} onNavigate={setPath} onApplicationTransition={onApplicationTransition} />;
    }
    if (path === "/offers") {
      return <OffersScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onChaseOffer={onChaseOffer} onExtendOffer={onExtendOffer} />;
    }
    if (path === "/compliance") {
      return <ComplianceScreen data={data} now={now} session={session} onNavigate={setPath}
        onVerifyRightToWork={onVerifyRightToWork} onSubmitDbs={onSubmitDbs} onChaseRegistration={onChaseRegistration} />;
    }
    if (path === "/insights") return <InsightsScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/team") return <TeamScreen data={data} now={now} session={session} onNavigate={setPath} />;
    if (path === "/notifications") {
      return <NotificationsScreen data={data} now={now} session={session} onNavigate={setPath}
        onReadNotification={onReadNotification} onUnreadNotification={onUnreadNotification} onReadAll={onReadAll} />;
    }
    if (path === "/settings/desk") return <DeskSettingsScreen data={data} now={now} session={session} onNavigate={setPath} desk={desk} onSaveDesk={onSaveDesk} />;
    if (path === "/settings/process") return <ProcessSettingsScreen data={data} now={now} session={session} onNavigate={setPath} policy={RD_POLICY} onSavePolicy={onSavePolicy} />;
    if (path === "/settings/channels") return <ChannelSettingsScreen data={data} now={now} session={session} onNavigate={setPath} channels={channels} onSaveChannels={onSaveChannels} />;
    if (path === "/settings/people") return <PeopleSettingsScreen data={data} now={now} session={session} onNavigate={setPath} onInviteMember={onInviteMember} onChangeRole={onChangeRole} />;
    if (path === "/me/profile") {
      return <ProfileScreen data={data} now={now} session={session} onNavigate={setPath} preferences={preferences} onSavePreferences={onSavePreferences} onSavePhoto={onSavePhoto} />;
    }
    if (applicationMatch) {
      const application = applicationById(data.applications, applicationMatch[1]);
      if (!application) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <ApplicationScreen application={application} data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onInterviewTransition={onInterviewTransition}
        onPostNote={onPostNote} onReact={onReact} onToggleWatch={onToggleWatch} onChaseReference={onChaseReference} />;
    }
    if (advertMatch) {
      const requisition = requisitionById(data.requisitions, advertMatch[1]);
      const advert = requisition ? advertFor(data.adverts, requisition.id) : null;
      if (!requisition || !advert) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <AdvertScreen requisition={requisition} advert={advert} data={data} now={now} session={session} onNavigate={setPath}
        onEditAdvert={onEditAdvert} onPostAdvert={onPostAdvert} onUnpostAdvert={onUnpostAdvert} />;
    }
    if (requisitionMatch) {
      const requisition = requisitionById(data.requisitions, requisitionMatch[1]);
      if (!requisition) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <RequisitionScreen requisition={requisition} data={data} now={now} session={session} onNavigate={setPath}
        onRequisitionTransition={onRequisitionTransition} onApplicationTransition={onApplicationTransition} />;
    }
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    {
      label: "Candidates",
      items: data.applications.map((a) => ({
        value: a.id,
        label: a.candidate.name + " · " + statusValue(APPLICATION_STAGE, a.stage).label + " · " + requisitionById(data.requisitions, a.requisitionId).title,
        route: "/applications/" + a.id,
      })),
    },
    {
      label: "Requisitions",
      items: data.requisitions.map((r) => ({
        value: r.id,
        label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site),
        route: "/requisitions/" + r.id,
      })),
    },
    {
      label: "Interviews booked",
      items: interviewsAhead(data.interviews, now).map((iv) => ({
        value: iv.id,
        label: applicationById(data.applications, iv.applicationId).candidate.name + " · " + formatWeekday(iv.at) + " " + formatTime(iv.at),
        route: "/interviews",
      })),
    },
  ];

  return (
    <RdShell
      path={path} onNavigate={setPath} dark={dark} onToggleTheme={() => setDark(!dark)}
      fold={fold} onSetFold={setFold}
      data={data} person={me} notifications={data.notifications} unread={unreadNotifications(data.notifications).length}
      onRead={onReadAll} onOpenNotifications={() => setPath("/notifications")}
      palette={palette} toasts={toasts} onDismissToast={dismiss} now={now}
      policyVersion={policyVersion}
    >
      {screen()}
      <CommandPalette
        open={palette.open} onClose={() => palette.setOpen(false)} routes={RD_ROUTES}
        recordGroups={recordGroups} onNavigate={setPath} label="Search the Recruitment Desk"
        placeholder="Search candidates, requisitions and interviews…"
        emptyMessage="Nothing matches that. Try a candidate's name, a requisition reference or a destination." />
      <ConfirmDialog request={confirm} onCancel={() => setConfirm(null)} onConfirm={() => { confirm.run(); setConfirm(null); }} />
      <FormDialog request={form} onCancel={() => setForm(null)} onSubmit={(values) => { form.run(values); setForm(null); }} />
      {preview}
    </RdShell>
  );
}
/* boot.js publishes a stub on window.RecruitmentDeskApp before anything is
   fetched, so <x-import> always resolves; the real component lands here and the
   stub swaps it in. */
window.__recruitmentDeskReal = App;
if (!window.RecruitmentDeskApp) window.RecruitmentDeskApp = App;

</script>
<script>
(() => {
  const wait = async (test, ms) => { for (let i = 0; i < ms / 25; i++) { if (test()) return true; await new Promise((r) => setTimeout(r, 25)); } return !!test(); };
  (async () => {
    await wait(() => window.React && window.ReactDOM && window.Babel, 20000);
    await wait(() => Object.keys(window).some((k) => k.indexOf("OriginDesignSystem") === 0), 20000);
    const nodes = [...document.querySelectorAll('script[data-rd-file]')];
    for (const node of nodes) {
      const name = node.getAttribute('data-rd-file');
      const out = window.Babel.transform(node.textContent, { presets: ["react"], filename: name }).code;
      try { new Function(out)(); } catch (e) { console.error('standalone: ' + name, e); throw e; }
    }
    const App = window.__recruitmentDeskReal;
    const boot = document.getElementById('rd-boot');
    if (boot) boot.remove();
    window.ReactDOM.createRoot(document.getElementById('rd-root')).render(window.React.createElement(App));
  })().catch((e) => console.error('standalone boot:', e));
})();
</script>
</body>
</html>
