/* ============================================================================
 * Methodology IT — mit-patterns.css
 * THE COMPOSITE PATTERN LAYER. Load AFTER mit.css.
 *
 * mit.css gives you primitives — colour, type, buttons, inputs, cards.
 * This file gives you the things that were being re-invented per app:
 * entity lists, drawers, forms, states, confirms, toasts, rich text,
 * recipient fields, attachments, meetings, reports, sign-in, colour
 * schemes, and the Customer Portal shell.
 *
 * SOURCE OF TRUTH. Do not fork it into an app. If an app needs something
 * this doesn't have, add it HERE and re-sync — that discipline is the only
 * reason the tenth app is cheaper to build than the third.
 *
 * Live demo + the written rules behind every class:
 *   mit-brand/mockups/patterns.html  →  "Pattern reference" in the sidebar
 *
 * Distribute with ./sync.sh (lands as /static/patterns.css in each tool).
 * ========================================================================== */

/* ============================================================================
 * MIT PATTERN LAYER
 *
 * Sits ON TOP of mit.css primitives. Defines the composite patterns that were
 * otherwise re-invented in every app: entity list, selection, drawer, form,
 * states, confirm, toast, recipient field, compose & send.
 *
 * Domain-neutral on purpose — nothing here knows what the app is for.
 * ========================================================================== */

/* --- TOKENS --------------------------------------------------------------- */
:root{
  --coral:#DC3C34; --coral-soft:rgba(220,60,52,.08);
  --accent:var(--coral); --accent-soft:var(--coral-soft); --accent-fg:#fff;

  --success:#2F9E5E; --success-soft:#EEF8F2;
  --warn:#E0A93C;    --warn-soft:rgba(224,169,60,.12);
  --info:#2563EB;    --info-soft:rgba(37,99,235,.10);
  --danger:#B91C1C;  --danger-soft:rgba(185,28,28,.10);
  --unknown:#6B7280; --unknown-soft:rgba(107,114,128,.10);

  --font-display:Sora,-apple-system,BlinkMacSystemFont,sans-serif;
  --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --ease:cubic-bezier(.2,.8,.2,1);

  --canvas:#F6F8FB; --surface:#FFF; --surface-alt:#F6F8FB;
  --hairline:#EEF2F7; --divider:#E2E8F0;
  --text:#0F172A; --text-muted:#475569; --text-subtle:#7E8A96;
  --sidebar:#1E242C; --sidebar-fg:#E6EAEF; --sidebar-muted:#8A95A1;
  --shadow:0 1px 2px rgba(15,23,42,.04),0 4px 16px rgba(15,23,42,.04);
  --disabled-fg:#94A3B8; --disabled-bg:#EDF1F6;
  --r-card:14px; --r-sm:10px; --r-pill:999px;
}
html.dark{
  --canvas:#13181E; --surface:#1B2229; --surface-alt:#161B22;
  --hairline:#252D36; --divider:#2B333D;
  --text:#F3F5F7; --text-muted:#A8B2BC; --text-subtle:#7E8A96;
  --sidebar:#161B22;
  --shadow:0 1px 2px rgba(0,0,0,.18),0 6px 18px rgba(0,0,0,.20);
  --disabled-fg:#5C6673; --disabled-bg:#232B34;
  --success-soft:rgba(47,158,94,.14);
}
html[data-accent="amber"]{ --accent:#E89721; --accent-soft:rgba(232,151,33,.12); --accent-fg:#1E242C; }

*,*::before,*::after{box-sizing:border-box}
/* Any display: rule beats the hidden attribute, which silently reveals rows
   that are meant to be closed (Cc, the link bar). Make hidden win. */
[hidden]{display:none!important}
html,body{margin:0;min-height:100%}
body{background:var(--canvas);color:var(--text);font-family:var(--font-body);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}
h1,h2,h3{font-family:var(--font-display);letter-spacing:-.02em;margin:0}
button{font-family:inherit}

/* --- SHELL ---------------------------------------------------------------- */
.app-shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}
.sidebar{background:var(--sidebar);color:var(--sidebar-fg);padding:22px 16px;display:flex;flex-direction:column;gap:22px;position:sticky;top:0;height:100vh}
/* Version + last-updated, pinned bottom-left of the sidebar. Part of the
   template on every app: the version answers "what changed?" on a support
   call, the date answers "is this thing stale?". Version is the app's own
   constant; the date should be computed (document.lastModified / build
   stamp), never hand-maintained — a hand-typed date is wrong within a week. */
/* The NAV scrolls when it overflows — the brand block and the version foot
   stay put. A version line that can scroll out of view isn't in the corner. */
.sidebar nav{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--sidebar-hover,rgba(127,131,140,.3)) transparent}
/* An app with a SECOND nav — Admin, Help, anything pinned under the main list —
   sizes that one to its contents. Without this every nav claims flex:1, the
   column is split evenly between them, and a short Admin group of two items
   takes the same height as a work list of eleven: the main list scrolls with
   its first entries hidden above the fold, which is precisely where the most
   important item sits. Seen on the R&B portal, where Ask, Dashboard and
   Projects were all scrolled out of view by an Admin group of one.
   grow:0 so it never claims free space, shrink:1 so a short window squeezes
   THIS list rather than collapsing the main one to nothing (flex:none here
   crushed the work nav to zero height at 450px tall). */
