<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claims Desk — Corven Line</title>
<!-- Resources referenced only as strings inside the product's own code, lifted
     so the bundler can find them. At runtime window.__resources holds the
     inlined blob URLs. -->
<meta name="ext-resource-dependency" content="../../assets/templates/logos/03-logo-demo.svg" data-resource-id="brandMark">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-40-03.jpg" data-resource-id="userAvatar">
<link rel="stylesheet" href="../../styles.css">
<link rel="stylesheet" href="./kit.css">
<style>html,body{margin:0;padding:0}body{background:var(--sidebar)}</style>
<template id="__bundler_thumbnail" data-bg-color="#12293f">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg"><rect width="1200" height="800" fill="#12293f"/><g transform="translate(360 240)"><rect x="0" y="0" width="480" height="88" rx="10" fill="#f2b134"/><rect x="0" y="112" width="480" height="88" rx="10" fill="#e8edf2"/><rect x="0" y="224" width="480" height="88" rx="10" fill="#e8edf2" opacity="0.55"/><rect x="0" y="352" width="480" height="10" rx="5" fill="#3b5a78"/><rect x="0" y="352" width="300" height="10" rx="5" fill="#f2b134"/></g></svg>
</template>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" 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>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-presets="react" data-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/babel" data-presets="react" data-file="enums.js">
/* Claims Desk — status vocabulary.
   A value not listed here does not exist. If a screen needs one, it is added
   here first, with a tone grouping, a reason, and at least one fixture.
   Anything computed from other fields is NOT a value — derived flags live at
   the bottom of this file and take their clock as an argument.

   Policy constants live here too: they are part of this product's vocabulary
   (an adjuster's authority, the carrier's liability limits, the time bar), and
   nothing outside this folder may read them. */

const CLAIM_POLICY = {
  /* The legal filing deadline: one year from delivery, Hague-Visby art. III(6). */
  timeBarDays: 365,
  /* Above this the adjuster cannot decide alone — a manager signs off. */
  authorityLimitCents: 2500000,
  /* Package / weight limitation of liability, Hague-Visby art. IV(5)(a):
     the HIGHER of 666.67 SDR per package and 2 SDR per kilo. */
  packageLimitSdr: 666.67,
  weightLimitSdrPerKg: 2,
  /* IMF SDR/USD, 21 Aug 2026. One rate, one place. */
  sdrUsdRate: 1.34,
};

/* claim.status — where one cargo claim sits between filing and closure.
   Seven values, five tones, so the grouping answers the question the work
   queue asks: "can I act on it, and if not, who is holding it?"
     neutral = waiting on somebody outside the carrier (customer, surveyor)
               or already closed — nothing for the adjuster to do
     warning = waiting on the adjuster; this is the only actionable tone
     info    = waiting on another team inside the carrier (legal, treasury)
     success = the money has left us
   Deliberately absent because they are derived: `overdue` and `time_barred`
   (both computed from deliveredAt + timeBarDays), `blocked` (computed from the
   mandatory document set), `needs_signoff` (computed from the payable amount
   against authorityLimitCents). */
const CLAIM_STATUS = [
  { id: "awaiting_documents", label: "Awaiting documents", tone: "neutral" },
  { id: "awaiting_survey", label: "Awaiting survey", tone: "neutral" },
  { id: "ready_for_decision", label: "Ready for decision", tone: "warning" },
  { id: "awaiting_signoff", label: "Awaiting sign-off", tone: "info" },
  { id: "escalated_legal", label: "With legal", tone: "info" },
  { id: "approved_awaiting_payment", label: "Approved — awaiting payment", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
  { id: "rejected", label: "Rejected", tone: "neutral" },
];
/* ids: "awaiting_documents" | "awaiting_survey" | "ready_for_decision" |
        "awaiting_signoff" | "escalated_legal" | "approved_awaiting_payment" |
        "paid" | "rejected"
   `rejected` is neutral, not danger: turning a claim down is a decision we
   made and can defend, not a failure event. `awaiting_signoff` is a stored
   state, not a derived one: the decision has been submitted and is sitting with
   the manager, which is not the same fact as "the amount is above the limit"
   (that is derived, and lives in CLAIM_FLAGS.needsSignoff). */

/* document.status — one item of evidence on one claim.
   neutral = asked for, not here yet · success = on file and usable ·
   danger = it arrived and is unusable, which is a failure someone must fix. */
const DOCUMENT_STATUS = [
  { id: "requested", label: "Requested", tone: "neutral" },
  { id: "received", label: "Received", tone: "success" },
  { id: "illegible", label: "Illegible", tone: "danger" },
];
/* ids: "requested" | "received" | "illegible" */

/* settlement.status — the payout of an approved claim.
   warning = waiting on the adjuster to schedule it · info = booked or moving ·
   success = the customer's bank has it · danger = it bounced back. */
const SETTLEMENT_STATUS = [
  { id: "unscheduled", label: "To schedule", tone: "warning" },
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "sent", label: "Sent", tone: "info" },
  { id: "cleared", label: "Cleared", tone: "success" },
  { id: "returned", label: "Returned", tone: "danger" },
];
/* ids: "unscheduled" | "scheduled" | "sent" | "cleared" | "returned" */

/* response.status — what the customer did with our offer.
   neutral = the ball is theirs · success = settled · warning = they have
   rejected the offer and it is back with the adjuster. Disputing is a
   deliberate act needing attention, not a system failure, so it is warning
   rather than danger. */
const RESPONSE_STATUS = [
  { id: "awaiting_response", label: "Awaiting response", tone: "neutral" },
  { id: "accepted", label: "Accepted", tone: "success" },
  { id: "disputed", label: "Disputed", tone: "warning" },
];
/* ids: "awaiting_response" | "accepted" | "disputed" */

/* recovery.status — pursuing the party that caused the damage. The mirror of
   claim.status, and it takes the same grouping: warning = waiting on us,
   info = waiting on them, success = money back, danger = liability refused,
   neutral = we closed it ourselves.
   Deliberately absent: `time_barred` (derived from fileByAt). */
const RECOVERY_STATUS = [
  { id: "to_file", label: "To file", tone: "warning" },
  { id: "filed", label: "Filed", tone: "info" },
  { id: "negotiating", label: "Negotiating", tone: "info" },
  { id: "recovered", label: "Recovered", tone: "success" },
  { id: "denied", label: "Denied", tone: "danger" },
  { id: "written_off", label: "Written off", tone: "neutral" },
];
/* ids: "to_file" | "filed" | "negotiating" | "recovered" | "denied" | "written_off" */

/* Classification lists — read through labelOf, never reaching a Badge. */
const DOCUMENT_TYPES = [
  { value: "port_photos", label: "Port photographs", mandatory: false },
  { value: "surveyor_report", label: "Surveyor's report", mandatory: true },
  { value: "packing_list", label: "Packing list", mandatory: true },
  { value: "commercial_invoice", label: "Commercial invoice", mandatory: true },
  { value: "delivery_receipt", label: "Delivery receipt", mandatory: true },
];
const MANDATORY_DOCUMENT_TYPES = DOCUMENT_TYPES.filter((d) => d.mandatory).map((d) => d.value);

const COUNTERPARTY_KINDS = [
  { value: "terminal", label: "Terminal" },
  { value: "haulier", label: "Road haulier" },
  { value: "packer", label: "Packer / stuffer" },
  { value: "lashing", label: "Lashing contractor" },
];

const REJECT_REASONS = [
  { value: "pre_shipment_damage", label: "Damage pre-dates shipment" },
  { value: "excepted_peril", label: "Excepted peril (art. IV(2))" },
  { value: "insufficient_evidence", label: "Evidence does not support the loss" },
  { value: "below_deductible", label: "Loss falls below the deductible" },
  { value: "time_barred", label: "Filed after the time bar" },
];

const DECISION_OUTCOMES = [
  { value: "approve_full", label: "Approve in full" },
  { value: "approve_part", label: "Approve in part" },
  { value: "reject", label: "Reject" },
];

/* What this desk raises, and therefore what a person can choose to be emailed
   about. One vocabulary: every notification carries one of these values, and
   the alerts screen offers exactly this list. */
const ALERT_KINDS = [
  { value: "time_bar", label: "Time bars", hint: "A claim moving inside 90 days of its bar, or past it." },
  { value: "documents", label: "Evidence", hint: "Documents arriving, and documents rejected as illegible." },
  { value: "signoff", label: "Sign-off", hint: "Decisions signed off, sent back, or still unsigned after a week." },
  { value: "payment", label: "Payments", hint: "Payments cleared by the bank, and payments returned." },
  { value: "response", label: "Customer responses", hint: "Offers accepted and offers disputed." },
  { value: "recovery", label: "Recoveries", hint: "Replies from terminals, hauliers and packers." },
];

/* Desk preferences — the shape of the settings this product actually acts on.
   Nothing is offered here that no screen reads. */
const DESK_DEFAULTS = { landing: "/", queueTab: "mine", openDecidable: true };

/* Derived flags. Clock injected, never read from the module. */
const CLAIM_FLAGS = {
  /* The record's own deadline — stored nowhere, computed everywhere. */
  timeBarAt: (claim) => claim.deliveredAt + CLAIM_POLICY.timeBarDays * 864e5,
  daysLeft: (claim, now) => Math.round((CLAIM_FLAGS.timeBarAt(claim) - now) / 864e5),
  isOpen: (claim) => ["paid", "rejected"].indexOf(claim.status) === -1,
  isTimeBarred: (claim, now) => CLAIM_FLAGS.timeBarAt(claim) < now,
  /* Whose desk it is on. */
  awaitsAdjuster: (claim) => claim.status === "ready_for_decision",
  /* Submitted and sitting with the manager. Not the same fact as needsSignoff,
     which is about the amount rather than about where the file is. */
  awaitsSignoff: (claim) => claim.status === "awaiting_signoff",
  /* How long the manager has had it. Only meaningful while awaitsSignoff. */
  signoffWaitingSince: (claim) => (claim.decision ? claim.decision.decidedAt : claim.claimedAt),
  missingMandatory: (claim) => MANDATORY_DOCUMENT_TYPES.filter((type) => {
    const doc = claim.documents.find((d) => d.type === type);
    return !doc || doc.status !== "received";
  }),
  evidenceComplete: (claim) => CLAIM_FLAGS.missingMandatory(claim).length === 0,
  /* THE act-ability predicate. The row, the tab count, the queue headline and
     the nav badge all call this one function — a claim is decidable when it is
     on the adjuster's desk AND every mandatory document is on file. */
  isDecidable: (claim) => CLAIM_FLAGS.awaitsAdjuster(claim) && CLAIM_FLAGS.evidenceComplete(claim),
  /* Open, but nothing the adjuster can do — recessed, never disabled. */
  isWaitingOnOthers: (claim) => CLAIM_FLAGS.isOpen(claim) && !CLAIM_FLAGS.awaitsAdjuster(claim),
  /* Authority. Takes the payable figure rather than recomputing it, so the
     screen and the flag can never disagree. */
  needsSignoff: (payableCents) => payableCents > CLAIM_POLICY.authorityLimitCents,
};

const SETTLEMENT_FLAGS = {
  exists: (claim) => !!claim.settlement,
  needsScheduling: (claim) => !!claim.settlement && ["unscheduled", "returned"].indexOf(claim.settlement.status) > -1,
  isReleased: (claim) => !!claim.settlement && ["sent", "cleared"].indexOf(claim.settlement.status) > -1,
};

const RESPONSE_FLAGS = {
  exists: (claim) => !!claim.response,
  isDisputed: (claim) => !!claim.response && claim.response.status === "disputed",
  awaitsCustomer: (claim) => !!claim.response && claim.response.status === "awaiting_response",
  /* What the customer wants beyond our offer. Null when they have not said. */
  gap: (claim) => (claim.response && claim.response.demandCents ? claim.response.demandCents - claim.settlement.amountCents : null),
};

const RECOVERY_FLAGS = {
  daysLeft: (recovery, now) => Math.round((recovery.fileByAt - now) / 864e5),
  isOpen: (recovery) => ["to_file", "filed", "negotiating"].indexOf(recovery.status) > -1,
  isClosed: (recovery) => !RECOVERY_FLAGS.isOpen(recovery),
  /* The act-ability predicate for recoveries — waiting on us to serve notice. */
  awaitsAdjuster: (recovery) => recovery.status === "to_file",
  shortfall: (recovery) => recovery.soughtCents - recovery.recoveredCents,
};

/* Transitions, named once and derived from the current status: one function per
   entity, called by the row action, the record header and the queue alike.
   kind: "primary" the record's normal path · "nav" moves to another screen ·
   "undo" instant + consequential, raises an undo toast · "confirm"
   irreversible, opens a confirm dialog · "form" opens a staged dialog. */
function claimTransitions(claim) {
  switch (claim.status) {
    case "awaiting_documents": return [{ id: "chase_documents", label: "Chase documents", kind: "undo", primary: true }];
    case "awaiting_survey": return [{ id: "chase_survey", label: "Chase surveyor", kind: "undo", primary: true }];
    case "ready_for_decision": return [
      { id: "record_decision", label: "Record decision", kind: "nav", primary: true },
      { id: "escalate", label: "Escalate to legal", kind: "confirm" },
    ];
    case "escalated_legal": return [{ id: "recall", label: "Recall from legal", kind: "confirm" }];
    /* The decision is with the manager. The adjuster cannot sign her own work,
       so neither transition here approves anything: she can chase it, or take
       it back and reopen the assessment. Both are reversible. */
    case "awaiting_signoff": return [
      { id: "chase_signoff", label: "Chase sign-off", kind: "undo", primary: true },
      { id: "withdraw_decision", label: "Withdraw decision", kind: "undo" },
    ];
    case "approved_awaiting_payment": return [{ id: "schedule_payment", label: "Schedule payment", kind: "undo", primary: true }];
    default: return [];
  }
}
function settlementTransitions(claim) {
  if (!claim.settlement) return [];
  switch (claim.settlement.status) {
    case "unscheduled": return [{ id: "schedule_payment", label: "Schedule payment", kind: "undo", primary: true }];
    case "returned": return [{ id: "schedule_payment", label: "Schedule payment", kind: "undo", primary: true }];
    case "scheduled": return [{ id: "send_payment", label: "Send payment", kind: "confirm", primary: true }];
    default: return [];
  }
}
function responseTransitions(claim) {
  if (!claim.response) return [];
  switch (claim.response.status) {
    case "awaiting_response": return [{ id: "record_response", label: "Record response", kind: "form", primary: true }];
    case "disputed": return [
      { id: "reopen_assessment", label: "Reopen assessment", kind: "undo", primary: true },
      { id: "escalate", label: "Escalate to legal", kind: "confirm" },
    ];
    default: return [];
  }
}
function recoveryTransitions(recovery) {  switch (recovery.status) {
    case "to_file": return [
      { id: "file_notice", label: "File notice", kind: "undo", primary: true },
      { id: "write_off", label: "Write off recovery", kind: "confirm", destructive: true },
    ];
    case "filed":
    case "negotiating": return [
      { id: "record_recovery", label: "Record recovery", kind: "form", primary: true },
      { id: "write_off", label: "Write off recovery", kind: "confirm", destructive: true },
    ];
    default: return [];
  }
}

/* One item of evidence. Requesting again is reversible; marking a document
   illegible is reversible too — it is a judgement, not a deletion. */
function documentTransitions(document) {
  switch (document.status) {
    case "requested": return [{ id: "request_again", label: "Request again", kind: "undo", primary: true }];
    case "illegible": return [{ id: "request_again", label: "Request again", kind: "undo", primary: true }];
    case "received": return [{ id: "mark_illegible", label: "Mark illegible", kind: "undo" }];
    default: return [];
  }
}

const STATUS_FIELDS = {
  "claim.status": { entity: "claim", field: "status", means: "Where the claim sits between filing and closure", values: CLAIM_STATUS },
  "document.status": { entity: "document", field: "status", means: "Whether one item of evidence is usable", values: DOCUMENT_STATUS },
  "settlement.status": { entity: "settlement", field: "status", means: "The payout of an approved claim", values: SETTLEMENT_STATUS },
  "response.status": { entity: "response", field: "status", means: "What the customer did with our offer", values: RESPONSE_STATUS },
  "recovery.status": { entity: "recovery", field: "status", means: "Pursuit of the party that caused the damage", values: RECOVERY_STATUS },
};

/* Coverage — values with no fixture yet, so nobody assumes they are unused:
   recovery.status `written_off`, reachable only by writing a recovery off,
   which the kit does. claim.status `awaiting_signoff` now has two fixtures
   (CLM-26-0195 and CLM-26-0207) behind the sign-off queue. Every other value in
   every field appears at least once in fixtures.js. */

Object.assign(window, {
  CLAIM_POLICY, CLAIM_STATUS, DOCUMENT_STATUS, SETTLEMENT_STATUS, RESPONSE_STATUS, RECOVERY_STATUS,
  DOCUMENT_TYPES, MANDATORY_DOCUMENT_TYPES, COUNTERPARTY_KINDS, REJECT_REASONS, DECISION_OUTCOMES,
  ALERT_KINDS, DESK_DEFAULTS,
  CLAIM_FLAGS, SETTLEMENT_FLAGS, RESPONSE_FLAGS, RECOVERY_FLAGS,
  claimTransitions, settlementTransitions, responseTransitions, recoveryTransitions, documentTransitions, STATUS_FIELDS,
});

</script>
<script type="text/babel" data-presets="react" data-file="nav.js">
/* Claims Desk — the route table.
   Deliberately NOT routes: the ⌘K palette, the notifications *panel* (the panel
   is a state of the bar; /notifications behind it is the destination), the row
   menus, the confirm dialogs, the record-response and record-recovery dialogs,
   the queue's tabs and text filter. They are states of a screen, not
   destinations — a breadcrumb that wants one of them is a bug in the screen.

   Five top-level destinations, so this product still runs the TOP-NAV shell.
   The claim's own three views (assessment, evidence, decision) and the four
   settings views are registered routes reached by tabs inside their own
   surface, never a second row of navigation. */

const CLAIMS_NAV = [
  { path: "/", label: "Work queue", badge: "claimsDecidable" },
  { path: "/signoff", label: "Sign-off", badge: "signoffWaiting" },
  { path: "/payments", label: "Payments", badge: "settlementsToSchedule" },
  { path: "/responses", label: "Responses", badge: "offersDisputed" },
  { path: "/recovery", label: "Recovery", badge: "recoveriesToFile" },

  /* Registered, not in the nav. */
  { path: "/claims/new", label: "New claim", parent: "/", offSidebar: true, group: "Create" },
  { path: "/claims/:id", label: "Claim", parent: "/", offSidebar: true },
  { path: "/claims/:id/evidence", label: "Evidence", parent: "/claims/:id", offSidebar: true },
  { path: "/claims/:id/decision", label: "Decision", parent: "/claims/:id", offSidebar: true },
  { path: "/recovery/new", label: "New recovery", parent: "/recovery", offSidebar: true, group: "Create" },
  { path: "/recovery/:id", label: "Recovery file", parent: "/recovery", offSidebar: true },
  /* A claimant is reached from any of its claims and from the palette, never
     from one destination, so it hangs under no parent and its breadcrumb is
     its own name. */
  { path: "/claimants/:id", label: "Claimant", parent: "/", offSidebar: true },
  { path: "/notifications", label: "Notifications", offSidebar: true },
  /* Settings: /settings IS the profile view, so the area has no empty namespace
     parent and no route resolving to a redirect. The other three hang off it
     and are reached by the tab strip. */
  { path: "/settings", label: "Settings", offSidebar: true },
  { path: "/settings/authority", label: "Authority", parent: "/settings", offSidebar: true },
  { path: "/settings/alerts", label: "Alerts", parent: "/settings", offSidebar: true },
  { path: "/settings/desk", label: "Desk", parent: "/settings", offSidebar: true },

  /* Auth namespace — excluded from the shell nav; no product chrome. */
  { path: "/signin", label: "Sign in", offSidebar: true },
  { path: "/signout", label: "Sign out", offSidebar: true },
];

const CLAIMS_ROUTES = indexRoutes(CLAIMS_NAV);

/* Projections the shell renders — derived by filtering the tree, never retyped. */
const CLAIMS_DESTINATIONS = CLAIMS_NAV.filter((r) => !r.offSidebar);

const CLAIMS_ACCOUNT_MENU = [
  CLAIMS_ROUTES["/settings"],
  CLAIMS_ROUTES["/signout"],
];

/* The claim's own views, in tab order. Registered routes, filtered from the
   tree — the tab strip never carries a label the route table does not have. */
const CLAIM_VIEWS = [
  { value: "assessment", route: CLAIMS_ROUTES["/claims/:id"], label: "Assessment" },
  { value: "evidence", route: CLAIMS_ROUTES["/claims/:id/evidence"], label: "Evidence" },
  { value: "decision", route: CLAIMS_ROUTES["/claims/:id/decision"], label: "Decision" },
];

/* The settings views, same device. The first tab's route is /settings itself,
   whose table label is the area's name rather than the view's. */
const SETTINGS_VIEWS = [
  { value: "profile", route: CLAIMS_ROUTES["/settings"], label: "Profile" },
  { value: "authority", route: CLAIMS_ROUTES["/settings/authority"], label: "Authority" },
  { value: "alerts", route: CLAIMS_ROUTES["/settings/alerts"], label: "Alerts" },
  { value: "desk", route: CLAIMS_ROUTES["/settings/desk"], label: "Desk" },
];

/* Paths that render without product chrome (AGENTS §7.1). */
const CLAIMS_AUTH_PATHS = ["/signin", "/signout"];

/* The preview menu's page list — this table read whole and grouped, never
   retyped, with the parameterised routes resolved against the fixture so the
   link lands on a real record rather than on ":id". A pattern with no fixture
   example is left out rather than offered as a link resolving nowhere. */
const CLAIMS_PAGES = (data) => {
  const claim = data.claims[0], recovery = data.recoveries[0];
  const claimant = claim ? (data.claimants || []).find((p) => p.name === claim.customer) : null;
  return templatePages(CLAIMS_NAV, CLAIMS_AUTH_PATHS, {
    "/claims/:id": claim ? "/claims/" + claim.id : null,
    "/claims/:id/evidence": claim ? "/claims/" + claim.id + "/evidence" : null,
    "/claims/:id/decision": claim ? "/claims/" + claim.id + "/decision" : null,
    "/recovery/:id": recovery ? "/recovery/" + recovery.id : null,
    "/claimants/:id": claimant ? "/claimants/" + claimant.id : null,
  });
};

Object.assign(window, { CLAIMS_AUTH_PATHS, CLAIMS_NAV, CLAIMS_ROUTES, CLAIMS_DESTINATIONS, CLAIMS_ACCOUNT_MENU, CLAIM_VIEWS, SETTINGS_VIEWS, CLAIMS_PAGES });

</script>
<script type="text/babel" data-presets="react" data-file="fixtures.js">
/* Claims Desk — the one fixture set. Every screen reads these arrays; a record
   that appears twice is this entry read twice, never a second literal written
   to match. Timestamps are offsets from load, so every time bar, countdown and
   date column is true whenever the kit is opened.

   Narrative check: the notifications, the file notes and the recovery
   correspondence below all name real records — CLM-26-0179, CLM-25-0977,
   CLM-25-0913, CLM-26-0207, CLM-25-0929, REC-26-0038, REC-26-0022 — and agree
   with their status, amounts and clocks at the timestamp they carry. */

const CLAIMS_NOW = Date.now();
const D = 864e5;
const dAgo = (days) => CLAIMS_NOW - days * D;
const dAhead = (days) => CLAIMS_NOW + days * D;

const CLAIMS_USER = { name: "Imogen Petrakis", role: "Cargo claims adjuster", email: "i.petrakis@corvenline.example", avatar: window.__resources.userAvatar, alerts: ["time_bar", "documents"] };
const CLAIMS_MANAGER = { name: "Roland Achebe", role: "Claims manager" };
const CLAIMS_CARRIER = { name: "Corven Line", desk: "Claims desk", region: "North Atlantic" };

const doc = (type, status, receivedAt, note) => ({ type: type, status: status, receivedAt: receivedAt || null, note: note || null });

/* Every claim carries its cargo facts, the customer's claimed figure, the
   adjuster's adjustments (null until the file has been assessed), its evidence,
   and — once decided — its settlement and the customer's response.
   deliveredAt is the only clock stored: the time bar is derived from it. */
