<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Talent Desk — Marque</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/img/avatar-40-01.jpg" data-resource-id="assets_img_avatar_40_01_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar.jpg" data-resource-id="assets_img_avatar_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-40-04.jpg" data-resource-id="assets_img_avatar_40_04_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-06.jpg" data-resource-id="assets_img_avatar_80_06_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-01.jpg" data-resource-id="assets_img_avatar_80_01_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-05.jpg" data-resource-id="assets_img_avatar_80_05_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-40-02.jpg" data-resource-id="assets_img_avatar_40_02_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-40-03.jpg" data-resource-id="assets_img_avatar_40_03_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-03.jpg" data-resource-id="assets_img_avatar_80_03_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-02.jpg" data-resource-id="assets_img_avatar_80_02_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-80-04.jpg" data-resource-id="assets_img_avatar_80_04_jpg">
<meta name="ext-resource-dependency" content="../../assets/img/avatar-40-05.jpg" data-resource-id="assets_img_avatar_40_05_jpg">
<meta name="ext-resource-dependency" content="../../assets/templates/icons/instagram.svg" data-resource-id="assets_templates_icons_instagram_svg">
<meta name="ext-resource-dependency" content="../../assets/templates/icons/tiktok.svg" data-resource-id="assets_templates_icons_tiktok_svg">
<meta name="ext-resource-dependency" content="../../assets/templates/icons/youtube.svg" data-resource-id="assets_templates_icons_youtube_svg">
<meta name="ext-resource-dependency" content="../../assets/templates/logos/27-logo-demo.svg" data-resource-id="assets_templates_logos_27_logo_demo_svg">
<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="#1b1720">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg"><rect width="1200" height="800" fill="#1b1720"/><g transform="translate(420 250)"><circle cx="80" cy="80" r="80" fill="#a06cf5"/><circle cx="200" cy="80" r="80" fill="#efe9f7" opacity="0.9"/><circle cx="320" cy="80" r="80" fill="#efe9f7" opacity="0.45"/><rect x="0" y="230" width="360" height="14" rx="7" fill="#3a3145"/><rect x="0" y="230" width="230" height="14" rx="7" fill="#a06cf5"/></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">
/* Marque Talent 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.

   Agency policy constants live here too: they are part of this product's
   vocabulary (the commission floor, the usage uplifts, the notice window), and
   nothing outside this folder may read them. */

const AGENCY_POLICY = {
  /* Standard commission. Deals may be negotiated below it; the deal stores its
     own rate and the ledger reads that, never this. */
  standardCommissionRate: 0.2,
  /* A creator counts as "coming free" this many days before their booking ends,
     because that is how far ahead brands ask us to hold someone. */
  comingFreeDays: 30,
  /* Deliverables inside this window are "due now" on the week ladder and in the
     nav badge. One number, one meaning. */
  dueSoonDays: 7,
  /* Invoices are net-30 from issue. One term, one place. */
  paymentTermsDays: 30,
  /* Usage rights inside this many days are "expiring": the window in which a
     renewal is still a conversation rather than a takedown. */
  rightsNoticeDays: 60,
  /* Sign-in attempts before the account locks (AGENTS §7.3.6: lockout is a
     designed state, not a toast). One number, read by the form and by the
     copy that counts down. */
  signInAttempts: 3,
  /* A brief whose per-creator budget cannot buy anyone on the books is below
     the floor. The floor is derived from the roster, never typed — see
     rosterFloor() in metrics.js. */
};

/* creator.status — whether we can sell this creator today.
   Three values, three tones, grouped by the question the roster asks: "can I
   pitch them?"
     success = pitchable now
     info    = working, and the roster says until when
     neutral = they have asked us to stop pitching them
   Deliberately absent because they are derived: `coming_free` (bookedUntil
   inside AGENCY_POLICY.comingFreeDays), `conflicted` (an exclusivity that bites
   against one particular brief — it is a property of the pair, not of the
   creator), and `overbooked`. */
const CREATOR_STATUS = [
  { id: "available", label: "Free", tone: "success" },
  { id: "booked", label: "Booked", tone: "info" },
  { id: "paused", label: "Paused", tone: "neutral" },
];
/* ids: "available" | "booked" | "paused" */

/* brief.status — an inbound brand request, from arrival to outcome.
   warning = on the talent manager's desk · info = with the brand ·
   success = it became a deal · neutral = closed, by them or by us.
   `passed` is neutral, not danger: turning work down is a decision we made and
   can defend. Deliberately absent because derived: `expiring` (the brand's
   respond-by date against the clock). */
const BRIEF_STATUS = [
  { id: "to_triage", label: "To triage", tone: "warning" },
  { id: "pitching", label: "Pitched", tone: "info" },
  { id: "won", label: "Won", tone: "success" },
  { id: "passed", label: "Passed", tone: "neutral" },
  { id: "lost", label: "Lost", tone: "neutral" },
];
/* ids: "to_triage" | "pitching" | "won" | "passed" | "lost" */

/* contract.status — where the paper on a deal stands.
   warning = on us to move it · info = with the brand and the creator ·
   success = executed. A deal exists from the moment terms are agreed, so
   `drafted` is the first state and there is no "none". */
const CONTRACT_STATUS = [
  { id: "drafted", label: "Drafted", tone: "warning" },
  { id: "out_for_signature", label: "Out for signature", tone: "info" },
  { id: "signed", label: "Signed", tone: "success" },
  { id: "void", label: "Void", tone: "neutral" },
];
/* ids: "drafted" | "out_for_signature" | "signed" | "void"
   `void` exists because voiding a deal has to land somewhere: a deal killed
   before signature is not "drafted" any more and never became "signed". It is
   neutral, not danger — walking away from unsigned paper is a decision we made
   and can defend. Only reachable while nothing is signed and nothing is live. */

/* deliverable.status — one post or asset on one deal.
   neutral = nothing to do yet · info = with the brand · warning = it has come
   back and somebody has to fix it · success = live.
   Deliberately absent because derived: `overdue` (dueAt against the clock — a
   revision that is late is still `revisions_requested`) and `due_soon`. */
const DELIVERABLE_STATUS = [
  { id: "scheduled", label: "Scheduled", tone: "neutral" },
  { id: "submitted", label: "With brand", tone: "info" },
  { id: "revisions_requested", label: "Revisions requested", tone: "warning" },
  { id: "approved", label: "Approved", tone: "info" },
  { id: "published", label: "Published", tone: "success" },
];
/* ids: "scheduled" | "submitted" | "revisions_requested" | "approved" | "published" */

/* invoice.status — what the brand owes the agency.
   warning = on us to issue it · info = with the brand · success = banked.
   Deliberately absent because derived: `overdue` (dueAt < now and not paid). */
const INVOICE_STATUS = [
  { id: "draft", label: "Not issued", tone: "warning" },
  { id: "issued", label: "Issued", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
];
/* ids: "draft" | "issued" | "paid" */

/* payout.status — what the agency owes the creator, after commission.
   neutral = we cannot pay yet because the brand has not paid us, and the row
   says which brand · warning = the money is in and it is on us · success =
   gone. A payout is never `overdue`: if it is late it is late because the
   invoice is, and that is the invoice's fact to carry. */
const PAYOUT_STATUS = [
  { id: "awaiting_funds", label: "Awaiting funds", tone: "neutral" },
  { id: "due", label: "Due", tone: "warning" },
  { id: "paid", label: "Paid", tone: "success" },
];
/* ids: "awaiting_funds" | "due" | "paid" */

/* ── Classification lists — read through labelOf, never reaching a Badge. ── */

const CATEGORIES = [
  { value: "beauty", label: "Beauty" },
  { value: "fitness", label: "Fitness" },
  { value: "food", label: "Food & drink" },
  { value: "home", label: "Home & interiors" },
  { value: "fashion", label: "Fashion" },
  { value: "tech", label: "Technology" },
  { value: "travel", label: "Travel" },
  { value: "dating", label: "Dating & social" },
];

const PLATFORMS = [
  { value: "instagram", label: "Instagram", handlePrefix: "@" },
  { value: "tiktok", label: "TikTok", handlePrefix: "@" },
  { value: "youtube", label: "YouTube", handlePrefix: "" },
];

/* The rate card is keyed by these. A creator without a rate for a kind cannot
   quote for a brief that asks for it — that is a rate miss, not a zero. */
const DELIVERABLE_KINDS = [
  { value: "ig_reel", label: "Instagram Reel", platform: "instagram" },
  { value: "ig_story", label: "Story frame", platform: "instagram" },
  { value: "tiktok", label: "TikTok video", platform: "tiktok" },
  { value: "yt_integration", label: "YouTube integration", platform: "youtube" },
  { value: "ugc_pack", label: "UGC asset pack", platform: "instagram" },
];

/* Usage rights. The uplift is a multiplier on the organic fee — one number per
   scope, in one place, so the quote on the match screen and the fee on the deal
   sheet can never disagree. */
const USAGE_SCOPES = [
  { value: "organic", label: "Organic only", uplift: 0, months: 0 },
  { value: "paid_social_3m", label: "Paid social", uplift: 0.35, months: 3 },
  { value: "whitelisting_6m", label: "Whitelisting", uplift: 0.6, months: 6 },
  { value: "buyout_12m", label: "Full buyout", uplift: 1.2, months: 12 },
];

/* Why we turned a brief down. One of these is required to pass on a brief. */
const PASS_REASONS = [
  { value: "below_floor", label: "Budget below our floor" },
  { value: "wrong_category", label: "No one on the books in this category" },
  { value: "no_availability", label: "Nobody free in the brand's window" },
  { value: "brand_conflict", label: "Exclusivity conflict with a competing brand" },
];

/* The one password constant: the live checklist on the set-new screen and the
   validation that blocks its submit read this same list (AGENTS §7.5). */
const PASSWORD_RULES = [
  { id: "length", label: "At least 12 characters", test: (v) => v.length >= 12 },
  { id: "case", label: "An upper and a lower case letter", test: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) },
  { id: "number", label: "A number", test: (v) => /[0-9]/.test(v) },
];
const passwordMeets = (value) => PASSWORD_RULES.every((rule) => rule.test(value || ""));

/* ── Derived flags. Clock injected, never read from the module. ─────────── */

const CREATOR_FLAGS = {
  isPitchable: (creator) => creator.status === "available",
  /* Booked, but the booking ends inside the notice window — the answer to the
     question brands ask most often. */
  isComingFree: (creator, now) =>
    creator.status === "booked" &&
    creator.bookedUntil - now <= AGENCY_POLICY.comingFreeDays * 864e5 &&
    creator.bookedUntil >= now,
  daysUntilFree: (creator, now) => Math.round((creator.bookedUntil - now) / 864e5),
  /* Exclusivities that are still running today. */
  liveExclusivities: (creator, now) => (creator.exclusivities || []).filter((e) => e.until > now),
};

const BRIEF_FLAGS = {
  /* THE act-ability predicate for briefs. The nav badge, the triage deck and
     the tab count all call this one function. */
  needsTriage: (brief) => brief.status === "to_triage",
  isOpen: (brief) => ["to_triage", "pitching"].indexOf(brief.status) > -1,
  isClosed: (brief) => ["passed", "lost"].indexOf(brief.status) > -1,
  daysToRespond: (brief, now) => Math.round((brief.respondByAt - now) / 864e5),
  /* The brand wants the campaign live inside this window; availability is
     judged against its start, never against today. */
  windowStart: (brief) => brief.liveFromAt,
};

const CONTRACT_FLAGS = {
  isSigned: (deal) => deal.contract.status === "signed",
  isVoid: (deal) => deal.contract.status === "void",
  /* On the talent manager to move the paper. */
  awaitsAgency: (deal) => deal.contract.status === "drafted",
  awaitsCountersign: (deal) => deal.contract.status === "out_for_signature",
  daysOut: (deal, now) => (deal.contract.sentAt ? Math.round((now - deal.contract.sentAt) / 864e5) : null),
};

const DELIVERABLE_FLAGS = {
  isLive: (item) => item.status === "published",
  isWithBrand: (item) => ["submitted", "approved"].indexOf(item.status) > -1,
  needsRevision: (item) => item.status === "revisions_requested",
  /* Handed over. A deliverable that is with the brand cannot be late: we did
     our part on time and the clock is theirs now. */
  isDelivered: (item) => ["submitted", "approved", "published"].indexOf(item.status) > -1,
  /* Late, and it is still ours to fix. */
  isOverdue: (item, now) => item.dueAt < now && !DELIVERABLE_FLAGS.isDelivered(item),
  isDueSoon: (item, now) =>
    item.dueAt >= now &&
    item.dueAt - now <= AGENCY_POLICY.dueSoonDays * 864e5 &&
    !DELIVERABLE_FLAGS.isDelivered(item),
  /* THE act-ability predicate for deliverables: work the talent manager has to
     move this week. The nav badge, the "Needs you" section and the week ladder
     all call this. */
  needsAttention: (item, now) =>
    DELIVERABLE_FLAGS.needsRevision(item) || DELIVERABLE_FLAGS.isOverdue(item, now) || DELIVERABLE_FLAGS.isDueSoon(item, now),
};

const INVOICE_FLAGS = {
  /* Net-30 from issue. The due date is derived, never stored, so changing the
     terms moves every date and every overdue count with it. */
  dueAt: (invoice) => (invoice.issuedAt ? invoice.issuedAt + AGENCY_POLICY.paymentTermsDays * 864e5 : null),
  isSettled: (invoice) => invoice.status === "paid",
  isOutstanding: (invoice) => invoice.status !== "paid",
  isOverdue: (invoice, now) => invoice.status === "issued" && INVOICE_FLAGS.dueAt(invoice) < now,
  awaitsAgency: (invoice) => invoice.status === "draft",
  daysOverdue: (invoice, now) => Math.round((now - INVOICE_FLAGS.dueAt(invoice)) / 864e5),
};

/* Usage rights on a signed deal. Not a status field: what a brand may run and
   until when is computed from the scope it bought and the date the window
   opened, so it is a set of derived flags and `usageEndsAt` in metrics.js
   delegates here rather than working it out a second time. An organic deal
   grants nothing that can expire, so it has no date and never flags. */
const RIGHTS_FLAGS = {
  monthsOf: (deal) => (USAGE_SCOPES.find((u) => u.value === deal.usage) || USAGE_SCOPES[0]).months,
  isTimeLimited: (deal) => RIGHTS_FLAGS.monthsOf(deal) > 0,
  endsAt: (deal) => {
    if (!RIGHTS_FLAGS.isTimeLimited(deal)) return null;
    const end = new Date(deal.usageStartAt);
    end.setMonth(end.getMonth() + RIGHTS_FLAGS.monthsOf(deal));
    return end.getTime();
  },
  /* Still running, and close enough that renewing is a conversation. */
  isExpiring: (deal, now) => {
    const end = RIGHTS_FLAGS.endsAt(deal);
    return end !== null && end >= now && end - now <= AGENCY_POLICY.rightsNoticeDays * 864e5;
  },
  hasLapsed: (deal, now) => {
    const end = RIGHTS_FLAGS.endsAt(deal);
    return end !== null && end < now;
  },
};

const PAYOUT_FLAGS = {
  isSettled: (payout) => payout.status === "paid",
  /* THE act-ability predicate for money going out: the brand has paid and it is
     on us. `awaiting_funds` is blocked, not actionable, and the row names the
     brand it is waiting on. */
  isReleasable: (payout) => payout.status === "due",
  isBlocked: (payout) => payout.status === "awaiting_funds",
};

/* ── Transitions, named once, derived from the current status. ────────────
   One function per entity, called by the row action, the record header and the
   section alike. A transition that does not apply is omitted, never disabled.
   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. */

/* The brief's normal path is to match it to the roster. When nothing on the
   book fits it, the normal path is to say no — so the primary weight follows
   the fit count, decided here rather than on the screen. */
function briefTransitions(brief, fitCount) {
  const nobodyFits = fitCount === 0;
  switch (brief.status) {
    case "to_triage": return [
      { id: "pitch_creators", label: "Match creators", kind: "nav", primary: !nobodyFits },
      { id: "pass_brief", label: "Pass on brief", kind: "form", primary: nobodyFits },
    ];
    case "pitching": return [
      /* The brief's whole point is to become a deal, so booking one is the
         primary path the moment creators are in front of the brand. */
      { id: "book_deal", label: "Book the deal", kind: "form", primary: true },
      { id: "chase_brand", label: "Chase the brand", kind: "undo" },
      { id: "pass_brief", label: "Pass on brief", kind: "form" },
    ];
    case "passed":
    case "lost": return [{ id: "reopen_brief", label: "Reopen brief", kind: "undo", primary: true }];
    default: return [];
  }
}

function contractTransitions(deal) {
  switch (deal.contract.status) {
    case "drafted": return [{ id: "send_contract", label: "Send for signature", kind: "undo", primary: true }];
    case "out_for_signature": return [
      { id: "mark_signed", label: "Mark signed", kind: "undo", primary: true },
      { id: "chase_signature", label: "Chase signature", kind: "undo" },
    ];
    default: return [];
  }
}

/* Voiding a deal is the only irreversible thing on the deal sheet. It is
   offered only while the paper is unsigned and nothing has gone live — after
   either of those the transition is omitted, never disabled. */
function dealTransitions(deal, publishedCount) {
  if (publishedCount > 0) return [];
  if (["drafted", "out_for_signature"].indexOf(deal.contract.status) === -1) return [];
  return [{ id: "void_deal", label: "Void the deal", kind: "confirm", destructive: true }];
}

function deliverableTransitions(item) {
  switch (item.status) {
    case "scheduled": return [
      { id: "submit_to_brand", label: "Submit to brand", kind: "undo", primary: true },
      { id: "chase_creator", label: "Chase the creator", kind: "undo" },
    ];
    case "revisions_requested": return [
      { id: "submit_to_brand", label: "Submit to brand", kind: "undo", primary: true },
      { id: "chase_creator", label: "Chase the creator", kind: "undo" },
    ];
    case "submitted": return [
      { id: "mark_approved", label: "Mark approved", kind: "undo", primary: true },
      { id: "record_revisions", label: "Record revisions", kind: "form" },
    ];
    case "approved": return [{ id: "mark_published", label: "Mark published", kind: "form", primary: true }];
    default: return [];
  }
}

function invoiceTransitions(invoice) {
  switch (invoice.status) {
    case "draft": return [{ id: "issue_invoice", label: "Issue invoice", kind: "undo", primary: true }];
    case "issued": return [
      { id: "record_payment", label: "Record payment", kind: "undo", primary: true },
      { id: "chase_payment", label: "Chase payment", kind: "undo" },
    ];
    default: return [];
  }
}

function payoutTransitions(payout) {
  /* Money leaving the agency's account cannot be recalled. */
  return payout.status === "due" ? [{ id: "release_payout", label: "Release payout", kind: "confirm", primary: true }] : [];
}

const STATUS_FIELDS = {
  "creator.status": { entity: "creator", field: "status", means: "Whether we can sell this creator today", values: CREATOR_STATUS },
  "brief.status": { entity: "brief", field: "status", means: "An inbound brand request, from arrival to outcome", values: BRIEF_STATUS },
  "contract.status": { entity: "contract", field: "status", means: "Where the paper on a deal stands", values: CONTRACT_STATUS },
  "deliverable.status": { entity: "deliverable", field: "status", means: "One post or asset on one deal", values: DELIVERABLE_STATUS },
  "invoice.status": { entity: "invoice", field: "status", means: "What the brand owes the agency", values: INVOICE_STATUS },
  "payout.status": { entity: "payout", field: "status", means: "What the agency owes the creator after commission", values: PAYOUT_STATUS },
};

/* Coverage — values with no fixture yet, so nobody assumes they are unused:
   deliverable.status `approved` and contract.status `void`. Both are reachable
   only by taking the action that leads to them (approving something the brand
   has back, voiding an unsigned deal), which the kit does. `contract.status:
   drafted` has no fixture either and is where every booked deal starts, so
   booking one from a pitched brief lands on it.
   Every other value in every field appears at least once in fixtures.js. */

Object.assign(window, {
  AGENCY_POLICY,
  CREATOR_STATUS, BRIEF_STATUS, CONTRACT_STATUS, DELIVERABLE_STATUS, INVOICE_STATUS, PAYOUT_STATUS,
  CATEGORIES, PLATFORMS, DELIVERABLE_KINDS, USAGE_SCOPES, PASS_REASONS, PASSWORD_RULES, passwordMeets,
  CREATOR_FLAGS, BRIEF_FLAGS, CONTRACT_FLAGS, DELIVERABLE_FLAGS, INVOICE_FLAGS, PAYOUT_FLAGS, RIGHTS_FLAGS,
  briefTransitions, contractTransitions, dealTransitions, deliverableTransitions, invoiceTransitions, payoutTransitions,
  STATUS_FIELDS,
});

</script>
<script type="text/babel" data-presets="react" data-file="nav.js">
/* Marque Talent Desk — the route table.
   Deliberately NOT routes: the ⌘K palette, the notifications panel, the row
   menus, the confirm dialogs, the pass-on-brief and mark-published dialogs, the
   briefs pipeline tabs, the deliverables week ladder. 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 runs the TOP-NAV shell. Brands
   deliberately did NOT become a sixth: a brand is reached through the work that
   names it — a brief, a deal, an invoice — and six items in the bar would have
   spent the last slot the shell has on a screen nobody opens cold.

   There is no deals list either: a deal is always reached through the work that
   produced it. It sits under Briefs in the tree because that is where it came
   from, so a deal screen keeps Briefs marked active and its breadcrumb resolves
   without inventing a namespace. Brands sit there for the same reason. */

const MARQUE_NAV = [
  { path: "/", label: "Roster" },
  { path: "/briefs", label: "Briefs", badge: "briefsToTriage" },  { path: "/deliverables", label: "Deliverables", badge: "deliverablesNeedingAttention" },
  { path: "/performance", label: "Performance" },
  { path: "/money", label: "Money", badge: "invoicesOverdue" },

  /* Registered, not in the nav. The two creates are routes rather than dialogs
     because each is a real form (AGENTS §5), and each hangs off the list whose
     records it makes. */
  { path: "/talent/new", label: "New creator", parent: "/", offSidebar: true },
  { path: "/talent/:id", label: "Creator", parent: "/", offSidebar: true },
  { path: "/talent/:id/statement", label: "Statement", parent: "/talent/:id", offSidebar: true },
  { path: "/briefs/new", label: "New brief", parent: "/briefs", offSidebar: true },
  { path: "/briefs/:id", label: "Brief", parent: "/briefs", offSidebar: true },
  { path: "/brands/:id", label: "Brand", parent: "/briefs", offSidebar: true },
  { path: "/deals/:id", label: "Deal", parent: "/briefs", offSidebar: true },
  { path: "/deliverables/:id", label: "Deliverable", parent: "/deliverables", offSidebar: true },
  { path: "/performance/:id", label: "Wrap report", parent: "/performance", offSidebar: true },
  { path: "/inbox", label: "Inbox", offSidebar: true },
  { path: "/search", label: "Search results", offSidebar: true },
  { path: "/account/profile", label: "Manager profile", offSidebar: true },
  { path: "/account/agency", label: "Agency and commission", offSidebar: true },
  { path: "/account/team", label: "Team and ownership", offSidebar: true },

  /* Auth namespace — excluded from the shell nav; no product chrome. Every one
     of these is linked from another auth screen: nothing here is a dead end and
     nothing is registered that the flow cannot reach (AGENTS §7.2). */
  { path: "/signin", label: "Sign in", offSidebar: true },
  { path: "/signout", label: "Sign out", offSidebar: true },
  { path: "/forgot-password", label: "Forgot password", offSidebar: true },
  { path: "/forgot-password/sent", label: "Reset link sent", offSidebar: true },
  { path: "/reset-password", label: "Set a new password", offSidebar: true },
  { path: "/reset-password/done", label: "Password set", offSidebar: true },
  { path: "/reset-password/expired", label: "Link expired", offSidebar: true },
  { path: "/account-locked", label: "Account locked", offSidebar: true },
];

const MARQUE_ROUTES = indexRoutes(MARQUE_NAV);

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

const MARQUE_ACCOUNT_MENU = [
  MARQUE_ROUTES["/account/profile"],
  MARQUE_ROUTES["/account/agency"],
  MARQUE_ROUTES["/account/team"],
  MARQUE_ROUTES["/signout"],
];

/* Paths that render without product chrome (AGENTS §7.1). */
const MARQUE_AUTH_PATHS = [
  "/signin", "/signout", "/forgot-password", "/forgot-password/sent",
  "/reset-password", "/reset-password/done", "/reset-password/expired", "/account-locked",
];

/* 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 MARQUE_PAGES = (data) => {
  const creator = data.creators[0], brief = data.briefs[0], deal = data.deals[0];
  /* The brand with the most work behind it, so the link lands on a full page
     rather than on the emptiest one in the fixture. */
  const brand = data.brands.slice().sort((a, b) =>
    (dealsForBrand(data.deals, b).length - dealsForBrand(data.deals, a).length) ||
    (briefsForBrand(data.briefs, b).length - briefsForBrand(data.briefs, a).length))[0];
  /* The deliverable that needs somebody, and a campaign that has finished — the
     two records those screens are worth opening on. */
  const rows = allDeliverables(data.deals, data.creators);
  const item = (deliverablesNeedingAttention(rows, data.now)[0] || rows[0]);
  const measured = completeDeals(data.deals)[0] || deal;
  return templatePages(MARQUE_NAV, MARQUE_AUTH_PATHS, {
    "/talent/:id": creator ? "/talent/" + creator.id : null,
    "/talent/:id/statement": creator ? "/talent/" + creator.id + "/statement" : null,
    "/briefs/:id": brief ? "/briefs/" + brief.id : null,
    "/brands/:id": brand ? "/brands/" + brand.id : null,
    "/deals/:id": deal ? "/deals/" + deal.id : null,
    "/deliverables/:id": item ? "/deliverables/" + item.item.id : null,
    "/performance/:id": measured ? "/performance/" + measured.id : null,
  });
};

Object.assign(window, { MARQUE_NAV, MARQUE_ROUTES, MARQUE_DESTINATIONS, MARQUE_ACCOUNT_MENU, MARQUE_AUTH_PATHS, MARQUE_PAGES });

</script>
<script type="text/babel" data-presets="react" data-file="fixtures.js">
/* Marque Talent Desk — the only data in the product. One record, one fixture:
   every screen reads these arrays, and no screen writes a second literal to
   match. Money that two records describe is written once and derived at read
   time — an invoice's amount and a payout's amount are both the deal's fee run
   through metrics.js, never copied here.

   The awkward states are deliberate and each appears exactly once:
     · Marisol Duarte is exclusive to Lumen Beauty in beauty until 31 Jan 2027,
       and Sable & Salt's brief is beauty — the conflict that gets agencies sued.
     · Ferro's "One-pan Sunday" came back with revisions and the revised cut is
       a day over.
     · Tessellate Home is 22 days late on $31,000, which is why Juno Park has
       not been paid.
     · Cindr's brief fits nobody: wrong category, and the budget is below what
       the cheapest creator on the books quotes for the work. */

const MARQUE_NOW = Date.UTC(2026, 7, 21, 10, 20); /* Friday 21 August 2026 */
const at = (y, m, d, h, min) => Date.UTC(y, m - 1, d, h === undefined ? 9 : h, min || 0);

const MARQUE_AGENCY = { name: "Marque", desk: "Talent desk", book: "London · New York", billingEmail: "billing@marque.example" };
const MARQUE_USER = { name: "Priya Raghunathan", role: "Talent manager", email: "priya@marque.example", avatar: window.__resources.assets_img_avatar_40_01_jpg };
const MARQUE_FINANCE = { name: "Otis Delacroix", role: "Business affairs" };

/* ── The desk ────────────────────────────────────────────────────────────
   Who works here and who owns which creator. Ownership is a fact about a
   creator, so `managerId` sits on the creator above and this array holds only
   the people. MARQUE_USER and MARQUE_FINANCE are the same two people read once,
   never retyped. */
const MANAGERS = [
  { id: "priya", name: MARQUE_USER.name, role: MARQUE_USER.role, email: MARQUE_USER.email, avatar: MARQUE_USER.avatar, desk: "London", joinedAt: at(2021, 3, 1) },
  { id: "noa", name: "Noa Ferreira", role: "Talent manager", email: "noa@marque.example", avatar: window.__resources.assets_img_avatar_jpg, desk: "New York", joinedAt: at(2024, 2, 5) },
  { id: "otis", name: MARQUE_FINANCE.name, role: MARQUE_FINANCE.role, email: "otis@marque.example", avatar: window.__resources.assets_img_avatar_40_04_jpg, desk: "London", joinedAt: at(2020, 9, 14) },
];

/* ── Brands ──────────────────────────────────────────────────────────────
   A brand's NAME is written here and nowhere else: briefs, deals, bookings and
   exclusivities carry `brandId` and the name is derived at the bottom of this
   file, so renaming a brand moves every screen at once.
   Nothing about how they behave is stored — when we first heard from them, what
   they have spent, how long they take to pay and what we are exclusive to them
   for are all read off the briefs, deals and invoices that name them.
   `lumen`, `quill` and `verdant` hold no brief on this desk: they are the
   counterparties on an exclusivity or an outside booking, and a brand we owe
   something to is still a brand we have to be able to open. */
const BRANDS = [
  { id: "cindr", name: "Cindr", sector: "dating", hq: "Berlin", note: "Cold inbound. Nobody on the book works in dating and the budget is under our floor." },
  { id: "sable", name: "Sable & Salt", sector: "beauty", hq: "Copenhagen", note: "The biggest brief on the desk. Whitelisting is the whole negotiation." },
  { id: "halvard", name: "Halvard Athletic", sector: "fitness", hq: "Oslo", note: "Slow decision-makers, generous on usage. Pitched and waiting." },
  { id: "nook", name: "Nook Living", sector: "home", hq: "London", note: "Their legal takes a fortnight and always has — price the delay into the dates." },
  { id: "ferro", name: "Ferro Kitchenware", sector: "food", hq: "Milan", note: "Hands-on brand lead who gives notes in his own words. Worth the revisions." },
  { id: "vanta", name: "Vanta Motors", sector: "tech", hq: "Stockholm", note: "Full buyouts, big money, and we had to turn the last one down on a conflict." },
  { id: "pell", name: "Pell & Co", sector: "travel", hq: "Edinburgh", note: "Took the last one in-house. Worth one more brief, not two." },
  { id: "tessellate", name: "Tessellate Home", sector: "home", hq: "Lisbon", note: "Founder-led, pays late, and underdelivered against the reach we promised." },
  { id: "corriente", name: "Corriente Swim", sector: "fashion", hq: "Barcelona", note: "Best-run campaign of the year. Their paid social rights are nearly up." },
  { id: "ardent", name: "Ardent Sport", sector: "fitness", hq: "Manchester", note: "Small budgets, fast paper, pays on time. The easiest brand on the book." },
  { id: "verdant", name: "Verdant Nutrition", sector: "fitness", hq: "Los Angeles", note: "Booked Sunny direct before this desk existed, so the campaign has no deal here." },
  { id: "quill", name: "Quill Audio", sector: "tech", hq: "Berlin", note: "Holds Theo in technology to 15 December, which is why Vanta was declined." },
  { id: "lumen", name: "Lumen Beauty", sector: "beauty", hq: "Mexico City", note: "Holds Marisol in beauty to 31 January 2027. Read it before pitching her." },
];

/* ── The roster ──────────────────────────────────────────────────────────
   reach and engagement are NOT stored: both are computed from `platforms` in
   metrics.js, so a follower count changed here moves every figure that
   mentions it. `rates` is the rate card, in cents, per deliverable kind — a
   kind the creator has no rate for is work they do not take. */
const CREATORS = [
  {
    id: "wren", name: "Wren Adeyemi", managerId: "priya", handle: "wrenmakes", based: "Margate",
    portrait: window.__resources.assets_img_avatar_80_06_jpg,
    categories: ["home", "food"], status: "available",
    signedAt: at(2023, 4, 11), note: "Slow-build interiors with a joinery bent. Brands buy the hands, not the face.",
    platforms: [
      { platform: "instagram", followers: 412000, engagementRate: 0.038 },
      { platform: "tiktok", followers: 188000, engagementRate: 0.051 },
    ],
    rates: { ig_reel: 950000, ig_story: 180000, tiktok: 650000, ugc_pack: 400000 },
    exclusivities: [],
  },
  {
    id: "sunny", name: "Sunny Falk", managerId: "noa", handle: "sunnyfalk", based: "Los Angeles",
    portrait: window.__resources.assets_img_avatar_80_01_jpg,
    categories: ["fitness"], status: "booked",
    signedAt: at(2021, 9, 2), note: "Endurance, not aesthetics. The only creator on the book with a real YouTube audience.",
    /* An external booking: the campaign predates this desk, so there is no deal
       record for it and the brand is the booking's own fact. */
    booking: { brandId: "verdant", until: at(2026, 10, 3), dealId: null },
    bookedUntil: at(2026, 10, 3),
    platforms: [
      { platform: "instagram", followers: 890000, engagementRate: 0.024 },
      { platform: "tiktok", followers: 1200000, engagementRate: 0.062 },
      { platform: "youtube", followers: 340000, engagementRate: 0.044 },
    ],
    rates: { ig_reel: 2800000, ig_story: 550000, tiktok: 2400000, yt_integration: 4600000, ugc_pack: 1200000 },
    exclusivities: [],
  },
  {
    id: "marisol", name: "Marisol Duarte", managerId: "priya", handle: "marisolduarte", based: "Mexico City",
    portrait: window.__resources.assets_img_avatar_80_05_jpg,
    categories: ["beauty", "fashion"], status: "available",
    signedAt: at(2022, 2, 28), note: "The biggest audience on the book and the tightest contract. Read the exclusivity before you pitch her.",
    platforms: [
      { platform: "instagram", followers: 1400000, engagementRate: 0.019 },
      { platform: "tiktok", followers: 2100000, engagementRate: 0.074 },
    ],
    rates: { ig_reel: 3800000, ig_story: 750000, tiktok: 3400000, ugc_pack: 1600000 },
    /* The one that matters. Category-scoped, brand-scoped, dated. */
    exclusivities: [{ brandId: "lumen", category: "beauty", until: at(2027, 1, 31), signedAt: at(2026, 2, 1) }],
  },
  {
    id: "theo", name: "Theo Brandt-Lowe", managerId: "noa", handle: "brandtlowe", based: "Berlin",
    portrait: window.__resources.assets_img_avatar_40_02_jpg,
    categories: ["tech"], status: "booked",
    signedAt: at(2024, 1, 15), note: "Long-form teardowns. Comes off the Quill campaign at the end of the month.",
    booking: { brandId: "quill", until: at(2026, 8, 29), dealId: null },
    bookedUntil: at(2026, 8, 29),
    platforms: [
      { platform: "youtube", followers: 720000, engagementRate: 0.056 },
      { platform: "tiktok", followers: 310000, engagementRate: 0.048 },
    ],
    rates: { yt_integration: 4200000, tiktok: 1100000, ugc_pack: 800000 },
    exclusivities: [{ brandId: "quill", category: "tech", until: at(2026, 12, 15), signedAt: at(2026, 5, 30) }],
  },
  {
    id: "imani", name: "Imani Castellanos", managerId: "noa", handle: "imanicast", based: "New York",
    portrait: window.__resources.assets_img_avatar_40_03_jpg,
    categories: ["fashion", "beauty"], status: "available",
    signedAt: at(2022, 11, 7), note: "Styling-led, shoots her own stills. Corriente ran 26% over on reach.",
    platforms: [
      { platform: "instagram", followers: 640000, engagementRate: 0.031 },
      { platform: "tiktok", followers: 480000, engagementRate: 0.059 },
    ],
    rates: { ig_reel: 1600000, ig_story: 320000, tiktok: 1300000, ugc_pack: 650000 },
    exclusivities: [],
  },
  {
    id: "rafa", name: "Rafa Nkemelu", managerId: "noa", handle: "rafacooks", based: "Lagos",
    portrait: window.__resources.assets_img_avatar_80_03_jpg,
    categories: ["food"], status: "booked",
    signedAt: at(2023, 8, 19), note: "Highest engagement on the book by a distance. One-take cooking, no cuts.",
    booking: { brandId: null, until: at(2026, 9, 12), dealId: "DEAL-26-019" },
    bookedUntil: at(2026, 9, 12),
    platforms: [
      { platform: "tiktok", followers: 980000, engagementRate: 0.081 },
      { platform: "instagram", followers: 220000, engagementRate: 0.042 },
    ],
    rates: { tiktok: 1500000, ig_reel: 900000, ig_story: 200000, ugc_pack: 500000 },
    exclusivities: [],
  },
  {
    id: "juno", name: "Juno Park", managerId: "priya", handle: "junoparkstudio", based: "Seoul",
    portrait: window.__resources.assets_img_avatar_80_02_jpg,
    categories: ["home"], status: "available",
    signedAt: at(2024, 6, 3), note: "Small, expensive, architectural. Tessellate underran on reach and we owe her an answer.",
    platforms: [
      { platform: "instagram", followers: 310000, engagementRate: 0.046 },
      { platform: "youtube", followers: 96000, engagementRate: 0.06 },
    ],
    rates: { ig_reel: 750000, ig_story: 150000, yt_integration: 1400000, ugc_pack: 350000 },
    exclusivities: [],
  },
  {
    id: "della", name: "Della Voss", managerId: "priya", handle: "dellavoss", based: "Lisbon",
    portrait: window.__resources.assets_img_avatar_80_04_jpg,
    categories: ["travel"], status: "paused",
    signedAt: at(2020, 5, 26), note: "On sabbatical to 2 November at her own request. Do not pitch, do not hold.",
    pausedUntil: at(2026, 11, 2),
    platforms: [
      { platform: "instagram", followers: 1100000, engagementRate: 0.022 },
      { platform: "youtube", followers: 210000, engagementRate: 0.039 },
    ],
    rates: { ig_reel: 2600000, ig_story: 500000, yt_integration: 3000000, ugc_pack: 1000000 },
    exclusivities: [],
  },
  {
    id: "kit", name: "Kit Oyelaran", managerId: "priya", handle: "kitoyelaran", based: "Manchester",
    portrait: window.__resources.assets_img_avatar_40_05_jpg,
    categories: ["fitness", "fashion"], status: "available",
    signedAt: at(2025, 10, 14), note: "Newest signing and the cheapest rate on the book. Ardent came in 10% over promise.",
    platforms: [
      { platform: "tiktok", followers: 540000, engagementRate: 0.068 },
      { platform: "instagram", followers: 175000, engagementRate: 0.049 },
    ],
    rates: { tiktok: 800000, ig_reel: 550000, ig_story: 120000, ugc_pack: 300000 },
    exclusivities: [],
  },
];

/* ── Inbound briefs ──────────────────────────────────────────────────────
   `deliverables` is what the brand asked for; the quote a creator gives for it
   is computed in metrics.js from their rate card and the usage uplift, never
   stored. `budgetCents` is the whole campaign budget for one creator. */
const BRIEFS = [
  {
    id: "BR-26-047", brandId: "cindr", title: "Real talk, real matches",
    category: "dating", budgetCents: 900000,
    deliverables: [{ kind: "tiktok", quantity: 4 }],
    usage: "organic", audienceMin: 1000000,
    liveFromAt: at(2026, 9, 22), liveToAt: at(2026, 10, 20),
    receivedAt: at(2026, 8, 20, 16, 40), respondByAt: at(2026, 8, 26),
    contact: { name: "Dev Shanmugam", role: "Brand partnerships" },
    ask: "Four unscripted TikToks from a creator with a million-plus audience, talking honestly about dating in their twenties.",
    status: "to_triage",
  },
  {
    id: "BR-26-038", brandId: "sable", title: "Winter barrier repair",
    category: "beauty", budgetCents: 22500000,
    deliverables: [{ kind: "ig_reel", quantity: 2 }, { kind: "ig_story", quantity: 3 }, { kind: "tiktok", quantity: 1 }],
    usage: "whitelisting_6m", audienceMin: 750000,
    liveFromAt: at(2026, 10, 6), liveToAt: at(2026, 11, 14),
    receivedAt: at(2026, 8, 17, 11, 2), respondByAt: at(2026, 8, 25),
    contact: { name: "Ilse Brandão", role: "Head of brand" },
    ask: "A routine-led launch for the winter barrier cream, whitelisted to their paid social for six months.",
    status: "to_triage",
  },
  {
    id: "BR-26-041", brandId: "halvard", title: "Winter run series",
    category: "fitness", budgetCents: 11000000,
    deliverables: [{ kind: "yt_integration", quantity: 1 }, { kind: "tiktok", quantity: 2 }],
    usage: "organic", audienceMin: 800000,
    liveFromAt: at(2026, 10, 20), liveToAt: at(2026, 12, 1),
    receivedAt: at(2026, 8, 11, 9, 30), respondByAt: at(2026, 9, 1),
    contact: { name: "Marek Toft", role: "Marketing director" },
    ask: "One long-form YouTube integration across a six-week winter training block, cut down into two TikToks.",
    status: "pitching", pitchedAt: at(2026, 8, 14, 15, 10), pitched: ["sunny", "kit"],
  },
  {
    id: "BR-26-045", brandId: "nook", title: "First flat",
    category: "home", budgetCents: 5200000,
    deliverables: [{ kind: "ig_reel", quantity: 3 }, { kind: "ig_story", quantity: 6 }],
    usage: "organic", audienceMin: 300000,
    liveFromAt: at(2026, 9, 8), liveToAt: at(2026, 10, 5),
    receivedAt: at(2026, 7, 27, 10, 0), respondByAt: at(2026, 8, 8),
    contact: { name: "Bea Lindqvist", role: "Head of growth" },
    ask: "Furnishing a first rented flat on a budget, over four weeks, without it looking like a haul.",
    status: "won", pitchedAt: at(2026, 7, 29), pitched: ["wren", "juno"], dealId: "DEAL-26-021",
  },
  {
    id: "BR-26-044", brandId: "ferro", title: "Slow Sunday",
    category: "food", budgetCents: 9500000,
    deliverables: [{ kind: "tiktok", quantity: 4 }, { kind: "ig_reel", quantity: 1 }],
    usage: "paid_social_3m", audienceMin: 600000,
    liveFromAt: at(2026, 8, 10), liveToAt: at(2026, 9, 20),
    receivedAt: at(2026, 7, 14, 14, 20), respondByAt: at(2026, 7, 24),
    contact: { name: "Hal Winterbourne", role: "Brand lead" },
    ask: "Five pieces built around one cast-iron pan, shot the way he already cooks.",
    status: "won", pitchedAt: at(2026, 7, 16), pitched: ["rafa", "wren"], dealId: "DEAL-26-019",
  },
  {
    id: "BR-26-036", brandId: "vanta", title: "City EV, one week",
    category: "tech", budgetCents: 15000000,
    deliverables: [{ kind: "yt_integration", quantity: 1 }, { kind: "ugc_pack", quantity: 1 }],
    usage: "buyout_12m", audienceMin: 500000,
    liveFromAt: at(2026, 9, 1), liveToAt: at(2026, 9, 30),
    receivedAt: at(2026, 6, 30, 8, 45), respondByAt: at(2026, 7, 10),
    contact: { name: "Sunniva Ek", role: "Global partnerships" },
    ask: "A week of city driving on the new compact, bought out in full for twelve months.",
    status: "passed", passReason: "brand_conflict", passedAt: at(2026, 7, 3),
    passNote: "Theo is the only creator on the book in technology and he is exclusive to Quill Audio in the category until 15 December. Declined rather than risk it.",
  },
  {
    id: "BR-26-030", brandId: "pell", title: "Slow roads",
    category: "travel", budgetCents: 12000000,
    deliverables: [{ kind: "yt_integration", quantity: 1 }, { kind: "ig_reel", quantity: 2 }],
    usage: "paid_social_3m", audienceMin: 900000,
    liveFromAt: at(2026, 8, 1), liveToAt: at(2026, 9, 15),
    receivedAt: at(2026, 6, 9, 12, 0), respondByAt: at(2026, 6, 26),
    contact: { name: "Rowan Teague", role: "Creative director" },
    ask: "A two-week drive through northern Portugal, no itinerary, no voiceover.",
    status: "lost", pitchedAt: at(2026, 6, 12), pitched: ["della"], lostAt: at(2026, 7, 22),
    lostNote: "Brand took it in-house after Della asked to move the shoot dates.",
  },
  {
    id: "BR-26-028", brandId: "tessellate", title: "Tile stories",
    category: "home", budgetCents: 3400000,
    deliverables: [{ kind: "ig_reel", quantity: 2 }, { kind: "yt_integration", quantity: 1 }],
    usage: "organic", audienceMin: 250000,
    liveFromAt: at(2026, 6, 10), liveToAt: at(2026, 7, 10),
    receivedAt: at(2026, 5, 6, 9, 15), respondByAt: at(2026, 5, 20),
    contact: { name: "Gil Marchetti", role: "Founder" },
    ask: "Three pieces on the new terracotta range, shot in a real kitchen rather than a set.",
    status: "won", pitchedAt: at(2026, 5, 8), pitched: ["juno", "wren"], dealId: "DEAL-26-012",
  },
  {
    id: "BR-26-022", brandId: "corriente", title: "Salt season",
    category: "fashion", budgetCents: 6200000,
    deliverables: [{ kind: "ig_reel", quantity: 2 }, { kind: "tiktok", quantity: 1 }],
    usage: "paid_social_3m", audienceMin: 700000,
    liveFromAt: at(2026, 6, 1), liveToAt: at(2026, 7, 1),
    receivedAt: at(2026, 4, 28, 11, 30), respondByAt: at(2026, 5, 12),
    contact: { name: "Noor Haddad", role: "Head of marketing" },
    ask: "The summer capsule, worn in, on real coastline rather than a studio.",
    status: "won", pitchedAt: at(2026, 5, 1), pitched: ["imani", "marisol"], dealId: "DEAL-26-009",
  },
  {
    id: "BR-26-014", brandId: "ardent", title: "Couch to five",
    category: "fitness", budgetCents: 1800000,
    deliverables: [{ kind: "tiktok", quantity: 2 }],
    usage: "organic", audienceMin: 400000,
    liveFromAt: at(2026, 7, 1), liveToAt: at(2026, 7, 31),
    receivedAt: at(2026, 5, 20, 13, 0), respondByAt: at(2026, 6, 5),
    contact: { name: "Fenn Okoro", role: "Brand manager" },
    ask: "Two TikToks for the beginners' programme, from someone who was a beginner last year.",
    status: "won", pitchedAt: at(2026, 5, 22), pitched: ["kit"], dealId: "DEAL-26-004",
  },
];

/* ── Deals ───────────────────────────────────────────────────────────────
   `feeCents` is what the brand pays and it is written once. The commission,
   the creator's net, the invoice amount and the payout amount are all derived
   from it in metrics.js — none of them appears as a literal below.
   `promise` is what we told the brand in the pitch; performance is measured
   against it. */
const DEALS = [
  {
    id: "DEAL-26-004", briefId: "BR-26-014", creatorId: "kit", brandId: "ardent", title: "Couch to five",
    agreedAt: at(2026, 6, 8), feeCents: 1500000, commissionRate: 0.2,
    contract: { status: "signed", draftedAt: at(2026, 6, 9), sentAt: at(2026, 6, 10), signedAt: at(2026, 6, 15) },
    usage: "organic", usageStartAt: at(2026, 7, 6),
    promise: { reach: 700000, engagementRate: 0.05 },
    deliverables: [
      { id: "DLV-04-1", kind: "tiktok", quantity: 1, title: "Week one, honestly", dueAt: at(2026, 7, 6), status: "published", submittedAt: at(2026, 7, 2), publishedAt: at(2026, 7, 6, 18, 0), url: "tiktok.com/@kitoyelaran/7401", metrics: { reach: 412000, engagements: 31900, saves: 4100 } },
      { id: "DLV-04-2", kind: "tiktok", quantity: 1, title: "The five, and what it cost", dueAt: at(2026, 7, 15), status: "published", submittedAt: at(2026, 7, 11), publishedAt: at(2026, 7, 15, 19, 30), url: "tiktok.com/@kitoyelaran/7412", metrics: { reach: 355000, engagements: 24850, saves: 3050 } },
    ],
    invoice: { id: "INV-26-0164", status: "paid", issuedAt: at(2026, 7, 20), paidAt: at(2026, 8, 17, 14, 5) },
    payout: { id: "PAY-26-0091", status: "due", reference: null, paidAt: null },
  },
  {
    id: "DEAL-26-009", briefId: "BR-26-022", creatorId: "imani", brandId: "corriente", title: "Salt season",
    agreedAt: at(2026, 5, 15), feeCents: 5950000, commissionRate: 0.2,
    contract: { status: "signed", draftedAt: at(2026, 5, 16), sentAt: at(2026, 5, 18), signedAt: at(2026, 5, 20) },
    usage: "paid_social_3m", usageStartAt: at(2026, 6, 2),
    promise: { reach: 900000, engagementRate: 0.042 },
    deliverables: [
      { id: "DLV-09-1", kind: "ig_reel", quantity: 1, title: "Capsule, first wear", dueAt: at(2026, 6, 2), status: "published", submittedAt: at(2026, 5, 29), publishedAt: at(2026, 6, 2, 17, 0), url: "instagram.com/p/salt-1", metrics: { reach: 388000, engagements: 15900, saves: 6200 } },
      { id: "DLV-09-2", kind: "ig_reel", quantity: 1, title: "Three ways off the beach", dueAt: at(2026, 6, 12), status: "published", submittedAt: at(2026, 6, 9), publishedAt: at(2026, 6, 12, 16, 30), url: "instagram.com/p/salt-2", metrics: { reach: 341000, engagements: 12400, saves: 4900 } },
      { id: "DLV-09-3", kind: "tiktok", quantity: 1, title: "What actually survives salt", dueAt: at(2026, 6, 24), status: "published", submittedAt: at(2026, 6, 20), publishedAt: at(2026, 6, 24, 20, 0), url: "tiktok.com/@imanicast/7288", metrics: { reach: 402000, engagements: 26800, saves: 5400 } },
    ],
    invoice: { id: "INV-26-0138", status: "paid", issuedAt: at(2026, 6, 26), paidAt: at(2026, 7, 21, 10, 40) },
    payout: { id: "PAY-26-0071", status: "paid", reference: "SEPA-0071", paidAt: at(2026, 8, 3, 11, 0) },
  },
  {
    id: "DEAL-26-012", briefId: "BR-26-028", creatorId: "juno", brandId: "tessellate", title: "Tile stories",
    agreedAt: at(2026, 5, 29), feeCents: 3100000, commissionRate: 0.2,
    contract: { status: "signed", draftedAt: at(2026, 5, 30), sentAt: at(2026, 6, 1), signedAt: at(2026, 6, 2) },
    usage: "organic", usageStartAt: at(2026, 6, 12),
    promise: { reach: 380000, engagementRate: 0.048 },
    deliverables: [
      { id: "DLV-12-1", kind: "ig_reel", quantity: 1, title: "Terracotta, in a real kitchen", dueAt: at(2026, 6, 12), status: "published", submittedAt: at(2026, 6, 9), publishedAt: at(2026, 6, 12, 12, 0), url: "instagram.com/p/tile-1", metrics: { reach: 118000, engagements: 6100, saves: 2900 } },
      { id: "DLV-12-2", kind: "ig_reel", quantity: 1, title: "Grout is a colour decision", dueAt: at(2026, 6, 20), status: "published", submittedAt: at(2026, 6, 17), publishedAt: at(2026, 6, 20, 11, 0), url: "instagram.com/p/tile-2", metrics: { reach: 96000, engagements: 4400, saves: 2100 } },
      { id: "DLV-12-3", kind: "yt_integration", quantity: 1, title: "Renovating one wall", dueAt: at(2026, 6, 28), status: "published", submittedAt: at(2026, 6, 25), publishedAt: at(2026, 6, 28, 15, 0), url: "youtube.com/watch?v=tile3", metrics: { reach: 74000, engagements: 5200, saves: 1180 } },
    ],
    /* Issued the day after the last post; net-30 fell on 30 July and nothing
       has landed. This is why Juno has not been paid. */
    invoice: { id: "INV-26-0158", status: "issued", issuedAt: at(2026, 6, 30), paidAt: null, chasedAt: at(2026, 8, 12, 9, 0) },
    payout: { id: "PAY-26-0079", status: "awaiting_funds", reference: null, paidAt: null },
  },
  {
    id: "DEAL-26-019", briefId: "BR-26-044", creatorId: "rafa", brandId: "ferro", title: "Slow Sunday",
    agreedAt: at(2026, 8, 3), feeCents: 8800000, commissionRate: 0.18,
    contract: { status: "signed", draftedAt: at(2026, 8, 3), sentAt: at(2026, 8, 3, 16, 0), signedAt: at(2026, 8, 4, 10, 15) },
    usage: "paid_social_3m", usageStartAt: at(2026, 8, 13),
    promise: { reach: 1400000, engagementRate: 0.06 },
    deliverables: [
      { id: "DLV-19-1", kind: "tiktok", quantity: 1, title: "Cast-iron sear", dueAt: at(2026, 8, 12), status: "published", submittedAt: at(2026, 8, 9), publishedAt: at(2026, 8, 13, 18, 0), url: "tiktok.com/@rafacooks/7455", metrics: { reach: 486000, engagements: 39400, saves: 11200 } },
      /* The revision the brand asked for, and it is a day over. */
      { id: "DLV-19-2", kind: "tiktok", quantity: 1, title: "One-pan Sunday", dueAt: at(2026, 8, 20), status: "revisions_requested", submittedAt: at(2026, 8, 17, 14, 0), revisionAt: at(2026, 8, 18, 9, 20), revisionNote: "The apron shot crops the wordmark. Reshoot that beat or cut it — everything else is approved.", revisionBy: "Hal Winterbourne" },
      { id: "DLV-19-3", kind: "ig_reel", quantity: 1, title: "Ferro care ritual", dueAt: at(2026, 8, 26), status: "submitted", submittedAt: at(2026, 8, 20, 16, 45) },
      { id: "DLV-19-4", kind: "tiktok", quantity: 1, title: "Sunday leftovers", dueAt: at(2026, 8, 25), status: "scheduled" },
      { id: "DLV-19-5", kind: "tiktok", quantity: 1, title: "Braise night", dueAt: at(2026, 9, 2), status: "scheduled" },
    ],
    invoice: { id: "INV-26-0171", status: "issued", issuedAt: at(2026, 8, 14), paidAt: null },
    payout: { id: "PAY-26-0084", status: "awaiting_funds", reference: null, paidAt: null },
  },
  {
    id: "DEAL-26-021", briefId: "BR-26-045", creatorId: "wren", brandId: "nook", title: "First flat",
    agreedAt: at(2026, 8, 10), feeCents: 4150000, commissionRate: 0.2,
    /* Eight days out and nothing can start until it comes back. */
    contract: { status: "out_for_signature", draftedAt: at(2026, 8, 10), sentAt: at(2026, 8, 13, 11, 0), signedAt: null },
    usage: "organic", usageStartAt: at(2026, 9, 8),
    promise: { reach: 520000, engagementRate: 0.04 },
    deliverables: [
      { id: "DLV-21-1", kind: "ig_reel", quantity: 1, title: "Empty room tour", dueAt: at(2026, 9, 8), status: "scheduled" },
      { id: "DLV-21-2", kind: "ig_story", quantity: 6, title: "Move-in day frames", dueAt: at(2026, 9, 9), status: "scheduled" },
      { id: "DLV-21-3", kind: "ig_reel", quantity: 1, title: "One shelf, three ways", dueAt: at(2026, 9, 18), status: "scheduled" },
      { id: "DLV-21-4", kind: "ig_reel", quantity: 1, title: "The finished flat", dueAt: at(2026, 9, 29), status: "scheduled" },
    ],
    invoice: { id: "INV-26-0175", status: "draft", issuedAt: null, paidAt: null, holdReason: "Held until the contract is countersigned." },
    payout: { id: "PAY-26-0088", status: "awaiting_funds", reference: null, paidAt: null },
  },
];

/* Authored against the entities above: every one of these agrees with the
   record it names, at the timestamp it carries. */
const NOTIFICATIONS = [
  { id: "n1", at: at(2026, 8, 21, 8, 5), title: "Tessellate Home is past due", body: "INV-26-0158 was due on 30 July. Juno Park's payout is held until it lands.", route: "/money" },
  { id: "n2", at: at(2026, 8, 20, 9, 15), title: "Nook Living haven't countersigned", body: "Out for signature since 13 August. Wren's first post is due 8 September.", route: "/deals/DEAL-26-021" },
  { id: "n3", at: at(2026, 8, 18, 9, 20), title: "Ferro sent revisions on One-pan Sunday", body: "Hal Winterbourne wants the apron beat reshot or cut. The revised cut was due yesterday.", route: "/deliverables" },
  { id: "n4", at: at(2026, 8, 17, 11, 2), title: "Sable & Salt want an answer by Tuesday", body: "$225,000 in beauty, whitelisted six months. Read Marisol's exclusivity before you reply.", route: "/briefs/BR-26-038" },
  /* Both of these are true of the fixture above at this timestamp: Corriente's
     three-month paid social window opened on 2 June, and Theo's Quill booking
     ends on 29 August. */
  { id: "n5", at: at(2026, 8, 16, 7, 30), title: "Corriente's paid social rights end on 2 September", body: "Three months from 2 June. Renew the window or the ads have to come down.", route: "/performance/DEAL-26-009" },
  { id: "n6", at: at(2026, 8, 14, 8, 0), title: "Theo comes free on 29 August", body: "Off the Quill campaign in eight days, but exclusive to them in technology until 15 December.", route: "/talent/theo" },
];

/* The brand name, derived once. Every record above carries an id; this is the
   only place a name is attached to one, so no screen renders a second literal
   and a rename in BRANDS moves the roster, the ledgers and the palette
   together. A record pointing at a brand that does not exist is a bug, and it
   says so rather than printing an id at a reader. */
const brandNameOf = (id) => {
  const brand = BRANDS.find((b) => b.id === id);
  if (!brand) { console.warn("[fixtures] no brand with id", id); return String(id); }
  return brand.name;
};
BRIEFS.forEach((brief) => { brief.brand = brandNameOf(brief.brandId); });
DEALS.forEach((deal) => { deal.brand = brandNameOf(deal.brandId); });
CREATORS.forEach((creator) => {
  /* A booking with a deal on this desk takes its brand from the deal, so
     bookingBrandOf() stays the one answer; only an outside booking names one. */
  if (creator.booking && creator.booking.brandId) creator.booking.brand = brandNameOf(creator.booking.brandId);
  (creator.exclusivities || []).forEach((e) => { e.brand = brandNameOf(e.brandId); });
});

Object.assign(window, { MARQUE_NOW, MARQUE_AGENCY, MARQUE_USER, MARQUE_FINANCE, MANAGERS, BRANDS, CREATORS, BRIEFS, DEALS, NOTIFICATIONS, brandNameOf });

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

   Two chains matter.

   The quote chain, which the match screen and the deal sheet both run:
     rate card × quantity  →  organic fee
                           →  × (1 + usage uplift)  =  quote
     negotiated fee (stored once on the deal)
                           →  × commission rate     =  agency commission
                           →  − commission          =  the creator's net

   The money chain, which is never written twice in the fixture:
     deal.feeCents  →  the invoice's amount  (what the brand owes us)
                    →  the payout's amount   (what we owe the creator, net)

   Suppression: a derived figure is shown as a word, not a zero, when the
   record's state makes it meaningless. Named at each helper that does it. */

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 roster ──────────────────────────────────────────────────────── */

/* Reach and engagement are computed from the platform rows, never stored, so a
   follower count changed in the fixture moves every screen that mentions it. */
const reachOf = (creator) => sumBy(creator.platforms, "followers");
const engagementOf = (creator) => {
  const total = reachOf(creator);
  return total ? sumOf(creator.platforms, (p) => p.followers * p.engagementRate) / total : null;
};
const leadPlatformOf = (creator) => creator.platforms.slice().sort((a, b) => b.followers - a.followers)[0];

const pitchableCreators = (creators) => creators.filter(CREATOR_FLAGS.isPitchable);
const bookedCreators = (creators) => creators.filter((c) => c.status === "booked");
const comingFreeCreators = (creators, now) => creators.filter((c) => CREATOR_FLAGS.isComingFree(c, now));
const pausedCreators = (creators) => creators.filter((c) => c.status === "paused");

/* A booked creator's brand is the booking's own fact only when no deal on this
   desk owns it; where a deal does, the deal owns the name and the booking reads
   it, so the two can never disagree. */
function bookingBrandOf(creator, deals) {
  if (!creator.booking) return null;
  if (creator.booking.dealId) {
    const deal = dealById(deals, creator.booking.dealId);
    return deal ? deal.brand : null;
  }
  return creator.booking.brand;
}

/* Why a creator cannot be pitched, in words, for the roster and the match
   screen alike. Null when they can. */
function unavailableBecause(creator, deals, now) {
  if (creator.status === "paused") return "Paused at their own request until " + formatAbsolute(creator.pausedUntil).split(",")[0];
  if (creator.status === "booked") return "Booked to " + bookingBrandOf(creator, deals) + " until " + formatAbsolute(creator.bookedUntil).split(",")[0];
  return null;
}

/* ── The quote chain ─────────────────────────────────────────────────── */

const usageOf = (scope) => USAGE_SCOPES.find((u) => u.value === scope) || USAGE_SCOPES[0];
const rateFor = (creator, kind) => (creator.rates[kind] === undefined ? null : creator.rates[kind]);
/* The rate card, read as a range and as a list. Both derive from the card, so
   changing one rate moves the roster band and the quote together. */
const rateCardOf = (creator) => DELIVERABLE_KINDS.filter((k) => rateFor(creator, k.value) !== null).map((k) => ({ kind: k.value, label: k.label, cents: creator.rates[k.value] }));
const rateFloorOf = (creator) => Math.min.apply(null, rateCardOf(creator).map((r) => r.cents));
const rateCeilOf = (creator) => Math.max.apply(null, rateCardOf(creator).map((r) => r.cents));
const briefUnits = (brief) => sumBy(brief.deliverables, "quantity");

/* What this creator would charge for this brief. `complete` is false when they
   have no rate for something the brief asks for — that is work they do not
   take, and the quote is suppressed with the missing kinds named rather than
   totalled as if the line were free. */
function quoteFor(creator, brief) {
  const usage = usageOf(brief.usage);
  const missing = brief.deliverables.filter((line) => rateFor(creator, line.kind) === null).map((line) => line.kind);
  if (missing.length) return { complete: false, missing: missing, organicCents: null, upliftCents: null, totalCents: null, usage: usage };
  const organic = sumOf(brief.deliverables, (line) => rateFor(creator, line.kind) * line.quantity);
  const uplift = Math.round(organic * usage.uplift);
  return { complete: true, missing: [], organicCents: organic, upliftCents: uplift, totalCents: organic + uplift, usage: usage };
}

/* The cheapest complete quote anywhere on the book. This is the floor a brief's
   budget has to clear before it is worth anyone's time, and it is derived from
   the roster rather than typed — sign a cheaper creator and the floor moves.
   Null when nobody on the book can produce the brief at all. */
function rosterFloor(creators, brief) {
  const quotes = creators.map((c) => quoteFor(c, brief)).filter((q) => q.complete);
  if (!quotes.length) return null;
  return Math.min.apply(null, quotes.map((q) => q.totalCents));
}
function cheapestFitCreator(creators, brief) {
  const priced = creators
    .map((c) => ({ creator: c, quote: quoteFor(c, brief) }))
    .filter((row) => row.quote.complete)
    .sort((a, b) => a.quote.totalCents - b.quote.totalCents);
  return priced.length ? priced[0] : null;
}

/* ── Matching a brief to the roster ──────────────────────────────────────
   Three fit axes and two blockers. `fits` is THE act-ability predicate: the
   match screen's heading, the pitch bar's count and the row's own control all
   call this one function, so they cannot disagree. */
function exclusivityConflict(creator, brief, now) {
  return CREATOR_FLAGS.liveExclusivities(creator, now).find(
    (e) => e.category === brief.category && e.brand !== brief.brand
  ) || null;
}

function fitOf(creator, brief, now, deals) {
  const quote = quoteFor(creator, brief);
  const reach = reachOf(creator);
  /* Availability is judged against the brand's window, never against today. */
  const windowStart = BRIEF_FLAGS.windowStart(brief);
  const booked = creator.status === "booked" && creator.bookedUntil > windowStart;
  const paused = creator.status === "paused" && (creator.pausedUntil || 0) > windowStart;
  const conflict = exclusivityConflict(creator, brief, now);
  const axes = {
    audience: reach >= brief.audienceMin,
    category: creator.categories.indexOf(brief.category) > -1,
    rate: quote.complete && quote.totalCents <= brief.budgetCents,
  };
  const canTake = !booked && !paused && !conflict;
  return {
    creator: creator, quote: quote, reach: reach, axes: axes,
    booked: booked, paused: paused, conflict: conflict, canTake: canTake,
    blockedBecause: conflict
      ? "Exclusive to " + conflict.brand + " in " + labelOf(CATEGORIES, conflict.category).toLowerCase() + " until " + formatAbsolute(conflict.until).split(",")[0]
      : booked ? unavailableBecause(creator, deals, now)
      : paused ? unavailableBecause(creator, deals, now)
      : null,
    missedAxes: Object.keys(axes).filter((k) => !axes[k]),
    fits: canTake && axes.audience && axes.category && axes.rate,
  };
}

/* One pass over the roster, split three ways. Blocked comes before off-brief
   because a conflict is a hazard, not a near miss. */
function matchGroups(creators, brief, now, deals) {
  const rows = creators.map((c) => fitOf(c, brief, now, deals));
  return {
    all: rows,
    fits: rows.filter((r) => r.fits).sort((a, b) => b.reach - a.reach),
    blocked: rows.filter((r) => !r.canTake).sort((a, b) => (a.conflict ? -1 : 1)),
    offBrief: rows.filter((r) => r.canTake && !r.fits).sort((a, b) => a.missedAxes.length - b.missedAxes.length),
    conflicts: rows.filter((r) => !!r.conflict),
  };
}

/* ── Briefs ──────────────────────────────────────────────────────────── */
const briefsToTriage = (briefs) => briefs.filter(BRIEF_FLAGS.needsTriage);
const openBriefs = (briefs) => briefs.filter(BRIEF_FLAGS.isOpen);
const briefsWith = (briefs, status) => briefs.filter((b) => b.status === status);
const briefValue = (rows) => sumBy(rows, "budgetCents");

/* ── Deals ───────────────────────────────────────────────────────────── */
const commissionOf = (deal) => Math.round(deal.feeCents * deal.commissionRate);
const creatorNetOf = (deal) => deal.feeCents - commissionOf(deal);
/* The invoice and the payout do not store an amount. They read the deal. */
const invoiceAmountOf = (deal) => deal.feeCents;
const payoutAmountOf = (deal) => creatorNetOf(deal);

const dealsAwaitingSignature = (deals) => deals.filter(CONTRACT_FLAGS.awaitsCountersign);
const signedDeals = (deals) => deals.filter(CONTRACT_FLAGS.isSigned);

/* Where a deal's usage rights run out. RIGHTS_FLAGS owns the arithmetic; this
   is the name the screens have always called it by, kept so the deal sheet and
   the roster note spell the same date. */
const usageEndsAt = (deal) => RIGHTS_FLAGS.endsAt(deal);
const dealsWithRightsExpiring = (deals, now) => deals.filter((d) => CONTRACT_FLAGS.isSigned(d) && RIGHTS_FLAGS.isExpiring(d, now)).sort((a, b) => RIGHTS_FLAGS.endsAt(a) - RIGHTS_FLAGS.endsAt(b));

/* ── Deliverables ────────────────────────────────────────────────────────
   Flattened once, with the deal and the creator attached, so no screen joins
   these by hand. */
function allDeliverables(deals, creators) {
  return deals.reduce((rows, deal) => rows.concat(deal.deliverables.map((item) => ({
    item: item, deal: deal, creator: creatorById(creators, deal.creatorId),
  }))), []);
}
const deliverablesNeedingAttention = (rows, now) => rows.filter((r) => DELIVERABLE_FLAGS.needsAttention(r.item, now));
const deliverablesWithBrand = (rows) => rows.filter((r) => DELIVERABLE_FLAGS.isWithBrand(r.item));
const deliverablesLive = (rows) => rows.filter((r) => DELIVERABLE_FLAGS.isLive(r.item));
const deliverablesAhead = (rows, now) => rows.filter((r) => r.item.status === "scheduled" && !DELIVERABLE_FLAGS.isDueSoon(r.item, now) && !DELIVERABLE_FLAGS.isOverdue(r.item, now));
const byDue = (rows) => rows.slice().sort((a, b) => a.item.dueAt - b.item.dueAt);

/* Who owes the next move on a deliverable the manager cannot act on. */
function deliverableWaitingOn(row) {
  if (row.item.status === "submitted") return row.deal.brand;
  if (row.item.status === "approved") return row.creator.name.split(" ")[0] + ", to post";
  return null;
}

/* ── Performance ─────────────────────────────────────────────────────────
   Measured against `promise`, which is what we told the brand in the pitch. */
const publishedOf = (deal) => deal.deliverables.filter(DELIVERABLE_FLAGS.isLive);
const actualReach = (deal) => sumOf(publishedOf(deal), (d) => d.metrics.reach);
const actualEngagements = (deal) => sumOf(publishedOf(deal), (d) => d.metrics.engagements);
const actualSaves = (deal) => sumOf(publishedOf(deal), (d) => d.metrics.saves);
function actualEngagementRate(deal) {
  const reach = actualReach(deal);
  return reach ? actualEngagements(deal) / reach : null;
}

/* complete → every deliverable live · in_flight → some · not_started → none. */
function campaignState(deal) {
  const live = publishedOf(deal).length;
  if (!live) return "not_started";
  return live === deal.deliverables.length ? "complete" : "in_flight";
}

/* Suppressed rather than zeroed: a campaign is only measured against its
   promise once every piece is live. Half a campaign at 35% of promise is not
   underdelivery, it is Tuesday. Returns null for in_flight and not_started, and
   the screens print a word instead. */
function deliveryIndex(deal) {
  if (campaignState(deal) !== "complete") return null;
  return actualReach(deal) / deal.promise.reach;
}
function engagementIndex(deal) {
  if (campaignState(deal) !== "complete") return null;
  const rate = actualEngagementRate(deal);
  return rate === null ? null : rate / deal.promise.engagementRate;
}
const measurableDeals = (deals) => deals.filter((d) => campaignState(d) !== "not_started");
const completeDeals = (deals) => deals.filter((d) => campaignState(d) === "complete");

/* The agency-wide figure, over completed campaigns only: an in-flight campaign
   has not missed anything yet. */
function promiseKeptRate(deals) {
  const complete = completeDeals(deals);
  const promised = sumOf(complete, (d) => d.promise.reach);
  if (!promised) return null;
  return sumOf(complete, actualReach) / promised;
}

/* ── Money ───────────────────────────────────────────────────────────────
   Invoices and payouts are rows on their deals; these views attach the deal so
   every amount stays derived. */
const invoiceRows = (deals) => deals.map((deal) => ({ invoice: deal.invoice, deal: deal, amount: invoiceAmountOf(deal) }));
const payoutRows = (deals, creators) => deals.map((deal) => ({ payout: deal.payout, deal: deal, creator: creatorById(creators, deal.creatorId), amount: payoutAmountOf(deal) }));

const invoicesOutstanding = (rows) => rows.filter((r) => INVOICE_FLAGS.isOutstanding(r.invoice));
/* Issued and unpaid — the only money a brand actually owes us today. A draft
   invoice is ours, not theirs, and is counted separately wherever it appears. */
const invoicesAwaitingBrand = (rows) => rows.filter((r) => r.invoice.status === "issued");
const invoicesOverdue = (rows, now) => rows.filter((r) => INVOICE_FLAGS.isOverdue(r.invoice, now));
const invoicesToIssue = (rows) => rows.filter((r) => INVOICE_FLAGS.awaitsAgency(r.invoice));
const invoicesPaid = (rows) => rows.filter((r) => INVOICE_FLAGS.isSettled(r.invoice));

const payoutsOwed = (rows) => rows.filter((r) => !PAYOUT_FLAGS.isSettled(r.payout));
const payoutsReleasable = (rows) => rows.filter((r) => PAYOUT_FLAGS.isReleasable(r.payout));
const payoutsBlocked = (rows) => rows.filter((r) => PAYOUT_FLAGS.isBlocked(r.payout));
const moneyTotal = (rows) => sumBy(rows, "amount");

/* Commission the agency has actually banked: only on invoices the brand has
   paid. Commission on an unpaid invoice is a hope, not revenue. */
const commissionBanked = (rows) => sumOf(invoicesPaid(rows), (r) => commissionOf(r.deal));
const commissionPending = (rows) => sumOf(invoicesOutstanding(rows), (r) => commissionOf(r.deal));

/* Payment runs leave on Fridays. One helper, so the toast, the row and the
   dialog all name the same date. */
function nextPayoutRun(now) {
  /* UTC throughout, so the date the toast, the row and the dialog name does not
     depend on where the reader is sitting. */
  const d = new Date(now);
  d.setUTCHours(11, 0, 0, 0);
  const delta = (5 - d.getUTCDay() + 7) % 7;
  return d.getTime() + (delta === 0 && d.getTime() <= now ? 7 : delta) * 864e5;
}

/* ── Brands ──────────────────────────────────────────────────────────────
   Nothing about a brand is stored except its name and where it sits: how much
   they have spent, how long they take to pay and what we owe them are read off
   the briefs, deals and invoices that name them, so a brand page and the money
   ledger cannot disagree. */
const brandById = (brands, id) => brands.find((b) => b.id === id) || null;
const brandOf = (brands, record) => (record ? brandById(brands, record.brandId) : null);
const briefsForBrand = (briefs, brand) => briefs.filter((b) => b.brandId === brand.id).slice().sort((a, b) => b.receivedAt - a.receivedAt);
const dealsForBrand = (deals, brand) => deals.filter((d) => d.brandId === brand.id).slice().sort((a, b) => b.agreedAt - a.agreedAt);
const brandFirstBriefAt = (briefs, brand) => {
  const rows = briefsForBrand(briefs, brand);
  return rows.length ? Math.min.apply(null, rows.map((b) => b.receivedAt)) : null;
};
/* Over the briefs they have had an answer on. A brief still open has not been
   won or lost, so it is outside the rate rather than counted as a loss. */
function brandWinRate(briefs, brand) {
  const decided = briefsForBrand(briefs, brand).filter((b) => !BRIEF_FLAGS.isOpen(b));
  if (!decided.length) return null;
  return { rate: decided.filter((b) => b.status === "won").length / decided.length, decided: decided };
}
/* How long this brand actually takes to pay, over the invoices they have
   settled. Null until one is: a brand with nothing banked has no habit yet. */
function brandDaysToPay(deals, brand) {
  const paid = invoicesPaid(invoiceRows(dealsForBrand(deals, brand)));
  if (!paid.length) return null;
  return { days: Math.round(sumOf(paid, (r) => (r.invoice.paidAt - r.invoice.issuedAt) / 864e5) / paid.length), rows: paid };
}
/* Exclusivity we owe this brand: a creator's clause naming them, still running.
   Read from the roster, so it is the same list the match screen blocks on. */
const exclusivitiesTo = (creators, brand, now) => creators.reduce((rows, creator) => rows.concat(
  CREATOR_FLAGS.liveExclusivities(creator, now).filter((e) => e.brandId === brand.id).map((e) => ({ creator: creator, exclusivity: e }))
), []);
/* A campaign this brand booked outside the desk: no deal, no invoice, no
   commission — the booking is the only record of it. */
const outsideBookings = (creators, brand) => creators.filter((c) => c.booking && c.booking.brandId === brand.id);

/* ── One deliverable ─────────────────────────────────────────────────────── */
const deliverableRowById = (deals, creators, id) => allDeliverables(deals, creators).find((r) => r.item.id === id) || null;
const siblingDeliverables = (row, creators) => byDue(allDeliverables([row.deal], creators));
/* The record's history, read off the timestamps it already carries. Never a
   second narrative authored beside them. */
function deliverableHistory(row) {
  const { item, deal, creator } = row;
  const first = creator.name.split(" ")[0];
  return [
    { at: deal.agreedAt, what: "Booked on " + deal.id, who: deal.brand },
    item.submittedAt ? { at: item.submittedAt, what: "Submitted to " + deal.brand, who: first } : null,
    item.revisionAt ? { at: item.revisionAt, what: "Revisions requested", who: item.revisionBy, note: item.revisionNote } : null,
    item.chasedAt ? { at: item.chasedAt, what: "Chased " + first, who: MARQUE_USER.name } : null,
    item.approvedAt ? { at: item.approvedAt, what: "Approved", who: deal.brand } : null,
    item.publishedAt ? { at: item.publishedAt, what: "Published", who: first } : null,
  ].filter(Boolean).sort((a, b) => a.at - b.at);
}
/* This post's share of what the campaign has delivered so far. Suppressed
   rather than zeroed: an unpublished post has no share of anything. */
function postShareOfCampaign(row) {
  if (!DELIVERABLE_FLAGS.isLive(row.item)) return null;
  const total = actualReach(row.deal);
  return total ? row.item.metrics.reach / total : null;
}

/* ── A creator's statement ───────────────────────────────────────────────
   One helper for the money side of one creator, so the statement, the creator's
   file and the ledger cannot disagree: gross is what brands were billed,
   commission is ours, net is theirs, and paid against owed splits the net by
   what has actually left the account. */
function creatorStatement(deals, creators, creator) {
  const rows = deals.filter((d) => d.creatorId === creator.id).slice().sort((a, b) => b.agreedAt - a.agreedAt);
  const payouts = payoutRows(rows, creators);
  const paid = payouts.filter((r) => PAYOUT_FLAGS.isSettled(r.payout));
  const owed = payoutsOwed(payouts);
  const gross = sumBy(rows, "feeCents");
  return {
    deals: rows, payouts: payouts, paid: paid, owed: owed,
    releasable: payoutsReleasable(payouts), blocked: payoutsBlocked(payouts),
    grossCents: gross,
    commissionCents: sumOf(rows, commissionOf),
    netCents: sumOf(rows, creatorNetOf),
    paidCents: moneyTotal(paid),
    owedCents: moneyTotal(owed),
    /* What our commission has actually worked out at across their book, which
       is not the standard rate the moment one deal was signed below it. */
    effectiveRate: gross ? sumOf(rows, commissionOf) / gross : null,
  };
}
const statementYears = (rows) => rows.map((d) => new Date(d.agreedAt).getUTCFullYear()).filter((y, i, all) => all.indexOf(y) === i).sort((a, b) => b - a);

/* ── The desk's people ─────────────────────────────────────────────────── */
const managerById = (managers, id) => managers.find((m) => m.id === id) || null;
const managerOf = (managers, creator) => (creator ? managerById(managers, creator.managerId) : null);
const creatorsForManager = (creators, manager) => creators.filter((c) => c.managerId === manager.id);
/* One manager's book, every figure read off the creators they own. */
function managerBook(data, manager) {
  const roster = creatorsForManager(data.creators, manager);
  const ids = roster.map((c) => c.id);
  const deals = data.deals.filter((d) => ids.indexOf(d.creatorId) > -1);
  return {
    roster: roster, deals: deals,
    free: pitchableCreators(roster),
    reach: sumOf(roster, reachOf),
    feeCents: sumBy(deals, "feeCents"),
    commissionCents: sumOf(deals, commissionOf),
    attention: deliverablesNeedingAttention(allDeliverables(deals, data.creators), data.now),
  };
}
const unownedCreators = (creators, managers) => creators.filter((c) => !managers.some((m) => m.id === c.managerId));

/* ── App-scope search ────────────────────────────────────────────────────
   The palette footer's count and the results screen run this one function, so
   the number the footer promises is the number the screen shows. Every hit
   carries the line it matched on, so a reader can see why it is there. */
function searchDesk(data, query) {
  const q = String(query || "").trim().toLowerCase();
  if (!q) return { query: "", total: 0, groups: [] };
  const hit = function () { return Array.prototype.slice.call(arguments).some((t) => String(t === null || t === undefined ? "" : t).toLowerCase().indexOf(q) > -1); };
  /* A hit on prose the row does not print would otherwise be an unexplained
     line, so the matched phrase is quoted back with a little of its context.
     `hidden` is checked only when neither the title nor the line carries the
     match, so nothing is quoted twice. */
  const excerpt = (text) => {
    const source = String(text);
    const at = source.toLowerCase().indexOf(q);
    if (at === -1) return null;
    const from = Math.max(0, at - 24), to = Math.min(source.length, at + q.length + 32);
    return (from > 0 ? "…" : "") + source.slice(from, to).trim() + (to < source.length ? "…" : "");
  };
  const why = (row, hidden) => {
    if (hit(row.title, row.line)) return row;
    const found = (hidden || []).map(excerpt).filter(Boolean)[0];
    return found ? Object.assign({}, row, { line: row.line + " · “" + found + "”" }) : row;
  };
  const rows = allDeliverables(data.deals, data.creators);
  const groups = [
    { label: "Creators", items: data.creators.filter((c) => hit(c.name, c.handle, c.based, c.note, c.categories.map((x) => labelOf(CATEGORIES, x)).join(" "))).map((c) => why({
      id: c.id, title: c.name, line: "@" + c.handle + " · " + c.based + " · " + statusValue(CREATOR_STATUS, c.status).label.toLowerCase() + " · " + c.categories.map((x) => labelOf(CATEGORIES, x)).join(", "), route: "/talent/" + c.id }, [c.note])) },
    { label: "Brands", items: data.brands.filter((b) => hit(b.name, b.hq, b.note, labelOf(CATEGORIES, b.sector))).map((b) => why({
      id: b.id, title: b.name, line: labelOf(CATEGORIES, b.sector) + " · " + b.hq + " · " + briefsForBrand(data.briefs, b).length + (briefsForBrand(data.briefs, b).length === 1 ? " brief" : " briefs"), route: "/brands/" + b.id }, [b.note])) },
    { label: "Briefs", items: data.briefs.filter((b) => hit(b.id, b.brand, b.title, b.ask, b.contact.name)).map((b) => why({
      id: b.id, title: b.brand + " · " + b.title, line: b.id + " · " + formatMoney(b.budgetCents) + " · " + statusValue(BRIEF_STATUS, b.status).label, route: "/briefs/" + b.id }, [b.ask, b.contact.name])) },
    { label: "Deals", items: data.deals.filter((d) => hit(d.id, d.brand, d.title)).map((d) => ({
      id: d.id, title: d.brand + " × " + creatorById(data.creators, d.creatorId).name, line: d.id + " · " + formatMoney(d.feeCents) + " · " + statusValue(CONTRACT_STATUS, d.contract.status).label, route: "/deals/" + d.id })) },
    { label: "Deliverables", items: rows.filter((r) => hit(r.item.id, r.item.title, r.item.revisionNote, r.item.url)).map((r) => why({
      id: r.item.id, title: r.item.title, line: r.creator.name + " · " + r.deal.brand + " · " + statusValue(DELIVERABLE_STATUS, r.item.status).label, route: "/deliverables/" + r.item.id }, [r.item.revisionNote, r.item.url])) },
    { label: "Invoices and payouts", items: data.deals.filter((d) => hit(d.invoice.id, d.payout.id, d.payout.reference)).map((d) => ({
      id: d.invoice.id, title: d.invoice.id + " · " + d.payout.id, line: d.brand + " · " + formatMoney(invoiceAmountOf(d)) + " in, " + formatMoney(payoutAmountOf(d)) + " out", route: "/money" })) },
  ].filter((g) => g.items.length);
  return { query: String(query).trim(), total: groups.reduce((n, g) => n + g.items.length, 0), groups: groups };
}

/* ── Lookups ─────────────────────────────────────────────────────────── */
const creatorById = (creators, id) => creators.find((c) => c.id === id) || null;
const briefById = (briefs, id) => briefs.find((b) => b.id === id) || null;
const dealById = (deals, id) => deals.find((d) => d.id === id) || null;
const dealForBrief = (deals, brief) => (brief && brief.dealId ? dealById(deals, brief.dealId) : null);
const briefForDeal = (briefs, deal) => (deal ? briefById(briefs, deal.briefId) : null);

/* ── Nav badges ──────────────────────────────────────────────────────────
   Each counts a single kind of record with a single predicate, and says which.
   Where a screen's own section is deliberately looser than its badge, the
   screen's lede names the difference in words. */
const NAV_BADGES = {
  briefsToTriage: {
    count: (data) => briefsToTriage(data.briefs).length,
    label: "briefs waiting on your triage",
  },
  deliverablesNeedingAttention: {
    /* Revisions requested, overdue, or due inside the week — the same predicate
       the "Needs you" section runs. */
    count: (data) => deliverablesNeedingAttention(allDeliverables(data.deals, data.creators), data.now).length,
    label: "deliverables that need you this week",
  },
  invoicesOverdue: {
    count: (data) => invoicesOverdue(invoiceRows(data.deals), data.now).length,
    label: "invoices past their due date",
  },
};

Object.assign(window, {
  sumBy, sumOf,
  reachOf, engagementOf, leadPlatformOf, pitchableCreators, bookedCreators, comingFreeCreators, pausedCreators,
  bookingBrandOf, unavailableBecause,
  usageOf, rateFor, rateCardOf, rateFloorOf, rateCeilOf, briefUnits, quoteFor, rosterFloor, cheapestFitCreator,
  exclusivityConflict, fitOf, matchGroups,
  briefsToTriage, openBriefs, briefsWith, briefValue,
  commissionOf, creatorNetOf, invoiceAmountOf, payoutAmountOf, dealsAwaitingSignature, signedDeals, usageEndsAt, dealsWithRightsExpiring,
  brandById, brandOf, briefsForBrand, dealsForBrand, brandFirstBriefAt, brandWinRate, brandDaysToPay, exclusivitiesTo, outsideBookings,
  deliverableRowById, siblingDeliverables, deliverableHistory, postShareOfCampaign,
  creatorStatement, statementYears, managerById, managerOf, creatorsForManager, managerBook, unownedCreators, searchDesk,
  allDeliverables, deliverablesNeedingAttention, deliverablesWithBrand, deliverablesLive, deliverablesAhead, byDue, deliverableWaitingOn,
  publishedOf, actualReach, actualEngagements, actualSaves, actualEngagementRate, campaignState, deliveryIndex, engagementIndex,
  measurableDeals, completeDeals, promiseKeptRate,
  invoiceRows, payoutRows, invoicesOutstanding, invoicesAwaitingBrand, invoicesOverdue, invoicesToIssue, invoicesPaid,
  payoutsOwed, payoutsReleasable, payoutsBlocked, moneyTotal, commissionBanked, commissionPending, nextPayoutRun,
  creatorById, briefById, dealById, dealForBrief, briefForDeal, 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);

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

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

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

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

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

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

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

</script>
<script type="text/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.

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

const { Command } = window.OriginDesignSystem_7c998b;

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

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

  if (!open) return null;

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

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

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

</script>
<script type="text/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="MarqueParts.jsx">
/* Marque Talent Desk — shared parts, local to this product. Reported in README
   as local additions: Portrait / Who (creator identity, squared, one geometry),
   BookingBar / Availability (the 90-day availability device), PlatformLadder,
   DueClock (the forward-looking date column), Ledger rows, FormDialog, and the
   small repeated pieces. Nothing here holds data or assigns a status tone —
   StatusBadge does that. */
(() => {
  const {
    Button, Input, Textarea, Badge, RadioGroup, Select, 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,
    Avatar, MenuItem, MenuGroupLabel,
  } = 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; };


  /* Audience numbers are read, not counted: 1.4M, 412K. One spelling, wherever
     a follower count appears. */
  const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
  const formatFollowers = (n) => compact.format(n);

  /* The availability horizon. Ninety days is how far ahead brands ask us to
     hold someone, so it is the axis every booking bar is drawn on. */
  const HORIZON_DAYS = 90;

  function Eyebrow({ children }) {
    return <span className="mq-eyebrow">{children}</span>;
  }

  function Figure({ label, value, note, size }) {
    return (
      <div className={"mq-figure" + (size ? " mq-figure--" + size : "")}>
        <Eyebrow>{label}</Eyebrow>
        <div className="mq-figure__value">{value}</div>
        {note ? <div className="mq-figure__note">{note}</div> : null}
      </div>
    );
  }

  /* Platform marks. Brand-coloured source files, so they are drawn as images
     rather than inline icons; height is fixed and width follows the mark's own
     aspect ratio. */
  const PLATFORM_MARKS = {
    instagram: window.__resources.assets_templates_icons_instagram_svg,
    tiktok: window.__resources.assets_templates_icons_tiktok_svg,
    youtube: window.__resources.assets_templates_icons_youtube_svg,
  };
  function PlatformIcon({ platform, size = 14 }) {
    const src = PLATFORM_MARKS[platform];
    if (!src) return null;
    return <img className="mq-mark" src={src} alt="" aria-hidden="true" style={{ height: size + "px" }} />;
  }
  /* Platform options carry their mark. Built here so the select trigger and the
     open list read identically. */
  const platformItems = () => PLATFORMS.map((p) => ({
    value: p.value,
    label: <span className="mq-mark__row"><PlatformIcon platform={p.value} />{p.label}</span>,
  }));

  /* Creator identity. Squared, never a circle, and always at a size the source
     image can actually carry. */
  function Portrait({ creator, size = 40 }) {
    return <Avatar className="mq-portrait" size={size} src={creator.portrait} alt="" fallback={creator.name.split(" ").map((w) => w[0]).join("")} />;
  }

  function Who({ creator, size = 32, meta, onClick }) {
    return (
      <span className="mq-who">
        <Portrait creator={creator} size={size} />
        <span className="mq-who__text">
          {onClick
            ? <button type="button" className="mq-link mq-who__name" onClick={onClick}>{creator.name}</button>
            : <span className="mq-who__name">{creator.name}</span>}
          <span className="mq-who__meta">{meta || "@" + creator.handle}</span>
        </span>
      </span>
    );
  }

  /* The booking bar: the next ninety days, filled for the part of it the
     creator is not sellable. Free creators show an empty track, which is the
     point — the roster reads as a strip of open and closed weeks. */
  function BookingBar({ creator, now, label }) {
    const span = HORIZON_DAYS * 864e5;
    const until = creator.status === "booked" ? creator.bookedUntil : creator.status === "paused" ? creator.pausedUntil : null;
    const share = until ? Math.min(1, Math.max(0, (until - now) / span)) : 0;
    const tone = creator.status === "paused" ? "paused" : creator.status === "available" ? "free" : CREATOR_FLAGS.isComingFree(creator, now) ? "soon" : "";
    return (
      <span className={"mq-bookbar" + (tone ? " mq-bookbar--" + tone : "")} role="img" aria-label={label}>
        <span className="mq-bookbar__fill" style={{ width: (share * 100).toFixed(1) + "%" }} />
      </span>
    );
  }

  /* Status, the sentence that explains it, and the bar — spelled one way
     everywhere a creator's availability appears. */
  function Availability({ creator, now, deals, compactMode }) {
    const comingFree = CREATOR_FLAGS.isComingFree(creator, now);
    const blocked = unavailableBecause(creator, deals, now);
    const words = creator.status === "available"
      ? "Free to pitch today"
      : comingFree
        ? "Free in " + CREATOR_FLAGS.daysUntilFree(creator, now) + " days — " + blocked.replace("Booked to ", "with ")
        : blocked;
    return (
      <div className="mq-col mq-col--tight">
        <div className="mq-stack">
          <StatusBadge values={CREATOR_STATUS} value={creator.status} />
          {comingFree ? <Badge size="sm" variant="warning">Coming free</Badge> : null}
        </div>
        <BookingBar creator={creator} now={now} label={words} />
        {compactMode ? null : (
          <>
            <div className="mq-bookbar__ends"><span>Today</span><span>+{HORIZON_DAYS}d</span></div>
            <span className="mq-note">{words}</span>
          </>
        )}
      </div>
    );
  }

  function PlatformLadder({ creator }) {
    return (
      <div className="mq-plat">
        {creator.platforms.slice().sort((a, b) => b.followers - a.followers).map((p) => (
          <div className="mq-plat__row" key={p.platform}>
            <span className="mq-plat__label">{labelOf(PLATFORMS, p.platform)}</span>
            <span className="mq-plat__n">{formatFollowers(p.followers)}</span>
            <span className="mq-plat__e">{formatPercent(p.engagementRate)}</span>
          </div>
        ))}
        <div className="mq-plat__row">
          <span className="mq-plat__label">Total</span>
          <span className="mq-plat__n">{formatFollowers(reachOf(creator))}</span>
          <span className="mq-plat__e">{formatPercent(engagementOf(creator))}</span>
        </div>
      </div>
    );
  }

  /* A due date is a forward-looking column: direction is the first thing the
     reader needs, so the countdown spelling is primary and the absolute date is
     on hover. Delivered work has no countdown — it left on time or it didn't,
     and the row says which. */
  function DueClock({ item, now }) {
    const overdue = DELIVERABLE_FLAGS.isOverdue(item, now);
    const soon = DELIVERABLE_FLAGS.isDueSoon(item, now);
    /* Handed over: the countdown is spent, so the cell states the date it was
       due rather than a distance that would read as if it were still running. */
    if (DELIVERABLE_FLAGS.isDelivered(item)) {
      return <span className="mq-mono mq-muted" title={formatAbsolute(item.dueAt)}>Due {formatAbsolute(item.dueAt).split(",")[0]}</span>;
    }
    return (
      <span className={"mq-mono" + (overdue ? " mq-bad" : soon ? " mq-warn" : " mq-muted")} title={formatAbsolute(item.dueAt)}>
        {formatCountdown(item.dueAt, now)}
      </span>
    );
  }

  function KindList({ deliverables }) {
    return (
      <div className="mq-kinds">
        {deliverables.map((line) => (
          <Badge key={line.kind} variant="secondary" size="sm">{line.quantity} × {labelOf(DELIVERABLE_KINDS, line.kind)}</Badge>
        ))}
      </div>
    );
  }

  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, and a namespace
     parent with no screen renders as plain text. */
  function Crumbs({ path, params, labels, onNavigate }) {
    const trail = breadcrumb(path, MARQUE_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>
    );
  }

  function PageHead({ path, params, labels, eyebrow, title, lede, actions }) {
    return (
      <header className="mq-head">
        <div className="mq-head__text">
          {eyebrow ? <Eyebrow>{eyebrow}</Eyebrow> : <Crumbs path={path} params={params} labels={labels} />}
          <h1 className="mq-title">{title}</h1>
          {lede ? <p className="mq-lede">{lede}</p> : null}
        </div>
        {actions ? <div className="mq-head__actions">{actions}</div> : null}
      </header>
    );
  }

  /* Record header for the two record screens: the crumb, the identifier, the
     status, the name at display size. */
  function RecordHead({ crumb, id, status, title, lede, actions, media, children }) {
    return (
      <header className="mq-head">
        {media ? <div className="mq-head__media">{media}</div> : null}
        <div className="mq-head__text">
          {crumb}
          <div className="mq-stack"><span className="mq-mono">{id}</span>{status}</div>
          <h1 className="mq-title mq-title--sm">{title}</h1>
          {lede ? <p className="mq-lede">{lede}</p> : null}
          {children}
        </div>
        {actions ? <div className="mq-head__actions">{actions}</div> : null}
      </header>
    );
  }

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

  /* 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 parseCount = (text) => {
    const cleaned = String(text).replace(/[^0-9]/g, "");
    return cleaned ? parseInt(cleaned, 10) : NaN;
  };
  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"|"count"|"text"|"radio"|"select",
                options, hint, placeholder, when }] */
  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="mq-col mq-col--tight">
              {request.fields.filter((f) => !f.when || f.when(values)).map((field) => (
                <Field key={field.id}>
                  <FieldLabel htmlFor={"mq-f-" + field.id}>{field.label}</FieldLabel>
                  {field.kind === "radio" ? (
                    <RadioGroup items={field.options} value={values[field.id] || ""} onValueChange={(v) => set(field.id, v)} name={"mq-f-" + field.id} />
                  ) : field.kind === "select" ? (
                    <Select id={"mq-f-" + field.id} items={field.options} value={values[field.id] || ""} placeholder={field.placeholder} onValueChange={(v) => set(field.id, v)} />
                  ) : field.kind === "text" ? (
                    <Textarea id={"mq-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)} />
                  ) : field.kind === "count" ? (
                    <Input id={"mq-f-" + field.id} inputMode="numeric" placeholder={field.placeholder} value={values[field.id] || ""} invalid={submitted && !!errors[field.id]} onChange={(e) => set(field.id, e.target.value)} />
                  ) : (
                    <Input id={"mq-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, MARQUE_ROUTES);
    return (
      <div className="mq-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 Marque's route table, so it has no breadcrumb and nothing links to it. The five destinations in the bar, plus the brief, deal and account screens, are the whole product."}</EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button variant="outline" onClick={() => onNavigate("/")}>Back to the roster</Button>
          </EmptyContent>
        </Empty>
      </div>
    );
  }

  Object.assign(window, {
    registerBreadcrumbNavigate,
    HORIZON_DAYS, formatFollowers, Eyebrow, Figure, Portrait, Who, BookingBar, Availability, PlatformLadder,
    PlatformIcon, platformItems, DueClock, KindList, Crumbs, PageHead, RecordHead, LedgerRow, ConfirmDialog, FormDialog, ExportMenu,
    RouteNotBuilt, parseMoney, parseCount, moneyInputValue, resolvePath,
  });
})();

</script>
<script type="text/babel" data-presets="react" data-file="MarqueShell.jsx">
/* Marque Talent Desk shell — TOP-NAV shell, one navigation owner (the bar owns
   all five destinations; there is no sidebar and no second row of navigation).
   Five destinations is under the six the sidebar shell wants, and the record
   work happens *inside* a destination rather than across many of them, so the
   count decides it. Inferred, not requested — noted in the README.
   Density: 64px bar, 32px page gutters, editorial display type at the top of a
   page against 32px ledger rows where the work happens.
   Separation: hairlines and full-bleed --sidebar washes on the --background
   page plane; a card is used only where a thing is genuinely liftable off the
   page (the deal sheet, an inbound brief).
   Active state: foreground token + a filled --accent pill, derived from the
   route; counts sit after the label inside the pill.
   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:
     Roster       — the book: full-width creator bands, hairline-divided
     Briefs       — the triage deck over the pipeline ledger
     Deliverables — the three-week ladder over grouped work rows
     Performance  — the promise wall, chart-led, with the posts beneath it
     Money        — two facing ledgers with the commission as the spine
   Money is the destination that would most plausibly have been a table and is
   not: it answers two questions at once — what is coming in and what has to go
   out — and the only fact that matters is which side is blocking the other.
   Facing ledgers put the blocked payout beside the invoice that blocks it; two
   tables would have hidden that in a join. */
(() => {
  const { Button, Input, Kbd, MenuItem, MenuGroupLabel, Toast, ToastRegion, Avatar } = window.OriginDesignSystem_7c998b;

  /* Declared once for the product: 1280 full bar · 1024 search collapses to an
     icon · below 900 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 >= 900 ? "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 };
  };

  /* A deal has no destination of its own; it belongs to Briefs, which is where
     it came from and where the route table puts it. */
  const isActive = (path, route) =>
    path === route.path || (route.path !== "/" && path.indexOf(route.path + "/") === 0) || (route.path === "/briefs" && path.indexOf("/deals/") === 0) || (route.path === "/" && path.indexOf("/talent/") === 0);

  function Destinations({ path, data, onNavigate }) {
    return (
      <nav className="mq-dests" aria-label="Talent desk">
        {MARQUE_DESTINATIONS.map((route) => {
          const badge = badgeFor(route, data);
          return (
            <button type="button" key={route.path} className="mq-dest" aria-current={isActive(path, route) ? "page" : undefined} onClick={() => onNavigate(route.path)}>
              {route.label}
              {badge && badge.count > 0 ? <span className="mq-dest__count" aria-label={badge.count + " " + badge.label}>{badge.count}</span> : null}
            </button>
          );
        })}
      </nav>
    );
  }

  function DestinationsMenu({ path, data, onNavigate }) {
    const current = MARQUE_DESTINATIONS.find((r) => isActive(path, r)) || MARQUE_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) => MARQUE_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="mq-dest__count"> {badge.count}</span> : null}
            </MenuItem>
          );
        })}
      </Floating>
    );
  }

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

  function NotificationsButton({ items, unread, onRead, onNavigate }) {
    const trigger = (
      <span className="mq-utility">
        <Button variant="ghost" size="icon-sm" aria-label={unread ? unread + " unread notifications" : "Notifications"}><Icon name="bell" /></Button>
        {unread ? <span className="mq-badge-dot" aria-hidden="true">{unread}</span> : null}
      </span>
    );
    return (
      <Floating trigger={trigger} align="end" width="24rem" role="dialog" panelClass="o-popover mq-notify" label="Notifications">
        {(close) => (
          <>
            <div className="mq-notify__head">
              <span className="o-popover__title">Notifications</span>
              <Button variant="ghost" size="xs" onClick={onRead}>Mark all read</Button>
            </div>
            <div className="mq-notify__list">
              {items.map((n) => (
                <button type="button" key={n.id} className="mq-notify__item" onClick={() => { onNavigate(n.route); close(); }}>
                  <span className="mq-notify__title">{n.title}</span>
                  <span className="mq-notify__body">{n.body}</span>
                  <span className="mq-notify__time" title={formatAbsolute(n.at)}>{formatWhen(n.at, MARQUE_NOW)}</span>
                </button>
              ))}
            </div>
            {/* The panel shows the lines; the inbox is where they are worked
                through, so the destination behind the badge is one click away
                rather than only in the palette. */}
            <div className="mq-notify__foot">
              <button type="button" className="mq-link" onClick={() => { onNavigate("/inbox"); close(); }}>Open the inbox</button>
            </div>
          </>
        )}
      </Floating>
    );
  }

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

        <main className="mq-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, { MarqueShell, useShellMode });
})();

</script>
<script type="text/babel" data-presets="react" data-file="RosterScreen.jsx">
/* Roster — the book. The desk's home screen.
   Commit model: INSTANT. Nothing on this screen commits: the text filter, the
   category dropdown and the availability tabs are harmless and apply as typed.
   There is no Save button and no undoable action here.
   Data state: populated. Empty and no-results are the other two branches of
   one conditional below — never on screen beside these bands.
   Scope: the whole book. There is no period filter, so no figure on this
   screen moves when the tabs, the category or the text filter change; the
   filters narrow the list only, and the footer states what is showing.
   Primary display: full-width creator bands rather than a table, because the
   thing a talent manager reads first is who somebody is, and a name at 30px
   beside a portrait says that faster than a cell ever will. */
(() => {
  const {
    Button, Input, Select, Badge, Tabs, TabsList, TabsTab, Separator,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* Tabs narrow the list only. Each runs its own predicate — never a slice. */
  const ROSTER_TABS = [
    { value: "free", label: "Free", predicate: (c) => CREATOR_FLAGS.isPitchable(c) },
    { value: "soon", label: "Coming free", predicate: (c, now) => CREATOR_FLAGS.isComingFree(c, now) },
    { value: "booked", label: "Booked", predicate: (c) => c.status === "booked" },
    { value: "all", label: "Everyone", predicate: () => true },
  ];

  const matches = (creator, query) => {
    const q = query.trim().toLowerCase();
    if (!q) return true;
    return [creator.name, creator.handle, creator.based]
      .concat(creator.categories.map((c) => labelOf(CATEGORIES, c)))
      .join(" ").toLowerCase().indexOf(q) > -1;
  };

  /* The availability band: the three answers to the question brands ask most
     often, over the ninety-day horizon every booking bar is drawn on. */
  function AvailabilityBand({ creators, deals, now, onNavigate }) {
    const free = pitchableCreators(creators);
    const soon = comingFreeCreators(creators, now);
    const booked = bookedCreators(creators);
    const paused = pausedCreators(creators);
    return (
      <div className="mq-bleed">
        <div className="mq-split">
          <div className="mq-section">
            <div className="mq-figures">
              <Figure size="xl" label="Free to pitch" value={free.length} note={free.map((c) => c.name.split(" ")[0]).join(", ") + " — no booking against them today"} />
              <Figure size="xl" label={"Coming free inside " + AGENCY_POLICY.comingFreeDays + " days"} value={soon.length} note={soon.length ? soon.map((c) => c.name.split(" ")[0] + " on " + formatAbsolute(c.bookedUntil).split(",")[0]).join(" · ") : "Nobody comes off a campaign this month"} />
              <Figure size="xl" label="Booked" value={booked.length} note={booked.length + " of " + creators.length + " on the book, and " + paused.length + " paused at their own request"} />
            </div>
            <p className="mq-note">Availability is counted against today. On a brief, it is counted against the brand's own window instead — a creator booked until October is free for a campaign that starts in November.</p>
          </div>
          <div className="mq-section">
            <div className="mq-section__head"><h2 className="mq-h2">Coming off a campaign</h2></div>
            {soon.length === 0 ? (
              <p className="mq-note">Nobody on the book comes free in the next {AGENCY_POLICY.comingFreeDays} days. The next to free up is {bookedCreators(creators).slice().sort((a, b) => a.bookedUntil - b.bookedUntil)[0].name} on {formatAbsolute(bookedCreators(creators).slice().sort((a, b) => a.bookedUntil - b.bookedUntil)[0].bookedUntil).split(",")[0]}.</p>
            ) : (
              <div className="mq-rows">
                {soon.slice().sort((a, b) => a.bookedUntil - b.bookedUntil).map((creator) => (
                  <div className="mq-row" key={creator.id}>
                    <Portrait creator={creator} size={36} />
                    <div className="mq-row__main">
                      <span className="mq-row__title">{creator.name}</span>
                      <span className="mq-row__meta">{unavailableBecause(creator, deals, now)}</span>
                    </div>
                    <div className="mq-row__side">
                      <span className="mq-mono mq-warn" title={formatAbsolute(creator.bookedUntil)}>{formatCountdown(creator.bookedUntil, now)}</span>
                    </div>
                  </div>
                ))}
              </div>
            )}
            <p className="mq-note">Ranked by the date their booking ends. Hold nobody against a brief without checking their exclusivities first.</p>
          </div>
        </div>
      </div>
    );
  }

  function RateCard({ creator }) {
    const card = rateCardOf(creator);
    return (
      <Floating
        align="start" width="17rem" role="dialog" panelClass="o-popover" label={creator.name + " rate card"}
        trigger={<Button variant="ghost" size="xs">Rate card<Icon name="chevron-down" /></Button>}
      >
        <div className="mq-col mq-col--tight" style={{ padding: "var(--space-2)" }}>
          <Eyebrow>{creator.name} · organic, before usage</Eyebrow>
          <div className="mq-lg">
            {card.map((rate) => (
              <LedgerRow key={rate.kind} label={rate.label} value={formatMoney(rate.cents)} />
            ))}
          </div>
          <p className="mq-note">Usage is charged on top: {USAGE_SCOPES.filter((u) => u.uplift > 0).map((u) => u.label.toLowerCase() + " +" + Math.round(u.uplift * 100) + "%").join(", ")}.</p>
        </div>
      </Floating>
    );
  }

  function CreatorBand({ creator, deals, now, onNavigate }) {
    const recessed = creator.status === "paused";
    const live = CREATOR_FLAGS.liveExclusivities(creator, now);
    const booking = creator.booking && creator.booking.dealId ? dealById(deals, creator.booking.dealId) : null;
    return (
      <article className={"mq-cband" + (recessed ? " mq-cband--recessed" : "")}>
        <Portrait creator={creator} size={104} />
        <div className="mq-cband__id">
          <h3 className="mq-cband__name"><button type="button" className="mq-link" onClick={() => onNavigate("/talent/" + creator.id)}>{creator.name}</button></h3>
          <span className="mq-mono mq-muted">@{creator.handle} · {creator.based} · signed {formatAbsolute(creator.signedAt).split(",")[0]}</span>
          <div className="mq-stack">
            {creator.categories.map((c) => <Badge key={c} variant="secondary" size="sm">{labelOf(CATEGORIES, c)}</Badge>)}
          </div>
          <p className="mq-cband__note">{creator.note}</p>
          <div className="mq-stack">
            <span className="mq-mono">{formatMoney(rateFloorOf(creator))} – {formatMoney(rateCeilOf(creator))} a piece</span>
            <RateCard creator={creator} />
          </div>
        </div>
        <div className="mq-cband__plat">
          <PlatformLadder creator={creator} />
        </div>
        <div className="mq-cband__avail">
          <Availability creator={creator} now={now} deals={deals} />
          {booking ? (
            <Button variant="ghost" size="xs" onClick={() => onNavigate("/deals/" + booking.id)}>{booking.brand} deal<Icon name="arrow-right" /></Button>
          ) : null}
          {live.length ? (
            <div className="mq-excl">
              <span className="mq-eyebrow mq-bad">Exclusivity</span>
              {live.map((e) => (
                <span className="mq-note" key={e.brand}>
                  <strong>{e.brand}</strong> in {labelOf(CATEGORIES, e.category).toLowerCase()} until {formatAbsolute(e.until).split(",")[0]}
                </span>
              ))}
            </div>
          ) : null}
        </div>
      </article>
    );
  }

  function RosterScreen({ data, now, onNavigate, pushToast }) {
    const [tab, setTab] = React.useState("all");
    const [category, setCategory] = React.useState("");
    const [query, setQuery] = React.useState("");
    const creators = data.creators;
    const spec = ROSTER_TABS.find((t) => t.value === tab);
    const inTab = creators.filter((c) => spec.predicate(c, now));
    /* Ordered by how much has actually been booked through the desk: the busiest
       names first, audience breaking the ties between creators on equal counts. */
    const campaigns = (c) => data.deals.filter((d) => d.creatorId === c.id).length;
    const rows = inTab
      .filter((c) => (category ? c.categories.indexOf(category) > -1 : true))
      .filter((c) => matches(c, query))
      .sort((a, b) => campaigns(b) - campaigns(a) || reachOf(b) - reachOf(a));
    const free = pitchableCreators(creators);
    const soon = comingFreeCreators(creators, now);

    const clearFilters = () => { setQuery(""); setCategory(""); setTab("all"); };

    return (
      <div className="mq-page">
        <PageHead
          eyebrow={MARQUE_AGENCY.name + " · " + MARQUE_AGENCY.book}
          title="The book"
          lede={free.length + " of " + creators.length + " creators are free to pitch today, and " + (soon.length ? soon.length + " more come off a campaign inside " + AGENCY_POLICY.comingFreeDays + " days" : "nobody else frees up this month") + ". Audience and engagement are read from the platform rows, so they move when the numbers do."}
          actions={
            <div className="mq-actions__row">
              <Button size="sm" onClick={() => onNavigate("/talent/new")}>New creator</Button>
              <ExportMenu scope="Roster" onExport={(format) => pushToast({ title: "Export started", description: "Roster · " + format + " — we'll email it to " + MARQUE_USER.name.split(" ")[0].toLowerCase() + "@marque.example" })} />
            </div>
          }
        />

        <AvailabilityBand creators={creators} deals={data.deals} now={now} onNavigate={onNavigate} />

        <section className="mq-section">
          <div className="mq-filters">
            <div className="mq-filters__text">
              <Input size="sm" value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Filter the roster" placeholder="Filter by name, handle or city" startAddon={<Icon name="search" />} />
            </div>
            <div className="mq-stack">
              <Select
                size="sm" value={category} placeholder="Every category"
                items={[{ value: "", label: "Every category" }].concat(CATEGORIES.filter((c) => creators.some((cr) => cr.categories.indexOf(c.value) > -1)))}
                onValueChange={setCategory}
              />
              <Tabs value={tab} onValueChange={setTab}>
                <TabsList variant="underline" size="sm">
                  {ROSTER_TABS.map((t) => (
                    <TabsTab key={t.value} value={t.value} size="sm">
                      {t.label}<span className="mq-tab-count">{creators.filter((c) => t.predicate(c, now)).length}</span>
                    </TabsTab>
                  ))}
                </TabsList>
              </Tabs>
            </div>
          </div>

          {rows.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
                <EmptyTitle>Nobody on the book matches that</EmptyTitle>
                <EmptyDescription>{inTab.length} creators sit under {spec.label.toLowerCase()}{category ? " in " + labelOf(CATEGORIES, category).toLowerCase() : ""}. Clear the filters to see the whole book.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={clearFilters}>Clear filters</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="mq-roster">
              {rows.map((creator) => (
                <CreatorBand key={creator.id} creator={creator} deals={data.deals} now={now} onNavigate={onNavigate} />
              ))}
            </div>
          )}

          <div className="mq-foot">
            <span>Showing {rows.length} of {creators.length} creators{query ? " matching “" + query + "”" : ""}{category ? " in " + labelOf(CATEGORIES, category).toLowerCase() : ""} · sorted by campaigns booked, then total audience</span>
            <span>Rates are organic, before usage. A brief's quote adds the usage uplift.</span>
          </div>
        </section>
      </div>
    );
  }

  Object.assign(window, { RosterScreen, ROSTER_TABS });
})();

</script>
<script type="text/babel" data-presets="react" data-file="NewCreatorScreen.jsx">
/* New creator — signing somebody to the book. The roster's create.
   Commit model: STAGED, its own route rather than a dialog: an audience is a
   row per platform and a rate card is a row per kind, which is a form, not a
   couple of fields (AGENTS §5).
   Data state: one — an empty form.
   Derivation while typing: reach and engagement are computed from the platform
   rows by the same helpers every other screen uses, and the open briefs this
   creator would fit are run through the real fit predicate against the real
   fixture. Nothing on this screen is a plausible number.
   Primary display: a form, single column, grouped as who / audience / what they
   cost. */
(() => {
  const {
    Button, Input, Textarea, Select, Checkbox, Card, CardHeader, CardTitle, CardDescription, CardPanel,
    Field, FieldLabel, FieldDescription, FieldError, Badge,
  } = window.OriginDesignSystem_7c998b;

  const BLANK = {
    name: "", handle: "", based: "", note: "", managerId: "", categories: [],
    platforms: [{ platform: "", followers: "", engagement: "" }],
    rates: {},
  };
  /* "3.8" means 3.8%, because that is how a rate is quoted in this trade. */
  const parseRate = (text) => {
    const cleaned = String(text).replace(/[^0-9.]/g, "");
    if (!cleaned) return NaN;
    return parseFloat(cleaned) / 100;
  };

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

    const platforms = draft.platforms
      .filter((row) => row.platform && parseCount(row.followers) > 0 && !isNaN(parseRate(row.engagement)))
      .map((row) => ({ platform: row.platform, followers: parseCount(row.followers), engagementRate: parseRate(row.engagement) }));
    const rates = {};
    DELIVERABLE_KINDS.forEach((kind) => {
      const cents = parseMoney(draft.rates[kind.value] || "");
      if (!isNaN(cents) && cents > 0) rates[kind.value] = cents;
    });
    /* The draft as a creator, so the real helpers can read it. */
    const asCreator = {
      id: "draft", name: draft.name.trim() || "This creator", handle: draft.handle.trim(), based: draft.based.trim(),
      categories: draft.categories, status: "available", signedAt: now, note: draft.note.trim(),
      platforms: platforms, rates: rates, exclusivities: [], managerId: draft.managerId,
    };
    const reach = platforms.length ? reachOf(asCreator) : null;
    const engagement = platforms.length ? engagementOf(asCreator) : null;
    const card = platforms.length && Object.keys(rates).length ? rateCardOf(asCreator) : [];
    const fits = Object.keys(rates).length && platforms.length && draft.categories.length
      ? openBriefs(data.briefs).map((brief) => ({ brief: brief, fit: fitOf(asCreator, brief, now, data.deals) })).filter((row) => row.fit.fits)
      : [];
    const open = openBriefs(data.briefs);

    const errors = {};
    if (!draft.name.trim()) errors.name = "Their name, as it goes on a contract.";
    if (!draft.handle.trim()) errors.handle = "The handle brands will look them up by.";
    else if (data.creators.some((c) => c.handle.toLowerCase() === draft.handle.trim().replace(/^@/, "").toLowerCase())) errors.handle = "Somebody on the book already has that handle.";
    if (!draft.based.trim()) errors.based = "Where do they work from?";
    if (!draft.managerId) errors.managerId = "Somebody has to own them — an unowned creator is nobody's call to answer.";
    if (!draft.categories.length) errors.categories = "Pick at least one category, or no brief will ever match them.";
    if (!platforms.length) errors.platforms = "Add a platform with its followers and its engagement rate.";
    if (!Object.keys(rates).length) errors.rates = "A creator with no rate cannot be quoted for anything.";
    if (draft.note.trim().length < 20) errors.note = "Write the line you would say about them on a call — at least a sentence.";
    const valid = Object.keys(errors).length === 0;

    const setPlatform = (index, fields) => set({ platforms: draft.platforms.map((row, i) => (i === index ? Object.assign({}, row, fields) : row)) });
    const toggleCategory = (value) => set({ categories: draft.categories.indexOf(value) > -1 ? draft.categories.filter((c) => c !== value) : draft.categories.concat([value]) });

    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      onCreate({
        name: draft.name.trim(), handle: draft.handle.trim().replace(/^@/, ""), based: draft.based.trim(),
        note: draft.note.trim(), managerId: draft.managerId, categories: draft.categories,
        platforms: platforms, rates: rates,
      });
    };

    return (
      <div className="mq-page">
        <PageHead
          path="/talent/new"
          title="New creator"
          lede={"They join the book as free to pitch, with no exclusivity on file, and every open brief is matched against them from the moment you sign them. " + data.creators.length + " creators are on the book today."}
        />

        <Card>
          <CardHeader>
            <CardTitle>Who they are</CardTitle>
            <CardDescription>The categories are what a brief is matched on; the note is what you would say about them on a call.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel htmlFor="mq-nc-name">Name</FieldLabel>
                <Input id="mq-nc-name" value={draft.name} placeholder="Marisol Duarte" invalid={submitted && !!errors.name} onChange={(e) => set({ name: e.target.value })} />
                {submitted && errors.name ? <FieldError>{errors.name}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nc-handle">Handle</FieldLabel>
                <Input id="mq-nc-handle" value={draft.handle} startAddon="@" placeholder="marisolduarte" invalid={submitted && !!errors.handle} onChange={(e) => set({ handle: e.target.value })} />
                {submitted && errors.handle ? <FieldError>{errors.handle}</FieldError> : <FieldDescription>One handle, however many platforms they are on.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nc-based">Based</FieldLabel>
                <Input id="mq-nc-based" value={draft.based} placeholder="Mexico City" invalid={submitted && !!errors.based} onChange={(e) => set({ based: e.target.value })} />
                {submitted && errors.based ? <FieldError>{errors.based}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nc-manager">Manager</FieldLabel>
                <Select
                  id="mq-nc-manager" value={draft.managerId} placeholder="Who owns them?"
                  items={data.managers.filter((m) => m.role === MARQUE_USER.role).map((m) => ({ value: m.id, label: m.name + " · " + creatorsForManager(data.creators, m).length + " on their book" }))}
                  onValueChange={(v) => set({ managerId: v })}
                />
                {submitted && errors.managerId ? <FieldError>{errors.managerId}</FieldError> : <FieldDescription>Ownership is a field on the creator, and the team screen reads it.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel>Categories</FieldLabel>
                <div className="mq-checks">
                  {CATEGORIES.map((category) => (
                    <label className="mq-check" key={category.value}>
                      <Checkbox checked={draft.categories.indexOf(category.value) > -1} onCheckedChange={() => toggleCategory(category.value)} />
                      <span className="mq-check__text">{category.label}</span>
                    </label>
                  ))}
                </div>
                {submitted && errors.categories ? <FieldError>{errors.categories}</FieldError> : <FieldDescription>{open.length} briefs are open right now, in {open.map((b) => labelOf(CATEGORIES, b.category).toLowerCase()).filter((c, i, all) => all.indexOf(c) === i).join(", ")}.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nc-note">The line about them</FieldLabel>
                <Textarea id="mq-nc-note" rows={2} value={draft.note} placeholder="Slow-build interiors with a joinery bent. Brands buy the hands, not the face." invalid={submitted && !!errors.note} onChange={(e) => set({ note: e.target.value })} />
                {submitted && errors.note ? <FieldError>{errors.note}</FieldError> : null}
              </Field>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>Audience</CardTitle>
            <CardDescription>One row per platform. Reach and engagement are never stored — they are computed from these rows, so correcting a follower count later moves every figure that mentions it.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <div className="mq-repeat mq-repeat__head" aria-hidden="true"><span>Platform</span><span>Followers</span><span>Engagement</span><span /></div>
              {draft.platforms.map((row, index) => (
                <div className="mq-repeat" key={index}>
                  <Select size="sm" value={row.platform} placeholder="Platform" items={platformItems()} onValueChange={(v) => setPlatform(index, { platform: v })} />
                  <Input size="sm" inputMode="numeric" aria-label="Followers" placeholder="1400000" value={row.followers} onChange={(e) => setPlatform(index, { followers: e.target.value })} />
                  <Input size="sm" inputMode="decimal" aria-label="Engagement rate" endAddon="%" placeholder="1.9" value={row.engagement} onChange={(e) => setPlatform(index, { engagement: e.target.value })} />
                  {draft.platforms.length > 1 ? (
                    <Button variant="ghost" size="xs" aria-label="Remove this platform" onClick={() => set({ platforms: draft.platforms.filter((p, i) => i !== index) })}>Remove</Button>
                  ) : <span />}
                </div>
              ))}
              {submitted && errors.platforms ? <FieldError>{errors.platforms}</FieldError> : null}
              <div className="mq-stack">
                <Button variant="outline" size="sm" onClick={() => set({ platforms: draft.platforms.concat([{ platform: "", followers: "", engagement: "" }]) })}>Add a platform</Button>
                <span className="mq-note">
                  {reach === null
                    ? "Fill a row in and the totals appear."
                    : formatFollowers(reach) + " of audience at " + formatPercent(engagement) + " engaged, across " + platforms.length + " " + (platforms.length === 1 ? "platform" : "platforms") + " — " + labelOf(PLATFORMS, leadPlatformOf(asCreator).platform) + " carries " + formatPercent(leadPlatformOf(asCreator).followers / reach) + " of it."}
                </span>
              </div>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>Rate card</CardTitle>
            <CardDescription>Organic, before usage. Leave a kind empty and it is work they do not take — a brief asking for it gets no quote from them rather than a quote that prices it at nothing.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              {DELIVERABLE_KINDS.map((kind) => (
                <Field key={kind.value}>
                  <FieldLabel htmlFor={"mq-nc-rate-" + kind.value}>{kind.label}</FieldLabel>
                  <Input
                    id={"mq-nc-rate-" + kind.value} inputMode="decimal" startAddon="$" placeholder="Not offered"
                    value={draft.rates[kind.value] || ""}
                    onChange={(e) => set({ rates: Object.assign({}, draft.rates, { [kind.value]: e.target.value }) })}
                  />
                </Field>
              ))}
              {submitted && errors.rates ? <FieldError>{errors.rates}</FieldError> : null}
              <div className="mq-col mq-col--tight">
                <span className="mq-note">
                  {card.length === 0
                    ? "Price at least one kind and the range appears here."
                    : card.length + " of " + DELIVERABLE_KINDS.length + " kinds priced, " + formatMoney(rateFloorOf(asCreator)) + " – " + formatMoney(rateCeilOf(asCreator)) + " a piece. Usage is charged on top: " + USAGE_SCOPES.filter((u) => u.uplift > 0).map((u) => u.label.toLowerCase() + " +" + Math.round(u.uplift * 100) + "%").join(", ") + "."}
                </span>
                {fits.length ? (
                  <div className="mq-stack">
                    <Badge size="sm" variant="success">Fits {fits.length} of {open.length} open {open.length === 1 ? "brief" : "briefs"}</Badge>
                    <span className="mq-note">{fits.map((row) => row.brief.brand + " at " + formatMoney(row.fit.quote.totalCents) + " against " + formatMoney(row.brief.budgetCents)).join(" · ")}</span>
                  </div>
                ) : (
                  <span className="mq-note">As entered, they fit none of the {open.length} open briefs on their category, audience and rate together.</span>
                )}
              </div>
            </div>
          </CardPanel>
        </Card>

        <FormBar
          dirty={dirty}
          note={dirty ? "Nobody has been signed yet." : "An empty file. Fill it in from their deck."}
          onCancel={() => { setDraft(BLANK); setSubmitted(false); }}
          onSave={submit}
          label="New creator"
        />
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="TalentScreen.jsx">
/* Talent — the creator's file. The record behind every row on the roster, every
   name on a deal and every payout in the ledger.
   Commit model: INSTANT. Nothing on this screen commits: it is a document that
   navigates. The transitions a creator's work can take belong to the deal, the
   deliverables desk and the money ledger, so each action keeps exactly one
   entry point in the product.
   Data state: populated. A creator always has an audience, a rate card and an
   availability; the sections whose arrays are empty say so in one sentence
   rather than rendering a second state beside the first.
   Actions derive from the record: a free creator is offered the open briefs the
   fit predicate says they fit, a booked creator is offered the deal that booked
   them, and a paused creator is offered neither — the control is omitted, never
   disabled.
   Primary display: a document read top to bottom. Who they are, what they have
   delivered, what they cost, what we owe them. */
(() => {
  const { Button, Badge, Separator, MenuItem, MenuGroupLabel } = window.OriginDesignSystem_7c998b;

  /* Delivered against promised, as a bar. Drawn only for a campaign every piece
     of which is live — deliveryIndex suppresses the rest, and a half-run
     campaign at 40% of promise has not underdelivered, it is mid-flight. */
  function PromiseMeter({ deal }) {
    const index = deliveryIndex(deal);
    if (index === null) return null;
    return (
      <span className="mq-meter" role="img" aria-label={formatPercent(index) + " of the reach promised"}>
        <span className="mq-meter__fill" data-tone={index >= 1 ? "good" : "warn"} style={{ width: Math.min(100, index * 100).toFixed(1) + "%" }} />
      </span>
    );
  }

  function CampaignRow({ deal, now, onNavigate }) {
    const index = deliveryIndex(deal);
    const state = campaignState(deal);
    const live = publishedOf(deal);
    return (
      <div className="mq-tl__item" data-live={state === "complete" ? "yes" : "no"}>
        <span className="mq-tl__when" title={formatAbsolute(deal.agreedAt)}>{formatAbsolute(deal.agreedAt).split(",")[0]}</span>
        <div className="mq-tl__main">
          <button type="button" className="mq-link mq-tl__title" onClick={() => onNavigate("/deals/" + deal.id)}>{deal.brand} · {deal.title}</button>
          <span className="mq-row__meta">
            {formatMoney(deal.feeCents)} fee · {formatMoney(creatorNetOf(deal))} to them after {Math.round(deal.commissionRate * 100)}% commission · {live.length} of {deal.deliverables.length} pieces live
          </span>
          <PromiseMeter deal={deal} />
        </div>
        <div className="mq-tl__side">
          <StatusBadge values={CONTRACT_STATUS} value={deal.contract.status} />
          {index === null
            ? <span className="mq-mono mq-muted">{state === "not_started" ? "Nothing live" : "In flight"}</span>
            : <span className={"mq-mono " + (index >= 1 ? "mq-good" : "mq-warn")}>{formatPercent(index)} of promise</span>}
        </div>
      </div>
    );
  }

  function PitchMenu({ creator, fits, onNavigate }) {
    return (
      <Floating
        align="end" width="21rem" label="Open briefs this creator fits"
        trigger={<Button size="sm">Pitch to a brief<Icon name="chevron-down" /></Button>}
      >
        {(close) => (
          <>
            <MenuGroupLabel>{fits.length} open {fits.length === 1 ? "brief" : "briefs"} {creator.name.split(" ")[0]} fits</MenuGroupLabel>
            {fits.map((row) => (
              <MenuItem key={row.brief.id} onClick={() => { close(); onNavigate("/briefs/" + row.brief.id); }}>
                {row.brief.brand} · {formatMoney(row.fit.quote.totalCents)} against {formatMoney(row.brief.budgetCents)}
              </MenuItem>
            ))}
          </>
        )}
      </Floating>
    );
  }

  function TalentScreen({ creator, data, now, onNavigate, pushToast }) {
    const deals = data.deals.filter((d) => d.creatorId === creator.id).slice().sort((a, b) => b.agreedAt - a.agreedAt);
    const complete = completeDeals(deals);
    const rows = deals.reduce((all, deal) => all.concat(deal.deliverables.map((item) => ({ item: item, deal: deal, creator: creator }))), []);
    const inFlight = byDue(rows.filter((r) => !DELIVERABLE_FLAGS.isLive(r.item)));
    const attention = deliverablesNeedingAttention(rows, now);
    const payouts = payoutRows(deals, data.creators);
    const owed = payoutsOwed(payouts);
    const banked = payouts.filter((r) => PAYOUT_FLAGS.isSettled(r.payout));
    const card = rateCardOf(creator);
    const usageScopes = USAGE_SCOPES.filter((u) => u.uplift > 0);
    const live = CREATOR_FLAGS.liveExclusivities(creator, now);
    const booking = creator.booking && creator.booking.dealId ? dealById(data.deals, creator.booking.dealId) : null;
    const fits = openBriefs(data.briefs)
      .map((brief) => ({ brief: brief, fit: fitOf(creator, brief, now, data.deals) }))
      .filter((row) => row.fit.fits)
      .sort((a, b) => b.brief.budgetCents - a.brief.budgetCents);
    const blocked = unavailableBecause(creator, data.deals, now);

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/talent/:id" params={{ id: creator.id }} labels={{ "/talent/:id": creator.name }} />}
          id={"@" + creator.handle + " · " + creator.based + " · signed " + formatAbsolute(creator.signedAt).split(",")[0]}
          status={<StatusBadge values={CREATOR_STATUS} value={creator.status} />}
          title={creator.name}
          lede={creator.note}
          media={<Portrait creator={creator} size={88} />}
          actions={
            <div className="mq-actions__row">
              {CREATOR_FLAGS.isPitchable(creator) && fits.length ? <PitchMenu creator={creator} fits={fits} onNavigate={onNavigate} /> : null}
              {booking ? <Button size="sm" variant="outline" onClick={() => onNavigate("/deals/" + booking.id)}>Open the {booking.brand} deal<Icon name="arrow-right" /></Button> : null}
              <Button size="sm" variant="outline" onClick={() => onNavigate("/talent/" + creator.id + "/statement")}>Statement<Icon name="arrow-right" /></Button>
              <ExportMenu scope={creator.name} onExport={(format) => pushToast({ title: "Export started", description: creator.name + " · " + format + " — audience, rate card and campaign history." })} />
            </div>
          }
        >
          <div className="mq-stack" style={{ marginTop: "var(--space-2)" }}>
            {creator.categories.map((c) => <Badge key={c} variant="secondary" size="sm">{labelOf(CATEGORIES, c)}</Badge>)}
            <span className="mq-mono mq-muted">{formatFollowers(reachOf(creator))} audience · {formatPercent(engagementOf(creator))} engaged</span>
          </div>
        </RecordHead>

        <div className="mq-split">
          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Campaigns<span className="mq-tab-count">{deals.length}</span></h2>
                <p className="mq-note">
                  {deals.length === 0
                    ? "Nothing has been booked through the desk yet."
                    : complete.length
                      ? complete.length + " of " + deals.length + " ran to completion, and those are the only ones measured against what we promised the brand."
                      : "Nothing has finished running yet, so there is no promise to judge — the bars appear when every piece of a campaign is live."}
                </p>
              </div>
              {deals.length === 0 ? (
                <p className="mq-note">{creator.name} has no deal on this desk. {CREATOR_FLAGS.isPitchable(creator) ? (fits.length ? fits.length + " open " + (fits.length === 1 ? "brief fits" : "briefs fit") + " them today." : "No open brief fits them today.") : blocked + "."}</p>
              ) : (
                <>
                  <div className="mq-tl">
                    {deals.map((deal) => <CampaignRow key={deal.id} deal={deal} now={now} onNavigate={onNavigate} />)}
                  </div>
                  <div className="mq-foot">
                    <span>{formatMoney(sumBy(deals, "feeCents"))} of fees across {deals.length} {deals.length === 1 ? "campaign" : "campaigns"} · {formatMoney(sumOf(deals, creatorNetOf))} of it theirs</span>
                    <span>Oldest {formatAbsolute(deals[deals.length - 1].agreedAt).split(",")[0]}, newest {formatAbsolute(deals[0].agreedAt).split(",")[0]}</span>
                  </div>
                </>
              )}
            </section>

            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Still to come<span className="mq-tab-count">{inFlight.length}</span></h2>
                <p className="mq-note">
                  {inFlight.length
                    ? attention.length + " of " + inFlight.length + " " + (attention.length === 1 ? "needs" : "need") + " somebody this week. The deliverables desk owns the transitions."
                    : deals.length ? "Nothing outstanding — every piece they owe is live." : "Nothing is booked, so nothing is owed."}
                </p>
              </div>
              {inFlight.length ? (
                <>
                  <div className="mq-tl">
                    {inFlight.map((row) => (
                      <div className="mq-tl__item" key={row.item.id} data-attention={DELIVERABLE_FLAGS.needsAttention(row.item, now) ? "yes" : "no"}>
                        <span className="mq-tl__when" title={formatAbsolute(row.item.dueAt)}>{formatAbsolute(row.item.dueAt).split(",")[0]}</span>
                        <div className="mq-tl__main">
                          <span className="mq-tl__title">{row.item.title}</span>
                          <span className="mq-row__meta">
                            {row.deal.brand} · {row.item.quantity > 1 ? row.item.quantity + " × " : ""}{labelOf(DELIVERABLE_KINDS, row.item.kind)}
                            {deliverableWaitingOn(row) ? " · waiting on " + deliverableWaitingOn(row) : ""}
                          </span>
                        </div>
                        <div className="mq-tl__side">
                          <StatusBadge values={DELIVERABLE_STATUS} value={row.item.status} />
                          <DueClock item={row.item} now={now} />
                        </div>
                      </div>
                    ))}
                  </div>
                  <div className="mq-foot">
                    <span>Soonest {formatAbsolute(inFlight[0].item.dueAt).split(",")[0]} — {formatCountdown(inFlight[0].item.dueAt, now)}</span>
                    <Button variant="ghost" size="xs" onClick={() => onNavigate("/deliverables")}>Open the deliverables desk<Icon name="arrow-right" /></Button>
                  </div>
                </>
              ) : null}
            </section>
          </div>

          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Audience</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <PlatformLadder creator={creator} />
                <p className="mq-note">Read from the platform rows, so a follower count changed anywhere moves every figure that mentions it. {labelOf(PLATFORMS, leadPlatformOf(creator).platform)} carries {formatPercent(leadPlatformOf(creator).followers / reachOf(creator))} of the audience.</p>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Availability</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <Availability creator={creator} now={now} deals={data.deals} />
                <Separator />
                <div className="mq-col mq-col--tight">
                  <Eyebrow>Exclusivity</Eyebrow>
                  {creator.exclusivities.length === 0 ? (
                    <p className="mq-note">Nothing on file. Every category is open to pitch.</p>
                  ) : creator.exclusivities.map((e) => {
                    const running = live.indexOf(e) > -1;
                    return (
                      <p className="mq-note" key={e.brand}>
                        <strong className={running ? "mq-bad" : "mq-muted"}>{e.brand}</strong> in {labelOf(CATEGORIES, e.category).toLowerCase()}
                        {running ? " until " + formatAbsolute(e.until).split(",")[0] + " — " + formatCountdown(e.until, now) : " expired " + formatAbsolute(e.until).split(",")[0]}.
                        Signed {formatAbsolute(e.signedAt).split(",")[0]}.
                      </p>
                    );
                  })}
                </div>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Rate card</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-lg">
                  {card.map((rate) => <LedgerRow key={rate.kind} label={rate.label} value={formatMoney(rate.cents)} />)}
                  <LedgerRow label="Range" hint={card.length + " of " + DELIVERABLE_KINDS.length + " kinds — the rest is work they do not take, so a brief asking for one gets no quote"} value={formatMoney(rateFloorOf(creator)) + " – " + formatMoney(rateCeilOf(creator))} variant="net" />
                </div>
                <p className="mq-note">Organic, before usage: {usageScopes.map((u) => u.label.toLowerCase() + " adds " + Math.round(u.uplift * 100) + "%").join(", ")}.</p>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">What we owe them</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                {payouts.length === 0 ? (
                  <p className="mq-note">No campaign, so no payout and no commission. A zero here would be a figure about nothing.</p>
                ) : (
                  <>
                    <div className="mq-lg">
                      <LedgerRow label="Paid out" hint={banked.length + " of " + payouts.length + " payouts released"} value={banked.length ? formatMoney(moneyTotal(banked)) : <span className="mq-muted">Nothing yet</span>} />
                      <LedgerRow label="Outstanding" hint={owed.length ? owed.map((r) => r.deal.brand + " — " + statusValue(PAYOUT_STATUS, r.payout.status).label.toLowerCase()).join(", ") : "Nothing waiting"} value={owed.length ? formatMoney(moneyTotal(owed)) : <span className="mq-muted">Nothing</span>} />
                      <LedgerRow label="Commission we banked" hint="Only on invoices the brand has actually paid" value={formatMoney(commissionBanked(invoiceRows(deals)))} variant="net" />
                    </div>
                    <Button variant="ghost" size="sm" onClick={() => onNavigate("/talent/" + creator.id + "/statement")}>Open their statement<Icon name="arrow-right" /></Button>
                  </>
                )}
              </div>
            </section>
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="StatementScreen.jsx">
/* Statement — what one creator has earned, and what is still coming. The
   answer to the question a talent manager gets weekly, in a form that can be
   sent to the creator.
   Commit model: INSTANT. Nothing commits here: releasing a payout is money
   leaving the account and lives on the money ledger, which is its one entry
   point in the product.
   Data state: populated or empty, never both — a creator with no deal gets one
   sentence and a way back, because a statement of nothing is not a statement.
   Actions derive from the record: the release control appears on the ledger,
   not here; this screen offers the export and the ledger link.
   Primary display: one table, because that is what a statement is. Every
   figure beneath it sums the exact rows above it. */
(() => {
  const {
    Button, Separator, Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  function StatementScreen({ creator, data, now, onNavigate, pushToast }) {
    const s = creatorStatement(data.deals, data.creators, creator);
    const years = statementYears(s.deals);
    const standard = AGENCY_POLICY.standardCommissionRate;
    const below = s.deals.filter((d) => d.commissionRate < standard);

    if (s.deals.length === 0) {
      return (
        <div className="mq-page">
          <PageHead
            path="/talent/:id/statement"
            params={{ id: creator.id }}
            labels={{ "/talent/:id": creator.name }}
            title={creator.name + " — statement"}
            lede="Nothing has been booked for them through this desk, so there is nothing to state."
          />
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon"><Icon name="receipt" size={18} /></EmptyMedia>
              <EmptyTitle>No campaign, no statement</EmptyTitle>
              <EmptyDescription>A statement is a sum of deals, and {creator.name.split(" ")[0]} has none on the book. It will fill itself in the moment a brief becomes a deal.</EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button onClick={() => onNavigate("/talent/" + creator.id)}>Back to their file</Button>
            </EmptyContent>
          </Empty>
        </div>
      );
    }

    return (
      <div className="mq-page">
        <PageHead
          path="/talent/:id/statement"
          params={{ id: creator.id }}
          labels={{ "/talent/:id": creator.name }}
          title={creator.name + " — statement"}
          lede={"Every campaign booked through the desk since " + formatAbsolute(s.deals[s.deals.length - 1].agreedAt).split(",")[0] + ". " + formatMoney(s.netCents) + " is theirs across " + s.deals.length + " " + (s.deals.length === 1 ? "campaign" : "campaigns") + ", " + (s.paidCents ? "of which " + formatMoney(s.paidCents) + " has been paid." : "and none of it has been paid out yet.")}
          actions={<ExportMenu scope={creator.name + " statement"} onExport={(format) => pushToast({ title: "Export started", description: creator.name + " · " + format + " — every campaign, fee, commission and payout on this statement." })} />}
        />

        <div className="mq-bleed">
          <div className="mq-figures">
            <Figure size="xl" label="Theirs, after commission" value={formatMoney(s.netCents)} note={formatMoney(s.grossCents) + " billed to brands, " + formatMoney(s.commissionCents) + " ours"} />
            <Figure size="xl" label="Paid out" value={s.paid.length ? formatMoney(s.paidCents) : "Nothing yet"} note={s.paid.length ? s.paid.length + " of " + s.payouts.length + " payouts released, latest " + formatAbsolute(s.paid.slice().sort((a, b) => b.payout.paidAt - a.payout.paidAt)[0].payout.paidAt).split(",")[0] : "No payout has left the account for them"} />
            <Figure size="xl" label="Still owed" value={s.owed.length ? formatMoney(s.owedCents) : "Nothing"} note={s.owed.length ? [s.releasable.length ? s.releasable.length + " ready to release" : null, s.blocked.length ? s.blocked.length + " held until the brand pays" : null].filter(Boolean).join(", ") : "Every campaign they have run has been settled"} />
          </div>
          <p className="mq-note">
            Commission has worked out at {formatPercent(s.effectiveRate)} across their book against a standard of {Math.round(standard * 100)}%
            {below.length ? ", because " + below.map((d) => d.brand + " was signed at " + Math.round(d.commissionRate * 100) + "%").join(" and ") + "." : "."}
            {" "}Every deal stores the rate it was signed at, and this reads those rather than today's standard.
          </p>
        </div>

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">Campaign by campaign<span className="mq-tab-count">{s.deals.length}</span></h2>
            <p className="mq-note">{years.length === 1 ? "All of it in " + years[0] + "." : years.join(", ") + ". "}Newest first. A payout is only ready once the brand's invoice has landed, so the last column is the brand's clock as much as ours.</p>
          </div>
          <div className="mq-table-wrap">
            <Table className="mq-tbl">
              <TableHeader>
                <TableRow>
                  <TableHead>Campaign</TableHead>
                  <TableHead className="mq-drop-lg">Agreed</TableHead>
                  <TableHead className="mq-num">Fee</TableHead>
                  <TableHead className="mq-num mq-drop-md">Commission</TableHead>
                  <TableHead className="mq-num">Theirs</TableHead>
                  <TableHead>Payout</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {s.deals.map((deal) => {
                  const payout = s.payouts.find((r) => r.deal.id === deal.id);
                  const settled = PAYOUT_FLAGS.isSettled(payout.payout);
                  return (
                    <TableRow key={deal.id}>
                      <TableCell>
                        <span className="mq-cellstack">
                          <button type="button" className="mq-link" onClick={() => onNavigate("/deals/" + deal.id)}>{deal.brand} · {deal.title}</button>
                          <span className="mq-row__meta">{deal.id} · {publishedOf(deal).length} of {deal.deliverables.length} pieces live</span>
                        </span>
                      </TableCell>
                      <TableCell className="mq-drop-lg mq-muted"><span className="mq-mono" title={formatAbsolute(deal.agreedAt)}>{formatAbsolute(deal.agreedAt).split(",")[0]}</span></TableCell>
                      <TableCell className="mq-num mq-amount">{formatMoney(deal.feeCents)}</TableCell>
                      <TableCell className="mq-num mq-drop-md mq-muted">{formatMoney(commissionOf(deal))} <span className="mq-mono">({Math.round(deal.commissionRate * 100)}%)</span></TableCell>
                      <TableCell className="mq-num mq-amount">{formatMoney(creatorNetOf(deal))}</TableCell>
                      <TableCell>
                        <span className="mq-cellstack">
                          <StatusBadge values={PAYOUT_STATUS} value={payout.payout.status} />
                          <span className="mq-row__meta">
                            {settled
                              ? payout.payout.reference + " · " + formatAbsolute(payout.payout.paidAt).split(",")[0]
                              : PAYOUT_FLAGS.isReleasable(payout.payout)
                                ? "Ready — next run " + formatAbsolute(nextPayoutRun(now)).split(",")[0]
                                : "Held until " + deal.brand + " pay " + deal.invoice.id}
                          </span>
                        </span>
                      </TableCell>
                    </TableRow>
                  );
                })}
              </TableBody>
              <TableFooter>
                <TableRow>
                  <TableCell>{s.deals.length} {s.deals.length === 1 ? "campaign" : "campaigns"}</TableCell>
                  <TableCell className="mq-drop-lg" />
                  <TableCell className="mq-num mq-amount">{formatMoney(s.grossCents)}</TableCell>
                  <TableCell className="mq-num mq-drop-md">{formatMoney(s.commissionCents)}</TableCell>
                  <TableCell className="mq-num mq-amount">{formatMoney(s.netCents)}</TableCell>
                  <TableCell>{formatMoney(s.paidCents)} paid{s.owed.length ? ", " + formatMoney(s.owedCents) + " owed" : ""}</TableCell>
                </TableRow>
              </TableFooter>
            </Table>
          </div>
          <div className="mq-foot">
            <span>Fees are what the brand was billed; theirs is the fee less the commission stored on that deal. Nothing on this statement is a rounded figure.</span>
            <Button variant="ghost" size="xs" onClick={() => onNavigate("/money")}>Open the money ledger<Icon name="arrow-right" /></Button>
          </div>
        </section>

        {s.blocked.length ? (
          <section className="mq-section">
            <div className="mq-section__head">
              <h2 className="mq-h2">Why {formatMoney(moneyTotal(s.blocked))} has not moved</h2>
            </div>
            <div className="mq-sheet mq-sheet--tight">
              <div className="mq-lg">
                {s.blocked.map((r) => (
                  <LedgerRow
                    key={r.payout.id}
                    label={r.deal.brand}
                    hint={r.deal.invoice.status === "draft"
                      ? r.deal.invoice.id + " has not been issued — " + (r.deal.invoice.holdReason || "").toLowerCase()
                      : r.deal.invoice.id + " was due " + formatAbsolute(INVOICE_FLAGS.dueAt(r.deal.invoice)).split(",")[0] + " and is " + formatCountdown(INVOICE_FLAGS.dueAt(r.deal.invoice), now)}
                    value={formatMoney(r.amount)}
                  />
                ))}
              </div>
              <Separator />
              <p className="mq-note">We pay creators out of what the brand has paid us, so a late brand is a late payout. The chase lives on the money ledger, next to the invoice it belongs to.</p>
            </div>
          </section>
        ) : null}
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="BriefsScreen.jsx">
/* Briefs — inbound brand requests, and the triage decision.
   Commit model: INSTANT. The pipeline's tabs and text filter are harmless;
   "Chase the brand" and "Reopen brief" apply immediately and raise an undo
   toast; "Pass on brief" opens a staged dialog, which carries its own model.
   Data state: populated. The no-results branch belongs to the pipeline list
   only, and never renders beside rows.
   Scope: every brief the agency has received. No period filter, so nothing on
   this screen moves when the pipeline tabs change.
   One entry point per action: a brief waiting on triage appears in the deck and
   is deliberately absent from the pipeline below, so "Match creators" and "Pass
   on brief" exist exactly once each per brief.
   Primary display: the triage deck — an inbound brief rendered at the size of
   the decision it asks for — over a hairline pipeline ledger. */
(() => {
  const {
    Button, Input, Badge, Tabs, TabsList, TabsTab, Alert, AlertTitle, AlertDescription,
    Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* The pipeline holds everything that is no longer waiting on triage. Each tab
     runs its own predicate. */
  const PIPELINE_TABS = [
    { value: "pitching", label: "Pitched", predicate: (b) => b.status === "pitching" },
    { value: "won", label: "Won", predicate: (b) => b.status === "won" },
    { value: "closed", label: "Closed", predicate: BRIEF_FLAGS.isClosed },
  ];

  const matchesBrief = (brief, query) => {
    const q = query.trim().toLowerCase();
    if (!q) return true;
    return [brief.id, brief.brand, brief.title, labelOf(CATEGORIES, brief.category)].join(" ").toLowerCase().indexOf(q) > -1;
  };

  /* One inbound brief, at the size of the decision. Everything on the right is
     derived from the roster: the floor is the cheapest complete quote anyone on
     the book gives for this exact deliverable list at this usage. */
  function BriefBlock({ brief, data, now, onNavigate, onTransition }) {
    const groups = matchGroups(data.creators, brief, now, data.deals);
    const floor = rosterFloor(data.creators, brief);
    const cheapest = cheapestFitCreator(data.creators, brief);
    const inCategory = data.creators.filter((c) => c.categories.indexOf(brief.category) > -1);
    const belowFloor = floor !== null && brief.budgetCents < floor;
    const transitions = briefTransitions(brief, groups.fits.length);
    const units = briefUnits(brief);

    return (
      <article className="mq-brief">
        <div className="mq-brief__head">
          <Eyebrow>{brief.id} · arrived {formatWhen(brief.receivedAt, now)} · {brief.contact.name}, {brief.contact.role}</Eyebrow>
          <h3 className="mq-brief__brand">{brief.brand}</h3>
          <p className="mq-eyebrow">{brief.title} · {labelOf(CATEGORIES, brief.category)}</p>
          <p className="mq-brief__ask">{brief.ask}</p>
          <dl className="mq-specs">
            <dt>Wants</dt>
            <dd><KindList deliverables={brief.deliverables} /></dd>
            <dt>Usage</dt>
            <dd>{usageOf(brief.usage).label}{usageOf(brief.usage).months ? " · " + usageOf(brief.usage).months + " months · +" + Math.round(usageOf(brief.usage).uplift * 100) + "% on the organic fee" : " · no paid rights"}</dd>
            <dt>Window</dt>
            <dd>{formatAbsolute(brief.liveFromAt).split(",")[0]} – {formatAbsolute(brief.liveToAt).split(",")[0]}</dd>
            <dt>Audience</dt>
            <dd>{formatFollowers(brief.audienceMin)}+ total, they say</dd>
          </dl>
        </div>

        <div className="mq-brief__side">
          <Figure size="xl" label="Budget, one creator" value={formatMoney(brief.budgetCents)} note={formatMoney(Math.round(brief.budgetCents / units)) + " a piece across " + units + " pieces"} />
          <div className="mq-lg">
            <LedgerRow
              label="Cheapest quote on the book"
              hint={floor === null ? "Nobody on the book takes every piece this brief asks for." : cheapest.creator.name + (usageOf(brief.usage).uplift ? ", with the usage uplift" : ", organic — no usage to add")}
              value={floor === null ? <span className="mq-muted">No quote</span> : <span className={belowFloor ? "mq-bad mq-amount" : "mq-amount"}>{formatMoney(floor)}</span>}
            />
            <LedgerRow
              label="On the book in this category"
              value={inCategory.length ? inCategory.length : <span className="mq-bad">0</span>}
              hint={inCategory.length ? inCategory.map((c) => c.name.split(" ")[0]).join(", ") : "No one signed works in " + labelOf(CATEGORIES, brief.category).toLowerCase() + "."}
            />
            <LedgerRow label="Fits every axis" value={groups.fits.length ? groups.fits.length : <span className="mq-bad">0</span>} hint="Audience, category and rate, and able to take it." variant="sub" />
          </div>
          {groups.conflicts.length ? (
            <Alert variant="error">
              <AlertTitle>{groups.conflicts.length === 1 ? groups.conflicts[0].creator.name + " is under exclusivity" : groups.conflicts.length + " creators are under exclusivity"}</AlertTitle>
              <AlertDescription>{groups.conflicts.map((r) => r.creator.name.split(" ")[0] + " to " + r.conflict.brand + " until " + formatAbsolute(r.conflict.until).split(",")[0]).join(" · ")}</AlertDescription>
            </Alert>
          ) : null}
          {groups.fits.length === 0 ? (
            <Alert variant="error">
              <AlertTitle>Nobody on the book fits this brief</AlertTitle>
              <AlertDescription>
                {inCategory.length === 0 ? "No one signed works in " + labelOf(CATEGORIES, brief.category).toLowerCase() + ". " : ""}
                {belowFloor ? "The cheapest complete quote is " + formatMoney(floor) + " against a budget of " + formatMoney(brief.budgetCents) + "." : ""}
              </AlertDescription>
            </Alert>
          ) : null}
        </div>

        <div className="mq-brief__foot">
          <span className="mq-note">
            They want an answer by {formatAbsolute(brief.respondByAt).split(",")[0]} — <span className={BRIEF_FLAGS.daysToRespond(brief, now) <= 3 ? "mq-warn" : ""}>{formatCountdown(brief.respondByAt, now)}</span>.
          </span>
          <div className="mq-actions__row">
            {transitions.slice().sort((a, b) => (a.primary ? 1 : 0) - (b.primary ? 1 : 0)).map((t) => (
              <Button key={t.id} size="sm" variant={t.primary ? "default" : "outline"} onClick={() => onTransition(brief, t)}>{t.label}</Button>
            ))}
          </div>
        </div>
      </article>
    );
  }

  function BriefsScreen({ data, now, onNavigate, onBriefTransition, pushToast }) {
    const [tab, setTab] = React.useState("pitching");
    const [query, setQuery] = React.useState("");
    const briefs = data.briefs;
    const triage = briefsToTriage(briefs);
    const spec = PIPELINE_TABS.find((t) => t.value === tab);
    const inTab = briefs.filter(spec.predicate);
    const rows = inTab.filter((b) => matchesBrief(b, query)).sort((a, b) => b.receivedAt - a.receivedAt);
    const pipeline = briefs.filter((b) => !BRIEF_FLAGS.needsTriage(b));

    return (
      <div className="mq-page">
        <PageHead
          path="/briefs"
          title="Briefs"
          lede={triage.length
            ? triage.length + (triage.length === 1 ? " brief is waiting on you, worth " : " briefs are waiting on you, worth ") + formatMoney(briefValue(triage)) + (triage.length === 1 ? ". " : " between them. ") + "Each one is either worth pitching, wrong for the book, or under what anyone on it charges — the numbers beside it are computed from the roster, not typed."
            : "Nothing is waiting on triage. The " + pipeline.length + " briefs below are with brands, won, or closed."}
          actions={
            <div className="mq-actions__row">
              <Button size="sm" onClick={() => onNavigate("/briefs/new")}>New brief</Button>
              <ExportMenu scope="All briefs" onExport={(format) => pushToast({ title: "Export started", description: "Briefs · " + format + " — we'll email it to " + MARQUE_USER.name.split(" ")[0].toLowerCase() + "@marque.example" })} />
            </div>
          }
        />

        {triage.length ? (
          <section className="mq-section">
            <div className="mq-section__head">
              <h2 className="mq-h2">On your desk<span className="mq-tab-count">{triage.length}</span></h2>
              <p className="mq-note">Oldest first. Answering is the whole job here: pitch it, or say no in words the brand can hear.</p>
            </div>
            <div className="mq-triage">
              {triage.slice().sort((a, b) => a.respondByAt - b.respondByAt).map((brief) => (
                <BriefBlock key={brief.id} brief={brief} data={data} now={now} onNavigate={onNavigate} onTransition={onBriefTransition} />
              ))}
            </div>
          </section>
        ) : null}

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">Everything else</h2>
            <p className="mq-note">The {pipeline.length} briefs that are no longer waiting on triage.</p>
          </div>
          <div className="mq-filters">
            <div className="mq-filters__text">
              <Input size="sm" value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Filter briefs" placeholder="Filter by brand, brief or category" startAddon={<Icon name="search" />} />
            </div>
            <Tabs value={tab} onValueChange={setTab}>
              <TabsList variant="underline" size="sm">
                {PIPELINE_TABS.map((t) => (
                  <TabsTab key={t.value} value={t.value} size="sm">
                    {t.label}<span className="mq-tab-count">{briefs.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 {spec.label.toLowerCase()} brief matches “{query}”</EmptyTitle>
                <EmptyDescription>{inTab.length} briefs sit under {spec.label.toLowerCase()}. Clear the filter to see them.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => setQuery("")}>Clear filter</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="mq-table-wrap">
              <Table className="mq-tbl">
                <TableHeader>
                  <TableRow>
                    <TableHead>Brief</TableHead>
                    <TableHead className="mq-drop-lg">Category</TableHead>
                    <TableHead className="mq-drop-md">Window</TableHead>
                    <TableHead className="mq-num">Budget</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead>Outcome</TableHead>
                    <TableHead className="mq-actions"><span className="o-sr">Action</span></TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {rows.map((brief) => {
                    const deal = dealForBrief(data.deals, brief);
                    const creator = deal ? creatorById(data.creators, deal.creatorId) : null;
                    const recessed = BRIEF_FLAGS.isClosed(brief);
                    const transitions = briefTransitions(brief, 0).filter((t) => t.primary);
                    return (
                      <TableRow key={brief.id} className={recessed ? "mq-row-recessed" : ""}>
                        <TableCell>
                          <span className="mq-who__text">
                            <button type="button" className="mq-link" onClick={() => onNavigate("/briefs/" + brief.id)}>{brief.brand}</button>
                            <span className="mq-who__meta">{brief.id} · {brief.title}</span>
                          </span>
                        </TableCell>
                        <TableCell className="mq-drop-lg mq-muted">{labelOf(CATEGORIES, brief.category)}</TableCell>
                        <TableCell className="mq-drop-md mq-mono mq-muted">{formatAbsolute(brief.liveFromAt).split(",")[0]}</TableCell>
                        <TableCell className="mq-num mq-amount">{formatMoney(brief.budgetCents)}</TableCell>
                        <TableCell><StatusBadge values={BRIEF_STATUS} value={brief.status} /></TableCell>
                        <TableCell>
                          {deal ? (
                            <span className="mq-who__text">
                              <button type="button" className="mq-link" onClick={() => onNavigate("/deals/" + deal.id)}>{creator.name}</button>
                              <span className="mq-who__meta">{formatMoney(deal.feeCents)} · {deal.id}</span>
                            </span>
                          ) : brief.status === "pitching" ? (
                            <span className="mq-row__meta">Pitched {brief.pitched.map((id) => creatorById(data.creators, id).name.split(" ")[0]).join(" and ")} on {formatAbsolute(brief.pitchedAt).split(",")[0]}</span>
                          ) : brief.status === "passed" ? (
                            <span className="mq-row__meta">{labelOf(PASS_REASONS, brief.passReason)}</span>
                          ) : (
                            <span className="mq-row__meta">{brief.lostNote}</span>
                          )}
                        </TableCell>
                        <TableCell className="mq-actions">
                          {transitions.length ? (
                            <Button variant="outline" size="xs" onClick={() => onBriefTransition(brief, transitions[0])}>{transitions[0].label}</Button>
                          ) : null}
                        </TableCell>
                      </TableRow>
                    );
                  })}
                </TableBody>
              </Table>
            </div>
          )}
          <div className="mq-foot">
            <span>Showing {rows.length} of {inTab.length} {spec.label.toLowerCase()} briefs{query ? " matching “" + query + "”" : ""} · {briefs.length} received in all</span>
            <span>The brand's name opens the match. Sorted newest first.</span>
          </div>
        </section>
      </div>
    );
  }

  Object.assign(window, { BriefsScreen, PIPELINE_TABS });
})();

</script>
<script type="text/babel" data-presets="react" data-file="NewBriefScreen.jsx">
/* New brief — logging what a brand has sent in. The front door of the product:
   every brief on the desk arrived through here or through the fixture.
   Commit model: STAGED, and it is its own route rather than a dialog because
   the form is fifteen fields across five groups (AGENTS §5). Nothing applies
   until New brief; Cancel appears once dirty; errors render on submit.
   Data state: one — an empty form. There is no record yet to be loading or
   missing.
   Derivation while typing: the quote floor is read off the roster as the
   deliverable lines change, so the screen can say what the cheapest complete
   quote on the book would be against the budget being typed. It is the same
   rosterFloor() the match screen and the pass dialog run.
   Primary display: a form, single column, grouped by the question each group
   answers. */
(() => {
  const {
    Button, Input, Textarea, Select, Checkbox, Card, CardHeader, CardTitle, CardDescription, CardPanel,
    Field, FieldLabel, FieldDescription, FieldError, Separator,
  } = window.OriginDesignSystem_7c998b;

  const BLANK = {
    brandId: "", newBrandName: "", newBrandSector: "", newBrandHq: "",
    title: "", category: "", ask: "", budget: "", usage: "organic", audienceMin: "",
    lines: [{ kind: "", quantity: "1" }],
    liveFrom: "", liveTo: "", respondBy: "",
    contactName: "", contactRole: "",
  };
  const NEW_BRAND = "__new__";
  const dayOf = (value) => (value ? Date.parse(value + "T09:00:00Z") : null);

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

    const lines = draft.lines
      .filter((line) => line.kind && parseCount(line.quantity) > 0)
      .map((line) => ({ kind: line.kind, quantity: parseCount(line.quantity) }));
    const budgetCents = parseMoney(draft.budget);
    /* A shape complete enough to price. The floor is derived from the roster, so
       signing a cheaper creator moves what this screen says. */
    const shape = lines.length && draft.usage ? { deliverables: lines, usage: draft.usage, category: draft.category, budgetCents: isNaN(budgetCents) ? 0 : budgetCents } : null;
    const floor = shape ? rosterFloor(data.creators, shape) : null;
    const cheapest = shape ? cheapestFitCreator(data.creators, shape) : null;

    const errors = {};
    if (!draft.brandId) errors.brandId = "Say who it came from.";
    if (draft.brandId === NEW_BRAND) {
      if (!draft.newBrandName.trim()) errors.newBrandName = "Name the brand.";
      if (!draft.newBrandSector) errors.newBrandSector = "Pick the sector they sell in.";
      if (!draft.newBrandHq.trim()) errors.newBrandHq = "Where are they based?";
    }
    if (!draft.title.trim()) errors.title = "Give the campaign the name they call it.";
    if (!draft.category) errors.category = "Pick the category — it is what the roster is matched on.";
    if (draft.ask.trim().length < 20) errors.ask = "Paste their ask, in their words — at least a sentence.";
    if (isNaN(budgetCents) || budgetCents <= 0) errors.budget = "Enter the budget for one creator.";
    if (!lines.length) errors.lines = "Add at least one deliverable with a quantity.";
    if (!dayOf(draft.liveFrom)) errors.liveFrom = "When do they want it live?";
    if (!dayOf(draft.liveTo)) errors.liveTo = "When does the window close?";
    else if (dayOf(draft.liveFrom) && dayOf(draft.liveTo) < dayOf(draft.liveFrom)) errors.liveTo = "The window closes before it opens.";
    if (!dayOf(draft.respondBy)) errors.respondBy = "When do they need an answer?";
    else if (dayOf(draft.respondBy) < now) errors.respondBy = "That date has passed.";
    if (!draft.contactName.trim()) errors.contactName = "Who sent it?";
    if (!draft.contactRole.trim()) errors.contactRole = "What is their title?";
    const valid = Object.keys(errors).length === 0;

    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      onCreate({
        brandId: draft.brandId,
        newBrand: draft.brandId === NEW_BRAND
          ? { name: draft.newBrandName.trim(), sector: draft.newBrandSector, hq: draft.newBrandHq.trim() }
          : null,
        title: draft.title.trim(), category: draft.category, ask: draft.ask.trim(),
        budgetCents: budgetCents, usage: draft.usage,
        audienceMin: isNaN(parseCount(draft.audienceMin)) ? 0 : parseCount(draft.audienceMin),
        deliverables: lines,
        liveFromAt: dayOf(draft.liveFrom), liveToAt: dayOf(draft.liveTo), respondByAt: dayOf(draft.respondBy),
        contact: { name: draft.contactName.trim(), role: draft.contactRole.trim() },
      });
    };

    const setLine = (index, fields) => set({ lines: draft.lines.map((line, i) => (i === index ? Object.assign({}, line, fields) : line)) });

    return (
      <div className="mq-page">
        <PageHead
          path="/briefs/new"
          title="New brief"
          lede={"It lands on your desk to triage, with " + AGENCY_POLICY.dueSoonDays + " days of clock showing against the date they need an answer. Nothing is pitched until you match it to the roster."}
        />

        <Card>
          <CardHeader>
            <CardTitle>Who it came from</CardTitle>
            <CardDescription>A brand we have worked with brings its own history — briefs, campaigns and how long it takes to pay.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel htmlFor="mq-nb-brand">Brand</FieldLabel>
                <Select
                  id="mq-nb-brand" value={draft.brandId} placeholder="Pick the brand"
                  items={data.brands.slice().sort((a, b) => a.name.localeCompare(b.name)).map((b) => ({ value: b.id, label: b.name + " · " + briefsForBrand(data.briefs, b).length + (briefsForBrand(data.briefs, b).length === 1 ? " brief" : " briefs") }))
                    .concat([{ value: NEW_BRAND, label: "A brand we have not worked with" }])}
                  onValueChange={(v) => set({ brandId: v })}
                />
                {submitted && errors.brandId ? <FieldError>{errors.brandId}</FieldError> : null}
              </Field>
              {draft.brandId === NEW_BRAND ? (
                <>
                  <Field>
                    <FieldLabel htmlFor="mq-nb-bname">Brand name</FieldLabel>
                    <Input id="mq-nb-bname" value={draft.newBrandName} invalid={submitted && !!errors.newBrandName} onChange={(e) => set({ newBrandName: e.target.value })} />
                    {submitted && errors.newBrandName ? <FieldError>{errors.newBrandName}</FieldError> : <FieldDescription>Written once here, and every screen that names them reads it.</FieldDescription>}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="mq-nb-bsector">Sector</FieldLabel>
                    <Select id="mq-nb-bsector" value={draft.newBrandSector} placeholder="What do they sell?" items={CATEGORIES} onValueChange={(v) => set({ newBrandSector: v })} />
                    {submitted && errors.newBrandSector ? <FieldError>{errors.newBrandSector}</FieldError> : null}
                  </Field>
                  <Field>
                    <FieldLabel htmlFor="mq-nb-bhq">City</FieldLabel>
                    <Input id="mq-nb-bhq" value={draft.newBrandHq} placeholder="Copenhagen" invalid={submitted && !!errors.newBrandHq} onChange={(e) => set({ newBrandHq: e.target.value })} />
                    {submitted && errors.newBrandHq ? <FieldError>{errors.newBrandHq}</FieldError> : null}
                  </Field>
                </>
              ) : null}
              <Field>
                <FieldLabel htmlFor="mq-nb-contact">Their contact</FieldLabel>
                <Input id="mq-nb-contact" value={draft.contactName} placeholder="Ilse Brandão" invalid={submitted && !!errors.contactName} onChange={(e) => set({ contactName: e.target.value })} />
                {submitted && errors.contactName ? <FieldError>{errors.contactName}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-role">Their title</FieldLabel>
                <Input id="mq-nb-role" value={draft.contactRole} placeholder="Head of brand" invalid={submitted && !!errors.contactRole} onChange={(e) => set({ contactRole: e.target.value })} />
                {submitted && errors.contactRole ? <FieldError>{errors.contactRole}</FieldError> : <FieldDescription>Who we chase when the brief goes quiet.</FieldDescription>}
              </Field>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>What they are asking for</CardTitle>
            <CardDescription>The category is what the roster is matched on, and the audience floor is what disqualifies a creator before anything else does.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel htmlFor="mq-nb-title">Campaign</FieldLabel>
                <Input id="mq-nb-title" value={draft.title} placeholder="Winter barrier repair" invalid={submitted && !!errors.title} onChange={(e) => set({ title: e.target.value })} />
                {submitted && errors.title ? <FieldError>{errors.title}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-cat">Category</FieldLabel>
                <Select id="mq-nb-cat" value={draft.category} placeholder="Pick the category" items={CATEGORIES} onValueChange={(v) => set({ category: v })} />
                {submitted && errors.category ? <FieldError>{errors.category}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-ask">The ask, in their words</FieldLabel>
                <Textarea id="mq-nb-ask" rows={3} value={draft.ask} placeholder="Quote the brief. The creator reads the brand, not your summary of it." invalid={submitted && !!errors.ask} onChange={(e) => set({ ask: e.target.value })} />
                {submitted && errors.ask ? <FieldError>{errors.ask}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-budget">Budget, per creator</FieldLabel>
                <Input id="mq-nb-budget" inputMode="decimal" startAddon="$" value={draft.budget} placeholder="225,000" invalid={submitted && !!errors.budget} onChange={(e) => set({ budget: e.target.value })} />
                {submitted && errors.budget ? <FieldError>{errors.budget}</FieldError> : <FieldDescription>What one creator's whole campaign is worth to them.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-audience">Audience floor</FieldLabel>
                <Input id="mq-nb-audience" inputMode="numeric" value={draft.audienceMin} placeholder="750000" onChange={(e) => set({ audienceMin: e.target.value })} />
                <FieldDescription>Leave it empty if they have not asked for one. {pitchableCreators(data.creators).length} of {data.creators.length} creators are free to pitch today.</FieldDescription>
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-usage">Usage rights</FieldLabel>
                <Select id="mq-nb-usage" value={draft.usage} items={USAGE_SCOPES.map((u) => ({ value: u.value, label: u.label + (u.uplift ? " · +" + Math.round(u.uplift * 100) + "% on the fee, " + u.months + " months" : " · no uplift") }))} onValueChange={(v) => set({ usage: v })} />
                <FieldDescription>The uplift is priced into every quote, and a time-limited window is tracked to the day it ends.</FieldDescription>
              </Field>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>What they want made</CardTitle>
            <CardDescription>A kind nobody on the book has a rate for is work we do not take, and the quote below says so rather than pricing it at nothing.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <div className="mq-repeat mq-repeat--pair mq-repeat__head" aria-hidden="true"><span>Deliverable</span><span>Quantity</span><span /></div>
              {draft.lines.map((line, index) => (
                <div className="mq-repeat mq-repeat--pair" key={index}>
                  <Select
                    size="sm" value={line.kind} placeholder="Pick a deliverable"
                    items={DELIVERABLE_KINDS} onValueChange={(v) => setLine(index, { kind: v })}
                  />
                  <Input size="sm" inputMode="numeric" aria-label="Quantity" value={line.quantity} onChange={(e) => setLine(index, { quantity: e.target.value })} />
                  {draft.lines.length > 1 ? (
                    <Button variant="ghost" size="xs" aria-label="Remove this line" onClick={() => set({ lines: draft.lines.filter((l, i) => i !== index) })}>Remove</Button>
                  ) : <span />}
                </div>
              ))}
              {submitted && errors.lines ? <FieldError>{errors.lines}</FieldError> : null}
              <div className="mq-stack">
                <Button variant="outline" size="sm" onClick={() => set({ lines: draft.lines.concat([{ kind: "", quantity: "1" }]) })}>Add a deliverable</Button>
                <span className="mq-note">
                  {!shape
                    ? "Add a line and the quote floor appears."
                    : floor === null
                      ? "Nobody on the book takes all of this, so there is no quote to compare against the budget."
                      : "The cheapest complete quote on the book is " + formatMoney(floor) + " — " + cheapest.creator.name + (isNaN(budgetCents) || budgetCents <= 0 ? "." : floor <= budgetCents ? ", inside the budget." : ", which is " + formatMoney(floor - budgetCents) + " over the budget.")}
                </span>
              </div>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>Dates</CardTitle>
            <CardDescription>Availability is judged against the start of their window, never against today — a creator booked until October is free for a campaign starting in November.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel htmlFor="mq-nb-from">Live from</FieldLabel>
                <Input id="mq-nb-from" type="date" value={draft.liveFrom} invalid={submitted && !!errors.liveFrom} onChange={(e) => set({ liveFrom: e.target.value })} />
                {submitted && errors.liveFrom ? <FieldError>{errors.liveFrom}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-to">Live to</FieldLabel>
                <Input id="mq-nb-to" type="date" value={draft.liveTo} invalid={submitted && !!errors.liveTo} onChange={(e) => set({ liveTo: e.target.value })} />
                {submitted && errors.liveTo ? <FieldError>{errors.liveTo}</FieldError> : null}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-nb-respond">They need an answer by</FieldLabel>
                <Input id="mq-nb-respond" type="date" value={draft.respondBy} invalid={submitted && !!errors.respondBy} onChange={(e) => set({ respondBy: e.target.value })} />
                {submitted && errors.respondBy ? <FieldError>{errors.respondBy}</FieldError> : <FieldDescription>This is the clock the triage deck counts down.</FieldDescription>}
              </Field>
            </div>
          </CardPanel>
        </Card>

        <FormBar
          dirty={dirty}
          note={dirty ? "Nothing has been logged yet." : "An empty brief. Fill it in as it arrived."}
          onCancel={() => { setDraft(BLANK); setSubmitted(false); }}
          onSave={submit}
          label="New brief"
        />
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="MatchScreen.jsx">
/* Brief and match — who on the book fits this brand's ask, and who cannot take
   it at any price.
   Commit model: STAGED. The whole screen is one form: ticking creators builds a
   pitch, nothing leaves the desk until "Send pitch" is pressed, and Cancel
   appears once the selection differs from what was already pitched. There are
   no instant controls anywhere on it — "Chase the brand" and "Reopen brief"
   live on the briefs list, which owns them, so this screen holds exactly one
   commit model. "Pass on brief" opens a dialog, which carries its own.
   Data state: populated. A brief always has a roster to measure against.
   Actions derive from the record: a won brief shows where it landed and drops
   the pitch bar entirely; a closed one states why and links back.

   The exclusivity conflict is the thing this screen exists to prevent. It is
   stated three times, deliberately: as a full-width alert above everything, as
   a marked row in "Cannot take it", and as the reason on the disabled control
   itself. A conflict is a hazard, not a near miss, so it is never recessed. */
(() => {
  const {
    Button, Checkbox, Alert, AlertTitle, AlertDescription, AlertAction,
  } = window.OriginDesignSystem_7c998b;

  function Axis({ label, ok, value }) {
    return (
      <div className="mq-axis">
        <span className="mq-axis__label">{label}</span>
        <span className="mq-axis__mark" data-ok={ok ? "yes" : "no"}>
          <Icon name={ok ? "check" : "minus"} size={13} />{value}
        </span>
      </div>
    );
  }

  function FitRow({ row, brief, now, selected, onToggle, readOnly, variant }) {
    const creator = row.creator;
    const quote = row.quote;
    const disabledReason = row.blockedBecause || (quote.complete ? null : "No rate for " + quote.missing.map((k) => labelOf(DELIVERABLE_KINDS, k).toLowerCase()).join(" or "));
    const control = readOnly ? null : disabledReason ? (
      <FloatingTip side="left" label={disabledReason}>
        <span><Checkbox disabled checked={false} aria-label={"Cannot pitch " + creator.name + " — " + disabledReason} /></span>
      </FloatingTip>
    ) : (
      <Checkbox checked={selected} onCheckedChange={() => onToggle(creator.id)} aria-label={"Pitch " + creator.name + " at " + formatMoney(quote.totalCents)} />
    );
    return (
      <div className={"mq-fitrow" + (variant ? " mq-fitrow--" + variant : "")}>
        <Portrait creator={creator} size={56} />
        <div className="mq-who__text">
          <span className="mq-row__title">{creator.name}</span>
          <span className="mq-who__meta">@{creator.handle} · {creator.categories.map((c) => labelOf(CATEGORIES, c)).join(", ")}</span>
          {row.conflict ? (
            <span className="mq-note mq-bad"><strong>Exclusive to {row.conflict.brand}</strong> in {labelOf(CATEGORIES, row.conflict.category).toLowerCase()} until {formatAbsolute(row.conflict.until).split(",")[0]} — pitching them to {brief.brand} would breach it.</span>
          ) : row.blockedBecause ? (
            <span className="mq-note">{row.blockedBecause}</span>
          ) : null}
          {quote.complete ? null : (
            <span className="mq-note">{quote.missing.map((k) => labelOf(DELIVERABLE_KINDS, k).toLowerCase()).join(" and ")} is not on their rate card, so there is nothing to quote.</span>
          )}
        </div>
        <div className="mq-axes">
          <Axis label="Audience" ok={row.axes.audience} value={formatFollowers(row.reach)} />
          <Axis label="Category" ok={row.axes.category} value={row.axes.category ? labelOf(CATEGORIES, brief.category) : "Off"} />
          <Axis label="Rate" ok={row.axes.rate} value={quote.complete ? formatMoney(quote.totalCents) : "No rate"} />
        </div>
        <div className="mq-fitrow__quote">
          {quote.complete ? (
            <>
              <span className="mq-amount">{formatMoney(quote.totalCents)}</span>
              <span className="mq-row__meta">{formatMoney(quote.organicCents)} organic{quote.usage.uplift ? " + " + formatMoney(quote.upliftCents) + " usage" : ""}</span>
            </>
          ) : (
            <span className="mq-muted">No quote</span>
          )}
        </div>
        <div className="mq-actions">{control}</div>
      </div>
    );
  }

  function BriefRail({ brief, groups, floor, now }) {
    const usage = usageOf(brief.usage);
    return (
      <aside className="mq-rail">
        <div className="mq-sheet mq-sheet--tight">
          <Eyebrow>What they are asking for</Eyebrow>
          <dl className="mq-specs">
            <dt>Budget</dt><dd className="mq-amount">{formatMoney(brief.budgetCents)}</dd>
            <dt>Audience</dt><dd>{formatFollowers(brief.audienceMin)}+ total</dd>
            <dt>Category</dt><dd>{labelOf(CATEGORIES, brief.category)}</dd>
            <dt>Wants</dt><dd><KindList deliverables={brief.deliverables} /></dd>
            <dt>Usage</dt><dd>{usage.label}{usage.months ? ", " + usage.months + " months" : ""}{usage.uplift ? " · +" + Math.round(usage.uplift * 100) + "% on the fee" : ""}</dd>
            <dt>Window</dt><dd>{formatAbsolute(brief.liveFromAt).split(",")[0]} – {formatAbsolute(brief.liveToAt).split(",")[0]}</dd>
            <dt>Answer by</dt><dd>{formatAbsolute(brief.respondByAt).split(",")[0]}</dd>
          </dl>
        </div>
        <div className="mq-sheet mq-sheet--tight">
          <Eyebrow>How the book measures up</Eyebrow>
          <div className="mq-lg">
            <LedgerRow label="Fits every axis" value={groups.fits.length} hint="Audience, category and rate, and free in the window." />
            <LedgerRow label="Cannot take it" value={groups.blocked.length} hint="Booked across the window, paused, or under an exclusivity." />
            <LedgerRow label="Off brief" value={groups.offBrief.length} hint="Free to take it, but short on at least one axis." />
            <LedgerRow label="Cheapest complete quote" value={floor === null ? "—" : formatMoney(floor)} variant="sub" />
          </div>
          <p className="mq-note">Availability is judged against {formatAbsolute(brief.liveFromAt).split(",")[0]}, the day the brand wants to go live — not against today.</p>
        </div>
      </aside>
    );
  }

  function MatchScreen({ brief, data, now, onNavigate, onBriefTransition, onSendPitch }) {
    const initial = brief.pitched || [];
    const [selected, setSelected] = React.useState(initial);
    React.useEffect(() => { setSelected(brief.pitched || []); }, [brief.id, brief.status]);

    const groups = matchGroups(data.creators, brief, now, data.deals);
    const floor = rosterFloor(data.creators, brief);
    const open = BRIEF_FLAGS.isOpen(brief);
    const deal = dealForBrief(data.deals, brief);
    const byId = (id) => groups.all.find((r) => r.creator.id === id);
    const chosen = selected.map(byId).filter(Boolean);
    const quoted = sumOf(chosen.filter((r) => r.quote.complete), (r) => r.quote.totalCents);
    const stretch = chosen.filter((r) => !r.fits);
    const dirty = selected.slice().sort().join(",") !== initial.slice().sort().join(",");
    const toggle = (id) => setSelected((list) => (list.indexOf(id) > -1 ? list.filter((x) => x !== id) : list.concat([id])));

    const passTransition = briefTransitions(brief, groups.fits.length).find((t) => t.id === "pass_brief");

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/briefs/:id" params={{ id: brief.id }} labels={{ "/briefs/:id": brief.brand }} />}
          id={brief.id + " · received " + formatAbsolute(brief.receivedAt).split(",")[0]}
          status={<StatusBadge values={BRIEF_STATUS} value={brief.status} />}
          title={brief.brand + " — " + brief.title}
          lede={brief.ask}
          actions={
            <div className="mq-actions__row">
              {open && passTransition ? <Button variant="outline" size="sm" onClick={() => onBriefTransition(brief, passTransition)}>{passTransition.label}</Button> : null}
              <Button variant="ghost" size="sm" onClick={() => onNavigate("/brands/" + brief.brandId)}>{brief.brand}'s file<Icon name="arrow-right" /></Button>
            </div>
          }
        />

        {groups.conflicts.length ? (
          <Alert variant="error">
            <AlertTitle>
              {groups.conflicts.length === 1
                ? groups.conflicts[0].creator.name + " cannot be pitched to " + brief.brand
                : groups.conflicts.length + " creators cannot be pitched to " + brief.brand}
            </AlertTitle>
            <AlertDescription>
              {groups.conflicts.map((r) => (
                <span key={r.creator.id}>
                  {r.creator.name} signed an exclusivity with <strong>{r.conflict.brand}</strong> in {labelOf(CATEGORIES, r.conflict.category).toLowerCase()} on {formatAbsolute(r.conflict.signedAt).split(",")[0]}, running until <strong>{formatAbsolute(r.conflict.until).split(",")[0]}</strong> — {formatCountdown(r.conflict.until, now)}. {brief.brand} is a {labelOf(CATEGORIES, brief.category).toLowerCase()} brand, so any pitch is a breach.{" "}
                </span>
              ))}
            </AlertDescription>
          </Alert>
        ) : null}

        {deal ? (
          <Alert variant="success">
            <AlertTitle>Placed with {creatorById(data.creators, deal.creatorId).name}</AlertTitle>
            <AlertDescription>Agreed on {formatAbsolute(deal.agreedAt).split(",")[0]} at {formatMoney(deal.feeCents)}. The pitch is closed; the deal is where the work is now.</AlertDescription>
            <AlertAction><Button size="sm" onClick={() => onNavigate("/deals/" + deal.id)}>Open the deal<Icon name="arrow-right" /></Button></AlertAction>
          </Alert>
        ) : null}

        {!open && !deal ? (
          <Alert>
            <AlertTitle>{brief.status === "passed" ? "We passed on this brief" : "The brand went elsewhere"}</AlertTitle>
            <AlertDescription>
              {brief.status === "passed"
                ? labelOf(PASS_REASONS, brief.passReason) + " — " + brief.passNote + " Recorded " + formatAbsolute(brief.passedAt).split(",")[0] + "."
                : brief.lostNote + " Closed " + formatAbsolute(brief.lostAt).split(",")[0] + "."}
              {" "}Reopening it is done from the briefs list.
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="mq-split mq-split--rail">
          <BriefRail brief={brief} groups={groups} floor={floor} now={now} />

          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Fits the brief<span className="mq-tab-count">{groups.fits.length}</span></h2>
                <p className="mq-note">Clears the audience floor, works in {labelOf(CATEGORIES, brief.category).toLowerCase()}, quotes inside the budget, and is free across the brand's window.</p>
              </div>
              {groups.fits.length === 0 ? (
                <p className="mq-note">Nobody on the book clears all three axes for this one. {groups.blocked.length ? "Of the " + groups.blocked.length + " who cannot take it, " + groups.conflicts.length + " is barred by an exclusivity rather than a diary." : ""} The honest answer to {brief.brand} is no.</p>
              ) : (
                <div className="mq-rows">
                  {groups.fits.map((row) => (
                    <FitRow key={row.creator.id} row={row} brief={brief} now={now} readOnly={!open}
                      selected={selected.indexOf(row.creator.id) > -1} onToggle={toggle} />
                  ))}
                </div>
              )}
            </section>

            {groups.blocked.length ? (
              <section className="mq-section">
                <div className="mq-section__head">
                  <h2 className="mq-h2">Cannot take it<span className="mq-tab-count">{groups.blocked.length}</span></h2>
                  <p className="mq-note">Not a matter of fit. An exclusivity is a contract we signed; a booking is a promise we made.</p>
                </div>
                <div className="mq-rows">
                  {groups.blocked.map((row) => (
                    <FitRow key={row.creator.id} row={row} brief={brief} now={now} readOnly={!open}
                      variant={row.conflict ? "blocked" : "off"} selected={false} onToggle={toggle} />
                  ))}
                </div>
              </section>
            ) : null}

            {groups.offBrief.length ? (
              <section className="mq-section">
                <div className="mq-section__head">
                  <h2 className="mq-h2">Off brief<span className="mq-tab-count">{groups.offBrief.length}</span></h2>
                  <p className="mq-note">Free to take it, short on at least one axis. Pitch them anyway if you can argue it — the pitch bar will say what you are stretching.</p>
                </div>
                <div className="mq-rows">
                  {groups.offBrief.map((row) => (
                    <FitRow key={row.creator.id} row={row} brief={brief} now={now} readOnly={!open}
                      variant="off" selected={selected.indexOf(row.creator.id) > -1} onToggle={toggle} />
                  ))}
                </div>
              </section>
            ) : null}
          </div>
        </div>

        {open ? (
          <div className="mq-pitchbar">
            <div className="mq-pitchbar__text">
              <strong>{chosen.length === 0 ? "Nobody selected" : chosen.length + (chosen.length === 1 ? " creator" : " creators")}</strong>
              {chosen.length ? <span className="mq-amount">{formatMoney(quoted)} quoted against {formatMoney(brief.budgetCents)}</span> : <span className="mq-muted">Tick the creators you want to put in front of {brief.brand}.</span>}
              {stretch.length ? <span className="mq-warn">{stretch.length} of them {stretch.length === 1 ? "is" : "are"} off brief on {stretch[0].missedAxes.join(" and ")}</span> : null}
              {brief.pitched && brief.pitched.length ? <span className="mq-muted">Pitched {formatAbsolute(brief.pitchedAt).split(",")[0]}</span> : null}
            </div>
            <div className="mq-actions__row">
              {dirty ? <Button variant="outline" size="sm" onClick={() => setSelected(initial)}>Cancel</Button> : null}
              <Button size="sm" disabled={!dirty || chosen.length === 0} onClick={() => onSendPitch(brief, selected, quoted)}>
                Send pitch to {brief.brand}
              </Button>
            </div>
          </div>
        ) : null}
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="BrandScreen.jsx">
/* Brand — the client's file. The record behind every brand name on a brief, a
   deal, an invoice and an exclusivity clause.
   Commit model: INSTANT. Nothing here commits: it is a document that navigates.
   Chasing an invoice belongs to the money ledger and passing on a brief to the
   briefs desk, so each action keeps exactly one entry point in the product.
   Data state: populated. A brand exists here because something names it; the
   sections whose arrays are empty say so in a sentence rather than rendering a
   second state beside the first.
   Actions derive from the record: a brand with an open brief is offered it, a
   brand holding an exclusivity says whose, and a brand with nothing on the desk
   is offered neither — the control is omitted, never disabled.
   Primary display: a document read top to bottom. What they ask for, what they
   have run, what they owe, what we owe them.
   Nothing about a brand is stored except its name, sector and city: every
   figure below is read off the briefs, deals and invoices that name it. */
(() => {
  const { Button, Badge, Separator, Alert, AlertTitle, AlertDescription, AlertAction } = window.OriginDesignSystem_7c998b;

  function BriefRow({ brief, now, onNavigate }) {
    const open = BRIEF_FLAGS.isOpen(brief);
    return (
      <div className="mq-tl__item" data-live={brief.status === "won" ? "yes" : "no"}>
        <span className="mq-tl__when" title={formatAbsolute(brief.receivedAt)}>{formatAbsolute(brief.receivedAt).split(",")[0]}</span>
        <div className="mq-tl__main">
          <button type="button" className="mq-link mq-tl__title" onClick={() => onNavigate("/briefs/" + brief.id)}>{brief.title}</button>
          <span className="mq-row__meta">
            {brief.id} · {formatMoney(brief.budgetCents)} · {labelOf(CATEGORIES, brief.category)} · {briefUnits(brief)} pieces
            {brief.status === "passed" ? " · we passed: " + labelOf(PASS_REASONS, brief.passReason).toLowerCase() : ""}
            {brief.status === "lost" ? " · lost" : ""}
          </span>
        </div>
        <div className="mq-tl__side">
          <StatusBadge values={BRIEF_STATUS} value={brief.status} />
          {open
            ? <span className="mq-mono mq-warn" title={formatAbsolute(brief.respondByAt)}>{formatCountdown(brief.respondByAt, now)}</span>
            : <span className="mq-mono mq-muted">{brief.contact.name}</span>}
        </div>
      </div>
    );
  }

  function CampaignRow({ deal, creator, now, onNavigate }) {
    const index = deliveryIndex(deal);
    const state = campaignState(deal);
    const rightsEnd = RIGHTS_FLAGS.endsAt(deal);
    const expiring = RIGHTS_FLAGS.isExpiring(deal, now);
    return (
      <div className="mq-tl__item" data-live={state === "complete" ? "yes" : "no"}>
        <span className="mq-tl__when" title={formatAbsolute(deal.agreedAt)}>{formatAbsolute(deal.agreedAt).split(",")[0]}</span>
        <div className="mq-tl__main">
          <button type="button" className="mq-link mq-tl__title" onClick={() => onNavigate("/deals/" + deal.id)}>{deal.title} · {creator.name}</button>
          <span className="mq-row__meta">
            {formatMoney(deal.feeCents)} · {publishedOf(deal).length} of {deal.deliverables.length} pieces live · {usageOf(deal.usage).label.toLowerCase()}
            {rightsEnd === null ? "" : ", rights " + (RIGHTS_FLAGS.hasLapsed(deal, now) ? "lapsed " : "run to ") + formatAbsolute(rightsEnd).split(",")[0]}
          </span>
          {expiring ? <span className="mq-note mq-warn">{formatCountdown(rightsEnd, now)} on the usage window — renew it or the ads come down.</span> : null}
        </div>
        <div className="mq-tl__side">
          <StatusBadge values={CONTRACT_STATUS} value={deal.contract.status} />
          {index === null
            ? <span className="mq-mono mq-muted">{state === "not_started" ? "Nothing live" : "In flight"}</span>
            : <button type="button" className={"mq-link mq-mono " + (index >= 1 ? "mq-good" : "mq-warn")} onClick={() => onNavigate("/performance/" + deal.id)}>{formatPercent(index)} of promise</button>}
        </div>
      </div>
    );
  }

  function BrandScreen({ brand, data, now, onNavigate, pushToast }) {
    const briefs = briefsForBrand(data.briefs, brand);
    const open = briefs.filter(BRIEF_FLAGS.isOpen);
    const won = briefs.filter((b) => b.status === "won");
    const deals = dealsForBrand(data.deals, brand);
    const invoices = invoiceRows(deals);
    const overdue = invoicesOverdue(invoices, now);
    const outstanding = invoicesOutstanding(invoices);
    const banked = invoicesPaid(invoices);
    const habit = brandDaysToPay(data.deals, brand);
    const rate = brandWinRate(data.briefs, brand);
    const owedTo = exclusivitiesTo(data.creators, brand, now);
    const outside = outsideBookings(data.creators, brand);
    const firstAt = brandFirstBriefAt(data.briefs, brand);
    const blockedPayouts = payoutsBlocked(payoutRows(deals, data.creators));
    const expiring = deals.filter((d) => RIGHTS_FLAGS.isExpiring(d, now));

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/brands/:id" params={{ id: brand.id }} labels={{ "/brands/:id": brand.name }} />}
          id={labelOf(CATEGORIES, brand.sector) + " · " + brand.hq + (firstAt ? " · first brief " + formatAbsolute(firstAt).split(",")[0] : " · no brief on this desk")}
          status={deals.length ? <Badge size="sm" variant="secondary">{deals.length} {deals.length === 1 ? "campaign" : "campaigns"}</Badge> : null}
          title={brand.name}
          lede={brand.note}
          actions={
            <div className="mq-actions__row">
              {open.length ? <Button size="sm" onClick={() => onNavigate("/briefs/" + open[0].id)}>Open the live brief<Icon name="arrow-right" /></Button> : null}
              <ExportMenu scope={brand.name} onExport={(format) => pushToast({ title: "Export started", description: brand.name + " · " + format + " — briefs, campaigns and every invoice they have had." })} />
            </div>
          }
        />

        {overdue.length ? (
          <Alert variant="error">
            <AlertTitle>{brand.name} is {INVOICE_FLAGS.daysOverdue(overdue[0].invoice, now)} days past due on {formatMoney(moneyTotal(overdue))}</AlertTitle>
            <AlertDescription>
              {overdue.map((r) => r.invoice.id + " was due " + formatAbsolute(INVOICE_FLAGS.dueAt(r.invoice)).split(",")[0]).join(" · ")}.
              {blockedPayouts.length ? " " + blockedPayouts.map((r) => r.creator.name.split(" ")[0] + "'s " + formatMoney(r.amount)).join(" and ") + " cannot move until it lands." : ""}
            </AlertDescription>
            <AlertAction><Button variant="outline" size="sm" onClick={() => onNavigate("/money")}>Open the ledger</Button></AlertAction>
          </Alert>
        ) : null}

        <div className="mq-split">
          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Briefs<span className="mq-tab-count">{briefs.length}</span></h2>
                <p className="mq-note">
                  {briefs.length === 0
                    ? "Nothing has come in from " + brand.name + " on this desk. They are on file because " + (owedTo.length ? "we owe them an exclusivity." : "they booked one of our creators direct.")
                    : rate === null
                      ? open.length + " open, and no brief from them has been decided yet."
                      : formatPercent(rate.rate) + " of the " + rate.decided.length + " " + (rate.decided.length === 1 ? "brief" : "briefs") + " they have had an answer on became a deal" + (open.length ? ", and " + open.length + " " + (open.length === 1 ? "is" : "are") + " still open." : ".")}
                </p>
              </div>
              {briefs.length ? (
                <>
                  <div className="mq-tl">
                    {briefs.map((brief) => <BriefRow key={brief.id} brief={brief} now={now} onNavigate={onNavigate} />)}
                  </div>
                  <div className="mq-foot">
                    <span>{formatMoney(briefValue(briefs))} of budget across {briefs.length} {briefs.length === 1 ? "brief" : "briefs"} · {formatMoney(briefValue(won))} of it won</span>
                    <span>Oldest {formatAbsolute(briefs[briefs.length - 1].receivedAt).split(",")[0]}, newest {formatAbsolute(briefs[0].receivedAt).split(",")[0]}</span>
                  </div>
                </>
              ) : null}
            </section>

            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Campaigns<span className="mq-tab-count">{deals.length}</span></h2>
                <p className="mq-note">
                  {deals.length === 0
                    ? "No campaign has run through the desk for " + brand.name + "."
                    : completeDeals(deals).length
                      ? completeDeals(deals).length + " of " + deals.length + " ran to completion, and only those are measured against what we promised them."
                      : "Nothing has finished running, so there is no promise to judge yet."}
                </p>
              </div>
              {deals.length ? (
                <>
                  <div className="mq-tl">
                    {deals.map((deal) => <CampaignRow key={deal.id} deal={deal} creator={creatorById(data.creators, deal.creatorId)} now={now} onNavigate={onNavigate} />)}
                  </div>
                  <div className="mq-foot">
                    <span>{formatMoney(sumBy(deals, "feeCents"))} of fees · {formatMoney(sumOf(deals, commissionOf))} of it ours</span>
                    <span>{expiring.length ? expiring.length + " usage " + (expiring.length === 1 ? "window" : "windows") + " ends inside " + AGENCY_POLICY.rightsNoticeDays + " days" : "No usage window ends inside " + AGENCY_POLICY.rightsNoticeDays + " days"}</span>
                  </div>
                </>
              ) : null}
            </section>
          </div>

          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Money</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                {deals.length === 0 ? (
                  <p className="mq-note">No campaign, so no invoice and no commission. A zero here would be a figure about nothing.</p>
                ) : (
                  <>
                    <div className="mq-lg">
                      <LedgerRow label="Billed" hint={invoices.length + " " + (invoices.length === 1 ? "invoice" : "invoices") + " across " + deals.length + " " + (deals.length === 1 ? "campaign" : "campaigns")} value={formatMoney(moneyTotal(invoices))} />
                      <LedgerRow label="Banked" hint={banked.length ? banked.length + " settled, latest " + formatAbsolute(banked.slice().sort((a, b) => b.invoice.paidAt - a.invoice.paidAt)[0].invoice.paidAt).split(",")[0] : "Nothing settled yet"} value={banked.length ? formatMoney(moneyTotal(banked)) : <span className="mq-muted">Nothing yet</span>} />
                      <LedgerRow label="Outstanding" hint={outstanding.length ? outstanding.map((r) => r.invoice.id + " — " + statusValue(INVOICE_STATUS, r.invoice.status).label.toLowerCase()).join(", ") : "Nothing owed"} value={outstanding.length ? <span className={overdue.length ? "mq-bad" : ""}>{formatMoney(moneyTotal(outstanding))}</span> : <span className="mq-muted">Nothing</span>} />
                      <LedgerRow label="Commission from them" hint={banked.length ? "Only on invoices they have actually paid" : "Commission on an unpaid invoice is a hope, not revenue"} value={banked.length ? formatMoney(commissionBanked(invoices)) : <span className="mq-muted">Nothing banked</span>} variant="net" />
                    </div>
                    <p className="mq-note">
                      {habit === null
                        ? "They have never settled an invoice with us, so there is no habit to read. Terms are net " + AGENCY_POLICY.paymentTermsDays + " from issue."
                        : "They take " + habit.days + " days to pay on average across " + habit.rows.length + " settled " + (habit.rows.length === 1 ? "invoice" : "invoices") + ", against terms of net " + AGENCY_POLICY.paymentTermsDays + "."}
                    </p>
                  </>
                )}
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">What we owe them</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-col mq-col--tight">
                  <Eyebrow>Exclusivity</Eyebrow>
                  {owedTo.length === 0 ? (
                    <p className="mq-note">Nothing. No creator on the book is held to {brand.name} in any category, so every brief they send can be matched against the whole roster.</p>
                  ) : owedTo.map((row) => (
                    <p className="mq-note" key={row.creator.id + row.exclusivity.category}>
                      <button type="button" className="mq-link" onClick={() => onNavigate("/talent/" + row.creator.id)}><strong>{row.creator.name}</strong></button> is theirs in {labelOf(CATEGORIES, row.exclusivity.category).toLowerCase()} until {formatAbsolute(row.exclusivity.until).split(",")[0]} — {formatCountdown(row.exclusivity.until, now)}. Signed {formatAbsolute(row.exclusivity.signedAt).split(",")[0]}.
                    </p>
                  ))}
                </div>
                {outside.length ? (
                  <>
                    <Separator />
                    <div className="mq-col mq-col--tight">
                      <Eyebrow>Booked outside the desk</Eyebrow>
                      {outside.map((creator) => (
                        <p className="mq-note" key={creator.id}>
                          <button type="button" className="mq-link" onClick={() => onNavigate("/talent/" + creator.id)}><strong>{creator.name}</strong></button> is with them until {formatAbsolute(creator.bookedUntil).split(",")[0]} on a campaign that predates this desk, so there is no deal, no invoice and no commission on it here.
                        </p>
                      ))}
                    </div>
                  </>
                ) : null}
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Who we deal with</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                {briefs.length === 0 ? (
                  <p className="mq-note">No brief, so nobody on their side is on file. The contact comes in with the brief.</p>
                ) : (
                  <dl className="mq-specs">
                    {briefs
                      .filter((b, i) => briefs.findIndex((x) => x.contact.name === b.contact.name) === i)
                      .map((b) => (
                        <React.Fragment key={b.contact.name}>
                          <dt>{b.contact.role}</dt>
                          <dd>{b.contact.name} — last brief {formatAbsolute(b.receivedAt).split(",")[0]}, {b.id}</dd>
                        </React.Fragment>
                      ))}
                  </dl>
                )}
              </div>
            </section>
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="DealScreen.jsx">
/* Deal — the sheet. Negotiated fee, commission, deliverables with their own due
   dates, usage rights and where the paper stands.
   Commit model: INSTANT. Every control on this screen confirms itself: the
   contract transitions apply immediately and raise an undo toast, and
   cancelling the deal is irreversible so it opens a confirm dialog. There is no
   Save button anywhere.
   Data state: populated. A deal always has a fee, a contract and a deliverable
   list; where a figure is not yet meaningful it is suppressed with a word.
   Actions derive from the record: the contract's own status decides which
   transition the header offers, and a deal with anything published cannot be
   cancelled at all — the transition is omitted, not disabled.
   Deliverables are read-only here. The deal sheet is the document; the
   deliverables desk owns their transitions, so each action has exactly one
   entry point per screen. */
(() => {
  const { Button, Badge, Alert, AlertTitle, AlertDescription, AlertAction, Separator } = window.OriginDesignSystem_7c998b;

  /* Three stages, in the order the paper moves. `void` is not one of them — it
     is where a deal leaves the sequence, so it is appended rather than drawn as
     a step the contract is still travelling towards. */
  const CONTRACT_STAGES = CONTRACT_STATUS.filter((s) => s.id !== "void");

  function ContractSteps({ deal, now }) {
    const stamps = { drafted: deal.contract.draftedAt, out_for_signature: deal.contract.sentAt, signed: deal.contract.signedAt };
    const isVoid = CONTRACT_FLAGS.isVoid(deal);
    const current = isVoid ? CONTRACT_STAGES.length : CONTRACT_STAGES.findIndex((s) => s.id === deal.contract.status);
    const terminal = CONTRACT_FLAGS.isSigned(deal);
    return (
      <div className="mq-steps">
        {CONTRACT_STAGES.map((stage, i) => {
          const state = i < current ? "done" : i === current ? (terminal ? "done" : "current") : "ahead";
          const when = stamps[stage.id];
          return (
            <div className="mq-step" key={stage.id} data-state={state}>
              <span className="mq-step__label">{stage.label}</span>
              <span className="mq-step__when">
                {when ? formatAbsolute(when).split(",")[0] : i === current ? "now" : "not yet"}
                {i === current && stage.id === "out_for_signature" ? " · " + CONTRACT_FLAGS.daysOut(deal, now) + " days out" : ""}
              </span>
            </div>
          );
        })}
        {isVoid ? (
          <div className="mq-step" data-state="current">
            <span className="mq-step__label">Void</span>
            <span className="mq-step__when">{formatAbsolute(deal.contract.voidedAt).split(",")[0]}</span>
          </div>
        ) : null}
      </div>
    );
  }

  function DeliverableItem({ row, now }) {
    const item = row.item;
    const attention = DELIVERABLE_FLAGS.needsAttention(item, now);
    return (
      <div className="mq-tl__item" data-live={DELIVERABLE_FLAGS.isLive(item) ? "yes" : "no"} data-attention={attention ? "yes" : "no"}>
        <span className="mq-tl__when" title={formatAbsolute(item.dueAt)}>{formatAbsolute(item.dueAt).split(",")[0]}</span>
        <div className="mq-tl__main">
          <span className="mq-tl__title">{item.title}</span>
          <span className="mq-row__meta">{item.quantity > 1 ? item.quantity + " × " : ""}{labelOf(DELIVERABLE_KINDS, item.kind)}{DELIVERABLE_FLAGS.isLive(item) ? " · live " + formatAbsolute(item.publishedAt).split(",")[0] + " · " + formatFollowers(item.metrics.reach) + " reached" : ""}</span>
          {item.revisionNote ? <span className="mq-quote">“{item.revisionNote}” — {item.revisionBy}, {formatAbsolute(item.revisionAt).split(",")[0]}</span> : null}
        </div>
        <div className="mq-tl__side">
          <StatusBadge values={DELIVERABLE_STATUS} value={item.status} />
          <DueClock item={item} now={now} />
        </div>
      </div>
    );
  }

  function DealScreen({ deal, data, now, onNavigate, onContractTransition, onDealTransition }) {
    const creator = creatorById(data.creators, deal.creatorId);
    const brief = briefForDeal(data.briefs, deal);
    const usage = usageOf(deal.usage);
    const usageEnd = usageEndsAt(deal);
    const published = publishedOf(deal);
    const quote = quoteFor(creator, brief);
    const rows = byDue(deal.deliverables.map((item) => ({ item: item, deal: deal, creator: creator })));
    const attention = rows.filter((r) => DELIVERABLE_FLAGS.needsAttention(r.item, now));
    const transitions = contractTransitions(deal).concat(dealTransitions(deal, published.length));

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/deals/:id" params={{ id: deal.id }} labels={{ "/deals/:id": deal.id }} />}
          id={deal.id + " · agreed " + formatAbsolute(deal.agreedAt).split(",")[0]}
          status={<StatusBadge values={CONTRACT_STATUS} value={deal.contract.status} />}
          title={deal.brand + " × " + creator.name}
          lede={deal.title + ". " + (brief ? "From " + brief.id + ", pitched " + formatAbsolute(brief.pitchedAt).split(",")[0] + "." : "")}
          actions={
            <div className="mq-actions__row">
              {transitions.map((t) => (
                <Button key={t.id} size="sm" variant={t.destructive ? "destructive-outline" : t.primary ? "default" : "outline"}
                  onClick={() => (t.id === "void_deal" ? onDealTransition(deal, t) : onContractTransition(deal, t))}>
                  {t.label}
                </Button>
              ))}
            </div>
          }
        >
          <div className="mq-stack" style={{ marginTop: "var(--space-2)" }}>
            <Who creator={creator} size={32} meta={"@" + creator.handle + " · " + formatFollowers(reachOf(creator)) + " total audience"} />
          </div>
        </RecordHead>

        <ContractSteps deal={deal} now={now} />

        {CONTRACT_FLAGS.awaitsCountersign(deal) ? (
          <Alert variant="warning">
            <AlertTitle>Nothing can start until {deal.brand} countersign</AlertTitle>
            <AlertDescription>
              Sent on {formatAbsolute(deal.contract.sentAt).split(",")[0]}, {CONTRACT_FLAGS.daysOut(deal, now)} days ago. The first deliverable is due {formatAbsolute(rows[0].item.dueAt).split(",")[0]} — {formatCountdown(rows[0].item.dueAt, now)} — and the invoice is held behind the signature.
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="mq-split">
          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">Deliverables<span className="mq-tab-count">{deal.deliverables.length}</span></h2>
                <p className="mq-note">
                  {published.length} live, {attention.length} needing you{attention.length ? " — " + attention.map((r) => r.item.title).join(", ") : ""}. Submitting, approving and publishing happen on the deliverables desk.
                </p>
              </div>
              <div className="mq-tl">
                {rows.map((row) => <DeliverableItem key={row.item.id} row={row} now={now} />)}
              </div>
              <div className="mq-foot">
                <span>{deal.deliverables.length} deliverables · first due {formatAbsolute(rows[0].item.dueAt).split(",")[0]}, last {formatAbsolute(rows[rows.length - 1].item.dueAt).split(",")[0]}</span>
                <Button variant="ghost" size="xs" onClick={() => onNavigate("/deliverables")}>Open the deliverables desk<Icon name="arrow-right" /></Button>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Usage rights</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-lg">
                  <LedgerRow label="Scope" value={usage.label} hint={usage.uplift ? "Charged at +" + Math.round(usage.uplift * 100) + "% on the organic fee" : "No paid rights — the brand may not run these as ads"} />
                  <LedgerRow label="Runs from" value={formatAbsolute(deal.usageStartAt).split(",")[0]} hint={usage.months ? "The first post going live starts the clock" : "Nothing to run"} />
                  <LedgerRow
                    label="Expires"
                    value={usageEnd ? formatAbsolute(usageEnd).split(",")[0] : <span className="mq-muted">Not applicable</span>}
                    hint={usageEnd ? formatCountdown(usageEnd, now) + " — after that the brand takes the assets down or renews" : "Organic only, so there is nothing to expire"}
                    variant="sub"
                  />
                </div>
              </div>
            </section>
          </div>

          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">The money</h2></div>
              <div className="mq-sheet">
                <div className="mq-lg">
                  <LedgerRow label="Negotiated fee" hint={brief && quote.complete ? "Rate card quoted " + formatMoney(quote.totalCents) + " against a budget of " + formatMoney(brief.budgetCents) : "What the brand pays, agreed in writing"} value={formatMoney(deal.feeCents)} />
                  <LedgerRow label={"Agency commission, " + Math.round(deal.commissionRate * 100) + "%"} hint={deal.commissionRate === AGENCY_POLICY.standardCommissionRate ? "Our standard rate" : "Negotiated below our standard " + Math.round(AGENCY_POLICY.standardCommissionRate * 100) + "%"} value={"− " + formatMoney(commissionOf(deal))} />
                  <LedgerRow label={creator.name + " takes"} value={formatMoney(creatorNetOf(deal))} variant="net" />
                </div>
                <Separator />
                <div className="mq-lg">
                  <LedgerRow
                    label={"Invoice to " + deal.brand}
                    hint={deal.invoice.status === "draft" ? deal.invoice.holdReason : deal.invoice.status === "paid" ? "Banked " + formatAbsolute(deal.invoice.paidAt).split(",")[0] : "Issued " + formatAbsolute(deal.invoice.issuedAt).split(",")[0] + ", net " + AGENCY_POLICY.paymentTermsDays}
                    value={<span className="mq-stack" style={{ justifyContent: "flex-end" }}><StatusBadge values={INVOICE_STATUS} value={deal.invoice.status} /><span className="mq-amount">{formatMoney(invoiceAmountOf(deal))}</span></span>}
                  />
                  <LedgerRow
                    label={"Payout to " + creator.name.split(" ")[0]}
                    hint={deal.payout.status === "awaiting_funds" ? "Held until " + deal.brand + " pay" : deal.payout.status === "paid" ? "Released " + formatAbsolute(deal.payout.paidAt).split(",")[0] + " · " + deal.payout.reference : "Funds are in — release on the next run"}
                    value={<span className="mq-stack" style={{ justifyContent: "flex-end" }}><StatusBadge values={PAYOUT_STATUS} value={deal.payout.status} /><span className="mq-amount">{formatMoney(payoutAmountOf(deal))}</span></span>}
                  />
                </div>
                <Button variant="outline" size="sm" onClick={() => onNavigate("/money")}>Open the money ledger<Icon name="arrow-right" /></Button>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Against the promise</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-lg">
                  <LedgerRow label="Reach promised" hint={"Agreed in the pitch on " + (brief ? formatAbsolute(brief.pitchedAt).split(",")[0] : formatAbsolute(deal.agreedAt).split(",")[0])} value={formatFollowers(deal.promise.reach)} />
                  <LedgerRow label="Reach delivered" hint={published.length + " of " + deal.deliverables.length + " pieces live"} value={published.length ? formatFollowers(actualReach(deal)) : <span className="mq-muted">Nothing live yet</span>} />
                  <LedgerRow
                    label="Against promise"
                    hint={campaignState(deal) === "complete" ? "Every piece is live, so the campaign can be judged" : "Judged only once every piece is live"}
                    value={deliveryIndex(deal) === null
                      ? <span className="mq-muted">{campaignState(deal) === "not_started" ? "Not yet measurable" : "In flight"}</span>
                      : <span className={deliveryIndex(deal) >= 1 ? "mq-good" : "mq-warn"}>{formatPercent(deliveryIndex(deal))}</span>}
                    variant="sub"
                  />
                </div>
                <Button variant="ghost" size="sm" onClick={() => onNavigate("/performance")}>See the campaign<Icon name="arrow-right" /></Button>
              </div>
            </section>

            {brief ? (
              <section className="mq-section">
                <div className="mq-section__head"><h2 className="mq-h2">Where it came from</h2></div>
                <div className="mq-sheet mq-sheet--tight">
                  <div className="mq-col mq-col--tight">
                    <Eyebrow>{brief.id} · {brief.contact.name}, {brief.contact.role}</Eyebrow>
                    <p className="mq-note">{brief.ask}</p>
                    <div className="mq-stack">
                      <Badge variant="secondary" size="sm">{labelOf(CATEGORIES, brief.category)}</Badge>
                      <span className="mq-mono mq-muted">Budget {formatMoney(brief.budgetCents)}</span>
                    </div>
                    <Button variant="ghost" size="sm" onClick={() => onNavigate("/briefs/" + brief.id)}>Open the brief<Icon name="arrow-right" /></Button>
                  </div>
                </div>
              </section>
            ) : null}
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="DeliverablesScreen.jsx">
/* Deliverables — everything in flight, across every deal.
   Commit model: INSTANT. Each control confirms itself: submitting, approving
   and chasing apply immediately and raise an undo toast; recording revisions
   and marking a post published open staged dialogs, which carry their own
   model. No Save button.
   Data state: populated. The empty branch is one conditional and never renders
   beside rows.
   Scope: every deliverable on every deal, whatever the contract's state. The
   ladder shows three weeks; the sections below show all of them, and each
   section runs its own predicate rather than slicing the list.
   Recession: work sitting with the brand is recessed — muted surface, no
   primary control, and the row names who is being waited on. Its transitions
   stay reachable in the row menu, because recording what the brand said is
   still our job. Published work is not recessed: finished is not blocked.
   Primary display: the week ladder — the only calendar in the product, and the
   only place a due date is shown as a position rather than a number. */
(() => {
  const {
    Button, MenuItem, Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription,
  } = window.OriginDesignSystem_7c998b;

  const DAY = 864e5;
  const LADDER_WEEKS = 3;

  const startOfDay = (ts) => { const d = new Date(ts); d.setHours(0, 0, 0, 0); return d.getTime(); };
  /* The Monday of the week before this one — the ladder's left edge. */
  const ladderStart = (now) => {
    const today = startOfDay(now);
    const dow = (new Date(today).getDay() + 6) % 7;
    return today - (dow + 7) * DAY;
  };

  const chipTone = (item, now) => {
    if (DELIVERABLE_FLAGS.isOverdue(item, now)) return "danger";
    if (item.status === "revisions_requested") return "warning";
    if (item.status === "published") return "success";
    if (DELIVERABLE_FLAGS.isWithBrand(item)) return "info";
    return "";
  };

  function WeekLadder({ rows, now, onNavigate }) {
    const start = ladderStart(now);
    const today = startOfDay(now);
    const days = [];
    for (let i = 0; i < LADDER_WEEKS * 7; i++) days.push(start + i * DAY);
    const inLadder = rows.filter((r) => startOfDay(r.item.dueAt) >= start && startOfDay(r.item.dueAt) < start + LADDER_WEEKS * 7 * DAY);
    const next = byDue(rows.filter((r) => startOfDay(r.item.dueAt) >= start + LADDER_WEEKS * 7 * DAY))[0];

    return (
      <div className="mq-section">
        <div className="mq-section__head">
          <h2 className="mq-h2">Three weeks</h2>
          <p className="mq-note">{inLadder.length} of {rows.length} deliverables fall due between {formatAbsolute(start).split(",")[0]} and {formatAbsolute(start + (LADDER_WEEKS * 7 - 1) * DAY).split(",")[0]}. Placed by due date, coloured by what state they are in.</p>
        </div>
        <div className="mq-weekhead" aria-hidden="true">
          <span>Last week</span><span>This week</span><span>Next week</span>
        </div>
        <div className="mq-week">
          {days.map((day) => {
            const onDay = byDue(inLadder.filter((r) => startOfDay(r.item.dueAt) === day));
            return (
              <div className="mq-week__day" key={day} data-work={onDay.length ? "yes" : "no"} data-past={day < today ? "yes" : "no"} data-today={day === today ? "yes" : "no"}>
                <span className="mq-week__date">{new Date(day).toLocaleDateString("en-GB", { weekday: "short", day: "numeric" })}</span>
                {onDay.map((r) => (
                  <button type="button" className="mq-chip" key={r.item.id} data-tone={chipTone(r.item, now)} onClick={() => onNavigate("/deliverables/" + r.item.id)}
                    aria-label={r.item.title + " for " + r.deal.brand + ", " + statusValue(DELIVERABLE_STATUS, r.item.status).label.toLowerCase()}>
                    <span className="mq-chip__who">{r.creator.name.split(" ")[0]} · {r.deal.brand}</span>
                    <span className="mq-chip__what">{r.item.title}</span>
                  </button>
                ))}
              </div>
            );
          })}
        </div>
        <div className="mq-foot">
          <span>Today is {formatAbsolute(now).split(",")[0]}. Red is over its due date, amber is back for revisions, blue is with the brand, green is live.</span>
          {next ? <span>Next after this window: {next.item.title} for {next.deal.brand}, {formatAbsolute(next.item.dueAt).split(",")[0]}.</span> : null}
        </div>
      </div>
    );
  }

  function WorkRow({ row, now, recessed, onTransition, onNavigate }) {
    const item = row.item;
    const transitions = deliverableTransitions(item);
    const primary = transitions.find((t) => t.primary);
    const rest = transitions.filter((t) => !t.primary);
    const waiting = deliverableWaitingOn(row);
    return (
      <div className={"mq-row" + (recessed ? " mq-row--recessed" : "")}>
        <Portrait creator={row.creator} size={36} />
        <div className="mq-row__main">
          <span className="mq-row__title">
            <button type="button" className="mq-link" onClick={() => onNavigate("/deliverables/" + item.id)}>{item.title}</button>
          </span>
          <span className="mq-row__meta">
            {row.creator.name} · <button type="button" className="mq-link" onClick={() => onNavigate("/brands/" + row.deal.brandId)}>{row.deal.brand}</button>
            {" · "}{item.quantity > 1 ? item.quantity + " × " : ""}{labelOf(DELIVERABLE_KINDS, item.kind)}
            {waiting ? " · waiting on " + waiting : ""}
            {DELIVERABLE_FLAGS.isLive(item) ? " · " + formatFollowers(item.metrics.reach) + " reached, " + formatCount(item.metrics.saves) + " saves" : ""}
          </span>
          {item.revisionNote ? <span className="mq-quote">“{item.revisionNote}” — {item.revisionBy}, {formatWhen(item.revisionAt, now)}</span> : null}
        </div>
        <div className="mq-row__side">
          <DueClock item={item} now={now} />
          <StatusBadge values={DELIVERABLE_STATUS} value={item.status} />
          {transitions.length === 0 ? null : recessed ? (
            <Floating align="end" width="14rem" label={"Actions for " + item.title}
              trigger={<Button variant="ghost" size="icon-xs" aria-label={"Actions for " + item.title}><Icon name="ellipsis" size={14} /></Button>}>
              {(close) => transitions.map((t) => <MenuItem key={t.id} onClick={() => { close(); onTransition(row, t); }}>{t.label}</MenuItem>)}
            </Floating>
          ) : (
            <>
              {primary ? <Button size="xs" onClick={() => onTransition(row, primary)}>{primary.label}</Button> : null}
              {rest.length ? (
                <Floating align="end" width="14rem" label={"More actions for " + item.title}
                  trigger={<Button variant="ghost" size="icon-xs" aria-label={"More actions for " + item.title}><Icon name="ellipsis" size={14} /></Button>}>
                  {(close) => rest.map((t) => <MenuItem key={t.id} onClick={() => { close(); onTransition(row, t); }}>{t.label}</MenuItem>)}
                </Floating>
              ) : null}
            </>
          )}
        </div>
      </div>
    );
  }

  function Group({ title, note, rows, now, recessed, onTransition, onNavigate }) {
    if (!rows.length) return null;
    return (
      <section className="mq-section">
        <div className="mq-section__head">
          <h2 className="mq-h2">{title}<span className="mq-tab-count">{rows.length}</span></h2>
          <p className="mq-note">{note}</p>
        </div>
        <div className="mq-rows">
          {rows.map((row) => (
            <WorkRow key={row.item.id} row={row} now={now} recessed={recessed} onTransition={onTransition} onNavigate={onNavigate} />
          ))}
        </div>
      </section>
    );
  }

  function DeliverablesScreen({ data, now, onNavigate, onDeliverableTransition, pushToast }) {
    const rows = allDeliverables(data.deals, data.creators);
    const needsYou = byDue(deliverablesNeedingAttention(rows, now));
    const withBrand = byDue(deliverablesWithBrand(rows));
    const ahead = byDue(deliverablesAhead(rows, now));
    const live = deliverablesLive(rows).slice().sort((a, b) => b.item.publishedAt - a.item.publishedAt);
    const overdue = needsYou.filter((r) => DELIVERABLE_FLAGS.isOverdue(r.item, now));

    if (rows.length === 0) {
      return (
        <div className="mq-page">
          <PageHead path="/deliverables" title="Deliverables" />
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon"><Icon name="clapperboard" size={18} /></EmptyMedia>
              <EmptyTitle>Nothing is in flight</EmptyTitle>
              <EmptyDescription>Deliverables appear here the moment a deal is agreed, whether or not the contract has come back.</EmptyDescription>
            </EmptyHeader>
          </Empty>
        </div>
      );
    }

    return (
      <div className="mq-page">
        <PageHead
          path="/deliverables"
          title="In flight"
          lede={needsYou.length
            ? needsYou.length + " of " + rows.length + " deliverables need you this week — " + (overdue.length ? overdue.length + (overdue.length === 1 ? " past its due date, " : " past their due date, ") : "") + "the rest due inside " + AGENCY_POLICY.dueSoonDays + " days. " + withBrand.length + " " + (withBrand.length === 1 ? "is" : "are") + " with a brand and cannot move until they answer."
            : "Nothing needs you this week. " + withBrand.length + " with a brand, " + ahead.length + " scheduled ahead, " + live.length + " live."}
          actions={<ExportMenu scope="All deliverables" onExport={(format) => pushToast({ title: "Export started", description: "Deliverables · " + format + " — we'll email it to " + MARQUE_USER.name.split(" ")[0].toLowerCase() + "@marque.example" })} />}
        />

        <WeekLadder rows={rows} now={now} onNavigate={onNavigate} />

        <Group
          title="Needs you" rows={needsYou} now={now} onTransition={onDeliverableTransition} onNavigate={onNavigate}
          note="Back for revisions, past its due date, or due inside the week. This is the count on the Deliverables tab."
        />
        <Group
          title="With the brand" rows={withBrand} now={now} recessed onTransition={onDeliverableTransition} onNavigate={onNavigate}
          note="Submitted and waiting on an answer. Nothing here is late — we handed it over on time and the clock is theirs."
        />
        <Group
          title="Scheduled ahead" rows={ahead} now={now} onTransition={onDeliverableTransition} onNavigate={onNavigate}
          note={"Due more than " + AGENCY_POLICY.dueSoonDays + " days out. Chasing early is how a shoot day gets moved cheaply."}
        />
        <Group
          title="Live" rows={live} now={now} onTransition={onDeliverableTransition} onNavigate={onNavigate}
          note="Published and measured. Reach and saves come from the post, and feed the campaign's promise on Performance."
        />

        <div className="mq-foot">
          <span>{rows.length} deliverables across {data.deals.length} deals · {needsYou.length} need you, {withBrand.length} with a brand, {ahead.length} ahead, {live.length} live</span>
          <span>A due date reads as a countdown; the exact date is on hover.</span>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="DeliverableScreen.jsx">
/* Deliverable — one post or asset, as a record. The thing a row on the
   deliverables desk was always standing in for.
   Commit model: INSTANT, with the two multi-field transitions opening the
   staged dialog App.jsx owns (record revisions, mark published). Consequential
   moves apply here and raise an undo toast.
   Data state: populated. A deliverable always has a deal, a creator, a kind and
   a due date; the sections that depend on it being live say so in a sentence
   instead of rendering an empty second state.
   Actions derive from the record: the header carries exactly what
   deliverableTransitions() allows at this status, so a scheduled piece is
   offered submission and a published one is offered nothing.
   Primary display: a document — what was asked for, what happened to it, and
   what it did once it was live. */
(() => {
  const { Button, Badge, Separator, MenuItem, Alert, AlertTitle, AlertDescription } = window.OriginDesignSystem_7c998b;

  function HistoryStep({ entry, now }) {
    return (
      <div className="mq-tl__item">
        <span className="mq-tl__when" title={formatAbsolute(entry.at)}>{formatAbsolute(entry.at).split(",")[0]}</span>
        <div className="mq-tl__main">
          <span className="mq-tl__title">{entry.what}</span>
          <span className="mq-row__meta">{entry.who}{entry.note ? " — “" + entry.note + "”" : ""}</span>
        </div>
        <div className="mq-tl__side">
          <span className="mq-mono mq-muted" title={formatAbsolute(entry.at)}>{formatWhen(entry.at, now)}</span>
        </div>
      </div>
    );
  }

  function DeliverableScreen({ row, data, now, onNavigate, onDeliverableTransition, pushToast }) {
    const { item, deal, creator } = row;
    const brief = briefForDeal(data.briefs, deal);
    const brand = brandById(data.brands, deal.brandId);
    const transitions = deliverableTransitions(item);
    const primary = transitions.find((t) => t.primary);
    const rest = transitions.filter((t) => !t.primary);
    const siblings = siblingDeliverables(row, data.creators);
    const history = deliverableHistory(row);
    const live = DELIVERABLE_FLAGS.isLive(item);
    const share = postShareOfCampaign(row);
    const waiting = deliverableWaitingOn(row);
    const rightsEnd = RIGHTS_FLAGS.endsAt(deal);
    const line = brief ? brief.deliverables.find((l) => l.kind === item.kind) : null;

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/deliverables/:id" params={{ id: item.id }} labels={{ "/deliverables/:id": item.title }} />}
          id={item.id + " · " + (item.quantity > 1 ? item.quantity + " × " : "") + labelOf(DELIVERABLE_KINDS, item.kind)}
          status={<StatusBadge values={DELIVERABLE_STATUS} value={item.status} />}
          title={item.title}
          lede={creator.name + " for " + deal.brand + ", on " + deal.title + ". " + (waiting ? "Waiting on " + waiting + "." : live ? "Live since " + formatAbsolute(item.publishedAt).split(",")[0] + "." : "Ours to move.")}
          actions={
            <div className="mq-actions__row">
              {primary ? <Button size="sm" onClick={() => onDeliverableTransition(row, primary)}>{primary.label}</Button> : null}
              {rest.length ? (
                <Floating align="end" width="15rem" label="More actions"
                  trigger={<Button variant="outline" size="sm">More<Icon name="chevron-down" /></Button>}>
                  {(close) => rest.map((t) => <MenuItem key={t.id} onClick={() => { close(); onDeliverableTransition(row, t); }}>{t.label}</MenuItem>)}
                </Floating>
              ) : null}
            </div>
          }
        >
          <div className="mq-stack" style={{ marginTop: "var(--space-2)" }}>
            <Who creator={creator} size={28} onClick={() => onNavigate("/talent/" + creator.id)} />
            <DueClock item={item} now={now} />
          </div>
        </RecordHead>

        {DELIVERABLE_FLAGS.needsRevision(item) ? (
          <Alert variant="warning">
            <AlertTitle>{item.revisionBy} sent it back on {formatAbsolute(item.revisionAt).split(",")[0]}</AlertTitle>
            <AlertDescription>“{item.revisionNote}” — the revised cut is due {formatAbsolute(item.dueAt).split(",")[0]}, {formatCountdown(item.dueAt, now)}.</AlertDescription>
          </Alert>
        ) : null}

        <div className="mq-split">
          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">What was asked for</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                {brief ? (
                  <>
                    <p className="mq-lede" style={{ fontSize: "var(--text-base)" }}>{brief.ask}</p>
                    <dl className="mq-specs">
                      <dt>Brief</dt>
                      <dd><button type="button" className="mq-link" onClick={() => onNavigate("/briefs/" + brief.id)}>{brief.id} · {brief.title}</button> — {formatMoney(brief.budgetCents)} for {briefUnits(brief)} pieces</dd>
                      <dt>This piece</dt>
                      <dd>{item.quantity > 1 ? item.quantity + " × " : ""}{labelOf(DELIVERABLE_KINDS, item.kind)}{line ? ", one of the " + line.quantity + " " + labelOf(DELIVERABLE_KINDS, line.kind).toLowerCase() + (line.quantity === 1 ? "" : "s") + " they asked for" : ""}</dd>
                      <dt>Usage</dt>
                      <dd>{usageOf(deal.usage).label}{rightsEnd === null ? " — organic only, so nothing expires" : ", running to " + formatAbsolute(rightsEnd).split(",")[0] + " (" + formatCountdown(rightsEnd, now) + ")"}</dd>
                      <dt>Contact</dt>
                      <dd>{brief.contact.name}, {brief.contact.role.toLowerCase()} at <button type="button" className="mq-link" onClick={() => onNavigate("/brands/" + brand.id)}>{brand.name}</button></dd>
                    </dl>
                  </>
                ) : (
                  <p className="mq-note">This deal has no brief behind it on the desk, so the ask lives on the deal sheet.</p>
                )}
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">What has happened<span className="mq-tab-count">{history.length}</span></h2>
                <p className="mq-note">Read off the dates the record already carries, so nothing here is a story written beside them.</p>
              </div>
              <div className="mq-tl">
                {history.map((entry, i) => <HistoryStep key={entry.what + i} entry={entry} now={now} />)}
              </div>
              <div className="mq-foot">
                <span>{live ? "Live " + formatAbsolute(item.publishedAt).split(",")[0] : "Due " + formatAbsolute(item.dueAt).split(",")[0] + " — " + formatCountdown(item.dueAt, now)}</span>
                <span>{formatCountdown(item.dueAt, now).indexOf("over") > -1 && !DELIVERABLE_FLAGS.isDelivered(item) ? "Still ours to fix" : waiting ? "Waiting on " + waiting : "Nothing outstanding on this piece"}</span>
              </div>
            </section>

            {live ? (
              <section className="mq-section">
                <div className="mq-section__head">
                  <h2 className="mq-h2">What it did</h2>
                  <p className="mq-note">The platform's own numbers, entered when the post went live. They feed the campaign's promise, nothing else.</p>
                </div>
                <div className="mq-sheet mq-sheet--tight">
                  <div className="mq-figures">
                    <Figure label="Reach" value={formatFollowers(item.metrics.reach)} note={share === null ? null : formatPercent(share) + " of the " + formatFollowers(actualReach(deal)) + " this campaign has delivered"} />
                    <Figure label="Engagements" value={formatFollowers(item.metrics.engagements)} note={formatPercent(item.metrics.engagements / item.metrics.reach) + " of the accounts it reached"} />
                    <Figure label="Saves" value={formatFollowers(item.metrics.saves)} note={formatPercent(item.metrics.saves / item.metrics.engagements) + " of its engagements were saves"} />
                  </div>
                  <Separator />
                  <div className="mq-stack">
                    <span className="mq-mono mq-muted">{item.url}</span>
                    <Button variant="ghost" size="xs" onClick={() => onNavigate("/performance/" + deal.id)}>Open the wrap report<Icon name="arrow-right" /></Button>
                  </div>
                </div>
              </section>
            ) : null}
          </div>

          <div className="mq-col">
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">The deal</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-lg">
                  <LedgerRow label="Fee" hint={deal.id + " · agreed " + formatAbsolute(deal.agreedAt).split(",")[0]} value={formatMoney(deal.feeCents)} />
                  <LedgerRow label={"To " + creator.name.split(" ")[0]} hint={"After " + Math.round(deal.commissionRate * 100) + "% commission"} value={formatMoney(creatorNetOf(deal))} variant="net" />
                </div>
                <div className="mq-stack">
                  <StatusBadge values={CONTRACT_STATUS} value={deal.contract.status} />
                  <StatusBadge values={INVOICE_STATUS} value={deal.invoice.status} />
                  <StatusBadge values={PAYOUT_STATUS} value={deal.payout.status} />
                </div>
                <Button variant="ghost" size="sm" onClick={() => onNavigate("/deals/" + deal.id)}>Open the deal sheet<Icon name="arrow-right" /></Button>
              </div>
            </section>

            <section className="mq-section">
              <div className="mq-section__head">
                <h2 className="mq-h2">The rest of the campaign<span className="mq-tab-count">{siblings.length}</span></h2>
                <p className="mq-note">{publishedOf(deal).length} of {deal.deliverables.length} pieces are live. This one is marked.</p>
              </div>
              <div className="mq-rows">
                {siblings.map((sibling) => {
                  const current = sibling.item.id === item.id;
                  return (
                    <div className={"mq-row" + (DELIVERABLE_FLAGS.isWithBrand(sibling.item) ? " mq-row--recessed" : "")} key={sibling.item.id}>
                      <div className="mq-row__main">
                        <span className="mq-row__title">
                          {current
                            ? <span>{sibling.item.title} <Badge size="sm" variant="secondary">This piece</Badge></span>
                            : <button type="button" className="mq-link" onClick={() => onNavigate("/deliverables/" + sibling.item.id)}>{sibling.item.title}</button>}
                        </span>
                        <span className="mq-row__meta">{labelOf(DELIVERABLE_KINDS, sibling.item.kind)}{deliverableWaitingOn(sibling) ? " · waiting on " + deliverableWaitingOn(sibling) : ""}</span>
                      </div>
                      <div className="mq-row__side">
                        <StatusBadge values={DELIVERABLE_STATUS} value={sibling.item.status} />
                        <DueClock item={sibling.item} now={now} />
                      </div>
                    </div>
                  );
                })}
              </div>
              <div className="mq-foot">
                <span>Soonest outstanding {byDue(siblings.filter((s) => !DELIVERABLE_FLAGS.isDelivered(s.item))).length ? formatAbsolute(byDue(siblings.filter((s) => !DELIVERABLE_FLAGS.isDelivered(s.item)))[0].item.dueAt).split(",")[0] : "nothing"}</span>
                <Button variant="ghost" size="xs" onClick={() => onNavigate("/deliverables")}>The whole desk<Icon name="arrow-right" /></Button>
              </div>
            </section>
          </div>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="PerformanceScreen.jsx">
/* Performance — what went live, measured against what we promised in the pitch.
   Commit model: INSTANT. Nothing on this screen commits anything; it is a
   reading surface. Figures move when a deliverable is marked published on the
   deliverables desk, because they are computed from the posts.
   Data state: populated. A campaign with nothing live is not an empty state —
   it is a campaign whose figure is suppressed, and it says so in words.
   Scope: stated in words rather than filtered. The agency figure is over
   completed campaigns only; the wall shows every campaign with at least one
   post live. There is no period control, so nothing here moves under you.
   Primary display: the promise wall — one row per campaign, the promise a
   fixed vertical rule at the same place on every track, so over and under are
   read as position rather than arithmetic. */
(() => {
  const { Button, Badge, Separator } = window.OriginDesignSystem_7c998b;

  /* The wall sits at the same place on every track, so the bars are comparable
     across campaigns of very different sizes. A bar can run to the end of the
     track, which is 143% of promise; anything beyond that is stated in the
     figure rather than drawn. */
  const WALL = 70;
  const TRACK_CAP = 100 / (WALL / 100);

  function PromiseRow({ deal, creator, now, onNavigate }) {
    const state = campaignState(deal);
    const index = deliveryIndex(deal);
    const share = (actualReach(deal) / deal.promise.reach) * WALL;
    const width = Math.min(100, share);
    const under = index !== null && index < 1;
    return (
      <div className="mq-prow">
        <div className="mq-who">
          <Portrait creator={creator} size={40} />
          <span className="mq-who__text">
            <button type="button" className="mq-link" onClick={() => onNavigate("/performance/" + deal.id)}>{deal.brand}</button>
            <span className="mq-who__meta">{creator.name} · {publishedOf(deal).length} of {deal.deliverables.length} live</span>
          </span>
        </div>
        <div className="mq-prow__track">
          <div className="mq-ptrack">
            <span className="mq-ptrack__fill" data-under={under ? "yes" : "no"} data-flight={state === "in_flight" ? "yes" : "no"} style={{ width: width.toFixed(1) + "%" }} />
            <span className="mq-ptrack__wall" style={{ insetInlineStart: WALL + "%" }} />
            <span className="mq-ptrack__walllabel" style={{ insetInlineStart: WALL + "%" }}>promised {formatFollowers(deal.promise.reach)}</span>
          </div>
        </div>
        <div>
          <div className={"mq-pindex" + (index === null ? " mq-muted" : under ? " mq-warn" : " mq-good")} style={index === null ? { fontSize: "var(--text-sm)" } : null}>
            {index === null ? (state === "in_flight" ? "In flight" : "Not yet live") : formatPercent(index)}
          </div>
          <div className="mq-figure__note" style={{ textAlign: "right" }}>{formatFollowers(actualReach(deal))} reached</div>
        </div>
      </div>
    );
  }

  function PerformanceScreen({ data, now, onNavigate, pushToast }) {
    const measurable = measurableDeals(data.deals);
    const complete = completeDeals(data.deals);
    const notStarted = data.deals.filter((d) => campaignState(d) === "not_started");
    const kept = promiseKeptRate(data.deals);
    const liveRows = deliverablesLive(allDeliverables(data.deals, data.creators)).slice().sort((a, b) => b.item.metrics.reach - a.item.metrics.reach);
    const reachTotal = sumOf(liveRows, (r) => r.item.metrics.reach);
    const savesTotal = sumOf(liveRows, (r) => r.item.metrics.saves);
    const engagementsTotal = sumOf(liveRows, (r) => r.item.metrics.engagements);
    const missed = complete.filter((d) => deliveryIndex(d) < 1);

    return (
      <div className="mq-page">
        <PageHead
          path="/performance"
          title="Against the promise"
          lede={kept === null
            ? "No campaign has finished yet, so there is nothing to judge."
            : "Across the " + complete.length + " campaigns that have finished, we delivered " + formatPercent(kept) + " of the reach we promised" + (missed.length ? " — and " + missed.map((d) => d.brand).join(", ") + " came in under, which is the conversation to have before the renewal." : ".") + " Campaigns still in flight are not counted: half a campaign is not underdelivery."}
          actions={<ExportMenu scope="Campaign performance" onExport={(format) => pushToast({ title: "Export started", description: "Performance · " + format + " — we'll email it to " + MARQUE_USER.name.split(" ")[0].toLowerCase() + "@marque.example" })} />}
        />

        <div className="mq-bleed">
          <div className="mq-figures">
            <Figure size="xl" label="Promise kept" value={kept === null ? "—" : formatPercent(kept)} note={"Reach delivered against reach promised, over the " + complete.length + " finished campaigns"} />
            <Figure size="xl" label="Reach delivered" value={formatFollowers(reachTotal)} note={liveRows.length + " posts live across " + measurable.length + " campaigns"} />
            <Figure size="xl" label="Engagement" value={formatPercent(reachTotal ? engagementsTotal / reachTotal : null)} note={formatCount(engagementsTotal) + " engagements on the same " + liveRows.length + " posts"} />
            <Figure size="xl" label="Saves" value={formatCount(savesTotal)} note="The number brands ask about second, and the one creators are proudest of" />
          </div>
        </div>

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">The promise wall<span className="mq-tab-count">{measurable.length}</span></h2>
            <p className="mq-note">Every campaign with at least one post live. The rule is the reach we promised in the pitch; the bar is what the posts actually did. A bar reaching the end of its track is {Math.round(TRACK_CAP)}% of promise — past that the figure carries it.</p>
          </div>
          <div className="mq-promise">
            {measurable
              .slice()
              .sort((a, b) => (deliveryIndex(a) === null ? -1 : deliveryIndex(b) === null ? 1 : deliveryIndex(a) - deliveryIndex(b)))
              .map((deal) => (
                <PromiseRow key={deal.id} deal={deal} creator={creatorById(data.creators, deal.creatorId)} now={now} onNavigate={onNavigate} />
              ))}
          </div>
          {notStarted.length ? (
            <p className="mq-note">
              {notStarted.map((d) => d.brand + " × " + creatorById(data.creators, d.creatorId).name.split(" ")[0]).join(", ")} {notStarted.length === 1 ? "has" : "have"} nothing live yet, so {notStarted.length === 1 ? "it is" : "they are"} not on the wall. {notStarted.length === 1 ? "Its" : "Their"} first post is due {formatAbsolute(byDue(notStarted[0].deliverables.map((i) => ({ item: i })))[0].item.dueAt).split(",")[0]}.
            </p>
          ) : null}
        </section>

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">Every post live<span className="mq-tab-count">{liveRows.length}</span></h2>
            <p className="mq-note">Sorted by reach. These are the rows every figure above is summed from.</p>
          </div>
          <div className="mq-posts">
            {liveRows.map((row) => (
              <article className="mq-post" key={row.item.id}>
                <div className="mq-col mq-col--tight">
                  <Eyebrow>{row.deal.brand} · {formatAbsolute(row.item.publishedAt).split(",")[0]}</Eyebrow>
                  <button type="button" className="mq-link mq-row__title" onClick={() => onNavigate("/deliverables/" + row.item.id)}>{row.item.title}</button>
                  <span className="mq-who__meta">{row.creator.name} · {labelOf(DELIVERABLE_KINDS, row.item.kind)}</span>
                </div>
                <div className="mq-post__metrics">
                  <Figure size="sm" label="Reach" value={formatFollowers(row.item.metrics.reach)} />
                  <Figure size="sm" label="Engaged" value={formatPercent(row.item.metrics.engagements / row.item.metrics.reach)} />
                  <Figure size="sm" label="Saves" value={formatCount(row.item.metrics.saves)} />
                </div>
                <span className="mq-mono mq-muted">{row.item.url}</span>
              </article>
            ))}
          </div>
        </section>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="WrapScreen.jsx">
/* Wrap report — one campaign against the promise we made to win it. The
   artifact that gets sent to the brand, and the thing to read before a
   renewal conversation.
   Commit model: INSTANT. Nothing commits: marking a post live belongs to the
   deliverables desk, where the numbers are entered.
   Data state: one branch, chosen by campaignState() — a finished campaign is
   measured, an in-flight one states what is live so far and says plainly that
   it is not measurable yet, and a campaign with nothing live is neither.
   Suppression, not zeroing: deliveryIndex() returns null until every piece is
   live, so this screen prints a word where a percentage would be a lie.
   Primary display: the promise wall for one campaign — the delivered bar
   against the promised wall — over the posts that built it. */
(() => {
  const { Button, Badge, Separator, Alert, AlertTitle, AlertDescription, Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell } = window.OriginDesignSystem_7c998b;

  const WALL = 72; /* Where the promised line sits on the track, as on the wall. */

  function WrapScreen({ deal, data, now, onNavigate, pushToast }) {
    const creator = creatorById(data.creators, deal.creatorId);
    const brand = brandById(data.brands, deal.brandId);
    const brief = briefForDeal(data.briefs, deal);
    const state = campaignState(deal);
    const index = deliveryIndex(deal);
    const engIndex = engagementIndex(deal);
    const live = publishedOf(deal).slice().sort((a, b) => b.metrics.reach - a.metrics.reach);
    const rows = allDeliverables([deal], data.creators);
    const outstanding = byDue(rows.filter((r) => !DELIVERABLE_FLAGS.isLive(r.item)));
    const reach = actualReach(deal);
    const rightsEnd = RIGHTS_FLAGS.endsAt(deal);
    const expiring = RIGHTS_FLAGS.isExpiring(deal, now);
    const share = Math.min(100, (reach / deal.promise.reach) * WALL);
    const under = index !== null && index < 1;

    return (
      <div className="mq-page mq-page--tight">
        <RecordHead
          crumb={<Crumbs path="/performance/:id" params={{ id: deal.id }} labels={{ "/performance/:id": deal.brand + " · " + deal.title }} />}
          id={deal.id + " · " + formatAbsolute(deal.agreedAt).split(",")[0] + " · " + usageOf(deal.usage).label.toLowerCase()}
          status={<StatusBadge values={CONTRACT_STATUS} value={deal.contract.status} />}
          title={deal.brand + " · " + deal.title}
          lede={index === null
            ? state === "not_started"
              ? "Nothing is live yet, so there is nothing to measure. " + rows.length + " pieces are booked and the first is due " + formatAbsolute(outstanding[0].item.dueAt).split(",")[0] + "."
              : live.length + " of " + rows.length + " pieces are live and have reached " + formatFollowers(reach) + " against a promise of " + formatFollowers(deal.promise.reach) + ". The campaign is not measured until all of it is out — half a campaign is not underdelivery."
            : "Every piece is live. We reached " + formatFollowers(reach) + " against the " + formatFollowers(deal.promise.reach) + " we promised — " + formatPercent(index) + " of it."}
          actions={
            <div className="mq-actions__row">
              <Button size="sm" variant="outline" onClick={() => onNavigate("/brands/" + brand.id)}>{brand.name}<Icon name="arrow-right" /></Button>
              <ExportMenu scope={deal.brand + " wrap report"} onExport={(format) => pushToast({ title: "Export started", description: deal.brand + " · " + format + " — the promise, the delivery and every post behind it." })} />
            </div>
          }
        >
          <div className="mq-stack" style={{ marginTop: "var(--space-2)" }}>
            <Who creator={creator} size={28} onClick={() => onNavigate("/talent/" + creator.id)} />
            {brief ? <Badge size="sm" variant="secondary">{labelOf(CATEGORIES, brief.category)}</Badge> : null}
            <span className="mq-mono mq-muted">{formatMoney(deal.feeCents)} fee</span>
          </div>
        </RecordHead>

        {expiring ? (
          <Alert variant="warning">
            <AlertTitle>{brand.name}'s usage rights end on {formatAbsolute(rightsEnd).split(",")[0]}</AlertTitle>
            <AlertDescription>
              {usageOf(deal.usage).label} for {usageOf(deal.usage).months} months from {formatAbsolute(deal.usageStartAt).split(",")[0]} — {formatCountdown(rightsEnd, now)}. Renew the window with them or the assets have to come down, and a renewal is priced at {Math.round(usageOf(deal.usage).uplift * 100)}% on top of the organic fee.
            </AlertDescription>
          </Alert>
        ) : null}

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">Against the promise</h2>
            <p className="mq-note">The promise is what we told {brand.name} in the pitch on {brief && brief.pitchedAt ? formatAbsolute(brief.pitchedAt).split(",")[0] : formatAbsolute(deal.agreedAt).split(",")[0]}. The bar is what the live posts have actually done.</p>
          </div>
          <div className="mq-prow">
            <div className="mq-who">
              <Portrait creator={creator} size={40} />
              <span className="mq-who__text">
                <span className="mq-who__name">{formatFollowers(reach)} reached</span>
                <span className="mq-who__meta">{live.length} of {rows.length} pieces live</span>
              </span>
            </div>
            <div className="mq-prow__track">
              <div className="mq-ptrack">
                <span className="mq-ptrack__fill" data-under={under ? "yes" : "no"} data-flight={state === "in_flight" ? "yes" : "no"} style={{ width: share.toFixed(1) + "%" }} />
                <span className="mq-ptrack__wall" style={{ insetInlineStart: WALL + "%" }} />
                <span className="mq-ptrack__walllabel" style={{ insetInlineStart: WALL + "%" }}>promised {formatFollowers(deal.promise.reach)}</span>
              </div>
            </div>
            <div>
              <div className={"mq-pindex" + (index === null ? " mq-muted" : under ? " mq-warn" : " mq-good")} style={index === null ? { fontSize: "var(--text-sm)" } : null}>
                {index === null ? (state === "in_flight" ? "In flight" : "Not yet live") : formatPercent(index)}
              </div>
              <div className="mq-figure__note" style={{ textAlign: "right" }}>{index === null ? "not measurable yet" : "of the reach promised"}</div>
            </div>
          </div>
          <div className="mq-figures">
            <Figure label="Engagement rate" value={actualEngagementRate(deal) === null ? "—" : formatPercent(actualEngagementRate(deal))} note={engIndex === null ? "Promised " + formatPercent(deal.promise.engagementRate) + " — measured once every piece is live" : formatPercent(engIndex) + " of the " + formatPercent(deal.promise.engagementRate) + " promised"} />
            <Figure label="Engagements" value={formatFollowers(actualEngagements(deal))} note={"Across " + live.length + " live " + (live.length === 1 ? "piece" : "pieces")} />
            <Figure label="Saves" value={formatFollowers(actualSaves(deal))} note={actualEngagements(deal) ? formatPercent(actualSaves(deal) / actualEngagements(deal)) + " of engagements — the number that predicts a renewal" : "Nothing live yet"} />
            <Figure label="Cost per thousand reached" value={reach ? formatMoney(Math.round(deal.feeCents / (reach / 1000))) : "—"} note={reach ? "On the " + formatMoney(deal.feeCents) + " fee, at the reach delivered so far" : "No reach yet, so no cost to divide"} />
          </div>
        </section>

        <section className="mq-section">
          <div className="mq-section__head">
            <h2 className="mq-h2">The posts<span className="mq-tab-count">{live.length}</span></h2>
            <p className="mq-note">{live.length ? "Ranked by reach. Every figure is the platform's own, entered when the post went live." : "Nothing is live, so there is nothing to rank."}</p>
          </div>
          {live.length ? (
            <div className="mq-table-wrap">
              <Table className="mq-tbl">
                <TableHeader>
                  <TableRow>
                    <TableHead>Post</TableHead>
                    <TableHead className="mq-drop-lg">Live</TableHead>
                    <TableHead className="mq-num">Reach</TableHead>
                    <TableHead className="mq-num">Engagements</TableHead>
                    <TableHead className="mq-num mq-drop-md">Saves</TableHead>
                    <TableHead className="mq-num">Share of reach</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {live.map((item) => (
                    <TableRow key={item.id}>
                      <TableCell>
                        <span className="mq-cellstack">
                          <button type="button" className="mq-link" onClick={() => onNavigate("/deliverables/" + item.id)}>{item.title}</button>
                          <span className="mq-row__meta">{labelOf(DELIVERABLE_KINDS, item.kind)} · {item.url}</span>
                        </span>
                      </TableCell>
                      <TableCell className="mq-drop-lg mq-muted"><span className="mq-mono" title={formatAbsolute(item.publishedAt)}>{formatAbsolute(item.publishedAt).split(",")[0]}</span></TableCell>
                      <TableCell className="mq-num">{formatFollowers(item.metrics.reach)}</TableCell>
                      <TableCell className="mq-num">{formatFollowers(item.metrics.engagements)}</TableCell>
                      <TableCell className="mq-num mq-drop-md mq-muted">{formatFollowers(item.metrics.saves)}</TableCell>
                      <TableCell className="mq-num">{formatPercent(item.metrics.reach / reach)}</TableCell>
                    </TableRow>
                  ))}
                </TableBody>
                <TableFooter>
                  <TableRow>
                    <TableCell>{live.length} live of {rows.length} booked</TableCell>
                    <TableCell className="mq-drop-lg" />
                    <TableCell className="mq-num">{formatFollowers(reach)}</TableCell>
                    <TableCell className="mq-num">{formatFollowers(actualEngagements(deal))}</TableCell>
                    <TableCell className="mq-num mq-drop-md">{formatFollowers(actualSaves(deal))}</TableCell>
                    <TableCell className="mq-num">100%</TableCell>
                  </TableRow>
                </TableFooter>
              </Table>
            </div>
          ) : null}
          {outstanding.length ? (
            <div className="mq-foot">
              <span>{outstanding.length} {outstanding.length === 1 ? "piece is" : "pieces are"} still to come — soonest {formatAbsolute(outstanding[0].item.dueAt).split(",")[0]}, {formatCountdown(outstanding[0].item.dueAt, now)}{deliverableWaitingOn(outstanding[0]) ? ", waiting on " + deliverableWaitingOn(outstanding[0]) : ""}</span>
              <Button variant="ghost" size="xs" onClick={() => onNavigate("/deliverables/" + outstanding[0].item.id)}>Open the next piece<Icon name="arrow-right" /></Button>
            </div>
          ) : (
            <div className="mq-foot">
              <span>Every booked piece is live, which is why this campaign can be measured at all.</span>
              <Button variant="ghost" size="xs" onClick={() => onNavigate("/performance")}>The whole promise wall<Icon name="arrow-right" /></Button>
            </div>
          )}
        </section>

        <section className="mq-section">
          <div className="mq-section__head"><h2 className="mq-h2">What to say at the renewal</h2></div>
          <div className="mq-sheet mq-sheet--tight">
            <p className="mq-lede" style={{ fontSize: "var(--text-base)" }}>
              {index === null
                ? "Nothing yet. The conversation is worth having once every piece is out — until then any number quoted at " + brand.name + " is a number we would have to revise."
                : under
                  ? "We came in at " + formatPercent(index) + " of the reach we promised, " + formatFollowers(deal.promise.reach - reach) + " short. Lead with the saves — " + formatPercent(actualSaves(deal) / actualEngagements(deal)) + " of engagements — and go into the next brief with a promise built on what " + creator.name.split(" ")[0] + " actually delivered here rather than on their follower count."
                  : "We came in at " + formatPercent(index) + " of promise, " + formatFollowers(reach - deal.promise.reach) + " over. That is the number to open with, and it is the argument for holding the rate on the next brief from them."}
            </p>
            <Separator />
            <dl className="mq-specs">
              <dt>Promised</dt>
              <dd>{formatFollowers(deal.promise.reach)} reach at {formatPercent(deal.promise.engagementRate)} engagement</dd>
              <dt>Delivered</dt>
              <dd>{formatFollowers(reach)} reach{actualEngagementRate(deal) === null ? "" : " at " + formatPercent(actualEngagementRate(deal)) + " engagement"}{index === null ? " so far" : ""}</dd>
              <dt>Rights</dt>
              <dd>{rightsEnd === null ? "Organic only — nothing to renew and nothing to take down." : usageOf(deal.usage).label + " to " + formatAbsolute(rightsEnd).split(",")[0] + " (" + formatCountdown(rightsEnd, now) + ")"}</dd>
              <dt>Their record</dt>
              <dd><button type="button" className="mq-link" onClick={() => onNavigate("/brands/" + brand.id)}>{brand.name}</button> — {brandDaysToPay(data.deals, brand) === null ? "no settled invoice yet" : brandDaysToPay(data.deals, brand).days + " days to pay on average"}</dd>
            </dl>
          </div>
        </section>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="MoneyScreen.jsx">
/* Money — what the brands owe us on the left, what we owe the creators on the
   right, and the commission that is the difference between them.
   Commit model: INSTANT. Issuing, chasing and recording a payment apply
   immediately and raise an undo toast. Releasing a payout is money leaving the
   account and cannot be recalled, so it opens a confirm dialog. No Save button.
   Data state: populated. Every deal carries an invoice and a payout from the
   moment it is agreed, so neither ledger is ever empty while a deal exists.
   Scope: every deal, all time. No period control, so no figure moves under you.
   Recession: a payout waiting on a brand is recessed and names the brand that
   is holding it — that is the whole point of putting the two ledgers side by
   side. A paid row is not recessed: finished is not blocked.
   Primary display: two facing ledgers rather than two tables, because the fact
   that matters is which side is blocking the other, and a join would hide it. */
(() => {
  const { Button, MenuItem, Alert, AlertTitle, AlertDescription, AlertAction } = window.OriginDesignSystem_7c998b;

  function MoneyRow({ title, meta, note, amount, status, clock, transitions, recessed, onTransition, onNavigate, dealId }) {
    const primary = transitions.find((t) => t.primary);
    const rest = transitions.filter((t) => !t.primary);
    return (
      <div className={"mq-row" + (recessed ? " mq-row--recessed" : "")}>
        <div className="mq-row__main" style={{ gridColumn: "1 / 3" }}>
          <span className="mq-row__title">
            <button type="button" className="mq-link" onClick={() => onNavigate("/deals/" + dealId)}>{title}</button>
          </span>
          <span className="mq-row__meta">{meta}</span>
          {note ? <span className="mq-row__meta">{note}</span> : null}
        </div>
        <div className="mq-row__side">
          <span className="mq-cellstack mq-cellstack--end">
            <span className="mq-amount">{amount}</span>
            {clock}
          </span>
          {status}
          {transitions.length === 0 ? null : recessed ? (
            <Floating align="end" width="14rem" label="Actions"
              trigger={<Button variant="ghost" size="icon-xs" aria-label={"Actions for " + title}><Icon name="ellipsis" size={14} /></Button>}>
              {(close) => transitions.map((t) => <MenuItem key={t.id} onClick={() => { close(); onTransition(t); }}>{t.label}</MenuItem>)}
            </Floating>
          ) : (
            <>
              {primary ? <Button size="xs" variant={primary.kind === "confirm" ? "default" : "outline"} onClick={() => onTransition(primary)}>{primary.label}</Button> : null}
              {rest.length ? (
                <Floating align="end" width="14rem" label="More actions"
                  trigger={<Button variant="ghost" size="icon-xs" aria-label={"More actions for " + title}><Icon name="ellipsis" size={14} /></Button>}>
                  {(close) => rest.map((t) => <MenuItem key={t.id} onClick={() => { close(); onTransition(t); }}>{t.label}</MenuItem>)}
                </Floating>
              ) : null}
            </>
          )}
        </div>
      </div>
    );
  }

  function MoneyScreen({ data, now, onNavigate, onInvoiceTransition, onPayoutTransition, pushToast }) {
    const invoices = invoiceRows(data.deals);
    const payouts = payoutRows(data.deals, data.creators);
    const awaitingBrand = invoicesAwaitingBrand(invoices);
    const overdue = invoicesOverdue(invoices, now);
    const toIssue = invoicesToIssue(invoices);
    const owed = payoutsOwed(payouts);
    const releasable = payoutsReleasable(payouts);
    const blocked = payoutsBlocked(payouts);
    /* Of the blocked payouts, the ones a past-due invoice is actually holding —
       the rest are waiting on an invoice that is not late, or not yet issued. */
    const blockedByOverdue = blocked.filter((r) => INVOICE_FLAGS.isOverdue(r.deal.invoice, now));
    const banked = commissionBanked(invoices);
    const pending = commissionPending(invoices);
    const run = nextPayoutRun(now);

    const orderInvoices = invoices.slice().sort((a, b) => {
      const rank = (r) => (INVOICE_FLAGS.isOverdue(r.invoice, now) ? 0 : r.invoice.status === "issued" ? 1 : r.invoice.status === "draft" ? 2 : 3);
      return rank(a) - rank(b) || (b.invoice.issuedAt || 0) - (a.invoice.issuedAt || 0);
    });
    const orderPayouts = payouts.slice().sort((a, b) => {
      const rank = (r) => (r.payout.status === "due" ? 0 : r.payout.status === "awaiting_funds" ? 1 : 2);
      return rank(a) - rank(b) || b.amount - a.amount;
    });

    return (
      <div className="mq-page">
        <PageHead
          path="/money"
          title="Money"
          lede={overdue.length
            ? formatMoney(moneyTotal(overdue)) + " is past due from " + overdue.map((r) => r.deal.brand).join(", ") + ", and it is the reason " + blockedByOverdue.map((r) => r.creator.name.split(" ")[0] + "'s " + formatMoney(r.amount)).join(" and ") + " cannot move. " + releasable.length + " payout" + (releasable.length === 1 ? " is" : "s are") + " funded and ready for the run on " + formatAbsolute(run).split(",")[0] + "."
            : "Nothing is past due. " + formatMoney(moneyTotal(awaitingBrand)) + " is with brands and " + formatMoney(moneyTotal(owed)) + " is owed to creators."}
          actions={<ExportMenu scope="Invoices and payouts" onExport={(format) => pushToast({ title: "Export started", description: "Money · " + format + " — we'll email it to " + MARQUE_FINANCE.name.split(" ")[0].toLowerCase() + "@marque.example" })} />}
        />

        {overdue.length ? (
          <Alert variant="error">
            <AlertTitle>{overdue.length === 1 ? overdue[0].deal.brand + " is " + INVOICE_FLAGS.daysOverdue(overdue[0].invoice, now) + " days past due" : overdue.length + " invoices are past due"}</AlertTitle>
            <AlertDescription>
              {overdue.map((r) => {
                const payout = payouts.find((p) => p.deal.id === r.deal.id);
                return (
                  <span key={r.deal.id}>
                    {formatMoney(r.amount)} on {r.invoice.id}, due {formatAbsolute(INVOICE_FLAGS.dueAt(r.invoice)).split(",")[0]}
                    {r.invoice.chasedAt ? ", chased " + formatAbsolute(r.invoice.chasedAt).split(",")[0] : ", not chased yet"}.
                    {" "}{payout.creator.name} is owed {formatMoney(payout.amount)} behind it.{" "}
                  </span>
                );
              })}
            </AlertDescription>
          </Alert>
        ) : null}

        <div className="mq-bleed">
          <div className="mq-figures">
            <Figure size="xl" label="Owed by brands" value={formatMoney(moneyTotal(awaitingBrand))} note={awaitingBrand.length + (awaitingBrand.length === 1 ? " invoice issued and unpaid, of which " : " invoices issued and unpaid, of which ") + formatMoney(moneyTotal(overdue)) + " is past due"} />
            <Figure size="xl" label="Owed to creators" value={formatMoney(moneyTotal(owed))} note={releasable.length + " funded and ready, " + formatMoney(moneyTotal(blocked)) + " held behind a brand"} />
            <Figure size="xl" label="Commission banked" value={formatMoney(banked)} note={formatMoney(pending) + " more sits on invoices nobody has paid yet"} />
            {toIssue.length ? (
              <Figure size="xl" label="Not yet invoiced" value={formatMoney(moneyTotal(toIssue))} note={toIssue.map((r) => r.deal.brand + " — " + r.invoice.holdReason.toLowerCase()).join(" · ")} />
            ) : null}
          </div>
        </div>

        <div className="mq-facing">
          <section className="mq-section">
            <div className="mq-ledgerhead">
              <div className="mq-col mq-col--tight">
                <Eyebrow>In</Eyebrow>
                <h2 className="mq-h2">What the brands owe us</h2>
              </div>
              <span className="mq-note">{invoices.length} invoices · overdue first</span>
            </div>
            <div className="mq-rows">
              {orderInvoices.map((row) => {
                const isOverdue = INVOICE_FLAGS.isOverdue(row.invoice, now);
                return (
                  <MoneyRow
                    key={row.invoice.id}
                    dealId={row.deal.id}
                    title={row.deal.brand}
                    meta={row.invoice.id + " · " + row.deal.title + " · " + creatorById(data.creators, row.deal.creatorId).name}
                    note={row.invoice.status === "draft"
                      ? row.invoice.holdReason
                      : row.invoice.status === "paid"
                        ? "Paid " + formatAbsolute(row.invoice.paidAt).split(",")[0] + " · commission " + formatMoney(commissionOf(row.deal)) + " banked"
                        : "Issued " + formatAbsolute(row.invoice.issuedAt).split(",")[0] + ", net " + AGENCY_POLICY.paymentTermsDays + (row.invoice.chasedAt ? " · chased " + formatAbsolute(row.invoice.chasedAt).split(",")[0] : "")}
                    amount={formatMoney(row.amount)}
                    status={<StatusBadge values={INVOICE_STATUS} value={row.invoice.status} />}
                    clock={row.invoice.status === "issued"
                      ? <span className={"mq-mono " + (isOverdue ? "mq-bad" : "mq-muted")} title={formatAbsolute(INVOICE_FLAGS.dueAt(row.invoice))}>{formatCountdown(INVOICE_FLAGS.dueAt(row.invoice), now)}</span>
                      : null}
                    transitions={invoiceTransitions(row.invoice)}
                    onTransition={(t) => onInvoiceTransition(row, t)}
                    onNavigate={onNavigate}
                  />
                );
              })}
            </div>
            <div className="mq-foot">
              <span>{formatMoney(moneyTotal(awaitingBrand))} issued and unpaid · {formatMoney(moneyTotal(toIssue))} not yet issued · {formatMoney(moneyTotal(invoicesPaid(invoices)))} banked</span>
            </div>
          </section>

          <div className="mq-spine" aria-hidden="true" />

          <section className="mq-section">
            <div className="mq-ledgerhead">
              <div className="mq-col mq-col--tight">
                <Eyebrow>Out</Eyebrow>
                <h2 className="mq-h2">What we owe the creators</h2>
              </div>
              <span className="mq-note">Net of commission · ready first</span>
            </div>
            <div className="mq-rows">
              {orderPayouts.map((row) => (
                <MoneyRow
                  key={row.payout.id}
                  dealId={row.deal.id}
                  title={row.creator.name}
                  meta={row.payout.id + " · " + row.deal.brand + " · " + formatMoney(row.deal.feeCents) + " less " + Math.round(row.deal.commissionRate * 100) + "%"}
                  note={row.payout.status === "awaiting_funds"
                    ? "Held until " + row.deal.brand + " pay " + row.deal.invoice.id + (INVOICE_FLAGS.isOverdue(row.deal.invoice, now) ? " — " + INVOICE_FLAGS.daysOverdue(row.deal.invoice, now) + " days past due" : row.deal.invoice.status === "draft" ? " — not issued yet" : "")
                    : row.payout.status === "paid"
                      ? "Released " + formatAbsolute(row.payout.paidAt).split(",")[0] + " · " + row.payout.reference
                      : row.deal.brand + " paid " + formatAbsolute(row.deal.invoice.paidAt).split(",")[0] + ". Next run is " + formatAbsolute(run).split(",")[0] + "."}
                  amount={formatMoney(row.amount)}
                  status={<StatusBadge values={PAYOUT_STATUS} value={row.payout.status} />}
                  transitions={payoutTransitions(row.payout)}
                  recessed={PAYOUT_FLAGS.isBlocked(row.payout)}
                  onTransition={(t) => onPayoutTransition(row, t)}
                  onNavigate={onNavigate}
                />
              ))}
            </div>
            <div className="mq-foot">
              <span>{formatMoney(moneyTotal(releasable))} ready to release · {formatMoney(moneyTotal(blocked))} held behind a brand · {formatMoney(moneyTotal(payouts.filter((r) => PAYOUT_FLAGS.isSettled(r.payout))))} paid</span>
            </div>
          </section>
        </div>
      </div>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="InboxScreen.jsx">
/* Inbox — the notifications panel as a screen, for when four lines in a popover
   are not enough to work from.
   Commit model: INSTANT. Marking something read is harmless and applies as
   clicked, so there is no Save and no undo toast — reading is not a change to
   the book.
   Data state: populated. Unread and everything are two tabs over one list, not
   two states: the tabs narrow the list only and no figure above them moves.
   Actions derive from the record: every line opens the record it is about, so
   the row's link is the action; a line already read carries no Mark read.
   Primary display: a list grouped by day, because the only ordering that helps
   is the one that answers "what happened while I was out". */
(() => {
  const {
    Button, Badge, Tabs, TabsList, TabsTab,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  const startOfDay = (ts) => { const d = new Date(ts); d.setUTCHours(0, 0, 0, 0); return d.getTime(); };

  const INBOX_TABS = [
    { value: "unread", label: "Unread" },
    { value: "all", label: "Everything" },
  ];

  function InboxScreen({ notifications, readIds, data, now, onNavigate, onRead, onReadAll }) {
    const [tab, setTab] = React.useState("unread");
    const isRead = (n) => readIds.indexOf(n.id) > -1;
    const unread = notifications.filter((n) => !isRead(n));
    const ordered = notifications.slice().sort((a, b) => b.at - a.at);
    const rows = tab === "unread" ? ordered.filter((n) => !isRead(n)) : ordered;
    const days = rows.map((n) => startOfDay(n.at)).filter((d, i, all) => all.indexOf(d) === i);
    const oldest = ordered[ordered.length - 1];

    return (
      <div className="mq-page mq-page--tight">
        <PageHead
          path="/inbox"
          title="Inbox"
          lede={unread.length
            ? unread.length + " of " + notifications.length + " unread. Every line is about a record on the desk and opens it — nothing here is a message you have to answer somewhere else."
            : "Nothing unread. All " + notifications.length + " lines since " + formatAbsolute(oldest.at).split(",")[0] + " are still here, newest first."}
          actions={unread.length ? <Button size="sm" variant="outline" onClick={onReadAll}>Mark all read</Button> : null}
        />

        <section className="mq-section">
          <div className="mq-filters">
            <Tabs value={tab} onValueChange={setTab}>
              <TabsList variant="underline" size="sm">
                {INBOX_TABS.map((t) => (
                  <TabsTab key={t.value} value={t.value} size="sm">
                    {t.label}<span className="mq-tab-count">{t.value === "unread" ? unread.length : notifications.length}</span>
                  </TabsTab>
                ))}
              </TabsList>
            </Tabs>
            <span className="mq-note">Read against unread is the only distinction here; the urgency lives on the record each line points at.</span>
          </div>

          {rows.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="inbox" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing unread</EmptyTitle>
                <EmptyDescription>All {notifications.length} lines have been read. They are kept, so nothing that happened on the desk disappears when you clear it.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => setTab("all")}>Show everything</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="mq-col">
              {days.map((day) => {
                const onDay = rows.filter((n) => startOfDay(n.at) === day);
                return (
                  <div className="mq-col mq-col--tight" key={day}>
                    <Eyebrow>{formatAbsolute(day).split(",")[0]}{day === startOfDay(now) ? " · today" : ""} · {onDay.length} {onDay.length === 1 ? "line" : "lines"}</Eyebrow>
                    <div className="mq-rows">
                      {onDay.map((n) => {
                        const read = isRead(n);
                        const route = matchRoute(n.route, MARQUE_ROUTES);
                        return (
                          <div className={"mq-row" + (read ? " mq-row--recessed" : "")} key={n.id}>
                            <div className="mq-row__main mq-row__wide">
                              <span className="mq-row__title">
                                <button type="button" className="mq-link" onClick={() => { onRead(n.id); onNavigate(n.route); }}>{n.title}</button>
                                {read ? null : <Badge size="sm" variant="warning" style={{ marginInlineStart: "var(--space-2)" }}>New</Badge>}
                              </span>
                              <span className="mq-row__meta">{n.body}</span>
                            </div>
                            <div className="mq-row__side">
                              <span className="mq-mono mq-muted" title={formatAbsolute(n.at)}>{formatWhen(n.at, now)}</span>
                              <span className="mq-mono mq-muted">{route ? route.label : n.route}</span>
                              {read ? null : <Button variant="ghost" size="xs" onClick={() => onRead(n.id)}>Mark read</Button>}
                            </div>
                          </div>
                        );
                      })}
                    </div>
                  </div>
                );
              })}
              <div className="mq-foot">
                <span>Showing {rows.length} of {notifications.length} lines{tab === "unread" ? " that have not been read" : ""} · newest first</span>
                <span>Oldest {formatAbsolute(oldest.at).split(",")[0]}</span>
              </div>
            </div>
          )}
        </section>
      </div>
    );
  }

  Object.assign(window, { InboxScreen, INBOX_TABS });
})();

</script>
<script type="text/babel" data-presets="react" data-file="SearchScreen.jsx">
/* Search results — where the ⌘K palette lands when the answer is not one of
   the first few rows.
   Commit model: INSTANT. The field narrows the list as typed; there is nothing
   to save.
   Data state: one branch of three — no query, results, or nothing matched. Never
   two of them on screen.
   Actions derive from the record: every hit opens the record it names, and each
   line says what it matched on, so a reader can see why it is in the list.
   Primary display: grouped rows in one order, entity by entity, with the count
   in each heading counting that group and the footer counting the whole set.
   The palette and this screen run one function — searchDesk() — so the number
   the palette footer promises is the number that appears here. */
(() => {
  const {
    Button, Input, Kbd, Badge,
    Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent,
  } = window.OriginDesignSystem_7c998b;

  /* The footer counts each group, so each group's label has to survive being
     counted to one. Keyed by the group labels searchDesk() returns. */
  const SEARCH_SINGULAR = {
    "Creators": "creator",
    "Brands": "brand",
    "Briefs": "brief",
    "Deals": "deal",
    "Deliverables": "deliverable",
    "Invoices and payouts": "invoice or payout",
  };

  function SearchScreen({ query, data, now, onNavigate, onQuery, palette }) {
    const results = searchDesk(data, query);
    const trimmed = String(query || "").trim();

    return (
      <div className="mq-page mq-page--tight">
        <PageHead
          path="/search"
          title={trimmed ? "Results for “" + trimmed + "”" : "Search the desk"}
          lede={!trimmed
            ? "The whole desk in one field: creators, brands, briefs, deals, deliverables and the money on them. ⌘K opens the same search from anywhere."
            : results.total === 0
              ? "Nothing on the desk matches that."
              : results.total + " " + (results.total === 1 ? "record" : "records") + " across " + results.groups.length + " " + (results.groups.length === 1 ? "kind" : "kinds") + ". Each line says what it matched on."}
        />

        <section className="mq-section">
          <div className="mq-filters">
            <div className="mq-filters__text">
              <Input
                size="sm" value={query || ""} autoFocus
                aria-label="Search the talent desk"
                placeholder="Wren, Tessellate, INV-26-0158, one-pan…"
                startAddon={<Icon name="search" />}
                endAddon={trimmed ? <Button variant="ghost" size="xs" onClick={() => onQuery("")}>Clear</Button> : <Kbd>⌘K</Kbd>}
                onChange={(e) => onQuery(e.target.value)}
              />
            </div>
            <span className="mq-note">Searching names, handles, cities, brands, titles, briefs, revision notes, invoice and payout references.</span>
          </div>

          {!trimmed ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="search" size={18} /></EmptyMedia>
                <EmptyTitle>Type something to search</EmptyTitle>
                <EmptyDescription>
                  {data.creators.length} creators, {data.brands.length} brands, {data.briefs.length} briefs, {data.deals.length} deals and every deliverable and invoice on them.
                </EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button variant="outline" onClick={() => palette.setOpen(true)}>Open the palette instead</Button>
              </EmptyContent>
            </Empty>
          ) : results.total === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon"><Icon name="search-x" size={18} /></EmptyMedia>
                <EmptyTitle>Nothing matches “{trimmed}”</EmptyTitle>
                <EmptyDescription>No creator, brand, brief, deal, deliverable or reference on the desk contains that. A brand we have never worked with will not be here — brands arrive with their first brief.</EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Button onClick={() => onQuery("")}>Clear the search</Button>
              </EmptyContent>
            </Empty>
          ) : (
            <div className="mq-col">
              {results.groups.map((group) => (
                <div className="mq-col mq-col--tight" key={group.label}>
                  <div className="mq-section__head">
                    <h2 className="mq-h3">{group.label}<span className="mq-tab-count">{group.items.length}</span></h2>
                  </div>
                  <div className="mq-rows">
                    {group.items.map((item) => (
                      <div className="mq-row" key={group.label + item.id}>
                        <div className="mq-row__main mq-row__wide">
                          <span className="mq-row__title">
                            <button type="button" className="mq-link" onClick={() => onNavigate(item.route)}>{item.title}</button>
                          </span>
                          <span className="mq-row__meta">{item.line}</span>
                        </div>
                        <div className="mq-row__side">
                          <Badge size="sm" variant="secondary">{matchRoute(item.route, MARQUE_ROUTES) ? matchRoute(item.route, MARQUE_ROUTES).label : item.route}</Badge>
                          <Button variant="ghost" size="xs" onClick={() => onNavigate(item.route)}>Open<Icon name="arrow-right" /></Button>
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              ))}
              <div className="mq-foot">
                <span>{results.total} {results.total === 1 ? "record" : "records"} matching “{trimmed}” · {results.groups.map((g) => g.items.length + " " + (g.items.length === 1 ? SEARCH_SINGULAR[g.label] : g.label.toLowerCase())).join(" · ")}</span>
                <span>Ordered by kind, then as each list orders itself</span>
              </div>
            </div>
          )}
        </section>
      </div>
    );
  }

  Object.assign(window, { SearchScreen, SEARCH_SINGULAR });
})();

</script>
<script type="text/babel" data-presets="react" data-file="MarqueAccountScreens.jsx">
/* Account and auth — the two screens the account menu points at, and the two
   auth screens it leads to.

   Commit model: STAGED on both settings screens. Every control is a form field,
   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.

   Auth shell (AGENTS §7.1): its own shell, no navigation owner — no top bar, no
   destinations, no account menu. One layout variant: centred card, the agency
   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, credentials-rejected with the attempts left, locked, signed-out.
   The four password-reset screens and the lockout screen live in
   MarqueResetScreens.jsx and are reached from here — nothing is registered that
   the flow cannot get to, and nothing rendered here leads nowhere. MFA and SSO
   remain out of scope, so no screen offers them. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Field, FieldLabel, FieldDescription, FieldError, Input, Button, Badge, Alert, AlertTitle, AlertDescription, Avatar,
  } = window.OriginDesignSystem_7c998b;

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

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

  function ProfileScreen({ user, data, now, onNavigate, 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);
    const [photoError, setPhotoError] = React.useState(null);
    const fileRef = React.useRef(null);
    React.useEffect(() => { setDraft(initial); setSubmitted(false); setPhotoError(null); }, [initial]);
    /* Staged like every other field on this screen: the picked file is read into
       the draft and only reaches the account menu on save, so cancel and the undo
       toast both put the old photo back. */
    const pickPhoto = (file) => {
      if (!file) return;
      if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setPhotoError("Pick a PNG, JPEG or WebP image."); return; }
      if (file.size > 2 * 1024 * 1024) { setPhotoError("That file is " + Math.round(file.size / 1024 / 102.4) / 10 + "MB. The limit is 2MB."); return; }
      const reader = new FileReader();
      reader.onload = () => { setPhotoError(null); setDraft((d) => Object.assign({}, d, { avatar: String(reader.result) })); };
      reader.readAsDataURL(file);
    };
    const initialsOf = (name) => name.trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
    const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
    const errors = {};
    if (!draft.name.trim()) errors.name = "Enter the name brands and creators see.";
    if (!EMAIL.test(draft.email)) errors.email = "Enter a valid email address.";
    const valid = Object.keys(errors).length === 0;
    const mine = data.briefs.filter(BRIEF_FLAGS.isOpen);

    return (
      <div className="mq-page">
        <PageHead path="/account/profile" title="Manager profile" lede={"Your name goes on every pitch that leaves this desk \u2014 " + mine.length + " briefs are open under it right now."} />
        <Card>
          <CardHeader>
            <CardTitle>How you appear</CardTitle>
            <CardDescription>On pitches, contracts and the emails that carry them.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel>Photo</FieldLabel>
                <div className="mq-photo">
                  <Avatar size={64} src={draft.avatar || undefined} alt="" fallback={initialsOf(draft.name || user.name)} />
                  <div className="mq-photo__acts">
                    <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" hidden onChange={(e) => { pickPhoto(e.target.files && e.target.files[0]); e.target.value = ""; }} />
                    <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>
                </div>
                {photoError ? <FieldError>{photoError}</FieldError> : <FieldDescription>PNG, JPEG or WebP, up to 2MB. Without one, your initials stand in.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-p-name">Name</FieldLabel>
                <Input id="mq-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="mq-p-role">Title</FieldLabel>
                <Input id="mq-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 pitches.</FieldDescription>
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-p-email">Email</FieldLabel>
                <Input id="mq-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 and brand 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"
        />
      </div>
    );
  }

  function AgencyScreen({ agency, data, now, onNavigate, onSave }) {
    const initial = React.useMemo(() => ({ name: agency.name, book: agency.book, billingEmail: agency.billingEmail }), [agency]);
    const [draft, setDraft] = React.useState(initial);
    const [submitted, setSubmitted] = React.useState(false);
    React.useEffect(() => { setDraft(initial); setSubmitted(false); }, [initial]);
    const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
    const errors = {};
    if (!draft.name.trim()) errors.name = "Enter the agency name.";
    if (!draft.book.trim()) errors.book = "Name the cities this book covers.";
    if (!EMAIL.test(draft.billingEmail)) errors.billingEmail = "Enter a valid email address.";
    const valid = Object.keys(errors).length === 0;
    /* Commission is not edited here: every deal stores the rate it was signed
       at, and the ledger reads that. What this screen can honestly show is how
       far the book has drifted from the standard. */
    const standard = AGENCY_POLICY.standardCommissionRate;
    const below = data.deals.filter((deal) => deal.commissionRate < standard);

    return (
      <div className="mq-page">
        <PageHead path="/account/agency" title="Agency and commission" lede={data.deals.length + " deals are on the book, " + data.creators.length + " creators signed to it."} />
        <Card>
          <CardHeader>
            <CardTitle>The agency</CardTitle>
            <CardDescription>What brands see on a pitch and an invoice.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Field>
                <FieldLabel htmlFor="mq-a-name">Agency name</FieldLabel>
                <Input id="mq-a-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="mq-a-book">The book</FieldLabel>
                <Input id="mq-a-book" value={draft.book} invalid={submitted && !!errors.book} onChange={(e) => setDraft(Object.assign({}, draft, { book: e.target.value }))} />
                {submitted && errors.book ? <FieldError>{errors.book}</FieldError> : <FieldDescription>The cities this roster is sold in. Shown above the roster.</FieldDescription>}
              </Field>
              <Field>
                <FieldLabel htmlFor="mq-a-billing">Billing email</FieldLabel>
                <Input id="mq-a-billing" type="email" value={draft.billingEmail} invalid={submitted && !!errors.billingEmail} onChange={(e) => setDraft(Object.assign({}, draft, { billingEmail: e.target.value }))} />
                {submitted && errors.billingEmail ? <FieldError>{errors.billingEmail}</FieldError> : <FieldDescription>Where brands are told to send remittances. Net {AGENCY_POLICY.paymentTermsDays} from issue.</FieldDescription>}
              </Field>
            </div>
          </CardPanel>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>Commission</CardTitle>
            <CardDescription>The standard rate is {Math.round(standard * 100)}%. It is not edited here: every deal stores the rate it was signed at, and the money ledgers read that rate rather than this one.</CardDescription>
          </CardHeader>
          <CardPanel>
            <dl className="mq-kv">
              <div><dt>Standard rate</dt><dd>{Math.round(standard * 100)}%</dd></div>
              <div><dt>Deals signed below it</dt><dd>{below.length} of {data.deals.length}</dd></div>
              {below.map((deal) => (
                <div key={deal.id}><dt>{deal.brand} · {deal.id}</dt><dd>{Math.round(deal.commissionRate * 100)}%</dd></div>
              ))}
            </dl>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <span className="mq-note">Changing a rate means renegotiating a deal, which happens on the deal sheet.</span>
            <Button variant="outline" size="sm" onClick={() => onNavigate("/money")}>Open the ledgers</Button>
          </CardFooter>
        </Card>

        <FormBar
          dirty={dirty}
          note={dirty ? "Unsaved changes to " + initial.name + "." : "Everything here is as it was last saved."}
          onCancel={() => { setDraft(initial); setSubmitted(false); }}
          onSave={() => { setSubmitted(true); if (valid) onSave(draft); }}
          label="Save agency"
        />
      </div>
    );
  }

  /* ── Auth ─────────────────────────────────────────────────────────────── */

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

  function SignInScreen({ agency, user, onSignIn, onNavigate }) {
    const [values, setValues] = React.useState({ email: "", password: "" });
    const [submitted, setSubmitted] = React.useState(false);
    const [pending, setPending] = React.useState(false);
    /* Rejections are counted, not just reported: the copy names how many tries
       are left and the last one lands on /account-locked (AGENTS §7.3.6). The
       demo desk accepts the signed-in manager's own address. */
    const [rejected, setRejected] = React.useState(0);
    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 left = AGENCY_POLICY.signInAttempts - rejected;
    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      setPending(true);
      window.setTimeout(() => {
        setPending(false);
        if (values.email.trim().toLowerCase() === user.email.toLowerCase()) { onSignIn(); return; }
        const used = rejected + 1;
        setRejected(used);
        setValues({ email: values.email, password: "" });
        setSubmitted(false);
        if (used >= AGENCY_POLICY.signInAttempts) onNavigate("/account-locked");
      }, 600);
    };
    return (
      <AuthShell agency={agency} footer="The desk, the book and the money in one place.">
        <Card>
          <CardHeader>
            <CardTitle>Sign in</CardTitle>
            <CardDescription>To the {agency.desk.toLowerCase()} at {agency.name}.</CardDescription>
          </CardHeader>
          <CardPanel>
            {/* One state at a time: a rejection replaces the field errors rather
                than stacking with them (§7.3.3), which is why submitted resets
                on a rejection. */}
            {rejected > 0 && !submitted ? (
              <Alert variant="error" style={{ marginBottom: "var(--space-5)" }}>
                <AlertTitle>Email or password is incorrect</AlertTitle>
                <AlertDescription>{left} {left === 1 ? "attempt" : "attempts"} left before the account locks for fifteen minutes.</AlertDescription>
              </Alert>
            ) : null}
            <form className="mq-fields" onSubmit={(e) => { e.preventDefault(); submit(); }}>
              <Field>
                <FieldLabel htmlFor="mq-signin-email">Email</FieldLabel>
                <Input id="mq-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>
                <div className="mq-fieldrow">
                  <FieldLabel htmlFor="mq-signin-password">Password</FieldLabel>
                  <button type="button" className="mq-link" onClick={() => onNavigate("/forgot-password")}>Forgot password?</button>
                </div>
                <Input id="mq-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({ agency, user, onSignIn }) {
    return (
      <AuthShell agency={agency} footer="Pitches already sent stay sent; nothing on the desk changed.">
        <Card>
          <CardHeader>
            <CardTitle>You are signed out</CardTitle>
            <CardDescription>{user.name} was signed out of the {agency.name} {agency.desk.toLowerCase()}.</CardDescription>
          </CardHeader>
          <CardPanel>
            <Button onClick={onSignIn}>Sign in</Button>
          </CardPanel>
        </Card>
      </AuthShell>
    );
  }

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

</script>
<script type="text/babel" data-presets="react" data-file="MarqueResetScreens.jsx">
/* Password recovery and lockout — the four reset screens AGENTS §7.5 requires,
   plus the lockout state §7.3.6 requires, all on the same auth shell as sign-in:
   its own shell, no navigation owner, one layout variant (centred card).

   One state per screen, and the four are four screens rather than four states of
   one, because each has a different single action:
     /forgot-password         request a link      → Send reset link
     /forgot-password/sent    the confirmation    → Resend, with a cooldown
     /reset-password          set the new one     → Set new password
     /reset-password/done     the consequence     → Sign in
     /reset-password/expired  the dead link       → Send a new link
     /account-locked          the wait            → a real countdown

   Enumeration-safe throughout: nothing on the request or the confirmation
   screen says whether an account exists (§7.3.2). Every screen has a way back
   to sign-in, so nothing here is a dead end. Requirements render from
   PASSWORD_RULES, which is also what blocks the submit. */
(() => {
  const {
    Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter,
    Field, FieldLabel, FieldDescription, FieldError, Input, Button, Alert, AlertTitle, AlertDescription,
  } = window.OriginDesignSystem_7c998b;

  const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
  /* The link's life and the resend cooldown, stated once and rendered from here
     rather than written into the copy twice. */
  const LINK_MINUTES = 30;
  const RESEND_SECONDS = 45;

  function BackToSignIn({ onNavigate, label }) {
    return <Button variant="ghost" size="sm" onClick={() => onNavigate("/signin")}>{label || "Back to sign in"}</Button>;
  }

  function ForgotPasswordScreen({ agency, onNavigate, onRequest }) {
    const [email, setEmail] = React.useState("");
    const [submitted, setSubmitted] = React.useState(false);
    const [pending, setPending] = React.useState(false);
    const error = EMAIL.test(email) ? null : "Enter a valid email address.";
    const submit = () => {
      setSubmitted(true);
      if (error) return;
      setPending(true);
      window.setTimeout(() => { setPending(false); onRequest(email.trim()); }, 600);
    };
    return (
      <AuthShell agency={agency} footer="Nobody at the agency can read your password, so this is the only way back in.">
        <Card>
          <CardHeader>
            <CardTitle>Reset your password</CardTitle>
            <CardDescription>We will email a link that sets a new one. It is good for {LINK_MINUTES} minutes.</CardDescription>
          </CardHeader>
          <CardPanel>
            <form className="mq-fields" onSubmit={(e) => { e.preventDefault(); submit(); }}>
              <Field>
                <FieldLabel htmlFor="mq-fp-email">Email</FieldLabel>
                <Input id="mq-fp-email" type="email" autoComplete="email" value={email}
                  invalid={submitted && !!error} onChange={(e) => setEmail(e.target.value)} />
                {submitted && error ? <FieldError>{error}</FieldError> : <FieldDescription>The address you sign in with.</FieldDescription>}
              </Field>
              <Button type="submit" loading={pending} disabled={!email || pending}>Send reset link</Button>
            </form>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <BackToSignIn onNavigate={onNavigate} />
          </CardFooter>
        </Card>
      </AuthShell>
    );
  }

  function ResetSentScreen({ agency, email, onNavigate, onResend }) {
    /* A cooldown with a real countdown, not a disabled button that never says
       why (§7.5). */
    const [left, setLeft] = React.useState(RESEND_SECONDS);
    React.useEffect(() => {
      if (left <= 0) return undefined;
      const t = window.setTimeout(() => setLeft((n) => n - 1), 1000);
      return () => window.clearTimeout(t);
    }, [left]);
    return (
      <AuthShell agency={agency} footer="If it does not arrive, check the spam folder before asking for another.">
        <Card>
          <CardHeader>
            <CardTitle>Check your email</CardTitle>
            <CardDescription>
              If an account exists for {email ? <strong>{email}</strong> : "that address"}, a link to set a new password is on its way. It expires {LINK_MINUTES} minutes after it was sent.
            </CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              <Button variant="outline" disabled={left > 0} onClick={() => { setLeft(RESEND_SECONDS); onResend(); }}>
                {left > 0 ? "Resend link in " + left + "s" : "Resend link"}
              </Button>
              <p className="mq-note">Opening the link signs you out everywhere else, on this desk and on your phone.</p>
            </div>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <BackToSignIn onNavigate={onNavigate} />
            <Button variant="ghost" size="sm" onClick={() => onNavigate("/reset-password")}>I have the link</Button>
          </CardFooter>
        </Card>
      </AuthShell>
    );
  }

  function SetPasswordScreen({ agency, email, onNavigate, onSet }) {
    const [value, setValue] = React.useState("");
    const [reveal, setReveal] = React.useState(false);
    const [submitted, setSubmitted] = React.useState(false);
    const [pending, setPending] = React.useState(false);
    const met = PASSWORD_RULES.map((rule) => ({ rule: rule, ok: rule.test(value) }));
    const valid = passwordMeets(value);
    const submit = () => {
      setSubmitted(true);
      if (!valid) return;
      setPending(true);
      window.setTimeout(() => { setPending(false); onSet(); }, 600);
    };
    return (
      <AuthShell agency={agency} footer="One password, one field. A reveal toggle beats a confirm box you cannot read either.">
        <Card>
          <CardHeader>
            <CardTitle>Set a new password</CardTitle>
            <CardDescription>For {email || "your account"} on the {agency.desk.toLowerCase()}.</CardDescription>
          </CardHeader>
          <CardPanel>
            <form className="mq-fields" onSubmit={(e) => { e.preventDefault(); submit(); }}>
              <Field>
                <FieldLabel htmlFor="mq-rp-password">New password</FieldLabel>
                <Input id="mq-rp-password" type={reveal ? "text" : "password"} autoComplete="new-password" value={value}
                  invalid={submitted && !valid} onChange={(e) => setValue(e.target.value)}
                  endAddon={<button type="button" className="mq-link" aria-label={reveal ? "Hide password" : "Show password"} onClick={() => setReveal(!reveal)}>{reveal ? "Hide" : "Show"}</button>} />
                {submitted && !valid ? <FieldError>Your password has to meet all three requirements below.</FieldError> : null}
              </Field>
              <div className="mq-checks">
                {met.map((row) => (
                  <span className={"mq-check " + (row.ok ? "mq-good" : "mq-muted")} key={row.rule.id}>
                    <Icon name={row.ok ? "check" : "circle"} size={14} />
                    <span className="mq-check__text">{row.rule.label}</span>
                  </span>
                ))}
              </div>
              <Button type="submit" loading={pending} disabled={!value || pending}>Set new password</Button>
            </form>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <BackToSignIn onNavigate={onNavigate} />
            <Button variant="ghost" size="sm" onClick={() => onNavigate("/reset-password/expired")}>My link has expired</Button>
          </CardFooter>
        </Card>
      </AuthShell>
    );
  }

  function ResetDoneScreen({ agency, onNavigate }) {
    return (
      <AuthShell agency={agency} footer="Nothing on the desk changed: pitches already sent stay sent.">
        <Card>
          <CardHeader>
            <CardTitle>Password set</CardTitle>
            <CardDescription>Every other session was signed out, including your phone. Sign in again with the new password.</CardDescription>
          </CardHeader>
          <CardPanel>
            <Button onClick={() => onNavigate("/signin")}>Sign in</Button>
          </CardPanel>
        </Card>
      </AuthShell>
    );
  }

  function ResetExpiredScreen({ agency, onNavigate }) {
    return (
      <AuthShell agency={agency} footer="Links are short-lived on purpose; asking for another costs nothing.">
        <Card>
          <CardHeader>
            <CardTitle>That link has expired</CardTitle>
            <CardDescription>Reset links last {LINK_MINUTES} minutes and can only be used once. Nothing has changed about your account.</CardDescription>
          </CardHeader>
          <CardPanel>
            <Button onClick={() => onNavigate("/forgot-password")}>Send a new link</Button>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <BackToSignIn onNavigate={onNavigate} />
          </CardFooter>
        </Card>
      </AuthShell>
    );
  }

  function AccountLockedScreen({ agency, minutes, onNavigate }) {
    /* A bounded wait with a real countdown, and a recovery path on the same
       screen — never a toast (§7.3.6). */
    const [left, setLeft] = React.useState((minutes || 15) * 60);
    React.useEffect(() => {
      if (left <= 0) return undefined;
      const t = window.setTimeout(() => setLeft((n) => n - 1), 1000);
      return () => window.clearTimeout(t);
    }, [left]);
    const mm = Math.floor(left / 60), ss = left % 60;
    return (
      <AuthShell agency={agency} footer="The lock is on the account, not on the device — waiting it out on another browser will not help.">
        <Card>
          <CardHeader>
            <CardTitle>This account is locked</CardTitle>
            <CardDescription>{AGENCY_POLICY.signInAttempts} passwords were tried and none matched, so sign-in is closed for fifteen minutes.</CardDescription>
          </CardHeader>
          <CardPanel>
            <div className="mq-fields">
              {left > 0 ? (
                <Alert variant="warning">
                  <AlertTitle>Try again in {mm}:{String(ss).padStart(2, "0")}</AlertTitle>
                  <AlertDescription>Resetting your password opens it immediately — you do not have to wait for the clock.</AlertDescription>
                </Alert>
              ) : (
                <Alert variant="success">
                  <AlertTitle>The wait is over</AlertTitle>
                  <AlertDescription>You can sign in again now.</AlertDescription>
                </Alert>
              )}
              <Button onClick={() => onNavigate(left > 0 ? "/forgot-password" : "/signin")}>{left > 0 ? "Reset my password" : "Sign in"}</Button>
            </div>
          </CardPanel>
          <CardFooter className="mq-card-footer">
            <span className="mq-note">Still stuck? {agency.billingEmail.replace("billing", "help")} reaches a person.</span>
          </CardFooter>
        </Card>
      </AuthShell>
    );
  }

  Object.assign(window, { ForgotPasswordScreen, ResetSentScreen, SetPasswordScreen, ResetDoneScreen, ResetExpiredScreen, AccountLockedScreen });
})();

</script>
<script type="text/babel" data-presets="react" data-file="TeamScreen.jsx">
/* Team and ownership — who works this desk and whose creators are whose.
   Commit model: INSTANT, and there is deliberately nothing to commit:
   ownership is a field on the creator, so it is changed on the creator's file
   and this screen reads it. A control here would be a second entry point for
   the same fact.
   Data state: populated. Every manager has a book, even if it is an empty one,
   and a manager who owns nobody says so in a sentence.
   Actions derive from the record: a manager with somebody free to pitch is
   offered that creator; the business-affairs seat owns no roster, so it is
   offered nothing and its card says why.
   Primary display: one column stack per manager, each a card with the book
   under it — the only comparison worth making is manager against manager. */
(() => {
  const { Button, Badge, Card, CardHeader, CardTitle, CardDescription, CardPanel, CardFooter, Separator } = window.OriginDesignSystem_7c998b;

  function ManagerCard({ manager, data, now, onNavigate }) {
    const book = managerBook(data, manager);
    const owns = book.roster.length;
    return (
      <Card>
        <CardHeader>
          <CardTitle>
            <span className="mq-who">
              <Portrait creator={{ name: manager.name, portrait: manager.avatar }} size={36} />
              <span className="mq-who__text">
                <span className="mq-who__name">{manager.name}</span>
                <span className="mq-who__meta">{manager.role} · {manager.desk} · since {formatAbsolute(manager.joinedAt).split(",")[0]}</span>
              </span>
            </span>
          </CardTitle>
          <CardDescription>
            {owns === 0
              ? "Owns no creator. Business affairs sits across the whole book — the paper, the invoices and the payment runs — rather than a roster of its own."
              : owns + " " + (owns === 1 ? "creator" : "creators") + " on their book, " + book.free.length + " free to pitch today, " + formatFollowers(book.reach) + " of audience between them."}
          </CardDescription>
        </CardHeader>
        <CardPanel>
          {owns === 0 ? (
            <div className="mq-lg">
              <LedgerRow label="Invoices on the desk" hint="Every campaign, whoever owns the creator" value={invoiceRows(data.deals).length} />
              <LedgerRow label="Past due" hint={invoicesOverdue(invoiceRows(data.deals), now).length ? invoicesOverdue(invoiceRows(data.deals), now).map((r) => r.deal.brand).join(", ") : "Nothing late"} value={formatMoney(moneyTotal(invoicesOverdue(invoiceRows(data.deals), now)))} variant="net" />
            </div>
          ) : (
            <>
              <div className="mq-lg">
                <LedgerRow label="Fees booked" hint={book.deals.length + " " + (book.deals.length === 1 ? "campaign" : "campaigns") + " across their book"} value={formatMoney(book.feeCents)} />
                <LedgerRow label="Commission on it" hint="At the rate each deal was signed at" value={formatMoney(book.commissionCents)} variant="net" />
              </div>
              <Separator />
              <div className="mq-rows">
                {book.roster.slice().sort((a, b) => reachOf(b) - reachOf(a)).map((creator) => (
                  <div className={"mq-row" + (creator.status === "paused" ? " mq-row--recessed" : "")} key={creator.id}>
                    <div className="mq-row__main">
                      <span className="mq-row__title">
                        <button type="button" className="mq-link" onClick={() => onNavigate("/talent/" + creator.id)}>{creator.name}</button>
                      </span>
                      <span className="mq-row__meta">
                        @{creator.handle} · {formatFollowers(reachOf(creator))} · {creator.categories.map((c) => labelOf(CATEGORIES, c)).join(", ")}
                        {unavailableBecause(creator, data.deals, now) ? " · " + unavailableBecause(creator, data.deals, now).toLowerCase() : ""}
                      </span>
                    </div>
                    <div className="mq-row__side">
                      <StatusBadge values={CREATOR_STATUS} value={creator.status} />
                      <Button variant="ghost" size="xs" onClick={() => onNavigate("/talent/" + creator.id + "/statement")}>Statement</Button>
                    </div>
                  </div>
                ))}
              </div>
            </>
          )}
        </CardPanel>
        <CardFooter className="mq-card-footer">
          <span className="mq-note">
            {owns === 0
              ? manager.email
              : book.attention.length
                ? book.attention.length + " " + (book.attention.length === 1 ? "deliverable needs" : "deliverables need") + " somebody on their book this week."
                : "Nothing on their book needs anybody this week."}
          </span>
          {owns > 0 && book.attention.length ? <Button variant="outline" size="sm" onClick={() => onNavigate("/deliverables")}>Open the desk</Button> : null}
        </CardFooter>
      </Card>
    );
  }

  function TeamScreen({ data, now, onNavigate }) {
    const managers = data.managers;
    const withRoster = managers.filter((m) => creatorsForManager(data.creators, m).length > 0);
    const unowned = unownedCreators(data.creators, managers);
    const left = withRoster.filter((m, i) => i % 2 === 0).concat(managers.filter((m) => creatorsForManager(data.creators, m).length === 0));
    const right = withRoster.filter((m, i) => i % 2 === 1);

    return (
      <div className="mq-page">
        <PageHead
          path="/account/team"
          title="Team and ownership"
          lede={managers.length + " people on the desk, " + data.creators.length + " creators signed, " + withRoster.length + " books between them. Ownership is a field on the creator, so it is changed on their file and read here."}
        />

        <div className="mq-split mq-split--even">
          <div className="mq-col">
            {left.map((manager) => <ManagerCard key={manager.id} manager={manager} data={data} now={now} onNavigate={onNavigate} />)}
          </div>
          <div className="mq-col">
            {right.map((manager) => <ManagerCard key={manager.id} manager={manager} data={data} now={now} onNavigate={onNavigate} />)}
            <section className="mq-section">
              <div className="mq-section__head"><h2 className="mq-h2">Coverage</h2></div>
              <div className="mq-sheet mq-sheet--tight">
                <div className="mq-lg">
                  {managers.map((manager) => {
                    const owns = creatorsForManager(data.creators, manager);
                    return (
                      <LedgerRow
                        key={manager.id}
                        label={manager.name}
                        hint={owns.length ? owns.map((c) => c.name.split(" ")[0]).join(", ") : manager.role}
                        value={owns.length ? owns.length + " of " + data.creators.length : <span className="mq-muted">No roster</span>}
                      />
                    );
                  })}
                  <LedgerRow label="Unassigned" hint={unowned.length ? unowned.map((c) => c.name).join(", ") + " — nobody is answering their calls" : "Every creator on the book has a manager"} value={unowned.length ? <span className="mq-bad">{unowned.length}</span> : <span className="mq-muted">None</span>} variant="net" />
                </div>
                <p className="mq-note">A creator with no manager is the one failure this screen exists to catch: it is why the count is stated even when it is nought.</p>
              </div>
            </section>
          </div>
        </div>
      </div>
    );
  }

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

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

   The chain that matters: recording a brand's payment moves the invoice to
   paid AND releases the creator's payout from `awaiting_funds` to `due`, in one
   commit, because that is what actually happens. Marking a post published
   writes its metrics into the fixture, so the promise wall moves. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  // Control density is one product-wide decision (AGENTS.md §9), so it rides on
  // the shell root rather than being passed per screen.
  const controls = options.controlSize === "medium" ? "medium" : "compact";
  /* The address the reset flow was started with, so the confirmation screen can
     name it back without the URL carrying it. */
  const [resetEmail, setResetEmail] = React.useState("");
  /* Which notification lines have been read. A set of ids rather than one flag,
     because the inbox marks them one at a time and the bar's badge counts what
     is left. */
  const [readIds, setReadIds] = React.useState([]);
  /* The app-scope search's query. It is state rather than a path segment because
     /search is one registered route; the palette cannot hand its own query over
     (lib/command-palette.js owns it and stays byte-identical between products),
     so the footer opens the screen and the screen's field takes over. */
  const [search, setSearch] = React.useState("");
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [data, setData] = React.useState({ creators: CREATORS, brands: BRANDS, managers: MANAGERS, briefs: BRIEFS, deals: DEALS, now: MARQUE_NOW });
  const [user, setUser] = React.useState(MARQUE_USER);
  const [agency, setAgency] = React.useState(MARQUE_AGENCY);
  const palette = useCommandPalette();
  const now = data.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 patchBrief = (id, fields) => setData((d) => Object.assign({}, d, { briefs: d.briefs.map((b) => (b.id === id ? Object.assign({}, b, fields) : b)) }));
  const restoreBrief = (snapshot) => setData((d) => Object.assign({}, d, { briefs: d.briefs.map((b) => (b.id === snapshot.id ? snapshot : b)) }));
  const patchDeal = (id, fields) => setData((d) => Object.assign({}, d, { deals: d.deals.map((x) => (x.id === id ? Object.assign({}, x, fields) : x)) }));
  const restoreDeal = (snapshot) => setData((d) => Object.assign({}, d, { deals: d.deals.map((x) => (x.id === snapshot.id ? snapshot : x)) }));
  const patchDeliverable = (dealId, itemId, fields) => setData((d) => Object.assign({}, d, {
    deals: d.deals.map((deal) => (deal.id === dealId
      ? Object.assign({}, deal, { deliverables: deal.deliverables.map((i) => (i.id === itemId ? Object.assign({}, i, fields) : i)) })
      : deal)),
  }));

  /* Creates. Each writes into the fixture and re-renders, so every count it
     feeds moves — and each raises an undo toast, because signing somebody or
     logging a brief is consequential and entirely reversible. Ids continue the
     product's own series rather than being invented per record. */
  const nextId = (values, prefix, width) => {
    const numbers = values.map((v) => Number(String(v).split("-").pop())).filter((n) => !isNaN(n));
    return prefix + String(Math.max.apply(null, numbers.concat([0])) + 1).padStart(width, "0");
  };
  const addBrief = (draft) => {
    const brandId = draft.newBrand ? draft.newBrand.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : draft.brandId;
    const brief = Object.assign({}, draft, {
      id: nextId(data.briefs.map((b) => b.id), "BR-26-", 3),
      brandId: brandId,
      brand: draft.newBrand ? draft.newBrand.name : brandNameOf(brandId),
      receivedAt: now, status: "to_triage",
    });
    delete brief.newBrand;
    setData((d) => Object.assign({}, d, {
      brands: draft.newBrand
        ? d.brands.concat([{ id: brandId, name: draft.newBrand.name, sector: draft.newBrand.sector, hq: draft.newBrand.hq, note: "First brief on the desk. Nothing else on file yet." }])
        : d.brands,
      briefs: [brief].concat(d.briefs),
    }));
    setPath("/briefs/" + brief.id);
    pushToast({
      variant: "success",
      title: brief.brand + " logged",
      description: brief.id + " · " + formatMoney(brief.budgetCents) + " · they need an answer by " + formatAbsolute(brief.respondByAt).split(",")[0],
      onUndo: () => { setData((d) => Object.assign({}, d, { briefs: d.briefs.filter((b) => b.id !== brief.id), brands: draft.newBrand ? d.brands.filter((b) => b.id !== brandId) : d.brands })); setPath("/briefs"); },
    });
  };
  const addCreator = (draft) => {
    const id = draft.handle.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 12);
    /* Portraits come from the design system's avatar assets; a new signing takes
       the next one in the set rather than an empty frame. */
    const portraits = [window.__resources.assets_img_avatar_80_01_jpg, window.__resources.assets_img_avatar_80_02_jpg, window.__resources.assets_img_avatar_80_03_jpg, window.__resources.assets_img_avatar_80_04_jpg, window.__resources.assets_img_avatar_80_05_jpg, window.__resources.assets_img_avatar_80_06_jpg];
    const creator = Object.assign({}, draft, {
      id: id, status: "available", signedAt: now, exclusivities: [],
      portrait: portraits[data.creators.length % portraits.length],
    });
    setData((d) => Object.assign({}, d, { creators: d.creators.concat([creator]) }));
    setPath("/talent/" + id);
    pushToast({
      variant: "success",
      title: creator.name + " signed",
      description: formatFollowers(reachOf(creator)) + " of audience · " + formatMoney(rateFloorOf(creator)) + "–" + formatMoney(rateCeilOf(creator)) + " a piece · free to pitch today",
      onUndo: () => { setData((d) => Object.assign({}, d, { creators: d.creators.filter((c) => c.id !== id) })); setPath("/"); },
    });
  };

  const nextPayoutReference = () => {    const numbers = data.deals
      .map((d) => Number(String(d.payout.reference || "").split("-")[1]))
      .filter((n) => !isNaN(n));
    return "SEPA-" + String(Math.max.apply(null, numbers.concat([70])) + 1).padStart(4, "0");
  };

  /* ── Briefs ────────────────────────────────────────────────────────── */
  const onBriefTransition = (brief, transition) => {
    if (transition.id === "pitch_creators") { setPath("/briefs/" + brief.id); return; }
    if (transition.id === "chase_brand") {
      patchBrief(brief.id, { chasedAt: now });
      pushToast({ title: "Chased " + brief.brand, description: brief.contact.name + " · " + brief.id, onUndo: () => restoreBrief(brief) });
      return;
    }
    if (transition.id === "reopen_brief") {
      patchBrief(brief.id, { status: "to_triage", passReason: undefined, passNote: undefined, passedAt: undefined, lostNote: undefined, lostAt: undefined });
      pushToast({ title: "Brief reopened", description: brief.brand + " is back on your desk", onUndo: () => restoreBrief(brief) });
      return;
    }
    if (transition.id === "book_deal") {
      const pitched = (brief.pitched || []).map((id) => creatorById(data.creators, id)).filter(Boolean);
      const first = pitched[0];
      setForm({
        title: "Book " + brief.brand + "'s campaign",
        description: brief.id + " · " + formatMoney(brief.budgetCents) + " budget · " + briefUnits(brief) + " pieces, live " + formatAbsolute(brief.liveFromAt).split(",")[0] + " to " + formatAbsolute(brief.liveToAt).split(",")[0] + ". The deal starts with its paper undrafted and its invoice held until the contract comes back.",
        submitLabel: "Book the deal",
        initial: { creatorId: first ? first.id : "", fee: first ? moneyInputValue(quoteFor(first, brief).totalCents) : "", commission: String(AGENCY_POLICY.standardCommissionRate), reach: "" },
        fields: [
          { id: "creatorId", kind: "radio", label: "Which creator did they take?", options: pitched.map((c) => ({
            value: c.id,
            label: c.name + " — quoted " + (quoteFor(c, brief).complete ? formatMoney(quoteFor(c, brief).totalCents) : "nothing, they do not take all of this") + " · " + formatFollowers(reachOf(c)) + " audience",
          })) },
          { id: "fee", kind: "money", label: "Fee agreed", hint: "What the brand pays. The invoice and the payout both read this one number." },
          { id: "commission", kind: "select", label: "Our commission", placeholder: "Pick the rate", options: [
            { value: "0.2", label: "20% — the standard rate" },
            { value: "0.18", label: "18% — negotiated down" },
            { value: "0.15", label: "15% — negotiated down" },
          ] },
          { id: "reach", kind: "count", label: "Reach we are promising them", hint: "Performance is measured against this and nothing else, so promise what you would defend at the wrap." },
        ],
        validate: (v) => {
          const errors = {};
          const fee = parseMoney(v.fee || "");
          const reach = parseCount(v.reach || "");
          if (!v.creatorId) errors.creatorId = "Say who the brand took.";
          if (isNaN(fee) || fee <= 0) errors.fee = "Enter the fee they agreed.";
          if (!v.commission) errors.commission = "Pick the commission this deal was signed at.";
          if (isNaN(reach) || reach <= 0) errors.reach = "Enter the reach we promised.";
          return errors;
        },
        run: (v) => {
          const creator = creatorById(data.creators, v.creatorId);
          const number = nextId(data.deals.map((d) => d.id), "", 3);
          const dealId = "DEAL-26-" + number;
          const span = Math.max(1, brief.deliverables.length - 1);
          const deal = {
            id: dealId, briefId: brief.id, creatorId: creator.id, brandId: brief.brandId, brand: brief.brand,
            title: brief.title, agreedAt: now, feeCents: parseMoney(v.fee), commissionRate: parseFloat(v.commission),
            contract: { status: "drafted", draftedAt: now, sentAt: null, signedAt: null },
            usage: brief.usage, usageStartAt: brief.liveFromAt,
            /* The engagement half of the promise is the creator's own current
               rate, computed from their platform rows — not a number typed to
               look plausible. The reach half is the one the manager commits to. */
            promise: { reach: parseCount(v.reach), engagementRate: engagementOf(creator) },
            /* One deliverable per line the brand asked for, keeping the line's
               quantity, due evenly across their window. The titles are working
               names until somebody writes the real ones on the piece. */
            deliverables: brief.deliverables.map((line, i) => ({
              id: "DLV-" + number + "-" + (i + 1),
              kind: line.kind, quantity: line.quantity,
              title: brief.title + " — " + labelOf(DELIVERABLE_KINDS, line.kind),
              dueAt: Math.round(brief.liveFromAt + (i / span) * (brief.liveToAt - brief.liveFromAt)),
              status: "scheduled",
            })),
            invoice: { id: nextId(data.deals.map((d) => d.invoice.id), "INV-26-", 4), status: "draft", issuedAt: null, paidAt: null, holdReason: "Held until the contract is countersigned." },
            payout: { id: nextId(data.deals.map((d) => d.payout.id), "PAY-26-", 4), status: "awaiting_funds", reference: null, paidAt: null },
          };
          setData((d) => Object.assign({}, d, {
            deals: d.deals.concat([deal]),
            briefs: d.briefs.map((b) => (b.id === brief.id ? Object.assign({}, b, { status: "won", dealId: dealId }) : b)),
          }));
          setPath("/deals/" + dealId);
          pushToast({
            variant: "success",
            title: "Booked " + brief.brand + " × " + creator.name,
            description: formatMoney(parseMoney(v.fee)) + " · " + formatMoney(Math.round(parseMoney(v.fee) * parseFloat(v.commission))) + " commission · the contract needs drafting and sending",
            onUndo: () => {
              setData((d) => Object.assign({}, d, { deals: d.deals.filter((x) => x.id !== dealId), briefs: d.briefs.map((b) => (b.id === brief.id ? brief : b)) }));
              setPath("/briefs/" + brief.id);
            },
          });
        },
      });
      return;
    }
    if (transition.id === "pass_brief") {
      const groups = matchGroups(data.creators, brief, now, data.deals);
      const floor = rosterFloor(data.creators, brief);
      setForm({
        title: "Pass on " + brief.brand + "?",
        description: brief.id + " · " + formatMoney(brief.budgetCents) + " · " + labelOf(CATEGORIES, brief.category) + ". " + groups.fits.length + " creators fit every axis" + (floor === null ? " and nobody on the book takes all of it." : "; the cheapest complete quote is " + formatMoney(floor) + "."),
        submitLabel: "Pass on brief",
        initial: {},
        fields: [
          { id: "reason", kind: "radio", label: "Why are we saying no?", options: PASS_REASONS },
          { id: "note", kind: "text", label: "What we tell them", placeholder: "In your own words — this is what goes back to the brand and what the file remembers.", hint: "At least a sentence. A brand that gets a real reason comes back with a better brief." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.reason) errors.reason = "Pick the reason this brief is not for us.";
          if (!v.note || v.note.trim().length < 20) errors.note = "Write the reason out — at least a sentence.";
          return errors;
        },
        run: (v) => {
          patchBrief(brief.id, { status: "passed", passReason: v.reason, passNote: v.note.trim(), passedAt: now });
          pushToast({ title: "Passed on " + brief.brand, description: labelOf(PASS_REASONS, v.reason), onUndo: () => restoreBrief(brief) });
        },
      });
    }
  };

  const onSendPitch = (brief, creatorIds, quotedCents) => {
    const names = creatorIds.map((id) => creatorById(data.creators, id).name.split(" ")[0]);
    patchBrief(brief.id, { status: "pitching", pitched: creatorIds, pitchedAt: now });
    pushToast({
      variant: "success",
      title: "Pitched to " + brief.brand,
      description: names.join(", ") + " · " + formatMoney(quotedCents) + " against a budget of " + formatMoney(brief.budgetCents),
      onUndo: () => restoreBrief(brief),
    });
  };

  /* ── Contracts and deals ───────────────────────────────────────────── */
  const onContractTransition = (deal, transition) => {
    if (transition.id === "send_contract") {
      patchDeal(deal.id, { contract: Object.assign({}, deal.contract, { status: "out_for_signature", sentAt: now }) });
      pushToast({ title: "Contract sent for signature", description: deal.brand + " · " + deal.id, onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "chase_signature") {
      patchDeal(deal.id, { contract: Object.assign({}, deal.contract, { chasedAt: now }) });
      pushToast({ title: "Chased " + deal.brand, description: CONTRACT_FLAGS.daysOut(deal, now) + " days out on " + deal.id, onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "mark_signed") {
      patchDeal(deal.id, { contract: Object.assign({}, deal.contract, { status: "signed", signedAt: now }) });
      pushToast({
        variant: "success",
        title: "Contract signed",
        description: deal.brand + " × " + creatorById(data.creators, deal.creatorId).name + " · " + formatMoney(deal.feeCents) + " — the invoice can be issued",
        onUndo: () => restoreDeal(deal),
      });
    }
  };

  const onDealTransition = (deal, transition) => {
    if (transition.id !== "void_deal") return;
    const creator = creatorById(data.creators, deal.creatorId);
    const brief = briefForDeal(data.briefs, deal);
    setConfirm({
      title: "Void " + deal.id + " with " + deal.brand + "?",
      description: "The paper dies and " + creator.name + " loses " + formatMoney(creatorNetOf(deal)) + " of booked work. " + (brief ? brief.id + " goes back to pitched, so you can put somebody else in front of them." : "") + " Nothing has gone live, so nothing has to be taken down — but this cannot be undone.",
      confirmLabel: "Void the deal",
      destructive: true,
      run: () => {
        patchDeal(deal.id, {
          contract: Object.assign({}, deal.contract, { status: "void", voidedAt: now }),
          invoice: Object.assign({}, deal.invoice, { holdReason: "The deal was voided on " + formatAbsolute(now).split(",")[0] + "." }),
        });
        if (brief) patchBrief(brief.id, { status: "pitching", dealId: undefined });
        pushToast({ title: "Deal voided", description: deal.brand + " × " + creator.name + " · " + deal.id });
      },
    });
  };

  /* ── Deliverables ──────────────────────────────────────────────────── */
  const onDeliverableTransition = (row, transition) => {
    const { item, deal, creator } = row;
    if (transition.id === "chase_creator") {
      patchDeliverable(deal.id, item.id, { chasedAt: now });
      pushToast({ title: "Chased " + creator.name.split(" ")[0], description: item.title + " · " + formatCountdown(item.dueAt, now), onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "submit_to_brand") {
      patchDeliverable(deal.id, item.id, { status: "submitted", submittedAt: now });
      pushToast({ title: "Sent to " + deal.brand, description: item.title + " · with them for approval", onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "mark_approved") {
      patchDeliverable(deal.id, item.id, { status: "approved", approvedAt: now });
      pushToast({ variant: "success", title: "Approved by " + deal.brand, description: item.title + " · " + creator.name.split(" ")[0] + " can post it", onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "record_revisions") {
      const brief = briefForDeal(data.briefs, deal);
      setForm({
        title: "What has " + deal.brand + " asked for?",
        description: item.title + " · " + creator.name + " · submitted " + formatAbsolute(item.submittedAt).split(",")[0] + ".",
        submitLabel: "Record revisions",
        initial: { days: "3" },
        fields: [
          { id: "note", kind: "text", label: "In their words", placeholder: "Quote the note, so the creator reads the brand rather than your summary of it.", hint: "At least a sentence." },
          { id: "days", kind: "select", label: "New due date", placeholder: "How long do they get?", options: [
            { value: "2", label: "In 2 days — " + formatAbsolute(now + 2 * 864e5).split(",")[0] },
            { value: "3", label: "In 3 days — " + formatAbsolute(now + 3 * 864e5).split(",")[0] },
            { value: "7", label: "In a week — " + formatAbsolute(now + 7 * 864e5).split(",")[0] },
          ] },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.note || v.note.trim().length < 20) errors.note = "Record what they actually said — at least a sentence.";
          if (!v.days) errors.days = "Give the creator a date to work to.";
          return errors;
        },
        run: (v) => {
          patchDeliverable(deal.id, item.id, {
            status: "revisions_requested", revisionNote: v.note.trim(), revisionAt: now,
            revisionBy: brief ? brief.contact.name : deal.brand,
            dueAt: now + parseInt(v.days, 10) * 864e5,
          });
          pushToast({ title: "Revisions recorded", description: item.title + " · back to " + creator.name.split(" ")[0] + ", due in " + v.days + " days", onUndo: () => restoreDeal(deal) });
        },
      });
      return;
    }
    if (transition.id === "mark_published") {
      setForm({
        title: "Mark " + item.title + " live",
        description: creator.name + " · " + deal.brand + " · these figures feed the campaign's promise, which stands at " + formatFollowers(deal.promise.reach) + " reach.",
        submitLabel: "Mark published",
        initial: {},
        fields: [
          { id: "url", kind: "count", label: "Where it lives", placeholder: "tiktok.com/@handle/1234", hint: "The public link, so the brand can be sent it." },
          { id: "reach", kind: "count", label: "Reach", placeholder: "412000", hint: "Accounts reached, from the platform's own numbers." },
          { id: "engagements", kind: "count", label: "Engagements", placeholder: "31900" },
          { id: "saves", kind: "count", label: "Saves", placeholder: "4100" },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.url || String(v.url).trim().length < 6) errors.url = "Paste the link to the post.";
          const reach = parseCount(v.reach || "");
          const engagements = parseCount(v.engagements || "");
          const saves = parseCount(v.saves || "");
          if (isNaN(reach) || reach <= 0) errors.reach = "Enter the reach the post actually got.";
          if (isNaN(engagements) || engagements <= 0) errors.engagements = "Enter the engagements.";
          else if (!isNaN(reach) && engagements > reach) errors.engagements = "Engagements cannot be higher than reach.";
          if (isNaN(saves) || saves < 0) errors.saves = "Enter the saves, even if it is nought.";
          else if (!isNaN(engagements) && saves > engagements) errors.saves = "Saves are part of engagements, so they cannot exceed them.";
          return errors;
        },
        run: (v) => {
          patchDeliverable(deal.id, item.id, {
            status: "published", publishedAt: now, url: String(v.url).trim(),
            metrics: { reach: parseCount(v.reach), engagements: parseCount(v.engagements), saves: parseCount(v.saves) },
          });
          pushToast({ variant: "success", title: "Live", description: item.title + " · " + formatFollowers(parseCount(v.reach)) + " reached — the promise wall has moved" });
        },
      });
    }
  };

  /* ── Money ─────────────────────────────────────────────────────────── */
  const onInvoiceTransition = (row, transition) => {
    const deal = row.deal;
    if (transition.id === "issue_invoice") {
      patchDeal(deal.id, { invoice: Object.assign({}, deal.invoice, { status: "issued", issuedAt: now, holdReason: undefined }) });
      pushToast({
        title: "Invoice issued",
        description: formatMoney(invoiceAmountOf(deal)) + " to " + deal.brand + " · net " + AGENCY_POLICY.paymentTermsDays + ", due " + formatAbsolute(now + AGENCY_POLICY.paymentTermsDays * 864e5).split(",")[0],
        onUndo: () => restoreDeal(deal),
      });
      return;
    }
    if (transition.id === "chase_payment") {
      patchDeal(deal.id, { invoice: Object.assign({}, deal.invoice, { chasedAt: now }) });
      pushToast({ title: "Chased " + deal.brand, description: deal.invoice.id + " · " + formatCountdown(INVOICE_FLAGS.dueAt(deal.invoice), now), onUndo: () => restoreDeal(deal) });
      return;
    }
    if (transition.id === "record_payment") {
      /* One commit: the brand's money lands and the creator's payout stops
         being blocked. Two records describing the same money, moved together. */
      const creator = creatorById(data.creators, deal.creatorId);
      patchDeal(deal.id, {
        invoice: Object.assign({}, deal.invoice, { status: "paid", paidAt: now }),
        payout: deal.payout.status === "awaiting_funds" ? Object.assign({}, deal.payout, { status: "due" }) : deal.payout,
      });
      pushToast({
        variant: "success",
        title: deal.brand + " paid " + formatMoney(invoiceAmountOf(deal)),
        description: formatMoney(commissionOf(deal)) + " commission banked · " + creator.name.split(" ")[0] + "'s " + formatMoney(payoutAmountOf(deal)) + " is now ready to release",
        onUndo: () => restoreDeal(deal),
      });
    }
  };

  const onPayoutTransition = (row, transition) => {
    if (transition.id !== "release_payout") return;
    const deal = row.deal;
    setConfirm({
      title: "Release " + formatMoney(row.amount) + " to " + row.creator.name + "?",
      description: "The money leaves the agency account on the run of " + formatAbsolute(nextPayoutRun(now)).split(",")[0] + " and cannot be recalled. It is " + deal.brand + "'s " + formatMoney(invoiceAmountOf(deal)) + " less our " + Math.round(deal.commissionRate * 100) + "% commission of " + formatMoney(commissionOf(deal)) + ".",
      confirmLabel: "Release payout",
      run: () => {
        const reference = nextPayoutReference();
        patchDeal(deal.id, { payout: Object.assign({}, deal.payout, { status: "paid", paidAt: now, reference: reference }) });
        pushToast({ variant: "success", title: "Payout released", description: formatMoney(row.amount) + " to " + row.creator.name + " · " + reference });
      },
    });
  };

  /* ── Account, staged ───────────────────────────────────────────────── */  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 }));
    pushToast({ variant: "success", title: "Profile saved", description: draft.name.trim() + " · " + draft.role.trim(), onUndo: () => setUser(snapshot) });
  };
  const onSaveAgency = (draft) => {
    const snapshot = agency;
    setAgency(Object.assign({}, agency, { name: draft.name.trim(), book: draft.book.trim(), billingEmail: draft.billingEmail.trim() }));
    pushToast({ variant: "success", title: "Agency saved", description: draft.name.trim() + " · " + draft.book.trim(), onUndo: () => setAgency(snapshot) });
  };

  const goSearch = (query) => { setSearch(query); setPath("/search"); };
  const markRead = (id) => setReadIds((list) => (list.indexOf(id) > -1 ? list : list.concat([id])));
  const markAllRead = () => setReadIds(NOTIFICATIONS.map((n) => n.id));
  const unreadCount = NOTIFICATIONS.filter((n) => readIds.indexOf(n.id) === -1).length;

  /* ── Routing ───────────────────────────────────────────────────────── */
  /* 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="Marque" path={path} onNavigate={setPath} groups={MARQUE_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (MARQUE_AUTH_PATHS.indexOf(path) > -1) {
    const auth = () => {
      if (path === "/signin") return <SignInScreen agency={agency} user={user} onSignIn={() => setPath("/")} onNavigate={setPath} />;
      if (path === "/signout") return <SignedOutScreen agency={agency} user={user} onSignIn={() => setPath("/signin")} />;
      if (path === "/forgot-password") return <ForgotPasswordScreen agency={agency} onNavigate={setPath} onRequest={(email) => { setResetEmail(email); setPath("/forgot-password/sent"); }} />;
      if (path === "/forgot-password/sent") return <ResetSentScreen agency={agency} email={resetEmail} onNavigate={setPath} onResend={() => undefined} />;
      if (path === "/reset-password") return <SetPasswordScreen agency={agency} email={resetEmail} onNavigate={setPath} onSet={() => setPath("/reset-password/done")} />;
      if (path === "/reset-password/done") return <ResetDoneScreen agency={agency} onNavigate={setPath} />;
      if (path === "/reset-password/expired") return <ResetExpiredScreen agency={agency} onNavigate={setPath} />;
      return <AccountLockedScreen agency={agency} minutes={15} onNavigate={setPath} />;
    };
    return (
      <>
        {auth()}
        {preview}
      </>
    );
  }

  const talentMatch = path.match(/^\/talent\/([^/]+)$/);
  const statementMatch = path.match(/^\/talent\/([^/]+)\/statement$/);
  const creator = talentMatch ? creatorById(data.creators, talentMatch[1]) : null;
  const statementCreator = statementMatch ? creatorById(data.creators, statementMatch[1]) : null;
  const briefMatch = path.match(/^\/briefs\/([^/]+)$/);
  const dealMatch = path.match(/^\/deals\/([^/]+)$/);
  const brandMatch = path.match(/^\/brands\/([^/]+)$/);
  const itemMatch = path.match(/^\/deliverables\/([^/]+)$/);
  const wrapMatch = path.match(/^\/performance\/([^/]+)$/);
  const brief = briefMatch ? briefById(data.briefs, briefMatch[1]) : null;
  const deal = dealMatch ? dealById(data.deals, dealMatch[1]) : null;
  const brand = brandMatch ? brandById(data.brands, brandMatch[1]) : null;
  const item = itemMatch ? deliverableRowById(data.deals, data.creators, itemMatch[1]) : null;
  const wrapDeal = wrapMatch ? dealById(data.deals, wrapMatch[1]) : null;

  const screen = () => {
    if (path === "/") return <RosterScreen data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (path === "/talent/new") return <NewCreatorScreen data={data} now={now} onNavigate={setPath} onCreate={addCreator} />;
    if (path === "/briefs/new") return <NewBriefScreen data={data} now={now} onNavigate={setPath} onCreate={addBrief} />;
    if (creator) return <TalentScreen creator={creator} data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (statementCreator) return <StatementScreen creator={statementCreator} data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (brand) return <BrandScreen brand={brand} data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (item) return <DeliverableScreen row={item} data={data} now={now} onNavigate={setPath} onDeliverableTransition={onDeliverableTransition} pushToast={pushToast} />;
    if (wrapDeal) return <WrapScreen deal={wrapDeal} data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (path === "/inbox") return <InboxScreen notifications={NOTIFICATIONS} readIds={readIds} data={data} now={now} onNavigate={setPath} onRead={markRead} onReadAll={markAllRead} />;
    if (path === "/search") return <SearchScreen query={search} data={data} now={now} onNavigate={setPath} onQuery={setSearch} palette={palette} />;
    if (path === "/account/team") return <TeamScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/briefs") return <BriefsScreen data={data} now={now} onNavigate={setPath} onBriefTransition={onBriefTransition} pushToast={pushToast} />;
    if (path === "/deliverables") return <DeliverablesScreen data={data} now={now} onNavigate={setPath} onDeliverableTransition={onDeliverableTransition} pushToast={pushToast} />;
    if (path === "/performance") return <PerformanceScreen data={data} now={now} onNavigate={setPath} pushToast={pushToast} />;
    if (path === "/money") return <MoneyScreen data={data} now={now} onNavigate={setPath} onInvoiceTransition={onInvoiceTransition} onPayoutTransition={onPayoutTransition} pushToast={pushToast} />;
    if (path === "/account/profile") return <ProfileScreen user={user} data={data} now={now} onNavigate={setPath} onSave={onSaveProfile} />;
    if (path === "/account/agency") return <AgencyScreen agency={agency} data={data} now={now} onNavigate={setPath} onSave={onSaveAgency} />;
    if (brief) return <MatchScreen brief={brief} data={data} now={now} onNavigate={setPath} onBriefTransition={onBriefTransition} onSendPitch={onSendPitch} />;
    if (deal) return <DealScreen deal={deal} data={data} now={now} onNavigate={setPath} onContractTransition={onContractTransition} onDealTransition={onDealTransition} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Briefs waiting on you", items: briefsToTriage(data.briefs).map((b) => ({ value: b.id, label: b.brand + " · " + formatMoney(b.budgetCents) + " · " + labelOf(CATEGORIES, b.category), route: "/briefs/" + b.id })) },
    { label: "Deals", items: data.deals.map((d) => ({ value: d.id, label: d.brand + " × " + creatorById(data.creators, d.creatorId).name + " · " + formatMoney(d.feeCents), route: "/deals/" + d.id })) },
    { label: "Brands", items: data.brands.map((b) => ({ value: b.id, label: b.name + " · " + labelOf(CATEGORIES, b.sector) + " · " + briefsForBrand(data.briefs, b).length + " briefs", route: "/brands/" + b.id })) },
    { label: "Past due", items: invoicesOverdue(invoiceRows(data.deals), now).map((r) => ({ value: r.invoice.id, label: r.deal.brand + " · " + formatMoney(r.amount) + " · " + INVOICE_FLAGS.daysOverdue(r.invoice, now) + " days over", route: "/money" })) },
  ];

  return (
    <MarqueShell
      user={user}
      path={path}
      onNavigate={setPath}
      dark={dark}
      onToggleTheme={() => setDark(!dark)}
      data={data}
      notifications={NOTIFICATIONS}
      unread={unreadCount}
      onRead={markAllRead}
      palette={palette}
      toasts={toasts}
      onDismissToast={dismiss}
      controls={controls}
    >
      {screen()}
      <CommandPalette
        open={palette.open}
        onClose={() => palette.setOpen(false)}
        routes={MARQUE_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search briefs, deals and brands…"
        footer={(close) => (
          <button type="button" className="mq-link" onClick={() => { close(); goSearch(search); }}>Open the full results screen</button>
        )}
      />
      <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}
    </MarqueShell>
  );
}
/* boot.js publishes a stub on window.TalentDeskApp before anything is fetched,
   so <x-import> always resolves; the real component lands here and the stub
   swaps it in. */
window.__talentDeskReal = App;
if (!window.TalentDeskApp) window.TalentDeskApp = App;

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