.sidebar nav ~ nav{flex:0 1 auto;overflow-y:auto}
.sidebar nav::-webkit-scrollbar{width:5px}
.sidebar nav::-webkit-scrollbar-track{background:transparent}
.sidebar nav::-webkit-scrollbar-thumb{background:var(--sidebar-hover,rgba(127,131,140,.3));border-radius:3px}
.ver-foot{margin-top:auto;padding:10px 8px 0;font:500 10px/1.5 var(--font-mono);color:var(--sidebar-muted);flex:none}
.ver-foot .v{color:var(--sidebar-fg);border:1px solid var(--sidebar-muted);border-radius:4px;padding:2px 5px;margin-right:6px}
.brand{display:flex;align-items:center;gap:12px;padding:4px 8px}
.brand-mark{width:38px;height:38px;flex:none;display:grid;place-items:center;border:1.5px solid var(--accent);border-radius:10px;font:800 12px/1 var(--font-display);color:var(--sidebar-active-fg,#fff);overflow:hidden}
.brand-mark img{width:100%;height:100%;object-fit:contain}
.brand-name{font:700 15px/1.2 var(--font-display);color:var(--sidebar-active-fg,#fff)}
.brand-sub{font-size:11.5px;color:var(--sidebar-muted)}
.nav-group{font:700 10.5px/1 var(--font-body);letter-spacing:.14em;text-transform:uppercase;color:var(--sidebar-muted);padding:10px 12px 6px}
.nav-item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 12px;border:none;background:none;border-radius:9px;color:var(--sidebar-fg);font-size:14px;font-weight:500;text-align:left;cursor:pointer;position:relative}
.nav-item:hover{background:var(--sidebar-hover,rgba(127,131,140,.14));color:var(--sidebar-active-fg,#fff)}
.nav-item.is-active{color:var(--sidebar-active-fg,#fff);font-weight:600;background:var(--sidebar-hover,rgba(127,131,140,.14))}
.nav-item.is-active::before{content:"";position:absolute;left:-16px;top:7px;bottom:7px;width:3px;border-radius:0 3px 3px 0;background:var(--accent)}
.main{display:flex;flex-direction:column;min-width:0}
.topbar{display:flex;align-items:center;gap:14px;padding:13px 26px;background:var(--surface);border-bottom:1px solid var(--hairline);position:sticky;top:0;z-index:10}
.topbar h1{font-size:18px;font-weight:700}
.topbar .spacer{flex:1}
/* Capped for readability, but CENTRED — left-aligned, a wide monitor pools all
   the leftover space on one side, which reads as a broken layout rather than a
   deliberate measure. The cap itself grows with the screen: 1180 is right for a
   single-column form and needlessly tight for a page that genuinely owns two
   columns (a ticket beside its info panel, a board beside its filters). It
   never goes uncapped — a form row stretched across 2500px is unreadable, and
   "use the whole window" is how that happens. */
.content{padding:22px 26px 60px;max-width:1180px;width:100%;margin-inline:auto}
@media (min-width:1500px){.content{max-width:1440px}}
@media (min-width:1800px){.content{max-width:1700px}}
@media (min-width:2200px){.content{max-width:1960px}}

/* --- PRIMITIVES ----------------------------------------------------------- */
.surface{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-card);box-shadow:var(--shadow)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:8px;padding:8px 14px;font:600 12.5px/1 var(--font-body);cursor:pointer;border:1px solid transparent;white-space:nowrap}
.btn-primary{background:var(--accent);color:var(--accent-fg);border-color:var(--accent)}
.btn-primary:hover{filter:brightness(1.05)}
.btn-secondary{background:transparent;color:var(--text);border-color:var(--divider)}
.btn-secondary:hover{background:var(--surface-alt)}
.btn-ghost{background:transparent;color:var(--text-muted);border-color:transparent}
.btn-ghost:hover{background:var(--surface-alt);color:var(--text)}
.btn-danger{background:var(--danger);color:#fff;border-color:var(--danger)}
/* Disabled is its own colour, not the enabled colour at reduced opacity —
   a translucent accent still reads as "pressable", especially on dark. */
.btn:disabled{background:var(--disabled-bg);color:var(--disabled-fg);border-color:transparent;cursor:not-allowed;filter:none}
.pill{display:inline-flex;align-items:center;gap:5px;border-radius:var(--r-pill);padding:3px 9px;font:600 11px/1.35 var(--font-body)}
.pill.good{background:var(--success-soft);color:var(--success)}
.pill.warn{background:var(--warn-soft);color:var(--warn)}
.pill.bad{background:var(--danger-soft);color:var(--danger)}
.pill.info{background:var(--info-soft);color:var(--info)}
.pill.unknown{background:var(--unknown-soft);color:var(--unknown)}
.muted{color:var(--text-muted)} .subtle{color:var(--text-subtle)} .mono{font-family:var(--font-mono)}

/* ==========================================================================
   PATTERN 1 — LIST TOOLBAR
   Search left · filters · count · primary action pinned right. Always.
   Count is always "N of M", never a bare N.
   ========================================================================== */
.list-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}
/* .pagehead .sp is scoped to the page header, so a toolbar needed its own
   inline flex:1. Same name, same job, one less inline style per screen. */
.list-toolbar .sp{flex:1}
/* Search sits on its own row above the filters, so it stays put and full
   width instead of being squeezed by however many chips a list happens to
   have — and so the action button never wraps below it. */
.toolbar-search{margin-bottom:10px}
.toolbar-search .searchbox{flex:1 1 100%;max-width:none}
.searchbox{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-sm);padding:8px 12px;min-width:260px;flex:0 1 320px;box-shadow:var(--shadow)}
.searchbox input{border:none;outline:none;background:transparent;width:100%;font:400 13px/1.3 var(--font-body);color:var(--text)}
.searchbox svg{color:var(--text-subtle);flex:none}

/* PATTERN 1g — REPORT TABLE (and printing)
   An entity list helps you FIND a record to act on: hence density toggles,
   per-column filters, a row that opens a drawer. A report answers a question
   and gets shared — none of that furniture applies, and a filter row on a
   figure table reads as clutter you are expected to operate. Keep sorting
   ("which fell most?" is a reading question), drop the rest.
   A report must also PRINT: it is the thing people take into the meeting. */
.rtable{--cols:1fr;display:block;overflow-x:auto}
.rtable .rrow{display:grid;grid-template-columns:var(--cols);gap:12px;align-items:center;
  padding:9px 14px;border-bottom:1px solid var(--hairline)}
.rtable .rrow--head{position:sticky;top:0;background:var(--surface);z-index:2;
  border-bottom:1px solid var(--divider)}
.rtable .rrow--head .sortbtn{font:700 10.5px/1.2 var(--font-body);letter-spacing:.07em;
  text-transform:uppercase;color:var(--text-subtle)}
.rtable .rrow:last-child{border-bottom:0}
.rtable .rrow--total{font-weight:700;border-top:2px solid var(--divider);border-bottom:0;background:var(--surface-alt)}
.rtable .rrow:not(.rrow--head):hover{background:var(--surface-alt)}

/* Screen chrome never prints. The page header goes too: a document carries its
   own title, and printing both gave two headings and a wasted third of a page. */
@media print{
  .sidebar,.topbar,.tabs,.pagehead,.list-toolbar,.colfilters,.btn,.seg,
  .toasts,.drawer,.scrim,.callout{display:none!important}
  .app-shell{display:block}
  .main,.content{margin:0;padding:0;overflow:visible}
  .surface,.stat,.rtable{box-shadow:none;border:1px solid #ddd;break-inside:avoid}
  .statgrid{display:grid;grid-template-columns:repeat(4,1fr)}
  .rtable .rrow--head{position:static}
  .sec-h{break-after:avoid}
  .print-only{display:block!important}
  a[href]:after{content:''}
}
.print-only{display:none}

/* PATTERN 1h — SPEC SHEET (single-record printable document)
   One record, laid out to be READ and SENT: a customer-facing sheet for a part,
   an asset, a room. Distinct from a drawer (which edits) and from a report
   (which aggregates). Two rules earn their keep:
   - The IMAGE SLOT always renders. A sheet whose photo is merely absent looks
     broken; a slot that says what is missing and how to add it is honest, and
     it is the difference between "no photo yet" and "this product has no photo".
   - Fields come in pairs and never collapse to one column on paper, because a
     spec read aloud in a meeting is scanned, not parsed. */
.sheetdoc{max-width:860px}
/* A spec sheet has TWO renderings of one record: the screen form, which obeys
   the app's ordinary page grammar so it doesn't feel like a foreign document,
   and the PRINT form, which is a branded template — masthead, hero, spec table.
   Trying to make one layout serve both gives a cramped screen and a shabby
   handout. `.sheet-print` carries the template; the screen never shows it. */
.sheet-print{display:none}
@media print{
  .sheet-screen{display:none!important}
  /* The sheet fills the page and the footnote sits ON the bottom edge, where a
     document's small print belongs — not floating halfway up with white space
     under it, which reads as a page that ran out rather than one that ended. */
  .sheet-print{display:flex!important;flex-direction:column;min-height:100vh;
    position:relative;color:#111;font-family:var(--font-body)}
  /* Watermark. Deliberately an <img> rather than a CSS background-image:
     browsers drop background graphics from print unless the user ticks a box,
     so a background watermark is one nobody ever sees on paper. Out of flow so
     it can't push the layout, and behind everything so it never competes with
     a figure someone is reading. */
  .sp-mark{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    pointer-events:none;z-index:0;overflow:hidden}
  /* Greyscaled: a colour logo at low opacity reads as a printing fault rather
     than a watermark, and the yellow in particular looks like toner bleed. */
  .sp-mark img{width:58%;max-width:400px;opacity:.06;filter:grayscale(1)}
  .sheet-print>*:not(.sp-mark){position:relative;z-index:1}
  .sheet-print>.sp-body{flex:1}
  .sp-foot{margin-top:auto}

  /* Masthead: mark, the document's name, and who issued it. */
  .sp-band{display:flex;justify-content:space-between;align-items:center;
    background:#111;color:#fff;padding:14px 18px}
  /* The mark sits on its own white chip rather than being knocked out to a
     silhouette — inverting a logo throws away the thing that identifies it. */
  .sp-band img{height:34px;background:#fff;padding:5px 7px;border-radius:4px}
  .sp-band .sp-doc{font:800 15px/1 var(--font-display);letter-spacing:.16em;text-transform:uppercase}
  .sp-band .sp-co{font:400 9.5px/1.5 var(--font-body);text-align:right;opacity:.85}

  /* Hero: title and summary beside the photograph. */
  .sp-hero{display:flex;gap:18px;padding:13px 18px 11px;border-bottom:1px solid #ddd}
  .sp-hero .sp-copy{flex:1;min-width:0}
  .sp-title{font:800 19px/1.16 var(--font-display);margin:0 0 4px}
  .sp-sku{font:500 10.5px/1.3 var(--font-mono);color:#666;margin:0 0 8px}
  .sp-lede{font:400 11.5px/1.55 var(--font-body);color:#333;margin:0}
  .sp-img{width:142px;height:142px;flex:none;border:1px solid #ddd;display:flex;
    align-items:center;justify-content:center;overflow:hidden;background:#fafafa}
  .sp-img img{width:100%;height:100%;object-fit:contain}
  .sp-img .sp-nophoto{font:400 9.5px/1.5 var(--font-body);color:#999;text-align:center;padding:12px}

  /* Body: specifications beside a narrow column of blocks. */
  .sp-body{display:grid;grid-template-columns:1.55fr 1fr;gap:18px;padding:11px 18px}
  .sp-sec{font:700 9.5px/1.2 var(--font-body);letter-spacing:.12em;text-transform:uppercase;
    background:#f0f0f0;padding:5px 9px;margin:0 0 7px}
  .sp-tbl{width:100%;border-collapse:collapse;font-size:10.5px;margin-bottom:10px}
  .sp-tbl td{padding:4px 8px;border-bottom:1px solid #e6e6e6;vertical-align:top}
  .sp-tbl td:first-child{color:#555}
  .sp-tbl td:last-child{font-weight:700;text-align:right}
  .sp-note{font:400 9px/1.45 var(--font-body);color:#777;margin:-6px 0 10px}
  .sp-kv{font-size:10.5px;margin-bottom:12px}
  .sp-kv div{display:flex;justify-content:space-between;gap:10px;padding:3px 0;border-bottom:1px solid #eee}
  .sp-kv b{font-weight:700}
  /* QR sits with the small print, where someone looks after they've read the
     sheet — not competing with the product at the top of the page. */
  .sp-foot{display:flex;gap:14px;align-items:center;padding:8px 18px;
    border-top:2px solid #111;font:400 8.5px/1.45 var(--font-body);color:#666}
  .sp-qr{width:54px;height:54px;flex:none}
  .sp-qr img,.sp-qr svg{width:100%;height:100%;display:block}
  .sp-qrcap{font:600 8.5px/1.3 var(--font-body);color:#111;text-align:center;margin-top:3px}
  /* Only small units avoid breaking. Telling a whole COLUMN not to break
     shunted the entire specification to page two and left page one empty. */
  .sp-tbl,.sp-kv{break-inside:avoid}
  .sp-sec{break-after:avoid}
  /* Sized for US LETTER, which is 20mm shorter than A4 — checking against A4
     is how a sheet passes here and prints on two pages in Anaheim. */
  @page{size:portrait;margin:10mm}
}

/* PATTERN 1f — GLOBAL SEARCH RESULTS
   The topbar searchbox is a jump-to, not a filter: it crosses every entity and
   lands you ON a record. Results are grouped by type because "which kind of
   thing is this" is the first question a mixed list raises. Keyboard is not
   optional — the whole point is not touching the mouse. */
.searchwrap{position:relative;flex:0 1 320px;min-width:260px}
.searchwrap .searchbox{flex:1 1 auto;width:100%}
.gres{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:60;
  background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-sm);
  box-shadow:var(--shadow-lg,0 12px 32px rgba(0,0,0,.14));max-height:min(60vh,440px);overflow-y:auto;padding:5px}
.gres .gsec{font:700 10px/1.2 var(--font-body);letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-subtle);padding:9px 10px 5px}
.gres .gitem{display:flex;align-items:center;gap:10px;width:100%;text-align:left;border:0;background:none;
  padding:7px 10px;border-radius:var(--r-xs,6px);cursor:pointer;font:inherit;color:var(--text)}
.gres .gitem:hover,.gres .gitem.on{background:var(--surface-alt)}
.gres .gitem .gt{font:600 12.5px/1.3 var(--font-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gres .gitem .gc{font:400 11px/1.3 var(--font-body);color:var(--text-muted);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;margin-left:auto;padding-left:10px}
.gres .gempty{padding:14px 12px;font:400 12.5px/1.4 var(--font-body);color:var(--text-muted)}
.gres mark{background:var(--accent-soft);color:inherit;border-radius:2px;padding:0 1px}
.chipbar{display:flex;gap:6px;flex-wrap:wrap}
.chip{border:1px solid var(--divider);background:transparent;border-radius:var(--r-pill);padding:5px 11px;font:600 11.5px/1.2 var(--font-body);color:var(--text-muted);cursor:pointer}
.chip:hover{background:var(--surface-alt)}
.chip.on{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
.chip.rem{background:var(--danger-soft);border-color:var(--danger);color:var(--danger)}
.list-count{font:400 12px/1 var(--font-body);color:var(--text-subtle);white-space:nowrap}

/* ==========================================================================
   PATTERN 2 — ENTITY ROW (.urow)
   Avatar · identity · meta columns · status · actions.
   THE WHOLE ROW OPENS THE DRAWER. No Edit link, no inline row editing.
   ========================================================================== */
.ulist{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:10px}
.urow{display:flex;align-items:center;gap:14px;padding:12px 14px;border:1px solid var(--hairline);border-radius:12px;background:var(--surface);cursor:pointer;transition:border-color .12s var(--ease),box-shadow .12s var(--ease)}
.urow:hover{border-color:var(--divider);box-shadow:var(--shadow)}
.urow.on{border-color:var(--accent)}
.urow.picked{border-color:var(--accent);background:var(--accent-soft)}
.urow .av{width:40px;height:40px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font:700 13px/1 var(--font-display)}
.urow .who{min-width:0;flex:1 1 240px}
/* .nm used to be a flex box so a badge could sit beside the name. That is also
   why a long name ran under the card's edge sliced mid-word, with nothing to
   say it had been cut: text-overflow cannot reach the anonymous flex item that
   a bare text node becomes, so overflow:hidden clipped and no ellipsis ever
   drew. It is a BLOCK now — the ellipsis works on block content — and the
   badges keep their alignment by being inline-aligned rather than flex-aligned.
   The subtitle (.ct) has had this treatment all along; the name now matches. */
.urow .who .nm{font:600 14px/1.3 var(--font-body);color:var(--text);display:block;
  min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.urow .who .nm > *{vertical-align:middle}
.urow .who .nm > .pill,.urow .who .nm > .tchip{margin-left:7px}
.urow .who .ct{font-size:12px;color:var(--text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.urow .col{flex:0 0 auto;min-width:120px;font-size:12.5px;color:var(--text-muted)}
.urow .col .lbl{display:block;font:600 9.5px/1 var(--font-body);letter-spacing:.09em;text-transform:uppercase;color:var(--text-subtle);margin-bottom:3px}
/* .lbl is hidden in list density because the header row carries it. These two
   are the captions that must survive list mode, and they were being re-typed
   inline in every app at a slightly different size:
     .k    a caption UNDER the value, naming what the value IS (SERIAL, MOBILE
           NUMBER) — needed when one column legitimately holds two kinds of thing.
     .sub  a quieter second line of the same fact (who holds it, when it went). */
.urow .col .k{display:block;font:600 9.5px/1 var(--font-body);letter-spacing:.09em;
  text-transform:uppercase;color:var(--text-subtle);margin-top:4px}
.urow .col .sub{display:block;font:400 11px/1.35 var(--font-body);color:var(--text-subtle);margin-top:2px}
/* Numeric columns right-align with tabular figures so digits stack down the
   column and a wrong order of magnitude is visible without reading. */
.urow .col.num,.urow--head .col.num{text-align:right;font-variant-numeric:tabular-nums}
.urow .col.num .v{font:500 13px/1.3 var(--font-mono);color:var(--text)}
.urow .acts{display:flex;gap:4px;align-items:center;margin-left:auto}
/* Inline links inside a row. They inherit colour at rest so a row full of
   contact details doesn't turn into a wall of accent — the link only
   announces itself on hover. Click must not also open the drawer. */
.lnk{color:inherit;text-decoration:none;border-bottom:1px solid transparent}
/* A link rendered as a <button> for keyboard semantics must not inherit button
   chrome, or an inline reference reads as a control you're meant to press. */
button.lnk{border:none;background:none;padding:0;font:inherit;cursor:pointer;
  border-bottom:1px solid transparent}
.lnk:hover{color:var(--accent);border-bottom-color:var(--accent-soft)}
/* .lnk inherits colour so a row of contact details isn't a wall of accent. When
   a link IS the point of the row — the one thing to click — say so with a class
   rather than an inline colour that no theme can reach. */
.lnk.accent{color:var(--accent)}
.urow .col .lnk{font-variant-numeric:tabular-nums}
.urow .who .ct .lnk:hover{color:var(--accent)}
.icon-btn{width:30px;height:30px;border:none;background:transparent;border-radius:8px;color:var(--text-subtle);display:grid;place-items:center;cursor:pointer}
.icon-btn:hover{background:var(--surface-alt);color:var(--accent)}
/* Selection only exists when a bulk action exists. Checkbox is revealed on
   hover, or pinned visible once anything is picked. */
.urow .sel{width:18px;height:18px;flex:none;accent-color:var(--accent);cursor:pointer;opacity:0}
.urow:hover .sel,.urow .sel:focus-visible,.picking .urow .sel{opacity:1}
@media (max-width:820px){.urow{flex-wrap:wrap}.urow .col{min-width:0;flex:1 1 100px}.urow .sel{opacity:1}}

/* PATTERN 2c — DENSITY: CARDS vs LIST
   Same rows, same markup, two densities.
   CARDS  — every cell carries its own label. Comfortable, good for a short
            list you read one item at a time.
   LIST   — labels move to a header row once, rows tighten to a single
            bordered table. Good for scanning many at speed.
   The toggle is per list and never changes what a row does — a click still
   opens the drawer in both. */
.viewtog{display:flex;gap:2px;border:1px solid var(--divider);border-radius:8px;padding:2px;flex:none}
.viewtog button{width:28px;height:24px;border:none;background:transparent;border-radius:6px;cursor:pointer;
  color:var(--text-subtle);display:grid;place-items:center}
.viewtog button:hover{color:var(--text)}
.viewtog button.on{background:var(--accent-soft);color:var(--accent)}

/* List density is a real GRID, not flex. One column template drives both the
   header and every row, so a header cell is always exactly the width of the
   cells under it, and the name column can't swallow the slack and leave a
   gap before the first value.
   Declare every column as minmax(floor, Nfr) — NEVER a bare pixel width.
   A fixed column cannot grow, so on a wide screen all the slack lands on the
   one flexible column (usually the name) and it ends up full of white space
   while a fixed neighbour stays starved and clips its own control. The floor
   is whatever the column's CONTROL needs, not what its text needs: a date
   filter needs 176, a multi-select 130, a status pill 120. Give the fr weights
   in proportion to how much each column benefits from extra room. */
.ulist.as-list{display:block;gap:0;border:1px solid var(--hairline);border-radius:var(--r-card);
  background:var(--surface);box-shadow:var(--shadow);overflow-x:auto;overflow-y:hidden}
/* When the floors add up to more than the viewport, the list SCROLLS — it never
   clips. overflow:hidden here silently amputates the right-hand columns on a
   laptop, and the reader has no way to know a column is missing. The grid's
   own minmax floors decide when that happens; do not force max-content on the
   row, which widens it past what the tracks actually need. */
/* …but a scroller CLIPS, and the tallest thing in this box is a filter popover,
   not a row. On a short list the options list is cut off at the card's edge with
   no scrollbar to say so, and the names below the fold read as names that do not
   exist — Poppy Payne saw a three-tall Owner filter on an empty project and
   asked to be added to a roster she was already in (R&B Wire, 2026-08-21).
   While a popover is open the box stops clipping. A wide list loses its
   horizontal scroll offset for those few seconds; a filter that lies does not
   get to be the cheaper bug. */
.ulist.as-list:has(.multipop){overflow:visible}
.ulist.as-list .urow{display:grid;grid-template-columns:var(--cols);gap:14px;align-items:center;
  border:none;border-bottom:1px solid var(--hairline);border-radius:0;
  padding:9px 16px;background:transparent;box-shadow:none}
.ulist.as-list .urow:last-child{border-bottom:none}
.ulist.as-list .urow:hover{background:var(--surface-alt);box-shadow:none;border-color:var(--hairline)}
.ulist.as-list .av,.ulist.as-list .tile{width:26px;height:26px;font-size:9px;border-radius:7px}
.ulist.as-list .av{border-radius:50%}
.ulist.as-list .who{min-width:0;flex:none}
.ulist.as-list .who .nm{font-size:13px}
.ulist.as-list .who .ct{font-size:11.5px}
.ulist.as-list .col{min-width:0;flex:none;font-size:12.5px}
.ulist.as-list .col .lbl{display:none}          /* the header row carries them now */

/* PATTERN 2f — CARD CONTENT
   The cards density is a RESPONSIVE GRID, not a stack — a board you scan for
   shape, where the list density is a table you read down. Two content shapes,
   both inside the same .urow:
     compact  avatar · name · meta · chips pushed right. For directories.
     summary  heading · description · progress · footer. For things with state
              worth comparing at a glance — projects, campaigns, environments.
   Chips on a card are .tchip: the dense mono variant of .pill, for corners
   where a full pill would crowd the name. Same tone modifiers, so a status
   reads identically whichever density you are in.
   Invariant unchanged: a click opens the record. */
.ulist .urow{padding:14px 16px;border-radius:var(--r-card)}
.ulist .urow.is-summary{display:block}
/* A card is ~330px wide whatever the VIEWPORT is, so the narrow-screen wrap at
   the bottom of Pattern 2 never fires for it: on a desktop a card row with more
   than two meta columns kept laying out as one flex line, the columns' 120px
   floors summed past the card, and the overflow spilled OUT of the card and
   gave the page a horizontal scrollbar — labels sitting on top of the name,
   Fee and By floating in the gutter (TJW change orders, Jake's test pass
   2026-08-28). Cards density therefore wraps unconditionally: the identity
   takes its own full-width line, and the labelled cells flow beneath it. */
.ulist:not(.as-list) .urow{flex-wrap:wrap;row-gap:11px}
.ulist:not(.as-list) .who{flex:1 1 100%}
.ulist:not(.as-list) .col{flex:1 1 auto;min-width:96px}
.ulist:not(.as-list) .col.num{text-align:left}

.tile-h{font:700 14.5px/1.35 var(--font-display);color:var(--text)}
.tile-s{font:400 12.5px/1.45 var(--font-body);color:var(--text-muted);margin-top:3px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.tile-bar{height:7px;background:var(--surface-alt);border-radius:999px;overflow:hidden;margin:13px 0 9px}
.tile-bar > span{display:block;height:100%;border-radius:999px;background:var(--accent)}
.tile-f{display:flex;align-items:center;gap:9px;font:400 12px/1.3 var(--font-body);color:var(--text-subtle)}
.tile-f .faces{margin-left:auto}
.ulist .faces .av{width:26px;height:26px;font-size:9px;border-radius:50%;box-shadow:0 0 0 2px var(--surface)}
.ulist .faces .av+.av{margin-left:-7px}
/* compact card: identity left, chips right */
.urow .who{min-width:0;flex:1 1 auto}
.urow .who .nm{font:600 13.5px/1.25 var(--font-body);color:var(--text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.urow .who .cm{font-size:11.5px;color:var(--text-subtle);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.urow .chips{display:flex;gap:5px;flex:none;margin-left:auto;align-items:center}
.tchip{font:700 9.5px/1 var(--font-mono);letter-spacing:.03em;padding:4px 7px;border-radius:6px;
  flex:none;background:var(--surface-alt);color:var(--text-muted);white-space:nowrap}
.tchip.good{background:var(--success-soft);color:var(--success)}
.tchip.warn{background:var(--warn-soft);color:var(--warn)}
.tchip.bad{background:var(--danger-soft);color:var(--danger)}
.tchip.info{background:var(--info-soft);color:var(--info)}
.tchip.accent{background:var(--accent-soft);color:var(--accent)}
.tchip.ghost{background:transparent;border:1px dashed var(--divider);color:var(--text-subtle)}

/* Header cells are all one size — same font, same weight, same colour,
   whatever the column holds. */
.urow--head{cursor:default;background:var(--surface-alt);padding:0 16px!important}
.urow--head:hover{background:var(--surface-alt)!important}
.sortbtn{display:flex;align-items:center;gap:4px;width:100%;border:none;background:none;cursor:pointer;
  padding:9px 0;text-align:left;
  font:600 10px/1.2 var(--font-body);letter-spacing:.09em;text-transform:uppercase;color:var(--text-subtle)}
.sortbtn:hover{color:var(--text)}
.sortbtn.on{color:var(--accent)}
.sortbtn .arrow{font-size:8px;opacity:0}
.sortbtn.on .arrow{opacity:1}
.sortbtn.num{justify-content:flex-end;text-align:right}
/* Direction from an attribute, so a header rendered without an explicit
   .arrow span still says which way it sorted. Lets a generic click-to-sort
   handler light up any hand-rolled table head without touching its markup. */
.sortbtn[data-sort]{color:var(--accent)}
.sortbtn[data-sort]::after{content:'\25B2';font-size:7px;margin-left:3px}
.sortbtn[data-sort="desc"]::after{content:'\25BC'}

/* PATTERN 2d — COLUMN FILTERS
   Every column that can be filtered carries its own control, sitting directly
   under its header so the control is always the width of the data it filters.
   This REPLACES the global search box on an entity list: one search that
   guesses which field you meant is worse than a filter per column that
   doesn't have to.
   Control follows the data type, and is not a choice:
     text        free text, matches as you type
     enum        multi-select — statuses, owners, anything from a fixed set.
                 Never a single select; "show me open AND in progress" is the
                 normal question, not the exotic one.
     date        an operator (before / on / after) plus a date.
     number      operator plus a value, same shape as date.
   A filter NEVER repeats its own column header. The header directly above
   already names it, so the control is blank at rest — a "Task…" placeholder
   under a TASK header is the same word twice and buys nothing. Multi-selects
   read "Any" because that is the current value, not a label.
   A filtered column marks itself, so you can see WHICH column is narrowing
   the list without opening anything. Reset clears every filter at once and
   is disabled — not hidden — when nothing is active, so its absence is never
   mistaken for a list that can't be filtered.
   A NO-RESULTS state never removes the controls that produced it. Replace the
   rows, keep the header and the filter row — otherwise a filter matching
   nothing strands you, unable to widen the selection or add a second value
   without a full reset. */
.colfilters{display:grid;grid-template-columns:var(--cols);gap:14px;align-items:start;
  padding:8px 16px;background:var(--surface);border-bottom:1px solid var(--hairline)}
.colf{position:relative;min-width:0}
.colf :is(input,select){width:100%;border:1px solid var(--divider);border-radius:7px;padding:5px 8px;
  font:400 12px/1.3 var(--font-body);color:var(--text);background:var(--surface);outline:none}
.colf :is(input,select):focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.colf input::placeholder{color:var(--text-subtle)}
.colf.is-on :is(input,select){border-color:var(--accent)}
/* multi-select trigger — reads as a control at rest, as accent when narrowing */
.colf .multi{width:100%;display:flex;align-items:center;gap:6px;border:1px solid var(--divider);
  border-radius:7px;padding:5px 8px;background:var(--surface);cursor:pointer;
  font:400 12px/1.3 var(--font-body);color:var(--text-subtle);text-align:left}
.colf .multi:hover{border-color:var(--text-subtle)}
.colf .multi.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent);font-weight:600}
.colf .multi .caret{margin-left:auto;font-size:9px;opacity:.7}
.multipop{position:absolute;top:calc(100% + 4px);left:0;min-width:190px;z-index:40;
  background:var(--surface);border:1px solid var(--divider);border-radius:10px;
  box-shadow:0 10px 30px rgba(0,0,0,.16);padding:6px;max-height:min(58vh,420px);overflow:auto}
/* A scrollbar on a list that nearly fits is a trap: options above the fold read
   as absent, and the one you want is the one you can't see. Give it room first,
   scroll only when the list is genuinely long. */
.multipop label{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:7px;
  font:400 12.5px/1.3 var(--font-body);color:var(--text);cursor:pointer}
.multipop label:hover{background:var(--surface-alt)}
.multipop input[type=checkbox]{width:15px;height:15px;accent-color:var(--accent);flex:none}
.multipop .pop-f{display:flex;gap:6px;padding:6px 4px 2px;border-top:1px solid var(--hairline);margin-top:4px}
/* date / number filter — the VALUE is a native date input, so the control opens
   the platform's own calendar rather than a bespoke one nobody has learned. The
   operator (before / on / after) is a chip row directly beneath it, revealed
   once the cell is in use so an unused filter stays one line tall.
   The operator cannot live inside the picker: that popup is browser chrome and
   nothing can be rendered into it. Beneath the input is as close as it gets,
   and it reads as part of the same control. */
.colf .datef{display:block;min-width:0}
.colf .datef > input[type=date]{width:100%;min-width:0;border:1px solid var(--divider);
  border-radius:7px;padding:5px 8px;font:400 12px/1.3 var(--font-body);color:var(--text);
  background:var(--surface);outline:none}
.colf .datef > input[type=date]:hover{border-color:var(--text-subtle)}
.colf .datef > input[type=date]:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.colf .dateops{display:none;gap:4px;margin-top:5px}
.colf .datef:focus-within .dateops,.colf.is-on .dateops{display:flex}
.colf .dateops .chip{padding:2px 8px;font:600 10.5px/1.35 var(--font-body);text-transform:lowercase}
/* reset lives in the toolbar with the count, and stays visible but disabled */
.filters-reset{border:1px solid var(--divider);background:var(--surface);border-radius:8px;
  padding:5px 11px;font:600 11.5px/1.2 var(--font-body);color:var(--text-muted);cursor:pointer;white-space:nowrap}
.filters-reset:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
.filters-reset:disabled{color:var(--disabled-fg,#A8B0BA);background:var(--disabled-bg,var(--surface-alt));
  border-color:var(--hairline);cursor:not-allowed}

/* PATTERN 2e — KANBAN DENSITY
   The third density, alongside cards and list. Columns are the values of ONE
   enum — usually status — and the card shows only what you need to triage
   without opening it. Same invariant as the other densities: a click opens
   the record, density is presentation only. Filters and sort still apply;
   kanban narrows what is on the board, it does not escape the filters. */
.kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(232px,1fr);gap:12px;
  overflow-x:auto;padding-bottom:8px;align-items:start}
.kcol{background:var(--surface-alt);border:1px solid var(--hairline);border-radius:var(--r-card);padding:10px;min-height:110px}
.kcol-h{display:flex;align-items:center;gap:7px;padding:2px 4px 10px;
  font:600 10px/1.2 var(--font-body);letter-spacing:.09em;text-transform:uppercase;color:var(--text-subtle)}
.kcol-h .n{margin-left:auto;font-variant-numeric:tabular-nums;background:var(--surface);
  border:1px solid var(--hairline);border-radius:var(--r-pill);padding:1px 7px;color:var(--text-muted)}
.kcol-h .dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--unknown)}
/* Tone modifiers, so a board column's dot is `class="dot ${TONE[s]}"` instead of
   a tone-to-CSS-variable ternary re-typed in each app's JS — which is how two
   screens end up disagreeing about what colour "blocked" is. */
.kcol-h .dot.good{background:var(--success)}
.kcol-h .dot.warn{background:var(--warn)}
.kcol-h .dot.info{background:var(--info)}
.kcol-h .dot.bad{background:var(--danger)}
.kcol-h .dot.accent{background:var(--accent)}
.kcol-h .dot.unknown{background:var(--unknown)}
.kcard{background:var(--surface);border:1px solid var(--hairline);border-radius:10px;padding:10px 11px;
  margin-bottom:8px;cursor:pointer;box-shadow:var(--shadow)}
.kcard:last-child{margin-bottom:0}
.kcard:hover{border-color:var(--divider)}
.kcard .t{font:600 12.5px/1.35 var(--font-body);color:var(--text)}
.kcard .s{font:400 11.5px/1.35 var(--font-body);color:var(--text-subtle);margin-top:2px}
.kcard .m{display:flex;align-items:center;gap:8px;margin-top:9px;font-size:11.5px;color:var(--text-subtle)}
.kcard .m .av{width:22px;height:22px;font-size:8px;border-radius:50%}
.kcol .empty-line{padding:18px 8px;font-size:12px}

/* A standalone avatar. Previously .av only existed scoped inside .urow,
   .dhead and .token, so an avatar on a page header or a contact row had no
   styling at all. Same values; the scoped rules still win where they apply. */
.av{width:40px;height:40px;flex:none;border-radius:50%;background:var(--accent-soft);
  color:var(--accent);display:grid;place-items:center;font:700 13px/1 var(--font-display)}
.av img{width:100%;height:100%;object-fit:contain;border-radius:inherit;background:var(--surface)}

/* PATTERN 2b — SELECTION BAR. Appears only with a selection; says the count,
   offers the bulk actions, and always offers Clear. */
.selbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;border-radius:12px;background:var(--accent-soft);border:1px solid var(--accent);margin-bottom:10px}
.selbar .n{font:600 12.5px/1 var(--font-body);color:var(--accent)}

/* ==========================================================================
   PATTERN 3 — DRAWER. THE editor surface. Never a modal for editing.
   ========================================================================== */
/* A scrim that is merely transparent is still a sheet of glass over the whole
   page. Without this, closing a drawer leaves an invisible z-index:75 layer
   swallowing every click and the app looks fine but does nothing — which is
   what happened after closing the AI panel on 2026-08-11, and why three
   different apps had independently grown a manual `style.display` dance around
   it. Fade out, then stop existing to the pointer. */
.scrim{position:fixed;inset:0;background:rgba(10,13,17,.42);z-index:75;opacity:0;pointer-events:none;transition:opacity .2s var(--ease)}
.scrim.on{opacity:1;pointer-events:auto}
.drawer{position:fixed;top:0;right:0;height:100vh;width:min(480px,94vw);background:var(--surface);z-index:76;box-shadow:-12px 0 40px rgba(0,0,0,.22);display:flex;flex-direction:column;transform:translateX(100%);transition:transform .24s var(--ease)}
.drawer.on{transform:translateX(0)}
.dhead{display:flex;align-items:flex-start;gap:12px;padding:18px 20px;border-bottom:1px solid var(--hairline)}
.dhead .av{width:40px;height:40px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font:700 13px/1 var(--font-display)}
.dhead h3{font:600 16px/1.25 var(--font-display)}
.dhead .sub{font-size:12px;color:var(--text-subtle);margin-top:2px}
.dhead .x{margin-left:auto;border:none;background:none;font-size:20px;line-height:1;color:var(--text-subtle);cursor:pointer;padding:2px 4px}
.dbody{padding:18px 20px;overflow-y:auto;flex:1}
.dfoot{display:flex;gap:8px;align-items:center;padding:14px 20px;border-top:1px solid var(--hairline)}
.dfoot .spacer{flex:1}
.grp{font:600 10.5px/1 var(--font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin:20px 0 8px}
.grp:first-child{margin-top:0}
.grp.info{color:var(--info)}
.kv{display:grid;grid-template-columns:110px 1fr;gap:6px 12px;font-size:12.5px;margin:0}
.kv dt{color:var(--text-subtle);font-weight:600;font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;padding-top:2px}
.kv dd{margin:0;color:var(--text);word-break:break-word}

/* ==========================================================================
   PATTERN 4 — FORM
   label · control · help OR error (never both). Required = accent dot.
   Footer: destructive far left · spacer · Cancel · primary. Always.
   ========================================================================== */
.field{margin-bottom:14px}
.field > label{display:flex;align-items:center;gap:5px;font:600 11px/1 var(--font-body);letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px}
.field > label .req{width:5px;height:5px;border-radius:50%;background:var(--accent);flex:none}
.input,.field :is(input,select,textarea){width:100%;border:1px solid var(--divider);border-radius:8px;padding:8px 10px;font:400 13px/1.35 var(--font-body);color:var(--text);background:var(--surface);outline:none}
.field textarea{min-height:74px;resize:vertical}
.input:focus,.field :is(input,select,textarea):focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.field :is(input,select,textarea):disabled{background:var(--surface-alt);color:var(--text-subtle);cursor:not-allowed}
.help{display:block;font:400 11.5px/1.4 var(--font-body);color:var(--text-subtle);margin-top:5px}
.err{display:flex;align-items:center;gap:5px;font:500 11.5px/1.4 var(--font-body);color:var(--danger);margin-top:5px}
.field.has-error :is(input,select,textarea){border-color:var(--danger)}
.field-row{display:flex;gap:12px}
.field-row .field{flex:1;min-width:0}
.optcard{display:flex;gap:10px;padding:10px 12px;border-radius:9px;border:1px solid var(--divider);cursor:pointer;margin-bottom:8px}
.optcard.on{border-color:var(--accent);background:var(--accent-soft)}
.optcard .t{display:block;font:600 13px/1.3 var(--font-body);color:var(--text)}
.optcard .h{display:block;font:400 12px/1.4 var(--font-body);color:var(--text-subtle);margin-top:2px}

/* ==========================================================================
   PATTERN 4b — SETTABLE VALUE
   A value the user is allowed to change IS the control. It reads as text at
   rest, shows its affordance on hover, and commits on change. No "Edit" button
   beside it, and no drawer to change one field.
   This does NOT contradict "no inline row editing": that rule governs rows in
   an entity list, where a click already means open and a second meaning would
   be ambiguous. On a record page there is no such ambiguity.
   Unset reads as italic muted "not set", never as blank — blank is
   indistinguishable from a value that failed to load.
   Fixed set of options = select. Free text = input.
   Specificity note: brand.css styles input[type=text] at (0,1,1), so the text
   variant is declared as input.settext to match it.
   ========================================================================== */
.setfield{appearance:none;-webkit-appearance:none;width:auto;max-width:100%;border:1px solid transparent;background:transparent;
  border-radius:7px;padding:3px 26px 3px 8px;margin-left:-8px;font:inherit;color:var(--text);cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--text-subtle) 50%),
                   linear-gradient(135deg,var(--text-subtle) 50%,transparent 50%);
  background-position:calc(100% - 13px) calc(50% + 1px),calc(100% - 9px) calc(50% + 1px);
  background-size:4px 4px,4px 4px;background-repeat:no-repeat}
.setfield:hover{border-color:var(--divider);background-color:var(--surface-alt)}
.setfield:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
  background-color:var(--surface)}
.setfield.is-unset{color:var(--text-subtle);font-style:italic}
.setfield.good{color:var(--success)}.setfield.info{color:var(--info)}
.setfield.warn{color:var(--warn)}.setfield.unknown{color:var(--text-muted)}

/* A settable value whose display form is a STATUS reads as a chip, not as
   text — same pill the read-only status wears, except it opens. One chip
   carrying the current value beats a row of segmented buttons: the row spends
   horizontal space showing you four things you didn't pick, and grows every
   time a status is added. The chip says what IS, and offers the rest on
   demand. Read-only status = .pill. Settable status = .setfield.as-chip. */
.setfield.as-chip{margin-left:0;border-radius:var(--r-pill);padding:4px 24px 4px 11px;
  font:600 11px/1.35 var(--font-body);letter-spacing:.02em;text-transform:uppercase;
  background-color:var(--unknown-soft);color:var(--unknown);
  /* The caret is drawn in the CHIP's own colour, not --text-subtle. Inherited
     from .setfield it was grey-on-tinted-grey and effectively invisible: a
     Draft chip standing in a row of solid buttons read as a DISABLED button,
     and a tester concluded there was "no place to update the status" when the
     control was the first thing in the header (TJW change orders, 2026-08-28).
     The hairline says the same thing again — a pill you can open has an edge;
     a .pill, which is only a label, does not. */
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
                   linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-position:calc(100% - 11px) calc(50% + 1px),calc(100% - 7px) calc(50% + 1px);
  border-color:currentColor}
.setfield.as-chip:hover{filter:brightness(.97);border-color:currentColor}
/* Disabled means the CLIENT set it — no caret to promise an interaction that
   is not on offer, and no edge, so it falls back to reading as a plain label. */
.setfield.as-chip:disabled{background-image:none;border-color:transparent;padding-right:11px;cursor:default}
.setfield.as-chip:focus{border-color:transparent;box-shadow:0 0 0 3px var(--accent-soft)}
.setfield.as-chip.good{background-color:var(--success-soft);color:var(--success)}
.setfield.as-chip.warn{background-color:var(--warn-soft);color:var(--warn)}
.setfield.as-chip.bad{background-color:var(--danger-soft);color:var(--danger)}
.setfield.as-chip.info{background-color:var(--info-soft);color:var(--info)}
.setfield.as-chip.unknown{background-color:var(--unknown-soft);color:var(--unknown)}
input.setdate{width:auto;border:1px solid transparent;background:transparent;border-radius:7px;
  padding:3px 8px;margin-left:-8px;font:inherit;color:var(--text);cursor:pointer}
input.setdate:hover{border-color:var(--divider);background:var(--surface-alt)}
input.setdate:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
  background:var(--surface)}
input.settext{width:100%;max-width:340px;border:1px solid transparent;background:transparent;
  border-radius:7px;padding:4px 8px;margin-left:-8px;font:inherit;color:var(--text)}
input.settext:hover{border-color:var(--divider);background:var(--surface-alt)}
input.settext:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
  background:var(--surface)}

/* ==========================================================================
   PATTERN 4c — FIELD PROVENANCE
   On a record, every field is either OURS — stored by this app, therefore
   editable in place — or THEIRS, owned by a system of record, therefore static
   and labelled with where it came from. Never leave the reader to work it out:
   an un-editable field with no explanation reads as a broken control, and an
   editable copy of somebody else's data is a second source of truth.
   One line under the group explains the tag rather than repeating it per row.
   Rows breathe: a label column, hairline separators, values at 1.5 line
   height. A key/value grid at 6px gap is how a record ends up squished.
   ========================================================================== */
.frow{display:grid;grid-template-columns:minmax(120px,150px) 1fr;gap:16px;align-items:center;
  padding:11px 0;border-bottom:1px solid var(--hairline)}
.frow:last-of-type{border-bottom:none}
/* A .frow was sized for a drawer. In a narrow container — a record page's side
   rail, a 340px panel — the 150px label column leaves the value too little room
   for a real control ("ticket #" + Link + its help line all fighting over
   150px). Put `.fstack` on the container and the rows stack themselves at the
   width where side-by-side stops working. A container query rather than a
   per-row modifier, so the same panel markup travels between a wide page and a
   narrow rail without anyone re-deciding. */
.fstack{container-type:inline-size}
@container (max-width:430px){
  .fstack .frow{grid-template-columns:1fr;gap:5px;align-items:start}
  .fstack .frow > .fl{font-weight:600;color:var(--text-subtle);font-size:11.5px;
    letter-spacing:.04em;text-transform:uppercase}
}
.frow > .fl{font:500 12.5px/1.4 var(--font-body);color:var(--text-muted)}
/* overflow-wrap:anywhere because the values that land here are exactly the ones
   with no spaces to break at — an email address, a token, a BAQ name. Without
   it a long address runs straight out of a narrow panel and off the card. */
.frow > .fv{font:400 13.5px/1.5 var(--font-body);color:var(--text);min-width:0;
  display:flex;align-items:center;gap:9px;flex-wrap:wrap;overflow-wrap:anywhere}
.frow .src{font:500 9.5px/1 var(--font-body);letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-subtle);background:var(--surface-alt);border:1px solid var(--hairline);
  border-radius:var(--r-pill);padding:3px 7px;white-space:nowrap}
@media (max-width:560px){.frow{grid-template-columns:1fr;gap:4px}}

/* Two layouts for record fields, and the choice is about VALUE LENGTH, not
   taste:
     .frow   label left, value right, one per line. For long or free-text
             values — a pricing note, an address, a description — where the
             value needs the width.
     .fgrid  label above value, fields flowing into columns. For SHORT values —
             a date, a name, a status, a count. Stacking six of those down a
             full-width panel wastes most of the row and pushes everything
             below the fold for no gain.
   A panel of short values laid out as .frow is the single most common way a
   record page ends up feeling empty. */
.fgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px 24px}
.fgrid .fcell{min-width:0}
.fgrid .fl{display:block;font:600 10.5px/1 var(--font-body);letter-spacing:.07em;
  text-transform:uppercase;color:var(--text-subtle);margin-bottom:6px}
.fgrid .fv{font:400 13.5px/1.4 var(--font-body);color:var(--text);min-width:0;
  display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.fgrid .fcell.wide{grid-column:1/-1}

/* A CHILD LIST's add action sits BELOW the list. The list is what you came to
   read; the action is what you do having read it, and putting it above pushes
   the content down and reads as a header. The page-level primary action is
   different — that stays top-right in the page header, because it is what the
   whole screen is for. */
.list-add{display:flex;justify-content:flex-start;margin-top:12px}

/* ==========================================================================
   PATTERN 4d — PAGE HEADER, STAT TILE, ACTIVITY THREAD
   The three things every screen in every app needed, and which each app was
   otherwise re-inventing slightly differently.

   PAGE HEADER — title, one line of context, actions pinned right. The subtitle
   says what the screen is FOR, not what it is called; "Tasks / Every to-do in
   one place" earns its line, "Tasks / Task list" does not.

   STAT TILE — a number, its label, and one line of context underneath. The
   context line is the point: a bare 4 is trivia, "4 parts needing an order /
   2 urgent" is a decision. Not to be confused with .mbr .kpi-strip, which is
   the report document's figure row.

   ACTIVITY THREAD — a record's history. System entries and human notes in one
   timeline, because "who changed this" and "what did they say about it" are
   the same question asked twice.
   ========================================================================== */
.pagehead{display:flex;align-items:flex-start;gap:14px;margin-bottom:18px;flex-wrap:wrap}
.pagehead h1{font:800 25px/1.2 var(--font-display);margin:0;color:var(--text)}
.pagehead .sp{flex:1}
.pagesub{font:400 13px/1.5 var(--font-body);color:var(--text-muted);margin-top:3px}

.stat{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-card);
  padding:15px 17px;box-shadow:var(--shadow);min-width:0}
.stat b{display:block;font:800 25px/1.1 var(--font-display);font-variant-numeric:tabular-nums;color:var(--text)}
.stat span{display:block;font:500 12px/1.3 var(--font-body);color:var(--text-muted);margin-top:3px}
.stat em{display:block;font:400 11.5px/1.3 var(--font-body);color:var(--text-subtle);font-style:normal;margin-top:2px}
.stat.is-alert b{color:var(--danger)}
/* A stat tile that FILTERS the list below it. The outline (not a border) keeps
   the tile the same size selected or not, so a row of them doesn't shift by a
   pixel as you click through — which is what makes them feel like a filter
   rather than a set of buttons. */
.stat.pick{cursor:pointer;outline:2px solid transparent;outline-offset:-1px;transition:outline-color .12s var(--ease)}
.stat.pick:hover{outline-color:var(--divider)}
.stat.pick.on{outline-color:var(--accent)}
.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(178px,1fr));gap:12px}

.thread{display:flex;flex-direction:column}
.tev{display:flex;gap:11px;padding:12px 0;border-bottom:1px solid var(--hairline)}
.tev:last-child{border-bottom:none}
.tev .ic{width:26px;height:26px;flex:none;border-radius:8px;display:grid;place-items:center;
  background:var(--surface-alt);color:var(--text-subtle);font-size:12px}
/* The body column every thread entry needs. Previously each app hand-rolled it
   with an inline min-width:0 and its own two type sizes, which is why the same
   timeline row rendered at three different weights across the estate. */
.tev .b{min-width:0;flex:1}
.tev .t{font:600 12.5px/1.4 var(--font-body);color:var(--text)}
.tev .m{font:400 11.5px/1.5 var(--font-body);color:var(--text-subtle);margin-top:3px}
/* An entry still running — a loan not yet returned, a task not yet done. The
   icon carries it, not the row, so an open entry doesn't shout over the text. */
.tev.open .ic{background:var(--info-soft);color:var(--info)}

/* ==========================================================================
   PATTERN 5 — STATES. loading · empty · no-results · error · data.
   A list renders exactly one. Never a bare blank area.
   ========================================================================== */
.state{display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;padding:52px 24px;background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-card)}
.state .ic{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;background:var(--surface-alt);color:var(--text-subtle)}
.state h3{font:600 15px/1.3 var(--font-display);color:var(--text)}
.state p{margin:0;font-size:12.5px;color:var(--text-subtle);max-width:380px}
.state.is-error .ic{background:var(--danger-soft);color:var(--danger)}
.skel{height:66px;border-radius:12px;border:1px solid var(--hairline);background:linear-gradient(90deg,var(--surface) 0%,var(--surface-alt) 50%,var(--surface) 100%);background-size:200% 100%;animation:sk 1.2s ease-in-out infinite}
@keyframes sk{0%{background-position:200% 0}100%{background-position:-200% 0}}

/* ==========================================================================
   PATTERN 6 — CONFIRM. The only correct use of a centred modal.
   ========================================================================== */
.modal-wrap{position:fixed;inset:0;background:rgba(10,13,17,.42);z-index:90;display:grid;place-items:center;padding:16px}
.modal{width:100%;max-width:440px;background:var(--surface);border-radius:var(--r-card);padding:22px;box-shadow:0 24px 60px rgba(0,0,0,.28);max-height:86vh;overflow-y:auto}
.modal h3{font:700 16px/1.25 var(--font-display);margin-bottom:6px}
.modal p{margin:0 0 16px;font-size:13px;color:var(--text-muted)}
.modal .acts{display:flex;gap:8px;justify-content:flex-end;margin-top:18px}

/* ==========================================================================
   PATTERN 7 — TOAST. Confirms a completed write. Bottom right, 3s.
   ========================================================================== */
.toasts{position:fixed;right:20px;bottom:20px;z-index:95;display:flex;flex-direction:column;gap:8px}
.toast{display:flex;align-items:center;gap:9px;background:var(--surface);border:1px solid var(--hairline);border-left:3px solid var(--success);border-radius:10px;padding:11px 15px;box-shadow:0 10px 30px rgba(0,0,0,.16);font:500 12.5px/1.3 var(--font-body);color:var(--text);animation:tin .2s var(--ease)}
.toast.bad{border-left-color:var(--danger)}
@keyframes tin{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* ==========================================================================
   PATTERN 8 — RECIPIENT FIELD
   The one way a person is added to a message, anywhere in the estate.
   A committed recipient is ALWAYS a token — never loose text in an input,
   because loose text is what silently sends to nobody.
   Commit: Enter · comma · semicolon · blur · picking a suggestion.
   Backspace on an empty input removes the last token.
   A typed address that isn't valid still becomes a token, marked bad, so it
   is visible and removable rather than silently dropped.
   ========================================================================== */
.recip-wrap{position:relative}
.recip{display:flex;flex-wrap:wrap;align-items:center;gap:6px;border:1px solid var(--divider);border-radius:8px;padding:6px 8px;background:var(--surface);min-height:40px;cursor:text}
.recip.focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.recip.has-error{border-color:var(--danger)}
.recip input{flex:1;min-width:150px;border:none;outline:none;background:transparent;padding:4px 2px;font:400 13px/1.35 var(--font-body);color:var(--text)}
.token{display:inline-flex;align-items:center;gap:6px;max-width:100%;background:var(--surface-alt);border:1px solid var(--hairline);border-radius:var(--r-pill);padding:3px 7px 3px 3px;font:500 12px/1.3 var(--font-body);color:var(--text)}
.token .av{width:20px;height:20px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font:700 9px/1 var(--font-display)}
.token .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.token .x{border:none;background:none;cursor:pointer;color:var(--text-subtle);padding:0 1px;font-size:14px;line-height:1}
.token .x:hover{color:var(--danger)}
.token.bad{background:var(--danger-soft);border-color:rgba(185,28,28,.32);color:var(--danger)}
.token.bad .av{background:transparent;color:var(--danger)}
.suggest{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--hairline);border-radius:10px;box-shadow:0 12px 30px rgba(15,23,42,.16);z-index:30;max-height:236px;overflow-y:auto;padding:5px}
.s-item{display:flex;align-items:center;gap:10px;padding:7px 9px;border-radius:8px;cursor:pointer}
.s-item.on,.s-item:hover{background:var(--surface-alt)}
.s-item .av{width:26px;height:26px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font:700 10px/1 var(--font-display)}
.s-item .nm{font:600 13px/1.2 var(--font-body)}
.s-item .em{font-size:11.5px;color:var(--text-subtle)}
.s-empty{padding:14px;text-align:center;font-size:12.5px;color:var(--text-subtle)}

/* ==========================================================================
   PATTERN 9 — COMPOSE & SEND
   Sending is irreversible, so it is confirm-gated and the confirm names
   every recipient. Send is disabled until there is at least one recipient
   and a subject. Success is a toast; the composer resets.
   ========================================================================== */
.compose{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-card);box-shadow:var(--shadow);overflow:visible}
.crow{display:flex;gap:12px;align-items:flex-start;padding:11px 16px;border-bottom:1px solid var(--hairline)}
.crow > .lb{width:44px;flex:none;padding-top:11px;font:600 10.5px/1 var(--font-body);letter-spacing:.08em;text-transform:uppercase;color:var(--text-subtle)}
.crow > .ct{flex:1;min-width:0}
/* Subject gets the same box as To and Cc — a bare underline-less input beside
   two bordered fields reads as broken, not as minimal. No placeholder: the
   label already says Subject, and repeating it is noise. */
/* Also matches a subject cell nested inside a two-up compose row (To and
   Subject side by side), which is how a reply composer keeps the writing area
   near the top instead of pushing it down behind fields nobody edits. */
.crow.subject > .ct input,.crow .subject > .ct input{width:100%;border:1px solid var(--divider);border-radius:8px;background:var(--surface);
  outline:none;padding:9px 10px;font:600 13.5px/1.35 var(--font-body);color:var(--text);min-height:40px}
.crow.subject > .ct input:focus,.crow .subject > .ct input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.cc-toggle{display:flex;gap:8px;align-items:center;padding-top:11px}
.cc-toggle button{border:none;background:none;cursor:pointer;font:600 11.5px/1 var(--font-body);color:var(--text-subtle);padding:2px}
.cc-toggle button:hover{color:var(--accent)}
.cbody{padding:14px 16px}
.cbody textarea{width:100%;min-height:180px;border:none;outline:none;resize:vertical;background:transparent;font:400 13.5px/1.6 var(--font-body);color:var(--text)}
.cfoot{display:flex;align-items:center;gap:10px;padding:12px 16px;border-top:1px solid var(--hairline)}
.cfoot .spacer{flex:1}
.recap{font:400 12px/1.4 var(--font-body);color:var(--text-subtle)}
.recap b{color:var(--text);font-weight:600}

/* ==========================================================================
   PATTERN 10 — RICH TEXT (memo fields)
   Long-form fields — memos, notes, message bodies — are rich text. Identity
   fields (name, email, phone) never are.
   The toolbar is a FIXED set: bold, italic, lists, link, clear. No font,
   size or colour pickers — those are how a shared design system dies.
   Pasted content is sanitised to the allowed tags, so a paste out of Word or
   Outlook can't smuggle in its own fonts and colours.
   ========================================================================== */
.rte{border:1px solid var(--divider);border-radius:8px;background:var(--surface);overflow:hidden}
.rte.focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.rte-bar{display:flex;align-items:center;gap:2px;padding:5px 7px;border-bottom:1px solid var(--hairline);background:var(--surface-alt);flex-wrap:wrap}
.rte-b{min-width:28px;height:28px;padding:0 6px;border:none;background:transparent;border-radius:6px;color:var(--text-muted);cursor:pointer;font:600 12.5px/1 var(--font-body);display:inline-flex;align-items:center;justify-content:center}
.rte-b:hover{background:var(--surface);color:var(--text)}
.rte-b.on{background:var(--accent-soft);color:var(--accent)}
.rte-sep{width:1px;height:18px;background:var(--divider);margin:0 4px}
.rte-body{padding:10px 12px;outline:none;font:400 13px/1.6 var(--font-body);color:var(--text);overflow-y:auto;max-height:340px}
.rte-body:empty::before,.rte-body.is-empty::before{content:attr(data-ph);color:var(--text-subtle);pointer-events:none}
.rte-body p{margin:0 0 8px}
.rte-body ul,.rte-body ol{margin:0 0 8px;padding-left:22px}
.rte-body li{margin:2px 0}
.rte-body a{color:var(--accent);text-decoration:underline}
/* Attachments belong to the note, so they sit INSIDE the editor — a footer
   under the body, icon left. Putting them outside makes them look like a
   property of the record rather than of the thing being written. */
.rte-foot{display:flex;align-items:center;gap:8px;padding:7px 9px;border-top:1px solid var(--hairline);
  background:var(--surface-alt)}
.rte-foot .spacer{flex:1}
.rte-attach{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:0 8px;border:none;
  background:transparent;border-radius:6px;color:var(--text-muted);cursor:pointer;
  font:500 12px/1 var(--font-body)}
.rte-attach:hover{background:var(--surface);color:var(--accent)}
.rte-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:0}
.rte-link{display:flex;gap:6px;align-items:center;padding:7px;border-bottom:1px solid var(--hairline);background:var(--surface-alt)}
.rte-link input{flex:1;border:1px solid var(--divider);border-radius:6px;padding:5px 8px;font:400 12.5px/1.3 var(--font-body);background:var(--surface);color:var(--text);outline:none}
.rte-count{font:400 11px/1 var(--font-body);color:var(--text-subtle);margin-left:auto;padding-right:2px}

/* ==========================================================================
   PATTERN 11 — DIRECTORY PICKER
   The other half of the recipient field. Typing is for people you can name;
   the picker is for people you're choosing from. Same tokens come out.
   ========================================================================== */
.pickbtn{flex:none;border:1px dashed var(--divider);background:transparent;border-radius:var(--r-pill);
  padding:5px 11px;font:600 11.5px/1.2 var(--font-body);color:var(--text-muted);cursor:pointer;white-space:nowrap}
.pickbtn:hover{border-color:var(--accent);color:var(--accent);border-style:solid}
.picklist{max-height:320px;overflow-y:auto;margin:4px -6px 0;padding:0 6px}
.pickrow{display:flex;align-items:center;gap:10px;padding:8px 9px;border-radius:9px;cursor:pointer}
.pickrow:hover{background:var(--surface-alt)}
.pickrow.on{background:var(--accent-soft)}
.pickrow input{width:17px;height:17px;flex:none;accent-color:var(--accent);cursor:pointer}
.pickrow .av{width:28px;height:28px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font:700 10px/1 var(--font-display)}
.pickrow .nm{font:600 13px/1.2 var(--font-body)}
.pickrow .em{font-size:11.5px;color:var(--text-subtle)}
.pickrow.already{opacity:.5;cursor:default}
.pickrow.already:hover{background:transparent}

/* ==========================================================================
   PATTERN 12 — ATTACHMENTS
   Files are chosen with a button OR dropped anywhere on the composer. Every
   attached file shows its name and size and can be removed individually.
   The size cap is enforced in the UI, not discovered by the mail server.
   ========================================================================== */
.dropzone{position:relative}
.dropzone.over::after{content:"Drop files to attach";position:absolute;inset:0;z-index:5;
  display:grid;place-items:center;border:2px dashed var(--accent);border-radius:var(--r-sm);
  background:var(--accent-soft);color:var(--accent);font:600 13px/1 var(--font-body)}
.attach-list{display:flex;flex-direction:column;gap:6px;padding:10px 16px;border-top:1px solid var(--hairline)}
.attach{display:flex;align-items:center;gap:10px;padding:7px 10px;border:1px solid var(--hairline);border-radius:9px;background:var(--surface-alt)}
.attach .ic{width:28px;height:28px;flex:none;border-radius:7px;background:var(--surface);border:1px solid var(--hairline);
  display:grid;place-items:center;font:700 8.5px/1 var(--font-mono);color:var(--text-muted);text-transform:uppercase}
.attach .nm{flex:1;min-width:0;font:500 12.5px/1.3 var(--font-body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.attach .sz{font:400 11.5px/1 var(--font-mono);color:var(--text-subtle);flex:none}
.attach .x{border:none;background:none;cursor:pointer;color:var(--text-subtle);font-size:15px;line-height:1;padding:0 2px;flex:none}
.attach .x:hover{color:var(--danger)}
.attach.over-cap{border-color:rgba(185,28,28,.35);background:var(--danger-soft)}
/* A screenshot is the commonest attachment on a feedback thread, and a filename
   tells you nothing about one — so the icon slot carries a thumbnail when the
   file is an image, and clicking it opens the full size. */
.attach .ic img{width:100%;height:100%;object-fit:cover;border-radius:6px;display:block}
.attach.img .ic{padding:0;overflow:hidden;cursor:zoom-in}
/* Attachments already ON a message, as opposed to pending in a composer. */
.tshots{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.tshot{border:1px solid var(--hairline);border-radius:9px;overflow:hidden;background:var(--surface);
  cursor:zoom-in;display:block;line-height:0}
.tshot img{width:104px;height:74px;object-fit:cover;display:block}
.tshot.file{line-height:1.3;padding:7px 10px;font:500 12px/1.3 var(--font-body);color:var(--text-muted);
  display:inline-flex;align-items:center;gap:7px;cursor:pointer}
.tshot.file .sz{font:400 11px/1 var(--font-mono);color:var(--text-subtle)}
/* Paste is the fastest path from Cmd-Shift-4 to a report — say so, don't hide it. */
.pastehint{font:400 11.5px/1.4 var(--font-body);color:var(--text-subtle);margin-top:6px}
.pastehint kbd{font:600 10.5px/1 var(--font-mono);border:1px solid var(--divider);border-bottom-width:2px;
  border-radius:5px;padding:3px 5px;background:var(--surface-alt);color:var(--text-muted)}
.attach-total{display:flex;align-items:center;gap:6px;font:400 11.5px/1.3 var(--font-body);color:var(--text-subtle);padding-top:2px}
.attach-total.bad{color:var(--danger)}

/* ==========================================================================
   PATTERN 13 — THING ROW
   Same row as a person, one substitution: a rounded-square TYPE TILE instead
   of a circular initials avatar. Circles are people; squares are things.
   That single rule is what stops every list looking like a contact list.
   ========================================================================== */
/* Standalone, NOT scoped to .urow — the same tile has to work in a drawer
   header and anywhere else a thing is identified. */
.tile{width:40px;height:40px;flex:none;border-radius:10px;display:grid;place-items:center;
  font:700 9px/1 var(--font-mono);text-transform:uppercase;background:var(--surface-alt);
  border:1px solid var(--hairline);color:var(--text-muted)}
.tile.pdf{background:var(--danger-soft);border-color:rgba(185,28,28,.2);color:var(--danger)}
.tile.xls{background:var(--success-soft);border-color:rgba(47,158,94,.22);color:var(--success)}
.tile.doc{background:var(--info-soft);border-color:rgba(37,99,235,.2);color:var(--info)}
.tile.img{background:var(--warn-soft);border-color:rgba(224,169,60,.28);color:var(--warn)}
/* The same tints under TONE names, for a register of things that aren't files —
   equipment, rooms, circuits, parts. A tile tinted by kind is what lets a mixed
   list be scanned by shape instead of read line by line, and reusing .pdf to
   mean "hotspot" is how a class name stops meaning anything.
   Same fills as .tchip's tones; unlike .tchip these also carry a border, because
   a tile is a bounded shape and a chip is not.
   ON A TILE THESE MEAN KIND, NOT STATUS — the row's pill says how the thing is
   doing. Prefer the fixed hues over .accent for a category: accent is
   scheme-owned, so an accent tile changes colour per scheme and can collide
   with whichever fixed tone the new accent resembles. */
.tile.good{background:var(--success-soft);border-color:rgba(47,158,94,.22);color:var(--success)}
.tile.warn{background:var(--warn-soft);border-color:rgba(224,169,60,.28);color:var(--warn)}
.tile.info{background:var(--info-soft);border-color:rgba(37,99,235,.2);color:var(--info)}
.tile.bad{background:var(--danger-soft);border-color:rgba(185,28,28,.2);color:var(--danger)}
.tile.accent{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
.tile.unknown{background:var(--unknown-soft);border-color:var(--hairline);color:var(--unknown)}

/* ==========================================================================
   PATTERN 14 — SIGN-IN (emailed link, no password)
   Two states on one surface: the form, then "check your email". Never a
   redirect to a different-looking page between them.
   ========================================================================== */
.signin{min-height:calc(100vh - 160px);display:grid;place-items:center;padding:20px}
.signinbox{width:100%;max-width:400px;background:var(--surface);border:1px solid var(--hairline);
  border-radius:var(--r-card);box-shadow:var(--shadow);padding:30px 28px}
.signinbox .mark{width:44px;height:44px;display:grid;place-items:center;border:1.5px solid var(--accent);
  border-radius:11px;font:800 13px/1 var(--font-display);color:var(--accent);margin-bottom:16px}
.signinbox h2{font:700 20px/1.2 var(--font-display);margin-bottom:6px}
.signinbox .lead{font:400 13px/1.55 var(--font-body);color:var(--text-muted);margin:0 0 18px}
.signinbox label{display:block;font:600 11px/1 var(--font-body);letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px}
.signinbox input{width:100%;border:1px solid var(--divider);border-radius:8px;padding:10px 12px;
  font:400 13.5px/1.35 var(--font-body);color:var(--text);background:var(--surface);outline:none}
.signinbox input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.signinbox .btn{width:100%;margin-top:12px;padding:11px 14px;font-size:13px}
.signinbox .foot{margin-top:20px;padding-top:14px;border-top:1px solid var(--hairline);
  font:400 11.5px/1.4 var(--font-body);color:var(--text-subtle);text-align:center}
.note-bad{display:flex;gap:8px;align-items:flex-start;background:var(--danger-soft);border:1px solid rgba(185,28,28,.3);
  border-radius:9px;padding:10px 12px;font:500 12.5px/1.45 var(--font-body);color:var(--text);margin-bottom:14px}
.note-ok{display:flex;gap:8px;align-items:flex-start;background:var(--success-soft);border:1px solid rgba(47,158,94,.3);
  border-radius:9px;padding:10px 12px;font:500 12.5px/1.45 var(--font-body);color:var(--text);margin-bottom:14px}
.sent-ic{width:46px;height:46px;border-radius:12px;display:grid;place-items:center;background:var(--accent-soft);color:var(--accent);margin-bottom:16px}

/* ==========================================================================
   PATTERN 15a — MEETINGS REGISTER  (copied from the Dojo meetings tab)
   A meeting list is a normal list grid plus four things a plain list
   doesn't have: group headers, a type badge, source chips, and a
   follow-up pill. Scheduled meetings sit at the top, tinted, because
   what is coming is more actionable than what already happened.
   ========================================================================== */
.grouphd{font:700 10.5px/1 var(--font-body);letter-spacing:.08em;text-transform:uppercase;color:var(--text-subtle);
  display:flex;align-items:center;gap:9px;padding:14px 16px 8px;background:var(--surface)}
.grouphd span{font-weight:400;text-transform:none;letter-spacing:0;font-size:11.5px;color:var(--text-subtle)}
.ulist.as-list .grouphd{border-bottom:1px solid var(--hairline)}
.urow.sched{background:var(--info-soft)}
.urow.sched:hover{background:var(--info-soft);filter:brightness(.985)}

/* type of meeting — a closed set, like the timeline's moment kinds */
.ty{display:inline-flex;font:700 9px/1 var(--font-body);letter-spacing:.05em;text-transform:uppercase;
  padding:5px 8px;border-radius:6px;white-space:nowrap}
.ty.checkin{background:var(--info-soft);color:var(--info)}
.ty.internal{background:var(--unknown-soft);color:var(--text-muted)}
.ty.sales{background:var(--accent-soft);color:var(--accent)}
.ty.vendor{background:rgba(91,33,182,.12);color:#5b21b6}
.ty.project{background:var(--success-soft);color:var(--success)}
html.dark .ty.vendor{background:rgba(167,139,250,.16);color:#c4b5fd}

/* what we actually have for this meeting — a recording, notes, or both */
.src{display:inline-flex;gap:4px}
.src i{font:700 9px/1 var(--font-mono);font-style:normal;border:1px solid var(--divider);color:var(--text-subtle);
  border-radius:5px;padding:4px 6px;white-space:nowrap}
.src i.ff{border-color:var(--accent);color:var(--accent)}
.src i.note{border-color:var(--success);color:var(--success)}

.fu{font:700 11px/1 var(--font-mono);border-radius:var(--r-pill);padding:5px 9px;white-space:nowrap;display:inline-block}
.fu.open{background:var(--warn-soft);color:var(--warn)}
.fu.clear{background:var(--success-soft);color:var(--success)}
.fu.none{background:var(--chip,var(--surface-alt));color:var(--text-subtle);font-weight:400}

/* --- segmented view switch (Dojo) — one register, three ways in --------- */
.seg{display:inline-flex;align-items:center;gap:2px;background:var(--surface-alt);
  border:1px solid var(--hairline);border-radius:10px;padding:3px}
.seg button{font:600 12.5px/1 var(--font-body);color:var(--text-muted);padding:8px 14px;border:none;
  background:none;border-radius:8px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:7px}
.seg button:hover{color:var(--text)}
.seg button.on{background:var(--surface);color:var(--accent);box-shadow:var(--shadow)}
.seg .cnt{font:700 10px/1 var(--font-mono);background:var(--surface-alt);border-radius:var(--r-pill);
  padding:3px 6px;color:var(--text-subtle)}
.seg button.on .cnt{background:var(--accent-soft);color:var(--accent)}

/* --- wide drawer: a record with TABS, not a form -------------------------
   480px is for editing one record. 860px is for a record you read — tabs,
   prose, lists. Anything wider than that and it should have been a page. */
.drawer.wide{width:min(860px,96vw)}
.dacts{display:flex;gap:8px;margin:14px 0 0;flex-wrap:wrap}
.dtabs{display:flex;gap:2px;margin:16px 0 0}
.dtabs button{font:600 12.5px/1 var(--font-body);color:var(--text-muted);padding:10px 13px 12px;border:none;
  background:none;border-bottom:2px solid transparent;cursor:pointer;display:flex;gap:6px;align-items:center}
.dtabs button:hover{color:var(--text)}
.dtabs button.on{color:var(--accent);border-bottom-color:var(--accent)}
.dtabs .cnt{font:700 9.5px/1 var(--font-mono);background:var(--surface-alt);border-radius:var(--r-pill);
  padding:3px 6px;color:var(--text-subtle)}
.sec-h{font:700 10.5px/1 var(--font-body);letter-spacing:.08em;text-transform:uppercase;
  color:var(--text-subtle);margin:22px 0 10px}
.sec-h:first-child{margin-top:0}
.prose{font:400 13px/1.65 var(--font-body);color:var(--text-muted)}
.kw{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.kw span{font:600 10.5px/1 var(--font-body);background:var(--surface-alt);color:var(--text-muted);
  border-radius:6px;padding:6px 9px}
.att{display:flex;gap:10px;flex-wrap:wrap}
.att .p{display:flex;align-items:center;gap:8px;border:1px solid var(--hairline);border-radius:9px;
  padding:7px 11px 7px 8px;font-size:12px}
.att .p .nm{font-weight:600}
.att .p .em{color:var(--text-subtle);font-size:10.5px;font-family:var(--font-mono)}
/* External attendees are marked — who was in the room from outside is the
   thing you check before sending anything on. */
.att .p.ext{border-color:var(--accent);background:var(--accent-soft)}
.att .p .av{width:22px;height:22px;border-radius:50%;background:var(--surface-alt);color:var(--text-muted);
  display:grid;place-items:center;font:700 9px/1 var(--font-display);flex:none}
.att .p.ext .av{background:var(--accent);color:#fff}

/* --- follow-up rows ------------------------------------------------------ */
.fitem{display:flex;gap:11px;align-items:flex-start;padding:13px 0;border-bottom:1px solid var(--divider)}
.fitem:last-child{border-bottom:none}
.fitem.done{opacity:.55}
.fitem .own{flex:none}
.fitem .own .av{width:26px;height:26px;border-radius:50%;background:var(--surface-alt);color:var(--text-muted);
  display:grid;place-items:center;font:700 9.5px/1 var(--font-display)}
.fitem .own .av.me{background:var(--accent-soft);color:var(--accent)}
.fitem .bd{flex:1;min-width:0}
.fitem .tx{font:500 13px/1.5 var(--font-body);color:var(--text)}
.fitem .fm{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px;font-size:11px;color:var(--text-subtle)}
.fitem .stamp{font:400 10.5px/1 var(--font-mono);color:var(--accent);cursor:pointer;border:1px solid var(--accent);
  background:var(--accent-soft);border-radius:5px;padding:4px 6px}
/* Where the item came from — a machine heard it, or you wrote it down. */
.fitem .from{font:700 9px/1 var(--font-body);letter-spacing:.05em;text-transform:uppercase;border-radius:5px;padding:4px 7px}
.fitem .from.ff{background:var(--accent-soft);color:var(--accent)}
.fitem .from.me{background:var(--success-soft);color:var(--success)}
.fitem .acts{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}
.st{display:inline-flex;font:700 9.5px/1 var(--font-body);letter-spacing:.04em;text-transform:uppercase;
  padding:5px 9px;border-radius:var(--r-pill);white-space:nowrap}
.st.open{background:var(--warn-soft);color:var(--warn)}
.st.prog{background:var(--info-soft);color:var(--info)}
.st.closed{background:var(--success-soft);color:var(--success)}
.st.task{background:var(--unknown-soft);color:var(--text-muted)}
/* The two states a ticket queue actually turns on. `waiting` is deliberately
   the quietest colour on the list — the ball is with them and there is nothing
   to do. `updated` is the loudest, because it means they have come back and
   nobody has looked yet, and that is the one a queue loses people over. */
.st.waiting{background:var(--unknown-soft);color:var(--text-subtle)}
.st.updated{background:var(--accent-soft);color:var(--accent)}
.btn-sm{font:600 11px/1 var(--font-body);color:var(--accent);background:var(--surface);border:1px solid var(--accent);
  border-radius:7px;padding:7px 11px;cursor:pointer;white-space:nowrap}
.btn-sm:hover{background:var(--accent);color:var(--accent-fg)}
.btn-sm.ghost{color:var(--text-muted);border-color:var(--divider)}
.btn-sm.ghost:hover{background:var(--surface-alt);color:var(--text)}
.banner-info{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:var(--info-soft);
  border:1px solid rgba(37,99,235,.25);border-radius:9px;padding:11px 14px;
  font:400 12.5px/1.5 var(--font-body);color:var(--text)}
.savedhint{font-size:11px;color:var(--text-subtle);margin-top:8px}
.empty-line{text-align:center;padding:40px 20px;color:var(--text-subtle);font-size:13px}

/* --- recordings feed ----------------------------------------------------- */
.recfeed{display:flex;gap:13px;align-items:flex-start;padding:15px 0;border-bottom:1px solid var(--divider)}
.recfeed:last-child{border-bottom:none}
.recfeed .thumb{width:54px;height:54px;border-radius:11px;background:var(--accent-soft);color:var(--accent);
  display:grid;place-items:center;flex:none}
.recfeed .bd{flex:1;min-width:0}
.recfeed .t{font:600 13.5px/1.35 var(--font-body)}
.recfeed .m{font-size:11.5px;color:var(--text-subtle);margin-top:4px}
.recfeed .acts{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap;align-items:center}
.match{font:600 11px/1 var(--font-body);color:var(--text-muted);background:var(--surface-alt);border-radius:7px;padding:6px 9px}
.match b{color:var(--text)}
.match.auto{background:var(--success-soft);color:var(--success)} .match.auto b{color:var(--success)}
.match.no{background:var(--warn-soft);color:var(--warn)} .match.no b{color:var(--warn)}

/* PATTERN 15b — SCHEDULE
   Two ways to book, and the difference is who finds out. */
.sched-modes .optcard .t{display:flex;align-items:center;gap:7px}
.repeat-note{font:400 11.5px/1.45 var(--font-body);color:var(--text-subtle);margin-top:-4px;margin-bottom:12px}
.repeat-locked{opacity:.5}

/* ==========================================================================
   PATTERN 15 — MEETING RECORDING
   Adapted from the tabletop timeline, which is the right shape for anything
   with a clock running through it. A recording is NOT a drawer: it owns child
   content (timeline, transcript, actions), so per the drawer rule it gets a
   full page.
   The timeline is the primary view — not the transcript. A wall of transcript
   is what nobody reads; a timeline of tagged moments is what gets used.
   ========================================================================== */
.rec-head{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:14px}
.rec-head h2{font:700 20px/1.25 var(--font-display)}
.rec-head .meta{font-size:12.5px;color:var(--text-subtle);margin-top:4px}
.backlink{display:inline-flex;align-items:center;gap:5px;border:none;background:none;cursor:pointer;
  font:600 12px/1 var(--font-body);color:var(--text-muted);padding:0;margin-bottom:12px}
.backlink:hover{color:var(--accent)}
.faces{display:flex;align-items:center}
.faces .av{width:28px;height:28px;border-radius:50%;background:var(--accent-soft);color:var(--accent);
  display:grid;place-items:center;font:700 9.5px/1 var(--font-display);border:2px solid var(--surface);margin-left:-7px}
.faces .av:first-child{margin-left:0}
.faces .more{font-size:11.5px;color:var(--text-subtle);margin-left:8px}

/* Player — the scrubber is the spine every jump link drives. */
.player{display:flex;align-items:center;gap:14px;padding:14px 16px;background:var(--surface);
  border:1px solid var(--hairline);border-radius:var(--r-card);box-shadow:var(--shadow);margin-bottom:16px}
.playbtn{width:40px;height:40px;flex:none;border:none;border-radius:50%;background:var(--accent);color:var(--accent-fg);
  cursor:pointer;display:grid;place-items:center}
.playbtn:hover{filter:brightness(1.05)}
.rec__bar{flex:1;height:6px;border-radius:var(--r-pill);background:var(--surface-alt);overflow:hidden;cursor:pointer}
.rec__bar span{display:block;height:100%;background:var(--accent);transition:width .18s var(--ease)}
.rec__time{font:500 12px/1 var(--font-mono);color:var(--text-muted);white-space:nowrap;flex:none}

/* Tabs — timeline first, always. */
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--hairline);margin-bottom:16px}
.tab{border:none;background:none;cursor:pointer;padding:9px 14px;font:600 12.5px/1 var(--font-body);
  color:var(--text-muted);border-bottom:2px solid transparent;margin-bottom:-1px}
.tab:hover{color:var(--text)}
.tab.on{color:var(--accent);border-bottom-color:var(--accent)}
.tab .n{font:600 10.5px/1 var(--font-body);color:var(--text-subtle);margin-left:5px}

/* Timeline */
.ev-session{margin:20px 0 6px;font:700 11px/1 var(--font-body);letter-spacing:.08em;text-transform:uppercase;
  color:var(--accent);border-top:1px dashed var(--accent-soft);padding-top:12px}
.ev-session:first-child{margin-top:0;border-top:none;padding-top:0}
.ev{display:flex;gap:16px;padding:14px 0 14px 18px;border-left:2px solid var(--hairline);margin-left:56px;position:relative}
.ev__side{position:absolute;left:-56px;top:14px;width:48px;text-align:right}
.ev__clock{display:block;font:400 11.5px/1.2 var(--font-mono);color:var(--text-muted)}
.ev__tag{display:block;font:700 9.5px/1.2 var(--font-body);text-transform:uppercase;letter-spacing:.05em;margin-top:3px;color:var(--text-subtle)}
.ev__body{font:400 13.5px/1.65 var(--font-body);color:var(--text);min-width:0}
.ev::before{content:"";position:absolute;left:-6px;top:19px;width:10px;height:10px;border-radius:var(--r-pill);
  background:var(--divider);border:2px solid var(--surface)}
.ev--topic::before{background:var(--text-muted)}
.ev--decision::before{background:var(--success)}
.ev--action::before{background:var(--info)}
.ev--gap::before{background:var(--warn)}
.ev--risk::before{background:var(--danger)}
.ev--decision .ev__tag{color:var(--success)}
.ev--action   .ev__tag{color:var(--info)}
.ev--gap      .ev__tag{color:var(--warn)}
.ev--risk     .ev__tag{color:var(--danger)}
.ev.is-playing{background:var(--accent-soft);border-radius:0 10px 10px 0}
.ev.is-playing::before{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.ev q{display:block;font-style:italic;color:var(--text);margin:4px 0;padding-left:10px;border-left:3px solid var(--accent-soft)}
.ev .who{display:block;font:600 11.5px/1.3 var(--font-body);color:var(--text-muted);margin-top:6px}
.ev .note{font:400 12.5px/1.5 var(--font-body);color:var(--text-subtle);margin-top:4px}
.jump{display:inline-flex;align-items:center;gap:4px;margin-top:7px;border:none;background:none;padding:0;
  font:600 11.5px/1 var(--font-body);color:var(--accent);cursor:pointer}
.jump:hover{text-decoration:underline}

/* Transcript + actions */
.tr{display:flex;gap:14px;padding:11px 0;border-bottom:1px solid var(--hairline)}
.tr:last-child{border-bottom:none}
.tr .t{flex:none;width:48px;font:400 11.5px/1.6 var(--font-mono);color:var(--text-subtle);cursor:pointer}
.tr .t:hover{color:var(--accent)}
.tr .s{font:600 12.5px/1.5 var(--font-body);color:var(--text-muted);flex:none;width:110px}
.tr .x{font:400 13px/1.6 var(--font-body);color:var(--text);min-width:0}
.act{display:flex;gap:11px;align-items:flex-start;padding:11px 0;border-bottom:1px solid var(--hairline)}
.act:last-child{border-bottom:none}
.act input{width:17px;height:17px;flex:none;margin-top:2px;accent-color:var(--accent);cursor:pointer}
.act .t{font:500 13px/1.45 var(--font-body);color:var(--text)}
.act.done .t{text-decoration:line-through;color:var(--text-subtle)}
.act .m{font:400 11.5px/1.4 var(--font-body);color:var(--text-subtle);margin-top:3px}

/* ==========================================================================
   PATTERN 16 — REPORTS TAB  (copied from portal.methodologyit.tech)
   Grouped by month, newest first. A report row answers three questions
   without being opened: what it is, when it covers, and what's in it.
   ========================================================================== */
.rep-month{font:700 12px/1 var(--font-body);letter-spacing:.04em;color:var(--text-subtle);
  text-transform:uppercase;margin:20px 2px 10px}
.rep-month:first-child{margin-top:0}
.rep{display:flex;align-items:center;gap:16px;background:var(--surface);border:1px solid var(--hairline);
  border-radius:14px;box-shadow:var(--shadow);padding:16px 18px;margin-bottom:11px}
.rep:hover{border-color:var(--divider)}
.rep__icon{width:46px;height:46px;flex:0 0 46px;border-radius:11px;display:grid;place-items:center}
.rep__icon.t-mbr{background:var(--accent-soft);color:var(--accent)}
.rep__icon.t-dev{background:var(--info-soft);color:var(--info)}
.rep__icon.t-wk{background:var(--surface-alt);color:var(--text-muted)}
.rep__icon.t-net{background:var(--warn-soft);color:var(--warn)}
.rep__icon.t-lic{background:var(--success-soft);color:var(--success)}
.rep__meta{min-width:0;flex:1}
.rep__title{font:600 15px/1.2 var(--font-body);color:var(--text);display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.rep__tag{font:700 10px/1 var(--font-body);letter-spacing:.05em;text-transform:uppercase;color:var(--success);
  background:var(--success-soft);border-radius:5px;padding:3px 7px}
.rep__line{font:400 12.5px/1.3 var(--font-body);color:var(--text-subtle);margin-top:4px;
  display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.rep__pill{font:600 11px/1 var(--font-body);padding:3px 9px;border-radius:20px}
.rep__pill.t-mbr{background:var(--accent-soft);color:var(--accent)}
.rep__pill.t-dev{background:var(--info-soft);color:var(--info)}
.rep__pill.t-wk{background:var(--surface-alt);color:var(--text-muted)}
.rep__pill.t-net{background:var(--warn-soft);color:var(--warn)}
.rep__pill.t-lic{background:var(--success-soft);color:var(--success)}
.rep__hl{color:var(--text-muted);font-weight:500}
.rep__actions{display:flex;gap:8px;flex:0 0 auto}
.rv-overlay{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:60;display:flex;flex-direction:column}
.rv-bar{height:56px;flex:0 0 56px;background:var(--surface);border-bottom:1px solid var(--hairline);
  display:flex;align-items:center;gap:12px;padding:0 18px}
.rv-title{font:600 14px/1.2 var(--font-display);color:var(--text)}
.rv-frame{flex:1;overflow-y:auto;background:#E0E2E5;padding:32px 0}

/* ==========================================================================
   PATTERN 17 — MBR REPORT PAGE  (the house style, from ~/reports)
   THE default for every client-facing report. Not the app shell — a page of
   the monthly document, so a client gets one consistent package.
   Everything is scoped under .mbr because this is a document inside an app,
   and its tokens must not leak into the app around it.
   ========================================================================== */
.mbr{--coral:#DC3C34;--slate-900:#1E242C;--slate-700:#384452;--slate-300:#A8B2BC;
  --slate-100:#E6EAEF;--slate-050:#F3F5F7;--paper:#FAFAF7;
  --ink-1:#0F172A;--ink-2:#334155;--ink-3:#475569;--ok:#2F9E5E;--amber:#E89721;
  color:var(--ink-1)}
.mbr .page{width:8.5in;max-width:calc(100vw - 40px);min-height:11in;background:var(--paper);
  margin:0 auto 32px;padding:64px;box-shadow:0 6px 28px rgba(0,0,0,.06);display:flex;flex-direction:column}
.mbr .page-header{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px;
  border-bottom:1px solid var(--slate-100);margin-bottom:28px;font-size:10px;color:var(--ink-3)}
.mbr .page-header .left{display:flex;align-items:center;gap:10px}
.mbr .page-header .mark{width:16px;height:16px;background:var(--slate-900);border-radius:3px;color:#fff;
  font:700 10px/1 var(--font-display);display:inline-flex;align-items:center;justify-content:center}
.mbr .page-header strong{font-weight:600;color:var(--slate-700)}
.mbr .page-header .dot{color:var(--slate-300);margin:0 2px}
.mbr .page-header .right{font-family:var(--font-mono)}
.mbr .section-eyebrow{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.mbr .section-eyebrow .badge{background:var(--coral);color:#fff;font:500 10px/1 var(--font-mono);
  padding:4px 7px;border-radius:4px}
.mbr .section-eyebrow .label{font:600 11px/1 var(--font-body);text-transform:uppercase;
  color:var(--coral);letter-spacing:.16em}
/* The headline is a STATEMENT with the number in it, never a topic label. */
.mbr h2.section-title{font:700 34px/1.08 var(--font-display);letter-spacing:-.025em;color:var(--ink-1);
  margin:0 0 22px;max-width:600px}
.mbr p.lead{font:400 14px/1.65 var(--font-body);color:var(--ink-2);max-width:620px;margin:0 0 26px}
.mbr .kpi-strip{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--slate-100);
  border-radius:10px;background:#fff;overflow:hidden;margin-bottom:28px}
.mbr .kpi-cell{padding:18px 22px;border-left:1px solid var(--slate-100);display:flex;flex-direction:column;min-height:130px}
.mbr .kpi-cell:first-child{border-left:none}
.mbr .kpi-cell .eyebrow{font:600 10px/1 var(--font-body);text-transform:uppercase;color:var(--ink-3);
  letter-spacing:.14em;margin-bottom:8px}
.mbr .kpi-cell .number{font:700 28px/1 var(--font-display);font-variant-numeric:tabular-nums;
  letter-spacing:-.02em;color:var(--ink-1);margin:4px 0 auto}
.mbr .kpi-cell .number .unit{font:500 14px/1 var(--font-body);color:var(--ink-3);margin-left:2px}
.mbr .kpi-cell .sub{font-size:11px;margin-top:8px;color:var(--ink-3);display:flex;align-items:center;gap:6px}
.mbr .kpi-cell .sub.good{color:var(--ok)} .mbr .kpi-cell .sub.warn{color:var(--amber)} .mbr .kpi-cell .sub.bad{color:var(--coral)}
.mbr .kpi-cell .dot{width:5px;height:5px;border-radius:50%;display:inline-block}
.mbr .kpi-cell .dot.good{background:var(--ok)} .mbr .kpi-cell .dot.warn{background:var(--amber)} .mbr .kpi-cell .dot.bad{background:var(--coral)}
.mbr table.data{width:100%;border-collapse:collapse;font-size:12.5px;margin-bottom:24px}
.mbr table.data th{text-align:left;padding:8px 0;font:600 9.5px/1.4 var(--font-body);text-transform:uppercase;
  letter-spacing:.12em;color:var(--ink-3);border-bottom:1px solid var(--slate-100)}
.mbr table.data th.right{text-align:right}
.mbr table.data td{padding:7px 0;border-bottom:1px solid var(--slate-100);color:var(--ink-1);font-size:12px}
.mbr table.data td.right{text-align:right;font-variant-numeric:tabular-nums}
.mbr table.data td:not(:last-child),.mbr table.data th:not(:last-child){padding-right:14px}
.mbr table.data tr:last-child td{border-bottom:none}
.mbr .barwrap{display:inline-flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums;color:var(--ink-2)}
.mbr .barwrap .bar{width:120px;height:5px;background:var(--slate-100);border-radius:3px;overflow:hidden}
.mbr .barwrap .fill{height:100%;background:var(--ok);display:block}
.mbr .barwrap .fill.warn{background:var(--amber)} .mbr .barwrap .fill.bad{background:var(--coral)}
.mbr .two-col{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-bottom:24px}
.mbr .col-header{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--font-body);text-transform:uppercase;
  letter-spacing:.14em;color:var(--ink-1);padding-bottom:12px;border-bottom:1px solid var(--slate-100);margin-bottom:4px}
.mbr .col-header.success{color:var(--ok)} .mbr .col-header.amber{color:var(--amber)}
.mbr .item{padding:12px 0;border-top:1px solid var(--slate-100)}
.mbr .item:first-of-type{border-top:none}
.mbr .item .title{font:600 13px/1.4 var(--font-body);color:var(--ink-1);margin-bottom:2px}
.mbr .item .sub{font:400 12px/1.5 var(--font-body);color:var(--ink-3)}
/* Exactly ONE recommendation per section, and it sits at the foot of the page. */
.mbr .recommendation{background:var(--slate-900);color:#fff;border-left:4px solid var(--coral);
  padding:20px 24px;border-radius:10px;margin:auto 0 16px}
.mbr .recommendation .eyebrow{font:600 10px/1 var(--font-body);text-transform:uppercase;color:var(--coral);
  letter-spacing:.14em;margin-bottom:8px}
.mbr .recommendation .body{font:500 16px/1.45 var(--font-display);letter-spacing:-.01em;color:#fff}
.mbr .page-footer{display:flex;justify-content:space-between;align-items:center;font-size:9px;color:var(--ink-3);
  letter-spacing:.04em;padding-top:14px;border-top:1px solid var(--slate-100);margin-top:auto}
.mbr .page-footer .right{font:500 9px/1 var(--font-mono)}

/* --- MBR cover page ------------------------------------------------------
   The report opens with a cover. Dark hero over paper: wordmark, a coral
   rule + classification eyebrow, and a two-tone title where the SUBJECT is
   coral and the period is white. Below the fold: who it's for, what's
   inside, and the document code. */
.mbr .page.cover{padding:0;height:11in;overflow:hidden}
.mbr .hero{background:var(--slate-900);color:#fff;height:52%;padding:48px 64px;position:relative;overflow:hidden}
.mbr .hero-bg-grid{position:absolute;inset:0;opacity:.04;
  background-image:linear-gradient(rgba(255,255,255,.7) 1px,transparent 1px),
                   linear-gradient(90deg,rgba(255,255,255,.7) 1px,transparent 1px);
  background-size:48px 48px}
.mbr .hero-bg-glow{position:absolute;top:-200px;right:-200px;width:540px;height:540px;
  background:radial-gradient(circle,rgba(220,60,52,.22) 0%,transparent 70%)}
.mbr .hero-content{position:relative;height:100%;display:flex;flex-direction:column;justify-content:space-between}
.mbr .cover-wordmark{height:56px;width:auto;align-self:flex-start;object-fit:contain;display:block}
.mbr .hero-bottom{margin-top:auto}
.mbr .hero-eyebrow{display:flex;align-items:center;gap:12px;margin-bottom:18px;
  font:600 11px/1 var(--font-body);color:var(--coral);text-transform:uppercase;letter-spacing:.16em}
.mbr .hero-eyebrow .rule{width:32px;height:2px;background:var(--coral)}
.mbr .hero-title{font:700 64px/.98 var(--font-display);letter-spacing:-.035em;color:#fff}
.mbr .hero-title .coral{color:var(--coral)}
.mbr .cover-lower{background:var(--paper);height:48%;padding:32px 64px 36px;display:flex;flex-direction:column}
.mbr .recipient-row{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-bottom:24px}
.mbr .recipient-eyebrow{font:600 10px/1 var(--font-body);text-transform:uppercase;color:var(--ink-3);
  letter-spacing:.14em;margin-bottom:8px}
.mbr .recipient-name{font:700 22px/1.2 var(--font-display);letter-spacing:-.015em;color:var(--slate-900);margin-bottom:4px}
.mbr .toc{border-top:1px solid var(--slate-100);padding-top:18px;margin-bottom:auto}
.mbr .toc-eyebrow{font:600 10px/1 var(--font-body);text-transform:uppercase;color:var(--ink-3);
  letter-spacing:.14em;margin-bottom:12px}
.mbr .toc-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px 32px}
.mbr .toc-item{display:flex;align-items:baseline;gap:12px}
.mbr .toc-num{font:500 11px/1 var(--font-mono);color:var(--coral)}
.mbr .toc-label{font:500 13px/1.4 var(--font-body);color:var(--ink-1)}
.mbr .cover-footer{display:flex;justify-content:space-between;align-items:flex-end;
  border-top:1px solid var(--slate-100);padding-top:16px;font-size:10px;color:var(--ink-3)}
.mbr .cover-footer .right{text-align:right;font:600 10px/1 var(--font-mono);color:var(--slate-700)}

/* Faint bulb watermark behind every NON-cover page. A background brand mark,
   not a stamp — hence centred and at 3.5% opacity. */
.mbr .page:not(.cover){position:relative}
.mbr .page:not(.cover)::after{content:"";position:absolute;top:50%;left:50%;width:520px;height:520px;
  transform:translate(-50%,-50%);background-image:url('data:image/png;base64,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');
  background-repeat:no-repeat;background-position:center;background-size:contain;
  opacity:.035;pointer-events:none;z-index:0}
.mbr .page > *{position:relative;z-index:1}

/* ==========================================================================
   PATTERN 18 — COLOUR SCHEMES  (taken from company.methodologyit.tech)
   A scheme is a token set, not a stylesheet. Every component already reads
   these variables, so a new scheme is data — add a block, nothing else.
   Selectors are attribute-only, NOT html[...], so any element can carry a
   scheme — which is what lets each picker swatch paint itself.
   ========================================================================== */
/* MIT — the default. Coral, per the brand rule; the hub schemes are opt-in. */
[data-scheme="mit"]{--canvas:#F6F8FB;--surface:#FFFFFF;--surface-alt:#F6F8FB;--hairline:#EEF2F7;
  --divider:#E2E8F0;--sidebar:#1E242C;--sidebar-fg:#E6EAEF;--sidebar-muted:#8A95A1;
  --sidebar-hover:rgba(127,131,140,.14);--sidebar-active-fg:#FFFFFF;
  --text:#0F172A;--text-muted:#475569;--text-subtle:#7E8A96;
  --accent:#DC3C34;--accent-fg:#fff;--accent-soft:rgba(220,60,52,.08);
  --r-card:14px;--r-sm:10px;--r-pill:999px;
  --shadow:0 1px 2px rgba(15,23,42,.04),0 4px 16px rgba(15,23,42,.04)}
[data-scheme="slate"]{--canvas:#F6F8FB;--surface:#FFFFFF;--surface-alt:#F6F8FB;--hairline:#EAEEF3;--divider:#E2E8F0;--sidebar:#1E242C;--sidebar-fg:#E6EAEF;--sidebar-muted:#8A95A1;--sidebar-active-fg:#FFFFFF;--sidebar-hover:rgba(127,131,140,.14);--text:#0F172A;--text-muted:#475569;--text-subtle:#8A95A1;--accent:#E89721;--accent-fg:#1E242C;--accent-soft:#FEF6E8;--r-card:16px;--r-sm:10px;--r-pill:999px;--shadow:0 1px 2px hsl(214 17% 20% /.05),0 2px 8px hsl(214 17% 20% /.05);}
[data-scheme="ninja"]{--canvas:#FAFBFC;--surface:#FFFFFF;--surface-alt:#F7F9FB;--hairline:#E6EAF0;--divider:#D8DEE7;--sidebar:#FFFFFF;--sidebar-fg:#1A2233;--sidebar-muted:#9AA3B2;--sidebar-active-fg:#0B5E45;--sidebar-hover:rgba(15,125,92,.08);--text:#141A22;--text-muted:#5B6675;--text-subtle:#9AA3B2;--accent:#0F7D5C;--accent-fg:#FFFFFF;--accent-soft:#E9F6F0;--r-card:8px;--r-sm:6px;--r-pill:6px;--shadow:0 1px 1px rgba(20,26,34,.04),0 1px 3px rgba(20,26,34,.05);}
[data-scheme="atlas"]{--canvas:#EEF1F7;--surface:#FFFFFF;--surface-alt:#F4F6FB;--hairline:#E5EAF3;--divider:#DAE1EE;--sidebar:#33406B;--sidebar-fg:#DBE2F2;--sidebar-muted:#9AA5C8;--sidebar-active-fg:#FFFFFF;--sidebar-hover:rgba(255,255,255,.12);--text:#222A3F;--text-muted:#5E6B86;--text-subtle:#97A1B8;--accent:#4E63D6;--accent-fg:#FFFFFF;--accent-soft:#ECEFFB;--r-card:20px;--r-sm:14px;--r-pill:999px;--shadow:0 4px 14px rgba(48,64,107,.08),0 1px 3px rgba(48,64,107,.06);}
[data-scheme="taskora"]{--canvas:#F6F5FC;--surface:#FFFFFF;--surface-alt:#F7F5FC;--hairline:#ECE7F6;--divider:#E0D8F2;--sidebar:#FFFFFF;--sidebar-fg:#2A2440;--sidebar-muted:#9C95B5;--sidebar-active-fg:#5A33D6;--sidebar-hover:rgba(109,67,232,.08);--text:#231C3A;--text-muted:#6B6486;--text-subtle:#9C95B5;--accent:#6D43E8;--accent-fg:#FFFFFF;--accent-soft:#EFE9FE;--r-card:24px;--r-sm:16px;--r-pill:999px;--shadow:0 8px 24px rgba(109,67,232,.10),0 2px 6px rgba(109,67,232,.06);}
/* TJW Engineering — navy rail, TJW orange accent. Client scheme for the
   tjw-tools app (change orders + proposals), not in the demo picker. */
[data-scheme="tjw"]{--canvas:#F5F7FA;--surface:#FFFFFF;--surface-alt:#F6F8FB;--hairline:#EAEEF3;--divider:#DFE5EC;--sidebar:#274164;--sidebar-fg:#D8E0EC;--sidebar-muted:#8FA0B8;--sidebar-active-fg:#FFFFFF;--sidebar-hover:rgba(235,133,0,.16);--text:#16233A;--text-muted:#4C5B72;--text-subtle:#8494A9;--accent:#EB8500;--accent-fg:#FFFFFF;--accent-soft:rgba(235,133,0,.10);--r-card:14px;--r-sm:10px;--r-pill:999px;--shadow:0 1px 2px rgba(22,35,58,.05),0 4px 16px rgba(22,35,58,.06);}
/* R&B Wire — warm neutral ground, brand red, near-black rail. Client scheme,
   same token contract as the rest; nothing is special-cased for it. */
[data-scheme="rbwire"]{--canvas:#F7F6F3;--surface:#FFFFFF;--surface-alt:#FAF8F4;--hairline:#EDE9E1;--divider:#DFD9CE;--sidebar:#1C1917;--sidebar-fg:#E9E5DD;--sidebar-muted:#9C9488;--sidebar-active-fg:#FFFFFF;--sidebar-hover:rgba(226,35,26,.18);--text:#1A1614;--text-muted:#57514A;--text-subtle:#8C8478;--accent:#E2231A;--accent-fg:#FFFFFF;--accent-soft:rgba(226,35,26,.08);--r-card:14px;--r-sm:10px;--r-pill:999px;--shadow:0 1px 2px rgba(28,25,23,.04),0 4px 16px rgba(28,25,23,.05);}
[data-scheme="dashio"]{--canvas:#0E1116;--surface:#161B22;--surface-alt:#1B212A;--hairline:#262D38;--divider:#2F3742;--sidebar:#13171E;--sidebar-fg:#E7EBF2;--sidebar-muted:#79828F;--sidebar-active-fg:#FFFFFF;--sidebar-hover:rgba(255,255,255,.06);--text:#F2F5F8;--text-muted:#A6AEBC;--text-subtle:#6E7787;--accent:#E89721;--accent-fg:#13171E;--accent-soft:rgba(232,151,33,.14);--r-card:14px;--r-sm:10px;--r-pill:999px;--shadow:0 2px 8px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.35);}

/* The light/dark toggle has to win over the active scheme, so it is
   re-declared after the scheme blocks. dashio ships dark already. */
html.dark{--canvas:#13181E;--surface:#1B2229;--surface-alt:#161B22;--hairline:#252D36;--divider:#2B333D;
  --text:#F3F5F7;--text-muted:#A8B2BC;--text-subtle:#7E8A96;--sidebar:#161B22;
  --shadow:0 1px 2px rgba(0,0,0,.18),0 6px 18px rgba(0,0,0,.20);
  --disabled-fg:#5C6673;--disabled-bg:#232B34;--success-soft:rgba(47,158,94,.14)}

/* ==========================================================================
   PATTERN 19 — CUSTOMER PORTAL SHELL
   A SEPARATE product from the MSP portal — two-level tenancy, Tenant (our
   client) → Account (their customer) — but the same tokens, so it inherits
   every pattern above rather than forking the UI.
   Nav IA copied from portal.methodologyit.tech per the product plan.
   ========================================================================== */
.cp-wrap{border:1px solid var(--divider);border-radius:var(--r-card);overflow:hidden;
  box-shadow:var(--shadow);background:var(--surface)}
.cp-chrome{display:flex;align-items:center;gap:8px;padding:8px 14px;background:var(--surface-alt);
  border-bottom:1px solid var(--hairline);font:500 11.5px/1 var(--font-mono);color:var(--text-subtle)}
.cp-chrome .dot{width:9px;height:9px;border-radius:50%;background:var(--divider);flex:none}
.cp-body{display:grid;grid-template-columns:232px 1fr;min-height:460px}
.cp-rail{background:var(--sidebar);color:var(--sidebar-fg);padding:16px 12px;display:flex;flex-direction:column}
.cp-brand{display:flex;align-items:center;gap:10px;padding:2px 6px 14px}
.cp-brand .mk{width:32px;height:32px;flex:none;border-radius:8px;display:grid;place-items:center;
  background:var(--accent);color:var(--accent-fg);font:800 11px/1 var(--font-display);overflow:hidden}
.cp-brand .mk img{width:100%;height:100%;object-fit:contain}
.cp-brand .nm{font:700 13.5px/1.2 var(--font-display);color:var(--sidebar-active-fg,#fff)}
/* The version pill makes "what changed?" answerable on a support call. */
.cp-ver{font:500 9px/1 var(--font-mono);color:var(--sidebar-muted);border:1px solid var(--sidebar-muted);
  border-radius:4px;padding:3px 4px;margin-left:auto}
.cp-nav{display:flex;flex-direction:column;gap:2px;overflow-y:auto;flex:1}
.cp-grp{display:flex;align-items:center;gap:6px;width:100%;background:none;border:none;cursor:pointer;
  font:700 9.5px/1 var(--font-body);letter-spacing:.13em;text-transform:uppercase;color:var(--sidebar-muted);
  padding:12px 8px 5px;text-align:left}
.cp-grp:hover{color:var(--sidebar-fg)}
.cp-grp .chev{margin-left:auto;opacity:.6;font-size:9px}
.cp-item{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;border:none;background:none;
  color:var(--sidebar-fg);font:500 12.5px/1.3 var(--font-body);cursor:pointer;text-align:left;width:100%}
.cp-item:hover{background:var(--sidebar-hover,rgba(127,131,140,.14));color:var(--sidebar-active-fg,#fff)}
.cp-item.on{background:var(--sidebar-hover,rgba(127,131,140,.14));color:var(--sidebar-active-fg,#fff);font-weight:600}
.cp-item .n{margin-left:auto;font:700 9.5px/1 var(--font-mono);background:var(--accent);color:var(--accent-fg);
  border-radius:var(--r-pill);padding:3px 5px;min-width:16px;text-align:center}
.cp-item .kind{margin-left:auto;font:600 8.5px/1 var(--font-body);letter-spacing:.06em;text-transform:uppercase;
  color:var(--sidebar-muted)}
/* The "built for you" slot — bespoke modules, outside every group. It is the
   upsell made visible, so it never blends into the list. */
.cp-hl{margin-top:12px;border-radius:10px;padding:10px 11px;cursor:pointer;
  background:linear-gradient(135deg,var(--accent),color-mix(in srgb,var(--accent) 55%,#000));
  color:var(--accent-fg);display:flex;align-items:center;gap:9px;font:600 12.5px/1.2 var(--font-body)}
.cp-user{display:flex;align-items:center;gap:9px;margin-top:12px;padding-top:12px;border-top:1px solid var(--sidebar-hover,rgba(255,255,255,.1))}
/* Same bottom-left version + updated line as the internal shell. */
.cp-verline{padding:8px 2px 0;font:500 9.5px/1.5 var(--font-mono);color:var(--sidebar-muted)}
.cp-verline .v{color:var(--sidebar-fg);border:1px solid var(--sidebar-muted);border-radius:4px;padding:1px 4px;margin-right:5px}
.cp-user .av{width:28px;height:28px;flex:none;border-radius:50%;background:var(--accent-soft);color:var(--accent);
  display:grid;place-items:center;font:700 9.5px/1 var(--font-display)}
.cp-user .nm{font:600 12px/1.25 var(--font-body);color:var(--sidebar-active-fg,#fff)}
.cp-user .ac{font-size:10.5px;color:var(--sidebar-muted)}
.cp-main{padding:22px 24px;background:var(--canvas);min-width:0}
.cp-h{font:700 18px/1.2 var(--font-display);margin-bottom:4px}
.cp-sub{font-size:12.5px;color:var(--text-subtle);margin-bottom:16px}
/* A module that is off is not a dead end — it is a sales surface. */
.cp-gated{border:1px dashed var(--divider);border-radius:var(--r-card);padding:26px;text-align:center;background:var(--surface)}
.cp-gated h4{font:600 14px/1.3 var(--font-display);margin-bottom:6px}
.cp-gated p{margin:0 auto;max-width:340px;font-size:12.5px;color:var(--text-subtle)}

/* --- config back end ----------------------------------------------------- */
.modrow{display:grid;grid-template-columns:1fr 150px;gap:14px;align-items:start;
  padding:13px 0;border-bottom:1px solid var(--hairline)}
.modrow:last-of-type{border-bottom:none}
.modrow .nm{font:600 13px/1.35 var(--font-body)}
.modrow .hint{font-size:11.5px;color:var(--text-subtle);margin-top:3px}
.modrow select{width:100%;border:1px solid var(--divider);border-radius:8px;padding:7px 9px;
  font:500 12.5px/1.2 var(--font-body);background:var(--surface);color:var(--text);outline:none}
.modrow select:disabled{background:var(--disabled-bg);color:var(--disabled-fg);cursor:not-allowed}
.userpick{grid-column:1/-1;display:flex;gap:7px;flex-wrap:wrap;margin-top:4px;padding:10px 12px;
  background:var(--surface-alt);border:1px solid var(--hairline);border-radius:9px}
.userpick label{display:flex;align-items:center;gap:7px;font:500 12px/1 var(--font-body);cursor:pointer;
  background:var(--surface);border:1px solid var(--divider);border-radius:var(--r-pill);padding:6px 11px 6px 8px}
.userpick label.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
.userpick input{accent-color:var(--accent);cursor:pointer}
.lockbadge{font:700 9px/1 var(--font-body);letter-spacing:.05em;text-transform:uppercase;
  background:var(--surface-alt);color:var(--text-subtle);border-radius:5px;padding:4px 7px;margin-left:7px}

/* --- spec sheet ----------------------------------------------------------- */
.spec{display:flex;flex-direction:column;gap:18px}
.spec .surface{padding:18px 20px}
.spec h2{font:600 15px/1.3 var(--font-display);margin-bottom:4px}
.spec .rule{font-size:12.5px;color:var(--text-muted);margin:0 0 14px}
.swatches{display:flex;gap:8px;flex-wrap:wrap}
.sw{width:96px;border-radius:10px;overflow:hidden;border:1px solid var(--hairline)}
.sw .c{height:44px}
.sw .n{padding:5px 7px;font:500 10px/1.3 var(--font-mono);color:var(--text-muted);background:var(--surface)}
/* Page-level callout. Deliberately NOT called .note — the recording timeline
   already owns .note for its annotations, and the two collided. */
.callout{border-left:3px solid var(--accent);background:var(--accent-soft);border-radius:0 8px 8px 0;padding:10px 14px;font-size:12.5px;color:var(--text)}
/* Tone variants. A callout that is INFORMATION ("this is a mockup", "this is
   read-only") should not wear the accent, which everywhere else means "act on
   this". Same shape, same padding, same radius — only the hue changes, so a
   layout edit still reaches every callout in the estate. */
.callout.info{border-left-color:var(--info);background:var(--info-soft)}
.callout.info b{color:var(--info)}
.callout.warn{border-left-color:var(--warn);background:var(--warn-soft)}
.callout.warn b{color:var(--warn)}
.callout.bad{border-left-color:var(--danger);background:var(--danger-soft)}
.callout.bad b{color:var(--danger)}

/* ==========================================================================
   PATTERN 20 — PRESENT MODE
   One update on a screen other people are looking at: a tabletop inject on a
   share, a checkpoint on a meeting-room display, a single question put to a
   room. It is NOT a modal and NOT a page — it is a display surface, so it
   takes over the viewport entirely and owns its own contrast.

   Why it hard-codes colour instead of using the tokens: every other pattern
   is read by one person at arm's length on a screen they chose. This one is
   read by a room, through a projector or a video-call re-encode, at whatever
   brightness the host's laptop happens to be on. Both of those crush contrast
   badly, and mid-greys are the first thing to go. So the palette here is
   near-black behind near-white, fixed, and it does not follow the scheme —
   a scheme with a light surface would make this unreadable in the one place
   it has to be readable.

   RULES
   1. Body copy is never mid-grey. #EDEFF2 on #0B0D10 is ~17:1; the muted grey
      is allowed on chrome (who is presenting, the counter) and nowhere else.
   2. Accent is for the eyebrow and the position indicator. Never for a
      sentence anyone has to read.
   3. Type scales with the viewport, because the room is not sitting where you
      are. Everything is clamp()ed; nothing is a fixed px size.
   4. Render it at body level. `position:fixed` is relative to a transformed
      ancestor, not the viewport, so mounted inside an app shell that uses a
      transform it silently becomes a dark box in the corner.
   5. Facilitator-only text does not belong here. Anything that gives away
      what the update is probing stays in the console on the other screen.
   ========================================================================== */
.present{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;gap:clamp(14px,2vh,26px);
  background:#0B0D10;color:#fff;font-family:var(--font-body);
  padding:clamp(18px,3.2vh,34px) clamp(22px,5vw,72px)}
.present__bar{flex:none;display:flex;align-items:center;gap:11px}
.present__mark{width:clamp(26px,2.4vw,34px);height:clamp(26px,2.4vw,34px);border-radius:8px;flex:none;
  display:grid;place-items:center;background:rgba(255,255,255,.10);overflow:hidden}
.present__mark img{width:74%;height:74%;object-fit:contain}
.present__who{font:600 clamp(13px,1.3vw,16px)/1.2 var(--font-display);color:#fff}
.present__sub{font:400 clamp(11px,1.05vw,13px)/1.2 var(--font-body);color:#9AA3AE}
.present__bar .present__nav{margin-left:auto}

.present__stage{flex:1;display:flex;flex-direction:column;justify-content:center;
  width:100%;max-width:min(1180px,90vw);margin:0 auto}
.present__eyebrow{align-self:flex-start;display:inline-flex;align-items:center;gap:10px;
  font:700 clamp(10.5px,1.05vw,13.5px)/1 var(--font-body);letter-spacing:.14em;text-transform:uppercase;
  color:#FFC46B;border:1px solid rgba(255,196,107,.45);border-radius:var(--r-pill);padding:8px 15px}
.present__eyebrow .sep{color:rgba(255,196,107,.5)}
.present__clock{font-family:var(--font-mono);letter-spacing:.04em}
.present__title{font:800 clamp(30px,4.6vw,68px)/1.04 var(--font-display);letter-spacing:-.022em;
  color:#fff;margin:clamp(14px,2.4vh,28px) 0 0}
.present__body{font:400 clamp(18px,2.1vw,31px)/1.45 var(--font-body);color:#EDEFF2;
  margin:clamp(12px,1.9vh,22px) 0 0}
.present__empty{font:400 clamp(16px,1.8vw,24px)/1.45 var(--font-body);color:#9AA3AE}

.present__foot{flex:none;display:flex;align-items:center;gap:clamp(10px,1.6vw,18px);flex-wrap:wrap}
.present__dots{display:flex;gap:6px;flex-wrap:wrap}
.present__dot{width:clamp(18px,2vw,30px);height:5px;border-radius:3px;background:rgba(255,255,255,.20)}
.present__dot.done{background:rgba(255,255,255,.52)}
.present__dot.on{background:#FFC46B}
.present__count{font:500 clamp(11px,1vw,13px)/1 var(--font-mono);color:#9AA3AE;white-space:nowrap}
.present__nav{display:flex;gap:9px;margin-left:auto}
.present__btn{font:600 clamp(13px,1.15vw,15px)/1 var(--font-body);color:#fff;cursor:pointer;
  background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.30);border-radius:var(--r-pill);
  padding:clamp(10px,1.2vh,14px) clamp(15px,1.6vw,22px);transition:background .12s ease}
.present__btn:hover{background:rgba(255,255,255,.20)}
.present__btn--primary{background:#FFC46B;border-color:#FFC46B;color:#12151A}
.present__btn--primary:hover{background:#FFD08A;border-color:#FFD08A}
/* Disabled gets its own flat grey, never the enabled colour faded — pattern
   rule 5. A dimmed amber pill still reads as the thing you are meant to press. */
.present__btn[disabled]{background:transparent;border-color:rgba(255,255,255,.14);
  color:#6B7480;cursor:default}
.present__btn[disabled]:hover{background:transparent}
@media (max-width:640px){
  .present{padding:16px 18px}
  .present__foot{gap:10px}
  .present__nav{width:100%;margin-left:0}
  .present__btn{flex:1;text-align:center}
}

/* ==========================================================================
   PATTERN 21 — TWO-SIDED THREAD  (feedback, and anything else where an
   outside party and we talk on one record)
   The rule: an entry is a CONVERSATION, not a submission with a response
   bolted on. Both sides can add to it, both sides see the whole thing, and
   who spoke last is legible without reading a word — ours accent-edged and
   tinted, theirs plain and indented. The moment a thread only runs one way it
   stops being answered, because nobody can ask the follow-up question that
   most reports actually need.
   Used by feedback-widget.js in every app, and by the Dojo console.
   ========================================================================== */
.tthread{display:block}
.tentry{border:1px solid var(--hairline);border-radius:var(--r-card);background:var(--surface);
  padding:13px 15px;margin-bottom:9px}
.tentry .t{font:600 13.5px/1.35 var(--font-body);color:var(--text)}
.tentry .m{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px;
  font-size:11.5px;color:var(--text-subtle)}
/* The entry's head row. Title and meta on the left, the fold control hard right,
   so the way back into a folded conversation sits in the same place on every
   row. Descendant selectors above (not `>`) because the head nests them. */
.tentry .thead{display:flex;align-items:flex-start;gap:10px}
.tentry .thead > .tmain{flex:1;min-width:0}
.tentry .thead > .chip{flex:none}
/* ours */
.tmsg-us{margin-top:10px;border-left:3px solid var(--accent);background:var(--accent-soft);
  border-radius:0 8px 8px 0;padding:9px 12px;font-size:12.5px;color:var(--text)}
/* The AUTHOR line only — `:first-child`, not every <b>. Message bodies are rich
   text, so a bold word inside one would otherwise break onto its own line and
   come out accent-coloured. */
.tmsg-us > b:first-child{display:block;font-size:11px;color:var(--accent);margin-bottom:3px}
/* theirs — indented so the two sides read as a dialogue down the card */
.tmsg-them{margin-top:10px;margin-left:22px;border-left:3px solid var(--divider);
  background:var(--surface-alt);border-radius:0 8px 8px 0;padding:9px 12px;
  font-size:12.5px;color:var(--text)}
.tmsg-them > b:first-child{display:block;font-size:11px;color:var(--text-muted);margin-bottom:3px}
.tmsg-us .tm,.tmsg-them .tm{float:right;font-size:10.5px;color:var(--text-subtle);font-weight:400}
/* the reply box belongs INSIDE the entry — a composer somewhere else turns a
   conversation back into a form */
/* THE STANDARD: buttons sit BELOW the box and align LEFT — never beside it,
   never floated right. Beside it they squeeze the writing area on a narrow pane
   and drift vertically as the textarea is dragged taller; hard right puts them
   a whole box-width from where the writing stopped. Bottom-left is where the
   cursor already is. Applies to every composer — reply boxes and note boxes. */
.treply{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.treply textarea{width:100%;border:1px solid var(--divider);border-radius:8px;padding:8px 10px;
  font:400 12.5px/1.45 var(--font-body);color:var(--text);background:var(--surface);resize:vertical}
.treply textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.treply-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* Same foot under a full note composer: actions left, context after them. */
/* Tight to the box above it. 12px read as a gap rather than as "these belong
   to that" — the actions are part of the field, not a separate row. */
.rte-actions{display:flex;align-items:center;gap:10px;margin-top:7px;flex-wrap:wrap}
/* A button row that follows a .field rather than sitting inside one has that
   field's 14px bottom margin above it as well. Pull it back so a standalone
   action still reads as belonging to the box it acts on. */
.field + .rte-actions{margin-top:-6px}
/* "This is sorted" — the reporter's own close, and the one control on the
   client side that changes state. A checkbox precisely because it unticks: the
   person who raised it is the one who knows whether it is actually fixed, and
   "no it isn't" has to be as easy to say as "yes it is". */
.tdone{display:inline-flex;align-items:center;gap:6px;flex:none;cursor:pointer;
  font:600 11.5px/1 var(--font-body);color:var(--text-muted);white-space:nowrap;
  border:1px solid var(--divider);border-radius:var(--r-pill);padding:5px 11px;background:transparent}
.tdone:hover{background:var(--surface-alt);color:var(--text)}
.tdone input{width:14px;height:14px;margin:0;accent-color:var(--accent);cursor:pointer}
.tdone.on{border-color:var(--success);color:var(--success);background:var(--success-soft)}
@media (max-width:560px){
  .tmsg-them{margin-left:10px}
}

/* ==========================================================================
   PATTERN 21b — CORRESPONDENCE THREAD  (a ticket's entries)
   Pattern 21 tells two parties apart by ASYMMETRY — ours edged and tinted,
   theirs indented. That works when there are exactly two. A ticket has four
   kinds of entry: mail in, mail out, an internal note, and something the
   system did. Asymmetry cannot carry four, and trying makes a thread you have
   to decode instead of read.
   So: ONE card shape for every entry, an avatar rail down the left so the eye
   can scan WHO without reading a word, and the KIND named in the header rather
   than implied by shape. Ours still take the accent edge, because "did this
   leave the building" is the question the rail cannot answer.
   ========================================================================== */
.cthread{display:flex;flex-direction:column;gap:10px}
.centry{display:flex;gap:12px;padding:13px 15px;border:1px solid var(--hairline);
  border-radius:var(--r-card);background:var(--surface)}
.centry > .av{width:34px;height:34px;flex:none;border-radius:50%;font:700 11px/1 var(--font-display)}
.centry .cmain{flex:1;min-width:0}
.centry .chead{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;margin-bottom:5px}
.centry .cwho{font:700 13px/1.3 var(--font-body);color:var(--text)}
.centry .ckind{font:600 10.5px/1 var(--font-body);letter-spacing:.05em;text-transform:uppercase;color:var(--text-subtle)}
.centry .cwhen{margin-left:auto;font:400 11px/1 var(--font-mono);color:var(--text-subtle);flex:none}
.centry .ctext{font:400 12.5px/1.55 var(--font-body);color:var(--text)}
/* ours — the one thing the avatar rail can't tell you: did this go out? */
.centry.is-us{border-left:3px solid var(--accent)}
/* internal — never left the building, so it must never read as sent */
.centry.is-internal{background:var(--warn-soft);border-color:rgba(224,169,60,.35)}
.centry.is-internal .ckind{color:var(--warn)}
/* system — a fact, not a message; muted so it never competes with a person */
.centry.is-system{background:var(--surface-alt)}
.centry.is-system .ctext{color:var(--text-muted)}
.centry.is-system > .av{background:var(--surface);color:var(--text-subtle);border:1px solid var(--hairline)}

/* ==========================================================================
   PATTERN 23 — AI EDIT PANEL
   A drafting surface next to a field, for text a person owns and a model
   helps with. It is NOT a chat window and it is NOT a "regenerate" button.

   The two failures it exists to prevent, both seen for real:
     1. A GENERATE BUTTON WITH NOWHERE TO SAY WHAT TO CHANGE. Pressing it
        again can only redo the same instruction, so the only way to steer it
        is to type at it — and typing into the field being edited just adds
        text the model then faithfully preserves. The instruction needs its
        own input.
     2. THE MODEL'S SIDE OF THE CONVERSATION LANDING IN THE DOCUMENT. A reply
        like "could you provide the project details?" is a message to the
        author, not proposal copy. Messages belong in the panel; only an
        accepted VERSION is allowed to touch the field.

   So the panel keeps three things apart:
     .aiv    a version — candidate text, shown as it will read in the field
     .aim    a message — either side talking ABOUT the text
     the field itself, which changes only when a version is accepted.

   Versions are numbered and kept, because "put back the one before last" is
   the single most common thing anyone asks of a drafting tool.
   ========================================================================== */
.aipanel{display:flex;flex-direction:column;gap:10px;min-height:0;flex:1}
/* Sized to its content, not stretched to fill. Stretching pins the composer to
   the floor of the panel and leaves a lake of empty space above it whenever the
   conversation is short — the controls end up further from the text than they
   need to be. It still scrolls once the conversation outgrows the space. */
.aifeed{display:flex;flex-direction:column;gap:10px;overflow-y:auto;flex:0 1 auto;
  padding:2px 1px;scrollbar-width:thin}

/* A message. Ours is the model's, theirs is the author's — the reverse of
   PATTERN 21, because here the human is the one giving instructions. */
.aim{max-width:88%;padding:9px 12px;border-radius:12px;font:400 12.5px/1.55 var(--font-body);color:var(--text)}
.aim .w{display:block;font:600 10px/1 var(--font-body);letter-spacing:.08em;text-transform:uppercase;
  color:var(--text-subtle);margin-bottom:5px}
.aim.you{align-self:flex-end;background:var(--accent-soft);border:1px solid var(--accent-soft)}
.aim.ai{align-self:flex-start;background:var(--surface-alt);border:1px solid var(--hairline)}
.aim.is-error{background:var(--danger-soft);border-color:rgba(185,28,28,.3);color:var(--danger)}

/* A version. Deliberately looks like the document, not like a chat bubble —
   full width, paper background, body type — because that is what it will be. */
.aiv{border:1px solid var(--hairline);border-radius:var(--r-card);background:var(--surface);overflow:hidden}
.aiv.is-current{border-color:var(--accent)}
/* The accept control lives in the HEADER, not under the text. A version can be
   several paragraphs and the text area scrolls, so a button below it falls off
   the bottom of the panel exactly when the version is worth accepting — seen
   for real: "once I make the edited version, how do I get it into the field?"
   In the header it is on screen the moment the card is. */
.aiv-h{display:flex;align-items:center;gap:8px;padding:6px 8px 6px 12px;background:var(--surface-alt);
  border-bottom:1px solid var(--hairline);position:sticky;top:0;z-index:1}
.aiv-n{font:700 10px/1 var(--font-mono);color:var(--text-subtle);letter-spacing:.06em;text-transform:uppercase}
.aiv-note{font:400 11.5px/1.3 var(--font-body);color:var(--text-subtle);min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.aiv-h .sp{flex:1}
.aiv-t{padding:11px 13px;font:400 12.5px/1.6 var(--font-body);color:var(--text);white-space:pre-wrap;
  max-height:260px;overflow-y:auto}
/* Kept for a card that wants a second action row; the accept button itself is
   in the header. */
.aiv-f{display:flex;align-items:center;gap:7px;padding:8px 12px;border-top:1px solid var(--hairline)}
/* The accepted one says so and stops offering itself again. */
.aiv.is-current .aiv-h{background:var(--accent-soft)}
.aiv.is-current .aiv-n{color:var(--accent)}

/* The instruction box. Send sits bottom-LEFT under the box like every other
   composer in the layer. */
.aiask{display:flex;flex-direction:column;gap:8px;flex:none}
.aiask textarea{width:100%;min-height:62px;border:1px solid var(--divider);border-radius:8px;padding:9px 11px;
  font:400 12.5px/1.45 var(--font-body);color:var(--text);background:var(--surface);resize:vertical}
.aiask textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.aiask-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.aiask-foot .sp{flex:1}
/* The way back to the field, bottom-right and always in the same place. The
   per-version accept in a card header answers "use THAT one"; this answers the
   question people actually arrive with — "I'm done, put it back" — without
   making them find which card is newest. Disabled until there is something to
   return, so it never lies about what it will do. */
.aireturn{margin-left:auto}
/* Canned openers — the instructions people give a drafting model over and
   over. Cheaper to click than to type, and they teach what it will accept. */
.aihints{display:flex;gap:6px;flex-wrap:wrap}
.aihint{border:1px solid var(--divider);background:transparent;border-radius:var(--r-pill);
  padding:4px 10px;font:500 11px/1.2 var(--font-body);color:var(--text-muted);cursor:pointer}
.aihint:hover{background:var(--surface-alt);color:var(--text);border-color:var(--accent)}
/* Working state — a drafting model takes seconds, and a dead panel reads as broken. */
.aibusy{display:flex;align-items:center;gap:8px;font:400 12px/1 var(--font-body);color:var(--text-subtle);
  padding:4px 2px}
.aibusy .dot{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:aip 1s ease-in-out infinite}
.aibusy .dot:nth-child(2){animation-delay:.15s}
.aibusy .dot:nth-child(3){animation-delay:.3s}
@keyframes aip{0%,100%{opacity:.25}50%{opacity:1}}
/* The button that opens the panel, sitting on a field's toolbar. */
.aiopen{display:inline-flex;align-items:center;gap:5px;border:none;background:transparent;cursor:pointer;
  border-radius:6px;padding:0 8px;min-height:28px;font:700 11.5px/1 var(--font-body);color:var(--accent)}
.aiopen:hover{background:var(--accent-soft)}

/* ==========================================================================
   PATTERN 24 — GUIDANCE (help hints)
   Asked for by Poppy at R&B Wire, 2026-08-10: "a little pop up thing that
   says, if you click here this is what you have to do" — so people can teach
   themselves rather than book a call.

   TWO KINDS, and conflating them is the usual mistake:
     ASK  (.hint)     a ? beside a label or heading. Always available, never
                      speaks first, reveals in place.
     TELL (.hint-new) a first-run note for what somebody would not know to ask
                      about. Shown once, dismissed for good.

   In place, NOT a tooltip. Hover does not exist on a touch screen, a tooltip
   disappears the moment you move towards it to read it, and it cannot hold a
   list or a link. So a hint expands where it sits and pushes the page down,
   like a disclosure — which also means it is readable by a screen reader and
   survives a screenshot.

   THE RULE THAT MATTERS MOST: if a screen needs a hint to be usable, the hint
   is a bug report. Guidance explains POLICY AND CONSEQUENCE — "the [RB-1042]
   is what brings their reply back to this ticket" — never mechanics. A hint
   that says "click Save to save" is how people learn to ignore all of them.
   ========================================================================== */
.hint{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;
  flex:none;border-radius:50%;border:1px solid var(--divider);background:var(--surface);
  color:var(--text-subtle);font:700 10px/1 var(--font-body);cursor:pointer;
  vertical-align:middle;margin-left:5px;padding:0}
.hint:hover{border-color:var(--accent);color:var(--accent)}
.hint.on{background:var(--accent);border-color:var(--accent);color:var(--accent-fg)}
.hint-body{border-left:3px solid var(--info);background:var(--info-soft);
  border-radius:0 8px 8px 0;padding:9px 13px;margin:8px 0 0;
  font:400 12.5px/1.55 var(--font-body);color:var(--text)}
.hint-body p{margin:0 0 6px}.hint-body p:last-child{margin:0}
.hint-body ul{margin:4px 0 0 16px}
/* First-run. Dismissed per person per hint — a tip that returns every visit is
   noise, and noise is what gets the whole system switched off. */
.hint-new{display:flex;gap:11px;align-items:flex-start;background:var(--accent-soft);
  border:1px solid var(--accent);border-radius:var(--r-card);padding:12px 14px;margin-bottom:16px}
.hint-new .ic{width:24px;height:24px;flex:none;border-radius:50%;background:var(--accent);
  color:var(--accent-fg);display:grid;place-items:center;font:700 12px/1 var(--font-display)}
.hint-new .tx{flex:1;min-width:0;font:400 12.5px/1.55 var(--font-body);color:var(--text)}
/* `> b:first-child` — the TITLE only, not every bold. Scoping this to plain `b`
   made a bold word inside the body break onto its own line, which is precisely
   the bug already recorded against .tmsg-us in PATTERN 21. */
.hint-new .tx > b:first-child{display:block;font:700 13px/1.3 var(--font-display);margin-bottom:2px}
.hint-new .x{border:none;background:none;cursor:pointer;color:var(--text-subtle);
  font-size:17px;line-height:1;padding:0 2px;flex:none}
.hint-new .x:hover{color:var(--text)}
@media (max-width:560px){ .hint-new{padding:10px 12px} }

/* ==========================================================================
   PATTERN 25 — TIME SERIES WITH A THRESHOLD BAND
   A metric plotted against a standard it either meets or doesn't. The band is
   the point: a reader should see compliance without reading a single number.

   Built to the data-viz method, so the rules that method fixes are fixed here:

   * ONE measure per plot, one y-axis. Metrics on different scales (a loss
     percentage and a latency in ms) get stacked small multiples that share an
     x-axis — never a second y-axis. Dual-axis is the single most common
     charting error and it is not available in this layer.
   * Status colours are FIXED and never themed. They are deliberately NOT the
     brand's --success/--warn/--danger: those are tuned for badge text on a
     soft background and the brand red measures 2.48:1 as a plotted mark on the
     dark surface, under the 3:1 floor. The steps below clear 3:1 on both app
     surfaces. Validated with the data-viz validator against #1B2229 (dark) and
     #FFFFFF (light).
   * Status never carries meaning alone — every band ships a text label, and
     every chart is accompanied by its table view.
   ========================================================================== */
.viz{--viz-series:#2a78d6;--viz-good:#0ca30c;--viz-warn:#fab219;--viz-bad:#d03b3b;
  --viz-grid:var(--hairline);--viz-ink:var(--text-subtle)}
html.dark .viz{--viz-series:#3987e5}

.viz-card{background:var(--surface);border:1px solid var(--hairline);
  border-radius:var(--r-card);box-shadow:var(--shadow);padding:15px 17px}
.viz-head{display:flex;align-items:baseline;gap:10px;margin-bottom:2px}
.viz-title{font:600 13px/1.3 var(--font-display);color:var(--text)}
/* The standard being applied, stated in the chart rather than a caption —
   a chart whose threshold lives in prose elsewhere gets screenshotted alone. */
.viz-standard{font:400 11.5px/1.3 var(--font-body);color:var(--text-subtle);margin-left:auto;
  font-variant-numeric:tabular-nums}
.viz-plot{display:block;width:100%;height:auto;overflow:visible}

.viz-band{fill:var(--viz-good);opacity:.10}
.viz-band-line{stroke:var(--viz-good);stroke-width:1;stroke-dasharray:3 3;opacity:.55;fill:none}
.viz-band-warn{stroke:var(--viz-warn);stroke-width:1;stroke-dasharray:3 3;opacity:.6;fill:none}
.viz-bandlabel{font:600 9.5px/1 var(--font-body);fill:var(--viz-good);letter-spacing:.04em}
.viz-bandlabel--warn{fill:var(--viz-warn)}

.viz-grid{stroke:var(--viz-grid);stroke-width:1}
.viz-axis{font:500 9.5px/1 var(--font-body);fill:var(--viz-ink);font-variant-numeric:tabular-nums}
/* 2px lines, thin marks — the method's mark spec. */
.viz-line{fill:none;stroke:var(--viz-series);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.viz-area{fill:var(--viz-series);opacity:.10}
/* Breaches are marked individually so they survive being printed in greyscale
   and read without colour. */
.viz-dot{fill:var(--viz-bad);stroke:var(--surface);stroke-width:2}
.viz-dot--warn{fill:var(--viz-warn)}
/* A gap in measurement is drawn as a gap, never interpolated across: joining
   the line over missing hours asserts data that was never collected. */
.viz-gap{fill:var(--viz-ink);opacity:.09}

.viz-cross{stroke:var(--text-subtle);stroke-width:1;stroke-dasharray:2 3;opacity:.7}
.viz-tip{position:absolute;pointer-events:none;z-index:40;background:var(--surface);
  border:1px solid var(--divider);border-radius:9px;box-shadow:var(--shadow-lg);
  padding:8px 10px;font:500 11.5px/1.45 var(--font-body);color:var(--text);
  font-variant-numeric:tabular-nums;white-space:nowrap;transform:translate(-50%,-100%)}
.viz-tip b{font:700 11.5px/1.45 var(--font-display)}
.viz-tip .t-when{color:var(--text-subtle);font-weight:500}
.viz-wrap{position:relative}

/* Verdict pill — icon + word, so the judgement is never colour-only. */
.viz-verdict{display:inline-flex;align-items:center;gap:5px;border-radius:var(--r-pill);
  padding:3px 9px;font:700 10.5px/1.35 var(--font-body);letter-spacing:.04em;text-transform:uppercase}
.viz-verdict .d{width:7px;height:7px;border-radius:50%;flex:none}
.viz-verdict--met{background:var(--success-soft);color:var(--success)}
.viz-verdict--met .d{background:var(--viz-good)}
.viz-verdict--warn{background:var(--warn-soft);color:var(--warn)}
.viz-verdict--warn .d{background:var(--viz-warn)}
.viz-verdict--bad{background:var(--danger-soft);color:var(--danger)}
.viz-verdict--bad .d{background:var(--viz-bad)}
.viz-verdict--none{background:var(--surface-alt);color:var(--text-subtle)}
.viz-verdict--none .d{background:var(--text-subtle)}

@media (max-width:640px){ .viz-standard{display:none} }

/* ==========================================================================
   PATTERN 26 — ASK
   App-wide natural-language search over whatever the app knows. The nav item
   sits FIRST, above every group — Ask spans all modules, so it belongs to
   none of them.
   Three behaviours are the pattern, not the styling:
   - every answer names its sources, and each source opens the record;
     an answer that can't say where it came from is a rumour
   - restricted topics (pay, company financials) refuse WITH the reason and
     who can widen scope — rule 9 applies to answers too
   - no match is an honest "can't answer", never a confident guess
   ========================================================================== */
.ask-wrap{max-width:780px;margin:0 auto}
.ask-hero{text-align:center;padding:40px 16px 6px}
.ask-hero h2{font:700 24px/1.2 var(--font-display);margin-bottom:6px}
.ask-hero p{margin:0 0 22px;font-size:13px;color:var(--text-subtle)}
.ask-box{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--divider);
  border-radius:var(--r-card);padding:6px 8px 6px 16px;box-shadow:var(--shadow)}
.ask-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.ask-box input{flex:1;border:none;outline:none;background:transparent;font:400 15px/1.4 var(--font-body);color:var(--text);padding:10px 0}
.ask-box .btn-primary{flex:none}
.ask-suggest{display:flex;gap:7px;flex-wrap:wrap;justify-content:center;margin-top:14px}
.ask-suggest .chip{font-size:12px}
/* the transparency line: what was actually searched — computed, never typed */
.ask-scope{margin-top:26px;text-align:center;font:400 11.5px/1.5 var(--font-body);color:var(--text-subtle)}
.ask-thinking{margin-top:22px}
.ask-thinking .skel{height:15px;border-radius:6px;margin-bottom:9px}
.ask-thinking .skel:last-child{width:60%}
.ask-answer{margin-top:22px;background:var(--surface);border:1px solid var(--hairline);border-left:3px solid var(--accent);
  border-radius:var(--r-card);box-shadow:var(--shadow);padding:18px 20px}
.ask-answer .a{font:400 13.5px/1.65 var(--font-body);color:var(--text)}
.ask-answer .a b{font-weight:650}
.ask-srch{font:700 10px/1 var(--font-body);letter-spacing:.09em;text-transform:uppercase;color:var(--text-subtle);margin:16px 0 8px}
.ask-src{display:flex;gap:7px;flex-wrap:wrap}
.ask-src button{display:flex;align-items:center;gap:8px;border:1px solid var(--divider);background:var(--surface);
  border-radius:9px;padding:7px 11px;cursor:pointer;text-align:left;font-family:var(--font-body)}
.ask-src button:hover{border-color:var(--accent);background:var(--accent-soft)}
.ask-src .t{font:600 12px/1.25 var(--font-body);color:var(--text)}
.ask-src .m{font:400 10.5px/1.3 var(--font-body);color:var(--text-subtle);display:block}
.ask-src .k{font:700 8.5px/1 var(--font-mono);text-transform:uppercase;color:var(--accent);background:var(--accent-soft);
  border-radius:4px;padding:3px 5px;flex:none}
/* refusal and honest-miss cards — explanation, not a dead end */
.ask-gate{margin-top:22px;background:var(--warn-soft);border:1px solid rgba(224,169,60,.35);border-radius:var(--r-card);
  padding:16px 18px;font:400 13px/1.6 var(--font-body);color:var(--text)}
.ask-gate b{font-weight:650}
.ask-miss{margin-top:22px;background:var(--surface);border:1px dashed var(--divider);border-radius:var(--r-card);
  padding:16px 18px;font:400 13px/1.6 var(--font-body);color:var(--text-muted);text-align:center}

/* ==========================================================================
   PATTERN 27 — LOOKUP  (pick from the system of record, never type it)
   Wherever another system already holds the truth — projects in BQE Core,
   clients in Halo, devices in the RMM — the field is a SEARCH, not a text
   box. Typing the same project name a second time is how two systems drift
   apart, and nobody notices until the invoice is wrong.
   Four behaviours are the pattern, not the styling:
   - every result carries its identifier, so the person picking can tell two
     similarly-named records apart before they commit
   - a pick is SHOWN AS PICKED (.lk-picked) with a Change affordance — the
     link is a fact the person can see and undo, not an invisible side effect
   - when the source is unreachable, say so IN the field and leave the manual
     fields usable. A lookup that blocks the work when the far end is down
     is worse than no lookup
   - the manual fields the pick fills in stay editable. The record is a
     starting point; this document is the thing being written
   ========================================================================== */
.lk{position:relative}
.lk input[aria-expanded="true"]{border-bottom-left-radius:0;border-bottom-right-radius:0}
.lk-menu{position:absolute;z-index:60;top:calc(100% - 1px);left:0;right:0;max-height:288px;overflow:auto;
  background:var(--surface);border:1px solid var(--accent);border-top-color:var(--divider);
  border-radius:0 0 var(--r-input) var(--r-input);box-shadow:var(--shadow-lg)}
.lk-menu[hidden]{display:none}
.lk-opt{display:flex;align-items:baseline;gap:10px;width:100%;text-align:left;cursor:pointer;
  border:none;background:transparent;padding:9px 13px;font-family:var(--font-body);border-top:1px solid var(--hairline)}
.lk-opt:first-child{border-top:none}
.lk-opt:hover,.lk-opt[aria-selected="true"]{background:var(--accent-soft)}
.lk-opt .t{font:600 13px/1.35 var(--font-body);color:var(--text);flex:1;min-width:0}
.lk-opt .m{display:block;font:400 11px/1.4 var(--font-body);color:var(--text-subtle);font-weight:400}
/* the identifier — always present, always monospaced, so two similar names
   are told apart by the thing that is actually unique */
.lk-opt .id{font:600 11px/1 var(--font-mono);color:var(--accent);background:var(--accent-soft);
  border-radius:4px;padding:4px 6px;flex:none}
.lk-note{padding:11px 13px;font:400 12px/1.5 var(--font-body);color:var(--text-muted)}
.lk-note b{font-weight:650;color:var(--text)}
/* the pick, shown as a fact with a way back out */
.lk-picked{display:flex;align-items:center;gap:10px;background:var(--accent-soft);
  border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);
  border-radius:var(--r-input);padding:10px 12px}
.lk-picked .id{font:600 11px/1 var(--font-mono);color:var(--accent);flex:none}
.lk-picked .t{font:600 13px/1.35 var(--font-body);color:var(--text);flex:1;min-width:0}
.lk-picked .m{display:block;font:400 11px/1.4 var(--font-body);color:var(--text-subtle);font-weight:400}
.lk-picked button{flex:none;border:none;background:transparent;cursor:pointer;padding:0;
  font:600 12px/1 var(--font-body);color:var(--accent);text-decoration:underline;text-underline-offset:2px}
/* source unreachable — states it in place, never blocks the manual path */
.lk-down{display:flex;align-items:flex-start;gap:8px;background:var(--warn-soft);
  border:1px solid rgba(224,169,60,.35);border-radius:var(--r-input);padding:9px 12px;
  font:400 12px/1.5 var(--font-body);color:var(--text)}
.lk-down b{font-weight:650}

/* ==========================================================================
   PATTERN 26 — PART-TO-WHOLE DONUT
   What one whole is made of, at a glance: an account's spend across product
   families, a budget across cost centres. Six segments, never more — a seventh
   becomes "Other", because past that the small slices are decoration.

   Built to the data-viz method, and the two things that method fixes here:

   * The LEGEND IS THE CHART. Every segment is named with its value and share
     beside it, so identity never rests on colour. That is not politeness: three
     of the six light-mode hues sit under 3:1 against the surface, which the
     validator flags as WARN-with-relief — visible labels are the relief. It is
     also what makes the figure survive a greyscale print, which is how these
     get taken into a review.
   * The hues are the categorical theme's first six slots IN ORDER, never
     cycled and never re-picked per chart, so the same family is the same
     colour on every account. Validated with the data-viz validator against
     both surfaces: worst adjacent CVD ΔE 9.1 light / 8.4 dark, normal-vision
     19.6 / 19.3 — all checks pass.

   A donut and not a pie: the hole carries the total, which is the number
   someone actually reads out loud, and it stops the eye comparing angles it
   cannot compare accurately anyway.
   ========================================================================== */
.pw{--pw-1:#2a78d6;--pw-2:#eb6834;--pw-3:#1baf7a;--pw-4:#eda100;--pw-5:#e87ba4;--pw-6:#008300;
  --pw-other:var(--text-subtle);
  display:flex;align-items:center;gap:22px;flex-wrap:wrap}
html.dark .pw{--pw-1:#3987e5;--pw-2:#d95926;--pw-3:#199e70;--pw-4:#c98500;--pw-5:#d55181;--pw-6:#008300}
.pw-plot{flex:none;position:relative;width:170px;height:170px}
.pw-plot svg{display:block;width:100%;height:100%;overflow:visible}
/* A 2px surface-coloured gap between segments — the method's spacer rule, and
   the thing that keeps two adjacent hues from reading as one wedge. */
.pw-seg{fill:none;stroke-width:20;stroke-linecap:butt}
/* Held inside the RING, not the box. `inset:0` let a two-line caption run out
   under the segments, where it read as a label belonging to whichever slice it
   happened to cross. The inner circle is r60 less a 20 stroke = 100px across;
   28% inset lands just inside it, and the caption wraps rather than escapes. */
.pw-hole{position:absolute;inset:28%;display:flex;flex-direction:column;
  align-items:center;justify-content:center;text-align:center;pointer-events:none}
.pw-hole b{display:block;font:800 16px/1.15 var(--font-display);color:var(--text);
  font-variant-numeric:tabular-nums}
.pw-hole span{display:block;font:500 9.5px/1.25 var(--font-body);color:var(--text-subtle);
  text-transform:uppercase;letter-spacing:.04em;margin-top:3px}
.pw-key{flex:1;min-width:210px;display:flex;flex-direction:column;gap:2px}
.pw-key-row{display:flex;align-items:center;gap:9px;padding:4px 0;
  border-bottom:1px solid var(--hairline)}
.pw-key-row:last-child{border-bottom:none}
.pw-key-row .sw{width:10px;height:10px;border-radius:3px;flex:none}
/* Text wears text tokens, never the series colour — the swatch beside it is
   what carries identity. */
.pw-key-row .nm{flex:1;min-width:0;font:500 12.5px/1.35 var(--font-body);color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pw-key-row .vl{font:600 12.5px/1.35 var(--font-body);color:var(--text);
  font-variant-numeric:tabular-nums;flex:none}
.pw-key-row .sh{font:500 11.5px/1.35 var(--font-body);color:var(--text-subtle);
  font-variant-numeric:tabular-nums;flex:none;width:42px;text-align:right}
@media (max-width:560px){ .pw{gap:14px} .pw-plot{width:140px;height:140px} }
@media print{ .pw{gap:18px} .pw-plot{width:150px;height:150px} }

/* ==========================================================================
   PATTERN 27 — WORKSHEET TABLE (a table whose real home is paper)
   A list that gets printed, carried onto a floor, and written on: a job
   priority list, a pick list, a count sheet. It is NOT the entity list — there
   is no row click, no drawer, no filter row, because the reader is holding it.

   What paper needs and a screen table does not:
   * LANDSCAPE, declared in @page. A worksheet that needs a column dropped to
     fit portrait has had a decision made for it by the paper.
   * GRID LINES on every cell. On screen a hairline under each row is enough;
     on paper someone is tracking a number across eight columns with a finger,
     and the vertical rules are what stop the eye sliding a row.
   * A REPEATING HEADER. `thead` + `display:table-header-group` reprints the
     column names on page two. Without it the later pages are unlabelled
     numbers, which is how a sheet gets thrown away and reprinted.
   * The grouping value ONCE, at the top, not as a column repeating the same
     string down the page (R&B Wire, 2026-08-20: "Column A is not displayed as
     a separate column, but instead it is shown once at the very top of the
     page. This is to save column space.")
   * 14px/pt body. Set by the people reading it, not by the type scale.

   `break-inside:avoid` is deliberately NOT set on the table — a long worksheet
   is MEANT to run over pages; avoiding the break would push a 60-row table
   onto its own sheet and leave the first one half empty.
   ========================================================================== */
.wsheet{width:100%;border-collapse:collapse;font-family:var(--font-body);
  font-size:13px;font-variant-numeric:tabular-nums;background:var(--surface)}
.wsheet caption{caption-side:top;text-align:left;padding:0 0 8px;
  font:700 14px/1.3 var(--font-display);color:var(--text)}
.wsheet caption .when{font-weight:500;color:var(--text-subtle);margin-left:14px}
.wsheet th,.wsheet td{border:1px solid var(--divider);padding:5px 8px;text-align:left;
  vertical-align:top}
/* mit.css styles every non-.kv table header as a 10.5px uppercase micro-label,
   which is right for a dashboard and wrong on paper: the person holding this
   is matching a column name to a number across the width of a page, and
   "IN PROCESS TOTAL" at 10.5px is harder to find than "In Process Total" at 13.
   Asserted rather than inherited, so the worksheet keeps the headings it was
   specified with (R&B Wire, 2026-08-20). */
.wsheet thead th{background:var(--surface-alt);color:var(--text);
  font:700 13px/1.25 var(--font-body);letter-spacing:0;text-transform:none;
  white-space:nowrap;padding:6px 8px}
.wsheet td.num,.wsheet th.num{text-align:right;
  /* The host app's generic .num puts numbers in the mono face, which is right
     beside prose and wrong here: a worksheet is one table read in one voice,
     and mixed faces read as two different documents glued together (R&B Wire,
     feedback #30 — "the numbers need to be the same font as the two columns on
     the far left"). The table's own family wins; tabular figures keep the
     columns aligned without a second face. */
  font-family:var(--font-body)}
/* The job list is the one cell that legitimately holds several values; it wraps
   inside its own cell rather than widening the column for the worst row. */
.wsheet td.stack{white-space:pre-line;line-height:1.35}
.wsheet tbody tr:nth-child(even) td{background:var(--surface-alt)}
.wsheet-wrap{overflow-x:auto}

@page{size:landscape;margin:12mm}
@media print{
  .wsheet{font-size:14px}
  .wsheet-wrap{overflow:visible}
  /* Reprint the column names on every page. */
  .wsheet thead{display:table-header-group}
  .wsheet tfoot{display:table-footer-group}
  .wsheet tr{break-inside:avoid}
  /* Zebra striping is a screen affordance; on paper the grid does that job and
     the fill only costs toner and legibility under a pen. */
  .wsheet tbody tr:nth-child(even) td{background:transparent}
  .wsheet th,.wsheet td{border:1px solid #333}
  .wsheet thead th{background:#eee!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
  /* Each worksheet starts its own page — two departments on one sheet is how
     the wrong list gets handed to the wrong cell. */
  .wsheet-page{break-before:page}
  .wsheet-page:first-of-type{break-before:auto}
}

/* ==========================================================================
   PATTERN 28 — STANDALONE APP BAR
   For a module that normally lives INSIDE a host shell — the Company Hub's
   left nav, the client portal's rail — and therefore ships with no chrome of
   its own. Opened at its own URL it has no way to reach its other pages, and
   the reader is stranded on whatever the root redirect chose for them: TJW's
   change-order app sent every direct visitor to /change-orders with Customers,
   Proposals, Activity and Settings reachable only by typing the URL (Jake's
   test pass, 2026-08-28).
   The bar is the fallback shell, NOT a second navigation: render it only when
   the page is top-level (`window.self === window.top`) so the host's own nav
   is never doubled up inside its pane.
   ========================================================================== */
.appbar{display:flex;align-items:center;gap:14px;padding:0 22px;height:52px;
  background:var(--surface);border-bottom:1px solid var(--hairline);
  position:sticky;top:0;z-index:40;flex-wrap:wrap}
.appbar .mk{width:26px;height:26px;flex:none;border-radius:7px;display:grid;place-items:center;
  background:var(--accent);color:var(--accent-fg);font:800 9.5px/1 var(--font-display);overflow:hidden}
.appbar .mk img{width:100%;height:100%;object-fit:contain}
.appbar .nm{font:700 13.5px/1.2 var(--font-display);color:var(--text);white-space:nowrap}
.appbar .sp{flex:1}
.appbar nav{display:flex;gap:2px;flex-wrap:wrap}
.appbar a{display:inline-flex;align-items:center;gap:7px;padding:7px 11px;border-radius:8px;
  font:500 12.5px/1.2 var(--font-body);color:var(--text-muted);text-decoration:none;white-space:nowrap}
.appbar a:hover{background:var(--surface-alt);color:var(--text)}
/* aria-current, not a class — the bar is plain links, so the browser's own
   notion of "the page you are on" is the one thing that cannot drift. */
.appbar a[aria-current="page"]{background:var(--accent-soft);color:var(--accent);font-weight:600}
@media (max-width:640px){
  .appbar{height:auto;padding:9px 14px;row-gap:8px}
  .appbar .sp{flex-basis:100%}
}
@media print{.appbar{display:none}}