const CLAIMS = [
  {
    id: "CLM-26-0188", customer: "Ravenna Machine Tools", claimedAt: dAgo(138), deliveredAt: dAgo(151),
    bl: "CRVN-2604-88192", container: "MSKU 4417820", vessel: "Tanager Bay", voyage: "214E",
    commodity: "CNC lathe assemblies", packages: 24, weightKg: 18400,
    pol: "Genoa", pod: "Charleston",
    incident: "Lashing failure in hold 3 — six pallets crushed against the bulkhead",
    status: "ready_for_decision", claimedCents: 12840000,
    assessment: { depreciationCents: 1284000, salvageCents: 3150000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(6) },
    documents: [
      doc("port_photos", "received", dAgo(112), "41 frames, Charleston terminal"),
      doc("surveyor_report", "received", dAgo(38), "Cawley Marine, survey held 12 days after discharge"),
      doc("packing_list", "received", dAgo(120)),
      doc("commercial_invoice", "received", dAgo(120)),
      doc("delivery_receipt", "received", dAgo(126), "Signed with exception: \"6 pallets visibly crushed, contents unchecked\""),
    ],
    notes: [
      { at: dAgo(6), who: "Imogen Petrakis", text: "Assessed on the surveyor's figures. Betterment applied at 10% — the units were two years into a seven-year life." },
      { at: dAgo(34), who: "Cawley Marine", text: "Survey report filed. Damage consistent with lashing failure, not pre-shipment handling." },
    ],
  },
  {
    id: "CLM-26-0201", customer: "Fen & Marrow Foods", claimedAt: dAgo(84), deliveredAt: dAgo(96),
    bl: "CRVN-2605-11740", container: "TCLU 9930411", vessel: "Ardent Fen", voyage: "118W",
    commodity: "Tinned preserves", packages: 480, weightKg: 9600,
    pol: "Valencia", pod: "Newark",
    incident: "Water ingress through a failed door gasket — 62 cartons pulped",
    status: "ready_for_decision", claimedCents: 1860000,
    assessment: { depreciationCents: 0, salvageCents: 240000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(3) },
    documents: [
      doc("port_photos", "received", dAgo(80)),
      doc("surveyor_report", "received", dAgo(21), "Vandermeer Adjusters"),
      doc("packing_list", "received", dAgo(82)),
      doc("commercial_invoice", "received", dAgo(82)),
      doc("delivery_receipt", "received", dAgo(88), "Signed with exception: \"one pallet wet, carton damage\""),
    ],
  },
  {
    id: "CLM-26-0195", customer: "Ravenna Machine Tools", claimedAt: dAgo(92), deliveredAt: dAgo(104),
    bl: "CRVN-2605-30447", container: "MSKU 6620713", vessel: "Kestrel Fen", voyage: "119W",
    commodity: "Tooling and collet sets", packages: 340, weightKg: 22400,
    pol: "Genoa", pod: "Charleston",
    incident: "Seawater ingress through a split roof panel — 96 boxes corroded",
    status: "awaiting_signoff", claimedCents: 9650000,
    assessment: { depreciationCents: 480000, salvageCents: 620000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(9) },
    decision: { outcome: "approve_full", decidedAt: dAgo(4), decidedBy: "Imogen Petrakis", signedOffBy: null, justification: null },
    documents: [
      doc("port_photos", "received", dAgo(100)),
      doc("surveyor_report", "received", dAgo(30), "Cawley Marine — chloride testing on 40 sampled boxes"),
      doc("packing_list", "received", dAgo(101)),
      doc("commercial_invoice", "received", dAgo(101)),
      doc("delivery_receipt", "received", dAgo(102), "Signed with exception: \"upper tier boxes wet and rust stained\""),
    ],
    notes: [
      { at: dAgo(4), who: "Imogen Petrakis", text: "Approved in full at $80,500.00 and sent to Roland Achebe for sign-off — above my $25,000.00 authority." },
    ],
  },
  {
    id: "CLM-26-0207", customer: "Ferrymead Automotive", claimedAt: dAgo(60), deliveredAt: dAgo(72),
    bl: "CRVN-2606-10884", container: "FCIU 8830220", vessel: "Tanager Bay", voyage: "217E",
    commodity: "Gearbox castings", packages: 120, weightKg: 19200,
    pol: "Bremerhaven", pod: "Jacksonville",
    incident: "Crates dropped from the second tier during restow — 29 castings cracked",
    status: "awaiting_signoff", claimedCents: 5480000,
    assessment: { depreciationCents: 274000, salvageCents: 0, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(16) },
    decision: { outcome: "approve_part", decidedAt: dAgo(11), decidedBy: "Imogen Petrakis", signedOffBy: null, justification: "Twenty-nine of the one hundred and twenty crates are cracked; the remainder were surveyed as sound. Betterment of 5% is applied because the castings had been held in inventory for a year before shipment." },
    documents: [
      doc("port_photos", "received", dAgo(68)),
      doc("surveyor_report", "received", dAgo(40), "Atlantic Marine Consultants — the same surveyor as CLM-25-0913"),
      doc("packing_list", "received", dAgo(69)),
      doc("commercial_invoice", "received", dAgo(69)),
      doc("delivery_receipt", "received", dAgo(70), "Signed with exception: \"crate banding broken, castings visible\""),
    ],
    notes: [
      { at: dAgo(11), who: "Imogen Petrakis", text: "Approved in part at $47,060.00 and sent to Roland Achebe for sign-off." },
      { at: dAgo(3), who: "Imogen Petrakis", text: "Chased the sign-off — the consignee is asking for a date and this is their second claim on the same lane." },
    ],
  },
  {
    id: "CLM-26-0179", customer: "Aurelian Tile Imports", claimedAt: dAgo(174), deliveredAt: dAgo(188),
    bl: "CRVN-2603-70055", container: "HLXU 2288104", vessel: "Tanager Bay", voyage: "209E",
    commodity: "Glazed floor tile", packages: 1240, weightKg: 24600,
    pol: "Castellón", pod: "Savannah",
    incident: "Stack collapse in transit — 310 boxes broken",
    status: "ready_for_decision", claimedCents: 4125000, assessment: null,
    documents: [
      doc("port_photos", "received", dAgo(150)),
      doc("surveyor_report", "illegible", dAgo(9), "Scan is a photograph of a screen — pages 3–7 unreadable, re-requested"),
      doc("packing_list", "received", dAgo(160)),
      doc("commercial_invoice", "received", dAgo(160)),
      doc("delivery_receipt", "received", dAgo(166), "Clean receipt — damage reported 2 days after delivery"),
    ],
    notes: [
      { at: dAgo(9), who: "Imogen Petrakis", text: "Surveyor's report rejected as illegible. Cannot assess breakage percentage without pages 3–7." },
    ],
  },
  {
    id: "CLM-26-0212", customer: "Kettleworth Brewing", claimedAt: dAgo(52), deliveredAt: dAgo(60),
    bl: "CRVN-2606-40318", container: "CMAU 5510992", vessel: "Kestrel Fen", voyage: "121W",
    commodity: "Cask ale in kegs", packages: 96, weightKg: 5760,
    pol: "Liverpool", pod: "Baltimore",
    incident: "Twelve kegs punctured by a shifted stillage",
    status: "awaiting_documents", claimedCents: 2278000, assessment: null,
    documents: [
      doc("port_photos", "received", dAgo(48)),
      doc("surveyor_report", "received", dAgo(30), "Northgate Marine"),
      doc("packing_list", "requested", null, "Requested twice — chased 6 days ago"),
      doc("commercial_invoice", "requested", null, "Requested 18 days ago"),
      doc("delivery_receipt", "received", dAgo(54)),
    ],
  },
  {
    id: "CLM-26-0166", customer: "Silbury Ceramics", claimedAt: dAgo(200), deliveredAt: dAgo(212),
    bl: "CRVN-2602-99120", container: "OOLU 7712035", vessel: "Ardent Fen", voyage: "112W",
    commodity: "Sanitaryware", packages: 310, weightKg: 16800,
    pol: "Leixões", pod: "Houston",
    incident: "Container dropped during discharge — full stack fractured",
    status: "awaiting_survey", claimedCents: 9650000, assessment: null,
    documents: [
      doc("port_photos", "received", dAgo(190)),
      doc("surveyor_report", "requested", null, "Joint survey booked with the terminal for next week"),
      doc("packing_list", "received", dAgo(195)),
      doc("commercial_invoice", "received", dAgo(195)),
      doc("delivery_receipt", "received", dAgo(198), "Signed with exception: \"container corner post deformed\""),
    ],
  },
  {
    id: "CLM-26-0154", customer: "Trelawney Paper", claimedAt: dAgo(228), deliveredAt: dAgo(240),
    bl: "CRVN-2601-33871", container: "MSCU 1180644", vessel: "Kestrel Fen", voyage: "108W",
    commodity: "Coated paper reels", packages: 44, weightKg: 21120,
    pol: "Gothenburg", pod: "Norfolk",
    incident: "Condensation damage to outer wraps of 11 reels",
    status: "awaiting_documents", claimedCents: 894000, assessment: null,
    documents: [
      doc("port_photos", "requested", null, "Customer's photographs never supplied"),
      doc("surveyor_report", "received", dAgo(180), "Bergen Cargo Survey"),
      doc("packing_list", "received", dAgo(210)),
      doc("commercial_invoice", "received", dAgo(210)),
      doc("delivery_receipt", "requested", null, "Consignee cannot locate the signed copy"),
    ],
  },
  {
    id: "CLM-25-0977", customer: "Vosges Cold Chain", claimedAt: dAgo(306), deliveredAt: dAgo(318),
    bl: "CRVN-2510-20447", container: "MWCU 6650180", vessel: "Tanager Bay", voyage: "198E",
    commodity: "Frozen confectionery", packages: 20, weightKg: 22000,
    pol: "Antwerp", pod: "Miami",
    incident: "Reefer compressor failure for 61 hours — full load condemned",
    status: "escalated_legal", claimedCents: 31000000,
    assessment: { depreciationCents: 0, salvageCents: 0, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(96) },
    documents: [
      doc("port_photos", "received", dAgo(300)),
      doc("surveyor_report", "received", dAgo(280), "Delacroix & Fils, with the reefer data logger download"),
      doc("packing_list", "received", dAgo(302)),
      doc("commercial_invoice", "received", dAgo(302)),
      doc("delivery_receipt", "received", dAgo(304), "Signed with exception: \"load temperature +8°C on discharge\""),
    ],
    notes: [
      { at: dAgo(74), who: "Imogen Petrakis", text: "Escalated. The claimant argues the limitation of liability is broken by unseaworthiness — a reefer pre-trip inspection was skipped. Legal holding the file." },
    ],
  },
  {
    id: "CLM-26-0143", customer: "Nordkapp Seafood", claimedAt: dAgo(245), deliveredAt: dAgo(257),
    bl: "CRVN-2512-88710", container: "MWCU 4410277", vessel: "Ardent Fen", voyage: "104W",
    commodity: "Frozen cod fillet", packages: 900, weightKg: 19800,
    pol: "Ålesund", pod: "Boston",
    incident: "Set-point drift — 140 cartons out of specification",
    status: "approved_awaiting_payment", claimedCents: 7430000,
    assessment: { depreciationCents: 371500, salvageCents: 890000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(20) },
    decision: { outcome: "approve_full", decidedAt: dAgo(14), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "unscheduled", amountCents: 5668500 },
    documents: [
      doc("port_photos", "received", dAgo(240)),
      doc("surveyor_report", "received", dAgo(200), "Nordic Marine Survey"),
      doc("packing_list", "received", dAgo(242)),
      doc("commercial_invoice", "received", dAgo(242)),
      doc("delivery_receipt", "received", dAgo(244), "Signed with exception: \"pulp temperatures above specification\""),
    ],
  },
  {
    id: "CLM-26-0131", customer: "Pallas Glassworks", claimedAt: dAgo(288), deliveredAt: dAgo(300),
    bl: "CRVN-2511-51204", container: "TGHU 3390118", vessel: "Kestrel Fen", voyage: "099W",
    commodity: "Laboratory glassware", packages: 210, weightKg: 4200,
    pol: "Piraeus", pod: "Philadelphia",
    incident: "Forklift strike at the terminal — 38 cases broken",
    status: "approved_awaiting_payment", claimedCents: 1680000,
    assessment: { depreciationCents: 0, salvageCents: 132000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(40) },
    decision: { outcome: "approve_full", decidedAt: dAgo(32), decidedBy: "Imogen Petrakis", signedOffBy: null, justification: null },
    settlement: { status: "scheduled", amountCents: 1048000, scheduledFor: dAhead(3), reference: "PAY-26-3402" },
    documents: [
      doc("port_photos", "received", dAgo(285)),
      doc("surveyor_report", "received", dAgo(270), "Keystone Marine"),
      doc("packing_list", "received", dAgo(286)),
      doc("commercial_invoice", "received", dAgo(286)),
      doc("delivery_receipt", "received", dAgo(288), "Signed with exception: \"38 cases crushed\""),
    ],
  },
  {
    id: "CLM-25-0929", customer: "Vollmer Interiors", claimedAt: dAgo(276), deliveredAt: dAgo(288),
    bl: "CRVN-2511-19983", container: "SEGU 8820156", vessel: "Tanager Bay", voyage: "201E",
    commodity: "Upholstered seating", packages: 130, weightKg: 7800,
    pol: "Naples", pod: "New York",
    incident: "Rainwater ingress through a damaged roof panel",
    status: "approved_awaiting_payment", claimedCents: 3400000,
    assessment: { depreciationCents: 340000, salvageCents: 0, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(60) },
    decision: { outcome: "approve_full", decidedAt: dAgo(52), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "returned", amountCents: 2560000, scheduledFor: dAgo(12), sentAt: dAgo(9), returnedAt: dAgo(4), returnReason: "IBAN rejected by the beneficiary bank", reference: "PAY-26-3286" },
    documents: [
      doc("port_photos", "received", dAgo(270)),
      doc("surveyor_report", "received", dAgo(250), "Hudson Marine"),
      doc("packing_list", "received", dAgo(272)),
      doc("commercial_invoice", "received", dAgo(272)),
      doc("delivery_receipt", "received", dAgo(274), "Signed with exception: \"cartons water stained\""),
    ],
  },
  {
    id: "CLM-25-0942", customer: "Dunmore Textiles", claimedAt: dAgo(198), deliveredAt: dAgo(210),
    bl: "CRVN-2601-70228", container: "APZU 6610338", vessel: "Ardent Fen", voyage: "110W",
    commodity: "Woven furnishing fabric", packages: 620, weightKg: 12400,
    pol: "İzmir", pod: "Charleston",
    incident: "Rolls chafed through by an unsecured stow",
    status: "paid", claimedCents: 8900000,
    assessment: { depreciationCents: 890000, salvageCents: 1750000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(40) },
    decision: { outcome: "approve_full", decidedAt: dAgo(30), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "sent", amountCents: 5760000, scheduledFor: dAgo(4), sentAt: dAgo(2), reference: "PAY-26-3318" },
    response: { status: "awaiting_response", offerSentAt: dAgo(2) },
    recoveryId: "REC-26-0044",
    documents: [
      doc("port_photos", "received", dAgo(195)),
      doc("surveyor_report", "received", dAgo(170), "Levant Cargo Control"),
      doc("packing_list", "received", dAgo(196)),
      doc("commercial_invoice", "received", dAgo(196)),
      doc("delivery_receipt", "received", dAgo(198), "Signed with exception: \"outer rolls chafed\""),
    ],
  },
  {
    id: "CLM-25-0913", customer: "Ferrymead Automotive", claimedAt: dAgo(240), deliveredAt: dAgo(252),
    bl: "CRVN-2512-30991", container: "FCIU 2201884", vessel: "Kestrel Fen", voyage: "101W",
    commodity: "Crated engine components", packages: 168, weightKg: 26400,
    pol: "Bremerhaven", pod: "Jacksonville",
    incident: "Dropped container at the terminal — 11 crates crushed",
    status: "paid", claimedCents: 21040000,
    assessment: { depreciationCents: 2104000, salvageCents: 4046000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(70) },
    decision: { outcome: "approve_part", decidedAt: dAgo(60), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: "Eleven of the one hundred and sixty-eight crates are crushed; the rest were surveyed as sound and returned to stock. Betterment of 10% is applied because the castings were held in inventory for two years before shipment, and the scrap value realised by your buyer is credited back." },
    settlement: { status: "cleared", amountCents: 14390000, scheduledFor: dAgo(40), sentAt: dAgo(32), clearedAt: dAgo(26), reference: "PAY-26-3140" },
    response: { status: "disputed", offerSentAt: dAgo(32), respondedAt: dAgo(12), demandCents: 21040000, note: "The consignee rejects the betterment deduction and the salvage credit, and holds the carrier to the full invoice value of all one hundred and sixty-eight crates." },
    recoveryId: "REC-26-0038",
    documents: [
      doc("port_photos", "received", dAgo(235)),
      doc("surveyor_report", "received", dAgo(210), "Atlantic Marine Consultants"),
      doc("packing_list", "received", dAgo(236)),
      doc("commercial_invoice", "received", dAgo(236)),
      doc("delivery_receipt", "received", dAgo(238), "Signed with exception: \"container dropped, contents unchecked\""),
    ],
  },
  {
    id: "CLM-25-0908", customer: "Larkspur Wine Imports", claimedAt: dAgo(268), deliveredAt: dAgo(280),
    bl: "CRVN-2511-88034", container: "TRLU 5580227", vessel: "Ardent Fen", voyage: "098W",
    commodity: "Bottled wine", packages: 1100, weightKg: 16500,
    pol: "Sète", pod: "Baltimore",
    incident: "Heat damage — container stowed above deck without protection",
    status: "paid", claimedCents: 7900000,
    assessment: { depreciationCents: 0, salvageCents: 1260000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(80) },
    decision: { outcome: "approve_full", decidedAt: dAgo(70), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "cleared", amountCents: 6140000, scheduledFor: dAgo(60), sentAt: dAgo(52), clearedAt: dAgo(40), reference: "PAY-26-3061" },
    response: { status: "accepted", offerSentAt: dAgo(52), respondedAt: dAgo(46) },
    recoveryId: "REC-26-0022",
    documents: [
      doc("port_photos", "received", dAgo(265)),
      doc("surveyor_report", "received", dAgo(240), "Midi Cargo Survey"),
      doc("packing_list", "received", dAgo(266)),
      doc("commercial_invoice", "received", dAgo(266)),
      doc("delivery_receipt", "received", dAgo(268), "Signed with exception: \"bottles cooked, labels lifted\""),
    ],
  },
  {
    id: "CLM-25-0889", customer: "Ostend Chemicals", claimedAt: dAgo(300), deliveredAt: dAgo(312),
    bl: "CRVN-2510-66218", container: "DFSU 9910556", vessel: "Kestrel Fen", voyage: "094W",
    commodity: "Drummed lubricant", packages: 80, weightKg: 16000,
    pol: "Zeebrugge", pod: "Savannah",
    incident: "Drums punctured by a road trailer's tie-down",
    status: "paid", claimedCents: 3450000,
    assessment: { depreciationCents: 0, salvageCents: 275000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(110) },
    decision: { outcome: "approve_full", decidedAt: dAgo(100), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "cleared", amountCents: 2675000, scheduledFor: dAgo(80), sentAt: dAgo(70), clearedAt: dAgo(54), reference: "PAY-26-2903" },
    response: { status: "accepted", offerSentAt: dAgo(70), respondedAt: dAgo(64) },
    recoveryId: "REC-26-0031",
    documents: [
      doc("port_photos", "received", dAgo(298)),
      doc("surveyor_report", "received", dAgo(280), "Palmetto Marine"),
      doc("packing_list", "received", dAgo(299)),
      doc("commercial_invoice", "received", dAgo(299)),
      doc("delivery_receipt", "received", dAgo(300), "Signed with exception: \"two drums leaking on arrival\""),
    ],
  },
  {
    id: "CLM-25-0851", customer: "Halvard Steel", claimedAt: dAgo(330), deliveredAt: dAgo(342),
    bl: "CRVN-2509-11209", container: "MSKU 7730448", vessel: "Tanager Bay", voyage: "189E",
    commodity: "Cold-rolled coil", packages: 16, weightKg: 25600,
    pol: "Rotterdam", pod: "Mobile",
    incident: "Coils shifted in transit — edge damage from inadequate cradling",
    status: "paid", claimedCents: 11200000,
    assessment: { depreciationCents: 0, salvageCents: 1900000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(140) },
    decision: { outcome: "approve_full", decidedAt: dAgo(130), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "cleared", amountCents: 6860800, scheduledFor: dAgo(110), sentAt: dAgo(100), clearedAt: dAgo(88), reference: "PAY-26-2744" },
    response: { status: "accepted", offerSentAt: dAgo(100), respondedAt: dAgo(94) },
    recoveryId: "REC-25-0996",
    documents: [
      doc("port_photos", "received", dAgo(328)),
      doc("surveyor_report", "received", dAgo(310), "Gulf Marine Survey"),
      doc("packing_list", "received", dAgo(329)),
      doc("commercial_invoice", "received", dAgo(329)),
      doc("delivery_receipt", "received", dAgo(330), "Signed with exception: \"coil edges deformed\""),
    ],
  },
  {
    id: "CLM-25-0961", customer: "Silbury Ceramics", claimedAt: dAgo(210), deliveredAt: dAgo(222),
    bl: "CRVN-2601-58003", container: "OOLU 4420917", vessel: "Tanager Bay", voyage: "205E",
    commodity: "Vitreous china basins", packages: 190, weightKg: 11400,
    pol: "Leixões", pod: "Houston",
    incident: "Reach-stacker strike on the container door — 47 units broken",
    status: "paid", claimedCents: 4180000,
    assessment: { depreciationCents: 0, salvageCents: 310000, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(130) },
    decision: { outcome: "approve_full", decidedAt: dAgo(120), decidedBy: "Imogen Petrakis", signedOffBy: "Roland Achebe", justification: null },
    settlement: { status: "cleared", amountCents: 3370000, scheduledFor: dAgo(100), sentAt: dAgo(92), clearedAt: dAgo(80), reference: "PAY-26-2988" },
    response: { status: "accepted", offerSentAt: dAgo(92), respondedAt: dAgo(86) },
    /* Deliberately no recoveryId: the terminal that struck the door has never
       been pursued, which is what makes this the file a new recovery opens
       against. */
    documents: [
      doc("port_photos", "received", dAgo(206)),
      doc("surveyor_report", "received", dAgo(190), "Bayou Marine Survey — terminal incident report attached"),
      doc("packing_list", "received", dAgo(207)),
      doc("commercial_invoice", "received", dAgo(207)),
      doc("delivery_receipt", "received", dAgo(208), "Signed with exception: \"door panel stove in, contents broken\""),
    ],
  },
  {
    id: "CLM-25-0864", customer: "Marlowe Furniture", claimedAt: dAgo(318), deliveredAt: dAgo(330),
    bl: "CRVN-2509-40772", container: "CAIU 3320991", vessel: "Ardent Fen", voyage: "091W",
    commodity: "Flat-pack cabinetry", packages: 260, weightKg: 10400,
    pol: "Gdańsk", pod: "Norfolk",
    incident: "Scuffing and corner damage reported on 44 cartons",
    status: "rejected", claimedCents: 3340000,
    assessment: { depreciationCents: 0, salvageCents: 0, deductibleCents: 500000, assessedBy: "Imogen Petrakis", assessedAt: dAgo(46) },
    decision: { outcome: "reject", decidedAt: dAgo(38), decidedBy: "Imogen Petrakis", signedOffBy: null, rejectReason: "pre_shipment_damage", justification: "The surveyor's report and the packing photographs taken at Gdańsk both show corner crushing before the container was sealed. The delivery receipt was signed clean, and the damage described matches the pre-shipment condition, so the loss did not occur in the carrier's custody." },
    documents: [
      doc("port_photos", "received", dAgo(316)),
      doc("surveyor_report", "received", dAgo(300), "Baltic Marine Survey"),
      doc("packing_list", "received", dAgo(317)),
      doc("commercial_invoice", "received", dAgo(317)),
      doc("delivery_receipt", "received", dAgo(318), "Clean receipt, no exception noted"),
    ],
  },
];

/* Recoveries — the mirror image, with their own counterparties, their own
   deadlines and their own money. soughtCents always reads the settlement the
   claim actually paid, so the two sides can never drift apart. */
const RECOVERIES = [
  {
    id: "REC-26-0044", claimId: "CLM-25-0942", counterparty: { name: "Quayside Lashing Services", kind: "lashing", contact: "claims@quaysidelashing.example" },
    basis: "Terminal lashing contract, clause 11 — notice within 40 days of payment",
    soughtCents: 5760000, recoveredCents: 0, offerCents: null,
    status: "to_file", openedAt: dAgo(2), fileByAt: dAhead(38),
    log: [{ at: dAgo(2), who: "Imogen Petrakis", text: "Recovery opened against the lashing contractor on payment of CLM-25-0942. Notice not yet served." }],
  },
  {
    id: "REC-26-0038", claimId: "CLM-25-0913", counterparty: { name: "Meridian Terminal Services", kind: "terminal", contact: "liability@meridianterminal.example" },
    basis: "Terminal handling agreement, clause 8 — 12 months from the incident",
    soughtCents: 14390000, recoveredCents: 0, offerCents: null,
    status: "filed", openedAt: dAgo(24), noticeServedAt: dAgo(18), fileByAt: dAhead(160),
    log: [
      { at: dAgo(24), who: "Imogen Petrakis", text: "Recovery opened on payment of CLM-25-0913. Terminal's own incident report names the reach-stacker operator." },
      { at: dAgo(18), who: "Imogen Petrakis", text: "Notice of claim served with the survey report and the settlement proof of $143,900.00." },
      { at: dAgo(5), who: "Meridian Terminal Services", text: "Acknowledged. Liability under review by their insurer; response promised within 30 days." },
    ],
  },
  {
    id: "REC-26-0022", claimId: "CLM-25-0908", counterparty: { name: "Meridian Terminal Services", kind: "terminal", contact: "liability@meridianterminal.example" },
    basis: "Terminal handling agreement, clause 8 — 12 months from the incident",
    soughtCents: 6140000, recoveredCents: 0, offerCents: 3400000,
    status: "negotiating", openedAt: dAgo(64), noticeServedAt: dAgo(58), fileByAt: dAhead(88),
    log: [
      { at: dAgo(58), who: "Imogen Petrakis", text: "Notice served. Above-deck stowage was booked by the terminal against our stowage plan." },
      { at: dAgo(20), who: "Meridian Terminal Services", text: "Liability admitted in part. Offer of $34,000.00 in full and final settlement." },
      { at: dAgo(9), who: "Imogen Petrakis", text: "Offer rejected as below the surveyed loss. Counter-proposal of $52,000.00 sent." },
    ],
  },
  {
    id: "REC-26-0031", claimId: "CLM-25-0889", counterparty: { name: "Brandt Haulage", kind: "haulier", contact: "ops@brandthaulage.example" },
    basis: "Haulage subcontract, CMR liability",
    soughtCents: 2675000, recoveredCents: 1920000, offerCents: 1920000,
    status: "recovered", openedAt: dAgo(50), noticeServedAt: dAgo(46), fileByAt: dAhead(20), closedAt: dAgo(12),
    log: [
      { at: dAgo(46), who: "Imogen Petrakis", text: "Notice served. Tie-down puncture is squarely the haulier's." },
      { at: dAgo(12), who: "Brandt Haulage", text: "Settled at $19,200.00 — CMR weight limitation applied to the drummed weight." },
    ],
  },
  {
    id: "REC-25-0996", claimId: "CLM-25-0851", counterparty: { name: "Halland Packing Co", kind: "packer", contact: "claims@hallandpacking.example" },
    basis: "Stuffing services agreement, clause 6",
    soughtCents: 6860800, recoveredCents: 0, offerCents: null,
    status: "denied", openedAt: dAgo(80), noticeServedAt: dAgo(74), fileByAt: dAgo(10), closedAt: dAgo(40),
    log: [
      { at: dAgo(74), who: "Imogen Petrakis", text: "Notice served — cradling was specified in the stuffing instructions." },
      { at: dAgo(40), who: "Halland Packing Co", text: "Liability denied. They rely on the carrier's stowage plan having been altered after stuffing." },
    ],
  },
];

/* Claimants — the counterparty on the customer's side. Holds only what a claim
   cannot: who to write to, when they first shipped with us, their main lane and
   the terms they book on. Everything else about a claimant — what they have
   claimed, what we have paid, what they have disputed — is derived from CLAIMS
   by name, which is the join key. */
const CLAIMANTS = [
  { id: "ravenna-machine-tools", name: "Ravenna Machine Tools", contact: "Lucia Ferretti", email: "l.ferretti@ravennatools.example", since: dAgo(1580), lane: "Genoa → Charleston", terms: "Named account · contract of affreightment" },
  { id: "fen-marrow-foods", name: "Fen & Marrow Foods", contact: "Owen Brackley", email: "o.brackley@fenmarrow.example", since: dAgo(720), lane: "Valencia → Newark", terms: "Spot booking · bill of lading terms" },
  { id: "aurelian-tile-imports", name: "Aurelian Tile Imports", contact: "Nuria Palau", email: "n.palau@aureliantile.example", since: dAgo(2100), lane: "Castellón → Savannah", terms: "Named account · contract of affreightment" },
  { id: "kettleworth-brewing", name: "Kettleworth Brewing", contact: "Sam Ackroyd", email: "s.ackroyd@kettleworth.example", since: dAgo(410), lane: "Liverpool → Baltimore", terms: "Spot booking · bill of lading terms" },
  { id: "silbury-ceramics", name: "Silbury Ceramics", contact: "Inês Cardoso", email: "i.cardoso@silburyceramics.example", since: dAgo(1240), lane: "Leixões → Houston", terms: "Named account · bill of lading terms" },
  { id: "trelawney-paper", name: "Trelawney Paper", contact: "Erik Sandell", email: "e.sandell@trelawneypaper.example", since: dAgo(2600), lane: "Gothenburg → Norfolk", terms: "Named account · contract of affreightment" },
  { id: "vosges-cold-chain", name: "Vosges Cold Chain", contact: "Camille Rouart", email: "c.rouart@vosgescold.example", since: dAgo(1890), lane: "Antwerp → Miami", terms: "Reefer contract · pre-trip inspection clause" },
  { id: "nordkapp-seafood", name: "Nordkapp Seafood", contact: "Håkon Vik", email: "h.vik@nordkappseafood.example", since: dAgo(980), lane: "Ålesund → Boston", terms: "Reefer contract · pre-trip inspection clause" },
  { id: "pallas-glassworks", name: "Pallas Glassworks", contact: "Dimitra Solaki", email: "d.solaki@pallasglass.example", since: dAgo(1450), lane: "Piraeus → Philadelphia", terms: "Spot booking · bill of lading terms" },
  { id: "vollmer-interiors", name: "Vollmer Interiors", contact: "Greta Vollmer", email: "g.vollmer@vollmerinteriors.example", since: dAgo(640), lane: "Naples → New York", terms: "Spot booking · bill of lading terms" },
  { id: "dunmore-textiles", name: "Dunmore Textiles", contact: "Ayla Demir", email: "a.demir@dunmoretextiles.example", since: dAgo(2240), lane: "İzmir → Charleston", terms: "Named account · contract of affreightment" },
  { id: "ferrymead-automotive", name: "Ferrymead Automotive", contact: "Dale Okonkwo", email: "d.okonkwo@ferrymeadauto.example", since: dAgo(3100), lane: "Bremerhaven → Jacksonville", terms: "Named account · contract of affreightment" },
  { id: "larkspur-wine-imports", name: "Larkspur Wine Imports", contact: "Margaux Pell", email: "m.pell@larkspurwine.example", since: dAgo(870), lane: "Sète → Baltimore", terms: "Spot booking · bill of lading terms" },
  { id: "ostend-chemicals", name: "Ostend Chemicals", contact: "Joris Vandael", email: "j.vandael@ostendchem.example", since: dAgo(1720), lane: "Zeebrugge → Savannah", terms: "Named account · dangerous goods addendum" },
  { id: "halvard-steel", name: "Halvard Steel", contact: "Nils Halvard", email: "n.halvard@halvardsteel.example", since: dAgo(3400), lane: "Rotterdam → Mobile", terms: "Named account · contract of affreightment" },
  { id: "marlowe-furniture", name: "Marlowe Furniture", contact: "Piotr Marlowe", email: "p.marlowe@marlowefurniture.example", since: dAgo(520), lane: "Gdańsk → Norfolk", terms: "Spot booking · bill of lading terms" },
];

/* Notifications. Every one names a record and agrees with it at the timestamp
   it carries; `kind` is a value of ALERT_KINDS, so the alerts a person chooses
   in settings and the things this desk actually raises are one vocabulary. */
const NOTIFICATIONS = [
  { id: "ntf_9", kind: "documents", title: "Surveyor's report rejected as illegible", body: "CLM-26-0179 · Aurelian Tile Imports — pages 3–7 unreadable", at: dAgo(0.25), route: "/claims/CLM-26-0179/evidence" },
  { id: "ntf_8", kind: "signoff", title: "Sign-off still outstanding after 11 days", body: "CLM-26-0207 · Ferrymead Automotive — with Roland Achebe", at: dAgo(0.5), route: "/signoff" },
  { id: "ntf_7", kind: "time_bar", title: "Time bar inside 60 days", body: "CLM-25-0977 · Vosges Cold Chain — with legal since 74 days ago", at: dAgo(0.85), route: "/claims/CLM-25-0977" },
  { id: "ntf_6", kind: "payment", title: "Payment returned by the beneficiary bank", body: "CLM-25-0929 · Vollmer Interiors — IBAN rejected, $25,600.00 to reschedule", at: dAgo(4), route: "/payments" },
  { id: "ntf_5", kind: "recovery", title: "Meridian Terminal Services acknowledged the notice", body: "REC-26-0038 · liability under review by their insurer", at: dAgo(5), route: "/recovery/REC-26-0038" },
  { id: "ntf_4", kind: "documents", title: "Packing list still outstanding after two requests", body: "CLM-26-0212 · Kettleworth Brewing — last chased 6 days ago", at: dAgo(6), route: "/claims/CLM-26-0212/evidence" },
  { id: "ntf_3", kind: "response", title: "Customer disputed our offer", body: "CLM-25-0913 · Ferrymead Automotive holds us to $210,400.00", at: dAgo(12), route: "/responses" },
  { id: "ntf_2", kind: "time_bar", title: "Inside 90 days of the bar with payment unsent", body: "CLM-26-0131 · Pallas Glassworks — scheduled, not yet released", at: dAgo(16), route: "/claims/CLM-26-0131" },
  { id: "ntf_1", kind: "recovery", title: "Counterparty offered below the surveyed loss", body: "REC-26-0022 · Meridian Terminal Services offered $34,000.00 of $61,400.00", at: dAgo(20), route: "/recovery/REC-26-0022" },
];
/* What has already been read when the desk is opened — anything older than the
   last working day. The unread count everywhere derives from this one list. */
const NOTIFICATIONS_READ = ["ntf_1", "ntf_2", "ntf_3", "ntf_4"];

Object.assign(window, { CLAIMS_NOW, CLAIMS_USER, CLAIMS_MANAGER, CLAIMS_CARRIER, CLAIMS, CLAIMANTS, RECOVERIES, NOTIFICATIONS, NOTIFICATIONS_READ });

</script>
<script type="text/babel" data-presets="react" data-file="metrics.js">
/* Claims Desk — every figure on a screen comes from one of these helpers, so a
   number rendered twice is one function called twice. Each takes the array it
   summarises; none of them holds data.

   The money chain is the important one: claimed → deductions → assessed → cap
   → payable. No screen subtracts anything itself. */

const sumBy = (rows, key) => rows.reduce((total, row) => total + row[key], 0);
const sumOf = (rows, get) => rows.reduce((total, row) => total + get(row), 0);

/* ── The liability cap the bill of lading imposes ─────────────────────── */
/* Hague-Visby art. IV(5)(a): the HIGHER of 666.67 SDR per package and 2 SDR
   per kilo, converted at one rate held in CLAIM_POLICY. */
function liabilityCap(claim) {
  const byPackageSdr = claim.packages * CLAIM_POLICY.packageLimitSdr;
  const byWeightSdr = claim.weightKg * CLAIM_POLICY.weightLimitSdrPerKg;
  const sdr = Math.max(byPackageSdr, byWeightSdr);
  return {
    byPackageSdr: byPackageSdr,
    byWeightSdr: byWeightSdr,
    sdr: sdr,
    basis: byPackageSdr >= byWeightSdr ? "package" : "weight",
    cents: Math.round(sdr * CLAIM_POLICY.sdrUsdRate * 100),
  };
}

/* The one reconciliation. Pass draft adjustments to see the ledger the adjuster
   is typing; pass nothing and it reads the claim's saved assessment. */
function assess(claim, draft) {
  const adjustments = draft || claim.assessment;
  const cap = liabilityCap(claim);
  if (!adjustments) return { assessed: null, payable: null, cap: cap, capBinds: false };
  const depreciation = adjustments.depreciationCents || 0;
  const salvage = adjustments.salvageCents || 0;
  const deductible = adjustments.deductibleCents || 0;
  const deductions = depreciation + salvage + deductible;
  const assessed = claim.claimedCents - deductions;
  const payable = Math.min(assessed, cap.cents);
  return {
    claimed: claim.claimedCents,
    depreciation: depreciation, salvage: salvage, deductible: deductible, deductions: deductions,
    assessed: assessed, cap: cap, payable: payable,
    capBinds: assessed > cap.cents,
    cappedAway: Math.max(0, assessed - cap.cents),
  };
}

/* What this claim is worth to the customer today. The settlement owns the
   figure once one exists; before that the assessment does, and a rejected claim
   is worth nothing whatever the assessment said. */
function payableOf(claim) {
  if (claim.settlement) return claim.settlement.amountCents;
  if (claim.decision && claim.decision.outcome === "reject") return 0;
  return assess(claim).payable;
}

/* ── The queue ───────────────────────────────────────────────────────── */
const openClaims = (claims) => claims.filter(CLAIM_FLAGS.isOpen);
const closedClaims = (claims) => claims.filter((c) => !CLAIM_FLAGS.isOpen(c));
/* The act-ability predicate, applied to a list. */
const decidableClaims = (claims) => claims.filter(CLAIM_FLAGS.isDecidable);
const waitingClaims = (claims) => claims.filter(CLAIM_FLAGS.isWaitingOnOthers);
/* Ready for the adjuster but not yet decidable — the difference between these
   two arrays is what the queue headline has to explain in words. */
const stalledClaims = (claims) => claims.filter((c) => CLAIM_FLAGS.awaitsAdjuster(c) && !CLAIM_FLAGS.evidenceComplete(c));
const claimsInsideDays = (claims, days, now) => openClaims(claims).filter((c) => CLAIM_FLAGS.daysLeft(c, now) <= days);
const claimedExposure = (rows) => sumBy(rows, "claimedCents");
const byUrgency = (claims, now) => claims.slice().sort((a, b) => CLAIM_FLAGS.daysLeft(a, now) - CLAIM_FLAGS.daysLeft(b, now));

/* Who owes the next move, in words, for a claim the adjuster cannot act on. */
function waitingOn(claim) {
  switch (claim.status) {
    case "awaiting_documents": return "the customer";
    case "awaiting_survey": return "the surveyor";
    case "awaiting_signoff": return CLAIMS_MANAGER.name;
    case "escalated_legal": return "Legal";
    case "approved_awaiting_payment": return claim.settlement && claim.settlement.status === "unscheduled" ? "you, on the payments desk" : "Treasury";
    default: return null;
  }
}

/* ── Payments ────────────────────────────────────────────────────────── */
const settledClaims = (claims) => claims.filter(SETTLEMENT_FLAGS.exists);
const settlementsWith = (claims, status) => settledClaims(claims).filter((c) => c.settlement.status === status);
const settlementsToSchedule = (claims) => settledClaims(claims).filter(SETTLEMENT_FLAGS.needsScheduling);
const settlementTotal = (rows) => sumOf(rows, (c) => c.settlement.amountCents);
/* The release pipeline, in the order money moves. Every stage is a filter over
   the same array the table renders. */
function releasePipeline(claims) {
  return SETTLEMENT_STATUS.map((value) => {
    const rows = settlementsWith(claims, value.id);
    return { id: value.id, label: value.label, rows: rows, count: rows.length, amount: settlementTotal(rows) };
  }).filter((stage) => stage.count > 0);
}
/* Payment runs leave on Wednesdays. One helper, so the toast, the row and the
   dialog all name the same date. */
function nextRunDate(now) {
  const d = new Date(now);
  d.setHours(9, 0, 0, 0);
  const delta = (3 - d.getDay() + 7) % 7 || 7;
  return d.getTime() + delta * 864e5;
}

/* ── Responses ───────────────────────────────────────────────────────── */
const offeredClaims = (claims) => claims.filter(RESPONSE_FLAGS.exists);
const offersWith = (claims, status) => offeredClaims(claims).filter((c) => c.response.status === status);
const disputedOffers = (claims) => offersWith(claims, "disputed");
const answeredOffers = (claims) => offeredClaims(claims).filter((c) => c.response.status !== "awaiting_response");
const disputedGapTotal = (claims) => sumOf(disputedOffers(claims), (c) => RESPONSE_FLAGS.gap(c) || 0);

/* ── Recovery ────────────────────────────────────────────────────────── */
const openRecoveries = (recoveries) => recoveries.filter(RECOVERY_FLAGS.isOpen);
const closedRecoveries = (recoveries) => recoveries.filter(RECOVERY_FLAGS.isClosed);
const recoveriesToFile = (recoveries) => recoveries.filter(RECOVERY_FLAGS.awaitsAdjuster);
/* The figure the managers read. Money-weighted, over closed files only: an
   open recovery has not failed yet, and including it would flatter or damn the
   rate depending only on how much is in flight. */
function recoveryRate(recoveries) {
  const closed = closedRecoveries(recoveries);
  const pursued = sumBy(closed, "soughtCents");
  if (!pursued) return null;
  return sumBy(closed, "recoveredCents") / pursued;
}
function recoveryByKind(recoveries, kinds) {
  const pursued = sumBy(recoveries, "soughtCents");
  return kinds
    .map((kind) => {
      const rows = recoveries.filter((r) => r.counterparty.kind === kind.value);
      return {
        kind: kind.value, label: kind.label, count: rows.length,
        sought: sumBy(rows, "soughtCents"), recovered: sumBy(rows, "recoveredCents"),
        share: pursued ? sumBy(rows, "soughtCents") / pursued : 0,
      };
    })
    .filter((row) => row.count > 0)
    .sort((a, b) => b.sought - a.sought);
}

/* ── Sign-off ────────────────────────────────────────────────────────── */
/* The adjuster's side of the manager's queue: what she has sent up, what it is
   worth, and how long it has been sitting. She signs none of it. */
const awaitingSignoff = (claims) => claims.filter(CLAIM_FLAGS.awaitsSignoff);
const signedOffClaims = (claims) => claims.filter((c) => c.decision && c.decision.signedOffBy);
const decidedClaims = (claims) => claims.filter((c) => !!c.decision);
/* Payable is the figure the manager is being asked to approve, so it is read
   from the one owner rather than recomputed. */
const signoffValue = (rows) => sumOf(rows, (c) => payableOf(c) || 0);
const bySignoffWait = (claims) => awaitingSignoff(claims).slice().sort((a, b) => CLAIM_FLAGS.signoffWaitingSince(a) - CLAIM_FLAGS.signoffWaitingSince(b));
const longestSignoffWait = (claims) => bySignoffWait(claims)[0] || null;
/* Elapsed time, whole days. formatCountdown owns deadline words ("11 days
   over"); a wait is not a deadline, so it gets the plain count and the sentence
   around it says who is holding it. */
const daysSince = (ts, now) => Math.max(0, Math.round((now - ts) / 864e5));

/* ── Claimants ───────────────────────────────────────────────────────── */
/* A claimant record holds no money of its own: every figure below is the
   claimant's own claims read through the helpers the other screens use. */
const claimantById = (claimants, id) => claimants.find((p) => p.id === id) || null;
const claimantOf = (claimants, claim) => (claim ? claimants.find((p) => p.name === claim.customer) || null : null);
const claimsOfClaimant = (claims, claimant) => claims.filter((c) => c.customer === claimant.name);
const releasedClaims = (claims) => claims.filter(SETTLEMENT_FLAGS.isReleased);
const releasedTotal = (claims) => sumOf(releasedClaims(claims), (c) => c.settlement.amountCents);
const recoveriesForClaims = (recoveries, claims) => recoveries.filter((r) => claims.some((c) => c.id === r.claimId));
/* Money has left the account and nobody is pursuing it yet — the only claims a
   new recovery can be opened against. */
const claimsWithoutRecovery = (claims) => claims.filter((c) => SETTLEMENT_FLAGS.isReleased(c) && !c.recoveryId);
function claimantSummary(claimant, claims, recoveries, now) {
  const rows = claimsOfClaimant(claims, claimant);
  const open = openClaims(rows);
  return {
    rows: rows,
    open: open,
    claimed: claimedExposure(rows),
    openClaimed: claimedExposure(open),
    released: releasedClaims(rows),
    releasedTotal: releasedTotal(rows),
    rejected: rows.filter((c) => c.status === "rejected"),
    offers: offeredClaims(rows),
    answered: answeredOffers(rows),
    accepted: offersWith(rows, "accepted"),
    disputed: disputedOffers(rows),
    recoveries: recoveriesForClaims(recoveries, rows),
    firstClaim: rows.slice().sort((a, b) => a.claimedAt - b.claimedAt)[0] || null,
    nearestBar: byUrgency(open, now)[0] || null,
  };
}

/* ── Notifications ───────────────────────────────────────────────────── */
const unreadNotifications = (items, readIds) => items.filter((n) => readIds.indexOf(n.id) === -1);
const notificationsOfKind = (items, kind) => items.filter((n) => n.kind === kind);
/* Grouped for the page, newest first, one heading per calendar day. */
function notificationsByDay(items, now) {
  const days = [];
  items.slice().sort((a, b) => b.at - a.at).forEach((n) => {
    const key = new Date(n.at).toDateString();
    const found = days.find((d) => d.key === key);
    if (found) found.items.push(n);
    else days.push({ key: key, at: n.at, items: [n] });
  });
  return days;
}

/* ── Lookups ─────────────────────────────────────────────────────────── */
const claimById = (claims, id) => claims.find((c) => c.id === id) || null;
const recoveryById = (recoveries, id) => recoveries.find((r) => r.id === id) || null;
const recoveryForClaim = (recoveries, claim) => (claim && claim.recoveryId ? recoveryById(recoveries, claim.recoveryId) : null);

/* The nav badge predicates, resolved by name from nav.js. Each one counts a
   single kind of record, and says so. */
const NAV_BADGES = {
  claimsDecidable: {
    /* Claims you can decide now — ready for decision AND every mandatory
       document on file. The queue headline names the ones this excludes. */
    count: (data) => decidableClaims(data.claims).length,
    label: "claims you can decide now",
  },
  signoffWaiting: {
    /* Not act-ability — nobody on this desk can sign these. It counts what is
       stuck with the manager, which is the reason to open the screen. */
    count: (data) => awaitingSignoff(data.claims).length,
    label: "decisions waiting on a signature",
  },
  settlementsToSchedule: {
    count: (data) => settlementsToSchedule(data.claims).length,
    label: "payments to schedule",
  },
  offersDisputed: {
    count: (data) => disputedOffers(data.claims).length,
    label: "offers the customer has disputed",
  },
  recoveriesToFile: {
    count: (data) => recoveriesToFile(data.recoveries).length,
    label: "recoveries to file",
  },
};

Object.assign(window, {
  sumBy, sumOf, liabilityCap, assess, payableOf,
  openClaims, closedClaims, decidableClaims, waitingClaims, stalledClaims, claimsInsideDays,
  claimedExposure, byUrgency, waitingOn,
  settledClaims, settlementsWith, settlementsToSchedule, settlementTotal, releasePipeline, nextRunDate,
  offeredClaims, offersWith, disputedOffers, answeredOffers, disputedGapTotal,
  openRecoveries, closedRecoveries, recoveriesToFile, recoveryRate, recoveryByKind,
  awaitingSignoff, signedOffClaims, decidedClaims, signoffValue, bySignoffWait, longestSignoffWait, daysSince,
  claimantById, claimantOf, claimsOfClaimant, releasedClaims, releasedTotal, recoveriesForClaims, claimsWithoutRecovery, claimantSummary,
  unreadNotifications, notificationsOfKind, notificationsByDay,
  claimById, recoveryById, recoveryForClaim, NAV_BADGES,
});

</script>
<script type="text/babel" data-presets="react" data-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);
/* A counted noun reads "1 entry", never "1 entries". Irregular plurals pass
   their own second form; everything else gets an s. */
const formatPlural = (n, one, many) => formatCount(n) + " " + (n === 1 ? one : (many || one + "s"));

/* 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, formatPlural, formatWhen, formatAbsolute, formatCountdown, Icon, Floating, FloatingTip });

</script>
<script type="text/babel" data-presets="react" data-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. */

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 }) {
  /* 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(); }}
          />
        </div>
      </div>
    </>
  );
}

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

</script>
<script type="text/babel" data-presets="react" data-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/babel" data-presets="react" data-file="ClaimsParts.jsx">
/* Claims Desk — shared parts, local to this product. Reported in README as
   local additions: TimeBar / ClaimClock (the deadline device), Ledger rows,
   FormDialog (a staged dialog with two or three fields), RecordHead. Nothing
   here holds data or assigns a status tone — StatusBadge does that. */
(() => {
  const {
    Button, Input, Textarea, Badge, RadioGroup, Field, FieldLabel, FieldDescription, FieldError,
    Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator,
    AlertDialog, AlertDialogPopup, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter,
    Dialog, DialogPopup, DialogHeader, DialogTitle, DialogDescription, DialogPanel, DialogFooter,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
    MenuItem, MenuGroupLabel, Avatar,
  } = window.OriginDesignSystem_7c998b;
  /* The breadcrumb needs the product's router, and PageHead is rendered by every
     screen without it. The shell registers it here once per render; Crumbs reads
     it unless a caller passes onNavigate explicitly. */
  let breadcrumbNavigate = null;
  const registerBreadcrumbNavigate = (fn) => { breadcrumbNavigate = fn; };


  /* One threshold table for deadline pressure, product-wide. Tone is about the
     clock, never about the record's status. */
  const TIME_TONE_DAYS = { danger: 30, warning: 120 };
  const timeTone = (daysLeft) => (daysLeft <= TIME_TONE_DAYS.danger ? "danger" : daysLeft <= TIME_TONE_DAYS.warning ? "warning" : "neutral");
  const toneSuffix = (tone) => (tone === "neutral" ? "" : " cd-tb--" + tone);
  const clockClass = (tone) => "cd-clock" + (tone === "neutral" ? "" : " cd-clock--" + tone);

  /* The burn-down track. Elapsed share of the window, left to right, so the
     deadline is always the right-hand wall. */
  function TimeBar({ startAt, endAt, now, mini, label }) {
    const span = Math.max(1, endAt - startAt);
    const elapsed = Math.min(1, Math.max(0, (now - startAt) / span));
    const daysLeft = Math.round((endAt - now) / 864e5);
    return (
      <span className={"cd-tb" + (mini ? " cd-tb--mini" : "") + toneSuffix(timeTone(daysLeft))} role="img" aria-label={label}>
        <span className="cd-tb__fill" style={{ width: (elapsed * 100).toFixed(1) + "%" }} />
      </span>
    );
  }

  /* The claim's clock, spelled one way everywhere: bar plus countdown. */
  function ClaimClock({ claim, now, mini }) {
    const endAt = CLAIM_FLAGS.timeBarAt(claim);
    const daysLeft = CLAIM_FLAGS.daysLeft(claim, now);
    return (
      <span className="cd-cell-clock">
        <span className={clockClass(timeTone(daysLeft))} title={formatAbsolute(endAt)}>{formatCountdown(endAt, now)}</span>
        <TimeBar startAt={claim.deliveredAt} endAt={endAt} now={now} mini={mini} label={"Time bar: " + formatCountdown(endAt, now)} />
      </span>
    );
  }

  const resolvePath = (path, params) => (params ? path.replace(":id", params.id) : path);

  /* Breadcrumb only. The trail comes from the route table; params substitute
     the record's own id into the pattern the table holds. */
  function Crumbs({ path, params, labels, onNavigate }) {
    const trail = breadcrumb(path, CLAIMS_ROUTES);
    const go = onNavigate || breadcrumbNavigate;
    return (
      <Breadcrumb>
        <BreadcrumbList>
          {trail.map((seg, i) => {
            const href = resolvePath(seg.path, params);
            const label = (labels && labels[seg.path]) || seg.label;
            return (
              <React.Fragment key={seg.path}>
                {i > 0 ? <BreadcrumbSeparator /> : null}
                <BreadcrumbItem>
                  {i === trail.length - 1
                    ? <BreadcrumbPage>{label}</BreadcrumbPage>
                    : seg.isNamespace ? <span>{label}</span> : <BreadcrumbLink href={"#" + href} onSelect={() => go && go(href)}>{label}</BreadcrumbLink>}
                </BreadcrumbItem>
              </React.Fragment>
            );
          })}
        </BreadcrumbList>
      </Breadcrumb>
    );
  }

  /* `owner` is passed only by a screen that is the person's own desk rather than
     a shared destination — the face says whose queue this is, and is decoration
     on every other screen. Initials track the name, so a removed photo still
     renders the right person. */
  function PageHead({ path, params, labels, title, lede, actions, owner }) {
    return (
      <header className="cd-page-head">
        <div className="cd-page-head__lead">
          {owner ? (
            <Avatar size={56} src={owner.avatar || undefined} alt="" fallback={(owner.name || "").trim().split(/\s+/).slice(0, 2).map((w) => w[0] || "").join("").toUpperCase() || "?"} />
          ) : null}
          <div className="cd-page-head__text">
            <Crumbs path={path} params={params} labels={labels} />
            <h1 className="cd-title">{title}</h1>
            {lede ? <p className="cd-lede">{lede}</p> : null}
          </div>
        </div>
        {actions ? <div className="cd-page-head__actions">{actions}</div> : null}
      </header>
    );
  }

  /* The record header for the three claim screens: identity, status, the clock
     band, and the cargo line. The same block on all three, so the tabs change
     the surface below and nothing else. */
  function RecordHead({ claim, now, crumb, actions, children }) {
    const endAt = CLAIM_FLAGS.timeBarAt(claim);
    const daysLeft = CLAIM_FLAGS.daysLeft(claim, now);
    return (
      <div className="cd-head-record">
        <div className="cd-head-record__top">
          <div className="cd-page-head__text">
            {crumb}
            <span className="cd-head-record__id">
              <span className="cd-code">{claim.id}</span>
              <StatusBadge values={CLAIM_STATUS} value={claim.status} />
            </span>
            <h1 className="cd-title">
              <button type="button" className="cd-title-link" onClick={() => breadcrumbNavigate && breadcrumbNavigate("/claimants/" + claimantOf(CLAIMANTS, claim).id)}>{claim.customer}</button>
            </h1>
            <p className="cd-lede">{claim.incident}</p>
          </div>
          {actions ? <div className="cd-page-head__actions">{actions}</div> : null}
        </div>
        <div className="cd-head-record__bar">
          <TimeBar startAt={claim.deliveredAt} endAt={endAt} now={now} label={"Time bar: " + formatCountdown(endAt, now)} />
          <div className="cd-head-record__left">
            <div className={clockClass(timeTone(daysLeft))} title={formatAbsolute(endAt)} style={{ fontSize: "var(--text-sm)", fontWeight: "var(--font-weight-medium)" }}>{formatCountdown(endAt, now)}</div>
            <div className="cd-figure__note">to the time bar</div>
          </div>
          <div className="cd-head-record__ends">
            <span>Delivered {formatAbsolute(claim.deliveredAt).split(",")[0]}</span>
            <span>Time bar {formatAbsolute(endAt).split(",")[0]}</span>
          </div>
        </div>
        <div className="cd-facts">
          <span><strong>{claim.container}</strong> · {claim.commodity}</span>
          <span>{claim.pol} → {claim.pod}</span>
          <span>{claim.vessel} {claim.voyage}</span>
          <span>B/L <span className="cd-code">{claim.bl}</span></span>
        </div>
        {children}
      </div>
    );
  }

  function Figure({ label, value, note, xl }) {
    return (
      <div>
        <div className={xl ? "cd-figure cd-figure--xl" : "cd-figure"}>{value}</div>
        <div className="cd-figure__label">{label}</div>
        {note ? <div className="cd-figure__note">{note}</div> : null}
      </div>
    );
  }

  /* Evidence at a glance: one pip per document, in checklist order. */
  function DocumentPips({ documents }) {
    return (
      <span className="cd-pips" aria-hidden="true">
        {DOCUMENT_TYPES.map((type) => {
          const found = documents.find((d) => d.type === type.value);
          const status = found ? found.status : "requested";
          return <span key={type.value} className={"cd-pip cd-pip--" + status} />;
        })}
      </span>
    );
  }

  /* Irreversible only. Named consequence in the title, one action to confirm. */
  function ConfirmDialog({ request, onCancel, onConfirm }) {
    return (
      <AlertDialog open={!!request} onOpenChange={(open) => { if (!open) onCancel(); }}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>{request ? request.title : ""}</AlertDialogTitle>
            <AlertDialogDescription>{request ? request.description : ""}</AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <Button variant="outline" onClick={onCancel}>Cancel</Button>
            <Button variant={request && request.destructive ? "destructive" : "default"} onClick={onConfirm}>{request ? request.confirmLabel : ""}</Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    );
  }

  const parseMoney = (text) => {
    const cleaned = String(text).replace(/[^0-9.]/g, "");
    if (!cleaned) return NaN;
    return Math.round(parseFloat(cleaned) * 100);
  };
  const moneyInputValue = (cents) => (cents === null || cents === undefined || isNaN(cents) ? "" : (cents / 100).toFixed(2));

  /* A staged dialog — its own commit model, so an instant screen may open it.
     fields: [{ id, label, kind: "money"|"text"|"radio", options, hint, placeholder, optional }] */
  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 })); };
    return (
      <Dialog open onOpenChange={(open) => { if (!open) onCancel(); }}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>{request.title}</DialogTitle>
            <DialogDescription>{request.description}</DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <div className="cd-col cd-col--tight">
              {request.fields.filter((f) => !f.when || f.when(values)).map((field) => (
                <Field key={field.id}>
                  <FieldLabel htmlFor={"cd-f-" + field.id}>{field.label}</FieldLabel>
                  {field.kind === "radio" ? (
                    <RadioGroup items={field.options} value={values[field.id] || ""} onValueChange={(v) => set(field.id, v)} name={"cd-f-" + field.id} />
                  ) : field.kind === "text" ? (
                    <Textarea id={"cd-f-" + field.id} rows={4} placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  ) : (
                    <Input id={"cd-f-" + field.id} inputMode="decimal" startAddon="$" placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  )}
                  {submitted && errors[field.id]
                    ? <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>
    );
  }

  /* One export entry point, the same in every page header that has one. */
  function ExportMenu({ scope, onExport }) {
    return (
      <Floating
        align="end" width="12rem" label="Export"
        trigger={<Button variant="outline" size="sm"><Icon name="download" />Export<Icon name="chevron-down" /></Button>}
      >
        {(close) => (
          <>
            <MenuGroupLabel>{scope}</MenuGroupLabel>
            {["CSV", "XLSX"].map((format) => (
              <MenuItem key={format} onClick={() => { close(); onExport(format); }}>{format}</MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  function RouteNotBuilt({ path, onNavigate }) {
    const route = matchRoute(path, CLAIMS_ROUTES);
    return (
      <div className="cd-page">
        <PageHead path={path} title={route ? route.label : "Not found"} />
        <Empty>
          <EmptyHeader>
            <EmptyMedia variant="icon"><Icon name="route" size={18} /></EmptyMedia>
            <EmptyTitle>{route ? route.label + " is registered, not built" : "There is nothing at " + path}</EmptyTitle>
            <EmptyDescription>{route ? "The route and its place in the palette are real; the screen behind it is not part of this template." : "This path is not in the claims desk's route table, so it has no breadcrumb and nothing links to it. The four destinations in the bar, plus the claim, recovery and account screens, are the whole product."}</EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button variant="outline" onClick={() => onNavigate("/")}>Back to the work queue</Button>
          </EmptyContent>
        </Empty>
      </div>
    );
  }

  Object.assign(window, {
    registerBreadcrumbNavigate,
    TIME_TONE_DAYS, timeTone, clockClass, TimeBar, ClaimClock, Crumbs, PageHead, RecordHead, Figure, DocumentPips,
    ConfirmDialog, FormDialog, ExportMenu, RouteNotBuilt, parseMoney, moneyInputValue, resolvePath,
  });
})();

</script>
<script type="text/babel" data-presets="react" data-file="ClaimsShell.jsx">
/* Claims Desk shell — TOP-NAV shell, one navigation owner (the bar owns all
   five destinations; there is no sidebar and no second row of navigation).
   Density: 64px bar, 24px page gutters, 32px table rows — tight operational
   tables against generous decision surfaces.
   Separation: hairlines and full-bleed bands on the --sidebar page plane;
   cards are the only raised surface, one elevation step, never nested.
   Active state: foreground token + a persistent 2px underline, derived from
   the route; counts sit after the label inside the item.
   App-scope duties: search → the bar's palette field · session and account →
   the bar's account menu · utility → the bar (notifications, theme) · page
   context → the page header's breadcrumb and title.
   Primary content per destination:
     Work queue — burn-down band over one dense claims table
     Sign-off   — the waiting figures over one table, and what came back
     Payments   — release pipeline over one settlements table
     Responses  — the disputed offer, full width, beside the offer ledger
     Recovery   — the recovery rate, its breakdown, and the recoveries table
   Responses is the destination that would most plausibly have been a table and
   is not: it answers "where has my offer been refused, and by how much", which
   is one record at a time, not a list to scan. */
(() => {
  const { Button, Input, Kbd, Avatar, MenuItem, MenuGroupLabel, MenuSeparator, Toast, ToastRegion, Tabs, TabsList, TabsTab } = window.OriginDesignSystem_7c998b;

  /* Declared once for the product: 1280 full bar · 1024 search collapses to an
     icon · below 1024 the destinations collapse into one menu. Nothing is lost
     on the way down — the menu carries the same labels and the same counts. */
  function useShellMode() {
    const read = () => (window.innerWidth >= 1280 ? "full" : window.innerWidth >= 1024 ? "compact" : "menu");
    const [mode, setMode] = React.useState(read);
    React.useEffect(() => {
      const onResize = () => setMode(read());
      window.addEventListener("resize", onResize);
      return () => window.removeEventListener("resize", onResize);
    }, []);
    return mode;
  }

  const badgeFor = (route, data) => {
    if (!route.badge) return null;
    const spec = NAV_BADGES[route.badge];
    return { count: spec.count(data), label: spec.label };
  };

  function Destinations({ path, data, onNavigate }) {
    return (
      <nav className="cd-dests" aria-label="Claims desk">
        {CLAIMS_DESTINATIONS.map((route) => {
          const active = path === route.path || (route.path !== "/" && path.indexOf(route.path + "/") === 0) || (route.path === "/" && path.indexOf("/claims/") === 0);
          const badge = badgeFor(route, data);
          return (
            <button type="button" key={route.path} className="cd-dest" aria-current={active ? "page" : undefined} onClick={() => onNavigate(route.path)}>
              {route.label}
              {badge && badge.count > 0 ? <span className="cd-dest__count" aria-label={badge.count + " " + badge.label}>{badge.count}</span> : null}
            </button>
          );
        })}
      </nav>
    );
  }

  function DestinationsMenu({ path, data, onNavigate }) {
    const current = CLAIMS_DESTINATIONS.find((r) => path === r.path || (r.path !== "/" && path.indexOf(r.path + "/") === 0)) || CLAIMS_DESTINATIONS[0];
    const trigger = (
      <Button variant="ghost" size="sm" aria-label="Destinations">
        <Icon name="menu" />{current.label}<Icon name="chevron-down" />
      </Button>
    );
    return (
      <Floating trigger={trigger} align="start" width="15rem" label="Destinations">
        {(close) => CLAIMS_DESTINATIONS.map((route) => {
          const badge = badgeFor(route, data);
          return (
            <MenuItem key={route.path} onClick={() => { onNavigate(route.path); close(); }}>
              {route.label}{badge && badge.count > 0 ? <span className="cd-dest__count"> {badge.count}</span> : null}
            </MenuItem>
          );
        })}
      </Floating>
    );
  }

  function AccountMenu({ mode, onNavigate, user }) {
    const trigger = (
      <button type="button" className="cd-account" aria-label={"Account — " + user.name}>
        <Avatar size={28} src={user.avatar} alt="" fallback={(user.name || "").trim().split(/\s+/).slice(0, 2).map((w) => w[0] || "").join("").toUpperCase() || "?"} />
        <span className="cd-account__text">
          <span className="cd-account__name">{user.name}</span>
          <span className="cd-account__role">{user.role}</span>
        </span>
      </button>
    );
    return (
      <Floating trigger={trigger} align="end" width="16rem" label="Account">
        {(close) => (
          <>
            <MenuGroupLabel>{user.name} · {user.role}</MenuGroupLabel>
            {CLAIMS_ACCOUNT_MENU.map((route) => (
              <MenuItem key={route.path} onClick={() => { onNavigate(route.path); close(); }}>{route.label}</MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  /* The panel is a state of the bar, not a destination; /notifications behind
     it is. The panel shows the newest few and hands off the rest. */
  const PANEL_LIMIT = 5;

  function NotificationsButton({ items, readIds, unread, onRead, onOpen, onNavigate }) {
    const newest = items.slice().sort((a, b) => b.at - a.at).slice(0, PANEL_LIMIT);
    const trigger = (
      <span className="cd-utility">
        <Button variant="ghost" size="icon-sm" aria-label={unread ? unread + " unread notifications" : "Notifications"}><Icon name="bell" /></Button>
        {unread ? <span className="cd-badge-dot" aria-hidden="true">{unread}</span> : null}
      </span>
    );
    return (
      <Floating trigger={trigger} align="end" width="23rem" role="dialog" panelClass="o-popover cd-notify" label="Notifications">
        {(close) => (
          <>
            <div className="cd-notify__head">
              <span className="o-popover__title">Notifications</span>
              {unread ? <Button variant="ghost" size="xs" onClick={onRead}>Mark all read</Button> : null}
            </div>
            <div className="cd-notify__list">
              {newest.map((n) => (
                <button type="button" key={n.id} className={"cd-notify__item" + (readIds.indexOf(n.id) === -1 ? " is-unread" : "")} onClick={() => { onOpen(n); close(); }}>
                  <span className="cd-notify__title">{n.title}</span>
                  <span className="cd-notify__body">{n.body}</span>
                  <span className="cd-notify__time" title={formatAbsolute(n.at)}>{formatWhen(n.at, CLAIMS_NOW)}</span>
                </button>
              ))}
            </div>
            <div className="cd-notify__foot">
              <button type="button" className="cd-record__link" onClick={() => { onNavigate("/notifications"); close(); }}>
                See all {formatCount(items.length)} notifications
              </button>
            </div>
          </>
        )}
      </Floating>
    );
  }

  /* The claim's three views. Registered routes, reached by tabs — never a
     second set of top-level destinations. */
  function ClaimTabs({ claim, view, onNavigate }) {
    return (
      <Tabs value={view} onValueChange={(next) => onNavigate(resolvePath(CLAIM_VIEWS.find((v) => v.value === next).route.path, { id: claim.id }))}>
        <TabsList variant="underline">
          {CLAIM_VIEWS.map((v) => (
            <TabsTab key={v.value} value={v.value}>{v.label}</TabsTab>
          ))}
        </TabsList>
      </Tabs>
    );
  }

  /* The settings views. Same device as the claim's tabs, reading the same route
     table — the strip never carries a label the table does not have. */
  function SettingsTabs({ view, onNavigate }) {
    return (
      <Tabs value={view} onValueChange={(next) => onNavigate(SETTINGS_VIEWS.find((v) => v.value === next).route.path)}>
        <TabsList variant="underline">
          {SETTINGS_VIEWS.map((v) => (
            <TabsTab key={v.value} value={v.value}>{v.label}</TabsTab>
          ))}
        </TabsList>
      </Tabs>
    );
  }

  function ClaimsShell({ path, onNavigate, dark, onToggleTheme, data, user, notifications, readIds, unread, onRead, onOpenNotification, palette, toasts, onDismissToast, children }) {
    registerBreadcrumbNavigate(onNavigate);
    const mode = useShellMode();
    return (
      <div className="cd-shell" data-mode={mode}>
        <header className="cd-bar">
          {mode === "menu" ? <DestinationsMenu path={path} data={data} onNavigate={onNavigate} /> : null}
          <button type="button" className="cd-brand" onClick={() => onNavigate("/")} aria-label={CLAIMS_CARRIER.name + " — " + CLAIMS_CARRIER.desk}>
            <img className="cd-brand__mark" src={window.__resources.brandMark} alt="" width="40" height="40" />
            <span className="cd-brand__name">{CLAIMS_CARRIER.name}</span>
          </button>
          {mode === "menu" ? null : <Destinations path={path} data={data} onNavigate={onNavigate} />}
          <div className="cd-bar__end">
            {mode === "full" ? (
              <div className="cd-search" onClick={() => palette.setOpen(true)}>
                <Input readOnly size="sm" aria-label="Search the claims desk" placeholder="Search claims, containers, recoveries…" startAddon={<Icon name="search" />} endAddon={<Kbd>⌘K</Kbd>} onFocus={() => palette.setOpen(true)} />
              </div>
            ) : (
              <FloatingTip label="Search claims, containers, recoveries" side="bottom">
                <Button variant="ghost" size="icon-sm" aria-label="Search the claims desk" onClick={() => palette.setOpen(true)}><Icon name="search" /></Button>
              </FloatingTip>
            )}
            <NotificationsButton items={notifications} readIds={readIds} unread={unread} onRead={onRead} onOpen={onOpenNotification} onNavigate={onNavigate} />
            <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"} /></Button>
            </FloatingTip>
            <AccountMenu mode={mode} onNavigate={onNavigate} user={user} />
          </div>
        </header>

        <main className="cd-main">{children}</main>

        <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, { ClaimsShell, ClaimTabs, SettingsTabs, useShellMode });
})();

</script>
<script type="text/babel" data-presets="react" data-file="QueueScreen.jsx">
/* Work queue — the desk's home screen.
   Commit model: INSTANT. No Save button; the text filter and the tabs are
   harmless and apply as typed, the two chase actions apply immediately and
   raise an undo toast, and nothing on this screen is irreversible.
   Data state: populated. Empty and no-results are the other two branches of
   one conditional below — never on screen beside these rows.
   Scope: everything open on this desk. There is no period filter, so no figure
   on this screen moves when the tabs or the text filter change. */
(() => {
  const {
    Button, Input, Badge, Tabs, TabsList, TabsTab,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* Tabs narrow the list only. Each one runs its own predicate — never a slice
     — and the labels name the looser things they count, because "on my desk"
     is deliberately wider than "decidable". */
  const QUEUE_TABS = [
    { value: "mine", label: "On my desk", predicate: CLAIM_FLAGS.awaitsAdjuster },
    { value: "others", label: "Waiting on others", predicate: CLAIM_FLAGS.isWaitingOnOthers },
    { value: "closed", label: "Closed", predicate: (c) => !CLAIM_FLAGS.isOpen(c) },
    { value: "all", label: "All", predicate: () => true },
  ];

  const matches = (claim, query) => {
    const q = query.trim().toLowerCase();
    if (!q) return true;
    return [claim.id, claim.customer, claim.container, claim.bl, claim.commodity].join(" ").toLowerCase().indexOf(q) > -1;
  };

  /* The burn-down: one bar per open claim on a single time-bar axis, positioned
     by days left and sized by the amount claimed. The deadline is the right
     wall. Every bar drills through to its claim. */
  function BurnDownBand({ claims, now, onNavigate }) {
    const open = openClaims(claims);
    const inside90 = claimsInsideDays(claims, 90, now);
    const total = CLAIM_POLICY.timeBarDays;
    const maxClaimed = Math.max.apply(null, open.map((c) => c.claimedCents));
    const zoneLeft = (days) => ((total - days) / total) * 100;
    const urgent = byUrgency(open, now).slice(0, 3);

    return (
      <div className="cd-bleed">
        <div className="cd-split cd-split--band">
          <div className="cd-section">
            <div className="cd-section__head">
              <h2 className="cd-h2">Time bar burn-down</h2>
            </div>
            <div className="cd-figures">
              <Figure xl label="Claimed and open" value={formatMoney(claimedExposure(open))} note={open.length + " claims still open, from " + claims.length + " on the desk"} />
              <Figure label="Inside 90 days" value={formatMoney(claimedExposure(inside90))} note={inside90.length + " of the " + open.length + " open claims, including anything already past the bar"} />
            </div>
            <div className="cd-burn">
              <span className="cd-burn__zone cd-burn__zone--warning" style={{ left: zoneLeft(TIME_TONE_DAYS.warning) + "%", width: (zoneLeft(TIME_TONE_DAYS.danger) - zoneLeft(TIME_TONE_DAYS.warning)) + "%" }} />
              <span className="cd-burn__zone cd-burn__zone--danger" style={{ left: zoneLeft(TIME_TONE_DAYS.danger) + "%", width: (100 - zoneLeft(TIME_TONE_DAYS.danger)) + "%" }} />
              {open.map((claim) => {
                const daysLeft = CLAIM_FLAGS.daysLeft(claim, now);
                const tone = timeTone(daysLeft);
                const x = Math.min(99, Math.max(1, ((total - daysLeft) / total) * 100));
                const height = 24 + 108 * Math.sqrt(claim.claimedCents / maxClaimed);
                return (
                  <FloatingTip key={claim.id} side="bottom" label={claim.customer + " · " + claim.id + " · " + formatMoney(claim.claimedCents) + " · " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now)}>
                    <button
                      type="button"
                      className={"cd-burn__bar" + (tone === "neutral" ? "" : " cd-burn__bar--" + tone)}
                      style={{ left: x + "%", height: height + "px" }}
                      aria-label={claim.customer + ", " + formatMoney(claim.claimedCents) + " claimed, " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now)}
                      onClick={() => onNavigate("/claims/" + claim.id)}
                    />
                  </FloatingTip>
                );
              })}
            </div>
            <div className="cd-burn__axis" aria-hidden="true">
              <span className="cd-burn__tick cd-burn__tick--first" style={{ left: "0%" }}>365 days left</span>
              <span className="cd-burn__tick" style={{ left: zoneLeft(180) + "%" }}>180</span>
              <span className="cd-burn__tick" style={{ left: zoneLeft(TIME_TONE_DAYS.warning) + "%" }}>120</span>
              <span className="cd-burn__tick" style={{ left: zoneLeft(TIME_TONE_DAYS.danger) + "%" }}>30</span>
              <span className="cd-burn__tick cd-burn__tick--last" style={{ left: "100%" }}>0</span>
            </div>
            <div className="cd-burn__legend">
              <span>One bar per open claim, height by amount claimed, up to {formatMoney(maxClaimed)}.</span>
              <span>Amber from {TIME_TONE_DAYS.warning} days left, red from {TIME_TONE_DAYS.danger}.</span>
            </div>
          </div>

          <div className="cd-section">
            <div className="cd-section__head">
              <h2 className="cd-h2">Nearest the wall</h2>
              <p className="cd-note">The three open claims closest to their time bar, whoever is holding them.</p>
            </div>
            <div className="cd-rows">
              {urgent.map((claim) => (
                <div className="cd-row" key={claim.id}>
                  <div className="cd-row__main">
                    <span className="cd-record__name">{claim.customer}</span>
                    <span className="cd-record__meta"><code>{claim.id}</code> · {formatMoney(claim.claimedCents)} claimed</span>
                    <span className="cd-record__meta">{CLAIM_FLAGS.awaitsAdjuster(claim) ? "On your desk" : "Waiting on " + waitingOn(claim)}</span>
                  </div>
                  <div className="cd-row__side">
                    <ClaimClock claim={claim} now={now} mini />
                    <Button variant="ghost" size="xs" onClick={() => onNavigate("/claims/" + claim.id)}>Open<Icon name="arrow-right" /></Button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    );
  }

  function QueueScreen({ data, now, desk, onNavigate, onNewClaim, onChase, pushToast }) {
    const [tab, setTab] = React.useState((desk && desk.queueTab) || DESK_DEFAULTS.queueTab);
    const [query, setQuery] = React.useState("");
    const claims = data.claims;
    /* One place decides where a claim opens from this screen, so the desk
       preference cannot disagree with itself between the table and the band. */
    const claimHref = (claim) => "/claims/" + claim.id + (desk && desk.openDecidable && CLAIM_FLAGS.isDecidable(claim) ? "/decision" : "");
    const spec = QUEUE_TABS.find((t) => t.value === tab);
    const inTab = claims.filter(spec.predicate);
    const rows = inTab.filter((c) => matches(c, query)).sort((a, b) => CLAIM_FLAGS.daysLeft(a, now) - CLAIM_FLAGS.daysLeft(b, now));
    const decidable = decidableClaims(claims);
    const stalled = stalledClaims(claims);

    return (
      <div className="cd-page">
        <PageHead
          path="/"
          owner={CLAIMS_USER}
          title="Work queue"
          lede={"Everything open on the " + CLAIMS_CARRIER.region + " desk. " + decidable.length + " of the " + claims.filter(CLAIM_FLAGS.awaitsAdjuster).length + " claims on your desk can be decided now" + (stalled.length ? " — " + stalled.map((c) => c.id).join(", ") + " is held by its own evidence." : ".")}
          actions={<><ExportMenu scope="Open claims" onExport={(format) => pushToast({ title: "Export started", description: "Open claims · " + format + " — we'll email it to " + CLAIMS_USER.name.split(" ")[0].toLowerCase() + "@corvenline.example" })} /><Button size="sm" onClick={() => onNewClaim(null)}>New claim</Button></>}
        />

        <BurnDownBand claims={claims} now={now} onNavigate={onNavigate} />

        <section className="cd-section">
          <div className="cd-filters">
            <div className="cd-filters__text">
              <Input
                size="sm" value={query} onChange={(e) => setQuery(e.target.value)}
                aria-label="Filter claims" placeholder="Filter by claimant, claim or container"
                startAddon={<Icon name="search" />}
              />
            </div>
            <Tabs value={tab} onValueChange={setTab}>
              <TabsList variant="underline" size="sm">
                {QUEUE_TABS.map((t) => (
                  <TabsTab key={t.value} value={t.value} size="sm">
                    {t.label}<span className="cd-tab-count">{claims.filter(t.predicate).length}</span>
                  </TabsTab>
                ))}
              </TabsList>
            </Tabs>
          </div>

          {rows.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
                <EmptyTitle>No claim matches “{query}”</EmptyTitle>
                <EmptyDescription>{formatPlural(inTab.length, "claim")} sit under {spec.label.toLowerCase()}. Clear the filter to see them.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => setQuery("")}>Clear filter</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="cd-table-wrap">
              <Table className="cd-tbl">
                <TableHeader>
                  <TableRow>
                    <TableHead>Claim</TableHead>
                    <TableHead className="cd-drop-lg">Cargo</TableHead>
                    <TableHead className="cd-num">Claimed</TableHead>
                    <TableHead className="cd-num">Payable</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead className="cd-drop-md">Time bar</TableHead>
                    <TableHead className="cd-actions"><span className="o-sr">Action</span></TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((claim) => {
                    const recessed = CLAIM_FLAGS.isWaitingOnOthers(claim);
                    const payable = payableOf(claim);
                    const missing = CLAIM_FLAGS.missingMandatory(claim);
                    const decidableRow = CLAIM_FLAGS.isDecidable(claim);
                    const holdReason = missing.length
                      ? "Waiting on evidence — " + missing.map((m) => labelOf(DOCUMENT_TYPES, m).toLowerCase()).join(", ")
                      : null;
                    return (
                      <TableRow key={claim.id} className={recessed ? "cd-row-recessed" : ""}>
                        <TableCell>
                          <span className="cd-record">
                            <button type="button" className="cd-record__link" onClick={() => onNavigate(claimHref(claim))}>{claim.customer}</button>
                            <span className="cd-record__meta"><code>{claim.id}</code> · {claim.container}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-drop-lg cd-muted">
                          <span className="cd-record">
                            <span>{claim.commodity}</span>
                            <span className="cd-record__meta">{formatPlural(claim.packages, "package")} · {formatCount(claim.weightKg)} kg</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-num cd-amount">{formatMoney(claim.claimedCents)}</TableCell>
                        <TableCell className="cd-num">
                          {claim.decision && claim.decision.outcome === "reject"
                            ? <span className="cd-muted">Nothing payable</span>
                            : payable === null
                              ? <span className="cd-muted">Not assessed</span>
                              : <span className="cd-amount">{formatMoney(payable)}</span>}
                        </TableCell>
                        <TableCell>
                          <span className="cd-record">
                            <StatusBadge values={CLAIM_STATUS} value={claim.status} />
                            {recessed ? <span className="cd-record__meta">Waiting on {waitingOn(claim)}</span> : null}
                            {!recessed && holdReason ? <span className="cd-record__meta">{holdReason}</span> : null}
                          </span>
                        </TableCell>
                        <TableCell className="cd-drop-md">
                          {CLAIM_FLAGS.isOpen(claim)
                            ? <ClaimClock claim={claim} now={now} mini />
                            : <span className="cd-muted cd-clock">Closed {formatWhen(claim.decision ? claim.decision.decidedAt : claim.claimedAt, now)}</span>}
                        </TableCell>
                        <TableCell className="cd-actions">
                          {decidableRow ? (
                            <Button size="xs" onClick={() => onNavigate("/claims/" + claim.id + "/decision")}>Record decision</Button>
                          ) : CLAIM_FLAGS.awaitsAdjuster(claim) ? (
                            <FloatingTip side="bottom" label={holdReason}>
                              <Button size="xs" disabled>Record decision</Button>
                            </FloatingTip>
                          ) : claim.status === "awaiting_documents" || claim.status === "awaiting_survey" ? (
                            <Button variant="outline" size="xs" onClick={() => onChase(claim)}>{claimTransitions(claim)[0].label}</Button>
                          ) : null}
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          <div className="cd-foot">
            <span>Showing {rows.length} of {inTab.length} {spec.label.toLowerCase()}{query ? " matching “" + query + "”" : ""} · {formatPlural(claims.length, "claim")} on the desk</span>
            <span>Sorted by days left, soonest first.</span>
          </div>
        </section>
      </div>
    );
  }

  Object.assign(window, { QueueScreen, QUEUE_TABS });
})();

</script>
<script type="text/babel" data-presets="react" data-file="ClaimScreen.jsx">
/* Claim — assessment. The reconciliation between what the customer claims and
   what we owe is the anchor of this screen; everything else is a rail.
   Commit model: STAGED. Every control here is a form field: the three
   adjustments apply on Save assessment, Save and Cancel are disabled until the
   form is dirty and valid, and Save shows pending. The header's transitions are
   not controls on this form — they navigate, or they open a dialog carrying its
   own commit model.
   Data state: populated (a claim that does not exist is a branch in App). */
(() => {
  const {
    Button, Input, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Alert, AlertTitle, AlertDescription, Badge, Separator,
  } = window.OriginDesignSystem_7c998b;

  const sdr = (n) => new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(n) + " SDR";

  /* The transitions offered on a claim, derived from its status — the same
     function for all three claim screens. */
  function ClaimHeaderActions({ claim, view, onNavigate, onTransition }) {
    const transitions = claimTransitions(claim);
    const missing = CLAIM_FLAGS.missingMandatory(claim);
    const recovery = claim.recoveryId;
    return (
      <>
        {transitions.map((t) => {
          if (t.id === "record_decision") {
            /* One entry point per action per screen: on the decision screen the
               form below is the entry point, so the header does not repeat it. */
            if (view === "decision") return null;
            const blocked = missing.length > 0;
            const button = <Button size="sm" disabled={blocked} onClick={() => onNavigate("/claims/" + claim.id + "/decision")}>{t.label}</Button>;
            return blocked
              ? <FloatingTip key={t.id} side="bottom" label={"Waiting on evidence — " + missing.map((m) => labelOf(DOCUMENT_TYPES, m).toLowerCase()).join(", ")}>{button}</FloatingTip>
              : <React.Fragment key={t.id}>{button}</React.Fragment>;
          }
          if (t.id === "schedule_payment") {
            return <Button key={t.id} variant="outline" size="sm" onClick={() => onNavigate("/payments")}>Open on the payments desk</Button>;
          }
          return (
            <Button key={t.id} variant="outline" size="sm" onClick={() => onTransition(claim, t)}>{t.label}</Button>
          );
        })}
        {recovery ? <Button variant="ghost" size="sm" onClick={() => onNavigate("/recovery/" + recovery)}>Recovery file<Icon name="arrow-right" /></Button> : null}
      </>
    );
  }

  /* Identity, clock, cargo and the three views — identical on all three claim
     screens, so only the surface below changes. */
  function ClaimHeader({ claim, now, view, onNavigate, onTransition }) {
    return (
      <RecordHead
        claim={claim}
        now={now}
        crumb={<Crumbs path={CLAIM_VIEWS.find((v) => v.value === view).route.path} params={{ id: claim.id }} labels={{ "/claims/:id": claim.id }} />}
        actions={<ClaimHeaderActions claim={claim} view={view} onNavigate={onNavigate} onTransition={onTransition} />}
      >
        <ClaimTabs claim={claim} view={view} onNavigate={onNavigate} />
      </RecordHead>
    );
  }

  function LedgerRow({ label, hint, value, className }) {
    return (
      <div className={"cd-lg__row" + (className ? " " + className : "")}>
        <span className="cd-lg__label">{label}{hint ? <span className="cd-lg__hint">{hint}</span> : null}</span>
        <span className="cd-lg__value">{value}</span>
      </div>
    );
  }

  function AdjustmentRow({ id, label, hint, value, error, onChange }) {
    return (
      <div className="cd-lg__row cd-lg__row--input">
        <span className="cd-lg__label">
          <label htmlFor={id}>{label}</label>
          <span className="cd-lg__hint">{error ? <span style={{ color: "var(--destructive-foreground)" }}>{error}</span> : hint}</span>
        </span>
        <span className="cd-lg__value">
          <Input id={id} size="sm" inputMode="decimal" startAddon="−  $" wrapperClassName="cd-lg__input" value={value} invalid={!!error} onChange={(e) => onChange(e.target.value)} />
        </span>
      </div>
    );
  }

  function ProportionBars({ ledger }) {
    const max = Math.max(ledger.claimed, ledger.assessed, ledger.cap.cents);
    const pct = (n) => Math.max(0.5, (n / max) * 100) + "%";
    const rows = [
      { label: "Claimed", value: ledger.claimed, strong: false },
      { label: "Assessed", value: ledger.assessed, strong: false },
      { label: "Payable", value: ledger.payable, strong: true },
    ];
    return (
      <div className="cd-bars">
        {rows.map((row) => (
          <div className="cd-bars__row" key={row.label}>
            <span className="cd-muted">{row.label}</span>
            <span className="cd-bars__track">
              <span className={"cd-bars__fill" + (row.strong ? " cd-bars__fill--strong" : "")} style={{ width: pct(row.value) }} />
              <span className="cd-bars__cap" style={{ left: pct(ledger.cap.cents) }} title={"Liability cap " + formatMoney(ledger.cap.cents)} />
            </span>
            <span className="cd-bars__value">{formatMoney(row.value)}</span>
          </div>
        ))}
        <p className="cd-note">The red line is the liability cap at {formatMoney(ledger.cap.cents)}. Payable can touch it, never cross it.</p>
      </div>
    );
  }

  function ClaimScreen({ claim, data, now, onNavigate, onTransition, onSaveAssessment, pushToast }) {
    const blank = { depreciation: "", salvage: "", deductible: "" };
    const fromClaim = (c) => (c.assessment
      ? { depreciation: moneyInputValue(c.assessment.depreciationCents), salvage: moneyInputValue(c.assessment.salvageCents), deductible: moneyInputValue(c.assessment.deductibleCents) }
      : blank);
    const [draft, setDraft] = React.useState(() => fromClaim(claim));
    const [touched, setTouched] = React.useState({});
    const [saving, setSaving] = React.useState(false);
    React.useEffect(() => { setDraft(fromClaim(claim)); setTouched({}); }, [claim]);

    const saved = fromClaim(claim);
    const dirty = ["depreciation", "salvage", "deductible"].some((k) => draft[k] !== saved[k]);
    const cents = (key) => (draft[key].trim() === "" ? 0 : parseMoney(draft[key]));
    const fieldError = (key) => {
      const value = cents(key);
      if (isNaN(value) || value < 0) return "Enter an amount in dollars, or 0.";
      return null;
    };
    const drafted = { depreciationCents: cents("depreciation"), salvageCents: cents("salvage"), deductibleCents: cents("deductible") };
    const anyFieldError = ["depreciation", "salvage", "deductible"].some((k) => fieldError(k));
    const ledger = anyFieldError ? assess(claim, claim.assessment) : assess(claim, drafted);
    const overClaimed = !anyFieldError && ledger.assessed !== null && ledger.assessed < 0;
    const valid = !anyFieldError && !overClaimed;

    const save = () => {
      setSaving(true);
      window.setTimeout(() => {
        setSaving(false);
        onSaveAssessment(claim, drafted);
        pushToast({ variant: "success", title: "Assessment saved", description: claim.id + " · payable " + formatMoney(Math.min(claim.claimedCents - (drafted.depreciationCents + drafted.salvageCents + drafted.deductibleCents), liabilityCap(claim).cents)) });
      }, 500);
    };

    const cap = liabilityCap(claim);
    const capHint = "Higher of " + formatCount(claim.packages) + " packages × " + CLAIM_POLICY.packageLimitSdr + " SDR (" + sdr(cap.byPackageSdr) + ") and " + formatCount(claim.weightKg) + " kg × " + CLAIM_POLICY.weightLimitSdrPerKg + " SDR (" + sdr(cap.byWeightSdr) + "), at " + CLAIM_POLICY.sdrUsdRate + " USD/SDR";
    const missing = CLAIM_FLAGS.missingMandatory(claim);
    const recovery = recoveryForClaim(data.recoveries, claim);

    return (
      <div className="cd-page">
        <ClaimHeader claim={claim} now={now} view="assessment" onNavigate={onNavigate} onTransition={onTransition} />

        <div className="cd-split">
          <div className="cd-col">
            <Card className="cd-card">
              <CardHeader>
                <CardTitle>What we owe</CardTitle>
                <CardDescription>
                  The customer claims {formatMoney(claim.claimedCents)}. Everything below it is an adjustment you can defend in writing, and the cap is the last word.
                </CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-lg">
                  <LedgerRow label="Claimed by the customer" hint={"Commercial invoice value · " + claim.commodity} value={formatMoney(claim.claimedCents)} />
                  <AdjustmentRow id="cd-dep" label="Depreciation and betterment" hint="Age and condition of the goods at shipment" value={draft.depreciation} error={touched.depreciation ? fieldError("depreciation") : null} onChange={(v) => { setTouched((t) => Object.assign({}, t, { depreciation: true })); setDraft((d) => Object.assign({}, d, { depreciation: v })); }} />
                  <AdjustmentRow id="cd-sal" label="Salvage recovered" hint="Proceeds the consignee realised on the damaged goods" value={draft.salvage} error={touched.salvage ? fieldError("salvage") : null} onChange={(v) => { setTouched((t) => Object.assign({}, t, { salvage: true })); setDraft((d) => Object.assign({}, d, { salvage: v })); }} />
                  <AdjustmentRow id="cd-ded" label="Deductible" hint="Bill of lading clause 14" value={draft.deductible} error={touched.deductible ? fieldError("deductible") : null} onChange={(v) => { setTouched((t) => Object.assign({}, t, { deductible: true })); setDraft((d) => Object.assign({}, d, { deductible: v })); }} />
                  {ledger.assessed === null ? (
                    <LedgerRow className="cd-lg__row--sub" label="Assessed liability" hint="Enter the adjustments above to assess this claim" value={<span className="cd-muted">Not assessed</span>} />
                  ) : (
                    <>
                      <LedgerRow className="cd-lg__row--sub" label="Assessed liability" hint={"Claimed less " + formatMoney(ledger.deductions) + " of adjustments"} value={formatMoney(ledger.assessed)} />
                      <LedgerRow className="cd-lg__row--cap" label="Liability cap, bill of lading" hint={capHint} value={formatMoney(ledger.cap.cents)} />
                      <LedgerRow
                        className="cd-lg__row--payable"
                        label={<span style={{ fontWeight: "var(--font-weight-medium)" }}>Payable today</span>}
                        hint={ledger.capBinds
                          ? "The cap binds. " + formatMoney(ledger.cappedAway) + " of assessed liability is not payable, on the " + ledger.cap.basis + " limitation."
                          : "Below the cap by " + formatMoney(ledger.cap.cents - ledger.assessed) + ", so the assessment stands."}
                        value={formatMoney(ledger.payable)}
                      />
                    </>
                  )}
                </div>
                {overClaimed ? (
                  <Alert variant="error" style={{ marginTop: "var(--space-4)" }}>
                    <AlertTitle>The adjustments exceed the claim</AlertTitle>
                    <AlertDescription>{formatMoney(drafted.depreciationCents + drafted.salvageCents + drafted.deductibleCents)} of adjustments against {formatMoney(claim.claimedCents)} claimed leaves nothing to assess. Reduce one of them.</AlertDescription>
                  </Alert>
                ) : null}
                {ledger.assessed !== null && !overClaimed ? <div style={{ marginTop: "var(--space-6)" }}><ProportionBars ledger={ledger} /></div> : null}
              </CardPanel>
              <CardFooter className="cd-form-foot">
                <span className="cd-form-foot__note">
                  {claim.assessment
                    ? "Last assessed by " + claim.assessment.assessedBy + " on " + formatAbsolute(claim.assessment.assessedAt) + "."
                    : "This claim has not been assessed yet."}
                  {CLAIM_FLAGS.needsSignoff(ledger.payable || 0) ? " Above your " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority, so the decision needs " + CLAIMS_MANAGER.name + "'s sign-off." : ""}
                </span>
                {dirty ? <Button variant="outline" onClick={() => { setDraft(saved); setTouched({}); }}>Cancel</Button> : null}
                <Button disabled={!dirty || !valid} loading={saving} onClick={save}>Save assessment</Button>
              </CardFooter>
            </Card>
          </div>

          <div className="cd-col cd-col--tight">
            <Card>
              <CardHeader>
                <CardTitle>Cargo and carriage</CardTitle>
                <CardDescription>From the bill of lading and the delivery receipt.</CardDescription>
              </CardHeader>
              <CardPanel>
                <dl className="cd-defs">
                  <dt>Bill of lading</dt><dd className="cd-code">{claim.bl}</dd>
                  <dt>Container</dt><dd className="cd-code">{claim.container}</dd>
                  <dt>Vessel and voyage</dt><dd>{claim.vessel} {claim.voyage}</dd>
                  <dt>Loaded at</dt><dd>{claim.pol}</dd>
                  <dt>Discharged at</dt><dd>{claim.pod}</dd>
                  <dt>Commodity</dt><dd>{claim.commodity}</dd>
                  <dt>Packages</dt><dd>{formatCount(claim.packages)}</dd>
                  <dt>Gross weight</dt><dd>{formatCount(claim.weightKg)} kg</dd>
                  <dt>Delivered</dt><dd>{formatAbsolute(claim.deliveredAt)}</dd>
                  <dt>Claim filed</dt><dd>{formatAbsolute(claim.claimedAt)}</dd>
                </dl>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Evidence</CardTitle>
                <CardDescription>
                  {missing.length === 0
                    ? "All " + MANDATORY_DOCUMENT_TYPES.length + " mandatory documents are on file."
                    : (MANDATORY_DOCUMENT_TYPES.length - missing.length) + " of " + MANDATORY_DOCUMENT_TYPES.length + " mandatory documents on file."}
                </CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-col cd-col--tight">
                  <DocumentPips documents={claim.documents} />
                  {missing.length ? (
                    <p className="cd-note">Missing: {missing.map((m) => labelOf(DOCUMENT_TYPES, m)).join(", ")}. You cannot record a decision until they are on file.</p>
                  ) : (
                    <p className="cd-note">Nothing outstanding — the file can be decided. {DOCUMENT_TYPES.length} checklist rows, {MANDATORY_DOCUMENT_TYPES.length} of them mandatory.</p>
                  )}
                </div>
              </CardPanel>
              <CardFooter>
                <Button variant="ghost" size="sm" onClick={() => onNavigate("/claims/" + claim.id + "/evidence")}>Open evidence<Icon name="arrow-right" /></Button>
              </CardFooter>
            </Card>

            {claim.notes && claim.notes.length ? (
              <Card>
                <CardHeader>
                  <CardTitle>File notes</CardTitle>
                  <CardDescription>{formatPlural(claim.notes.length, "entry", "entries")}, newest first.</CardDescription>
                </CardHeader>
                <CardPanel>
                  <div className="cd-log">
                    {claim.notes.slice().sort((a, b) => b.at - a.at).map((note) => (
                      <div className="cd-log__item" key={note.at}>
                        <span className="cd-log__who">{note.who}</span>
                        <span className="cd-log__when" title={formatAbsolute(note.at)}>{formatWhen(note.at, now)}</span>
                        <span className="cd-log__text">{note.text}</span>
                      </div>
                    ))}
                  </div>
                </CardPanel>
              </Card>
            ) : null}

            {recovery ? (
              <Card>
                <CardHeader>
                  <CardTitle>Recovery</CardTitle>
                  <CardDescription>{recovery.counterparty.name} · {labelOf(COUNTERPARTY_KINDS, recovery.counterparty.kind)}</CardDescription>
                </CardHeader>
                <CardPanel>
                  <dl className="cd-defs">
                    <dt>Sought</dt><dd className="cd-amount">{formatMoney(recovery.soughtCents)}</dd>
                    <dt>Recovered</dt><dd className="cd-amount">{formatMoney(recovery.recoveredCents)}</dd>
                    <dt>Status</dt><dd><StatusBadge values={RECOVERY_STATUS} value={recovery.status} /></dd>
                  </dl>
                </CardPanel>
                <CardFooter>
                  <Button variant="ghost" size="sm" onClick={() => onNavigate("/recovery/" + recovery.id)}>Open the recovery file<Icon name="arrow-right" /></Button>
                </CardFooter>
              </Card>
            ) : null}
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { ClaimScreen, ClaimHeader, ClaimHeaderActions, LedgerRow, ProportionBars });
})();

</script>
<script type="text/babel" data-presets="react" data-file="EvidenceScreen.jsx">
/* Claim — evidence. What is on file, what is missing, and what is unusable.
   Commit model: INSTANT. No Save button; requesting a document again and
   marking one illegible apply immediately and raise an undo toast. Both are
   reversible judgements, so neither opens a confirm dialog.
   Data state: populated — every claim in the fixture carries all five
   checklist rows, in one of three states. */
(() => {
  const {
    Button, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Alert, AlertTitle, AlertDescription, AlertAction, Badge,
  } = window.OriginDesignSystem_7c998b;

  function EvidenceScreen({ claim, now, onNavigate, onTransition, onDocumentAction }) {
    const received = claim.documents.filter((d) => MANDATORY_DOCUMENT_TYPES.indexOf(d.type) > -1 && d.status === "received");
    const missing = CLAIM_FLAGS.missingMandatory(claim);
    const illegible = claim.documents.filter((d) => d.status === "illegible");
    const receipt = claim.documents.find((d) => d.type === "delivery_receipt");
    const owed = missing.map((type) => {
      const doc = claim.documents.find((d) => d.type === type);
      const from = type === "surveyor_report" ? "the surveyor" : "the customer";
      return { type: type, from: from, status: doc ? doc.status : "requested" };
    });

    return (
      <div className="cd-page">
        <ClaimHeader claim={claim} now={now} view="evidence" onNavigate={onNavigate} onTransition={onTransition} />

        <div className="cd-bleed">
          <div className="cd-split cd-split--band">
            <div className="cd-section">
              <div className="cd-figures">
                <Figure
                  xl
                  label="Mandatory documents on file"
                  value={received.length + " of " + MANDATORY_DOCUMENT_TYPES.length}
                  note={illegible.length ? illegible.length + " rejected as illegible, and rejected counts as not on file." : "Port photographs are supporting evidence, not mandatory."}
                />
                <div style={{ minWidth: "14rem", alignSelf: "center" }}>
                  <DocumentPips documents={claim.documents} />
                  <div className="cd-figure__note" style={{ marginTop: "var(--space-2)" }}>One pip per checklist row, in the order below.</div>
                </div>
              </div>
            </div>
            <div className="cd-section">
              {missing.length ? (
                <Alert variant="warning">
                  <AlertTitle>You cannot decide this claim yet</AlertTitle>
                  <AlertDescription>
                    {missing.map((m) => labelOf(DOCUMENT_TYPES, m)).join(", ")} {missing.length === 1 ? "is" : "are"} outstanding. The decision surface stays closed until every mandatory document is on file.
                  </AlertDescription>
                </Alert>
              ) : (
                <Alert variant="success">
                  <AlertTitle>The file is complete</AlertTitle>
                  <AlertDescription>All {MANDATORY_DOCUMENT_TYPES.length} mandatory documents are on file, so this claim can be decided — the decision is in the header above.</AlertDescription>
                </Alert>
              )}
            </div>
          </div>
        </div>

        <div className="cd-split cd-split--even">
          <Card>
            <CardHeader>
              <CardTitle>Document checklist</CardTitle>
              <CardDescription>Mandatory rows are marked. Nothing here is deleted — a document rejected as illegible stays on the file with its reason.</CardDescription>
            </CardHeader>
            <CardPanel>
              <div className="cd-rows">
                {DOCUMENT_TYPES.map((type) => {
                  const doc = claim.documents.find((d) => d.type === type.value) || { type: type.value, status: "requested", receivedAt: null, note: null };
                  const transitions = documentTransitions(doc);
                  return (
                    <div className="cd-doc" key={type.value}>
                      <div className="cd-doc__main">
                        <span className="cd-doc__title">
                          {type.label}
                          {type.mandatory ? <Badge size="sm" variant="outline">Mandatory</Badge> : <span className="cd-muted" style={{ fontSize: "var(--text-xs)", fontWeight: "var(--font-weight-normal)" }}>Supporting</span>}
                        </span>
                        <span className="cd-doc__note">
                          {doc.receivedAt ? "Received " + formatAbsolute(doc.receivedAt) : "Not received"}
                          {doc.note ? " — " + doc.note : ""}
                        </span>
                      </div>
                      <div className="cd-doc__side">
                        <StatusBadge values={DOCUMENT_STATUS} value={doc.status} />
                        {transitions.length ? (
                          <span className="cd-actions__row">
                            {transitions.map((t) => (
                              <Button
                                key={t.id}
                                size="xs"
                                variant="outline"
                                onClick={() => onDocumentAction(claim, doc, t)}
                              >{t.label}</Button>
                            ))}
                          </span>
                        ) : null}
                      </div>
                    </div>
                  );
                })}
              </div>
            </CardPanel>
            <CardFooter className="cd-foot">
              <span>{claim.documents.filter((d) => d.status === "received").length} of {DOCUMENT_TYPES.length} checklist rows received, {MANDATORY_DOCUMENT_TYPES.length} of them mandatory.</span>
            </CardFooter>
          </Card>

          <div className="cd-col cd-col--tight">
            <Card>
              <CardHeader>
                <CardTitle>What the delivery receipt says</CardTitle>
                <CardDescription>The exception noted at delivery is where a cargo claim is won or lost.</CardDescription>
              </CardHeader>
              <CardPanel>
                {receipt && receipt.status === "received" ? (
                  <div className="cd-letter">
                    <p className="cd-letter__quote">{receipt.note || "Received without exception."}</p>
                    <p className="cd-note">Signed at {claim.pod} on {formatAbsolute(claim.deliveredAt)}.</p>
                  </div>
                ) : (
                  <p className="cd-note">The delivery receipt is {receipt ? labelOf(DOCUMENT_STATUS.map((s) => ({ value: s.id, label: s.label.toLowerCase() })), receipt.status) : "not on file"}, so there is no exception on record. Without it the carrier cannot show the condition of the cargo at delivery.</p>
                )}
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Who owes what</CardTitle>
                <CardDescription>{owed.length ? owed.length + " outstanding" : "Nothing outstanding"}</CardDescription>
              </CardHeader>
              <CardPanel>
                {owed.length ? (
                  <div className="cd-rows">
                    {owed.map((row) => (
                      <div className="cd-row" key={row.type}>
                        <div className="cd-row__main">
                          <span className="cd-record__name">{labelOf(DOCUMENT_TYPES, row.type)}</span>
                          <span className="cd-record__meta">Owed by {row.from}</span>
                        </div>
                        <div className="cd-row__side">
                          <StatusBadge values={DOCUMENT_STATUS} value={row.status} />
                        </div>
                      </div>
                    ))}
                  </div>
                ) : (
                  <p className="cd-note">Every mandatory document is on file. Port photographs are supporting evidence only, so a gap there never blocks a decision.</p>
                )}
              </CardPanel>
            </Card>
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="DecisionScreen.jsx">
/* Claim — decision. The one screen where the desk commits.
   Commit model: STAGED. The outcome, the part amount, the reason and the
   justification are all form fields; nothing is recorded until the submit, the
   submit is disabled until the form is dirty and valid, Cancel appears once
   dirty, and the submit shows pending. The header's transitions navigate or
   open their own dialog.
   Data state: populated. The four branches below are record states, not data
   states: already decided · not yet assessed · evidence outstanding · ready. */
(() => {
  const {
    Button, Input, Textarea, Select, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Alert, AlertTitle, AlertDescription, AlertAction, Radio, Badge, Separator,
  } = window.OriginDesignSystem_7c998b;

  const MIN_JUSTIFICATION = 40;

  function DecisionOption({ outcome, selected, hint, disabled, onSelect, children }) {
    return (
      <div
        className="cd-option"
        role="radio"
        aria-checked={selected}
        aria-disabled={disabled || undefined}
        tabIndex={disabled ? -1 : 0}
        onClick={() => { if (!disabled) onSelect(); }}
        onKeyDown={(e) => { if (!disabled && (e.key === " " || e.key === "Enter")) { e.preventDefault(); onSelect(); } }}
      >
        <Radio checked={selected} disabled={disabled} onSelect={() => { if (!disabled) onSelect(); }} name="cd-outcome" />
        <span className="cd-option__body">
          <span className="cd-option__label">{outcome.label}</span>
          <span className="cd-option__hint">{hint}</span>
        </span>
        {selected && children ? <div className="cd-option__extra" onClick={(e) => e.stopPropagation()}>{children}</div> : null}
      </div>
    );
  }

  /* The letter the customer eventually reads, rendered from the values in the
     form — not a description of them. */
  function OfferPreview({ claim, outcome, amount, reasonValue, justification }) {
    if (!outcome) return <p className="cd-note">Pick an outcome and the letter builds here, from the figures and the words in the form.</p>;
    return (
      <div className="cd-letter">
        <p>Dear {claim.customer},</p>
        <p>Claim {claim.id}, container {claim.container}, {claim.vessel} {claim.voyage}.</p>
        {outcome === "reject" ? (
          <p>We have completed our investigation and regret that we cannot accept liability for this loss. Our reason is {(labelOf(REJECT_REASONS, reasonValue) || "—").toLowerCase()}.</p>
        ) : (
          <p>We have completed our investigation and offer <strong>{isNaN(amount) ? "—" : formatMoney(amount)}</strong> in full and final settlement of this claim.</p>
        )}
        {justification.trim() ? <p className="cd-letter__quote">{justification.trim()}</p> : null}
        <p>{CLAIMS_USER.name}, {CLAIMS_USER.role}<br />{CLAIMS_CARRIER.name}</p>
      </div>
    );
  }

  function RecordedDecision({ claim, onNavigate }) {
    const outcomeLabel = labelOf(DECISION_OUTCOMES, claim.decision.outcome);
    return (
      <Card className="cd-decide">
        <CardHeader>
          <CardTitle>{outcomeLabel} — recorded {formatAbsolute(claim.decision.decidedAt)}</CardTitle>
          <CardDescription>
            By {claim.decision.decidedBy}
            {claim.decision.signedOffBy ? ", signed off by " + claim.decision.signedOffBy + " above the " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority limit." : ", within the " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority limit."}
          </CardDescription>
        </CardHeader>
        <CardPanel>
          <div className="cd-lg">
            {claim.settlement ? (
              <LedgerRow label="Settled at" hint={"Payment " + (claim.settlement.reference || "not yet referenced")} value={<span className="cd-figure" style={{ fontSize: "var(--text-2xl)" }}>{formatMoney(claim.settlement.amountCents)}</span>} />
            ) : null}
            {claim.decision.rejectReason ? (
              <LedgerRow label="Reason" hint="From the rejection list" value={labelOf(REJECT_REASONS, claim.decision.rejectReason)} />
            ) : null}
          </div>
          {claim.decision.justification ? (
            <div style={{ marginTop: "var(--space-5)" }} className="cd-letter">
              <span className="cd-h3">Justification sent to the customer</span>
              <p className="cd-letter__quote">{claim.decision.justification}</p>
            </div>
          ) : null}
        </CardPanel>
        <CardFooter className="cd-form-foot">
          <span className="cd-form-foot__note">A recorded decision is not edited here. If the customer disputes it, the offer comes back through Responses.</span>
          {claim.settlement ? <Button variant="outline" onClick={() => onNavigate("/payments")}>Open on the payments desk</Button> : null}
          {claim.response ? <Button variant="ghost" onClick={() => onNavigate("/responses")}>See the customer's response<Icon name="arrow-right" /></Button> : null}
        </CardFooter>
      </Card>
    );
  }

  function DecisionScreen({ claim, now, onNavigate, onTransition, onRecordDecision }) {
    const [outcome, setOutcome] = React.useState("");
    const [partAmount, setPartAmount] = React.useState("");
    const [reasonValue, setReasonValue] = React.useState("");
    const [justification, setJustification] = React.useState("");
    const [submitted, setSubmitted] = React.useState(false);
    const [saving, setSaving] = React.useState(false);
    React.useEffect(() => { setOutcome(""); setPartAmount(""); setReasonValue(""); setJustification(""); setSubmitted(false); }, [claim.id]);

    const ledger = assess(claim);
    const missing = CLAIM_FLAGS.missingMandatory(claim);
    const payable = ledger.payable;
    const amount = outcome === "approve_full" ? payable : outcome === "approve_part" ? parseMoney(partAmount) : 0;
    const needsSignoff = outcome && outcome !== "reject" && CLAIM_FLAGS.needsSignoff(isNaN(amount) ? 0 : amount);
    const justificationShort = justification.trim().length < MIN_JUSTIFICATION;
    const errors = {};
    if (outcome === "approve_part") {
      if (isNaN(amount) || amount <= 0) errors.amount = "Enter the amount you are offering.";
      else if (amount > payable) errors.amount = "A part approval cannot exceed the payable figure of " + formatMoney(payable) + ".";
      if (justificationShort) errors.justification = "A part approval needs a written justification of at least " + MIN_JUSTIFICATION + " characters.";
    }
    if (outcome === "reject") {
      if (!reasonValue) errors.reason = "Pick the reason the claim is refused.";
      if (justificationShort) errors.justification = "A rejection needs a written justification of at least " + MIN_JUSTIFICATION + " characters.";
    }
    const dirty = !!outcome;
    const valid = dirty && Object.keys(errors).length === 0;
    const submitLabel = outcome === "reject" ? "Reject claim" : needsSignoff ? "Send for sign-off" : "Approve claim";

    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      setSaving(true);
      window.setTimeout(() => {
        setSaving(false);
        onRecordDecision(claim, {
          outcome: outcome,
          amountCents: outcome === "reject" ? 0 : amount,
          rejectReason: outcome === "reject" ? reasonValue : null,
          justification: justification.trim() || null,
          needsSignoff: needsSignoff,
        });
      }, 600);
    };

    const body = () => {
      if (claim.decision) return <RecordedDecision claim={claim} onNavigate={onNavigate} />;
      if (missing.length) {
        return (
          <Card className="cd-decide">
            <CardHeader>
              <CardTitle>The decision surface is closed</CardTitle>
              <CardDescription>A decision on incomplete evidence is a decision we cannot defend, so this claim cannot be decided yet.</CardDescription>
            </CardHeader>
            <CardPanel>
              <Alert variant="warning">
                <AlertTitle>{missing.length === 1 ? "One mandatory document is outstanding" : missing.length + " mandatory documents are outstanding"}</AlertTitle>
                <AlertDescription>{missing.map((m) => labelOf(DOCUMENT_TYPES, m)).join(", ")}. Once every mandatory document is on file, the three outcomes open here.</AlertDescription>
                <AlertAction>
                  <Button variant="outline" size="sm" onClick={() => onNavigate("/claims/" + claim.id + "/evidence")}>Open evidence</Button>
                </AlertAction>
              </Alert>
            </CardPanel>
          </Card>
        );
      }
      if (ledger.assessed === null) {
        return (
          <Card className="cd-decide">
            <CardHeader>
              <CardTitle>Assess the claim first</CardTitle>
              <CardDescription>There is nothing to approve until the adjustments and the cap have been worked out.</CardDescription>
            </CardHeader>
            <CardPanel>
              <Alert variant="info">
                <AlertTitle>No assessment on file</AlertTitle>
                <AlertDescription>{claim.customer} claims {formatMoney(claim.claimedCents)}. Record depreciation, salvage and the deductible on the assessment, and the payable figure appears here.</AlertDescription>
                <AlertAction>
                  <Button variant="outline" size="sm" onClick={() => onNavigate("/claims/" + claim.id)}>Open assessment</Button>
                </AlertAction>
              </Alert>
            </CardPanel>
          </Card>
        );
      }
      return (
        <Card className="cd-decide">
          <CardHeader>
            <CardTitle>Decide {claim.id}</CardTitle>
            <CardDescription>
              Payable today is {formatMoney(payable)}{ledger.capBinds ? ", held at the bill of lading cap — " + formatMoney(ledger.cappedAway) + " of assessed liability is not payable" : ", comfortably inside the cap of " + formatMoney(ledger.cap.cents)}.
            </CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="cd-options" role="radiogroup" aria-label="Decision outcome">
              <DecisionOption
                outcome={DECISION_OUTCOMES[0]}
                selected={outcome === "approve_full"}
                hint={"Offer the full payable figure of " + formatMoney(payable) + " against the " + formatMoney(claim.claimedCents) + " claimed."}
                onSelect={() => setOutcome("approve_full")}
              />
              <DecisionOption
                outcome={DECISION_OUTCOMES[1]}
                selected={outcome === "approve_part"}
                hint="Offer less than the payable figure. The customer reads your justification, so write it for them."
                onSelect={() => setOutcome("approve_part")}
              >
                <div>
                  <label className="cd-option__label" htmlFor="cd-part">Amount offered</label>
                  <div style={{ marginTop: "var(--space-2)", maxWidth: "12rem" }}>
                    <Input id="cd-part" inputMode="decimal" startAddon="$" placeholder={moneyInputValue(payable)} value={partAmount} invalid={submitted && !!errors.amount} onChange={(e) => setPartAmount(e.target.value)} />
                  </div>
                  <p className="cd-note" style={{ marginTop: "var(--space-2)" }}>
                    {submitted && errors.amount ? <span style={{ color: "var(--destructive-foreground)" }}>{errors.amount}</span> : "Anything up to " + formatMoney(payable) + "."}
                  </p>
                </div>
              </DecisionOption>
              <DecisionOption
                outcome={DECISION_OUTCOMES[2]}
                selected={outcome === "reject"}
                hint="Decline liability. A reason from the list and a written justification are both required."
                onSelect={() => setOutcome("reject")}
              >
                <div>
                  <label className="cd-option__label" htmlFor="cd-reason">Reason</label>
                  <div style={{ marginTop: "var(--space-2)", maxWidth: "22rem" }}>
                    <Select id="cd-reason" items={REJECT_REASONS} value={reasonValue} placeholder="Pick a reason" onValueChange={setReasonValue} />
                  </div>
                  {submitted && errors.reason ? <p className="cd-note" style={{ marginTop: "var(--space-2)", color: "var(--destructive-foreground)" }}>{errors.reason}</p> : null}
                </div>
              </DecisionOption>
            </div>

            {outcome === "approve_part" || outcome === "reject" ? (
              <div style={{ marginTop: "var(--space-6)" }}>
                <label className="cd-option__label" htmlFor="cd-just">Justification the customer will read</label>
                <div style={{ marginTop: "var(--space-2)" }}>
                  <Textarea id="cd-just" rows={5} value={justification} invalid={submitted && !!errors.justification}
                    placeholder={outcome === "reject" ? "Explain, in plain words, why the carrier is not liable for this loss." : "Explain which part of the loss we accept, and why the rest is not payable."}
                    onChange={(e) => setJustification(e.target.value)} />
                </div>
                <div className="cd-counter">
                  <span>{submitted && errors.justification ? <span style={{ color: "var(--destructive-foreground)" }}>{errors.justification}</span> : "It goes out under your name, unedited."}</span>
                  <span>{justification.trim().length} of {MIN_JUSTIFICATION} characters</span>
                </div>
              </div>
            ) : null}
          </CardPanel>
          <CardFooter className="cd-form-foot">
            <span className="cd-form-foot__note">
              {!outcome ? "Nothing is recorded until you submit." : needsSignoff
                ? formatMoney(isNaN(amount) ? 0 : amount) + " is above your " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority, so " + CLAIMS_MANAGER.name + " (" + CLAIMS_MANAGER.role.toLowerCase() + ") signs off before the offer goes out."
                : outcome === "reject" ? "A rejection is yours to make at any value." : formatMoney(isNaN(amount) ? 0 : amount) + " is inside your " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority, so this is yours to approve."}
            </span>
            {dirty ? <Button variant="outline" onClick={() => { setOutcome(""); setPartAmount(""); setReasonValue(""); setJustification(""); setSubmitted(false); }}>Cancel</Button> : null}
            <Button disabled={!dirty} loading={saving} onClick={submit}>{submitLabel}</Button>
          </CardFooter>
        </Card>
      );
    };

    return (
      <div className="cd-page">
        <ClaimHeader claim={claim} now={now} view="decision" onNavigate={onNavigate} onTransition={onTransition} />
        <div className="cd-split">
          <div className="cd-col">{body()}</div>
          <div className="cd-col cd-col--tight">
            <Card>
              <CardHeader>
                <CardTitle>What the customer will see</CardTitle>
                <CardDescription>Built from the figures and the words in the form, not a description of them.</CardDescription>
              </CardHeader>
              <CardPanel>
                {claim.decision ? (
                  <OfferPreview claim={claim} outcome={claim.decision.outcome} amount={claim.settlement ? claim.settlement.amountCents : 0} reasonValue={claim.decision.rejectReason} justification={claim.decision.justification || ""} />
                ) : (
                  <OfferPreview claim={claim} outcome={outcome} amount={amount} reasonValue={reasonValue} justification={justification} />
                )}
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Your authority</CardTitle>
                <CardDescription>{CLAIMS_USER.name} · {CLAIMS_USER.role}</CardDescription>
              </CardHeader>
              <CardPanel>
                <dl className="cd-defs">
                  <dt>Decide alone up to</dt><dd className="cd-amount">{formatMoney(CLAIM_POLICY.authorityLimitCents)}</dd>
                  <dt>Payable on this claim</dt><dd className="cd-amount">{payable === null ? "Not assessed" : formatMoney(payable)}</dd>
                  <dt>This decision</dt>
                  <dd className="cd-amount">
                    {claim.decision ? formatMoney(claim.settlement ? claim.settlement.amountCents : 0) : outcome ? formatMoney(isNaN(amount) ? 0 : amount) : "Not chosen yet"}
                  </dd>
                  <dt>Sign-off</dt>
                  <dd>
                    {claim.decision
                      ? (claim.decision.signedOffBy || "Not required")
                      : outcome
                        ? (needsSignoff ? CLAIMS_MANAGER.name : "Not required")
                        : (payable !== null && CLAIM_FLAGS.needsSignoff(payable) ? CLAIMS_MANAGER.name + ", if you approve in full" : "Not required")}
                  </dd>
                </dl>
              </CardPanel>
            </Card>
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { DecisionScreen, DecisionOption, OfferPreview });
})();

</script>
<script type="text/babel" data-presets="react" data-file="SignoffScreen.jsx">
/* Sign-off — what this desk has sent up, and what has come back.
   Commit model: INSTANT. Both row actions apply immediately and raise an undo
   toast; nothing here is irreversible and there is no Save button.
   Data state: populated, or the empty branch when nothing is waiting — one
   conditional, never both.
   Scope: everything on this desk above the adjuster's authority. No period
   filter, so no figure moves when the list does.

   Recession, deliberately not applied: every row here is waiting on the same
   person, so recessing all of them would carry no information. §5.4's real
   requirement — name what is being waited on — is met by the "With" column,
   which names the manager and how long he has had it. */
(() => {
  const {
    Button,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* A wait long enough to be worth a sentence rather than a number. */
  const STALE_SIGNOFF_DAYS = 7;

  function SignoffScreen({ data, now, onNavigate, onTransition }) {
    const claims = data.claims;
    const waiting = bySignoffWait(claims);
    const decided = decidedClaims(claims);
    const signed = signedOffClaims(claims);
    const oldest = longestSignoffWait(claims);
    const stale = waiting.filter((c) => daysSince(CLAIM_FLAGS.signoffWaitingSince(c), now) >= STALE_SIGNOFF_DAYS);
    const recentlySigned = signed.slice().sort((a, b) => b.decision.decidedAt - a.decision.decidedAt).slice(0, 4);

    return (
      <div className="cd-page">
        <PageHead
          path="/signoff"
          title="Sign-off"
          lede={"Decisions above your " + formatMoney(CLAIM_POLICY.authorityLimitCents) + " authority. " + CLAIMS_MANAGER.name + " signs them; you can chase one or take it back, and nothing here is yours to approve."}
        />

        <div className="cd-bleed">
          <div className="cd-figures">
            <Figure
              xl
              label="Waiting on a signature"
              value={waiting.length ? formatMoney(signoffValue(waiting)) : "Nothing waiting"}
              note={waiting.length ? formatPlural(waiting.length, "decision") + " with " + CLAIMS_MANAGER.name : "Nothing is with " + CLAIMS_MANAGER.name}
            />
            <Figure
              label="Longest wait"
              value={oldest ? formatPlural(daysSince(CLAIM_FLAGS.signoffWaitingSince(oldest), now), "day") : "—"}
              note={oldest ? oldest.id + " · " + oldest.customer : "No decision is waiting"}
            />
            <Figure
              label="Signed off on this desk"
              value={formatCount(signed.length)}
              note={"of " + formatPlural(decided.length, "decision") + " recorded, the rest inside your own authority"}
            />
          </div>
          {stale.length ? (
            <p className="cd-note" style={{ maxWidth: "46rem" }}>
              {stale.map((c) => c.id).join(", ")} {stale.length === 1 ? "has" : "have"} been unsigned for more than {formatPlural(STALE_SIGNOFF_DAYS, "day")}. Chasing writes a note to the file so the claimant can be told a date.
            </p>
          ) : null}
        </div>

        <section className="cd-section">
          <div className="cd-section__head">
            <h2 className="cd-h2">With {CLAIMS_MANAGER.name}</h2>
            <p className="cd-note">Sorted by how long each has been waiting, longest first.</p>
          </div>

          {waiting.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="check" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing is waiting on a signature</EmptyTitle>
                <EmptyDescription>Every decision recorded on this desk was inside your {formatMoney(CLAIM_POLICY.authorityLimitCents)} authority, or has already been signed.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => onNavigate("/")}>Open the queue</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="cd-table-wrap">
              <Table className="cd-tbl">
                <TableHeader>
                  <TableRow>
                    <TableHead>Claim</TableHead>
                    <TableHead className="cd-drop-lg">Decision</TableHead>
                    <TableHead className="cd-num">Payable</TableHead>
                    <TableHead className="cd-num cd-drop-md">Over authority by</TableHead>
                    <TableHead>With</TableHead>
                    <TableHead className="cd-actions"><span className="o-sr">Action</span></TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {waiting.map((claim) => {
                    const payable = payableOf(claim);
                    const waited = daysSince(CLAIM_FLAGS.signoffWaitingSince(claim), now);
                    const chase = claimTransitions(claim).find((t) => t.id === "chase_signoff");
                    return (
                      <TableRow key={claim.id}>
                        <TableCell>
                          <span className="cd-record">
                            <button type="button" className="cd-record__link" onClick={() => onNavigate("/claims/" + claim.id)}>{claim.customer}</button>
                            <span className="cd-record__meta"><code>{claim.id}</code> · {claim.container}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-drop-lg cd-muted">
                          <span className="cd-record">
                            <span>{labelOf(DECISION_OUTCOMES, claim.decision.outcome)}</span>
                            <span className="cd-record__meta">Recorded {formatWhen(claim.decision.decidedAt, now)} by {claim.decision.decidedBy}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-num cd-amount">{formatMoney(payable)}</TableCell>
                        <TableCell className="cd-num cd-drop-md cd-muted">{formatMoney(payable - CLAIM_POLICY.authorityLimitCents)}</TableCell>
                        <TableCell>
                          <span className="cd-record">
                            <StatusBadge values={CLAIM_STATUS} value={claim.status} />
                            <span className="cd-record__meta">{CLAIMS_MANAGER.name} · {formatPlural(waited, "day")}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-actions">
                          <Button variant="outline" size="xs" onClick={() => onTransition(claim, chase)}>{chase.label}</Button>
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          <div className="cd-foot">
            <span>{formatPlural(waiting.length, "decision")} waiting · {formatMoney(signoffValue(waiting))} in total</span>
            <span>Taking one back reopens its assessment on your desk.</span>
          </div>
        </section>

        <section className="cd-section">
          <div className="cd-section__head">
            <h2 className="cd-h2">Signed off before</h2>
            <p className="cd-note">The last {recentlySigned.length} of {formatPlural(signed.length, "decision")} {CLAIMS_MANAGER.name} has signed, newest first.</p>
          </div>
          <div className="cd-rows">
            {recentlySigned.map((claim) => (
              <div className="cd-row" key={claim.id}>
                <div className="cd-row__main">
                  <button type="button" className="cd-record__link" onClick={() => onNavigate("/claims/" + claim.id)}>{claim.customer}</button>
                  <span className="cd-record__meta"><code>{claim.id}</code> · {labelOf(DECISION_OUTCOMES, claim.decision.outcome)} · signed by {claim.decision.signedOffBy}</span>
                </div>
                <div className="cd-row__side">
                  <span className="cd-amount">{formatMoney(payableOf(claim))}</span>
                  <span className="cd-record__meta" title={formatAbsolute(claim.decision.decidedAt)}>Decided {formatWhen(claim.decision.decidedAt, now)}</span>
                </div>
              </div>
            ))}
          </div>
        </section>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="PaymentsScreen.jsx">
/* Payments — scheduling the payout and watching it land.
   Commit model: INSTANT. Scheduling applies immediately and raises an undo
   toast; sending money is irreversible and opens a confirm dialog. There is no
   Save button on this screen.
   Data state: populated. The empty branch (no approved claim carries a
   settlement) is the other half of one conditional.
   Recession, chosen once for the product: an open record the adjuster cannot
   act on is recessed — here that is a payment already sitting with the bank. A
   scheduled payment can still be sent, and a cleared one is closed rather than
   blocked, so neither is recessed. */
(() => {
  const {
    Button, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, Badge,
  } = window.OriginDesignSystem_7c998b;

  const STAGE_TONE = {};
  SETTLEMENT_STATUS.forEach((v) => { STAGE_TONE[v.id] = v.tone; });

  /* The date a settlement's own status makes relevant — one rule per column. */
  function settlementWhen(settlement) {
    if (settlement.status === "cleared") return { label: "Cleared", at: settlement.clearedAt };
    if (settlement.status === "returned") return { label: "Returned", at: settlement.returnedAt };
    if (settlement.status === "sent") return { label: "Sent", at: settlement.sentAt };
    if (settlement.status === "scheduled") return { label: "Scheduled", at: settlement.scheduledFor };
    return { label: "Not scheduled", at: null };
  }

  function PaymentsScreen({ data, now, onNavigate, onSettlementTransition, pushToast }) {
    const claims = data.claims;
    const rows = settledClaims(claims).slice().sort((a, b) => (settlementWhen(b.settlement).at || now) - (settlementWhen(a.settlement).at || now));
    const toSchedule = settlementsToSchedule(claims);
    const scheduled = settlementsWith(claims, "scheduled");
    const cleared = settlementsWith(claims, "cleared");
    const pipeline = releasePipeline(claims);
    const pipelineTotal = settlementTotal(rows);
    const runAt = nextRunDate(now);

    return (
      <div className="cd-page">
        <PageHead
          path="/payments"
          title="Payments"
          lede={"Every claim we have approved and not yet closed out. Payment runs leave on Wednesdays; the next one is " + formatAbsolute(runAt) + "."}
          actions={<ExportMenu scope="Settlements" onExport={(format) => pushToast({ title: "Export started", description: "Settlements · " + format + " — we'll email it to " + CLAIMS_USER.name.split(" ")[0].toLowerCase() + "@corvenline.example" })} />}
        />

        <div className="cd-bleed">
          <div className="cd-split cd-split--band">
            <div className="cd-section">
              <div className="cd-figures">
                <Figure
                  xl
                  label="Waiting on you to schedule"
                  value={formatMoney(settlementTotal(toSchedule))}
                  note={toSchedule.length + " of the " + rows.length + " settlements on this desk, including anything the bank has sent back"}
                />
                <Figure
                  label="In the next run"
                  value={formatMoney(settlementTotal(scheduled))}
                  note={scheduled.length + " scheduled for " + formatAbsolute(runAt)}
                />
                <Figure
                  label="Cleared to date"
                  value={formatMoney(settlementTotal(cleared))}
                  note={cleared.length + " settlements in the customer's account"}
                />
              </div>
              <div className="cd-section">
                <h2 className="cd-h3">Where the money is, across {formatMoney(pipelineTotal)}</h2>
                <div className="cd-flow">
                  {pipeline.map((stage) => (
                    <div className="cd-flow__seg" key={stage.id} data-tone={STAGE_TONE[stage.id]} style={{ flexGrow: Math.max(1, Math.round(stage.amount / 1000)), flexBasis: 0 }}>
                      <span className="cd-flow__amount">{formatMoney(stage.amount)}</span>
                      <span className="cd-flow__label">{stage.label} · {stage.count}</span>
                    </div>
                  ))}
                </div>
              </div>
            </div>

            <div className="cd-section">
              <div className="cd-section__head">
                <h2 className="cd-h2">Next run</h2>
                <span className="cd-clock">{formatCountdown(runAt, now)}</span>
              </div>
              <p className="cd-note">{formatAbsolute(runAt)}. Anything scheduled before it leaves that morning.</p>
              <div className="cd-rows">
                {scheduled.length ? scheduled.map((claim) => (
                  <div className="cd-row" key={claim.id}>
                    <div className="cd-row__main">
                      <span className="cd-record__name">{claim.customer}</span>
                      <span className="cd-record__meta"><code>{claim.id}</code> · {claim.settlement.reference}</span>
                    </div>
                    <div className="cd-row__side">
                      <span className="cd-amount">{formatMoney(claim.settlement.amountCents)}</span>
                      <span className="cd-record__meta">{formatWhen(claim.settlement.scheduledFor, now)}</span>
                    </div>
                  </div>
                )) : (
                  <p className="cd-note">Nothing is scheduled for this run yet. {toSchedule.length ? toSchedule.length + " settlements are waiting on you to book one." : ""}</p>
                )}
              </div>
            </div>
          </div>
        </div>

        <section className="cd-section">
          <div className="cd-section__head">
            <h2 className="cd-h2">Settlements</h2>
            <p className="cd-note">Newest movement first.</p>
          </div>
          {rows.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="banknote" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing approved is waiting to be paid</EmptyTitle>
                <EmptyDescription>A settlement appears here the moment a claim is approved on the decision screen.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button variant="outline" onClick={() => onNavigate("/")}>Back to the work queue</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="cd-table-wrap">
              <Table className="cd-tbl">
                <TableHeader>
                  <TableRow>
                    <TableHead>Claim</TableHead>
                    <TableHead className="cd-drop-lg">Reference</TableHead>
                    <TableHead className="cd-num">Amount</TableHead>
                    <TableHead>Settlement</TableHead>
                    <TableHead className="cd-drop-md">Movement</TableHead>
                    <TableHead className="cd-actions"><span className="o-sr">Action</span></TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((claim) => {
                    const settlement = claim.settlement;
                    const when = settlementWhen(settlement);
                    const transitions = settlementTransitions(claim);
                    const recessed = settlement.status === "sent";
                    return (
                      <TableRow key={claim.id} className={recessed ? "cd-row-recessed" : ""}>
                        <TableCell>
                          <span className="cd-record">
                            <button type="button" className="cd-record__link" onClick={() => onNavigate("/claims/" + claim.id)}>{claim.customer}</button>
                            <span className="cd-record__meta"><code>{claim.id}</code> · {labelOf(DECISION_OUTCOMES, claim.decision.outcome)}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-drop-lg cd-muted"><span className="cd-code">{settlement.reference || "—"}</span></TableCell>
                        <TableCell className="cd-num cd-amount">{formatMoney(settlement.amountCents)}</TableCell>
                        <TableCell>
                          <span className="cd-record">
                            <StatusBadge values={SETTLEMENT_STATUS} value={settlement.status} />
                            {settlement.status === "returned" ? <span className="cd-record__meta">{settlement.returnReason}</span> : null}
                            {settlement.status === "sent" ? <span className="cd-record__meta">With the beneficiary bank</span> : null}
                          </span>
                        </TableCell>
                        <TableCell className="cd-drop-md">
                          <span className="cd-record">
                            <span className="cd-muted" style={{ fontSize: "var(--text-xs)" }}>{when.label}</span>
                            <span title={when.at ? formatAbsolute(when.at) : ""}>{when.at ? formatWhen(when.at, now) : "—"}</span>
                          </span>
                        </TableCell>
                        <TableCell className="cd-actions">
                          {transitions.map((t) => (
                            <Button key={t.id} size="xs" onClick={() => onSettlementTransition(claim, t)}>{t.label}</Button>
                          ))}
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          <div className="cd-foot">
            <span>Showing {rows.length} of {rows.length} settlements · {formatMoney(pipelineTotal)} in total</span>
            <span>{toSchedule.length} waiting on you, {scheduled.length} in the run, {settlementsWith(claims, "sent").length} with the bank, {cleared.length} cleared.</span>
          </div>
        </section>
      </div>
    );
  }

  Object.assign(window, { PaymentsScreen, settlementWhen });
})();

</script>
<script type="text/babel" data-presets="react" data-file="ResponsesScreen.jsx">
/* Responses — what the customer did with the offer.
   Commit model: INSTANT. Recording a response opens a staged dialog, which
   carries its own commit model; reopening an assessment applies immediately
   with an undo toast; escalating to legal opens a confirm dialog.
   Data state: populated. The no-dispute branch below is a record state, not a
   second data state: the offer ledger is always there. */
(() => {
  const {
    Button, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, Badge, Separator,
  } = window.OriginDesignSystem_7c998b;

  function DisputedOffer({ claim, now, onNavigate, onResponseTransition }) {
    const gap = RESPONSE_FLAGS.gap(claim);
    const transitions = responseTransitions(claim);
    return (
      <Card className="cd-decide">
        <CardHeader>
          <CardTitle>{claim.customer} has refused our offer</CardTitle>
          <CardDescription>
            <span className="cd-code">{claim.id}</span> · offer sent {formatAbsolute(claim.response.offerSentAt)} · refused {formatAbsolute(claim.response.respondedAt)}
          </CardDescription>
        </CardHeader>
        <CardPanel>
          <div className="cd-lg">
            <LedgerRow label="We offered" hint={labelOf(DECISION_OUTCOMES, claim.decision.outcome) + ", paid " + formatAbsolute(claim.settlement.sentAt)} value={formatMoney(claim.settlement.amountCents)} />
            <LedgerRow label="They demand" hint={"Their claimed figure was " + formatMoney(claim.claimedCents)} value={formatMoney(claim.response.demandCents)} />
            <LedgerRow
              className="cd-lg__row--payable"
              label={<span style={{ fontWeight: "var(--font-weight-medium)" }}>The gap</span>}
              hint={"What is being argued over, on top of the " + formatMoney(claim.settlement.amountCents) + " already paid."}
              value={formatMoney(gap)}
            />
          </div>
          <div className="cd-letter" style={{ marginTop: "var(--space-6)" }}>
            <span className="cd-h3">In their words</span>
            <p className="cd-letter__quote">{claim.response.note}</p>
            <span className="cd-h3">What we told them</span>
            <p className="cd-letter__quote">{claim.decision.justification}</p>
          </div>
        </CardPanel>
        <CardFooter className="cd-form-foot">
          <span className="cd-form-foot__note">Reopening the assessment puts the claim back on your desk with the payment already made; escalating hands the gap to legal.</span>
          {transitions.map((t) => (
            <Button key={t.id} variant={t.primary ? "default" : "outline"} onClick={() => onResponseTransition(claim, t)}>{t.label}</Button>
          ))}
          <Button variant="ghost" onClick={() => onNavigate("/claims/" + claim.id)}>Open the claim<Icon name="arrow-right" /></Button>
        </CardFooter>
      </Card>
    );
  }

  function ResponsesScreen({ data, now, onNavigate, onResponseTransition, pushToast }) {
    const claims = data.claims;
    const offers = offeredClaims(claims).slice().sort((a, b) => b.response.offerSentAt - a.response.offerSentAt);
    const answered = answeredOffers(claims);
    const accepted = offersWith(claims, "accepted");
    const disputed = disputedOffers(claims);
    const awaiting = offersWith(claims, "awaiting_response");

    return (
      <div className="cd-page">
        <PageHead
          path="/responses"
          title="Responses"
          lede={"Every offer this desk has sent, and what the customer did with it. " + answered.length + " of " + offers.length + " have answered."}
          actions={<ExportMenu scope="Offers sent" onExport={(format) => pushToast({ title: "Export started", description: "Offers sent · " + format + " — we'll email it to " + CLAIMS_USER.name.split(" ")[0].toLowerCase() + "@corvenline.example" })} />}
        />

        <div className="cd-bleed">
          <div className="cd-figures">
            <Figure
              xl
              label="Accepted and closed"
              value={formatMoney(sumOf(accepted, (c) => c.settlement.amountCents))}
              note={accepted.length + " of the " + offers.length + " offers sent, " + awaiting.length + " still with the customer"}
            />
            <Figure
              label="In dispute"
              value={formatMoney(sumOf(disputed, (c) => c.settlement.amountCents))}
              note={disputed.length + (disputed.length === 1 ? " offer refused, already paid" : " offers refused, already paid")}
            />
            <Figure
              label="Argued over"
              value={formatMoney(disputedGapTotal(claims))}
              note="The gap between what we offered and what they demand"
            />
          </div>
        </div>

        <div className="cd-split">
          <div className="cd-col">
            {disputed.length ? (
              disputed.map((claim) => <DisputedOffer key={claim.id} claim={claim} now={now} onNavigate={onNavigate} onResponseTransition={onResponseTransition} />)
            ) : (
              <Card className="cd-decide">
                <CardPanel>
                  <Empty>
                    <EmptyHeader>
                      <EmptyMedia variant="icon"><Icon name="handshake" size={18} /></EmptyMedia>
                      <EmptyTitle>No offer is in dispute</EmptyTitle>
                      <EmptyDescription>All {answered.length} answered offers were accepted. {awaiting.length ? awaiting.length + " offers are still with the customer — record their answer from the ledger beside this." : ""}</EmptyDescription>
                    </EmptyHeader>
                  </Empty>
                </CardPanel>
              </Card>
            )}
          </div>

          <div className="cd-col cd-col--tight">
            <Card>
              <CardHeader>
                <CardTitle>Offer ledger</CardTitle>
                <CardDescription>{formatPlural(offers.length, "offer")} sent, newest first.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-rows">
                  {offers.map((claim) => {
                    const transitions = responseTransitions(claim);
                    const record = transitions.find((t) => t.id === "record_response");
                    const recessed = RESPONSE_FLAGS.awaitsCustomer(claim);
                    return (
                      <div className={"cd-row" + (recessed ? " cd-row--recessed" : "")} key={claim.id}>
                        <div className="cd-row__main">
                          <span className="cd-record__name">{claim.customer}</span>
                          <span className="cd-record__meta"><code>{claim.id}</code> · sent {formatWhen(claim.response.offerSentAt, now)}</span>
                          {recessed ? <span className="cd-record__meta">Waiting on the customer</span> : null}
                        </div>
                        <div className="cd-row__side">
                          <span className="cd-amount">{formatMoney(claim.settlement.amountCents)}</span>
                          <StatusBadge values={RESPONSE_STATUS} value={claim.response.status} />
                        </div>
                        {record ? (
                          <div className="cd-row__wide">
                            <Button variant="outline" size="xs" onClick={() => onResponseTransition(claim, record)}>{record.label}</Button>
                          </div>
                        ) : null}
                      </div>
                    );
                  })}
                </div>
              </CardPanel>
              <CardFooter className="cd-foot">
                <span>{formatMoney(sumOf(offers, (c) => c.settlement.amountCents))} offered across {formatPlural(offers.length, "claim")}</span>
              </CardFooter>
            </Card>
          </div>
        </div>
      </div>
    );
  }

  Object.assign(window, { ResponsesScreen, DisputedOffer });
})();

</script>
<script type="text/babel" data-presets="react" data-file="RecoveryScreen.jsx">
/* Recovery — the mirror image of the claim: once we have paid the customer, we
   pursue whoever caused the damage.
   Commit model: INSTANT. Filing a notice applies immediately with an undo
   toast; recording money back opens a staged dialog; writing a recovery off is
   irreversible and opens a confirm dialog.
   Data state: populated.
   Recession: a recovery sitting with the counterparty is open work the adjuster
   cannot act on, so it is recessed — the same rule as the queue and payments. */
(() => {
  const {
    Button, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter, Meter,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell, Badge,
  } = window.OriginDesignSystem_7c998b;

  function RecoveryScreen({ data, now, onNavigate, onRecoveryTransition, pushToast }) {
    const recoveries = data.recoveries;
    const claims = data.claims;
    const closed = closedRecoveries(recoveries);
    const open = openRecoveries(recoveries);
    const toFile = recoveriesToFile(recoveries);
    const rate = recoveryRate(recoveries);
    const byKind = recoveryByKind(recoveries, COUNTERPARTY_KINDS);
    const pursuedClosed = sumBy(closed, "soughtCents");
    const recovered = sumBy(closed, "recoveredCents");
    const rows = recoveries.slice().sort((a, b) => RECOVERY_FLAGS.daysLeft(a, now) - RECOVERY_FLAGS.daysLeft(b, now));

    return (
      <div className="cd-page">
        <PageHead
          path="/recovery"
          title="Recovery"
          lede={"Claims we have paid and are pursuing against the terminal, the trucker or the packer. Each recovery carries its own deadline, and " + toFile.length + " of the " + open.length + " open files is waiting on you."}
          actions={<><ExportMenu scope="Recoveries" onExport={(format) => pushToast({ title: "Export started", description: "Recoveries · " + format + " — we'll email it to " + CLAIMS_USER.name.split(" ")[0].toLowerCase() + "@corvenline.example" })} /><Button size="sm" onClick={() => onNavigate("/recovery/new")}>New recovery</Button></>}
        />

        <div className="cd-bleed">
          <div className="cd-split cd-split--band">
            <div className="cd-section">
              <div className="cd-figures">
                <Figure
                  xl
                  label="Recovery rate"
                  value={formatPercent(rate)}
                  note={formatMoney(recovered) + " back out of " + formatMoney(pursuedClosed) + " pursued and closed, across " + closed.length + " files. Open files are not in the rate."}
                />
                <Figure
                  label="Still in play"
                  value={formatMoney(sumBy(open, "soughtCents"))}
                  note={open.length + " open recoveries, " + toFile.length + " of them waiting on you to serve notice"}
                />
              </div>
            </div>
            <div className="cd-section">
              <div className="cd-section__head">
                <h2 className="cd-h2">Who we are pursuing</h2>
                <p className="cd-note">Share of the {formatMoney(sumBy(recoveries, "soughtCents"))} pursued across all {formatPlural(recoveries.length, "recovery", "recoveries")}.</p>
              </div>
              <div className="cd-col cd-col--tight">
                {byKind.map((row) => (
                  <div key={row.kind} style={{ display: "flex", flexDirection: "column", gap: "var(--space-1-5)" }}>
                    <div className="cd-mix__head" style={{ display: "flex", justifyContent: "space-between", gap: "var(--space-4)", fontSize: "var(--text-sm)" }}>
                      <span>{row.label}<span className="cd-tab-count">{row.count}</span></span>
                      <span className="cd-amount">{formatMoney(row.sought)}</span>
                    </div>
                    <Meter value={row.sought} max={sumBy(recoveries, "soughtCents")} aria-label={row.label + " share of the amount pursued"} />
                    {row.recovered ? <div className="cd-figure__note">{formatMoney(row.recovered)} recovered so far</div> : null}
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>

        <section className="cd-section">
          <div className="cd-section__head">
            <h2 className="cd-h2">Recovery files</h2>
            <p className="cd-note">Sorted by their own deadline, soonest first — a recovery's clock is contractual, not the cargo time bar.</p>
          </div>
          <div className="cd-table-wrap">
            <Table className="cd-tbl">
              <TableHeader>
                <TableRow>
                  <TableHead>Counterparty</TableHead>
                  <TableHead className="cd-drop-lg">Paid claim</TableHead>
                  <TableHead className="cd-num">Sought</TableHead>
                  <TableHead className="cd-num">Recovered</TableHead>
                  <TableHead>Status</TableHead>
                  <TableHead className="cd-drop-md">Their deadline</TableHead>
                  <TableHead className="cd-actions"><span className="o-sr">Action</span></TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {rows.map((recovery) => {
                  const claim = claimById(claims, recovery.claimId);
                  const transitions = recoveryTransitions(recovery);
                  const daysLeft = RECOVERY_FLAGS.daysLeft(recovery, now);
                  const recessed = RECOVERY_FLAGS.isOpen(recovery) && !RECOVERY_FLAGS.awaitsAdjuster(recovery);
                  return (
                    <TableRow key={recovery.id} className={recessed ? "cd-row-recessed" : ""}>
                      <TableCell>
                        <span className="cd-record">
                          <button type="button" className="cd-record__link" onClick={() => onNavigate("/recovery/" + recovery.id)}>{recovery.counterparty.name}</button>
                          <span className="cd-record__meta"><code>{recovery.id}</code> · {labelOf(COUNTERPARTY_KINDS, recovery.counterparty.kind)}</span>
                        </span>
                      </TableCell>
                      <TableCell className="cd-drop-lg">
                        <span className="cd-muted">{claim ? claim.customer : recovery.claimId}</span>
                      </TableCell>
                      <TableCell className="cd-num cd-amount">{formatMoney(recovery.soughtCents)}</TableCell>
                      <TableCell className="cd-num">
                        {recovery.recoveredCents ? <span className="cd-amount">{formatMoney(recovery.recoveredCents)}</span> : <span className="cd-muted">—</span>}
                      </TableCell>
                      <TableCell>
                        <span className="cd-record">
                          <StatusBadge values={RECOVERY_STATUS} value={recovery.status} />
                          {recovery.status === "negotiating" && recovery.offerCents ? <span className="cd-record__meta">Offered {formatMoney(recovery.offerCents)}</span> : null}
                          {recessed ? <span className="cd-record__meta">Waiting on {recovery.counterparty.name}</span> : null}
                        </span>
                      </TableCell>
                      <TableCell className="cd-drop-md">
                        {RECOVERY_FLAGS.isOpen(recovery) ? (
                          <span className="cd-cell-clock">
                            <span className={clockClass(timeTone(daysLeft))} title={formatAbsolute(recovery.fileByAt)}>{formatCountdown(recovery.fileByAt, now)}</span>
                            <TimeBar startAt={recovery.openedAt} endAt={recovery.fileByAt} now={now} mini label={"Recovery deadline: " + formatCountdown(recovery.fileByAt, now)} />
                          </span>
                        ) : (
                          <span className="cd-muted cd-clock">Closed {formatWhen(recovery.closedAt, now)}</span>
                        )}
                      </TableCell>
                      <TableCell className="cd-actions">
                        {transitions.filter((t) => t.primary).map((t) => (
                          <Button key={t.id} size="xs" onClick={() => onRecoveryTransition(recovery, t)}>{t.label}</Button>
                        ))}
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
            </Table>
          </div>
          <div className="cd-foot">
            <span>Showing {rows.length} of {formatPlural(recoveries.length, "recovery", "recoveries")} · {formatMoney(sumBy(recoveries, "soughtCents"))} pursued in total</span>
            <span>{open.length} open, {closed.length} closed.</span>
          </div>
        </section>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="RecoveryDetailScreen.jsx">
/* Recovery file — one pursuit, against one counterparty, on its own clock.
   Commit model: INSTANT. Filing a notice applies immediately with an undo
   toast, recording money back opens a staged dialog, writing off opens a
   confirm dialog.
   Data state: populated. A recovery id that does not resolve is a branch in
   App, not a state of this screen. */
(() => {
  const {
    Button, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter, Badge,
  } = window.OriginDesignSystem_7c998b;

  function RecoveryDetailScreen({ recovery, data, now, onNavigate, onRecoveryTransition }) {
    const claim = claimById(data.claims, recovery.claimId);
    const daysLeft = RECOVERY_FLAGS.daysLeft(recovery, now);
    const transitions = recoveryTransitions(recovery);
    const shortfall = RECOVERY_FLAGS.shortfall(recovery);

    return (
      <div className="cd-page">
        <div className="cd-head-record">
          <div className="cd-head-record__top">
            <div className="cd-page-head__text">
              <Crumbs path="/recovery/:id" params={{ id: recovery.id }} labels={{ "/recovery/:id": recovery.id }} />
              <span className="cd-head-record__id">
                <span className="cd-code">{recovery.id}</span>
                <StatusBadge values={RECOVERY_STATUS} value={recovery.status} />
                <Badge size="sm" variant="outline">{labelOf(COUNTERPARTY_KINDS, recovery.counterparty.kind)}</Badge>
              </span>
              <h1 className="cd-title">{recovery.counterparty.name}</h1>
              <p className="cd-lede">{recovery.basis}</p>
            </div>
            <div className="cd-page-head__actions">
              {transitions.map((t) => (
                <Button key={t.id} size="sm" variant={t.primary ? "default" : t.destructive ? "destructive-outline" : "outline"} onClick={() => onRecoveryTransition(recovery, t)}>{t.label}</Button>
              ))}
              {claim ? <Button variant="ghost" size="sm" onClick={() => onNavigate("/claims/" + claim.id)}>The paid claim<Icon name="arrow-right" /></Button> : null}
            </div>
          </div>
          <div className="cd-head-record__bar">
            <TimeBar startAt={recovery.openedAt} endAt={recovery.fileByAt} now={now} label={"Recovery deadline: " + formatCountdown(recovery.fileByAt, now)} />
            <div className="cd-head-record__left">
              <div className={clockClass(timeTone(daysLeft))} style={{ fontSize: "var(--text-sm)", fontWeight: "var(--font-weight-medium)" }} title={formatAbsolute(recovery.fileByAt)}>{formatCountdown(recovery.fileByAt, now)}</div>
              <div className="cd-figure__note">on the contract clock</div>
            </div>
            <div className="cd-head-record__ends">
              <span>Opened {formatAbsolute(recovery.openedAt).split(",")[0]}</span>
              <span>Deadline {formatAbsolute(recovery.fileByAt).split(",")[0]}</span>
            </div>
          </div>
          <div className="cd-facts">
            <span><strong>{claim ? claim.customer : recovery.claimId}</strong> · {claim ? claim.container : ""}</span>
            <span>{recovery.noticeServedAt ? "Notice served " + formatAbsolute(recovery.noticeServedAt) : "Notice not served"}</span>
            <span>{recovery.counterparty.contact}</span>
          </div>
        </div>

        <div className="cd-split">
          <div className="cd-col">
            <Card className="cd-card">
              <CardHeader>
                <CardTitle>The mirror ledger</CardTitle>
                <CardDescription>What we paid out, what we are pursuing, and what has come back. The claim owns the first figure; this file only follows it.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-lg">
                  {claim ? (
                    <LedgerRow
                      label={"Paid to " + claim.customer}
                      hint={"Settlement " + claim.settlement.reference + ", cleared " + (claim.settlement.clearedAt ? formatAbsolute(claim.settlement.clearedAt) : "not yet")}
                      value={formatMoney(claim.settlement.amountCents)}
                    />
                  ) : null}
                  <LedgerRow label={"Sought from " + recovery.counterparty.name} hint={recovery.basis} value={formatMoney(recovery.soughtCents)} />
                  {recovery.offerCents ? (
                    <LedgerRow label="Their offer on the table" hint={recovery.status === "recovered" ? "Accepted and settled" : "Rejected as below the surveyed loss"} value={formatMoney(recovery.offerCents)} />
                  ) : null}
                  <LedgerRow className="cd-lg__row--sub" label="Recovered" hint={recovery.recoveredCents ? "Received " + formatAbsolute(recovery.closedAt) : "Nothing received yet"} value={formatMoney(recovery.recoveredCents)} />
                  <LedgerRow
                    className="cd-lg__row--payable"
                    label={<span style={{ fontWeight: "var(--font-weight-medium)" }}>{RECOVERY_FLAGS.isOpen(recovery) ? "Still out" : "Written down"}</span>}
                    hint={RECOVERY_FLAGS.isOpen(recovery)
                      ? "Chasing " + recovery.counterparty.name + " for the balance, " + formatCountdown(recovery.fileByAt, now) + " on the contract clock."
                      : "This file is closed at " + formatPercent(recovery.recoveredCents / recovery.soughtCents) + " of the amount sought."}
                    value={formatMoney(shortfall)}
                  />
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Correspondence</CardTitle>
                <CardDescription>{formatPlural(recovery.log.length, "entry", "entries")}, newest first.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-log">
                  {recovery.log.slice().sort((a, b) => b.at - a.at).map((entry) => (
                    <div className="cd-log__item" key={entry.at}>
                      <span className="cd-log__who">{entry.who}</span>
                      <span className="cd-log__when" title={formatAbsolute(entry.at)}>{formatWhen(entry.at, now)}</span>
                      <span className="cd-log__text">{entry.text}</span>
                    </div>
                  ))}
                </div>
              </CardPanel>
            </Card>
          </div>

          <div className="cd-col cd-col--tight">
            <Card>
              <CardHeader>
                <CardTitle>The claim behind it</CardTitle>
                <CardDescription>{claim ? claim.id + " · " + labelOf(DECISION_OUTCOMES, claim.decision.outcome) : recovery.claimId}</CardDescription>
              </CardHeader>
              <CardPanel>
                {claim ? (
                  <dl className="cd-defs">
                    <dt>Claimant</dt><dd>{claim.customer}</dd>
                    <dt>Container</dt><dd className="cd-code">{claim.container}</dd>
                    <dt>Voyage</dt><dd>{claim.vessel} {claim.voyage}</dd>
                    <dt>Damage</dt><dd>{claim.incident}</dd>
                    <dt>Claimed</dt><dd className="cd-amount">{formatMoney(claim.claimedCents)}</dd>
                    <dt>Paid</dt><dd className="cd-amount">{formatMoney(claim.settlement.amountCents)}</dd>
                  </dl>
                ) : <p className="cd-note">The paid claim is not on this desk.</p>}
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>The counterparty</CardTitle>
                <CardDescription>{labelOf(COUNTERPARTY_KINDS, recovery.counterparty.kind)}</CardDescription>
              </CardHeader>
              <CardPanel>
                <dl className="cd-defs">
                  <dt>Name</dt><dd>{recovery.counterparty.name}</dd>
                  <dt>Contact</dt><dd className="cd-code">{recovery.counterparty.contact}</dd>
                  <dt>Basis</dt><dd>{recovery.basis}</dd>
                  <dt>Opened</dt><dd>{formatAbsolute(recovery.openedAt)}</dd>
                  <dt>Notice served</dt><dd>{recovery.noticeServedAt ? formatAbsolute(recovery.noticeServedAt) : "Not served"}</dd>
                  <dt>Deadline</dt><dd>{formatAbsolute(recovery.fileByAt)}</dd>
                </dl>
              </CardPanel>
            </Card>
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="ClaimantScreen.jsx">
/* Claimant — one customer, and everything this desk knows about them.
   Commit model: INSTANT, and in fact read-only: a claimant record holds no
   transitions of its own, because every action belongs to a claim. Nothing on
   this screen is a control except navigation.
   Data state: populated. A claimant with no claims cannot be reached — the
   record is created by its claims — so there is no empty branch here.
   Derivation: the claimant fixture holds only what a claim cannot (who to write
   to, when they first shipped, their lane and terms). Every figure below is
   their own claims read through the same helpers the queue and payments use. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter, Button,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  } = window.OriginDesignSystem_7c998b;

  function ClaimantScreen({ claimant, data, now, onNavigate, onNewClaim }) {
    const s = claimantSummary(claimant, data.claims, data.recoveries, now);
    const disputedGap = disputedGapTotal(s.rows);

    const lede = formatPlural(s.rows.length, "claim") + " against us, first filed "
      + formatAbsolute(s.firstClaim.claimedAt).split(",")[0]
      + (s.open.length ? ". " + formatPlural(s.open.length, "claim") + " still open" : ". Nothing open")
      + (s.rejected.length ? ", " + formatPlural(s.rejected.length, "claim") + " turned down." : ".");

    return (
      <div className="cd-page">
        <PageHead
          path="/claimants/:id"
          params={{ id: claimant.id }}
          labels={{ "/claimants/:id": claimant.name }}
          title={claimant.name}
          lede={lede}
          actions={<Button size="sm" onClick={() => onNewClaim(claimant.id)}>New claim</Button>}
        />

        <div className="cd-facts">
          <span><strong>{claimant.contact}</strong> · {claimant.email}</span>
          <span>{claimant.lane}</span>
          <span>{claimant.terms}</span>
          <span>Shipping with us since {formatAbsolute(claimant.since).split(",")[0]}</span>
        </div>

        <div className="cd-bleed">
          <div className="cd-figures">
            <Figure
              xl
              label="Claimed against us"
              value={formatMoney(s.claimed)}
              note={"across " + formatPlural(s.rows.length, "claim") + " on the " + CLAIMS_CARRIER.region + " desk"}
            />
            <Figure
              label="Still open"
              value={s.open.length ? formatMoney(s.openClaimed) : "Nothing open"}
              note={s.open.length
                ? formatPlural(s.open.length, "claim") + " claimed, nearest bar " + formatCountdown(CLAIM_FLAGS.timeBarAt(s.nearestBar), now)
                : "Every claim they have filed is closed"}
            />
            <Figure
              label="Paid out"
              value={s.released.length ? formatMoney(s.releasedTotal) : "Nothing paid"}
              note={s.released.length
                ? formatPlural(s.released.length, "settlement") + " sent or cleared"
                : "No settlement has left the account"}
            />
          </div>
        </div>

        <section className="cd-section">
          <div className="cd-section__head">
            <h2 className="cd-h2">Their claims</h2>
            <p className="cd-note">Every claim this claimant has filed, open and closed, soonest time bar first.</p>
          </div>
          <div className="cd-table-wrap">
            <Table className="cd-tbl">
              <TableHeader>
                <TableRow>
                  <TableHead>Claim</TableHead>
                  <TableHead className="cd-drop-lg">Cargo</TableHead>
                  <TableHead className="cd-num">Claimed</TableHead>
                  <TableHead className="cd-num">Payable</TableHead>
                  <TableHead>Status</TableHead>
                  <TableHead className="cd-drop-md">Time bar</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {byUrgency(s.rows, now).map((claim) => {
                  const recessed = CLAIM_FLAGS.isWaitingOnOthers(claim);
                  const payable = payableOf(claim);
                  return (
                    <TableRow key={claim.id} className={recessed ? "cd-row-recessed" : ""}>
                      <TableCell>
                        <span className="cd-record">
                          <button type="button" className="cd-record__link" onClick={() => onNavigate("/claims/" + claim.id)}><code>{claim.id}</code></button>
                          <span className="cd-record__meta">{claim.incident}</span>
                        </span>
                      </TableCell>
                      <TableCell className="cd-drop-lg cd-muted">
                        <span className="cd-record">
                          <span>{claim.commodity}</span>
                          <span className="cd-record__meta">{claim.container} · {claim.pol} → {claim.pod}</span>
                        </span>
                      </TableCell>
                      <TableCell className="cd-num cd-amount">{formatMoney(claim.claimedCents)}</TableCell>
                      <TableCell className="cd-num">
                        {claim.decision && claim.decision.outcome === "reject"
                          ? <span className="cd-muted">Nothing payable</span>
                          : payable === null
                            ? <span className="cd-muted">Not assessed</span>
                            : <span className="cd-amount">{formatMoney(payable)}</span>}
                      </TableCell>
                      <TableCell>
                        <span className="cd-record">
                          <StatusBadge values={CLAIM_STATUS} value={claim.status} />
                          {recessed ? <span className="cd-record__meta">Waiting on {waitingOn(claim)}</span> : null}
                        </span>
                      </TableCell>
                      <TableCell className="cd-drop-md">
                        {CLAIM_FLAGS.isOpen(claim)
                          ? <ClaimClock claim={claim} now={now} mini />
                          : <span className="cd-muted cd-clock">Closed {formatWhen(claim.decision ? claim.decision.decidedAt : claim.claimedAt, now)}</span>}
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
            </Table>
          </div>
          <div className="cd-foot">
            <span>{formatPlural(s.rows.length, "claim")} · {formatMoney(s.claimed)} claimed in total</span>
            <span>Figures read the same helpers as the work queue.</span>
          </div>
        </section>

        <div className="cd-split cd-split--even">
          <Card>
            <CardHeader>
              <CardTitle>How our offers landed</CardTitle>
              <CardDescription>What this claimant did once we put a figure to them.</CardDescription>
            </CardHeader>
            <CardPanel>
              {s.offers.length === 0 ? (
                <p className="cd-note">No offer has been put to them yet, so there is nothing to read here.</p>
              ) : (
                <dl className="cd-kv">
                  <div><dt>Offers made</dt><dd>{formatCount(s.offers.length)}</dd></div>
                  <div><dt>Answered</dt><dd>{formatCount(s.answered.length)}</dd></div>
                  <div><dt>Accepted</dt><dd>{formatCount(s.accepted.length)}</dd></div>
                  <div><dt>Disputed</dt><dd>{formatCount(s.disputed.length)}</dd></div>
                  {s.disputed.length ? <div><dt>Held against us</dt><dd>{formatMoney(disputedGap)}</dd></div> : null}
                </dl>
              )}
            </CardPanel>
            {s.disputed.length ? (
              <CardFooter className="cd-card-footer">
                <span className="cd-footnote">{s.disputed.map((c) => c.id).join(", ")} {s.disputed.length === 1 ? "is" : "are"} in dispute.</span>
                <button type="button" className="cd-record__link" onClick={() => onNavigate("/responses")}>Open responses</button>
              </CardFooter>
            ) : null}
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>Recovered on their claims</CardTitle>
              <CardDescription>What we pursued from the party that caused the damage.</CardDescription>
            </CardHeader>
            <CardPanel>
              {s.recoveries.length === 0 ? (
                <p className="cd-note">No recovery has been opened against any of their claims.</p>
              ) : (
                <div className="cd-rows">
                  {s.recoveries.map((recovery) => (
                    <div className="cd-row" key={recovery.id}>
                      <div className="cd-row__main">
                        <button type="button" className="cd-record__link" onClick={() => onNavigate("/recovery/" + recovery.id)}>{recovery.counterparty.name}</button>
                        <span className="cd-record__meta"><code>{recovery.id}</code> · from {recovery.claimId}</span>
                      </div>
                      <div className="cd-row__side">
                        <StatusBadge values={RECOVERY_STATUS} value={recovery.status} />
                        <span className="cd-record__meta">
                          {recovery.recoveredCents ? formatMoney(recovery.recoveredCents) + " back of " + formatMoney(recovery.soughtCents) : formatMoney(recovery.soughtCents) + " sought"}
                        </span>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </CardPanel>
          </Card>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="NotificationsScreen.jsx">
/* Notifications — the destination behind the bar's panel.
   Commit model: INSTANT. Opening one marks it read; "Mark all read" applies
   immediately and raises an undo toast. No Save button, nothing irreversible.
   Data state: populated, or the no-results branch when the Unread tab is empty
   — one conditional, never both.
   Scope: everything this desk has raised. The tabs narrow the list only; the
   two figures in the header sentence count the whole set either way.

   Every item carries a kind from ALERT_KINDS, which is the same list the alerts
   settings offer, so the footer can say honestly which of these would have
   reached the inbox. */
(() => {
  const {
    Button, Tabs, TabsList, TabsTab,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  const dayLabel = (at, now) => {
    const days = Math.round((new Date(now).setHours(0, 0, 0, 0) - new Date(at).setHours(0, 0, 0, 0)) / 864e5);
    if (days <= 0) return "Today";
    if (days === 1) return "Yesterday";
    return formatAbsolute(at).split(",")[0];
  };

  function NotificationsScreen({ items, readIds, user, now, onNavigate, onOpen, onReadAll }) {
    const [tab, setTab] = React.useState("unread");
    const unread = unreadNotifications(items, readIds);
    const TABS = [
      { value: "unread", label: "Unread", rows: unread },
      { value: "all", label: "All", rows: items },
    ];
    const spec = TABS.find((t) => t.value === tab);
    const days = notificationsByDay(spec.rows, now);
    const emailed = ALERT_KINDS.filter((k) => (user.alerts || []).indexOf(k.value) > -1);
    const wouldEmail = items.filter((n) => (user.alerts || []).indexOf(n.kind) > -1);

    return (
      <div className="cd-page">
        <PageHead
          path="/notifications"
          title="Notifications"
          lede={unread.length
            ? formatPlural(unread.length, "notification") + " you have not opened, of " + formatCount(items.length) + " raised on this desk."
            : "Everything raised on this desk has been opened. " + formatPlural(items.length, "notification") + " in total."}
          actions={unread.length ? <Button variant="outline" size="sm" onClick={onReadAll}>Mark all read</Button> : null}
        />

        <section className="cd-section">
          <div className="cd-filters">
            <p className="cd-note" style={{ maxWidth: "26rem" }}>Opening one marks it read and takes you to the record it names.</p>
            <Tabs value={tab} onValueChange={setTab}>
              <TabsList variant="underline" size="sm">
                {TABS.map((t) => (
                  <TabsTab key={t.value} value={t.value} size="sm">
                    {t.label}<span className="cd-tab-count">{t.rows.length}</span>
                  </TabsTab>
                ))}
              </TabsList>
            </Tabs>
          </div>

          {spec.rows.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="check" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing unread</EmptyTitle>
                <EmptyDescription>All {formatCount(items.length)} notifications on this desk have been opened.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => setTab("all")}>Show all notifications</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="cd-days">
              {days.map((day) => (
                <div className="cd-day" key={day.key}>
                  <h3 className="cd-day__label">{dayLabel(day.at, now)}</h3>
                  <div className="cd-rows">
                    {day.items.map((n) => {
                      const isUnread = readIds.indexOf(n.id) === -1;
                      return (
                        <button type="button" key={n.id} className={"cd-notice" + (isUnread ? " is-unread" : "")} onClick={() => onOpen(n)}>
                          <span className="cd-notice__dot" aria-hidden="true" />
                          <span className="cd-notice__main">
                            <span className="cd-notice__title">{n.title}</span>
                            <span className="cd-record__meta">{n.body}</span>
                          </span>
                          <span className="cd-notice__side">
                            <span className="cd-record__meta">{labelOf(ALERT_KINDS, n.kind)}</span>
                            <span className="cd-record__meta" title={formatAbsolute(n.at)}>{formatWhen(n.at, now)}</span>
                          </span>
                          {isUnread ? <span className="o-sr">Unread</span> : null}
                        </button>
                      );
                    })}
                  </div>
                </div>
              ))}
            </div>
          )}

          <div className="cd-foot">
            <span>Showing {spec.rows.length} of {formatPlural(items.length, "notification")} · {spec.label.toLowerCase()}</span>
            <span>
              {emailed.length
                ? formatCount(wouldEmail.length) + " of these would also have reached " + user.email + " — "
                : "None of these reach your inbox — "}
              <button type="button" className="cd-record__link" onClick={() => onNavigate("/settings/alerts")}>choose which kinds are emailed</button>
            </span>
          </div>
        </section>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="NewClaimScreen.jsx">
/* New claim — opening a file when a claim letter arrives.
   Commit model: STAGED. Every control is a form field, nothing exists until
   Save, Cancel appears once the form is dirty, and errors render on validation
   rather than on first paint.
   Data state: populated (a form is never empty-stated).

   The panel on the right is not decoration: it runs the real evaluators on the
   draft. `liabilityCap` gives the ceiling the bill of lading imposes from the
   package and weight figures alone — before anything is assessed — and
   `CLAIM_FLAGS.timeBarAt` gives the deadline from the delivery date. Both are
   the same functions every other screen reads, so what the form promises is
   what the file will say. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel,
    Field, FieldLabel, FieldDescription, FieldError, Input, Textarea, Select, Button, Badge,
  } = window.OriginDesignSystem_7c998b;

  const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
  const NEW_CLAIMANT = "__new";
  const MIN_INCIDENT = 20;
  const EMPTY = {
    claimantId: "", newName: "", newContact: "", newEmail: "",
    bl: "", container: "", vessel: "", voyage: "", pol: "", pod: "",
    commodity: "", packages: "", weightKg: "", deliveredOn: "", claimed: "", incident: "",
  };

  const toDate = (value) => {
    if (!value) return null;
    const ms = Date.parse(value + "T09:00:00");
    return isNaN(ms) ? null : ms;
  };
  const toInt = (value) => {
    const n = Number(String(value).replace(/[^0-9]/g, ""));
    return value !== "" && !isNaN(n) && n > 0 ? n : null;
  };

  function NewClaimScreen({ data, now, forClaimant, onNavigate, onCreate }) {
    const [draft, setDraft] = React.useState(Object.assign({}, EMPTY, forClaimant ? { claimantId: forClaimant } : null));
    const [submitted, setSubmitted] = React.useState(false);
    const set = (fields) => setDraft(Object.assign({}, draft, fields));
    const dirty = JSON.stringify(draft) !== JSON.stringify(Object.assign({}, EMPTY, forClaimant ? { claimantId: forClaimant } : null));

    const claimantItems = data.claimants
      .slice()
      .sort((a, b) => a.name.localeCompare(b.name))
      .map((p) => ({ value: p.id, label: p.name }))
      .concat([{ value: NEW_CLAIMANT, label: "A claimant we have not had before…" }]);
    const isNew = draft.claimantId === NEW_CLAIMANT;

    const packages = toInt(draft.packages);
    const weightKg = toInt(draft.weightKg);
    const deliveredAt = toDate(draft.deliveredOn);
    const claimedCents = parseMoney(draft.claimed || "");
    /* The real evaluator, on the draft. Both figures exist long before an
       assessment does, which is the whole reason to show them here. */
    const cap = packages && weightKg ? liabilityCap({ packages: packages, weightKg: weightKg }) : null;
    const timeBarAt = deliveredAt ? CLAIM_FLAGS.timeBarAt({ deliveredAt: deliveredAt }) : null;
    const daysLeft = timeBarAt ? Math.round((timeBarAt - now) / 864e5) : null;
    const capBinds = cap && !isNaN(claimedCents) && claimedCents > cap.cents;

    const errors = {};
    if (!draft.claimantId) errors.claimantId = "Pick the claimant, or open a record for a new one.";
    if (isNew && !draft.newName.trim()) errors.newName = "Enter the claimant's name as it appears on the bill of lading.";
    if (isNew && !draft.newContact.trim()) errors.newContact = "Enter who we write to.";
    if (isNew && !EMAIL.test(draft.newEmail)) errors.newEmail = "Enter a valid email address.";
    if (!draft.bl.trim()) errors.bl = "The bill of lading number identifies the contract of carriage.";
    if (!draft.container.trim()) errors.container = "Enter the container number.";
    if (!draft.vessel.trim()) errors.vessel = "Enter the vessel.";
    if (!draft.pol.trim()) errors.pol = "Enter the load port.";
    if (!draft.pod.trim()) errors.pod = "Enter the discharge port.";
    if (!draft.commodity.trim()) errors.commodity = "Enter what was in the container.";
    if (!packages) errors.packages = "Packages set half of the liability cap. Enter a whole number.";
    if (!weightKg) errors.weightKg = "Weight sets the other half of the cap. Enter kilos.";
    if (!deliveredAt) errors.deliveredOn = "Delivery starts the one-year time bar.";
    else if (deliveredAt > now) errors.deliveredOn = "A claim cannot be filed before the cargo is delivered.";
    if (isNaN(claimedCents) || claimedCents <= 0) errors.claimed = "Enter the figure the claimant is holding us to.";
    if (draft.incident.trim().length < MIN_INCIDENT) errors.incident = "Describe the loss in a sentence — it is the line every other screen shows.";
    const valid = Object.keys(errors).length === 0;

    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      onCreate({
        claimantId: isNew ? null : draft.claimantId,
        newClaimant: isNew ? { name: draft.newName.trim(), contact: draft.newContact.trim(), email: draft.newEmail.trim(), lane: draft.pol.trim() + " → " + draft.pod.trim() } : null,
        bl: draft.bl.trim(), container: draft.container.trim(), vessel: draft.vessel.trim(), voyage: draft.voyage.trim() || "—",
        pol: draft.pol.trim(), pod: draft.pod.trim(), commodity: draft.commodity.trim(),
        packages: packages, weightKg: weightKg, deliveredAt: deliveredAt,
        claimedCents: claimedCents, incident: draft.incident.trim(),
      });
    };

    return (
      <div className="cd-page">
        <PageHead
          path="/claims/new"
          title="New claim"
          lede="Open a file from the claim letter. Nothing is assessed here — the file starts waiting on documents, with the whole checklist requested."
        />

        <div className="cd-split">
          <div className="cd-col">
            <Card>
              <CardHeader>
                <CardTitle>Who is claiming</CardTitle>
                <CardDescription>The claimant's own record collects every claim they file, so the same name is never opened twice.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-fields">
                  <Field>
                    <FieldLabel htmlFor="cd-n-claimant">Claimant</FieldLabel>
                    <Select id="cd-n-claimant" items={claimantItems} placeholder="Pick a claimant…" value={draft.claimantId} invalid={submitted && !!errors.claimantId} onValueChange={(v) => set({ claimantId: v })} />
                    {submitted && errors.claimantId ? <FieldError>{errors.claimantId}</FieldError> : <FieldDescription>{formatPlural(data.claimants.length, "claimant")} have shipped with us.</FieldDescription>}
                  </Field>
                  {isNew ? (
                    <div className="cd-grid2">
                      <Field>
                        <FieldLabel htmlFor="cd-n-name">Name on the bill of lading</FieldLabel>
                        <Input id="cd-n-name" value={draft.newName} invalid={submitted && !!errors.newName} onChange={(e) => set({ newName: e.target.value })} />
                        {submitted && errors.newName ? <FieldError>{errors.newName}</FieldError> : null}
                      </Field>
                      <Field>
                        <FieldLabel htmlFor="cd-n-contact">Who we write to</FieldLabel>
                        <Input id="cd-n-contact" value={draft.newContact} invalid={submitted && !!errors.newContact} onChange={(e) => set({ newContact: e.target.value })} />
                        {submitted && errors.newContact ? <FieldError>{errors.newContact}</FieldError> : null}
                      </Field>
                      <Field>
                        <FieldLabel htmlFor="cd-n-email">Their email</FieldLabel>
                        <Input id="cd-n-email" type="email" value={draft.newEmail} invalid={submitted && !!errors.newEmail} onChange={(e) => set({ newEmail: e.target.value })} />
                        {submitted && errors.newEmail ? <FieldError>{errors.newEmail}</FieldError> : <FieldDescription>Every document request and offer goes here.</FieldDescription>}
                      </Field>
                    </div>
                  ) : null}
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>The shipment</CardTitle>
                <CardDescription>Packages and weight set the liability cap; the delivery date starts the time bar. Both are shown as you type them.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-grid2">
                  <Field>
                    <FieldLabel htmlFor="cd-n-bl">Bill of lading</FieldLabel>
                    <Input id="cd-n-bl" placeholder="CRVN-2606-00000" value={draft.bl} invalid={submitted && !!errors.bl} onChange={(e) => set({ bl: e.target.value })} />
                    {submitted && errors.bl ? <FieldError>{errors.bl}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-container">Container</FieldLabel>
                    <Input id="cd-n-container" placeholder="MSKU 0000000" value={draft.container} invalid={submitted && !!errors.container} onChange={(e) => set({ container: e.target.value })} />
                    {submitted && errors.container ? <FieldError>{errors.container}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-vessel">Vessel</FieldLabel>
                    <Input id="cd-n-vessel" placeholder="Tanager Bay" value={draft.vessel} invalid={submitted && !!errors.vessel} onChange={(e) => set({ vessel: e.target.value })} />
                    {submitted && errors.vessel ? <FieldError>{errors.vessel}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-voyage">Voyage</FieldLabel>
                    <Input id="cd-n-voyage" placeholder="214E" value={draft.voyage} onChange={(e) => set({ voyage: e.target.value })} />
                    <FieldDescription>Optional — the vessel and dates find it if the letter omits it.</FieldDescription>
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-pol">Load port</FieldLabel>
                    <Input id="cd-n-pol" placeholder="Genoa" value={draft.pol} invalid={submitted && !!errors.pol} onChange={(e) => set({ pol: e.target.value })} />
                    {submitted && errors.pol ? <FieldError>{errors.pol}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-pod">Discharge port</FieldLabel>
                    <Input id="cd-n-pod" placeholder="Charleston" value={draft.pod} invalid={submitted && !!errors.pod} onChange={(e) => set({ pod: e.target.value })} />
                    {submitted && errors.pod ? <FieldError>{errors.pod}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-commodity">Commodity</FieldLabel>
                    <Input id="cd-n-commodity" placeholder="Glazed floor tile" value={draft.commodity} invalid={submitted && !!errors.commodity} onChange={(e) => set({ commodity: e.target.value })} />
                    {submitted && errors.commodity ? <FieldError>{errors.commodity}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-delivered">Delivered on</FieldLabel>
                    <Input id="cd-n-delivered" type="date" value={draft.deliveredOn} invalid={submitted && !!errors.deliveredOn} onChange={(e) => set({ deliveredOn: e.target.value })} />
                    {submitted && errors.deliveredOn ? <FieldError>{errors.deliveredOn}</FieldError> : <FieldDescription>The one-year time bar runs from this date.</FieldDescription>}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-packages">Packages</FieldLabel>
                    <Input id="cd-n-packages" inputMode="numeric" placeholder="240" value={draft.packages} invalid={submitted && !!errors.packages} onChange={(e) => set({ packages: e.target.value })} />
                    {submitted && errors.packages ? <FieldError>{errors.packages}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-weight">Weight</FieldLabel>
                    <Input id="cd-n-weight" inputMode="numeric" endAddon="kg" placeholder="18400" value={draft.weightKg} invalid={submitted && !!errors.weightKg} onChange={(e) => set({ weightKg: e.target.value })} />
                    {submitted && errors.weightKg ? <FieldError>{errors.weightKg}</FieldError> : null}
                  </Field>
                </div>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>The loss</CardTitle>
                <CardDescription>What they are holding us to, and what they say happened.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-fields">
                  <Field>
                    <FieldLabel htmlFor="cd-n-claimed">Amount claimed</FieldLabel>
                    <div style={{ maxWidth: "14rem" }}>
                      <Input id="cd-n-claimed" inputMode="decimal" startAddon="$" placeholder="41,250.00" value={draft.claimed} invalid={submitted && !!errors.claimed} onChange={(e) => set({ claimed: e.target.value })} />
                    </div>
                    {submitted && errors.claimed ? <FieldError>{errors.claimed}</FieldError> : <FieldDescription>Their figure, before any deduction. Nothing is assessed until the evidence is in.</FieldDescription>}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-n-incident">What happened</FieldLabel>
                    <Textarea id="cd-n-incident" rows={4} placeholder="Lashing failure in hold 3 — six pallets crushed against the bulkhead" value={draft.incident} invalid={submitted && !!errors.incident} onChange={(e) => set({ incident: e.target.value })} />
                    {submitted && errors.incident ? <FieldError>{errors.incident}</FieldError> : <FieldDescription>One line, in the claimant's terms. It is the line the queue and every record header show.</FieldDescription>}
                  </Field>
                </div>
              </CardPanel>
            </Card>
          </div>

          <div className="cd-col">
            <Card>
              <CardHeader>
                <CardTitle>What the file will say</CardTitle>
                <CardDescription>Computed from the figures above by the same helpers every other screen uses.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-col cd-col--tight">
                  <div>
                    <div className="cd-figure__label">Time bar</div>
                    {timeBarAt ? (
                      <>
                        <div className={"cd-figure " + clockClass(timeTone(daysLeft))} style={{ fontSize: "var(--text-2xl)" }}>{formatCountdown(timeBarAt, now)}</div>
                        <div style={{ marginBlock: "var(--space-2)" }}>
                          <TimeBar startAt={deliveredAt} endAt={timeBarAt} now={now} label={"Time bar: " + formatCountdown(timeBarAt, now)} />
                        </div>
                        <div className="cd-figure__note">Expires {formatAbsolute(timeBarAt).split(",")[0]}, {CLAIM_POLICY.timeBarDays} days after delivery.</div>
                      </>
                    ) : (
                      <div className="cd-figure__note">Set the delivery date to see the deadline.</div>
                    )}
                  </div>

                  <div>
                    <div className="cd-figure__label">Liability cap</div>
                    {cap ? (
                      <>
                        <div className="cd-figure" style={{ fontSize: "var(--text-2xl)" }}>{formatMoney(cap.cents)}</div>
                        <div className="cd-figure__note">
                          The higher of {formatCount(Math.round(cap.byPackageSdr))} SDR by package and {formatCount(Math.round(cap.byWeightSdr))} SDR by weight — the {cap.basis} basis binds, at {CLAIM_POLICY.sdrUsdRate} USD per SDR.
                        </div>
                      </>
                    ) : (
                      <div className="cd-figure__note">Enter packages and weight to see the ceiling the bill of lading imposes.</div>
                    )}
                  </div>

                  {capBinds ? (
                    <p className="cd-note">
                      The claim of {formatMoney(claimedCents)} is above that ceiling. Nothing is wrong with opening it — the cap simply binds at {formatMoney(cap.cents)} when it is assessed.
                    </p>
                  ) : null}

                  <div>
                    <div className="cd-figure__label">Evidence the file will request</div>
                    <div className="cd-rows" style={{ marginTop: "var(--space-2)" }}>
                      {DOCUMENT_TYPES.map((type) => (
                        <div className="cd-row" key={type.value} style={{ paddingBlock: "var(--space-2)" }}>
                          <div className="cd-row__main"><span style={{ fontSize: "var(--text-sm)" }}>{type.label}</span></div>
                          <div className="cd-row__side">{type.mandatory ? <Badge size="sm" variant="outline">Mandatory</Badge> : <span className="cd-record__meta">Optional</span>}</div>
                        </div>
                      ))}
                    </div>
                  </div>
                </div>
              </CardPanel>
            </Card>
          </div>
        </div>

        <div className="cd-formbar">
          <span className="cd-formbar__note">
            {submitted && !valid
              ? formatPlural(Object.keys(errors).length, "field") + " still to fill in."
              : "The file opens waiting on documents, with all " + formatCount(DOCUMENT_TYPES.length) + " requested from the claimant."}
          </span>
          {dirty ? <Button variant="outline" onClick={() => onNavigate("/")}>Cancel</Button> : null}
          <Button disabled={!dirty} onClick={submit}>Save claim</Button>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="NewRecoveryScreen.jsx">
/* New recovery — opening a file against whoever caused a loss we have paid.
   Commit model: STAGED. Nothing exists until Save, Cancel appears once dirty,
   errors render on validation.
   Data state: the form, or the empty branch when every paid claim is already
   being pursued — one conditional, never both.

   The amount sought is never typed. It reads the settlement the claim actually
   paid, so the two sides of the same money can never drift apart (AGENTS §2:
   when two records describe the same money, one owns it and the other reads
   it). The clock on the right runs the same TimeBar geometry as everywhere. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel,
    Field, FieldLabel, FieldDescription, FieldError, Input, Textarea, Select, Button,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
  const MIN_BASIS = 20;
  const EMPTY_DRAFT = { claimId: "", name: "", kind: "", contact: "", basis: "", fileBy: "" };

  const toDate = (value) => {
    if (!value) return null;
    const ms = Date.parse(value + "T09:00:00");
    return isNaN(ms) ? null : ms;
  };

  function NewRecoveryScreen({ data, now, onNavigate, onCreate }) {
    const [draft, setDraft] = React.useState(EMPTY_DRAFT);
    const [submitted, setSubmitted] = React.useState(false);
    const set = (fields) => setDraft(Object.assign({}, draft, fields));
    const dirty = JSON.stringify(draft) !== JSON.stringify(EMPTY_DRAFT);

    const eligible = claimsWithoutRecovery(data.claims);
    const claim = eligible.find((c) => c.id === draft.claimId) || null;
    const fileByAt = toDate(draft.fileBy);
    const daysLeft = fileByAt ? Math.round((fileByAt - now) / 864e5) : null;

    const errors = {};
    if (!draft.claimId) errors.claimId = "Pick the claim this recovery follows.";
    if (!draft.name.trim()) errors.name = "Name the party we are pursuing.";
    if (!draft.kind) errors.kind = "Say what they are — it decides which contract binds them.";
    if (!EMAIL.test(draft.contact)) errors.contact = "Enter the address notice will be served on.";
    if (draft.basis.trim().length < MIN_BASIS) errors.basis = "State the clause or liability the claim rests on.";
    if (!fileByAt) errors.fileBy = "Every recovery has a contractual deadline. Enter it.";
    else if (fileByAt <= now) errors.fileBy = "That deadline has already passed — there is nothing left to file.";
    const valid = Object.keys(errors).length === 0;

    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      onCreate({
        claimId: claim.id,
        counterparty: { name: draft.name.trim(), kind: draft.kind, contact: draft.contact.trim() },
        basis: draft.basis.trim(),
        fileByAt: fileByAt,
      });
    };

    if (eligible.length === 0) {
      return (
        <div className="cd-page">
          <PageHead path="/recovery/new" title="New recovery" lede="A recovery follows money that has already left the account." />
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon"><Icon name="check" size={18} /></EmptyMedia>
              <EmptyTitle>Every paid claim is already being pursued</EmptyTitle>
              <EmptyDescription>All {formatPlural(releasedClaims(data.claims).length, "settlement")} sent or cleared on this desk have a recovery file open against them. One opens here as soon as another payment is released.</EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button onClick={() => onNavigate("/recovery")}>Open recovery</Button>
            </EmptyContent>
          </Empty>
        </div>
      );
    }

    return (
      <div className="cd-page">
        <PageHead
          path="/recovery/new"
          title="New recovery"
          lede={"A recovery follows money that has already left the account. " + formatPlural(eligible.length, "paid claim") + " on this desk " + (eligible.length === 1 ? "has" : "have") + " nobody being pursued for it."}
        />

        <div className="cd-split">
          <div className="cd-col">
            <Card>
              <CardHeader>
                <CardTitle>The claim we paid</CardTitle>
                <CardDescription>Only settlements the bank has taken can be recovered; the figure sought comes from the payment itself.</CardDescription>
              </CardHeader>
              <CardPanel>
                <Field>
                  <FieldLabel htmlFor="cd-r-claim">Claim</FieldLabel>
                  <Select
                    id="cd-r-claim"
                    placeholder="Pick a paid claim…"
                    items={eligible.map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(c.settlement.amountCents) }))}
                    value={draft.claimId}
                    invalid={submitted && !!errors.claimId}
                    onValueChange={(v) => set({ claimId: v })}
                  />
                  {submitted && errors.claimId ? <FieldError>{errors.claimId}</FieldError> : <FieldDescription>{formatPlural(eligible.length, "claim")} paid with no recovery open.</FieldDescription>}
                </Field>
              </CardPanel>
            </Card>

            <Card>
              <CardHeader>
                <CardTitle>Who caused it</CardTitle>
                <CardDescription>Notice is served on this address, and the recovery rate is read by counterparty kind.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-fields">
                  <div className="cd-grid2">
                    <Field>
                      <FieldLabel htmlFor="cd-r-name">Counterparty</FieldLabel>
                      <Input id="cd-r-name" placeholder="Meridian Terminal Services" value={draft.name} invalid={submitted && !!errors.name} onChange={(e) => set({ name: e.target.value })} />
                      {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : null}
                    </Field>
                    <Field>
                      <FieldLabel htmlFor="cd-r-kind">What they are</FieldLabel>
                      <Select id="cd-r-kind" placeholder="Pick one…" items={COUNTERPARTY_KINDS} value={draft.kind} invalid={submitted && !!errors.kind} onValueChange={(v) => set({ kind: v })} />
                      {submitted && errors.kind ? <FieldError>{errors.kind}</FieldError> : null}
                    </Field>
                  </div>
                  <Field>
                    <FieldLabel htmlFor="cd-r-contact">Where notice is served</FieldLabel>
                    <Input id="cd-r-contact" type="email" placeholder="liability@meridianterminal.example" value={draft.contact} invalid={submitted && !!errors.contact} onChange={(e) => set({ contact: e.target.value })} />
                    {submitted && errors.contact ? <FieldError>{errors.contact}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-r-basis">On what basis</FieldLabel>
                    <Textarea id="cd-r-basis" rows={3} placeholder="Terminal handling agreement, clause 8 — 12 months from the incident" value={draft.basis} invalid={submitted && !!errors.basis} onChange={(e) => set({ basis: e.target.value })} />
                    {submitted && errors.basis ? <FieldError>{errors.basis}</FieldError> : <FieldDescription>The clause or liability regime. It is the line the recovery file leads with.</FieldDescription>}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="cd-r-fileby">File notice by</FieldLabel>
                    <div style={{ maxWidth: "14rem" }}>
                      <Input id="cd-r-fileby" type="date" value={draft.fileBy} invalid={submitted && !!errors.fileBy} onChange={(e) => set({ fileBy: e.target.value })} />
                    </div>
                    {submitted && errors.fileBy ? <FieldError>{errors.fileBy}</FieldError> : <FieldDescription>The contract's own deadline — terminal agreements usually run 12 months from the incident, haulage subcontracts 40 days from payment.</FieldDescription>}
                  </Field>
                </div>
              </CardPanel>
            </Card>
          </div>

          <div className="cd-col">
            <Card>
              <CardHeader>
                <CardTitle>What the file will pursue</CardTitle>
                <CardDescription>Read from the settlement, never typed — the claim owns this money and the recovery reads it.</CardDescription>
              </CardHeader>
              <CardPanel>
                <div className="cd-col cd-col--tight">
                  <div>
                    <div className="cd-figure__label">Sought</div>
                    {claim ? (
                      <>
                        <div className="cd-figure" style={{ fontSize: "var(--text-2xl)" }}>{formatMoney(claim.settlement.amountCents)}</div>
                        <div className="cd-figure__note">The settlement {claim.id} paid {claim.customer}, cleared {formatWhen(claim.settlement.clearedAt || claim.settlement.sentAt, now)} on {claim.settlement.reference}.</div>
                      </>
                    ) : (
                      <div className="cd-figure__note">Pick a claim to see what this recovery is worth.</div>
                    )}
                  </div>

                  <div>
                    <div className="cd-figure__label">Deadline to serve notice</div>
                    {fileByAt && fileByAt > now ? (
                      <>
                        <div className={"cd-figure " + clockClass(timeTone(daysLeft))} style={{ fontSize: "var(--text-2xl)" }}>{formatCountdown(fileByAt, now)}</div>
                        <div style={{ marginBlock: "var(--space-2)" }}>
                          <TimeBar startAt={now} endAt={fileByAt} now={now} label={"Notice deadline: " + formatCountdown(fileByAt, now)} />
                        </div>
                        <div className="cd-figure__note">Expires {formatAbsolute(fileByAt).split(",")[0]}. The file opens as to file, and the recovery destination counts it.</div>
                      </>
                    ) : (
                      <div className="cd-figure__note">Set the contractual deadline to see the clock this file will run on.</div>
                    )}
                  </div>

                  {claim ? (
                    <p className="cd-note">
                      Nothing here changes {claim.id}. The claimant has been paid and the file stays closed; this pursues the {formatMoney(claim.settlement.amountCents)} from whoever caused the damage.
                    </p>
                  ) : null}
                </div>
              </CardPanel>
            </Card>
          </div>
        </div>

        <div className="cd-formbar">
          <span className="cd-formbar__note">
            {submitted && !valid
              ? formatPlural(Object.keys(errors).length, "field") + " still to fill in."
              : "The file opens as to file. Serving the notice is the next step, on the recovery itself."}
          </span>
          {dirty ? <Button variant="outline" onClick={() => onNavigate("/recovery")}>Cancel</Button> : null}
          <Button disabled={!dirty} onClick={submit}>Save recovery</Button>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="ClaimsSettingsScreens.jsx">
/* Settings — four views behind one account-menu entry, reached by a tab strip
   inside the settings surface rather than by a second row of navigation.

   Commit model: STAGED on every view that has a control. Nothing applies until
   Save, Save and Cancel are disabled until the form is dirty, Cancel appears
   only once it is, and errors render on validation. The photo obeys the same
   rule: picking one previews it in the draft avatar and only reaches the
   account menu on Save. Authority holds no controls at all — it is set by the
   carrier, not by the person it limits — so it carries no form bar.

   Nothing is offered here that no screen reads: each desk preference names the
   screen it changes, and the alerts list is the same ALERT_KINDS vocabulary
   every notification carries. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Field, FieldLabel, FieldDescription, FieldError, Input, Checkbox, Select, Button, Avatar,
  } = window.OriginDesignSystem_7c998b;

  const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
  /* Two letters at most, from the name the person actually typed — the fallback
     has to track the draft, not the saved user, or it lies while editing. */
  const initialsOf = (name) => name.trim().split(/\s+/).slice(0, 2).map((w) => w[0] || "").join("").toUpperCase() || "?";
  const PHOTO_MAX_MB = 5;

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

  /* ── Profile ──────────────────────────────────────────────────────────── */

  function ProfileView({ user, onSave }) {
    const initial = React.useMemo(() => ({ name: user.name, role: user.role, email: user.email, avatar: user.avatar || null }), [user]);
    const [draft, setDraft] = React.useState(initial);
    const [submitted, setSubmitted] = React.useState(false);
    /* The picked file is previewed from an object URL held only while the draft
       is unsaved; anything this screen created gets revoked when it goes. */
    const madeHere = React.useRef([]);
    const fileRef = React.useRef(null);
    const [photoError, setPhotoError] = React.useState(null);
    React.useEffect(() => { setDraft(initial); setSubmitted(false); setPhotoError(null); }, [initial]);
    React.useEffect(() => () => { madeHere.current.forEach((url) => URL.revokeObjectURL(url)); }, []);
    const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
    const errors = {};
    if (!draft.name.trim()) errors.name = "Enter the name that goes on a decision.";
    if (!EMAIL.test(draft.email)) errors.email = "Enter a valid email address.";
    const valid = Object.keys(errors).length === 0;

    const pickPhoto = (event) => {
      const file = event.target.files && event.target.files[0];
      event.target.value = "";
      if (!file) return;
      if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setPhotoError("Use a PNG, JPG or WebP image."); return; }
      if (file.size > PHOTO_MAX_MB * 1024 * 1024) { setPhotoError("That file is over " + PHOTO_MAX_MB + " MB. Use a smaller image."); return; }
      const url = URL.createObjectURL(file);
      madeHere.current.push(url);
      setPhotoError(null);
      setDraft(Object.assign({}, draft, { avatar: url }));
    };

    return (
      <>
        <Card>
          <CardHeader>
            <CardTitle>How you appear on a file</CardTitle>
            <CardDescription>Claimants and counterparties see the name and the title; the email is where their replies land.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="cd-fields">
              <Field>
                <FieldLabel htmlFor="cd-p-photo">Photo</FieldLabel>
                <div className="cd-photo">
                  <Avatar size={72} src={draft.avatar || undefined} alt="" fallback={initialsOf(draft.name)} />
                  <div className="cd-photo__text">
                    <div className="cd-photo__actions">
                      <Button variant="outline" size="sm" onClick={() => fileRef.current && fileRef.current.click()}>{draft.avatar ? "Replace photo" : "Upload photo"}</Button>
                      {draft.avatar ? <Button variant="ghost" size="sm" onClick={() => { setPhotoError(null); setDraft(Object.assign({}, draft, { avatar: null })); }}>Remove</Button> : null}
                    </div>
                    <input ref={fileRef} id="cd-p-photo" className="o-sr" type="file" accept="image/png,image/jpeg,image/webp" onChange={pickPhoto} />
                    {photoError ? <FieldError>{photoError}</FieldError> : <FieldDescription>Square PNG, JPG or WebP under {PHOTO_MAX_MB} MB. Without one, your initials stand in.</FieldDescription>}
                  </div>
                </div>
              </Field>
              <Field>
                <FieldLabel htmlFor="cd-p-name">Name</FieldLabel>
                <Input id="cd-p-name" autoComplete="name" value={draft.name} invalid={submitted && !!errors.name} onChange={(e) => setDraft(Object.assign({}, draft, { name: e.target.value }))} />
                {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="cd-p-role">Title</FieldLabel>
                <Input id="cd-p-role" value={draft.role} onChange={(e) => setDraft(Object.assign({}, draft, { role: e.target.value }))} />
                <FieldDescription>Shown under your name in the account menu and on outgoing correspondence.</FieldDescription>
              </Field>
              <Field>
                <FieldLabel htmlFor="cd-p-email">Email</FieldLabel>
                <Input id="cd-p-email" type="email" autoComplete="email" value={draft.email} invalid={submitted && !!errors.email} onChange={(e) => setDraft(Object.assign({}, draft, { email: e.target.value }))} />
                {submitted && errors.email ? <FieldError>{errors.email}</FieldError> : <FieldDescription>Where exports, alerts and replies are sent.</FieldDescription>}
              </Field>
            </div>
          </CardPanel>
        </Card>
        <FormBar
          dirty={dirty}
          note={dirty ? "Unsaved changes to your profile." : "Everything here is as it was last saved."}
          onCancel={() => { setDraft(initial); setSubmitted(false); setPhotoError(null); }}
          onSave={() => { setSubmitted(true); if (valid) onSave(draft); }}
          label="Save profile"
        />
      </>
    );
  }

  /* ── Authority ────────────────────────────────────────────────────────── */

  function AuthorityView({ data, now, onNavigate }) {
    /* Authority is not editable by the person it limits. What this view can
       honestly show is what it costs today: the claims whose payable figure is
       above it, counted with the predicate the decision screen uses, and the
       ones already sitting with the manager because of it. */
    const limit = CLAIM_POLICY.authorityLimitCents;
    const above = data.claims.filter((claim) => {
      const payable = payableOf(claim);
      return payable !== null && CLAIM_FLAGS.needsSignoff(payable);
    });
    const waiting = awaitingSignoff(data.claims);

    return (
      <>
        <Card>
          <CardHeader>
            <CardTitle>Your authority</CardTitle>
            <CardDescription>Set by the carrier, not by you — it is shown here so nothing about a sign-off is a surprise.</CardDescription>
          </CardHeader>
          <CardPanel>
            <dl className="cd-defs">
              <dt>Decide alone up to</dt><dd className="cd-amount">{formatMoney(limit)}</dd>
              <dt>Signs off above it</dt><dd>{CLAIMS_MANAGER.name} · {CLAIMS_MANAGER.role}</dd>
              <dt>Claims on the desk above it</dt><dd>{above.length} of {data.claims.length}</dd>
              <dt>Waiting on a signature now</dt><dd>{waiting.length ? formatPlural(waiting.length, "decision") + " · " + formatMoney(signoffValue(waiting)) : "None"}</dd>
            </dl>
          </CardPanel>
          <CardFooter className="cd-card-footer">
            <span className="cd-footnote">{above.length ? above.map((c) => c.id).join(", ") + " would need a signature today." : "Nothing on the desk needs a signature today."}</span>
            <Button variant="outline" size="sm" onClick={() => onNavigate(waiting.length ? "/signoff" : "/")}>{waiting.length ? "Open sign-off" : "Open the queue"}</Button>
          </CardFooter>
        </Card>
      </>
    );
  }

  /* ── Alerts ───────────────────────────────────────────────────────────── */

  function AlertsView({ user, data, now, onNavigate, onSave }) {
    const initial = React.useMemo(() => ({ alerts: (user.alerts || []).slice() }), [user]);
    const [draft, setDraft] = React.useState(initial);
    React.useEffect(() => { setDraft(initial); }, [initial]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
    const toggle = (value) => setDraft({ alerts: draft.alerts.indexOf(value) > -1 ? draft.alerts.filter((v) => v !== value) : draft.alerts.concat([value]) });
    /* What each kind has actually raised, so a choice is made against the
       traffic rather than against the label. */
    const raised = (kind) => notificationsOfKind(data.notifications, kind).length;
    const covered = data.notifications.filter((n) => draft.alerts.indexOf(n.kind) > -1);

    return (
      <>
        <Card>
          <CardHeader>
            <CardTitle>What we email you</CardTitle>
            <CardDescription>Every kind is raised on the desk either way; these choose which also reach {user.email}.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="cd-checks">
              {ALERT_KINDS.map((kind) => (
                <label className="cd-check" key={kind.value}>
                  <Checkbox checked={draft.alerts.indexOf(kind.value) > -1} onCheckedChange={() => toggle(kind.value)} />
                  <span className="cd-check__text">
                    <span>{kind.label}</span>
                    <span className="cd-record__meta">{kind.hint}</span>
                  </span>
                  <span className="cd-check__count">{raised(kind.value) ? formatPlural(raised(kind.value), "notification") : "None raised"}</span>
                </label>
              ))}
            </div>
          </CardPanel>
          <CardFooter className="cd-card-footer">
            <span className="cd-footnote">
              {draft.alerts.length} of {ALERT_KINDS.length} kinds selected — {formatCount(covered.length)} of the {formatCount(data.notifications.length)} notifications on the desk would have been emailed.
            </span>
            <button type="button" className="cd-record__link" onClick={() => onNavigate("/notifications")}>Open notifications</button>
          </CardFooter>
        </Card>
        <FormBar
          dirty={dirty}
          note={dirty ? "Unsaved changes to your alerts." : "Everything here is as it was last saved."}
          onCancel={() => setDraft(initial)}
          onSave={() => onSave(draft)}
          label="Save alerts"
        />
      </>
    );
  }

  /* ── Desk ─────────────────────────────────────────────────────────────── */

  function DeskView({ desk, onSave }) {
    const initial = React.useMemo(() => Object.assign({}, DESK_DEFAULTS, desk), [desk]);
    const [draft, setDraft] = React.useState(initial);
    React.useEffect(() => { setDraft(initial); }, [initial]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
    const set = (fields) => setDraft(Object.assign({}, draft, fields));
    const landingItems = CLAIMS_DESTINATIONS.map((route) => ({ value: route.path, label: route.label }));
    const tabItems = QUEUE_TABS.map((tab) => ({ value: tab.value, label: tab.label }));

    return (
      <>
        <Card>
          <CardHeader>
            <CardTitle>How the desk opens</CardTitle>
            <CardDescription>Three settings, each naming the screen it changes. Nothing here is cosmetic.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="cd-fields">
              <Field>
                <FieldLabel htmlFor="cd-d-landing">Where signing in lands you</FieldLabel>
                <Select id="cd-d-landing" items={landingItems} value={draft.landing} onValueChange={(v) => set({ landing: v })} />
                <FieldDescription>The first screen after you sign in.</FieldDescription>
              </Field>
              <Field>
                <FieldLabel htmlFor="cd-d-tab">The work queue's first tab</FieldLabel>
                <Select id="cd-d-tab" items={tabItems} value={draft.queueTab} onValueChange={(v) => set({ queueTab: v })} />
                <FieldDescription>Which set of claims the queue shows before you touch a filter.</FieldDescription>
              </Field>
              <Field>
                <FieldLabel>Opening a claim</FieldLabel>
                <label className="cd-check">
                  <Checkbox checked={!!draft.openDecidable} onCheckedChange={() => set({ openDecidable: !draft.openDecidable })} />
                  <span className="cd-check__text">
                    <span>Open a decidable claim at its decision</span>
                    <span className="cd-record__meta">Claims with every mandatory document on file skip the assessment and open on the decision surface. Everything else opens on the assessment as usual.</span>
                  </span>
                </label>
              </Field>
            </div>
          </CardPanel>
        </Card>
        <FormBar
          dirty={dirty}
          note={dirty ? "Unsaved changes to your desk." : "Everything here is as it was last saved."}
          onCancel={() => setDraft(initial)}
          onSave={() => onSave(draft)}
          label="Save desk"
        />
      </>
    );
  }

  /* ── The settings surface ─────────────────────────────────────────────── */

  const LEDES = {
    profile: "The name, title and face that go on every decision, offer and recovery notice.",
    authority: "What you can settle alone, who signs above it, and what that costs the desk today.",
    alerts: "Which of the things this desk raises also reach your inbox.",
    desk: "How the desk opens, and where a claim opens from the queue.",
  };

  function SettingsScreen({ view, user, desk, data, now, onNavigate, onSaveProfile, onSaveAlerts, onSaveDesk }) {
    const spec = SETTINGS_VIEWS.find((v) => v.value === view) || SETTINGS_VIEWS[0];
    return (
      <div className="cd-page">
        <PageHead path={spec.route.path} title="Settings" lede={LEDES[spec.value]} />
        <SettingsTabs view={spec.value} onNavigate={onNavigate} />
        {spec.value === "profile" ? <ProfileView user={user} onSave={onSaveProfile} /> : null}
        {spec.value === "authority" ? <AuthorityView data={data} now={now} onNavigate={onNavigate} /> : null}
        {spec.value === "alerts" ? <AlertsView user={user} data={data} now={now} onNavigate={onNavigate} onSave={onSaveAlerts} /> : null}
        {spec.value === "desk" ? <DeskView desk={desk} onSave={onSaveDesk} /> : null}
      </div>
    );
  }

  Object.assign(window, { SettingsScreen, FormBar });
})();

</script>
<script type="text/babel" data-presets="react" data-file="ClaimsAuthScreens.jsx">
/* Auth — its own shell, no navigation owner (AGENTS §7.1): no top bar, no
   destinations, no account menu. One layout variant: centred card, the carrier
   mark above the form as the only navigation. One credential strategy:
   password. No providers, because this project ships no `assets/providers/`
   marks and §7.4 forbids drawing them. Designed states: idle, field-invalid,
   submitting, signed-out. Rate limiting, lockout, MFA and password reset are
   out of scope for this template, so nothing links to them.

   Commit model: STAGED, as §7.3 requires of every auth form. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel,
    Field, FieldLabel, FieldError, Input, Button,
  } = window.OriginDesignSystem_7c998b;

  const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

  function AuthShell({ carrier, children, footer }) {
    return (
      <div className="cd-auth">
        <div className="cd-auth__col">
          <div className="cd-auth__mark">
            <img className="cd-auth__logo" src={window.__resources.brandMark} alt="" width="40" height="40" />
            <span className="cd-auth__words">
              <span className="cd-brand__name">{carrier.name}</span>
              <span className="cd-auth__desk">{carrier.desk}</span>
            </span>
          </div>
          {children}
          {footer ? <p className="cd-auth__foot">{footer}</p> : null}
        </div>
      </div>
    );
  }

  function SignInScreen({ carrier, user, onSignIn }) {
    const [values, setValues] = React.useState({ email: "", password: "" });
    const [submitted, setSubmitted] = React.useState(false);
    const [pending, setPending] = React.useState(false);
    const errors = {};
    if (!EMAIL.test(values.email)) errors.email = "Enter a valid email address.";
    if (values.password.length < 8) errors.password = "Your password is at least 8 characters.";
    const valid = Object.keys(errors).length === 0;
    const dirty = values.email !== "" || values.password !== "";
    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      setPending(true);
      window.setTimeout(() => { setPending(false); onSignIn(); }, 600);
    };
    return (
      <AuthShell carrier={carrier} footer={carrier.region + " cargo claims."}>
        <Card>
          <CardHeader>
            <CardTitle>Sign in</CardTitle>
            <CardDescription>To the {carrier.name} {carrier.desk.toLowerCase()}.</CardDescription>
          </CardHeader>
          <CardPanel>
            <form className="cd-fields" onSubmit={(e) => { e.preventDefault(); submit(); }}>
              <Field>
                <FieldLabel htmlFor="cd-signin-email">Email</FieldLabel>
                <Input id="cd-signin-email" type="email" autoComplete="email" placeholder={user.email} value={values.email}
                  invalid={submitted && !!errors.email} onChange={(e) => setValues(Object.assign({}, values, { email: e.target.value }))} />
                {submitted && errors.email ? <FieldError>{errors.email}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="cd-signin-password">Password</FieldLabel>
                <Input id="cd-signin-password" type="password" autoComplete="current-password" value={values.password}
                  invalid={submitted && !!errors.password} onChange={(e) => setValues(Object.assign({}, values, { password: e.target.value }))} />
                {submitted && errors.password ? <FieldError>{errors.password}</FieldError> : null}
              </Field>
              <Button type="submit" loading={pending} disabled={!dirty || pending}>Sign in</Button>
            </form>
          </CardPanel>
        </Card>
      </AuthShell>
    );
  }

  function SignedOutScreen({ carrier, user, onSignIn }) {
    return (
      <AuthShell carrier={carrier} footer="Time bars keep running whether anybody is signed in or not.">
        <Card>
          <CardHeader>
            <CardTitle>You are signed out</CardTitle>
            <CardDescription>{user.name} was signed out of the {carrier.name} {carrier.desk.toLowerCase()}.</CardDescription>
          </CardHeader>
          <CardPanel>
            <Button onClick={onSignIn}>Sign in</Button>
          </CardPanel>
        </Card>
      </AuthShell>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="App.jsx">
/* Claims 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 live on their screens,
   consequential ones apply here and raise an undo toast, irreversible ones open
   a confirm dialog, and anything needing two or three fields opens a staged
   dialog. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  const [read, setRead] = React.useState(NOTIFICATIONS_READ);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [data, setData] = React.useState({ claims: CLAIMS, claimants: CLAIMANTS, recoveries: RECOVERIES, notifications: NOTIFICATIONS });
  const [user, setUser] = React.useState(CLAIMS_USER);
  const [desk, setDesk] = React.useState(DESK_DEFAULTS);
  /* Which claimant a new claim is being opened for, set by the claimant
     record's own button. The create screen reads it once and clears it on
     submit — it is a prefill, not a route parameter. */
  const [newClaimFor, setNewClaimFor] = React.useState(null);
  /* One way in, so a prefill can never be inherited: every entry point states
     the claimant it means, and leaving the screen unsaved drops it. */
  const openNewClaim = (claimantId) => { setNewClaimFor(claimantId || null); setPath("/claims/new"); };
  React.useEffect(() => { if (path !== "/claims/new" && newClaimFor) setNewClaimFor(null); }, [path]);
  const palette = useCommandPalette();
  const now = CLAIMS_NOW;

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

  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: id }, toast)]));
    window.setTimeout(() => dismiss(id), 8000);
  };

  /* A committing action writes into the fixture and re-renders, so every figure
     that claims to derive from it actually moves. */
  const patchClaim = (id, fields) => setData((d) => Object.assign({}, d, { claims: d.claims.map((c) => (c.id === id ? Object.assign({}, c, fields) : c)) }));
  const restoreClaim = (snapshot) => setData((d) => Object.assign({}, d, { claims: d.claims.map((c) => (c.id === snapshot.id ? snapshot : c)) }));
  const patchRecovery = (id, fields) => setData((d) => Object.assign({}, d, { recoveries: d.recoveries.map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const restoreRecovery = (snapshot) => setData((d) => Object.assign({}, d, { recoveries: d.recoveries.map((r) => (r.id === snapshot.id ? snapshot : r)) }));
  const withNote = (claim, text) => (claim.notes || []).concat([{ at: now, who: CLAIMS_USER.name, text: text }]);
  const nextReference = () => {
    const numbers = settledClaims(data.claims)
      .map((c) => Number(String(c.settlement.reference || "").split("-")[2]))
      .filter((n) => !isNaN(n));
    return "PAY-26-" + (Math.max.apply(null, numbers.concat([3400])) + 1);
  };

  /* ── Claim transitions ─────────────────────────────────────────────── */
  const onClaimTransition = (claim, transition) => {
    if (transition.id === "chase_documents" || transition.id === "chase_survey") {
      const missing = CLAIM_FLAGS.missingMandatory(claim).map((m) => labelOf(DOCUMENT_TYPES, m).toLowerCase());
      const who = transition.id === "chase_survey" ? "the surveyor" : "the customer";
      patchClaim(claim.id, { chasedAt: now, notes: withNote(claim, "Chased " + who + (missing.length ? " for " + missing.join(", ") + "." : ".")) });
      pushToast({ title: "Chased " + who, description: claim.id + " · " + claim.customer, onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "chase_signoff") {
      const payable = payableOf(claim);
      patchClaim(claim.id, { chasedAt: now, notes: withNote(claim, "Chased " + CLAIMS_MANAGER.name + " for sign-off on " + formatMoney(payable) + ".") });
      pushToast({ title: "Chased " + CLAIMS_MANAGER.name, description: claim.id + " · " + formatMoney(payable) + " waiting on a signature", onUndo: () => restoreClaim(claim) });
      return;
    }
    /* Taking a decision back is reversible, so it raises an undo toast rather
       than a confirm dialog. The decision is cleared, not kept: a withdrawn
       decision that stayed on the record would show a payable figure nobody
       has approved. */
    if (transition.id === "withdraw_decision") {
      patchClaim(claim.id, {
        status: "ready_for_decision",
        decision: null,
        notes: withNote(claim, "Decision withdrawn from sign-off before " + CLAIMS_MANAGER.name + " signed it. Back on this desk as ready for decision."),
      });
      pushToast({ title: "Decision withdrawn", description: claim.id + " is back on your desk", onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "escalate") {
      setConfirm({
        title: "Escalate " + claim.id + " to legal?",
        description: "Legal takes the file and this desk stops work on it. The time bar keeps running — " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now) + " — and only legal can hand it back.",
        confirmLabel: "Escalate to legal",
        run: () => {
          patchClaim(claim.id, { status: "escalated_legal", notes: withNote(claim, "Escalated to legal.") });
          pushToast({ title: "Escalated to legal", description: claim.id + " · " + claim.customer });
        },
      });
      return;
    }
    if (transition.id === "recall") {
      setConfirm({
        title: "Recall " + claim.id + " from legal?",
        description: "The file comes back to your desk as ready for decision, and legal closes its own matter. " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now) + " on the time bar.",
        confirmLabel: "Recall from legal",
        run: () => {
          patchClaim(claim.id, { status: "ready_for_decision", notes: withNote(claim, "Recalled from legal.") });
          pushToast({ title: "Recalled from legal", description: claim.id + " is back on your desk" });
        },
      });
    }
  };

  const onDocumentAction = (claim, document, transition) => {
    const label = labelOf(DOCUMENT_TYPES, document.type);
    const next = transition.id === "mark_illegible"
      ? { status: "illegible", note: "Rejected as illegible by " + CLAIMS_USER.name + "." }
      : { status: "requested", receivedAt: null, note: "Requested again on " + formatAbsolute(now) + "." };
    patchClaim(claim.id, {
      documents: claim.documents.map((d) => (d.type === document.type ? Object.assign({}, d, next) : d)),
      notes: withNote(claim, transition.id === "mark_illegible" ? label + " rejected as illegible." : label + " requested again."),
    });
    pushToast({
      title: transition.id === "mark_illegible" ? label + " marked illegible" : label + " requested again",
      description: claim.id + " · " + claim.customer,
      onUndo: () => restoreClaim(claim),
    });
  };

  const onSaveAssessment = (claim, adjustments) => {
    patchClaim(claim.id, {
      assessment: Object.assign({}, adjustments, { assessedBy: CLAIMS_USER.name, assessedAt: now }),
      status: claim.status === "ready_for_decision" ? claim.status : claim.status,
    });
  };

  const onRecordDecision = (claim, decision) => {
    const recorded = {
      outcome: decision.outcome,
      decidedAt: now,
      decidedBy: CLAIMS_USER.name,
      signedOffBy: decision.needsSignoff ? null : null,
      rejectReason: decision.rejectReason,
      justification: decision.justification,
    };
    if (decision.outcome === "reject") {
      patchClaim(claim.id, { status: "rejected", decision: recorded, notes: withNote(claim, "Claim rejected — " + labelOf(REJECT_REASONS, decision.rejectReason) + ".") });
      pushToast({ title: "Claim rejected", description: claim.id + " · " + labelOf(REJECT_REASONS, decision.rejectReason) });
      return;
    }
    if (decision.needsSignoff) {
      patchClaim(claim.id, {
        status: "awaiting_signoff",
        decision: recorded,
        notes: withNote(claim, "Decision of " + formatMoney(decision.amountCents) + " sent to " + CLAIMS_MANAGER.name + " for sign-off."),
      });
      pushToast({ variant: "success", title: "Sent for sign-off", description: formatMoney(decision.amountCents) + " · with " + CLAIMS_MANAGER.name });
      return;
    }
    patchClaim(claim.id, {
      status: "approved_awaiting_payment",
      decision: recorded,
      settlement: { status: "unscheduled", amountCents: decision.amountCents },
      notes: withNote(claim, labelOf(DECISION_OUTCOMES, decision.outcome) + " at " + formatMoney(decision.amountCents) + "."),
    });
    pushToast({ variant: "success", title: "Claim approved", description: formatMoney(decision.amountCents) + " to " + claim.customer + " · now on the payments desk" });
  };

  /* ── Settlements ───────────────────────────────────────────────────── */
  const onSettlementTransition = (claim, transition) => {
    if (transition.id === "schedule_payment") {
      const runAt = nextRunDate(now);
      const reference = nextReference();
      patchClaim(claim.id, { settlement: Object.assign({}, claim.settlement, { status: "scheduled", scheduledFor: runAt, reference: reference, returnedAt: undefined, returnReason: undefined }) });
      pushToast({ variant: "success", title: "Payment scheduled", description: formatMoney(claim.settlement.amountCents) + " to " + claim.customer + " on " + formatAbsolute(runAt) + " · " + reference, onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "send_payment") {
      setConfirm({
        title: "Send " + formatMoney(claim.settlement.amountCents) + " to " + claim.customer + "?",
        description: "The money leaves the account today and cannot be recalled. " + claim.customer + " is told the claim is settled, and the offer goes to them for acceptance.",
        confirmLabel: "Send payment",
        run: () => {
          patchClaim(claim.id, {
            status: "paid",
            settlement: Object.assign({}, claim.settlement, { status: "sent", sentAt: now }),
            response: { status: "awaiting_response", offerSentAt: now },
            notes: withNote(claim, "Payment of " + formatMoney(claim.settlement.amountCents) + " sent, " + claim.settlement.reference + "."),
          });
          pushToast({ title: "Payment sent", description: formatMoney(claim.settlement.amountCents) + " to " + claim.customer + " · " + claim.settlement.reference });
        },
      });
    }
  };

  /* ── Customer responses ────────────────────────────────────────────── */
  const onResponseTransition = (claim, transition) => {
    if (transition.id === "record_response") {
      setForm({
        title: "Record the customer's response",
        description: claim.customer + " · " + claim.id + " · we offered " + formatMoney(claim.settlement.amountCents) + ".",
        submitLabel: "Record response",
        initial: {},
        fields: [
          { id: "answer", kind: "radio", label: "What did they say?", options: [{ value: "accepted", label: "Accepted the offer" }, { value: "disputed", label: "Disputed the offer" }] },
          { id: "demand", kind: "money", label: "What they hold us to", hint: "Their figure, above our " + formatMoney(claim.settlement.amountCents) + " offer.", when: (v) => v.answer === "disputed" },
          { id: "note", kind: "text", label: "In their words", placeholder: "Quote the letter or the call, so the file carries their reasoning.", when: (v) => v.answer === "disputed" },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.answer) errors.answer = "Pick what the customer said.";
          if (v.answer === "disputed") {
            const demand = parseMoney(v.demand || "");
            if (isNaN(demand) || demand <= claim.settlement.amountCents) errors.demand = "A dispute means they want more than the " + formatMoney(claim.settlement.amountCents) + " offered.";
            if (!v.note || v.note.trim().length < 20) errors.note = "Record what they actually said — at least a sentence.";
          }
          return errors;
        },
        run: (v) => {
          if (v.answer === "accepted") {
            patchClaim(claim.id, { response: Object.assign({}, claim.response, { status: "accepted", respondedAt: now }), notes: withNote(claim, "Customer accepted the offer of " + formatMoney(claim.settlement.amountCents) + ".") });
            pushToast({ variant: "success", title: "Offer accepted", description: claim.id + " · " + claim.customer });
            return;
          }
          const demand = parseMoney(v.demand);
          patchClaim(claim.id, {
            response: Object.assign({}, claim.response, { status: "disputed", respondedAt: now, demandCents: demand, note: v.note.trim() }),
            notes: withNote(claim, "Customer disputed the offer and holds us to " + formatMoney(demand) + "."),
          });
          pushToast({ title: "Dispute recorded", description: formatMoney(demand - claim.settlement.amountCents) + " now in dispute · " + claim.id });
        },
      });
      return;
    }
    if (transition.id === "reopen_assessment") {
      patchClaim(claim.id, { status: "ready_for_decision", notes: withNote(claim, "Assessment reopened after the customer disputed the offer.") });
      pushToast({ title: "Assessment reopened", description: claim.id + " is back on your desk — the payment already made stands", onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "escalate") onClaimTransition(claim, transition);
  };

  /* ── Recoveries ────────────────────────────────────────────────────── */
  const onRecoveryTransition = (recovery, transition) => {
    if (transition.id === "file_notice") {
      patchRecovery(recovery.id, {
        status: "filed",
        noticeServedAt: now,
        log: recovery.log.concat([{ at: now, who: CLAIMS_USER.name, text: "Notice of claim served on " + recovery.counterparty.name + " for " + formatMoney(recovery.soughtCents) + ", with the survey report and proof of payment." }]),
      });
      pushToast({ variant: "success", title: "Notice filed", description: recovery.id + " · " + recovery.counterparty.name, onUndo: () => restoreRecovery(recovery) });
      return;
    }
    if (transition.id === "record_recovery") {
      setForm({
        title: "Record money recovered",
        description: recovery.counterparty.name + " · " + recovery.id + " · " + formatMoney(recovery.soughtCents) + " sought.",
        submitLabel: "Record recovery",
        initial: {},
        fields: [
          { id: "amount", kind: "money", label: "Amount recovered", hint: "Anything up to the " + formatMoney(recovery.soughtCents) + " sought." },
          { id: "note", kind: "text", label: "On what basis", placeholder: "How they settled, and on what limitation." },
        ],
        validate: (v) => {
          const errors = {};
          const amount = parseMoney(v.amount || "");
          if (isNaN(amount) || amount <= 0) errors.amount = "Enter the amount they have paid.";
          else if (amount > recovery.soughtCents) errors.amount = "You cannot recover more than the " + formatMoney(recovery.soughtCents) + " sought.";
          if (!v.note || v.note.trim().length < 20) errors.note = "Record the basis — at least a sentence.";
          return errors;
        },
        run: (v) => {
          const amount = parseMoney(v.amount);
          patchRecovery(recovery.id, {
            status: "recovered", recoveredCents: amount, offerCents: amount, closedAt: now,
            log: recovery.log.concat([{ at: now, who: recovery.counterparty.name, text: v.note.trim() }]),
          });
          pushToast({ variant: "success", title: "Recovery recorded", description: formatMoney(amount) + " back from " + recovery.counterparty.name });
        },
      });
      return;
    }
    if (transition.id === "write_off") {
      setConfirm({
        title: "Write off " + formatMoney(RECOVERY_FLAGS.shortfall(recovery)) + " against " + recovery.counterparty.name + "?",
        description: "The file closes and the shortfall stays with us. It counts against the recovery rate, and the contract deadline of " + formatAbsolute(recovery.fileByAt) + " cannot be reopened once it passes.",
        confirmLabel: "Write off recovery",
        destructive: true,
        run: () => {
          patchRecovery(recovery.id, {
            status: "written_off", closedAt: now,
            log: recovery.log.concat([{ at: now, who: CLAIMS_USER.name, text: "Written off at " + formatMoney(RECOVERY_FLAGS.shortfall(recovery)) + " — not worth pursuing further." }]),
          });
          pushToast({ title: "Recovery written off", description: recovery.id + " · " + recovery.counterparty.name });
        },
      });
    }
  };

  /* ── Creating records ─────────────────────────────────────────────── */
  /* Ids continue the series already in the fixture rather than restarting: the
     next claim after CLM-26-0212 is CLM-26-0213, on the desk and in the
     breadcrumb alike. */
  const nextId = (rows, prefix) => {
    const numbers = rows.map((r) => (String(r.id).indexOf(prefix) === 0 ? Number(String(r.id).slice(prefix.length)) : NaN)).filter((n) => !isNaN(n));
    return prefix + String(Math.max.apply(null, numbers.concat([0])) + 1).padStart(4, "0");
  };
  const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");

  const onCreateClaim = (input) => {
    const id = nextId(data.claims, "CLM-26-");
    const existing = input.claimantId ? claimantById(data.claimants, input.claimantId) : null;
    let claimantId = existing ? existing.id : slugify(input.newClaimant.name);
    if (!existing && claimantById(data.claimants, claimantId)) claimantId = claimantId + "-" + id.slice(-4);
    const name = existing ? existing.name : input.newClaimant.name;
    const claim = {
      id: id, customer: name, claimedAt: now, deliveredAt: input.deliveredAt,
      bl: input.bl, container: input.container, vessel: input.vessel, voyage: input.voyage,
      commodity: input.commodity, packages: input.packages, weightKg: input.weightKg,
      pol: input.pol, pod: input.pod, incident: input.incident,
      status: "awaiting_documents", claimedCents: input.claimedCents, assessment: null,
      /* The whole checklist is opened as requested — the file starts waiting on
         the claimant, which is what awaiting_documents means. */
      documents: DOCUMENT_TYPES.map((type) => ({ type: type.value, status: "requested", receivedAt: null, note: "Requested when the file was opened." })),
      notes: [{ at: now, who: user.name, text: "File opened from the claim letter. All " + DOCUMENT_TYPES.length + " documents requested from " + name + "." }],
    };
    setData((d) => Object.assign({}, d, {
      claims: d.claims.concat([claim]),
      claimants: existing ? d.claimants : d.claimants.concat([{
        id: claimantId, name: name, contact: input.newClaimant.contact, email: input.newClaimant.email,
        since: now, lane: input.newClaimant.lane, terms: "Spot booking · bill of lading terms",
      }]),
    }));
    setNewClaimFor(null);
    pushToast({ variant: "success", title: "Claim opened", description: id + " · " + name + " · " + formatMoney(input.claimedCents) + " claimed" });
    setPath("/claims/" + id + "/evidence");
  };

  const onCreateRecovery = (input) => {
    const claim = claimById(data.claims, input.claimId);
    const id = nextId(data.recoveries, "REC-26-");
    /* soughtCents reads the settlement rather than storing a second figure. */
    const sought = claim.settlement.amountCents;
    const recovery = {
      id: id, claimId: claim.id, counterparty: input.counterparty, basis: input.basis,
      soughtCents: sought, recoveredCents: 0, offerCents: null,
      status: "to_file", openedAt: now, fileByAt: input.fileByAt,
      log: [{ at: now, who: user.name, text: "Recovery opened against " + input.counterparty.name + " on the settlement of " + formatMoney(sought) + " paid on " + claim.id + ". Notice not yet served." }],
    };
    setData((d) => Object.assign({}, d, {
      recoveries: d.recoveries.concat([recovery]),
      claims: d.claims.map((c) => (c.id === claim.id ? Object.assign({}, c, { recoveryId: id }) : c)),
    }));
    pushToast({ variant: "success", title: "Recovery opened", description: id + " · " + formatMoney(sought) + " against " + input.counterparty.name });
    setPath("/recovery/" + id);
  };

  /* ── Notifications ─────────────────────────────────────────────────── */
  /* Read state is per notification, so the bar's count, the panel and the page
     all derive from one list rather than from a single "seen" flag. */
  const unread = unreadNotifications(data.notifications, read);
  const markRead = (id) => setRead((ids) => (ids.indexOf(id) > -1 ? ids : ids.concat([id])));
  const onOpenNotification = (n) => { markRead(n.id); setPath(n.route); };
  const onReadAll = () => {
    const snapshot = read;
    setRead(data.notifications.map((n) => n.id));
    pushToast({ title: "Marked read", description: formatPlural(unread.length, "notification") + " cleared", onUndo: () => setRead(snapshot) });
  };

  /* ── Routing ───────────────────────────────────────────────────────── */
  const claimMatch = path.match(/^\/claims\/([^/]+)(\/(evidence|decision))?$/);
  const recoveryMatch = path.match(/^\/recovery\/([^/]+)$/);
  const claimantMatch = path.match(/^\/claimants\/([^/]+)$/);
  const settingsMatch = path === "/settings" ? "profile" : (path.match(/^\/settings\/(authority|alerts|desk)$/) || [])[1];
  const claim = claimMatch ? claimById(data.claims, claimMatch[1]) : null;
  const recovery = recoveryMatch ? recoveryById(data.recoveries, recoveryMatch[1]) : null;
  const claimant = claimantMatch ? claimantById(data.claimants, claimantMatch[1]) : null;

  const onSaveProfile = (draft) => {
    const snapshot = user;
    setUser(Object.assign({}, user, { name: draft.name.trim(), role: draft.role.trim(), email: draft.email.trim(), avatar: draft.avatar || null }));
    pushToast({ variant: "success", title: "Profile saved", description: draft.name.trim() + " · " + draft.role.trim(), onUndo: () => setUser(snapshot) });
  };
  const onSaveAlerts = (draft) => {
    const snapshot = user;
    setUser(Object.assign({}, user, { alerts: draft.alerts }));
    pushToast({ variant: "success", title: "Alerts saved", description: draft.alerts.length + " kinds of alert will be emailed to " + user.email, onUndo: () => setUser(snapshot) });
  };
  const onSaveDesk = (draft) => {
    const snapshot = desk;
    setDesk(draft);
    const landing = CLAIMS_DESTINATIONS.find((r) => r.path === draft.landing);
    pushToast({ variant: "success", title: "Desk saved", description: "Signing in opens " + (landing ? landing.label : "Work queue"), onUndo: () => setDesk(snapshot) });
  };

  /* Auth has no navigation owner, so it is returned before the shell. */
  /* Harness chrome, not product chrome: the floating page menu is how a reader
     reaches every registered screen, including the auth ones the shell cannot
     render. It sits outside the shell so it is on those screens too. */
  const preview = (
    <TemplateMenu product="Claims Desk" path={path} onNavigate={setPath} groups={CLAIMS_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (CLAIMS_AUTH_PATHS.indexOf(path) > -1) {
    return (
      <>
        {path === "/signin"
          ? <SignInScreen carrier={CLAIMS_CARRIER} user={user} onSignIn={() => setPath(desk.landing)} />
          : <SignedOutScreen carrier={CLAIMS_CARRIER} user={user} onSignIn={() => setPath("/signin")} />}
        {preview}
      </>
    );
  }

  const screen = () => {
    if (path === "/") return <QueueScreen data={data} now={now} desk={desk} onNavigate={setPath} onNewClaim={openNewClaim} onChase={(c) => onClaimTransition(c, claimTransitions(c)[0])} pushToast={pushToast} />;
    if (path === "/signoff") return <SignoffScreen data={data} now={now} onNavigate={setPath} onTransition={onClaimTransition} />;
    if (path === "/payments") return <PaymentsScreen data={data} now={now} onNavigate={setPath} onSettlementTransition={onSettlementTransition} pushToast={pushToast} />;
    if (path === "/responses") return <ResponsesScreen data={data} now={now} onNavigate={setPath} onResponseTransition={onResponseTransition} pushToast={pushToast} />;
    if (path === "/recovery") return <RecoveryScreen data={data} now={now} onNavigate={setPath} onRecoveryTransition={onRecoveryTransition} pushToast={pushToast} />;
    if (path === "/notifications") return <NotificationsScreen items={data.notifications} readIds={read} user={user} now={now} onNavigate={setPath} onOpen={onOpenNotification} onReadAll={onReadAll} />;
    if (path === "/claims/new") return <NewClaimScreen data={data} now={now} forClaimant={newClaimFor} onNavigate={setPath} onCreate={onCreateClaim} />;
    if (path === "/recovery/new") return <NewRecoveryScreen data={data} now={now} onNavigate={setPath} onCreate={onCreateRecovery} />;
    if (claim) {
      const view = claimMatch[3] || "assessment";
      if (view === "evidence") return <EvidenceScreen claim={claim} now={now} onNavigate={setPath} onTransition={onClaimTransition} onDocumentAction={onDocumentAction} />;
      if (view === "decision") return <DecisionScreen claim={claim} now={now} onNavigate={setPath} onTransition={onClaimTransition} onRecordDecision={onRecordDecision} />;
      return <ClaimScreen claim={claim} data={data} now={now} onNavigate={setPath} onTransition={onClaimTransition} onSaveAssessment={onSaveAssessment} pushToast={pushToast} />;
    }
    if (claimant) return <ClaimantScreen claimant={claimant} data={data} now={now} onNavigate={setPath} onNewClaim={openNewClaim} />;
    if (settingsMatch) {
      return (
        <SettingsScreen
          view={settingsMatch} user={user} desk={desk} data={data} now={now} onNavigate={setPath}
          onSaveProfile={onSaveProfile} onSaveAlerts={onSaveAlerts} onSaveDesk={onSaveDesk}
        />
      );
    }
    if (recovery) return <RecoveryDetailScreen recovery={recovery} data={data} now={now} onNavigate={setPath} onRecoveryTransition={onRecoveryTransition} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Claims you can decide now", items: decidableClaims(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(payableOf(c) || c.claimedCents), route: "/claims/" + c.id + "/decision" })) },
    { label: "Waiting on a signature", items: awaitingSignoff(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(payableOf(c)), route: "/signoff" })) },
    { label: "Payments to schedule", items: settlementsToSchedule(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(c.settlement.amountCents), route: "/payments" })) },
    { label: "Recoveries to file", items: recoveriesToFile(data.recoveries).map((r) => ({ value: r.id, label: r.id + " · " + r.counterparty.name + " · " + formatMoney(r.soughtCents), route: "/recovery/" + r.id })) },
    { label: "Claimants", items: data.claimants.map((p) => ({ value: p.id, label: p.name + " · " + formatPlural(claimsOfClaimant(data.claims, p).length, "claim"), route: "/claimants/" + p.id })) },
  ];

  return (
    <ClaimsShell
      user={user}
      path={path}
      onNavigate={setPath}
      dark={dark}
      onToggleTheme={() => setDark(!dark)}
      data={data}
      notifications={data.notifications}
      readIds={read}
      unread={unread.length}
      onRead={onReadAll}
      onOpenNotification={onOpenNotification}
      palette={palette}
      toasts={toasts}
      onDismissToast={dismiss}
    >
      {screen()}
      <CommandPalette
        open={palette.open}
        onClose={() => palette.setOpen(false)}
        routes={CLAIMS_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search claims, containers, recoveries…"
      />
      <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}
    </ClaimsShell>
  );
}
/* boot.js publishes a stub on window.ClaimsDeskApp before anything is fetched,
   so <x-import> always resolves; the real component lands here and the stub
   swaps it in. */
window.__claimsDeskReal = App;
if (!window.ClaimsDeskApp) window.ClaimsDeskApp = App;

</script>
<script type="text/babel" data-presets="react" data-file="mount.jsx">
ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(window.ClaimsDeskApp, {}));
</script>
</body>
</html>
