/* Adealia Brokerage — the component layer.
   =========================================================================

   Load order is `adealia-tokens.css` → `app.css` → `components.css`, and this
   file is LAST on purpose: it is authoritative. The tokens file supplies the
   system, `app.css` still holds the original assistant's layout, and anything
   in here overrides both.

   WHY THIS FILE EXISTS. Nine pages were built without a component layer, so
   the app grew 265 one-off classes, five page-header classes, three content
   widths, and browser-default form controls on its three most-used screens.
   None of that was drift — `app.css` records the decision to keep the
   original assistant's structure and take only colour and type. That decision
   was right for a two-page app and was never revisited at eleven pages.
   See `planning/reports/01-ui-ux-review.md` §7.3.

   THE RULE THAT KEEPS IT WORKING: a new one-off class is a change to this
   file, or it is not merged. Everything here is `.ui-` prefixed so a reviewer
   can see at a glance whether a template is using the system or going around
   it.

   ========================================================================= */


/* ── 1. The palette, from the design concept ──────────────────────────────
   The look is `design_concept/R1 Dashboard.dc.html`, ported value for value.
   Every colour below was read out of that file's computed styles rather than
   matched by eye, and the oklch values are kept verbatim so the result is the
   concept and not an impression of it.

   THE LEGACY TOKEN NAMES STAY AND ONLY THE VALUES MOVE. That is the trick
   that makes this affordable: nine hundred lines of component CSS already
   speak in `--cream`, `--ink-1`, `--line`, so repointing the tokens repaints
   the whole app without touching the rules. `app.css` used the same move when
   it took the Adealia palette, and says so in its own header.

   WHAT CHANGED IN KIND, not just in value:
     * The shell is LIGHT. The concept gets its contrast from typography and
       one dark anchor — the filled active nav pill — instead of a dark frame.
       Navy and graphite were both attempts to solve by shell what this solves
       by type, and both read as costume.
     * The primary action is DARK INK, not copper. Copper survives only as the
       amber that marks a deadline.
     * The accent is GREEN, taken from R1's own mark.
     * An italic carries exactly one word per screen: the name in the
       greeting. The concept made that a SERIF italic and said it was the
       whole reason the design worked; the serif is gone (§Type) and the one
       italic word stayed, because the idea was the restraint and not the
       face.  */

:root {
  /* ── Ground ───────────────────────────────────────────────────────────
     THE SEPTEMBER 2026 REDESIGN'S NEUTRALS. Report 114;
     `planning/design/redesign-2026-09.md` §3 is the table they came from.

     They replace the oklch ramp ported from `design_concept/R1 Dashboard`.
     The paper is the same warm paper to within a hundredth; what changes is
     that the four grounds are further apart, that a card is WHITE rather
     than 0.995 of one, and that the two tints every rule in this file used
     to guess at inline now have names.

     Hex, not oklch, because the source is a hex table and a conversion is
     one more place to be wrong. The old ramp was oklch because it was read
     off a Claude Design file's computed styles; this one was read off a
     table somebody wrote down. */
  --page-bg:     #F7F5F0;   /* the paper, behind the whole shell */
  --rail-bg:     #FBFAF6;   /* rail, search field, tinted panels */
  --card-bg:     #FFFFFF;   /* cards, header, tables */
  --chip-bg:     #EFEBE2;   /* count badges, meter track, segmented track */
  --tint-bg:     #F2EEE6;   /* the second, warmer fill */

  --cream:       var(--page-bg);
  --warm-paper:  var(--page-bg);
  --surface-1:   var(--page-bg);
  --surface-2:   var(--rail-bg);
  --surface-3:   var(--card-bg);
  --white:       var(--card-bg);
  --sunken:      var(--rail-bg);

  /* ── Rules ────────────────────────────────────────────────────────── */
  --line:        #E4DFD5;   /* card edge — every card and control border */
  --line-1:      var(--line);
  --line-2:      #EEEAE2;   /* hairline — row dividers, section rules */
  --line-3:      #C9C1B2;   /* edge-strong — the one heavier rule, dashed drops */

  /* A SECOND CORRECTION TO THE MOCKUP, and it is the same kind as §3's.
     The mockup draws every input and textarea with a `#B6AFA3` edge, which
     measures 2.18:1 on white. WCAG 1.4.11 wants 3:1 for the boundary of a
     control, and that is exactly the case it exists for. `app.css` already
     minted a value for this — the note beside `--line-input` there has the
     history — and the oklch port silently redeclared it to 1.54:1, which
     put every field on every form back below the line it was raised over.
     So there is deliberately NO `--line-input` declaration below and
     `app.css`'s value stands:
     3.30:1 on white, 3.16 on the rail, 3.03 on the paper.
     BOUND: 2.78:1 on `--chip-bg`. No field in this app sits on a chip; one
     that did would need a darker edge, not this one. */

  /* THE FAINTEST INK IS `--ink-4` AND NOTHING ELSE.
     The redesign names two quiet greys — `#5F7085` for meta text and
     `#7A8899` for the faintest labels. §3 of the spec already replaced the
     second, because it is 3.61:1. Measured 8 September 2026, the FIRST one
     does not survive this app either: `#5F7085` is 4.26:1 on `--chip-bg`
     and 4.38:1 on `--tint-bg`, and `--muted-ink` is the token this app
     sprays over tinted rows, chips and warn panels — 60-odd call sites that
     no rule keeps on white. `--ink-4` is the same tier to the eye (5.41 vs
     5.07 on white) and clears 4.5:1 on every ground here.
     One name, one value, safe wherever it lands. */
  --muted-ink:   var(--ink-4);

  /* ── Ink ──────────────────────────────────────────────────────────────
     THE TOP OF THE RAMP IS THE BRAND NAVY AGAIN. The concept's ink was
     `oklch(0.26 0.025 255)` — #1C2530, a cool near-black that is nobody's
     brand colour. The mockup's body ink, its active nav row and its primary
     button are all `#1A2547`, which is `--ink-navy`, which this project has
     held all along. So the ramp points AT the token rather than restating a
     value beside it. */
  --ink-1:       var(--ink-navy);     /* #1A2547 — 15.00:1 on a card */
  --ink-2:       var(--steel-slate);  /* #33475B —  9.58:1 */
  --ink-3:       #55677D;             /* nav labels, inactive controls — 5.80:1 */
  --ink-4:       #616B78;             /* the faintest —  5.41:1 */

  /* ── The one dark anchor. Active nav, primary button. ─────────────── */
  --anchor:      var(--ink-navy);
  --anchor-press: var(--navy-deep);   /* #11173A — white stays 17.39:1 */
  --on-anchor:   #FFFFFF;

  /* ── Accent: COPPER. Links, the bell badge, the meter fill. ───────────
     WHAT WAS HERE BEFORE, AND WHY IT IS NOT ANY MORE. Report 22 ported
     `design_concept/R1 Dashboard.dc.html` and made two decisions in front of
     the person who has to look at this every day:

       * the accent became GREEN, taken from R1's own mark, on the grounds
         that it was the customer's colour; and
       * the primary action became DARK INK rather than copper — "the copper
         primary was chosen against the navy shell and reads as an alert on
         paper, where amber already means a deadline" — so `--copper-cta` and
         `--copper-press` were pointed at the anchor and every existing
         button followed without being edited.

     THE SEPTEMBER 2026 MOCKUP SUPERSEDES THE FIRST AND KEEPS THE SECOND.
     Its own stylesheet says `a { color: #A85A22 }` with `#7F4319` on hover;
     its bell badge is `#A85A22` with white on it; its CE meter is `#A85A22`
     on an `#EFEBE2` track. And its primary button is still `#1A2547` navy —
     so the ink anchor survives as `--anchor`, which is what
     `.ui-btn--primary` actually uses. Green was a decision about one
     customer; R1 is one customer of several, and copper is the Adealia
     brand colour that `adealia-tokens.css` has held all along.
     Report 114; `planning/design/redesign-2026-09.md` §1.

     THE INK IS `--copper-press`, NOT `--copper-cta`, AND CONTRAST DECIDES
     THAT. Measured 8 September 2026: `#A85A22` is 4.25:1 on `--chip-bg` and
     4.37 on `--tint-bg`, and a link in this app lands on both — inside CE
     evidence blocks, inside tinted notices, beside a count chip. `#94501E`
     is the same hue one step down and its WORST ground here is 5.15. Same
     argument as `--ink-4` above: one value, safe wherever the token lands,
     rather than a value that is right only where the mockup happened to
     draw it.
       ink   #94501E  6.13 white · 5.63 page · 5.87 rail · 5.30 tint · 5.15 chip
       hover #7F4319  7.72 white · 6.49 chip — the mockup's own hover
       fill  #A85A22  white on it is 5.06, which is what a badge needs */
  --accent:      var(--copper-press);   /* #94501E — link and accent INK */
  --accent-hi:   #7F4319;               /* the mockup's link hover */
  --action-blue: var(--accent);
  --blue-deep:   var(--accent-hi);
  --peach:       var(--accent);
  --peach-deep:  var(--accent-hi);
  --copper-deep: var(--accent);
  /* NOT repointed: `--copper-lift` is the vendored `#E08A45`, and its header
     says it is for accents ON NAVY only. It is 5.64:1 on `--ink-navy` and
     4.59 on `--navy-soft`, which is the darkest the chat bubble's gradient
     gets. `--accent` on that bubble would be unreadable. */

  /* ── Status. Amber is a DEADLINE, not a call to action. ───────────── */
  /* WARN GOES BACK TO THE VENDORED PAIR, 8 September 2026. Report 22 had
     paled `--warn-bg` to a near-white `#FFFBF4` so that a deadline could not
     be mistaken for a call to action. The September mockup draws the vendored
     `#FAEBDA` on every "Due tomorrow" chip, and it is the one visible colour
     this project still disagreed with it about. Report 22's reason survives
     the change: what stops amber reading as a CTA is that the CTA is navy,
     which it now is — not that the amber was washed out. `--warn-fg` on it
     is 7.18:1. `--ok-*` and `--alert-*` are already within a shade of the
     mockup and are left alone. */
  --warn-bg:     #FAEBDA;
  --warn-fg:     #7A3D10;
  --warn-dot:    oklch(0.55 0.06 65);
  --ok-bg:       oklch(0.96 0.02 155);
  --ok-fg:       oklch(0.40 0.08 155);
  --ok-dot:      oklch(0.50 0.10 155);
  --alert-bg:    oklch(0.96 0.02 30);
  --alert-fg:    oklch(0.45 0.11 30);
  --alert-dot:   oklch(0.55 0.14 30);
  /* INFO IS PAPER, NOT BLUE. The concept has no blue in it at all, and a
     blue notice on this paper is the one thing on a page that belongs to a
     different design. An explanation is not a status: it gets the tinted row
     every other quiet block gets, and the colours stay for the three states
     that actually mean something — ok, warn, alert. */
  --info-bg:     oklch(0.975 0.008 85);
  --info-fg:     oklch(0.40 0.02 255);
  --info-dot:    var(--muted-ink);
  --muted-bg:    oklch(0.95 0.008 85);
  --muted-fg:    oklch(0.46 0.012 255);
  --muted-dot:   oklch(0.66 0.010 255);

  /* ── Type ─────────────────────────────────────────────────────────── */
  --font-sans:    'Instrument Sans', Helvetica, Arial, sans-serif;
  /* ONE TYPEFACE. Instrument Sans is the body face and it is the display
     face too, so the whole UI is one family plus JetBrains Mono for data.
     That is simpler than any swap: it removes a face from the system rather
     than exchanging one for another.

     Two faces were tried and rejected in front of the person who has to look
     at this every day. Instrument Serif — the concept's choice — read as a
     newspaper at 46px and, being thin, receded against everything near it.
     Space Grotesk fixed that and brought its own letterforms with it, which
     is character a tool does not need.

     I argued for a distinct display face on the grounds that a heading should
     not be "large body text". Looking at the render, that was over-weighted:
     size, weight and colour separate a heading perfectly well, and every
     modern product UI is built on one family. Harmless beats characterful for
     a page somebody opens every morning.

     THREE WEIGHTS SINCE REPORT 114 — 400, 500, 600 — and every one is a
     real cut. 700 and 800 were spent on 24 headings and a bell badge; the
     redesign's largest text is 30/600, so 700 had nothing left to separate
     and asking for it downloaded a cut nothing drew. IBM Plex Sans lost
     partly on this: it loads 400/500/600 only, so its 700 headings would
     have been faked — and now nothing needs one.

     NOTHING IN THE TYPE IS SYNTHESISED. The greeting's one italic word was
     the last exception until `app.css` started asking for the italic axis;
     `ital,wght@1,400;1,600` is two more files and a real oblique. */
  --font-display: 'Instrument Sans', Helvetica, Arial, sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;

  /* ── SIX STEPS, THREE WEIGHTS ─────────────────────────────────────────
     `planning/design/redesign-2026-09.md` §4. The mockup states its own
     rule and it is the cleanup list: one family, six steps, three weights;
     no uppercase labels, no 46px page titles, nothing under 13px, and mono
     reserved for figures and dates so numbers line up in a column.

     Before this there were nine sizes in the vendored scale, a tenth here,
     and 119 declarations that used none of them — 61 in `app.css` and 58 in
     this file, all under 13px. Report 114 has the counts. */
  --fs-page-title: 30px;   /* 600, -0.02em. Was 46 */
  --fs-section:    18px;   /* 600 */
  --fs-row:        16px;   /* 500 — a row's own title */
  --fs-body:       16px;   /* 400 / 1.55. Was 15 */
  --fs-meta:       14px;   /* 400, --muted-ink — the line under a title */
  --fs-figure:     17px;   /* mono 500 — figures and dates ONLY */

  /* THE FLOOR, and it is a seventh size rather than a seventh step.
     A SECTION LABEL — the word over a card, a table header, a folder name.
     The mockup draws these at 12/600 and its own written rule says nothing
     under 13; the spec wins where they differ, so this is 13. It is the
     only thing between `--fs-meta` and the micro-type below, and 33
     uppercase labels landed here. */
  --fs-label:      13px;   /* 600, sentence case, no tracking */

  /* NOT TEXT AT ALL, and the only thing in the file allowed under the floor.
     A count in a pill, a digit on a bell, a five-letter tab caption under an
     icon — shapes a person recognises rather than reads, which the redesign
     itself sizes at 11–12 (§5 the bell badge, §6 the tab label, §7 the count
     pill). If what you are sizing is a sentence, you want `--fs-meta`. */
  --fs-micro:      12px;

  /* THE VENDORED SCALE POINTS AT THE STEPS. `adealia-tokens.css` ships a
     1.25-ratio scale from a 15px body — nine sizes, none of them the
     redesign's — and six hundred lines in this file and `app.css` already
     say `var(--fs-small)` and `var(--fs-caption)`. Repointing the names
     moves every one of those onto a step without touching a rule, which is
     the same move §1 makes with the colours. */
  --fs-display:  var(--fs-page-title);
  --fs-h1:       var(--fs-page-title);
  --fs-h2:       var(--fs-section);
  --fs-h3:       var(--fs-section);
  --fs-body-lg:  var(--fs-row);
  --fs-small:    var(--fs-meta);
  --fs-caption:  var(--fs-meta);
  --fs-eyebrow:  var(--fs-meta);
  /* An eyebrow that is not shouted needs no tracking to hold it apart. */
  --tracking-eyebrow: 0;

  /* ── Form ─────────────────────────────────────────────────────────── */
  /* 14 -> 12 on a desk, and back to 14 on a phone (§15). `redesign-2026-09`
     §7 states both, and report 114 §6 named this as the value phase 3 owns.
     `--shell-radius` below is the CARD THE WHOLE APP SITS IN and is a
     different object: 18px, §5. */
  --radius:      12px;   /* cards, tiles */
  --shell-radius: 18px;  /* the app's own card */
  --radius-sm:   10px;   /* buttons, inputs */
  --radius-nav:   9px;   /* nav rows */
  --radius-pill: 999px;  /* chips, badges */

  --shadow-sm: none;
  --shadow-md: 0 12px 32px color-mix(in srgb, var(--ink-navy) 10%, transparent);
  --shadow-focus: 0 0 0 2px var(--card-bg), 0 0 0 4px var(--accent);

  /* DEADLINES ESCALATE IN THREE STEPS, and only the last one is red.
     `--due-near` pointed at `--warn-dot`, a signal colour bright enough to
     read as an alarm — so "Due in 59 days" on a licence with three months to
     run looked exactly as urgent as one that had lapsed. Near is amber, spoken
     quietly; urgent is amber, spoken; overdue is the only red on the page. */
  --due-calm:   var(--muted-ink);
  --due-near:   oklch(0.50 0.07 65);
  --due-urgent: oklch(0.45 0.10 55);
  --due-past:   oklch(0.45 0.15 30);
}


/* ── 1b. The shell is light, and the anchor is one dark pill ──────────────
   252px rail on `--rail-bg`, one hairline against the page. The only filled
   shape in it is the active row, and that single dark pill is what gives a
   light shell its silhouette.

   264 -> 252 is `redesign-2026-09.md` §5's number, and report 114 §6 named it
   for this phase. Every label still fits on one line: "Continuing education"
   is the longest and it is 158px inside a 224px content box. */

.sidebar {
  width: 252px;
  background: var(--rail-bg);
  border-right: 1px solid var(--line);
  padding: 22px 14px 16px;
  gap: 26px;
}

/* THE NAV IS THREE GROUPS AND THE GROUPING IS THE POINT — §5, and §26 has the
   whole argument. The gap that used to sit between ROWS now sits between
   GROUPS; each group keeps the 2px it had. */
.nav { gap: 18px; }
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-group__label {
  margin: 0 0 4px;
  padding: 0 10px;
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-4);   /* 5.18:1 on the rail */
}
.nav-item {
  padding: 9px 12px;
  border-radius: var(--radius-nav);
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-3);
  background: none;
  box-shadow: none;
}
.nav-item:hover { background: var(--card-bg); color: var(--ink-1); }
.nav-item.active {
  background: var(--anchor);
  color: var(--on-anchor);
  font-weight: 600;
  box-shadow: none;
}
.nav-item .ico { color: currentColor; }

/* THE OUTBOUND LINKS BECOME CHIPS. Eleven rows that leave the product used to
   occupy more of the rail than the product's own pages; as chips they wrap
   into three lines and stop competing. */
/* §5 SEPARATES THE OUTBOUND LINKS FROM THE NAV WITH A HAIRLINE, and the rule
   is doing work rather than decorating: above it are this product's own
   pages, below it are seven addresses that leave it. 26px of rail gap alone
   did not say which was which. */
.links-section {
  border-bottom: 0;
  border-top: 1px solid var(--line-2);
  padding: 16px 0 0;
  overflow: visible;
}
.side-group { display: flex; flex-direction: column; gap: 8px; }
.side-group-label {
  font-family: var(--font-sans);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-4);
  padding: 0 8px;
}
.side-group:has(.side-link) {
  display: block;
}
.side-group:has(.side-link) .side-group-label { display: block; margin-bottom: 8px; }
.side-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 0 6px 6px 0;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card-bg);
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--ink-2);
}
.side-link:hover { border-color: var(--ink-4); color: var(--ink-1); background: var(--card-bg); }
.side-link .ico { display: none; }   /* a chip is a word, not a word and a glyph */

.product-link {
  border-radius: var(--radius-nav);
  color: var(--ink-3);
  padding: 8px 10px;
}
.product-link:hover { background: var(--card-bg); color: var(--ink-1); }
.product-link.current {
  background: var(--ok-bg);
  color: var(--ok-fg);
}
.product-link.current .ico { color: var(--ok-fg); }
.prod-sub { color: var(--ink-4); }
.sidebar-foot, .powered {
  color: var(--ink-4);
  border-top: 1px solid var(--line);
}
.conv-item { color: var(--ink-3); border-radius: var(--radius-nav); }
.conv-item:hover { background: var(--card-bg); }
.conv-item.active { background: var(--anchor); color: var(--on-anchor); box-shadow: none; }
.conv-empty, .hist-head { color: var(--ink-4); }
.new-chat-mini {
  background: var(--card-bg); border-color: var(--line); color: var(--ink-2);
}

/* The brand block: a green chip with the initial, the firm, and one quiet
   line under it. */
.brand-r1 { border-radius: 9px; }
.brand-logo { filter: none; opacity: 1; }

/* ── The top bar is paper, not a dark band ──────────────────────────────
   §26: CARD WHITE, NOT RAIL. The mockup's header is `#FFFFFF` with the
   search field on `#FBFAF6` inside it, and this had the two the other way
   round — a rail-coloured band with a white box in it. Swapping them is what
   makes the header read as the top of the card rather than as a second rail,
   and it puts the one thing in the header a person types into on the one
   fill §7 gives a search box. The rule under it drops to the hairline,
   because it separates two parts of one card. */
.main-col > header {
  background: var(--card-bg);
  border-bottom: 1px solid var(--line-2);
  backdrop-filter: none;
  padding: 14px clamp(16px, 4vw, 28px);
}
/* AND THE LEFT SIDE IS GIVEN BACK BELOW 900px, WHICH IS NOT DECORATION.
   `app.css` reserves 62px of header padding there so the `position: fixed`
   rail toggle never sits on the header's own content. The shorthand above
   loads after it and resets that side to 16px — which put 40px of
   `.topsearch` underneath the toggle on a phone. Reported by Danny on
   8 September 2026 and measured at 390px: toggle 12→56, search box 16→276.
   A shorthand in one file quietly undoing a longhand in another is the whole
   failure, so the clearance is restated here, where the shorthand can be
   seen next to it. */
@media (max-width: 900px) {
  .main-col > header { padding-left: 62px; }
}
.brand-name { color: var(--ink-1); font-weight: 600; }
.brand-section { color: var(--ink-3); font-weight: 500; }
.brand-section::before, .brand-sep { color: var(--ink-4); }
.bell { color: var(--ink-2); border-radius: 10px; }
.bell:hover { color: var(--ink-1); background: var(--card-bg); }
.user-chip {
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--card-bg);
  padding: 5px 10px 5px 6px;
}
.user-chip:hover, .user-menu.is-open .user-menu-btn { background: var(--rail-bg); }
.user-name { color: var(--ink-1); font-weight: 600; }
.user-role, .user-menu-caret { color: var(--ink-4); }
.avatar { background: var(--anchor); color: var(--on-anchor); background-image: none; }
.admin-bar {
  background: var(--warn-bg);
  color: var(--warn-fg);
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}

/* Flat. The concept has no gradients and they were the most dated thing in
   the stylesheet. */
.brand-mark, .welcome-mark, .msg.user .bubble, .auth-mark { background-image: none; }



/* ── 1c. Home: the greeting, the stat row, the two columns ────────────────
   These three are the concept's Home, and they are the classes the template
   was written against. Shipped in that order by mistake once — the markup
   landed a step ahead of the CSS, and the page rendered as a stacked list of
   unstyled labels with the card headers jammed against the edges. */

.ui-page-head--home { align-items: flex-start; margin-bottom: var(--space-6); }

.ui-greeting {
  font-family: var(--font-display);
  font-size: var(--fs-page-title);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--ink-1);
  margin: var(--space-2) 0 0;
}
/* ONE WORD IN ITALIC. The name, and nothing else, ever. */
/* A REAL oblique since report 114 — `app.css` asks for Instrument Sans's
   italic axis, so the browser no longer slants an upright by hand. The slant
   is what marks the name as the only personal word on the page. Report 22
   gave that job to a serif italic; the job survives the face changing. */
.ui-greeting em { font-style: italic; }

.ui-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}
.ui-stat {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 18px 20px;
  background: var(--card-bg);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
}
.ui-stat__label {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-3);
}
.ui-stat__value {
  margin: 0;
  font-family: var(--font-display);
  font-size: 34px;
  font-weight: 400;
  line-height: 1;
  color: var(--ink-1);
  font-variant-numeric: tabular-nums;
}
.ui-stat__sub { margin: 0; font-size: var(--fs-meta); color: var(--ink-4); }
.ui-stat__sub.is-warn { color: var(--warn-fg); }

/* The split. 1100px is where the side column stops earning its width. */
.ui-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: var(--space-4);
  align-items: start;
}
.ui-split--single { grid-template-columns: minmax(0, 1fr); }
.ui-split__main, .ui-split__side {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}
@media (max-width: 1100px) {
  .ui-split { grid-template-columns: minmax(0, 1fr); }
}

/* A FLUSH CARD HAS NO PADDING, so the band must not pull itself out of it.
   `.ui-card__band` escapes a padded card with a negative margin; on a flush
   card that negative margin has nothing to cancel and drags the heading
   outside the border — the header sat clipped, half above the card. */
.ui-card--flush > .ui-card__band { margin: 0; border-radius: var(--radius) var(--radius) 0 0; }
.ui-card--flush > *:not(.ui-card__band) { margin-inline: var(--space-5); }
.ui-card--flush > *:last-child { margin-bottom: var(--space-5); }



/* ── 1d. Corrections measured against the concept ─────────────────────────
   Every value here was WRONG until it was compared side by side with
   `design_concept/R1 Dashboard.dc.html` at 1440px, reading computed styles
   from both pages rather than trusting the port. Copying a design by eye gets
   the colours and misses the millimetres, and the millimetres are the
   difference between "close" and "the same". */

body { font-size: 16px; }                    /* was 15 */

/* 400 was here because Instrument Serif shipped no bold and the browser
   synthesised one. 600 is chosen and real: at 46px a 400 reads thin, and
   Instrument Sans has a true 600. */
.ui-greeting, .ui-page-head h1 { font-weight: 600; }

/* §3 and §20 now set this: 13/600, sentence case, no tracking. The 0.14em
   that was here existed to hold 11px capitals apart, and there are no
   capitals left to hold. Report 114. */
.ui-eyebrow { letter-spacing: 0; }

.ui-stat__label { font-size: var(--fs-meta); }         /* was 13 */

/* The concept's buttons are padded, not fixed-height. A height plus zero
   vertical padding gave a shorter control and a different optical weight. */
.ui-btn {
  height: auto;
  padding: 11px 18px;
  font-size: 13.5px;
  line-height: 1;
}
.ui-btn--sm { padding: 7px 12px; font-size: var(--fs-label); }

.ui-page { padding: 34px 40px 56px; }        /* was 32px 40px 72px */



/* ── 2. The page shell ────────────────────────────────────────────────────
   ONE wrapper and ONE header for every page. This replaces `.dash`, `.people`,
   `.page` and `.prof` (three different max-widths) and `.dash-head`,
   `.people-head`, `.prof-head`, `.ev-head` and `.lib-head` (five headers for
   one concept, one page stacking two of them).

   1240px, not 1040: at 1440 the old column left a 240px dead gutter beside a
   262px sidebar, so 48% of the viewport was empty paper on the CE page. */

.ui-page {
  flex: 1;
  overflow-y: auto;
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding: var(--space-7) clamp(var(--space-4), 4vw, var(--space-8)) var(--space-10);
}

.ui-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  /* ROW-GAP 0, deliberately. A single `gap` also applies between wrapped
     flex ROWS, so on the pages whose subtitle is a direct child rather than
     inside `__text` it added 16px on top of the subtitle's own 8px margin —
     the h1-to-subtitle step measured 8px on eight pages and 24px on Profile
     and the Licensing queue, and pushed the queue's back link to 28px above
     its title. The columns still need their gap; the rows never did. */
  row-gap: 0;
  column-gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
}
.ui-page-head__text { min-width: 0; }

/* TOLERANT OF BOTH SHAPES. Some pages wrap their title and subtitle in a
   `__text` div so an action can sit beside them; others put the back link,
   the h1 and the subtitle straight in. Rather than force one markup, the
   direct children that should own a line claim one. */
.ui-page-head > .ui-back,
.ui-page-head > h1,
.ui-page-head > .ui-page-head__sub { flex: 0 0 100%; }
.ui-page-head > h1 + .ui-page-head__sub { margin-top: var(--space-2); }

.ui-page-head h1 {
  font-family: var(--font-display);
  font-size: var(--fs-page-title);
  font-weight: 600;
  letter-spacing: var(--tracking-display);
  line-height: var(--lh-tight);
  color: var(--ink-1);
  margin: 0;
  text-wrap: balance;
}
.ui-page-head__sub {
  font-size: var(--fs-small);
  color: var(--ink-3);
  margin: var(--space-2) 0 0;
}
.ui-page-head__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* The route back up. Sits ABOVE the title, not beside it, so it never
   competes with the h1 for the first fixation. */
.ui-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--ink-3);
  text-decoration: none;
  margin-bottom: var(--space-3);
}
.ui-back:hover { color: var(--copper-cta); }


/* The section crumb in the header. Quiet — it says where you are, and the
   page's own <h1> says what this is. The separator is a pseudo-element so a
   page that sets no section renders neither, with no conditional. */
.brand-section { color: var(--ink-2); font-weight: 500; }
.brand-section:empty { display: none; }
.brand-section::before {
  content: "/";
  color: var(--ink-4);
  font-weight: 400;
  margin: 0 var(--space-2);
}


/* ── 3. Eyebrow, and the end of shouting ──────────────────────────────────
   There were eight specifications for one label style — 10.5/11/12/13px, in
   three colours and four letter-spacings — so "WHAT YOU NEED TO DO" shouted
   louder than the content of its own card while "ROLE" was nearly invisible.

   ONE eyebrow, and it labels a SECTION. A field label is not an eyebrow: it
   is sentence case, and it is quiet, because the value is the thing being
   read and the label is only there to say what it is. */

/* THE END OF SHOUTING, FINISHED. §3 gave the app one eyebrow instead of
   eight and left it uppercase; report 114 takes the caps off it too. It is
   the section label now — 13/600, sentence case, sans, no tracking — and
   nine other labels in this file were made the same thing. */
.ui-eyebrow {
  font-family: var(--font-sans);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-3);
  margin: 0;
}

.ui-label {
  display: block;
  font-size: var(--fs-caption);
  font-weight: 500;
  color: var(--ink-3);
  margin-bottom: var(--space-1);
  text-transform: none;
  letter-spacing: 0;
}
.ui-value {
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--ink-1);
}

/* The same rule, applied to the labels that already exist under their own
   names. These are FIELD labels — "Full name", "Phone", "Office" — and every
   one of them was 11px uppercase with letter-spacing, which is an eyebrow.
   An eyebrow labels a section; there is one per screen. Forty of them down a
   form is just shouting, and it was loud enough that "ROLE" competed with the
   person's name for attention on the roster. */
.ui-page .prof-grid > label,
.ui-page .lic-grid > label,
.ui-page .prof-readonly dt,
.ui-page .person-label,
.ui-page .ev-desc-label,
.ui-page .course-search label,
.ui-page .auth-label {
  font-family: var(--font-sans);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-3);
}

/* ONE TEXT EDGE PER CARD.
   A label's ink started at the control's BOX edge while the control's own
   text started 13px further in — 1px border plus 12px padding — so every
   label on every form sat 13px left of the value it described. Measured at
   exactly 13.00px on twenty pairs. Worse, the read-only rows on the SAME
   Profile card measured 0.00px, so one card held two different text edges
   twenty pixels apart, which is what makes a form read as unaligned even
   when nobody can say why.

   `text-indent` rather than padding, because these labels WRAP their control
   — padding would move the input too and preserve the offset. text-indent
   inherits, so the nested control has to put it back. */
.ui-page .prof-grid > label,
.ui-page .lic-grid > label,
.ui-page .ev-desc-label,
.ui-page .course-search label,
.ui-page .auth-label {
  text-indent: calc(var(--space-3) + 1px);
}
.ui-page .prof-grid > label > input,
.ui-page .prof-grid > label > select,
.ui-page .lic-grid > label > input,
.ui-page .lic-grid > label > select,
.ui-page .ev-desc-label > textarea,
.ui-page .course-search label > input,
.ui-page label > .ui-file { text-indent: 0; }

/* The read-only pairs join the same edge, so the card has one. */
.ui-page .prof-readonly dt,
.ui-page .prof-readonly dd { padding-left: calc(var(--space-3) + 1px); }

/* A standalone label above a control does not wrap it, so padding is right. */
.ui-label { padding-left: calc(var(--space-3) + 1px); }
/* The label wraps its control on these forms, so only its own first line
   should carry the label styling — the value inside stays full size. */
.ui-page .prof-grid > label > input,
.ui-page .prof-grid > label > select,
.ui-page .lic-grid > label > input,
.ui-page .lic-grid > label > select {
  font-size: var(--fs-body);
  color: var(--ink-1);
  margin-top: var(--space-1);
}
.ui-page .prof-readonly dd { font-size: var(--fs-body); color: var(--ink-1); }


/* ── 4. Card ──────────────────────────────────────────────────────────────
   A line, not a shadow. */

.ui-card {
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5) var(--space-5);
}
.ui-card--sunken { background: var(--surface-2); border-color: var(--line-2); }
.ui-card--flush  { padding: 0; overflow: hidden; }

/* ADE-176. The queue card Licensing's task links to by fragment.
   `.main-col > header` is `position: sticky; top: 0`, so a bare anchor
   scrolls the card's own heading underneath the bar. This is the browser's
   own answer to that and needs no JavaScript. 84 is the bar's measured bottom
   edge — 74px at 1280px — plus ten; the walkthrough asserts the card's top
   edge is BELOW the bar rather than trusting the number. */
.ce-queue-card { scroll-margin-top: 84px; }

.ui-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.ui-card__title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-1);
  margin: 0;
}


/* ── 5. Buttons ───────────────────────────────────────────────────────────
   ONE copper action per view, on the REPEATING action.

   The rule existed and was applied backwards: the Events page had four navy
   Register buttons and no copper at all, while the Admin page gave a 910px
   full-bleed copper bar to "Create brokerage" — the least frequent action on
   it. Copper marks the thing you came to do, at a normal size. */

.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 34px;
  padding: 0 var(--space-4);
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.ui-btn:disabled { opacity: 0.45; cursor: not-allowed; }

.ui-btn--primary {
  background: var(--copper-cta);
  color: var(--on-anchor);
  border-color: var(--copper-cta);
}
.ui-btn--primary:hover:not(:disabled) {
  background: var(--copper-press);
  border-color: var(--copper-press);
  color: var(--on-anchor);
}

.ui-btn--secondary {
  background: var(--surface-3);
  color: var(--ink-1);
  border-color: var(--line-input);
}
.ui-btn--secondary:hover:not(:disabled) {
  background: var(--surface-2);
  border-color: var(--ink-4);
}

.ui-btn--ghost {
  background: none;
  color: var(--ink-2);
  border-color: transparent;
  padding: 0 var(--space-2);
}
.ui-btn--ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--ink-1); }

/* Destructive, and deliberately NOT a text link beside a solid button.
   Approve and Reject carry equal consequence — a rejection reaches a broker —
   and they were rendered at wildly unequal weight. */
.ui-btn--danger {
  background: var(--surface-3);
  color: var(--alert-fg);
  border-color: var(--alert-dot);
}
.ui-btn--danger:hover:not(:disabled) { background: var(--alert-bg); }

.ui-btn--sm { height: 28px; padding: 0 var(--space-3); font-size: var(--fs-caption); }


/* ── 6. Form controls ─────────────────────────────────────────────────────
   The base rule is UNSCOPED. It was scoped to `.auth-form` and `.panel`, so
   every field outside those two containers rendered as an operating-system
   default: 48 native selects on the roster, `mm/dd/yyyy` on the CE form, and
   two raw "Choose File" buttons on the page Licensing uses most. */

/* SELECTED BY EXCLUSION, not by enumeration. The list used to name seven
   types, so `tel`, `url`, `time`, `datetime-local`, `month` and `week` fell
   through to the browser default — the Phone field on Profile rendered 33px
   tall in Arial with a 10px text inset, in a row beside two 36px IBM Plex
   fields. The focus rule below already used a bare `input`, so the two rules
   disagreed about which controls existed. Any future input type is now
   styled by default and has to opt OUT. */
.ui-input,
.ui-page input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]),
.ui-page select,
.ui-page textarea {
  width: 100%;
  min-height: 36px;
  padding: 0 var(--space-3);
  font-family: var(--font-sans);
  /* 16px on a phone or iOS zooms the whole page on focus. The rule this
     replaces was found by that happening. */
  font-size: var(--fs-body);
  color: var(--ink-1);
  background: var(--surface-3);
  border: 1px solid var(--line-input);
  border-radius: var(--radius-sm);
  appearance: none;
  -webkit-appearance: none;
}
.ui-page textarea { padding: var(--space-2) var(--space-3); min-height: 80px; resize: vertical; }
.ui-page input::placeholder, .ui-page textarea::placeholder { color: var(--ink-4); }

/* The chevron is ours because `appearance: none` removes the platform's. */
.ui-page select {
  padding-right: var(--space-7);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%234F6178' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
}

.ui-page input:focus-visible,
.ui-page select:focus-visible,
.ui-page textarea:focus-visible,
.ui-input:focus-visible {
  outline: none;
  border-color: var(--action-blue);
  box-shadow: var(--shadow-focus);
}

/* A file input cannot be styled, so it is hidden behind a label that can be.
   `.ui-file input` stays focusable — it is moved out of sight, not removed,
   because `display: none` would take it out of the tab order. */
.ui-file { display: inline-flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.ui-file input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  overflow: hidden;
}
.ui-file__button {
  /* Explicit, because the legacy upload forms set uppercase on their labels
     and this button sits inside one. */
  text-transform: none;
  letter-spacing: 0;
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: 34px; padding: 0 var(--space-4);
  font-size: var(--fs-small); font-weight: 600;
  color: var(--ink-1); background: var(--surface-3);
  border: 1px solid var(--line-input); border-radius: var(--radius-sm);
  cursor: pointer;
}
.ui-file__button:hover { background: var(--surface-2); }
.ui-file input[type="file"]:focus-visible + .ui-file__button {
  border-color: var(--action-blue); box-shadow: var(--shadow-focus);
}
.ui-file label { display: inline-flex; position: relative; }

/* WHAT IS STAGED FOR UPLOAD. ADE-184. `.ui-file` hides the native control, so
   the filename it would have printed has to be printed here instead. Empty and
   `hidden` until `chrome.js` has something to say, so a form with no file
   chosen looks exactly as it did before this existed.

   The `[hidden]` rule is not redundant: `display: block` below would otherwise
   beat the browser's own `[hidden] { display: none }`, and the panel would
   render as an empty box under every picker on the page. */
.ui-file-staged { display: block; margin-top: var(--space-3); }
/* The library's upload form is a horizontal flex row — Choose, Upload, hint —
   so the panel has to break the row rather than become a squeezed column
   between the two buttons. Measured at 1280px: without this the file names
   wrap to two lines and push `Upload` to the right of them. */
.lib-upload .ui-file-staged { flex-basis: 100%; margin-top: 0; }
.ui-file-staged[hidden] { display: none; }
.ui-file-staged__list { margin: 0; padding: 0; list-style: none; }
.ui-file-staged__row {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-3);
  padding: 6px 0;
  font-size: var(--fs-small);
  color: var(--ink-2);
}
.ui-file-staged__row + .ui-file-staged__row { border-top: 1px solid var(--line-2); }
/* The name can be a phone camera's 40-character filename, so it wraps rather
   than pushing the size off a 390px screen. `anywhere` and not `break-all`:
   a name breaks at its own separators first. */
.ui-file-staged__name { font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
.ui-file-staged__size { color: var(--ink-4); font-variant-numeric: tabular-nums; }
/* A whole-row line rather than a badge: it is a sentence about what will
   happen to this file, not a status word. */
.ui-file-staged__warn { flex-basis: 100%; color: var(--warn-fg); }
.ui-file-staged__row.is-over .ui-file-staged__name { color: var(--warn-fg); }
/* A 34px tap target, matching `.ui-file__button` beside it. The phone sweep
   measures this — it did NOT when this comment was first written, and review
   caught the comment rather than the CSS: `walk_a_file_is_staged` ran only
   from desktop walks and `walk_phone` staged no file at all. It stages one on
   `/resources` now and measures this box, which is the case the ticket is
   about — a broker attaching a certificate from her phone. */
.ui-file-staged__clear {
  display: inline-flex; align-items: center;
  margin-top: var(--space-2);
  min-height: 34px; padding: 0 var(--space-3);
  font-size: var(--fs-small); font-weight: 600;
  color: var(--ink-2); background: transparent;
  border: 1px solid var(--line-input); border-radius: var(--radius-sm);
  cursor: pointer;
  /* The label carries a filename, so it must be free to wrap. */
  text-align: left; overflow-wrap: anywhere;
}
.ui-file-staged__clear:hover { border-color: var(--ink-4); color: var(--ink-1); }
.ui-file-staged__clear:focus-visible {
  outline: none; border-color: var(--action-blue); box-shadow: var(--shadow-focus);
}

/* A plain count beside a section name. Not a status, so not a badge. */
.course-count { font-size: var(--fs-small); color: var(--ink-3); }

.ui-field { display: flex; flex-direction: column; }
.ui-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-4);
}
.ui-hint { font-size: var(--fs-caption); color: var(--ink-3); margin: var(--space-1) 0 0; }


/* ── 7. Badge ─────────────────────────────────────────────────────────────
   Five validated semantic pairs sat unused while status was rendered as grey
   body text — "active" was a word inside a native select, "Needs review" was
   copper text with no chip. Colour now carries the state, and the text still
   carries it too, so nothing depends on seeing hue. */

.ui-badge {
  display: inline-flex;
  align-items: center;
  /* ASYMMETRIC PADDING, because the dot is not symmetric load. With 12px on
     both sides plus a 6px dot and an 8px gap, the text sat 26px from the left
     edge and 12px from the right — a chip that looks left-heavy however
     carefully the padding was matched. 8/12 with a 4px gap puts the OPTICAL
     centre where the mathematical one was supposed to be. */
  gap: var(--space-1);
  height: 22px;
  padding-inline: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.ui-badge::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.ui-badge--ok    { background: var(--ok-bg);    color: var(--ok-fg); }
.ui-badge--warn  { background: var(--warn-bg);  color: var(--warn-fg); }
.ui-badge--alert { background: var(--alert-bg); color: var(--alert-fg); }
.ui-badge--info  { background: var(--info-bg);  color: var(--info-fg); }
.ui-badge--muted { background: var(--muted-bg); color: var(--muted-fg); }
.ui-badge--bare::before { display: none; }


/* ── 8. Table ─────────────────────────────────────────────────────────────
   Twelve people rendered as twelve 87px cards — about four and a half rows on
   a 900px screen. The Admin users page already proved the app could do
   better at 37px a row, which is why it read more like a product than the
   People page did.

   One card around the whole table, hairlines between rows, no shadow. */

.ui-table-wrap {
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.ui-table-scroll { overflow-x: auto; }

.ui-table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
.ui-table th {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-3);
  background: var(--surface-3);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.ui-table td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line-2);
  color: var(--ink-2);
  vertical-align: middle;
}
.ui-table tbody tr:last-child td { border-bottom: 0; }
.ui-table tbody tr:hover { background: var(--surface-2); }
.ui-table__num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ui-table__right { text-align: right; }

/* The whole row is the target, and the name is still the accessible link.
   The pseudo-element covers the row, so a click anywhere lands on the anchor
   without nesting one inside every cell. */
.ui-table tbody tr { position: relative; }
.ui-table__link {
  font-weight: 600;
  color: var(--ink-1);
  text-decoration: none;
}
/* SCOPED TO A ROW, AND THAT IS ADE-70's FINDING RATHER THAN TIDINESS.
   `position: absolute; inset: 0` is positioned against the nearest POSITIONED
   ancestor, and the only thing that makes that the row is
   `.ui-table tbody tr { position: relative }` two rules up. Used ANYWHERE
   ELSE the anchor has no positioned ancestor, so `inset: 0` resolves against
   the page — and the pseudo-element becomes an invisible full-page shield
   that swallows every click under it.

   Measured 5 September 2026. ADE-70 put `ui-table__link` on the links to the
   other occurrences of a repeating event, in a `<ul>` on `/events/<id>`. The
   list rendered correctly, every unit test was green, and the **Undo** button
   on the invitation hold three cards below became unclickable: Playwright
   reported *"<a class='ui-table__link' …> intercepts pointer events"* sixty
   times and timed out. That is state-of-play §2's instrument pair read the
   other way — the thing a person can see is not the thing a tap lands on —
   and it was invisible to `pytest` because the markup was right.

   The selector now says where the mechanism works. An anchor outside a table
   row keeps the weight and the colour and gets no overlay. */
.ui-table tbody tr .ui-table__link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.ui-table tbody tr:hover .ui-table__link { color: var(--copper-cta); }
/* Anything a person can actually operate has to sit above that overlay.
   ADE-59 ADDED `input` AND `label`, AND IT WAS FOUND THE SAME WAY ADE-70's
   own entry was: the preferences grid is a `.ui-table` whose cells hold
   checkboxes, every one of them rendered correctly, and Playwright reported
   `<span class="ui-table__link"> … intercepts pointer events` and timed out
   trying to untick one. The list above was a list of the controls that
   existed in a table cell when it was written, and a checkbox was simply not
   one of them yet. */
.ui-table td form,
.ui-table td button,
.ui-table td select,
.ui-table td input,
.ui-table td label,
.ui-table td .ui-badge { position: relative; z-index: 1; }

.ui-table__sub { display: block; font-size: var(--fs-caption); color: var(--ink-3); }


/* ── 9. Count, and the filled card head ───────────────────────────────────
   WHAT REPLACED THE METRIC STRIP. A row of big numbers above the cards
   restated them — "2 YOUR ACTIONS" sat directly above a card listing those
   two actions — and it was a second grid, so its dividers never met the gaps
   between the cards below. The number belongs to the card, so it lives in the
   card's head.

   The filled head is the depth device this product had none of. A card was a
   white box with a heading in it; now it has a top band on `--surface-2` with
   a rule under it, which is what makes a list of rows read as a table rather
   than as loose text. */

.ui-card__band {
  margin: calc(var(--space-5) * -1) calc(var(--space-5) * -1) var(--space-4);
  padding: var(--space-3) var(--space-5);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.ui-count {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  padding: 1px 8px;
  margin-left: var(--space-2);
  vertical-align: 2px;
}
.ui-count--warn {
  color: var(--warn-fg);
  background: var(--warn-bg);
  border-color: transparent;
}

/* ── 10. Empty state ──────────────────────────────────────────────────────
   Absence was a grey sentence floating over 700px of blank canvas. An empty
   state says what is missing, what follows from that, and either offers the
   action or names who can take it — because on several of these screens the
   reader genuinely cannot do anything, and telling them to is worse than
   silence. */

/* NOT A DASHED BOX. A dashed border is a mockup convention — it says "content
   goes here" to a designer and "something is broken" to everybody else, and
   it was the first thing Cam and Pat saw. Absence on paper, under a rule,
   left-aligned like every other block on the page. */
.ui-empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: var(--space-2);
  padding: var(--space-7) 0 var(--space-4);
  background: none;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
}
.ui-empty__title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  color: var(--ink-1);
  margin: 0;
}
.ui-empty__body { font-size: var(--fs-small); color: var(--ink-3); margin: 0; max-width: 46ch; }

/* Plain h2/p inside an empty state get the same treatment without needing a
   class each. Several of these predate the component and read perfectly well
   as headings; only their styling was missing. */
.ui-empty > h2, .ui-empty > h3 {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  color: var(--ink-1);
  margin: 0;
}
.ui-empty > p { font-size: var(--fs-small); color: var(--ink-3); margin: 0; max-width: 46ch; }
.ui-empty__action { margin-top: var(--space-3); }


/* ── 11. Section ──────────────────────────────────────────────────────────
   Vertical rhythm between blocks on a page, so a template never reaches for
   a margin of its own. */

/* A form that reads as one row of controls rather than a block. */
.ui-inline { display: inline-flex; align-items: center; gap: var(--space-2); }

.ui-stack   { display: flex; flex-direction: column; gap: var(--space-6); }
.ui-stack--tight { gap: var(--space-3); }
.ui-grid-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--space-4);
  /* Cards size to their content. Stretching them to the tallest in the row
     gives the short ones a block of dead space under the last line, which
     reads as something that failed to load. */
  align-items: start;
}


/* ── 12. Motion ───────────────────────────────────────────────────────────
   There was none at all in anything built after the chat page: rows did not
   respond to hover, buttons did not respond to press. One rule, one duration,
   and a reduced-motion guard — adding animation without one would be creating
   a fault while fixing another. */

.ui-btn, .ui-card, .ui-table tbody tr,
.ui-input, .ui-page input, .ui-page select, .ui-page textarea,
.ui-file__button, .ui-back, .ui-table__link {
  transition:
    background-color var(--dur-base) var(--ease-out),
    border-color     var(--dur-base) var(--ease-out),
    color            var(--dur-base) var(--ease-out),
    box-shadow       var(--dur-base) var(--ease-out);
}
.ui-btn:active:not(:disabled) { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ── 13. The card internals, restyled rather than renamed ─────────────────
   `.card-title`, `.card-heading`, `.card-action` and `.card-task*` were
   already the SHARED vocabulary — app.css says so: "Card internals. Kept
   generic — every vertical's card uses these." That decision was right, so
   these keep their names and change their appearance. Renaming them would
   have touched six partials to no benefit.

   The one that mattered: `.card-title` was 13px/700 UPPERCASE, which is why
   "WHAT YOU NEED TO DO" shouted louder than the card's own content. A card
   title is a title. */

.ui-card .card-title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--ink-1);
  margin: 0 0 var(--space-3);
}
.ui-card .card-heading {
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--ink-2);
  margin: 0 0 var(--space-3);
}
.ui-card .card-tasks { gap: var(--space-3); margin-bottom: var(--space-4); }
.ui-card .card-task { gap: var(--space-1); }
.ui-card .card-task-title { font-size: var(--fs-small); color: var(--ink-1); }
.ui-card .card-task-title:hover { color: var(--accent); }
.ui-card .card-task-when { font-size: var(--fs-caption); color: var(--ink-3); }

/* The card's action is a real control, not an underlined afterthought. */
.ui-card .card-action {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 0 var(--space-3);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink-1);
  background: var(--surface-3);
  border: 1px solid var(--line-input);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background-color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
.ui-card .card-action:hover {
  text-decoration: none;
  background: var(--surface-2);
  border-color: var(--ink-4);
}

/* STATUS BECOMES A BADGE, everywhere the same. These five words were coloured
   text on six different screens and read as emphasis rather than as state.
   The text still says what the colour says, so nothing depends on hue. */
.lic-expiry, .card-task-when.task-overdue, .card-task-when.task-today,
.person-status, .ce-state {
  display: inline-flex;
  /* A badge is as wide as its word. These sit inside column flex containers
     (`.ce-card-row`, `.card-task`), where the default `align-items: stretch`
     made every one of them span the whole card. */
  align-self: flex-start;
  align-items: center;
  gap: var(--space-2);
  height: 22px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  white-space: nowrap;
}
.lic-expiry::before, .person-status::before, .ce-state::before,
.card-task-when.task-overdue::before, .card-task-when.task-today::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}

.lic-ok,       .ce-matched, .ce-approved,
.ce-state--met                           { background: var(--ok-bg);    color: var(--ok-fg); }
.lic-expiring, .ce-questionable, .ce-unmatched,
.ce-state--short,
.card-task-when.task-today               { background: var(--warn-bg);  color: var(--warn-fg); }
.lic-expired,  .ce-rejected,
.card-task-when.task-overdue             { background: var(--alert-bg); color: var(--alert-fg); }
/* Not measured is MUTED, never alarming. Joining the warning colours would
   say a broker is behind when the truth is that nobody has recorded their
   state's rules — a fact about us, not about them. */
.lic-unknown, .ce-state--unknown         { background: var(--muted-bg); color: var(--muted-fg); }
.person-status                           { background: var(--muted-bg); color: var(--muted-fg); }


/* ── 13e. A deadline escalates ────────────────────────────────────────────
   THE COMPLAINT THIS ANSWERS: nothing catches the eye. A licence 59 days from
   expiry rendered as 13px grey small print — the quietest thing on the screen
   was the reason the broker opened the app.

   Four states, one vocabulary, applied wherever a date is shown. They differ
   by WEIGHT as well as by colour, so the escalation survives a colour-blind
   reader and a bad phone screen in sunlight. */

.task-later   { color: var(--due-calm);   font-weight: 400; }
.task-soon    { color: var(--due-near);   font-weight: 500; }
.task-urgent  { color: var(--due-urgent); font-weight: 600; }
.task-today   { color: var(--due-urgent); font-weight: 600; }
.task-overdue { color: var(--due-past);   font-weight: 600; }
.task-none    { color: var(--ink-4);      font-weight: 400; }

.lic-ok       { color: var(--ok-fg); }
.lic-expiring { color: var(--due-near);   font-weight: 600; }
.lic-expired  { color: var(--due-past);   font-weight: 600; }

/* THE ROW ITSELF CARRIES THE STATE, not only its date. A person scanning a
   list of twenty reads the left edge before they read any words. */
.ui-table tbody tr:has(.task-soon)    td:first-child { box-shadow: inset 3px 0 0 var(--muted-dot); }
.ui-table tbody tr:has(.task-urgent)  td:first-child { box-shadow: inset 3px 0 0 var(--warn-dot); }
.ui-table tbody tr:has(.task-today)   td:first-child { box-shadow: inset 3px 0 0 var(--copper-amber); }
.ui-table tbody tr:has(.task-overdue) td:first-child { box-shadow: inset 3px 0 0 var(--alert-dot); }
.card-task:has(.task-soon), .card-task:has(.task-urgent),
.card-task:has(.task-today), .card-task:has(.task-overdue) {
  padding-left: var(--space-3);
  border-left: 3px solid var(--muted-dot);
}
.card-task:has(.task-urgent)  { border-left-color: var(--warn-dot); }
.card-task:has(.task-today)   { border-left-color: var(--copper-amber); }
.card-task:has(.task-overdue) { border-left-color: var(--alert-dot); }


/* ── 13b. Mono does the data ──────────────────────────────────────────────
   The token file says "Numerals: IBM Plex Mono, because this product is read
   as data" — and then every actual datum was set in the sans. Mono appeared
   at 10–12.5px for LABELS and nowhere else. The Licensing queue exists to
   compare `WRONG-1` against `NM-CON-210`, and the only mono on that screen
   was the word BROKER.

   The rule: mono for anything you compare character by character — licence
   numbers, course numbers, dates, hours, file names, emails. Sans for prose.
   Plex Mono runs large beside Plex Sans, so it is set at 0.94em to match. */

   A FIRST PASS OF THIS WAS TOO GREEDY and is worth recording. Selecting by
   the classes that usually hold data — `.lic-name`, `.card-event-when`,
   `.prof-readonly dd` — put a course NAME and a full date sentence into mono,
   and "Coming up" started reading like a terminal window. Those classes carry
   a datum on one screen and prose on another, so the class cannot decide it.
   Mono is opted INTO, by `.ui-data` and `.ui-id`, plus the few selectors that
   are only ever a datum. */

.person-email, .ui-table__sub,
.history-when, .note-when,
.course-tbl td, .tbl td,
.ui-data {
  font-family: var(--font-mono);
  font-size: 0.94em;
  font-variant-numeric: tabular-nums;
}

/* An identifier you compare gets a field to sit in. */
.ui-id, .course-number {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  color: var(--ink-2);
}


/* ── 13c. Status colours the OBJECT ───────────────────────────────────────
   Five validated semantic pairs, all of them spent on 22px pills. Bea has a
   licence expiring and two courses stuck, and nothing on her screen LOOKS
   wrong, because visually nothing is. There was exactly one place in the
   product where state coloured a whole object — the copper left edge on an
   unread notification — and it was the best-looking thing in the app.
   Generalised, as an edge rather than a wash, so no contrast is spent. */

.ui-card--flag       { border-left: 3px solid var(--warn-dot); }
.ui-card--flag-alert { border-left: 3px solid var(--alert-dot); }
.ui-card--flag-ok    { border-left: 3px solid var(--ok-dot); }
.ui-card--flag .ui-card__band,
.ui-card--flag-alert .ui-card__band,
.ui-card--flag-ok .ui-card__band { border-radius: 0 var(--radius) 0 0; }

/* On the Licensing queue this becomes a colour column down the left edge,
   readable before a single word — which is how somebody doing forty of these
   a day actually works. */
.ce-item--questionable, .ce-item--unmatched { box-shadow: inset 3px 0 0 var(--warn-dot); }
.ce-item--rejected                          { box-shadow: inset 3px 0 0 var(--alert-dot); }
.ce-item--matched, .ce-item--approved       { box-shadow: inset 3px 0 0 var(--ok-dot); }


/* ── 13d. The page ends ───────────────────────────────────────────────────
   Every screen stopped mid-air over a field of paper. A rule and one mono
   line is the cheapest possible answer, and it is exactly the detail that
   makes a record system feel like one. */

/* The footer rides in its own shell so it shares the page's gutter, but it
   must not claim the flex height that `.ui-page` does or it pushes the
   content up. */
/* 24px below a 10.5px line of type was more air than the mark needs; 16
   keeps it a footer without making it a band.
   ADE-168: this line used to end "the phone rule further down overrides this
   to clear the bottom bar and is not affected", and BOTH halves were wrong.
   §15's `.ui-page` shorthand overrode it to 56px, and clearing the bar is
   not this element's job — `.main-col` reserves the bar's height now, and
   §15 restates these two longhands after that shorthand so the footer keeps
   the value below. */
.ui-page--foot { flex: none; overflow: visible; padding-top: 0; padding-bottom: var(--space-4); }

/* Anything that is a datum rather than prose. */
.ui-data { font-family: var(--font-mono); font-size: 0.94em; font-variant-numeric: tabular-nums; }

.ui-page-foot {
  /* `margin: 0`, not `margin-top: 0`. This is a <p>, so it carried the
     browser's default 1em bottom margin — 10.5px of nothing under the last
     rule on every page, which is a sixth of the footer's whole height. The
     top was zeroed when this was written and the bottom was missed. */
  margin: 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-2);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-4);
}


/* The bottom bar exists only on a phone. Above 720px the rail is visible and
   a second navigation would be two answers to one question. */
.tabbar { display: none; }
.tabbar__tab { position: relative; }


/* ── 14. The precision layer ──────────────────────────────────────────────
   Everything here was found by MEASURING the running app rather than reading
   the source, which is why none of it was caught while the components were
   being written. Each rule names the number it corrects. */

/* A header that claims to be right-aligned, and is not. `.ui-table th` sets
   `text-align: left` at specificity (0,1,1); `.ui-table__right` is (0,1,0),
   so the parent won and the modifier did nothing. On Events the "You" header
   sat 193.70px left of the buttons it labels. */
.ui-table th.ui-table__right { text-align: right; }

/* Sibling cards were touching — measured 0.00px between them on Profile, CE,
   Approved courses and the Licensing queue, where two 12px radii meet and cut
   a paper-coloured pinch out of the seam. Section rhythm across the app was
   0 / 16 / 24px depending on which wrapper a page happened to use. */
.ui-page > .ui-card + .ui-card,
.ui-page > form > .ui-card + .ui-card { margin-top: var(--space-6); }

/* NOT FIXED, and the attempt is recorded because it was a bad trade.
   The crumb genuinely rides ~0.75px low: the bar centres a 17px brand name
   against a 15px section, so their baselines do not quite meet. The obvious
   fix — `align-items: baseline` on `.brand` — makes it worse, because the row
   also holds two IMAGES, and an image in a baseline-aligned row sits its
   BOTTOM edge on the text baseline. Measured: the header grew from 76px to
   84px and the brand name fell 9px below the mark it sits beside. A 0.75px
   error is below the threshold anyone can see; a 9px one is not.
   Left centred. If it is ever worth fixing, align the two text spans to each
   other inside their own wrapper, not the whole row. */

/* FIGURES THAT LINE UP. Every one of these repeats down a column — "Due in 2
   days" over "Due in 59 days", three stacked CE cards showing 2026-07-02 and
   6.00 — and all of them measured `font-variant-numeric: normal`. */
.card-task-when, .lic-expiry, .ui-page-head__sub, .lic-type,
.ui-page .prof-readonly dd, .ui-table__sub, .ui-count {
  font-variant-numeric: tabular-nums;
}

/* A measure of 175 characters is not a paragraph, it is a line. `.field-hint`
   measured 175ch, `.ce-stub` 169ch, and `.ce-reason` — which repeats on every
   card in the Licensing queue — 158ch. Only the empty state had a limit. */
.ui-page p, .ce-reason, .ce-stub, .prof-note, .field-hint, .ui-hint,
.ui-card .card-heading { max-width: 72ch; }
/* …but a rule that closes the page is not prose, and 72ch cut it off a third
   of the way across. Two class selectors, because `.ui-page p` is (0,1,1) and
   a bare `.ui-page-foot` is (0,1,0) — it lost, silently. */
.ui-page .ui-page-foot { max-width: none; }

/* Orphans. `text-wrap: balance` was set on the page title and nowhere else,
   so cards dropped "MDT" and "Orientation" alone onto their own lines. */
.ui-card__title, .ui-card .card-title, .ui-empty__title { text-wrap: balance; }
.card-task-title, .card-event-when, .ce-reason,
.ui-card .card-heading, .ui-empty__body { text-wrap: pretty; }

/* THREE FOCUS LANGUAGES, two tab stops apart. Inputs drew the design system's
   ring; the legacy search and chat fields drew an Apple blue that is not in
   this palette; and buttons drew nothing at all, so Chrome's black UA outline
   landed on a copper fill three stops from a blue-ringed input. */
.ui-btn:focus-visible,
.ui-card .card-action:focus-visible,
.ui-file__button:focus-visible {
  outline: 2px solid var(--action-blue);
  outline-offset: 2px;
}
/* The ring's inner spacer was `--surface-1`, the PAPER colour, while every
   control it draws around sits on a white card — a cream halo on white. */
.ui-page input:focus-visible,
.ui-page select:focus-visible,
.ui-page textarea:focus-visible,
.ui-input:focus-visible {
  box-shadow: 0 0 0 2px var(--surface-3), 0 0 0 4px var(--action-blue);
}
/* A row link's ring was being clipped by the table wrapper's own
   `overflow: hidden`. Draw it inside the row instead of around it. */
.ui-table__link:focus-visible { outline-offset: -2px; }


/* ── 15. Phone ────────────────────────────────────────────────────────────
   A table is the wrong shape on a 390px screen, so below 720px each row
   becomes a stacked block and the header is dropped. The markup does not
   change; only its presentation does, which keeps one template per page. */

@media (max-width: 720px) {
  .ui-table thead { display: none; }
  .ui-table, .ui-table tbody, .ui-table tr, .ui-table td { display: block; width: 100%; }
  .ui-table tbody tr {
    padding: var(--space-4);
    border-bottom: 1px solid var(--line-2);
  }
  .ui-table tbody tr:last-child { border-bottom: 0; }
  .ui-table td {
    padding: 0 0 var(--space-2);
    border-bottom: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
  }
  .ui-table td:last-child { padding-bottom: 0; }
  /* ADE-234. A VALUE THAT CANNOT WRAP IS A VALUE THAT GETS CUT.

     The stacked cell is a flex row of two items — the caption from
     `data-label`, and the value as an anonymous item. An anonymous flex item
     is still floored by `min-width: auto`, so its narrowest possible width is
     its MIN-CONTENT width; and §8's `.ui-table__num { white-space: nowrap }`
     makes the min-content width of a date the whole string. At 390px the
     Events list's When cell is 399px of content in 320px of cell:
     `Wednesday, 9 September 2026, 9:00 AM-12:00 PM MDT` pushed the table
     417px wide inside a 356px `.ui-table-scroll`, so the list had to be
     dragged sideways and the time read `1:00 PM-5:00 P` at the edge. A
     broker could not tell when a class ended.

     `nowrap` is right in a COLUMN, where the header sets the width and a
     wrapped date would ragged the column. It is wrong in a BLOCK, which is
     what §15 turns every cell into. So it is turned off here and nowhere
     else — the desktop table is untouched.

     `flex-wrap` is the other half. Without it the value stays beside its
     caption and wraps inside the ~274px left over; with it, a value whose
     max-content will not fit beside the caption takes the next line at the
     cell's full width, which is the tester's own suggestion. Both are needed:
     `flex-wrap` alone cannot help a line that is one unbreakable 353px word,
     and `white-space` alone spends 46px of a 390px screen on a caption. */
  .ui-table td { flex-wrap: wrap; }
  .ui-table td.ui-table__num { white-space: normal; }
  /* The column name comes back as a caption, from the same source as <th>. */
  .ui-table td[data-label]::before {
    content: attr(data-label);
    font-size: var(--fs-caption);
    color: var(--ink-3);
    flex: none;
  }
  /* The name cell is the row's heading on a phone, not a labelled field, so
     it spans the block instead of sitting in a label/value pair. */
  .ui-table td[data-label="—"] { display: block; }
  .ui-table td[data-label="—"]::before { content: none; }

  /* The section crumb is the first thing to go when the bar gets tight — the
     page's own <h1> is directly below it and says the same word. */
  .brand-section { display: none; }

  /* Equal columns stop working once each is under about 120px — the number
     and its label collide. The strip becomes rows, and the dividers turn with
     it so it is still one object. */
  /* ── The bottom bar ────────────────────────────────────────────────
     FIVE destinations, one tap, in the bottom third where a thumb already
     is. Every target is a full fifth of the width — 78px at 390px, which is
     still well past 44 — so nothing here is 38px in a corner.

     ADE-168: THE BAR IS DRAWN OVER THE PAGE AND THE PAGE MUST RESERVE ITS
     HEIGHT. Two numbers make that height and each is written ONCE, below,
     so the reservation cannot drift from the object: the tap target and the
     rule above it. The home-indicator inset is added on top of both, because
     it is space the device takes and not space this bar occupies. */
  :root {
    --tabbar-tap: 56px;   /* one thumb; the whole reason the bar exists */
    --tabbar-rule: 1px;   /* the hairline that separates it from the page */
  }
  /* §6's TREATMENT, report 115. The bar's shape did not move — five tabs,
     56px of thumb, fixed to the bottom — only how it is drawn:

       * CARD WHITE, not the rail fill. The bar is the bottom edge of the
         card the same way the header is its top, and §6 draws both white.
       * 21px OF ICON over a caption, up from 20.
       * THE CAPTION IS 400 AND THE ACTIVE ONE IS 600. Every tab was 600, so
         weight said nothing and the only difference between where you are
         and where you are not was a colour and a 2px mark. §6 states the
         pair: "active in navy and 600 weight".
       * The caption is `--fs-micro`, 12px, where §6 draws 11.5. The floor
         is the rule where the two differ (§4, and report 114 §3.2 settled
         it for the other fourteen chips); `type_probe.py` fails anything
         under 12 whatever class it carries, so 11.5 is not available. */
  .tabbar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 120;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    background: var(--card-bg);
    border-top: var(--tabbar-rule) solid var(--line-2);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .tabbar__tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: var(--tabbar-tap);
    padding: 6px 4px;
    font-size: var(--fs-micro);
    font-weight: 400;
    letter-spacing: .01em;
    color: var(--ink-3);
    text-decoration: none;
  }
  .tabbar__tab .ico { width: 21px; height: 21px; color: currentColor; }
  .tabbar__tab.is-on { color: var(--anchor); font-weight: 600; }
  /* The marker is on the TOP edge, so a thumb resting on the bar never
     covers the one thing that says where you are.
     `top: 0` AND NOT `margin-top: -62px`. The tab is `position: relative`
     (declared outside this block), so the offset can be taken from the tab's
     own top edge instead of guessed backwards from its flex baseline — the
     magic number was one pixel of padding away from being wrong, and this
     phase changed the padding. */
  .tabbar__tab.is-on::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 28px; height: 2px;
    background: var(--anchor);
    border-radius: 0 0 2px 2px;
  }
  .tabbar__tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -3px;
  }
  /* ADE-168. THE RESERVATION IS ON THE COLUMN, NOT ON THE CONTENT BLOCKS.
     `.main-col` is `height: 100vh` and `box-sizing: border-box`, so padding
     here takes the height out of the flex CONTENT box — the scroller
     (`.ui-page`, `flex: 1`) and everything under it end above the bar, on
     every page, whatever a page happens to put at the bottom of itself.

     THE RULE THIS REPLACES WAS DEAD AND HAD BEEN SINCE IT WAS WRITTEN:
     `.ui-page, .ui-page--foot { padding-bottom: calc(72px + …) }` sat nine
     lines above `.ui-page { padding: … var(--space-9) }`, same specificity,
     later in the file, so the shorthand won and the clearance was 56px — one
     pixel LESS than the bar. §14's own lesson, met in §15: a shorthand later
     in the same block beats a longhand earlier in it, silently.

     AND IT WAS ON THE WRONG ELEMENT ANYWAY. Padding inside a scroller is
     space at the end of a scroll, not space above a fixed bar. What actually
     held content off the bar was the page FOOTER happening to be 101px tall
     — so `/chat`, which turns the footer off (`body.is-chat`), had its
     composer under the bar: the textarea and Send both answered
     `elementFromPoint` with a `.tabbar__tab`. Measured on `main`, 390×844. */
  .main-col {
    padding-bottom: calc(var(--tabbar-tap) + var(--tabbar-rule)
                         + env(safe-area-inset-bottom));
  }

  /* 44px, not 34 or 38. One thumb, in a car. */
  .rail-toggle { width: 44px; height: 44px; }
  .bell { width: 44px; height: 44px; }
  .nav-item, .side-link, .product-link { min-height: 44px; }

  .ui-page { padding: var(--space-5) var(--space-4) var(--space-9); }
  /* AND THIS RULE HAS TO COME AFTER THAT SHORTHAND, which is the whole
     lesson of the block above. `.ui-page--foot` carries BOTH classes, so the
     shorthand gave the closing rule 20px of head and 56px of tail — 56px of
     blank paper between the mark and the bar once the column reserves the
     bar's height. §13d sets these at (0,1,0) too and loses on source order.
     Its own comment says "the phone rule further down overrides this to
     clear the bottom bar and is not affected"; that stopped being true the
     day the shorthand was added below it. */
  .ui-page--foot { padding-top: 0; padding-bottom: var(--space-4); }
  .ui-page-head { margin-bottom: var(--space-5); }
  :root { --fs-page-title: 26px; }

  /* §6: A CARD IS 14px ON A PHONE. §7 wants 12 on a desk and 14 here, which
     is the one place in the redesign a value moves with the viewport: the
     same corner reads tighter on a 390px card than on a 1000px one. */
  :root { --radius: 14px; }

  /* ── §6: EVERY ROW STACKS ─────────────────────────────────────────────
     "A row is a column of title, meta, then any control." Two row shapes in
     this product were still three columns at 390px:

     1. `.card-row` — avatar, text, deadline pill — put the pill in an `auto`
        column beside a `minmax(0,1fr)` of text, so a long deadline took its
        width out of the title and the title ellipsised. The pill takes its
        own line under the text now, aligned with it.
     2. A CONTROL THAT MATTERS GOES FULL WIDTH. §6 quotes the mockup's
        Register button: `align-self: stretch`, navy. In this product that is
        a `.ui-btn--primary` inside a stacked table cell — the Register button
        on the events list is exactly it — and it sat right-aligned at the end
        of a wrapped row, which is the hardest place on a 390px screen for a
        thumb to find. It claims the row's full width instead.

     REPORT 113'S HAZARD IS WHY THE FIRST ONE IS `1fr` AND NOT `auto`: a
     stacked row is a flex or grid row whose value is an anonymous item, and
     an anonymous item cannot shrink below its own min-content width. §15
     already turns `nowrap` off in a stacked cell for that reason; the title
     here carried its own `white-space: nowrap`, which is right in a column
     of equal rows and wrong in a block, so it comes off too. */
  .card-row {
    grid-template-columns: 34px minmax(0, 1fr);
    row-gap: 6px;
    align-items: start;
  }
  .card-row__title { white-space: normal; }
  .card-row > .ui-due { grid-column: 2; justify-self: start; }

  /* THE BUTTON IS NOT THE FLEX ITEM — the <form> around it is, and that is
     the whole reason this needs two rules rather than one. `events/index.html`
     wraps Register in a POST form with its CSRF token; a `width: 100%` on the
     button alone would fill a form that is still shrink-to-fit. */
  .ui-table td > form:has(> .ui-btn--primary),
  .ui-table td > .ui-btn--primary { flex: 1 0 100%; }
  .ui-table td .ui-btn--primary {
    display: flex;
    width: 100%;
    justify-content: center;
    padding-block: 12px;
  }
}


/* ── 16. What the concept's card actually is  [LAST ON PURPOSE]
   These corrections sat at §1e and lost silently: §9 and §13 declare the
   same selectors at the same specificity, and a later rule wins. Anything
   that corrects an earlier section belongs at the end of the file, not
   beside the thing it corrects.
 ──────────────────────────────
   Measured off `design_concept/R1 Dashboard.dc.html`, after the first port
   looked wrong and could not be argued into looking right.

   TWO OBJECTS, TWO SCALES, and the first port confused them.
     A STAT TILE is small: 14px radius, 18px 20px, and its label is a quiet
     12px/500 in muted ink. It is a number with a caption.
     A CARD is larger: 16px radius, 18px 22px 22px, 14px between children,
     and its title is a real 17px/600 HEADING in full ink with an optional
     mono count chip beside it and a link pinned to the right of the row.
   I gave the card the tile's label — 12px, muted, no weight — and the page
   went flat: nothing on it was a heading, so nothing on it was a landmark.
   The concept has no filled band either; the head is a plain flex row. */

.ui-card, .ui-card--flush {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 18px 22px 22px;
  overflow: hidden;
}
.ui-split .ui-card--flush > *:not(.ui-card__band) { margin-inline: 0; }
.ui-split .ui-card--flush > *:last-child { margin-bottom: 0; }

/* The band stops being a band and becomes the head row: heading, chip, link. */
.ui-card__band {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.ui-card .card-title, .ui-card__title {
  font-family: var(--font-sans);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--ink-1);
  margin: 0;
}
/* The way out of the card, pinned right on the head row. */
.ui-card__more {
  margin-left: auto;
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}
.ui-card__more:hover { color: var(--accent-hi); }

/* The count chip. Mono, because it is a quantity and mono is what this design
   gives quantities; amber only when the number is one somebody must act on. */
.ui-card__band .ui-count {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--chip-bg);
  color: var(--ink-3);
  border: 0;
  min-width: 0;
  height: auto;
  line-height: 1.5;
}
.ui-card__band .ui-count--warn {
  background: oklch(0.93 0.04 78);
  color: oklch(0.42 0.07 62);
}

/* 16b. A LIST CARD holds its rows edge to edge. The head keeps the card's
   padding, the rows take the full width and are divided by hairlines, so four
   tasks read as one list rather than four paragraphs with air between them. */
.ui-card:has(.card-rows) { padding: 18px 0 0; }
.ui-card:has(.card-rows) .ui-card__band { margin: 0 22px 14px; }
.card-rows { list-style: none; margin: 0; padding: 0; }
.card-row {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 14px 22px;
  border-top: 1px solid var(--line-2);
}
.card-row:hover { background: var(--page-bg); }
.card-row__avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--chip-bg);
  color: var(--ink-2);
  font-size: var(--fs-micro);
  font-weight: 600;
  display: grid;
  place-items: center;
}
.card-row__avatar--none { color: var(--ink-4); }
.card-row__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.card-row__title {
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card-row__sub { font-size: var(--fs-meta); color: var(--muted-ink); }
/* ADE-176. The title is an <a> when its task points somewhere. There is no
   global anchor reset in this stylesheet, so the underline has to be taken
   off here; the copper on row hover is `card-task-title`'s, so the two cards
   on one home page answer a pointer the same way. */
a.card-row__title { text-decoration: none; }
.card-row:hover a.card-row__title { color: var(--accent); }

/* 16c. A DEADLINE IS A PILL, and its colour is the deadline's, not the row's.
   Calm by default; amber when it is close; red when it has passed. Three
   states and no more, because a broker reads this at a glance or not at all. */
.ui-due {
  font-size: var(--fs-meta);
  font-weight: 500;
  padding: 5px 11px;
  border-radius: 999px;
  white-space: nowrap;
  background: var(--tint-bg);
  color: var(--ink-3);
}
.ui-due--today, .ui-due--urgent, .ui-due--soon {
  background: oklch(0.93 0.04 78);
  color: oklch(0.42 0.08 60);
}
.ui-due--overdue {
  background: oklch(0.94 0.05 30);
  color: oklch(0.44 0.14 30);
}

/* 16d. THE EVENT LEADS WITH ITS DATE. A tinted row with a date block reads as
   a calendar; the same words in a bare list read as prose. */
.card-events { list-style: none; margin: 0; padding: 0; display: flex;
               flex-direction: column; gap: 10px; }
.card-event {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  gap: 16px;
  align-items: center;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--page-bg);
  border: 0;
}
.card-event__date {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 7px 0;
  border-radius: 10px;
  background: var(--card-bg);
  border: 1px solid var(--line);
}
/* Mono STAYS: a month abbreviation is a date, and dates are what mono is
   reserved for. The caps and the tracking go — the template already writes
   "Sep". `--fs-micro`, because this is a chip and not a sentence. */
.card-event__month {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: var(--muted-ink);
}
.card-event__day { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; }
.card-event__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.card-event-title { font-size: 14.5px; font-weight: 600; color: var(--ink-1); }
.card-event-when { font-size: var(--fs-meta); color: var(--muted-ink); }

/* 16e. THE TINTED CARD. Reserved for the card that exists only because
   something is wrong — CE with a course to check, a licence that has lapsed.
   Tinting the whole card is what lets every other card stay quiet: the page
   has one loud object, and it is loud because it earned it. Keyed off
   `data-card` so no card template has to know which one it is. */
.ui-card[data-card="ce_status"], .ui-card[data-card="license_status"] {
  background: var(--warn-bg);
  border-color: oklch(0.89 0.05 78);
  padding: 22px;
}
.ui-card[data-card="ce_status"] .card-title,
.ui-card[data-card="license_status"] .card-title {
  font-size: var(--fs-label);
  font-weight: 600;
  color: oklch(0.48 0.08 62);
}
.ui-card[data-card="ce_status"] .ui-badge {
  margin-left: auto;
  /* No status dot. The whole card is already the status. */
  
  font-size: var(--fs-label);
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--warn-bg);
  color: oklch(0.46 0.09 55);
}
.ui-card[data-card="ce_status"] .ui-badge::before { display: none; }
.ui-card[data-card="ce_status"] .card-heading,
.ui-card[data-card="license_status"] .card-heading {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: oklch(0.3 0.04 62);
  margin: 0 0 8px;
}
.ui-card[data-card="ce_status"] .lic-name,
.ui-card[data-card="ce_status"] .ce-reason,
.ui-card[data-card="license_status"] .card-detail,
.ui-card[data-card="license_status"] .lic-name {
  color: oklch(0.45 0.04 62);
}
.ui-card[data-card="ce_status"] .card-action,
.ui-card[data-card="license_status"] .card-action {
  align-self: flex-start;
  padding: 10px 16px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 600;
  background: oklch(0.3 0.04 62);
  color: var(--on-anchor);
  border: 0;
}
.ui-card[data-card="ce_status"] .card-action:hover,
.ui-card[data-card="license_status"] .card-action:hover {
  background: oklch(0.24 0.04 62);
  color: var(--on-anchor);
}

/* The stat number is 30/600, matching the display face rather than shouting
   over it. This said "not the serif" when the display face was one. */
.ui-stat {
  border-color: var(--line);
  border-radius: 14px;
  padding: 18px 20px;
}
.ui-stat__value {
  font-family: var(--font-sans);
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.03em;
}
.ui-stat__label { font-size: var(--fs-meta); font-weight: 500; color: var(--muted-ink); }
.ui-stat__sub { font-size: var(--fs-meta); color: var(--muted-ink); }
.ui-stat__sub.is-warn { color: oklch(0.55 0.06 65); }

/* ── 17. The chrome, as the concept builds it  [LAST ON PURPOSE] ──────────
   Three moves, all structural rather than decorative, and all of them are
   what made the port still read as a different design after the palette and
   the type already matched:

   1. THE BRAND MOVES INTO THE RAIL. It used to sit in the header, where it
      spent the widest strip on the screen repeating a fact the reader knew
      before they signed in. In the rail it is an anchor — chip, firm, and one
      line saying what this software is — and it hands the header back.
   2. THE HEADER BECOMES SEARCH. That is the only thing worth the full width
      of the page at every moment: a way to reach anything from anywhere.
   3. THE RAIL COUNTS. "Tasks 5" answers the app's standing question before
      you click, which is the difference between navigation and a menu.

   Every value below is measured off `design_concept/R1 Dashboard.dc.html`,
   not judged by eye. */

/* 17a. The rail's brand block. Sits above the nav, inside the rail's own
   26px top padding, and aligns to the nav rows' 12px text inset. */
.rail-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 8px;
  min-width: 0;
}
.rail-brand__chip {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 9px;
  background: var(--accent);
  color: var(--on-anchor);
  display: grid;
  place-items: center;
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: -0.02em;
  /* THE ONE `uppercase` LEFT IN THE APP, and it is not a label. This is a
     one-glyph monogram in a 32px square — the brokerage's initial — and a
     lower-case initial in an avatar is a different shape, not a quieter
     one. Report 114 took the other 32 off. */
  text-transform: uppercase;
}
/* A brokerage that uploaded a mark gets the mark. It still occupies the same
   32px square, so the nav below never shifts between tenants. */
.rail-brand__chip--img {
  background: var(--card-bg);
  border: 1px solid var(--line);
  object-fit: contain;
  padding: 3px;
}
.rail-brand__text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}
.rail-brand__name {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-brand__sub {
  font-size: var(--fs-label);
  color: var(--muted-ink);
}

/* 17b. The count on a nav row. Right-aligned, which is why the row becomes a
   flex container — but ONLY the rows that carry one, so an ordinary row keeps
   its icon tight against its label.

   §5 AND §7: A COUNT PILL IS THE CHIP, and it was an amber badge. Amber in
   this palette means a DEADLINE — every "Due tomorrow" chip in the product
   wears it — so an open-task count in the same colour said something it did
   not mean. `--ink-3` on `--chip-bg` is 4.87:1 at 12px; the mockup's own
   pair, and it is why §3 records that the mockup avoids copper here. */
.nav-item:has(.nav-item__count) {
  display: flex;
  align-items: center;
  gap: 10px;
}
.nav-item__count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  background: var(--chip-bg);
  color: var(--ink-3);
}
/* On the dark active pill the amber loses its ground, so the badge borrows
   the pill's own foreground instead of fighting it. */
.nav-item.active .nav-item__count {
  background: oklch(1 0 0 / 0.16);
  color: var(--on-anchor);
}

/* 17c. The header. The search field is the left half; identity is the right. */
.topsearch {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
}
/* §7: RAIL FILL, CARD EDGE, 10px. The fill and the header's swapped in §1b;
   the EDGE is `--line-input` and not `--line`, which is this project's third
   run at the same correction. `redesign-2026-09.md` §3 records it: the mockup
   draws every control it has at `#B6AFA3`, 2.18:1 on white, against the 3:1
   WCAG 1.4.11 wants for the boundary of a component — and a search field is
   a box a person types into, so its edge is the only thing that says where it
   begins. `--line-input` is 3.16:1 on the rail fill and 3.30 on the card.
   `contrast.py` could not see this one until §26 taught it to look at the
   wrapper of a border-less input. */
.topsearch__form {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 1;
  /* A FLEX ITEM WITHOUT THIS CANNOT SHRINK BELOW ITS OWN MIN-CONTENT WIDTH,
     which for this box is the icon, the input's minimum and the `/` key.
     `scripts/overflow_probe.py` found it at 1280 on `/chat`, the one page
     that adds two status chips to the header: the field was 260px inside a
     146px `.topsearch`, so 133px of it ran under the chips. `.topsearch`
     has had `min-width: 0` since it was written; the box inside it had not.
     Report 115. */
  min-width: 0;
  max-width: 420px;
  padding: 9px 12px;
  border-radius: 10px;
  background: var(--rail-bg);
  border: 1px solid var(--line-input);
}
/* The magnifier used to be drawn here — a ring and a box-shadow for the
   handle — because the glyph set had no search icon. It has one now, so the
   field is the same stroked 24-box hand as the bell beside it. */
.topsearch__ico {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--muted-ink);
  stroke-width: 1.9;
}
.topsearch__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink-1);
}
.topsearch__input::placeholder { color: var(--muted-ink); }
.topsearch__input:focus { outline: none; }
.topsearch__form:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.topsearch__key {
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: var(--muted-ink);
  /* §5 draws it as a KEY: a boxed glyph on the card fill, which is what
     tells a reader it is something to press rather than a stray slash. */
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--card-bg);
}

/* 17d. The bell is a square control, not a bare glyph, and the count rides
   its corner. */
.main-col > header .bell {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--card-bg);
  display: grid;
  place-items: center;
  color: var(--ink-2);
}
.main-col > header .bell:hover { border-color: var(--line-3); }
.main-col > header .bell .ico { width: 16px; height: 16px; }
.main-col > header .bell-count {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 999px;
  /* THE FILL MOVED BECAUSE WHITE COULD NOT SIT ON IT. `oklch(0.62 0.16 35)`
     is 3.91:1 under white at 12px — the count on this badge is the smallest
     text in the chrome and it was below AA. `scripts/contrast.py` found it.
     `--copper-cta` is the mockup's own answer — its bell badge is `#A85A22`
     with white on it — and it measures 5.06. Report 114. */
  background: var(--copper-cta);
  color: var(--on-anchor);
  font-size: var(--fs-micro);
  font-weight: 600;
  display: grid;
  place-items: center;
  border: 0;
}

/* 17e. Identity is a pill: initials first, then two quiet lines. The avatar
   leads because it is the fastest thing to recognise at a glance, and the
   template's source order puts it second — `order` fixes that without a
   markup change that eight other pages would have to follow. */
.header-right { gap: 14px; }
.main-col > header .user-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 10px 5px 6px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card-bg);
  box-shadow: none;
}
.main-col > header .user-chip:hover { border-color: var(--line-3); }
.main-col > header .user-chip .avatar {
  order: -1;
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: 50%;
  background: var(--anchor);
  color: var(--on-anchor);
  font-size: var(--fs-micro);
  font-weight: 600;
  display: grid;
  place-items: center;
}
.main-col > header .user-text { line-height: 1.2; text-align: left; }
.main-col > header .user-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-1);
}
.main-col > header .user-role {
  font-size: var(--fs-label);
  font-weight: 400;
  color: var(--muted-ink);
}
.main-col > header .user-menu-caret { color: var(--muted-ink); font-size: var(--fs-micro); }

/* 17f. Phone. The concept has no media queries at all, so this is the adapt
   half of "copy it, then make it work on a phone". The search field keeps the
   header — it is the reason the header exists now — and everything that
   cannot fit beside it stands down: the two-line identity becomes the avatar
   alone, and the rail's own brand block is enough that the header carries no
   firm name at any width. */
@media (max-width: 900px) {
  /* app.css already clears the fixed rail toggle with 62px of header padding;
     the field starts after it, so it needs no inset of its own. */
  .topsearch__key { display: none; }
  .main-col > header .user-text { display: none; }
  .main-col > header .user-chip { padding: 4px; gap: 0; }
  /* Nothing left to drop down beside — the avatar IS the button. */
  .main-col > header .user-menu-caret { display: none; }
}

/* ── 18. The rail's own furniture, and the page's last line ───────────────
   The concept's rail is TEXT. No icons: eight labels in one column, and the
   only marks on it are the ones that carry information — a count, a dot on
   where you are, a chip on the product you are in. Icons beside every row
   made eight rows read as eight buttons, which is the busiest the quietest
   part of the screen could possibly be. */

/* THE GREEN DOT ON THE ACTIVE ROW IS GONE, and it is the fourth green
   surface report 114 §3.5 was about — it survived that pass because its hue
   is 145 and the lint that found the other three looks for 15x. §5 defines an
   active row in one sentence: solid navy, white text, a 9px radius and a
   count pill on the right. A dot beside that is a second answer to a question
   the pill already answers, in the one colour this palette reserves for
   data. The span is out of `_sidebar.html` as well. */

/* A product row is a label and a blurb. The one you are in is tinted and says
   so on the right, instead of spending a second line on it. */
.product-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 9px;
  color: var(--ink-3);
  background: none;
  border: 0;
  box-shadow: none;
}
.product-link:hover { background: var(--tint-bg); }
.product-link .prod-name {
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-size: 14px;
  font-weight: 500;
  min-width: 0;
}
.product-link .prod-sub {
  font-size: var(--fs-label);
  font-weight: 400;
  color: var(--muted-ink);
}
/* THE GREEN SURFACES GO WITH THE GREEN ACCENT. §1 said green was "links and
   the brand chip, never a surface" and then §18 and §22 made it one anyway.
   With the accent copper there is no green left in the palette except the
   ok/warn/alert status pairs, which are data — so the "you are here" marks
   become the count chip every other quiet marker in the redesign uses.
   `--accent` on `--chip-bg` is 5.15:1. Report 114. */
.product-link.current {
  background: var(--chip-bg);
  color: var(--accent);
}
/* A marker beside the product you are already in. A chip, not a sentence. */
.prod-here {
  margin-left: auto;
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--accent);
}

/* The page's last line: two mono marks either side of a rule. */
.ui-page-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-top: 8px;
  border-top: 1px solid var(--line-2);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--muted-ink);
}

/* ── 19. The task block ──────────────────────────────────────────────────
   The concept's own-tasks card is not a list with rules — it is one tinted
   block per task: what to do, and when it is due under it. That is the same
   object as an event row minus the date block, and reading them as a pair is
   the point: both answer "what is coming at me", so both should look alike.
   Colour on the due line only, and only when it means something. */
.card-tasks { list-style: none; margin: 0; padding: 0; display: flex;
              flex-direction: column; gap: 10px; }
.card-task {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  border-radius: 12px;
  background: var(--page-bg);
  border: 0;
  border-left: 0;
  margin: 0;
}
.card-task-title {
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink-1);
  text-decoration: none;
}
a.card-task-title:hover { color: var(--accent); }
.card-task-when { font-size: var(--fs-meta); color: var(--muted-ink); }
.card-task-when.task-soon, .card-task-when.task-today,
.card-task-when.task-urgent { color: oklch(0.55 0.06 65); }
.card-task-when.task-overdue { color: oklch(0.5 0.15 30); font-weight: 500; }
/* §13 gave the block a 3px deadline stripe on its left edge. Inside a tinted
   block it reads as a torn edge rather than a signal, and the due line right
   under the title already carries the same information in words — so the
   stripe goes and the words stay. */
.card-task:has(.task-soon), .card-task:has(.task-urgent),
.card-task:has(.task-today), .card-task:has(.task-overdue) {
  padding-left: 16px;
  border-left: 0;
}
/* The due line is text here, not a pill: it sits under its own title inside a
   block, where a second filled shape would be one shape too many. */
.card-task .card-task-when.task-today,
.card-task .card-task-when.task-urgent,
.card-task .card-task-when.task-overdue,
.card-task .card-task-when.task-soon {
  background: none;
  padding: 0;
  border-radius: 0;
}
.card-task .card-task-when::before { display: none; }

/* The crumb used to trail the firm's name, so its leading "/" was a separator.
   The firm's name is in the rail now and the crumb stands alone, where a bare
   slash in front of it reads as a broken path. It becomes a quiet label to
   the right of the search field. */
.main-col > header .brand-section::before { content: none; }
.main-col > header .brand-section {
  font-size: 13px;
  font-weight: 500;
  color: var(--muted-ink);
  white-space: nowrap;
}

/* THE RAIL IS ONE SCROLLING COLUMN, as the concept builds it. app.css divided
   it into fixed regions that each scrolled on their own — sized for the chat
   page, where "Recent chats" and the knowledge-base picker also live. With
   the brand block added on top, those regions ran out of room and painted
   over each other: on /chat the picker sat on top of the product rail.
   One column, one scrollbar, sections in document order. */
.sidebar { overflow-y: auto; overflow-x: hidden; }
.sidebar-mid { flex: 0 0 auto; display: flex; flex-direction: column; gap: 18px; }
.links-section, .history-section {
  flex: 0 0 auto;
  min-height: 0;
  overflow: visible;
}
/* Pinned to the bottom while the rail is short, and pushed along by the
   content once it is not. */
.sidebar-foot { margin-top: auto; }

/* Chat ends in its composer, not in a rule and two marks. */
body.is-chat .ui-page--foot { display: none; }

.powered {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 12px 4px;
  border-top: 1px solid var(--line-2);
  font-size: var(--fs-label);
  color: var(--muted-ink);
}
.powered__name { font-weight: 600; color: var(--ink-2); }

/* ── 20. THE DESIGN ON EVERY PAGE ─────────────────────────────────────────
   §16–§19 built the home page and the chrome around it. The other ten screens
   inherited the palette and the type but kept their own geometry, so the
   product still changed shape when you left home. Everything below is the
   same measured set of values applied to the components those screens are
   actually made of: page heads, tables, forms, buttons, and two list layouts
   that predate the component layer.

   Nothing here is new design. It is the concept's card, the concept's row,
   and the concept's control, on the pages that did not have them yet. */

/* 20a. THE PAGE HEAD PUTS ITS ACTIONS BESIDE THE TITLE.
   §2 gave the back link, the h1 and the subtitle `flex: 0 0 100%` so that
   pages with different markup would each stack correctly — which also forced
   the actions onto a fourth line, left-aligned under everything. On Events
   the "New event" button sat below the count; on Notifications "Mark all
   read" did the same. A two-column grid takes both shapes: everything that
   is not an action claims column one, and the actions span the whole block in
   column two, centred against it. */
.ui-page-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 20px;
  row-gap: 0;
  align-items: start;
}
/* Some heads wrap their actions in `__actions`; four put a single button
   straight in. Both are treated as actions, so neither ends up as a
   full-width bar under the subtitle. */
.ui-page-head > *:not(.ui-page-head__actions):not(.ui-btn) { grid-column: 1; }
.ui-page-head > .ui-btn,
.ui-page-head__actions {
  grid-column: 2;
  grid-row: 1 / span 4;
  align-self: center;
  justify-self: end;
}
.ui-page-head h1 { font-weight: 600; }
.ui-page-head__sub { font-size: 13.5px; color: var(--muted-ink); }
.ui-back { font-size: 13px; color: var(--muted-ink); text-decoration: none; }
.ui-back:hover { color: var(--accent); }

/* 20b. THE SITE-ADMIN BANNER IS A NOTICE, NOT A STRIPE.
   Full-bleed with a border top and bottom, it read as a second header and
   pushed every page 40px down the screen. As the concept's tinted notice —
   inside the page's own margins, with a dot to start the sentence — it says
   the same thing and stops being chrome. */
.ui-page--bar { flex: none; overflow: visible; padding-top: 24px; padding-bottom: 0; }
.admin-bar {
  margin: 0;
  padding: 14px 18px;
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--warn-bg);
  border: 1px solid oklch(0.9 0.04 78);
  border-radius: 14px;
  font-size: 13.5px;
  line-height: 1.5;
  color: oklch(0.4 0.05 62);
}
.admin-bar::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: 7px;
  border-radius: 50%;
  background: oklch(0.62 0.13 55);
}

/* 20c. CONTROLS. The concept's field: a 10px radius, the card's own paper,
   the hairline everything else uses, and a green focus ring rather than the
   blue one left over from the first palette. */
.ui-input,
.ui-page input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]),
.ui-page select,
.ui-page textarea,
.auth-form input {
  min-height: 40px;
  padding: 0 14px;
  font-size: 14px;
  /* background-COLOR, not the shorthand: §6 draws the select's chevron with a
     background-image, and `background:` here would erase it — which it did,
     leaving every select on the admin pages with no affordance at all. */
  background-color: var(--card-bg);
  border: 1px solid var(--line-input);
  border-radius: 10px;
}
.ui-page textarea, .auth-form textarea { padding: 10px 14px; }
.ui-page input:focus-visible,
.ui-page select:focus-visible,
.ui-page textarea:focus-visible,
.ui-input:focus-visible,
.auth-form input:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Field labels are the stat tile's label: 12px, 500, muted. They were 10px
   uppercase with 0.14em of tracking on six pages, which shouted "EMAIL" at a
   person who was looking at an empty box anyway. */
.ui-page .prof-grid > label,
.ui-page .lic-grid > label,
.ui-page .prof-readonly dt,
.ui-page .person-label,
.ui-page .ev-desc-label,
.ui-page .course-search label,
.ui-page .auth-label,
.ui-field > label,
.auth-label {
  font-family: var(--font-sans);
  font-size: var(--fs-meta);
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--muted-ink);
}

/* 20d. BUTTONS. Ink for the thing you came to do, paper for everything else.
   The copper primary is retired: it was chosen against the navy shell and
   reads as an alert on paper, where amber already means a deadline. */
.ui-btn, .auth-btn, .card-btn {
  height: auto;
  padding: 11px 18px;
  font-size: 13.5px;
  font-weight: 600;
  border-radius: 10px;
}
.ui-btn--primary, .auth-btn {
  background: var(--anchor);
  border-color: var(--anchor);
  color: var(--on-anchor);
}
.ui-btn--primary:hover:not(:disabled), .auth-btn:hover {
  background: var(--anchor-press);
  border-color: var(--anchor-press);
  color: var(--on-anchor);
}
.ui-btn--secondary, .card-btn {
  background: var(--card-bg);
  color: var(--ink-1);
  border: 1px solid var(--line-input);
}
.ui-btn--secondary:hover:not(:disabled), .card-btn:hover {
  background: var(--card-bg);
  border-color: var(--ink-4);
}
.ui-btn--sm { padding: 7px 12px; font-size: var(--fs-label); }

/* 20e. TABLES take the card's geometry and the row's hairline, so a table and
   a list card are the same object with different contents. */
.ui-table-wrap {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 16px;
}
.ui-table th {
  padding: 14px 22px;
  background: var(--card-bg);
  color: var(--muted-ink);
  border-bottom: 1px solid var(--line);
}
.ui-table td {
  padding: 14px 22px;
  border-bottom: 1px solid var(--line-2);
  font-size: 14px;
  color: var(--ink-2);
}
.ui-table tbody tr:hover { background: var(--page-bg); }
.ui-table__link { font-size: 14.5px; color: var(--ink-1); }
.ui-table tbody tr:hover .ui-table__link { color: var(--accent); }
.ui-table__sub { font-size: var(--fs-meta); color: var(--muted-ink); margin-top: 3px; }

/* 20f. EMPTY STATES sit inside a card like everything else, rather than
   floating on bare paper. */
.ui-empty {
  background: var(--card-bg);
  border: 1px dashed var(--line-3);
  border-radius: 16px;
  padding: 40px 22px;
  text-align: center;
}
.ui-empty__title { font-size: 17px; font-weight: 600; margin: 0 0 6px; }
.ui-empty__body { font-size: 13.5px; color: var(--muted-ink); margin: 0 auto; max-width: 46ch; }
.ui-empty__action { margin-top: 18px; }

/* 20g. THE LOGOS COME BACK, each where it belongs.
   R1's is a wide three-line lockup and Adealia's is a tall house mark, so
   neither survives being forced into the other's slot — which is why the port
   dropped both. Each now appears where its shape works and where it means
   something: the customer's mark at the top of their workspace, the vendor's
   on the line that credits the vendor. */
.rail-brand--logo {
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  padding: 0 8px;
}
.rail-brand__logo {
  height: 38px;
  width: auto;
  max-width: 100%;
  display: block;
}
/* On the rail's own paper the mark needs no plate behind it. */
.rail-brand--logo .rail-brand__sub { padding-left: 1px; }

.powered__name { display: inline-flex; align-items: center; gap: 6px; }
.powered__mark { height: 15px; width: auto; display: block; }

/* 20h. THE SIGN-IN CARD. The mark was 192px tall — the largest object in the
   product, on the one page that has nothing to say yet. At 62px it is a mark;
   the card around it takes the same geometry as every other card. */
.auth-body { background: var(--page-bg); }
.auth-card {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: none;
  padding: 34px 32px 24px;
}
.auth-logo { max-width: 62px; margin: 0 auto 20px; }
.auth-title {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink-1);
}
.auth-sub { font-size: 13.5px; color: var(--muted-ink); }
.auth-btn { width: 100%; margin-top: 18px; }
/* ADE-202. The second answer on the join screen. Paper, not ink: joining is
   the thing the person came to do and declining is the way out, so the two
   must not read as a pair of equal choices — but it is a real button with a
   real border and real contrast, not a link pretending to be one. Written
   with both classes so it beats `.ui-btn--primary, .auth-btn` above and
   `#send, .auth-btn` in app.css, neither of which it should inherit a
   background from. */
.auth-btn.auth-btn--quiet {
  margin-top: 10px;
  background: var(--card-bg);
  color: var(--ink-1);
  border: 1px solid var(--line-input);
}
.auth-btn.auth-btn--quiet:hover { background: var(--surface-2); }
.auth-hint { font-size: var(--fs-meta); color: var(--muted-ink); }
.auth-foot {
  margin-top: 22px;
  padding-top: 14px;
  border-top: 1px solid var(--line-2);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-size: var(--fs-meta);
  color: var(--muted-ink);
}
.auth-foot img { height: 15px; }

/* 20i. THE RESOURCE LIST IS ONE CARD, not twenty.
   Twenty separate boxes with 8px between them makes twenty objects to scan;
   the eye counts boxes before it reads names. As one card with hairlines it
   is a list, which is what a person looking for a form is reading. The rows
   still open — the disclosure is unchanged, only its container is. */
.library--page {
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding: 34px clamp(16px, 4vw, 40px) 56px;
  overflow-y: auto;
}
.library--page .ui-page-head { margin-bottom: 24px; }
.lib-head { max-width: none; margin: 0 0 18px; padding: 0; }
.lib-list { max-width: none; margin: 0; padding: 0; }
.lib-folder { margin-bottom: 26px; }
.lib-folder-name {
  font-family: var(--font-sans);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--ink-1);
  border: 0;
  padding: 0 0 12px;
  gap: 12px;
}
.lib-folder-name .lib-count {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--chip-bg);
  color: var(--ink-3);
}
.lib-cards {
  gap: 0;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow: hidden;
}
.lib-item {
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.lib-item + .lib-item { border-top: 1px solid var(--line-2); }
.lib-item-head { padding: 14px 22px; gap: 12px; }
.lib-item-head:hover { background: var(--page-bg); }
.lib-item-name { font-size: 14.5px; }
.lib-item-body { padding: 0 22px 18px; }
.lib-item.open { background: var(--rail-bg); }

/* The upload row is a control strip above the list, on the same paper. */
.lib-upload {
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 14px 18px;
  margin-bottom: 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 14px;
}
.lib-upload-hint { font-size: var(--fs-meta); color: var(--muted-ink); }

/* The search field on this page is the concept's field at page width. */
.lib-head input[type="search"] {
  width: 100%;
  min-height: 44px;
  padding: 0 16px;
  font-size: 14px;
  background: var(--card-bg);
  border: 1px solid var(--line-input);
  border-radius: 12px;
}
.lib-head input[type="search"]:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.lib-filters { gap: 8px; }
.filter {
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  background: var(--card-bg);
  border: 1px solid var(--line-input);
  color: var(--ink-2);
}
.filter:hover { border-color: var(--ink-4); }
.filter.active {
  background: var(--anchor);
  border-color: var(--anchor);
  color: var(--on-anchor);
}
/* `currentColor`, and no `opacity`. `.lib-count { color: var(--ink-4) }` in
   `app.css` is (0,1,0) and this selector never set a colour, so the count
   kept the quiet grey when its pill turned navy: measured 2.77:1 at 12px on
   the ACTIVE filter, before the 0.75 opacity made it worse. Following the
   pill means it is white on navy when active and `--ink-2` on white when it
   is not, and the mono face is what marks it as a count. Report 114. */
.filter .lib-count { font-family: var(--font-mono); font-size: var(--fs-micro); color: currentColor; }

/* 20j. The explanatory blocks, on paper. */
.ce-stub, .ce-hint {
  background: var(--info-bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px 16px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--info-fg);
}
.field-note, .lib-note-inline { font-size: var(--fs-meta); color: var(--muted-ink); }

/* 20k. THE LEGACY PANEL BECOMES A CARD.
   Admin, the invite page, the reset-link page and the calendar page were
   written before the component layer and use `.panel`, `.grid` and bare
   `h3`/`h4`. Rather than rewrite four templates' internals, the old names get
   the new geometry: a panel IS a card, a grid IS a table, and a panel's h3 is
   a card title. The markup stays; only what it looks like moves. */
.panel {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 18px 22px 22px;
  margin-bottom: 20px;
  box-shadow: none;
}
.panel > h3 {
  font-family: var(--font-sans);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-1);
  margin: 0 0 14px;
}
.panel > h4 {
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--muted-ink);
  margin: 22px 0 10px;
}
.panel .muted, .muted { color: var(--muted-ink); }
.panel .mono, .mono { font-family: var(--font-mono); font-size: var(--fs-meta); }
.field-hint { font-size: var(--fs-meta); color: var(--muted-ink); margin: 6px 0 0; }

table.grid { width: 100%; border-collapse: collapse; font-size: 14px; }
table.grid th {
  text-align: left;
  padding: 10px 0;
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--muted-ink);
  border-bottom: 1px solid var(--line);
}
table.grid td {
  padding: 12px 0;
  border-bottom: 1px solid var(--line-2);
  color: var(--ink-2);
}
table.grid tbody tr:last-child td { border-bottom: 0; }

/* The rows of controls under each section. */
.inline-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 12px;
}
.inline-row input[type="text"], .inline-row input[type="email"] { flex: 1; min-width: 180px; }
.inline-check {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: var(--muted-ink);
  white-space: nowrap;
}
.plain-list { list-style: none; margin: 0; padding: 0; display: flex;
              flex-direction: column; gap: 8px; font-size: 14px; }
.plain-list li { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* A pill on this page is a fact about a row, not a status. */
.pill {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--chip-bg);
  color: var(--ink-3);
}
.pill--warn { background: oklch(0.93 0.04 78); color: oklch(0.42 0.07 62); }

.copy-field {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  margin-top: 12px;
}
.panel--ok { border-color: oklch(0.85 0.06 155); background: oklch(0.97 0.02 155); }
.auth-err {
  background: oklch(0.96 0.03 30);
  border: 1px solid oklch(0.88 0.06 30);
  border-radius: 12px;
  padding: 12px 16px;
  font-size: 13.5px;
  color: oklch(0.42 0.13 30);
  margin-bottom: 18px;
}

/* Native controls take the accent, so a checked box is the design's green
   rather than the operating system's blue. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* The select's chevron, in the muted ink rather than the first palette's
   slate — it is redrawn here because it is baked into a data URI. */
.ui-page select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%23716f76' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 38px;
}

/* An unread notification used to carry a 3px bar on the row's left edge,
   which hung outside the card's rounded corner and squared it off. A dot
   before the title says the same thing inside the row. */
.note-unread { border-left: 0; }
.note-unread td:first-child { position: relative; padding-left: 38px; }
.note-unread td:first-child::before {
  content: "";
  position: absolute;
  left: 22px;
  top: 21px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

/* ── 21. Phone, after §20 ─────────────────────────────────────────────────
   §20 re-declares `.ui-table td` at the end of the file, which beats §15's
   media query — a later rule at equal specificity wins regardless of the
   query around the earlier one. On a phone that put a hairline under every
   FIELD of a stacked row, so one person's four facts looked like four
   people. The row divider is the only line that belongs there, and it is a
   full-strength one because it separates records rather than fields. */
@media (max-width: 720px) {
  .ui-table td { border-bottom: 0; padding: 0 0 8px; }
  .ui-table tbody tr {
    padding: 16px 18px;
    border-bottom: 1px solid var(--line);
  }
  .ui-table th, .ui-table td { font-size: 13.5px; }
  /* The head stacks: a title, then its actions, both left-aligned. */
  .ui-page-head { grid-template-columns: minmax(0, 1fr); }
  .ui-page-head > .ui-btn,
  .ui-page-head__actions {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
    margin-top: 14px;
  }
  .lib-item-head { padding: 14px 16px; }
  .lib-item-body { padding: 0 16px 16px; }
  .ui-table td, .ui-table th { padding-inline: 0; }
  .note-unread td:first-child { padding-left: 14px; }
  .note-unread td:first-child::before { left: 0; top: 8px; }
}

/* ── 22. Chat ─────────────────────────────────────────────────────────────

   22a. YOUR OWN MESSAGE WAS INVISIBLE. app.css painted the user bubble with a
   navy GRADIENT and white text; §1 turned every gradient in the product off
   with `background-image: none` — which left the shorthand's colour at
   transparent, so white text sat on near-white paper and the thing you had
   just typed vanished. It is the ink pill now: the same dark object as the
   active nav row and the primary button, which is the one place this design
   spends solid ink. */
.msg.user .bubble {
  background: var(--anchor);
  color: var(--on-anchor);
  box-shadow: none;
  border: 0;
}
/* ON NAVY, so it is `--copper-lift` and not `--accent`: 5.64:1 on
   `--ink-navy` and 4.59 on `--navy-soft`, the darkest the bubble's
   gradient gets. `--accent` there is unreadable. */
.msg.user .bubble a { color: var(--copper-lift); }
.msg.user .bubble code {
  background: oklch(1 0 0 / 0.14);
  border-color: oklch(1 0 0 / 0.22);
  color: var(--on-anchor);
}
.msg.assistant .bubble {
  background: var(--card-bg);
  border: 1px solid var(--line);
  box-shadow: none;
  color: var(--ink-1);
}
.bubble a { color: var(--accent); }
.bubble.err {
  background: oklch(0.96 0.03 30);
  border: 1px solid oklch(0.88 0.06 30);
  color: oklch(0.42 0.13 30);
}

/* 22b. THE RAIL PUTS CHAT'S OWN THINGS FIRST — on the chat page.
   Recent chats sat under eleven outbound links and a product rail, so
   reaching your own history meant scrolling past three groups that have
   nothing to do with the conversation you are in. Order, not deletion: the
   links still work, they just stop being the first thing in the way. */
body.is-chat .sidebar-mid { display: flex; flex-direction: column; }
body.is-chat .history-section { order: 1; }
body.is-chat .kb-section     { order: 2; }
body.is-chat .links-section  { order: 3; }
/* On every other page the picker and the history do not render, so the links
   are the only section and the order is moot. */
.kb-section, .history-section { min-width: 0; }
.hist-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.new-chat-mini {
  width: 26px;
  height: 26px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid var(--line-input);
  background: var(--card-bg);
  color: var(--ink-2);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.new-chat-mini:hover { border-color: var(--accent); color: var(--accent); }
.conv-list { display: flex; flex-direction: column; gap: 2px; }
.conv-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 9px;
  font-size: 13.5px;
  color: var(--ink-3);
  cursor: pointer;
}
.conv-item:hover { background: var(--tint-bg); color: var(--ink-1); }
.conv-item.active { background: var(--chip-bg); color: var(--accent); }
.conv-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.conv-del {
  border: 0;
  background: none;
  color: var(--ink-4);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
}
.conv-item:hover .conv-del { opacity: 1; }
.conv-del:hover { color: oklch(0.5 0.15 30); }
.conv-empty { font-size: var(--fs-meta); color: var(--muted-ink); margin: 0; padding: 0 12px; }

/* ── 23. The band between a phone and a desk ──────────────────────────────
   The concept is a 1440px picture and the phone layer was built to §15, so
   everything from 901px to about 1280 — a laptop at half screen, a small
   MacBook, an iPad in landscape — fell between them. The rail still took its
   full 264px, the greeting was still set at 46px in a column half that wide,
   and the head's action column pushed the title into two words a line.

   Nothing here is a new layout. It is the desktop one, given room. */
@media (min-width: 901px) and (max-width: 1279px) {
  /* The rail gives back 32px. Every label still fits on one line at 232px —
     "Continuing education" is the longest and it is 158px. */
  .sidebar { width: 232px; padding: 22px 14px 16px; }
  .rail-brand__logo { height: 34px; }

  .ui-page, .library--page, .ui-page--foot, .ui-page--bar {
    padding-inline: clamp(20px, 3vw, 32px);
  }

  /* A 46px display line needs about 520px to hold a name on the same line as
     the greeting. Below that it breaks mid-phrase, which is worse than a
     smaller line. */
  .ui-page-head h1, .ui-greeting { font-size: clamp(30px, 3.4vw, 46px); }

  /* Under 1100 the actions stop competing with the title for width and take
     their own line, still left-aligned with it. */
  @media (max-width: 1099px) {
    .ui-page-head { grid-template-columns: minmax(0, 1fr); }
    .ui-page-head > .ui-btn,
    .ui-page-head__actions {
      grid-column: 1;
      grid-row: auto;
      justify-self: start;
      margin-top: 16px;
    }
  }

  /* Cards and rows lose the widest padding first — 22px of gutter either side
     of a 600px card is a quarter of it. */
  .ui-card, .ui-card--flush { padding: 16px 18px 18px; }
  .ui-card:has(.card-rows) { padding: 16px 0 0; }
  .ui-card:has(.card-rows) .ui-card__band { margin: 0 18px 12px; }
  .card-row, .ui-table th, .ui-table td { padding-inline: 18px; }
  .lib-item-head, .lib-item-body { padding-inline: 18px; }

  /* Three stat tiles at 190px minimum wrap to two rows at this width and
     leave one orphan; 150px keeps a set of three on one line. */
  .ui-stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

  /* THE HEADER RUNS OUT OF ROOM ON CHAT, which is the only page that adds two
     status chips to it. At 940 they landed on top of the search field. The
     field gives up width first, because it is the one thing here that still
     works at half its size; then the document count goes, because "Knowledge
     base ready" already says the thing that matters and the count is on the
     Resources page. */
  /* CHAT ONLY. The cap was unscoped and every other page paid for chat's two
     status chips: at 1100 the field was pinched to 215px with a hand's width
     of empty header beside it, because the product name had already stood
     down to make room that nothing then used. */
  body.is-chat .topsearch__form { max-width: 260px; }
  .main-col > header { gap: 14px; }
  body.is-chat .brand-section { display: none; }
}
@media (min-width: 901px) and (max-width: 1099px) {
  body.is-chat .status { display: none; }
  body.is-chat .topsearch__form { max-width: 200px; }
}

/* §14 caps prose at 72ch through `.ui-page p`, which also caught the
   site-admin banner — the same silent loss the page footer had to be excluded
   from. The box takes the page's full width, like the cards below it; the
   sentence keeps the measure. */
.ui-page .admin-bar { max-width: none; }
.admin-bar__text { max-width: 78ch; }

/* ── 24. Two things §20 and §14 got wrong on the CE page ──────────────────

   24a. TWO BARE BUTTONS LANDED IN THE SAME GRID CELL. §20a treats a `.ui-btn`
   that is a direct child of the head as an action, so it does not fall to
   column one as a full-width bar. With ONE button that is right; with two —
   "Approved-course list" and "Licensing queue" on the CE page — both were
   assigned the same column and row and printed on top of each other, so the
   one underneath was unreadable. The templates now wrap them in
   `__actions`, which is the pattern everywhere else. The rule below stays as
   a fallback for a single stray button and is deliberately not clever: two
   bare buttons are a markup mistake, and this makes them look like one
   rather than silently overlapping. */
.ui-page-head > .ui-btn ~ .ui-btn { grid-row: auto; margin-top: 8px; }

/* 24b. THE NOTICE BOX STOPPED TWO THIRDS OF THE WAY ACROSS, for the same
   reason the site-admin banner did: §14 caps prose at 72ch through
   `.ui-page p`, and `.ce-stub` is both a `<p>` and named in that rule, so the
   limit applied to the BOX rather than the sentence in it. Third time this
   selector has caught something that is not a paragraph. The box spans, the
   sentence keeps the measure. */
.ui-page .ce-stub { max-width: none; }
.ce-stub__text { display: block; max-width: 78ch; }

/* ── 25. The product lockup ───────────────────────────────────────────────
   The mark and the name of the software, in the top-left corner of the
   header. It is deliberately QUIET: 22px of mark and a 13.5px name, against a
   38px logo in the rail and a 46px greeting below. A product name is a fact
   you check once and then stop reading, and the loudest thing on a page
   should be the thing you came for. "Adealia" carries the weight; "Brokerage
   Services" is what it is, at the same size in muted ink. */
.product {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: none;
  padding: 4px 6px;
  margin-left: -6px;
  border-radius: 9px;
  text-decoration: none;
  color: var(--ink-1);
  min-width: 0;
}
.product:hover { background: var(--tint-bg); color: var(--ink-1); }
.product__mark { height: 22px; width: auto; display: block; flex: none; }
.product__name {
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--muted-ink);
  white-space: nowrap;
}
.product__name strong { font-weight: 600; color: var(--ink-1); }

/* A hairline between the product and the search field. The two are different
   kinds of thing — one is who made this, one is what you are doing — and 20px
   of air alone did not say so. */
.product + .topsearch {
  padding-left: 18px;
  border-left: 1px solid var(--line);
}

/* Space is the first thing this gives up. Between 901 and 1200 the words go
   and the mark stays, which still says whose software this is; on a phone the
   rail toggle owns that corner, so the whole lockup goes. */
@media (max-width: 1200px) {
  .product__name { display: none; }
  .product + .topsearch { padding-left: 14px; }
}
@media (max-width: 900px) {
  .product { display: none; }
  .product + .topsearch { padding-left: 0; border-left: 0; }
}

/* ---- 21. Text fields on a phone ---- */
/* iOS zooms the whole page when a focused field measures under 16px, and the
   page does not zoom back. On a broker's telephone that fires on sign-in, on
   every field of the CE form, and on every field of the profile.
   THIS DEFECT HAS BEEN FIXED FIVE TIMES AND KEPT COMING BACK, and the reason
   is worth reading before touching this rule:
     * `app.css` carries five page-by-page 16px rules — `.prof-grid`,
       `.lic-grid`, `.person-field select`, `.ev-desc-label textarea` and
       `.course-search input`. Every one of them is DEAD. §20c above,
       loaded later and far more specific, sets 14px and wins.
     * §6 of this file sets `var(--fs-body)` with a comment saying it is 16px
       so that this cannot happen. `--fs-body` is 15px.
   So the fix is one rule, at the end, on the same selector list §20c uses.
   Any narrower rule loses to §20c, which is how the last five were lost.
   Phone only: 14px is the concept's measured desktop value and is correct
   there, where no browser zooms on focus. */
/* The `:root ` prefix is deliberate and is the point. Chasing this with an
   ordinary selector is what produced five dead rules: whatever you write,
   §20c above is more specific and comes later. `:root` costs one class of
   weight and wins without naming a page.
   TWO selector lists, because there are two weights to beat:
     * §20c is (0,1,1) on `.ui-page select`, so `:root .ui-page …` at (0,2,1)
       clears it;
     * §4 is (0,2,2) on `.ui-page .prof-grid > label > select` — the profile,
       the licence rows AND the CE form, which all use `.prof-grid`. Only the
       second list below clears that one, and it was measured in a browser
       rather than counted in my head, because the first attempt tied and
       lost. */
@media (max-width: 900px) {
  :root .ui-input,
  :root .ui-page input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]),
  :root .ui-page select,
  :root .ui-page textarea,
  :root .auth-form input,
  :root .topsearch__input,
  :root .ui-page .prof-grid > label > input,
  :root .ui-page .prof-grid > label > select,
  :root .ui-page .lic-grid > label > input,
  :root .ui-page .lic-grid > label > select {
    font-size: 16px;
  }
}

/* ---- 22. The guide ---- */
/* One long page of prose in a product that otherwise has almost none, so the
   rules here are about READING rather than about chrome. Two decisions worth
   knowing before changing them:

   The measure is capped at 68ch. Every other page in this app is a table or a
   card grid and wants the width; a paragraph does not, and a 1400px line is
   the difference between a page R1 reads and a page R1 skims.

   The section cards are the same `.ui-card` as everywhere else rather than a
   new shape. A guide that looks like a different product is a guide that
   reads like somebody else's documentation. */

.guide-toc {
  margin: 0 0 var(--space-6);
  padding: var(--space-4) var(--space-5);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
}
.guide-toc ul {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5);
}
.guide-toc a {
  color: var(--ink-2); text-decoration: none;
  font-size: var(--fs-small); font-weight: 600;
}
.guide-toc a:hover { color: var(--accent); text-decoration: underline; }

.guide-section { margin-bottom: var(--space-5); scroll-margin-top: var(--space-8); }
.guide-section__title {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display);
  font-size: var(--fs-h2); line-height: 1.2; color: var(--ink-1);
}

.guide-body { max-width: 68ch; color: var(--ink-2); }
.guide-body > :first-child { margin-top: 0; }
.guide-body > :last-child { margin-bottom: 0; }
.guide-body p { margin: 0 0 var(--space-4); line-height: 1.65; }
.guide-body h2 {
  margin: var(--space-6) 0 var(--space-3);
  font-size: var(--fs-h3); line-height: 1.3; color: var(--ink-1);
}
.guide-body h3 {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--fs-body-lg); color: var(--ink-1);
}
.guide-body ul, .guide-body ol { margin: 0 0 var(--space-4); padding-left: var(--space-5); }
.guide-body li { margin-bottom: var(--space-2); line-height: 1.6; }
.guide-body strong { color: var(--ink-1); font-weight: 600; }
.guide-body em { font-style: italic; }
.guide-body code {
  font-family: var(--font-mono); font-size: 0.9em;
  padding: 1px 5px; border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--ink-1);
}
.guide-body a { color: var(--accent); text-decoration: underline; }
.guide-body hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-6) 0; }
/* A table is the one thing here that may exceed the measure — several of them
   are three columns of comparison, and squeezing those to 68ch is what makes
   a comparison unreadable. It still scrolls inside its own box. */
.guide-body .ui-table-wrap { max-width: none; margin: 0 0 var(--space-4); }

/* ---- 22. The approved-list controls on the CE form ---- */
/* The hint used to be a sentence. Each course in it is now a button that
   fills the form, so it has to LOOK pressable without turning the hint into a
   toolbar — the sentence is still the point, and the button is the offer. */
:root .ce-hint-use {
  display: inline-block;
  margin: 6px 0 0;
  padding: 4px 10px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  text-align: left;
  color: var(--ink-1);
  background: var(--surface-3);
  border: 1px solid var(--line-input);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
:root .ce-hint-use:hover { border-color: var(--action-blue); }
:root .ce-hint-use:focus-visible {
  outline: none;
  border-color: var(--action-blue);
  box-shadow: var(--shadow-focus);
}
:root .ce-hint-options { margin: 6px 0 0; padding: 0; list-style: none; }
:root .ce-hint-options li { margin: 0 0 4px; }
:root .ce-hint-options .ce-hint-use { width: 100%; }

/* Provenance on a submission. Quiet on purpose: a picked course is not
   suspicious, it is a fact the reviewer needs, and styling it as a warning
   would tell Licensing to distrust the most convenient path. */
:root .ce-picked {
  margin: 4px 0 0;
  font-size: var(--fs-meta);
  color: var(--muted-ink);
}

/* ADE-187: this one has been round the loop. A reviewer's own send-back
   coming back is the one row on a queue of forty they already have context
   for, and the page head counts them. Marked rather than reordered — the
   argument is in `ce_repo.review_queue`. */
:root .ce-came-back {
  margin: 4px 0 0;
  font-size: var(--fs-meta);
  font-weight: 500;
  color: oklch(0.45 0.10 55);
}

/* ── 26. The certificate on a CE card ─────────────────────────────────────
   Two independent facts sit on a review card and must not blur together: what
   the state's approved list says, and what the broker's own certificate says.
   The second gets its own block, tinted only when it disagrees — a certificate
   that confirms everything should be quiet, because a reviewer who sees a
   coloured box on every row stops reading them. */
.ce-evidence {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 12px 0 0;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--page-bg);
  border: 1px solid var(--line-2);
  font-size: 13.5px;
}
.ce-evidence--query {
  background: var(--warn-bg);
  border-color: oklch(0.9 0.04 78);
}
.ce-evidence--none { color: var(--muted-ink); background: none; border-style: dashed; }
.ce-evidence__verdict { margin: 0; font-weight: 600; color: var(--ink-1); }
.ce-evidence--query .ce-evidence__verdict { color: oklch(0.4 0.07 62); }
.ce-evidence__file {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 0;
  font-size: 13px;
}
.ce-evidence__file a { color: var(--accent); font-weight: 600; text-decoration: none; }
.ce-evidence__file a:hover { text-decoration: underline; }
.ce-evidence__name {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  color: var(--muted-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The broker's own list gets the same pair of links, quieter — they are
   looking at their own file, not judging somebody else's. */
.ce-card-row .ce-evidence { margin-top: 8px; padding: 8px 12px; }

/* ── 27. Where a broker stands ────────────────────────────────────────────
   One block per licence. The bar is the TOTAL only, and every named minimum
   gets its own row underneath — because a broker at 36 of 36 hours can still
   be short of the annual Core, and a single bar cannot say that. A page that
   showed only the bar would tell them they were finished. */
.ce-standing + .ce-standing {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--line-2);
}
.ce-standing__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
}
.ce-standing__summary { font-size: 14px; color: var(--ink-1); margin: 0 0 10px; }

.ce-bar {
  height: 8px;
  border-radius: 999px;
  background: var(--chip-bg);
  overflow: hidden;
  margin-bottom: 12px;
}
/* §7: copper fill, chip track. `--copper-cta` and not `--accent`, because
   this is a FILL and the mockup draws it `#A85A22`; nothing reads text off
   it, so the ink copper's darker step buys nothing here. */
.ce-bar__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--copper-cta);
}

/* ADE-240/237 — `.ce-obligations` IS THE SAME LIST WITH A SECOND NAME, not a
   second rule. The CE page grew a sibling list for the lines that are not
   hours — a property-management declaration and a NMREC meeting — and they
   are drawn exactly as the hour rows are, because the same state has to read
   the same whichever line it is on.

   THE NAMES DIFFER SO THAT A SELECTOR KEEPS ITS MEANING. `.ce-parts` and
   `.ce-part--short` are addressed by `scripts/walkthrough.py` and by the
   tests, and a second list wearing the same class quietly turned
   "the property-management hours are counted against her" into a count of
   two — green and red both for the wrong reason. The CHILD classes are
   shared on purpose (`.ce-part__label`, `.ce-part__figure`, `.ce-state`):
   nothing addresses those by row type, and duplicating them would be two
   copies of a layout to get out of step. */
.ce-parts, .ce-obligations { list-style: none; margin: 0; padding: 0;
            display: flex;
            flex-direction: column; gap: 2px; }
/* ADE-185: THE ROW THAT CARRIES THE MOST INFORMATION IN THE PRODUCT.
   "NMREC Core / every year / 4.0 hours each year · outstanding for years
   1, 2, 3" is a name, a per-year quantity and a list of years, and at 390px
   it was two children of one un-wrapping `space-between` row.

   WHAT ACTUALLY HAPPENED, measured rather than guessed. The figure was
   `white-space: nowrap`, so it could not shrink and took 312 of the card's
   351px; the LABEL was crushed to 39px and its own text then overflowed its
   box — `scrollWidth` 154 against a `clientWidth` of 39, four lines tall,
   "NMREC Core every year" broken mid-phrase and cut off. Nothing hung
   outside the card, which is why "the page does not scroll sideways" was
   green throughout. That is what a tester read as broken.

   Nothing is dropped. `flex-wrap` lets the figure take its own line the
   moment the pair stops fitting, at any width and in any card, rather than
   at a breakpoint somebody guessed — and `row-gap` keeps that second line
   attached to its label instead of floating between rows. Measured after:
   label 21px tall and not clipped, figure on its own line, 0px of sideways
   scroll. */
.ce-part, .ce-obligation {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: 14px;
  row-gap: 2px;
  padding: 7px 0;
  font-size: 13.5px;
  border-bottom: 1px solid var(--line-2);
}
.ce-part:last-child, .ce-obligation:last-child { border-bottom: 0; }
.ce-part__label { font-weight: 500; color: var(--ink-1); min-width: 0; }
/* "every year" is the single most important word on this block. It is the
   difference between 12 Core hours in one year and 4 in each of three. */
/* "every year" is a phrase, not a figure, so it leaves mono behind with the
   caps. It stays the quietest thing on the row and is still legible. */
.ce-part__period {
  font-size: var(--fs-label);
  font-weight: 500;
  color: oklch(0.5 0.07 65);
  margin-left: 8px;
}
/* NOT `nowrap`. A figure that may not wrap cannot shrink, so in a flex row
   it takes the width it wants and the LABEL pays for it — which is how a
   four-word part name ended up 50px wide and four lines tall on a phone
   while the figure itself ran 50px out past the card's right edge (401
   against an edge at 351 at 390px; 431 on the two-licence broker's worst
   row). The label was CRUSHED, not clipped: `scrollWidth == clientWidth`
   throughout, so nothing overflowed its own box. The escaping figure is the
   part a check can see, and the first version of this comment credited the
   wrong one.

   NO `text-align` EITHER, and that is worth a sentence because it is the
   obvious thing to reach for. This box is `flex: 0 1 auto`, so it is exactly
   as wide as its text and aligning inside it moves nothing. What puts the
   figure hard right on a wide screen is the row's `space-between`, and what
   puts it hard LEFT once it has wrapped is the same rule — `space-between`
   places the first item on a flex line at the start, and a wrapped figure is
   alone on its line. One rule, both states. */
.ce-part__figure { color: var(--muted-ink); min-width: 0; }
.ce-part--met .ce-part__figure,
.ce-obligation--met .ce-part__figure { color: oklch(0.42 0.08 155); }
.ce-part--short .ce-part__figure,
.ce-obligation--short .ce-part__figure { color: oklch(0.45 0.10 55);
                                         font-weight: 500; }
.ce-part--not_counted, .ce-obligation--not_counted { opacity: 0.7; }

/* ADE-189: WHAT THE TALLIES ABOVE COUNT. It sits between the tallies and the
   search box and has to be clearly attached to the first — hence the tight
   top margin and the roomy bottom one — and it is measured rather than
   guessed: without the bottom margin it printed a line clear of the search
   field's own label. Held to a readable measure, because it is the only
   paragraph of prose on a page of numbers. */
.ce-what-we-count { margin: 2px 0 20px; max-width: 74ch; }

/* ADE-190: WHY THIS TOTAL IS THIS NUMBER. Quieter than the summary above it
   and louder than the caveat below: it is an explanation of the figure the
   broker just read, not a warning about it. */
.ce-standing__because {
  margin: -4px 0 10px;
  font-size: var(--fs-meta);
  line-height: 1.5;
  color: var(--ink-3);
}

/* ADE-171: WHAT YOU OWE, IN THE PAGE HEAD.
   One item per licence, wrapping to its own line rather than being clipped —
   this sits beside a title on a desktop and above the fold on a phone, and a
   broker with two licences must read both. */
.ce-owed {
  display: flex;
  flex-wrap: wrap;
  column-gap: 18px;
  row-gap: 4px;
}
.ce-owed__item { display: inline-flex; flex-wrap: wrap; gap: 6px;
                 align-items: baseline; }
/* THE SAME TRAP `.ui-file-staged` RECORDS ABOVE, met again and found by
   mutating this line rather than by reading it: an author `display` beats the
   browser's own `[hidden] { display: none }`, so putting `hidden` on this
   paragraph changed NOTHING on the rendered page — measured, 0 red across the
   whole suite and 0 red in the browser, because the figure was still there to
   read. Nothing sets `hidden` on it today; the template branches on
   `standings`. The rule is here so that stays a choice. */
.ce-owed[hidden], .ce-owed__item[hidden] { display: none; }
.ce-owed__figure { color: var(--ink-1); font-weight: 600; }
.ce-owed__where { color: var(--ink-3); }

/* The disclaimer. Quiet, but never absent while the numbers are unconfirmed —
   we may show arithmetic and may not imply somebody is compliant. */
.ce-standing__caveat {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--warn-bg);
  border: 1px solid oklch(0.9 0.04 78);
  font-size: var(--fs-meta);
  line-height: 1.5;
  color: oklch(0.4 0.05 62);
}


/* ── 21. The CE compliance tallies ────────────────────────────────────────
   Three numbers that deliberately do not add up to the row count. Short, met,
   and — separately — the licences whose state rules we have never recorded.

   The third is muted rather than coloured for the same reason its badge is:
   an unmeasured licence is not a failing one, and giving it a warning colour
   would put Licensing to work on a data gap of ours as though it were a
   broker's problem.

   THE PEOPLE PAGE SHARES THE SHAPE (ADE-156, `.roster-tallies`) rather than
   getting a second one that drifts. Its four numbers are the same kind of
   thing: how many people the firm has never been asked about. They are ALL
   muted, because none of them is a broker failing at anything — an unanswered
   membership is a gap in the firm's own records, and the argument two
   paragraphs up is the argument for that too. What marks the one to act on is
   its position and the sentence under the row, not a colour. */
.ce-tallies,
.roster-tallies {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.roster-tallies { margin-bottom: var(--space-2); }
.ce-tally,
.roster-tally {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--muted-bg);
  color: var(--muted-fg);
  font-size: var(--fs-small);
}
.ce-tally strong,
.roster-tally strong {
  display: block;
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 1.1;
}
.roster-tallies__note {
  margin: 0 0 var(--space-5);
  color: var(--ink-3);
  font-size: var(--fs-small);
  max-width: 62ch;
}
.ce-tally--short { background: var(--warn-bg); color: var(--warn-fg); }
.ce-tally--met   { background: var(--ok-bg);   color: var(--ok-fg); }

/* THE TALLIES ARE ALSO THE FILTER (ADE-133). They were already the four
   numbers a reader wants to act on, so making them the control is one fewer
   thing on the page than a separate row of chips would have been.

   They keep their own colours when selected and gain a ring instead. Turning
   the active one a single accent colour would have cost the reader the thing
   the colour was carrying — that the short number is the warning one — at
   exactly the moment they are looking at it. */
a.ce-tally { text-decoration: none; }
a.ce-tally:hover { filter: brightness(0.97); }
a.ce-tally.is-active {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* The console's search. A GET form, so its result is a URL. */
.ce-console-search { margin-bottom: var(--space-5); }
.ce-console-search__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
}
/* LAYOUT ONLY. `.ui-page input` in §6 already gives this its border, its
   radius, its colours and — in the phone media query at §21 — the 16px that
   stops iOS zooming the page on focus. Restating any of that here is how a
   component drifts away from the design system it is sitting in. */
.ce-console-search input[type="search"] {
  flex: 1 1 260px;
  width: auto;
  min-width: 0;
}

/* Fifty rows a page, and the control that moves between them. */
.ce-pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin-top: var(--space-4);
}
.ce-pager__where { font-size: var(--fs-small); color: var(--muted-ink); }

/* ── 22. What changed on a CE submission ──────────────────────────────────
   A short list on a card that is already dense, so it is set quieter than the
   fields around it and indented off the card's left edge — it is context for
   the decision, not part of it. */
.ce-edits {
  margin: var(--space-3) 0;
  padding-left: var(--space-4);
  border-left: 2px solid var(--line);
}
.ce-edits .ui-eyebrow { margin-bottom: var(--space-2); }


/* ── 23. A course's whole history ─────────────────────────────────────────
   ADE-134. The queue card's `.ce-edits` above is a footnote beside a decision
   somebody is making right now; this is the page where the record IS the
   subject, so it is set at reading size and gets a rail down the left with a
   dot per event — the shape a person already reads as "in order, top to
   bottom".

   THE DOT CARRIES THE MEANING, not a colour wash. Every event on this page is
   a fact rather than a state, and washing a rejection in alert red would make
   the record look like an alarm every time somebody opened it. The three
   semantic dots are spent on the three things a reader scans for: a decision
   that ended it, a decision that sent it back, and a gap. */
.ce-hist {
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--space-5);
  border-left: 2px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.ce-hist__item { position: relative; }
.ce-hist__item::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-5) - 6px);
  top: 6px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--line-2);
  box-shadow: 0 0 0 3px var(--surface-1);
}
.ce-hist__item--decision::before   { background: var(--ok-dot); }
.ce-hist__item--unrecorded::before { background: var(--warn-dot); }

.ce-hist__what { margin: 0; font-size: 14px; color: var(--ink-1); }
.ce-hist__who  { font-weight: 600; }

/* The words a person wrote, set apart from the sentence the platform wrote.
   On a send-back this is the entire instruction, and it is the one thing on
   the page that no template composed. */
.ce-hist__note {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-3);
  border-left: 2px solid var(--line-2);
  font-size: 14px;
  color: var(--ink-2);
}

/* A hole in the record, and it says so rather than being left blank. */
.ce-hist__gap-note {
  margin: var(--space-2) 0 0;
  font-size: var(--fs-small);
  color: var(--muted-ink);
  max-width: 68ch;
}
.ce-hist__item--unrecorded .ce-hist__what { color: var(--muted-ink); }

.ce-hist__when { margin: var(--space-1) 0 0; }

/* The way to the page above, from the queue card and the broker's own list.
   A link and not a button: it opens a record, it changes nothing. */
.ce-hist-link { margin: var(--space-2) 0 0; font-size: var(--fs-small); }
.ce-hist-link a { color: var(--accent); font-weight: 600; text-decoration: none; }
.ce-hist-link a:hover { text-decoration: underline; }

/* ── ADE-161: signing a state's CE requirements off ─────────────────
   Reuses `.ce-part` and `.ce-part__label` for the named minimums, because a
   part is the same object on both screens and a second look for it would let
   the sign-off page and the broker's page disagree about what a part IS. What
   is new here is the list of states, the source line, the number fields, and
   the both-numbers provenance line. */

.req-list { list-style: none; margin: 0; padding: 0; }
.req-item {
  padding: 14px 0;
  border-bottom: 1px solid var(--line-2);
}
.req-item:last-child { border-bottom: 0; }
.req-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 12px;
  row-gap: 4px;
}
.req-name { font-weight: 600; font-size: 15px; color: var(--ink-1); }

/* THE THREE ANSWERS, AND THE MUTED ONE IS DELIBERATE. A state we hold no
   rules for is a fact about US, exactly like the console's "not measured"
   column (report 36 §3) — colouring it like a problem would make it look like
   a fact about the brokerage. A state nobody has confirmed IS a fact about
   the brokerage and something they can act on today, so it takes the warning
   colour the console's short badge takes. */
/* The status chip the redesign draws: 13/500 in a pill. §7. */
.req-state {
  font-size: var(--fs-label);
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid transparent;
}
.req-state--met {
  color: oklch(0.42 0.08 155);
  background: oklch(0.96 0.03 155);
  border-color: oklch(0.9 0.05 155);
}
.req-state--short {
  color: oklch(0.45 0.10 55);
  background: oklch(0.965 0.035 75);
  border-color: oklch(0.9 0.05 72);
}
.req-state--none {
  color: var(--muted-ink);
  background: var(--page-bg);
  border-color: var(--line-2);
}

.req-note { margin: 8px 0 0; font-size: 13px; line-height: 1.55;
            color: var(--ink-2); max-width: 74ch; }
.req-count { margin: 4px 0 10px; font-size: var(--fs-meta); color: var(--muted-ink); }
.req-subhead { margin: 18px 0 6px; font-size: 13px; font-weight: 600;
               color: var(--ink-1); }

/* WHERE WE READ IT AND WHEN — half of what is being agreed with, so it sits
   above the numbers rather than under them as a footnote. */
.req-source {
  margin: 0 0 14px;
  font-size: var(--fs-meta);
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 74ch;
}

.req-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.req-field { display: block; flex: 1 1 220px; min-width: 0; }
.req-field__label {
  display: block;
  font-size: var(--fs-meta);
  color: var(--muted-ink);
  margin-bottom: 4px;
}
.req-parts { list-style: none; margin: 0; padding: 0; }
.req-part {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  column-gap: 14px;
  row-gap: 6px;
  padding: 10px 0;
  font-size: 13.5px;
  border-bottom: 1px solid var(--line-2);
}
.req-part:last-child { border-bottom: 0; }
.req-part__label { font-weight: 500; color: var(--ink-1); min-width: 0; }
.req-part__period {
  display: block;
  font-size: var(--fs-label);
  font-weight: 500;
  color: oklch(0.5 0.07 65);
  margin-top: 2px;
}
/* NOT `nowrap` and no fixed width, for the reason `.ce-part__figure` records
   at length: a box that may not shrink takes the width it wants and the label
   pays for it. `flex: 0 1 auto` with a floor is what keeps a number field
   usable on a phone without crushing a four-word part name. */
.req-part__hours { flex: 0 1 130px; min-width: 96px; }

/* BOTH NUMBERS, SIDE BY SIDE. Quieter than the field it belongs to — it is a
   note about where the number came from, not a warning — and it takes the
   full row so the two figures read as one sentence rather than as a column. */
.req-edit {
  flex-basis: 100%;
  margin: 6px 0 0;
  font-size: var(--fs-meta);
  line-height: 1.5;
  color: oklch(0.45 0.05 62);
}
.req-edit strong { font-family: var(--font-mono); font-weight: 600; }

/* ADE-162: A LICENCE WHOSE STATE NOBODY HAS SIGNED OFF. It is the console's
   muted "not measured" treatment rather than the warning one, for report 36
   §3's reason — an unmeasured licence is a fact about the firm's paperwork,
   not about the broker reading the page. */
.ce-standing--dormant .ce-standing__summary { color: var(--ink-2); }
.ce-standing__dormant-link { margin: 8px 0 0; font-size: var(--fs-meta); }

/* ── The outbox — ADE-210 ────────────────────────────────────────────
   ONE NEW RULE THAT MATTERS AND IT IS `pre-wrap`. A composed email has blank
   lines between its paragraphs and no line longer than a sentence; rendered
   with the page's normal whitespace handling it arrives as one grey block and
   the page cannot be used for the one thing it is for, which is reading the
   words. `pre` alone would be worse — it does not wrap, so a phone gets a
   horizontal scrollbar per message.

   The face is the page's, not the monospace of a code block: this is a letter
   somebody receives, and setting it in a terminal font would make a person
   judge the wording as output rather than as prose. */
.outbox-msg__body {
  margin: 10px 0 0;
  padding: 12px 14px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--ink-1);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: 8px;
}

/* THE STATE COUNTS WRAP. `.ui-inline` is `inline-flex` with no wrap, and four
   badges do not fit across a 390px card — the last one was clipped at the
   card's edge, which is the one of the four an owner most needs to see. */
.outbox-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 8px 0 0;
}

/* The envelope lines, quieter than the words they surround. */
.outbox-msg__to { margin: 2px 0 0; }
.outbox-msg__address { font-family: var(--font-mono); }
.outbox-msg__why,
.outbox-msg__reason,
.outbox-msg__sent { margin: 6px 0 0; }
/* ADE-220 took `.outbox-msg__ref` off the page with the line it painted. The
   identity is still on the article as `data-source-ref`, where support and the
   walkthrough can read it; it is our own composed key and is not prose. */

/* ── Signatures — ADE-211 ────────────────────────────────────────────
   THE CONSENT PARAGRAPH IS `pre-wrap` for `.outbox-msg__body`'s reason and
   with the same trade: the string is stored with its blank lines in it, and
   normal whitespace handling would run three paragraphs into one. `pre`
   alone does not wrap, so a phone would get a horizontal scrollbar on the one
   block a person has to read before they sign. The face is the page's — this
   is a sentence somebody agrees to, not program output. */
.sig-consent {
  margin: 10px 0 0;
  padding: 12px 14px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--ink-1);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: 8px;
}

/* THE DOCUMENT ITSELF, ON THE SIGNING PAGE. A fixed height rather than an
   aspect ratio: a PDF viewer inside an <object> gets no intrinsic size, so
   without one it collapses to nothing and the page shows a signing form over
   an invisible document. 60vh leaves the words and the button reachable
   without scrolling past the file on a laptop; `min-height` keeps it usable
   on a phone, where 60vh of a short viewport is not a page of a form. */
.sig-viewer {
  display: block;
  width: 100%;
  height: 60vh;
  min-height: 320px;
  margin: 10px 0 0;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--surface-2);
}

/* THE TICK BOX AND ITS LABEL ARE ONE TAP TARGET. A 13px checkbox on its own
   is under the 44px minimum the phone walk measures, and the label is the
   sentence a person is agreeing to — so the <label> wraps both and gets the
   height. */
.sig-tick {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  min-height: 44px;
  padding: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-1);
  cursor: pointer;
}
.sig-tick input { width: 18px; height: 18px; margin-top: 2px; flex: none; }

/* The record's facts. A two-column grid on a desk and one column on a phone,
   because a 64-character digest beside a label in 390px is a digest that runs
   off the side. */
.sig-facts {
  display: grid;
  grid-template-columns: minmax(9rem, max-content) 1fr;
  gap: 6px var(--space-3);
  margin: 10px 0 0;
  font-size: 13.5px;
}
.sig-facts dt { color: var(--ink-2); }
.sig-facts dd { margin: 0; color: var(--ink-1); overflow-wrap: anywhere; }
@media (max-width: 560px) {
  .sig-facts { grid-template-columns: 1fr; gap: 2px; }
  .sig-facts dd { margin: 0 0 8px; }
}

.sig-row__who { margin: 2px 0 0; }
.sig-row__hash { display: inline-block; margin: 8px 0 0; overflow-wrap: anywhere; }
.sig-seal-check { margin: 8px 0 0; }

/* ── ADE-59. What reaches me ──────────────────────────────────────────
   The four channels and what each can carry today. A list rather than a
   table: the sentences are of different lengths and a column of them reads
   as data, which they are not — they are the page telling the truth about
   what is switched on. */
.prefs-standing { margin: 0; padding: 0 0 0 var(--space-4); }
.prefs-standing li { margin: 0 0 6px; color: var(--ink-2); font-size: 13.5px; }
.prefs-standing strong { color: var(--ink-1); }

/* The grid's checkboxes sit under a heading that names the channel, so the
   cell needs nothing but room to be tapped. 44px is the touch target the
   phone walk measures elsewhere in this file. */
.prefs-table td input[type="checkbox"] { width: 18px; height: 18px; }

/* ADE-59. A reason somebody CHOSE reads differently from one the product
   could not get past. Muted rather than alert: nothing is wrong, and painting
   a person's own decision in the failure colour is what makes an owner go and
   "fix" it. */
.outbox-msg__reason--chosen { font-style: italic; }

/* ADE-58. What a firm may write into its own reminder, and what we fill in.
   A plain list of field names — the whole card is collapsed until somebody
   opens it, so this costs nothing on the page everybody else reads. */
.settings-wording__fields { margin: 6px 0 10px; padding: 0 0 0 var(--space-4); }
.settings-wording__fields li { margin: 0 0 4px; color: var(--ink-2); font-size: 13px; }
.settings-wording__open { cursor: pointer; font-weight: 600; margin: 10px 0 0; }

/* ── ADE-56. THE WIDE GRID, which is now the only thing `.tbl` is ─────────
   Report 110. `.tbl` was the first table in the app and it kept the look it
   was born with — a 1px box around every cell and a filled header — long
   after §8 and §20e gave the product a table made of hairlines. Two pages
   wore it: the approved-course list and its version history, which are OUR
   five columns and are `.ui-table` now, and the spreadsheet preview on
   `courses/confirm.html`, which is somebody else's file and stays here.

   IT DOES NOT STACK ON A PHONE, AND THAT IS THE REASON IT IS STILL A
   SEPARATE CLASS. §15 turns a `.ui-table` into labelled blocks below 720px,
   which needs a `data-label` per cell — and the columns of this one are
   whatever the regulator called them in the file somebody just chose. So it
   keeps the `.tbl-scroll` box and scrolls sideways inside it. The walkthrough
   measures that as `the column check fits a phone`: if this table pushed the
   page instead, the Import button would go off the side and the page could
   not be used at all.

   Geometry only. No cell, no heading and no route moves. */
.tbl-scroll {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow-x: auto;
}
.tbl th, .tbl td { border: 0; padding: 12px 18px; }
.tbl th {
  background: var(--card-bg);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--muted-ink);
  border-bottom: 1px solid var(--line);
}
.tbl td {
  border-bottom: 1px solid var(--line-2);
  color: var(--ink-2);
}
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody tr:hover { background: var(--page-bg); }

/* ADE-56. A COLUMN'S NAME IS A CAPTION, NEVER A DATUM.
   §15 paints the dropped `<th>` back as `td[data-label]::before`, and the
   pseudo-element inherits the CELL's font — so on `/admin/users` the caption
   "Last login" came out in the mono face the date beside it is set in, and
   on the admin calendar so did "When". Mono is opted into by a VALUE in this
   design (§13b); the word naming the column is prose. Declared after §15 so
   it wins on source order at the same specificity, which is that section's
   own lesson. */
@media (max-width: 720px) {
  .ui-table td[data-label]::before {
    font-family: var(--font-sans);
    letter-spacing: normal;
    text-transform: none;
  }
}


/* ── 26. THE APP IS ONE CARD ON THE PAGE GROUND  [report 115] ─────────────
   `planning/design/redesign-2026-09.md` §5, in one sentence: *"The app is one
   rounded card. max-width: 1440px, border-radius: 18px, 1px solid #E4DFD5,
   and a soft shadow."* Until now the rail ran to the left edge of the window
   and the content to the right one, so on a wide monitor the product was a
   2560px band with 1240px of page floating in the middle of it and nothing
   saying where the app stopped.

   THE WRAPPER IS `.app-shell`, ADDED IN `base.html`, AND IT IS DELIBERATELY
   NOT A RULE ON `body`. `auth/_shell.html` overrides `{% block layout %}`
   entirely — a sign-in page is a page, not an app, and it should not arrive
   in a card. A `body` rule could not tell the two apart.

   THE THREE FILLS, and getting them in the right order is what makes the
   card read as one object rather than three:
     * `body` is `--page-bg` — the ground the card floats on;
     * `.app-shell` is `--card-bg` — the card;
     * `.main-col` is `--page-bg` again — the paper INSIDE the card that the
       page's own cards sit on, which is what the mockup draws (its content
       column is `#F7F5F0` while its header and rail are not).
   The rail and the header paint their own; without the third fill the whole
   inside of the card was white and every `.ui-card` on it disappeared.

   MEASURED AT THREE WIDTHS BEFORE IT WAS COMMITTED TO, because a max-width
   is exactly the kind of change that looks right on the machine it was
   written on:
     * 1920 — the card is 1440 with 240px of ground either side. This is the
       width the change is FOR.
     * 1440 — 20px of ground either side, so the card is 1400 and the content
       column loses 40px against what it had. `.ui-page` is already capped at
       1240, so no page gets narrower; the gutter does.
     * 1280 — the card is 1240 and the rail is 232 (§23's band). The content
       column is 1008, which is 24px MORE than the 1240-cap would have given
       it. Nothing was pinched.
   Below 901px there is no card at all: a phone has no ground to spare, and
   the off-canvas rail is `position: fixed` to the window rather than to the
   shell. */

.app-shell {
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 100%;
  min-width: 0;
  background: var(--card-bg);
}

/* The paper inside the card. `body` used to paint this and the shell now sits
   between the two. */
.main-col { background: var(--page-bg); }

@media (min-width: 901px) {
  /* `box-sizing: border-box` is set on `*` in app.css and this depends on it:
     `body` is `height: 100%`, so its CONTENT box is the window less this
     padding, and `.app-shell { height: 100% }` measures against that. No
     `calc`, and nothing to keep in step if the gap changes. */
  body { padding: var(--space-5); }
  .app-shell {
    max-width: 1440px;
    margin: 0 auto;
    border: 1px solid var(--line);
    border-radius: var(--shell-radius);
    /* §5's own shadow: `0 14px 40px rgba(26,37,71,0.07)`, written through the
       token so it follows the navy rather than restating it. */
    box-shadow: 0 14px 40px color-mix(in srgb, var(--ink-navy) 7%, transparent);
    /* The rail's right edge and the header's bottom edge both run into the
       card's corners; without this they cut across the radius. */
    overflow: hidden;
  }
}

/* WHAT THIS SECTION DELIBERATELY DOES **NOT** DO, recorded because I built it
   and then took it out again. The rail's vertical order looked wrong — the
   *Powered by Adealia* line §5 asks for is below the fold at 1440x900 — and
   the first version of §26 rewrote it: `flex: none` on the brand, the nav and
   the foot, `flex: 1 0 auto` on the middle, `overflow-y: auto` on the rail.

   **EVERY ONE OF THOSE RULES WAS ALREADY THERE, forty lines up in §22b**,
   under a comment that says *"THE RAIL IS ONE SCROLLING COLUMN"* — added when
   the brand block went into the rail, for the same reason. The rail was not
   clipping anything: it scrolls, the mark is at the end of that scroll, and
   `margin-top: auto` puts it on the floor whenever the content is short
   enough. R1 has seven quick links and three product rows, so at R1 it never
   is, at any window height this project can open.

   AND THE ONE THING THE REWRITE ADDED WAS A DEFECT. Giving `.nav` and the
   outbound links their own inner scrollers — which is what "fix the order"
   turns into — cut the last two nav rows off inside a scroller nobody would
   think to drag, and painted the links over the *Powered by* line. Measured
   at 1280x800, and it is exactly the failure §22b's comment describes.

   `check_the_rail_is_grouped` in `scripts/walkthrough.py` now asks the
   browser the question this was groping at — is anything in the rail beyond
   reach — so the next person to think the rail's order looks wrong has an
   instrument instead of an opinion. §26 changes nothing about it.

   THE SECTION CRUMB DOES CHANGE, and that one is real. Measured at 1440x900
   on `/chat`, the only page that adds two status chips to the header:
   `.brand-section` ran from 808 to 864 while `.header-right` began at 835, so
   the word "Assistant" was painted under the "Knowledge base ready" chip.
   It is a flex item with no `min-width: 0`, so it could not shrink below its
   own content and overflowed `.topsearch` instead. §15 already drops it
   entirely below 720px, where the page's own <h1> says the same word one
   line down; between those two widths it ellipsises now. */
.brand-section {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* §22b's `.main-col > header .brand-section` is (0,1,2) and sets
   `white-space: nowrap`, which is what makes the ellipsis possible; this is
   (0,1,0) and deliberately does not fight it.

   AND ON CHAT THE CRUMB STANDS DOWN AT EVERY WIDTH, which is §23's own
   decision applied at one more one. §23 already hides it between 901 and
   1279 — *"the field gives up width first, because it is the one thing here
   that still works at half its size"* — and above 1279 it was left showing,
   where the two status chips this page adds squeeze it to an ellipsis and
   nothing else. It is a location marker, not content: the page's own hero
   says "Adealia · Assistant" two inches below it, and §15 drops it on every
   page below 720px for the same reason. */
body.is-chat .brand-section { display: none; }
/* AND THE PRODUCT LOSES ITS WORDS ON CHAT, which is §25's own trade at one
   more width. §25 already drops `.product__name` below 1200px — *"the words
   go and the mark stays, which still says whose software this is"* — and
   chat is the page that runs out of room first at ANY width, because it is
   the only one that adds two status chips to the header. Without this the
   field is 160px at 1280 and the reader is typing into a slot. */
body.is-chat .product__name { display: none; }

/* ══ SCREENS: home ══════════════════════════════════════════════════════
   `planning/design/redesign-2026-09.md` §10, mockup lines 129–266 (home),
   621–717 (owner home) and 267–321 (tasks). Report 116, phase 4.

   ONE BLOCK AT THE END OF THE FILE, because three agents are drawing three
   sets of screens against this stylesheet at once and a rule edited in the
   middle of it is a merge conflict somebody resolves by hand. Everything
   below either targets a class only `dashboard/` or `tasks/` renders, or is
   scoped by `.ui-card[data-card=…]`, which exists on the home page alone.

   WHAT THE MOCKUP CHANGES ABOUT THIS PAGE. It is not a repaint: home goes
   from three loose tiles over two columns of padded cards to one row of
   LINKED tiles over two columns of LIST cards — a card head, then rows
   divided by hairlines, edge to edge, each row a title, a meta line, a
   status chip and a chevron. §7's row is 13–14px of vertical padding with a
   top hairline except on the first, and that is the vocabulary every list on
   these three screens now speaks. */

/* ── 116a. ADE-55: the stat row is one card of links ──────────────────────
   The ticket is open because the tiles restate the cards below them. The
   mockup's answer is to keep them and make each a COUNT THAT GOES SOMEWHERE
   — "Your actions 3" opens Tasks, "CE waiting 2" opens continuing education,
   "Coming up 4" opens the calendar. A card names individual items; a tile
   names a total and hands you the page that explains it. `dashboard/index
   .html` maps a card's own key onto the endpoint that card's own "View all"
   already points at, so the two cannot disagree.

   THE HAIRLINES ARE THE GAP. `gap: 1px` over a `--line-2` ground draws a
   divider between every pair of tiles in BOTH axes, so the row still reads
   as one object when it wraps to two lines at a narrow width. A
   `border-right` cannot do that: the last tile on a wrapped row keeps an
   edge with nothing beside it. */
.ui-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  background: var(--line-2);
  border: 1px solid var(--line);
  /* 16px, NOT `var(--radius)`, and that is a finding rather than a choice —
     see 116j at the end of this block. §16 gives `.ui-card` a hardcoded 16px
     and `--radius` reaches no card in this product, so a tile row at 12
     would be the only object on the page with a different corner. */
  border-radius: 16px;
  overflow: hidden;
  margin-bottom: var(--space-5);
}
.ui-stat {
  flex: 1 1 180px;
  min-width: 180px;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: 18px 22px;
  background: var(--card-bg);
  border: 0;
  border-radius: 0;
}
a.ui-stat {
  text-decoration: none;
  color: inherit;
  transition: background 140ms ease;
}
a.ui-stat:hover { background: var(--tint-bg); }
/* Drawn INSIDE the tile: the row clips at its own radius, so a ring around
   the outside of the first or last tile is cut off by it. */
a.ui-stat:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ui-stat__label {
  margin: 0;
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--muted-ink);
}
/* "3 overdue", "with Licensing" — the second half of the label, in the
   colour a deadline wears in this palette. `--warn-fg` rather than
   `--warn-dot`: the tile tints on hover, and the dot value is 4.3:1 on
   `--tint-bg` where this one is 6.6. */
/* ON THE LABEL'S LINE, NOT UNDER IT. "with Licensing" qualifies "CE waiting"
   — it is not a second fact — so it reads as one phrase and costs no height.
   Wraps rather than overflowing on a narrow tile. */
.ui-stat__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 6px;
}
.ui-stat__sub { margin: 0; font-size: var(--fs-meta); color: var(--muted-ink); }
.ui-stat__sub.is-warn { color: var(--warn-fg); }
/* MONO, because §4 reserves it for figures and dates and a count is a
   figure. `margin-top: auto` puts every number on the floor of its tile, so
   a one-word label and a four-word one still line their figures up — the
   tiles are stretch-aligned flex items, which is what gives `auto` a
   distance to eat. */
.ui-stat__value {
  margin: 0;
  margin-top: auto;
  padding-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink-1);
}

/* ── 116b. The two columns are 1.55 : 1 ───────────────────────────────────
   The mockup's grid, and it is wider on the right than this page had: a
   fixed `minmax(300px, 380px)` side column against a `1fr` main gave the CE
   card 380px at every width from 1100 up, so at 1920 the left column grew
   and the right one did not. Both scale now. */
/* IN A `min-width` QUERY, and that is not decoration. §1c's
   `@media (max-width: 1100px)` single-column rule sits forty lines into this
   file and this block is at the end of it, so an unconditional rule here
   would win at 900px and put the side column back beside the main one on a
   tablet. `:not(.ui-split--single)` for the same reason: a page with no side
   cards sets that class, and it is declared earlier too. */
@media (min-width: 1101px) {
  .ui-split:not(.ui-split--single) {
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  }
}
.ui-split { gap: var(--space-5); }

/* ── 116c. A LIST CARD, and it is the shape of all three screens ──────────
   §7: a card is a header row, then rows separated by hairlines, edge to
   edge. §16 already built this for `.card-rows` — the attention card — and
   the mockup draws the other two home lists the same way, so the rule stops
   being about one card. */
.ui-card:has(.card-tasks), .ui-card:has(.card-events) { padding: 18px 0 0; }
.ui-card:has(.card-tasks) .ui-card__band,
.ui-card:has(.card-events) .ui-card__band { margin: 0 22px 14px; }
.ui-split .ui-card--flush:has(.card-tasks) > *:last-child,
.ui-split .ui-card--flush:has(.card-events) > *:last-child { margin-bottom: 0; }

/* §7: the card head is 18/600 with a count chip beside it and the way out
   pinned right. §16 built it at 17; the mockup's section step is 18 and
   `--fs-section` is the token for it. */
.ui-card:has(.card-tasks) .card-title,
.ui-card:has(.card-events) .card-title,
.ui-card:has(.card-rows) .card-title {
  font-size: var(--fs-section);
  letter-spacing: -0.01em;
}

/* ── 116d. The row: title, meta, chip, chevron ────────────────────────────
   THE WHOLE ROW IS THE LINK, not the title inside it. A chevron beside a
   row whose only target is six words of title is a lie about where a thumb
   may land; the mockup draws the row as one `<a>` and so does this. The
   DESTINATION does not move — it is still `url_for(t.endpoint,
   **t.endpoint_args)`, `domain/task.SUBJECT_ENDPOINTS`' answer, which is
   what report 89 fixed and ADE-221 measured. A task with no endpoint is a
   real state and stays a `<div>` with no chevron. */
.ui-card .card-tasks { display: block; margin: 0; gap: 0; }
/* §19 built this card as one TINTED BLOCK per task — a rounded `--page-bg`
   rectangle with 10px of air between it and the next. The mockup's list is
   flush rows on white with a hairline between them, so the fill, the corner
   and the gap all come off here rather than being edited out of §19: three
   agents are drawing screens against that section at once. */
.card-task {
  display: block;
  padding: 0;
  margin: 0;
  background: none;
  border-radius: 0;
  border-top: 1px solid var(--line-2);
}
.card-tasks > .card-task:first-child { border-top: 0; }
.card-task__row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: 14px 22px;
  text-decoration: none;
  color: inherit;
  transition: background 140ms ease;
}
a.card-task__row:hover { background: var(--tint-bg); }
a.card-task__row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.card-task__text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.ui-card .card-task-title {
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--ink-1);
}
/* ONE LINE, ELLIPSISED. `kind_label · body` is the same string the Tasks
   page prints under the same title, and on a CE row it runs to two
   sentences — the mockup's meta line is one. `overflow_probe.py` treats a
   declared ellipsis as an answer rather than a defect. */
.card-task__meta {
  font-size: var(--fs-meta);
  color: var(--muted-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* THE CHEVRON IS `--muted-ink`, WHERE THE MOCKUP DRAWS `#A8A093`.
   Third instance of the correction §3 of the spec makes twice already: the
   mockup's faintest greys are drawn on white in a picture and land on
   tinted rows in this app. `#A8A093` is 2.59:1 on `--card-bg` — it is a
   glyph in a text node, so `contrast.py` measures it like any other word,
   and it would fail there. `--muted-ink` is 5.41. Report 116 M13 is that
   value put back — measured at 2.48 for the oklch spelling — and
   `contrast.py` is the only instrument in this project that sees it. */
.card-chev {
  flex: none;
  font-size: var(--fs-row);
  line-height: 1;
  color: var(--muted-ink);
}

/* THE DEADLINE IS A CHIP, and the escalation §13e settled survives it.
   §7's status chip is 13/500 on a 999px radius with no dot; §13e's rule is
   that a deadline is calm, then amber, then red, and differs by WEIGHT as
   well as by hue so a colour-blind reader still reads it. Both hold: every
   state is a chip now, three tints, and the two loud ones are 600. What
   goes is the leading dot — §7 draws none, and the chip's own fill is
   already the state. */
/* EVERY STATE IS SPELLED OUT because §19 spells four of them out: *"the due
   line is text here, not a pill"* is `.card-task .card-task-when.task-today`
   and its three siblings, at (0,3,0). A one-selector base at (0,2,0) would
   be overruled on exactly the four rows that matter and hold only on the two
   that do not, which is worse than not writing it. */
.card-task .card-task-when,
.card-task .card-task-when.task-later,
.card-task .card-task-when.task-none,
.card-task .card-task-when.task-soon,
.card-task .card-task-when.task-urgent,
.card-task .card-task-when.task-today,
.card-task .card-task-when.task-overdue {
  flex: none;
  align-self: center;
  display: inline-flex;
  align-items: center;
  height: auto;
  gap: 0;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-label);
  font-weight: 500;
  white-space: nowrap;
  background: var(--tint-bg);
  color: var(--ink-3);
}
.card-task .card-task-when::before { display: none; }
.card-task .card-task-when.task-soon,
.card-task .card-task-when.task-urgent,
.card-task .card-task-when.task-today {
  background: var(--warn-bg);
  color: var(--warn-fg);
  font-weight: 600;
}
.card-task .card-task-when.task-overdue {
  background: var(--alert-bg);
  color: var(--alert-fg);
  font-weight: 600;
}
/* §13e's LEFT-EDGE STRIPE STAYS OFF, and §19 is the one that took it off:
   *"inside a tinted block it reads as a torn edge rather than a signal, and
   the due line right under the title already carries the same information in
   words"*. I rebuilt it as an inset shadow, looked at it, and it reads the
   same way in a flush list — a grey rule down the left of the card, on every
   row, because on this fixture every one of Bea's three deadlines is inside
   its own window. The chip carries the escalation now, which is more than the
   plain coloured text it replaces. §19's rule already zeroes the border; this
   restates only the padding, so a row's text lines up with the head above it.

   WHAT THE FIXTURE CANNOT TELL YOU, and it is worth writing down: "Due
   tomorrow" and "Due in 53 days" are BOTH `task-soon` here. A licence and a
   CE shortfall take the licence module's ninety-day window
   (`SOON_WINDOW_BY_KIND`), and an event task takes the generic week, so all
   three of her rows land in the same band. Three amber chips is the product's
   own answer to those three dates, not a decision made in this stylesheet. */
.card-task:has(.task-soon), .card-task:has(.task-urgent),
.card-task:has(.task-today), .card-task:has(.task-overdue) {
  padding-left: 0;
  border-left: 0;
}

/* ── 116e. Coming up: the same row, led by a date block ───────────────────
   §7's row again, and the mockup's date block is a 46px bordered square on
   the rail fill with the month over the day. It was a tinted rounded block
   floating in a gap; a list of them reads as a calendar only when they are
   in one column with hairlines between.

   NO CHEVRON HERE, AND NOT AS AN OVERSIGHT. The mockup's Coming up row
   opens the event. This card's rows carry no event id — `pipeline/cards
   /upcoming_events` puts `title`, `when`, the date parts and an OPTIONAL
   external `link` on each row and nothing else — so most rows are not links
   at all, and the ones that are open somebody else's site in a new tab. A
   chevron would promise the mockup's behaviour on a card that cannot do it.
   Report 116 §7 has it as the thing to overrule. */
.ui-card .card-events { display: block; margin: 0; gap: 0; }
.card-event {
  display: flex;
  /* `app.css` says `flex-direction: column` on this class and §16d made it a
     grid, so the column never showed. Turning it back into a flex row
     without saying `row` puts the date block above the title. */
  flex-direction: row;
  align-items: center;
  gap: var(--space-4);
  padding: 13px 22px;
  border-top: 1px solid var(--line-2);
  border-radius: 0;
  background: none;
}
.card-events > .card-event:first-child { border-top: 0; }
.card-event__date {
  flex: none;
  width: 46px;
  gap: 1px;
  padding: 6px 0;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--rail-bg);
}
/* §4: mono is figures and DATES. A day number is a date, and 17/500 is the
   figure step — it was 19/600 sans, which is the row title's weight on a
   number. */
.card-event__day {
  font-family: var(--font-mono);
  font-size: var(--fs-figure);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink-1);
}
.card-event__text { flex: 1; min-width: 0; gap: 2px; }
.ui-card .card-event-title { font-size: var(--fs-row); font-weight: 500; }
.ui-card .card-event-when { font-size: var(--fs-meta); color: var(--muted-ink); }

/* ── 116f. The attention card's row, in the same words ────────────────────
   The owner home is a different composition, not a variant — but its rows
   and the broker's speak one vocabulary, and this is where the two meet.
   §7's row title is 16/500; this was 14.5/600, which is a smaller, heavier
   thing than every other title on the page.

   THE STRUCTURE IS UNTOUCHED. `.card-row`, `.card-row__text`, `.card-row
   __title` and `.ui-due` are what `check_every_row_stacks` measures at
   390px and what `tests/test_attention_card_links.py` reads the hrefs off;
   the avatar column is report 22's and stays. Only the type moves. */
.card-row { padding: 14px 22px; }
.ui-card .card-row__title { font-size: var(--fs-row); font-weight: 500; }
.ui-card .card-row__sub { font-size: var(--fs-meta); }
.ui-card .ui-due { font-size: var(--fs-label); }

/* ── 116g. The CE block, and what could not be built ──────────────────────
   THE MOCKUP'S RIGHT COLUMN IS A METER THIS PRODUCT HAS NO NUMBER FOR.
   Lines 234–246 draw a white card holding a mono `6`, "of 36 hours", a
   copper meter on a chip track, a line of provenance, and then a tinted
   block for whatever needs attention. `pipeline/cards/ce_status` offers the
   last of those five and refuses the rest in writing: *"A COUNT OF COURSES,
   never '6 of 36 hours'. The requirement rules that would make a denominator
   true do not exist."* Report 116 §3 has the whole of it. A meter over an
   invented denominator is the one thing this card's own module forbids, and
   inventing one is a query this phase may not add.

   SO THE CARD IS THE MOCKUP'S TINTED BLOCK and nothing else — which is what
   it already was, and §16e's argument for it stands. What changes is the
   type inside it and the way out of it. */
.ui-card[data-card="ce_status"] .card-heading,
.ui-card[data-card="license_status"] .card-heading {
  font-size: var(--fs-row);
  font-weight: 500;
  letter-spacing: 0;
  margin: 0 0 6px;
}
.ui-card[data-card="ce_status"] .ce-reason,
.ui-card[data-card="ce_status"] .lic-name,
.ui-card[data-card="license_status"] .card-detail,
.ui-card[data-card="license_status"] .lic-name {
  font-size: var(--fs-meta);
  line-height: 1.5;
}
/* THE WAY OUT IS A LINK, WHERE IT WAS A FILLED BUTTON — the mockup's own
   shape, and the reason is on the page rather than in the picture. The page
   head already carries a navy "Continuing education" button, and this card
   sat two inches under it with a second filled button to the same route.
   Two buttons to one page is the page telling you twice. The mockup
   separates them by weight: the head has the button, the block has an
   underlined link. */
.ui-card[data-card="ce_status"] .card-action,
.ui-card[data-card="license_status"] .card-action,
.ui-card[data-card="course_list_missing"] .card-action {
  align-self: flex-start;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: var(--fs-meta);
  font-weight: 500;
  text-decoration: underline;
  color: var(--warn-fg);
}
.ui-card[data-card="ce_status"] .card-action:hover,
.ui-card[data-card="license_status"] .card-action:hover {
  background: none;
  color: var(--warn-fg);
  text-decoration: underline;
}
/* The course-list card is not a tinted one — it is white — so its link takes
   the accent rather than the warn ink. */
.ui-card[data-card="course_list_missing"] .card-action { color: var(--accent); }
.ui-card[data-card="course_list_missing"] .card-action:hover { color: var(--accent-hi); }
.ui-card[data-card="course_list_missing"] .card-heading {
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--warn-fg);
  margin: 0 0 8px;
}
.ui-card[data-card="course_list_missing"] .ce-reason {
  font-size: var(--fs-meta);
  line-height: 1.55;
  margin: 0 0 var(--space-4);
}

/* ── 116h. Tasks ──────────────────────────────────────────────────────────
   Mockup lines 267–321. The row is home's row — title, meta, chip, and the
   control on the right — and THE LINK TARGET OF EVERY ROW IS UNTOUCHED.
   Report 89 fixed a defect where a broker's task title opened the reader's
   own profile instead of the person it named; `a.ui-table__link`'s href is
   still `url_for(t.endpoint, **t.endpoint_args)` and nothing here goes near
   it.

   IT STAYS A TABLE, and that is a decision rather than an omission. The
   mockup's tasks page is a broker's own list; this one carries a Who column
   for the rows that are somebody else's (ADE-147), a `data-label` on every
   cell that report 113's phone layout paints as its column name, and a
   `<thead>` a screen reader reads. A `<ul>` would lose all three to buy a
   shape the CSS can give the table anyway. */
.ui-table-wrap--tasks .ui-table th {
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--muted-ink);
  text-transform: none;
}
.ui-table-wrap--tasks .ui-table__link {
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--ink-1);
}
.ui-table-wrap--tasks .ui-table__sub {
  font-size: var(--fs-meta);
  color: var(--muted-ink);
}
/* THE SPACING IS DESK-ONLY, AND FINDING THAT OUT COST A WALKTHROUGH FAILURE.
   §15 collapses every cell to a block at 720px and gives it
   `padding: 0 0 var(--space-2)`, which is (0,1,1); `.ui-table-wrap--tasks
   .ui-table td` is (0,2,1) and beat it inside its own media query, so 15px
   of block padding landed on a stacked cell and pushed the Who cell 30px
   above the close button. ADE-147's own check measures that gap and wants it
   under 24 — the name has to read as belonging to the button beside it —
   and it went red, which is the check earning its place a second time. So
   the row's air is stated for the desk and §15 keeps the phone. */
@media (min-width: 721px) {
  .ui-table-wrap--tasks .ui-table td { padding-block: 15px; }
  .ui-table-wrap--tasks .ui-table__sub { margin-top: 3px; }
}
/* The same chip as home's rows, off the same `task-*` state class. */
.ui-table-wrap--tasks .task-due {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-label);
  font-weight: 500;
  background: var(--tint-bg);
  color: var(--ink-3);
}
.ui-table-wrap--tasks .task-due.task-soon,
.ui-table-wrap--tasks .task-due.task-urgent,
.ui-table-wrap--tasks .task-due.task-today {
  background: var(--warn-bg); color: var(--warn-fg); font-weight: 600;
}
.ui-table-wrap--tasks .task-due.task-overdue {
  background: var(--alert-bg); color: var(--alert-fg); font-weight: 600;
}
/* §7's section step over each block. These were `.ui-eyebrow` — 14px,
   muted, the same size as the meta line inside the rows under them — so
   nothing on the page was a landmark. */
.tasks-heading {
  margin: 0 0 var(--space-3);
  font-size: var(--fs-section);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-1);
}

/* ── 116i. On a phone ─────────────────────────────────────────────────────
   §6: every row stacks. `.card-row` is already handled by §15; the two rows
   this phase adds need the same treatment — the chip takes its own line
   under the text rather than eating the title's width, which is report
   113's hazard in a flex row (an anonymous item cannot shrink below its own
   min-content width). */
@media (max-width: 720px) {
  .card-task__row {
    flex-wrap: wrap;
    gap: 6px var(--space-3);
    padding: 14px 16px;
  }
  .card-task__text { flex: 1 0 100%; }
  .card-task__meta { white-space: normal; }
  .card-task-when { order: 2; }
  .card-chev { order: 3; margin-left: auto; }
  .card-event { padding: 13px 16px; }
  .ui-card:has(.card-tasks) .ui-card__band,
  .ui-card:has(.card-events) .ui-card__band { margin: 0 16px 12px; }
  .card-row { padding: 14px 16px; }
  .ui-stat { padding: 16px 18px; }
}

/* ── 116j. WHAT I FOUND AND DID NOT CHANGE: `--radius` draws one object ───
   Report 115 §7 left this for phase 4: *"`--radius` 14 → 12 changes every
   card on every screen, and no screen template was opened to look at what
   that does."* I am the first to look, and it changes NO card.

   Measured in a browser over six pages, 8 September 2026, with `--radius` at
   12: `.ui-card` computes 16, `.ui-table-wrap` computes 16, `.auth-card`
   computes 18, and `.guide-toc` — the Guide's contents list — computes 12.
   §16 declares `.ui-card, .ui-card--flush { border-radius: 16px }` and §20
   declares `.ui-table-wrap { border-radius: 16px }`, both literals and both
   after the `var(--radius)` rules they override. So the shell phase's 14 → 12
   moved ONE visible object in the whole product, and it is not a card. The
   only other survivor is `.ui-card--flush > .ui-card__band`, whose top
   corners are 12 on a band with `background: none` inside a card with
   `overflow: hidden` — no pixel changes.

   Report 115's own M15 ("a card is 14px on a desk again") is red on a LINT
   that reads the token out of the stylesheet, not on anything a browser
   measures — so the mutation proves the token's value and says nothing about
   a card.

   NOT CHANGED HERE. `.ui-card` is on every screen in the product and two
   other agents are drawing theirs against it right now; a 16 → 12 in this
   block would move every card on every page in the app from one agent's
   branch, silently, in a merge. It is one line in §16 and one in §20 for
   whoever wants it — and the spec (12) and the mockup (14) do not agree
   about the answer either. Report 116 §4 and §7. */

/* ══ END SCREENS: home ═══════════════════════════════════════════════════ */
/* ══ SCREENS: events ══════════════════════════════════════════════════════
   The September 2026 redesign, phase 4 — the events screens and the Connect
   Centre. Report 117. Spec: `planning/design/redesign-2026-09.md` §7 (the
   component vocabulary) and §10 (mockup lines 322–370 events, 371–394 event
   detail, 395–419 Connect, 420–458 a channel, 459–497 a discussion, 792–815
   the events list on a phone).

   THREE AGENTS ARE WRITING SCREEN CSS AT ONCE, so everything this phase adds
   for these five screens is inside this block and nothing outside it moved.
   Nothing here declares a colour or a size literally: every value is a token
   from §1 and every step is one of §4's six.

   WHAT IS DELIBERATELY NOT HERE, because it belongs to somebody else:

     * §7's STATUS CHIP is "13/500, 999px, green pair or amber pair". This
       app's chip is `.ui-badge` — 14/600 with a state dot, §7 of this file —
       and it is on every screen in the product. Restyling it is a change to
       the design system, and three screen agents each answering that in
       their own block is three answers. The Going chip the mockup draws is
       `.ui-badge--ok` unchanged.

   AND ONE THING THAT IS HERE ON A SECOND PASS. The events list was left a
   `<table>` in this phase's first version and the decision was overruled:
   the mockup's card of date-block rows is the shape, and the reasons for
   keeping the table were the cost of changing three instruments rather than
   an argument about the screen. §15's phone rules, report 115's full-width
   Register selector and about thirty walkthrough checks were addressed to
   that markup and every one of them is updated. Report 117 §3.1.
   ─────────────────────────────────────────────────────────────────────── */

/* E1. THE SWITCH IS §7'S SEGMENTED CONTROL — "chip-filled track, 4px
   padding, white active pane, 8px inner radius". It was two outline buttons
   with the current one filled a slightly different cream, which asks the
   reader to compare two neutrals to find out where they are. A track makes
   the pair one object and the white pane is the answer.

   THE MARKUP DID NOT MOVE. `.ev-switch` is still two links and the one you
   are on is still `aria-current="page"` — `app.css`'s own note says the
   styling hangs off the attribute rather than off a class the template has
   to compute a second time, and that is still true.

   `.ev-switch .ui-btn` is (0,2,0), so it beats `.ui-btn--sm`'s two
   declarations at (0,1,0) without either being edited. */
.ev-switch {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-1);
  margin: 0 0 var(--space-5);
  background: var(--chip-bg);
  border-radius: 11px;
}
.ev-switch .ui-btn {
  height: auto;
  padding: 7px 14px;
  border: 0;
  background: none;
  box-shadow: none;
  border-radius: 8px;
  font-size: var(--fs-meta);
  font-weight: 500;
  /* 4.87:1 on `--chip-bg`. `--muted-ink` would be 4.55 and this is the one
     control on the page that is a destination, so it takes the darker ink. */
  color: var(--ink-3);
}
.ev-switch .ui-btn:hover { background: none; color: var(--ink-1); }
.ev-switch .ui-btn[aria-current="page"] {
  background: var(--card-bg);
  color: var(--ink-1);
  font-weight: 500;
}

/* E2. THE LIST IS A CARD PER MONTH, mockup 334–352.

   A month heading at §4's row step in the meta grey, then a card whose rows
   are hairline-separated. Each row leads with a date block — a bordered 46px
   tile on the rail fill, the month over the day, the day in mono because §4
   reserves mono for figures and dates — then the title at §4's row step, then
   the when-line under it.

   THE WHEN COLUMN IS GONE. That is the shape, not a saving: the block carries
   the day and the sentence carries the whole date, and a row that drew both a
   block and a column printed one fact twice.

   THE ROW IS A FLEX ROW THAT WRAPS, and the text column is `flex: 1 1 260px`
   with `min-width: 0`. Report 113's hazard is why both halves of that are
   written down: a flex item is floored at its own min-content width unless it
   is told otherwise, and the min-content width of `Wednesday, 9 September
   2026, 9:00 AM–12:00 PM MDT` is the whole string. Nothing here sets
   `white-space: nowrap`, so the when-line wraps at 390px by default — which
   is what ADE-234 had to add two rules to a table to achieve.

   THE WHOLE ROW IS THE TARGET AND THE TITLE IS THE LINK, which is §8's
   mechanism copied rather than reused: §8 scopes its `::after` overlay to
   `.ui-table tbody tr` because `inset: 0` resolves against the nearest
   POSITIONED ancestor, and an anchor with none becomes an invisible full-page
   shield (ADE-70, measured 5 September 2026). `.ev-row` is the positioned
   ancestor here, and everything a person can operate is lifted above the
   overlay for the same reason §8 lists four selectors. */
.ev-list { display: flex; flex-direction: column; gap: var(--space-6); }
.ui-page .ev-month__label {
  margin: 0 0 10px;
  font-size: var(--fs-row);
  font-weight: 600;
  color: var(--muted-ink);
}
.ev-card {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.ev-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: 16px 22px;
  border-top: 1px solid var(--line-2);
  transition: background 140ms ease;
}
.ev-row:first-child { border-top: 0; }
.ev-row:hover { background: var(--tint-bg); }
/* §8's overlay, re-scoped. Without `position: relative` on the row the
   `inset: 0` below resolves against the page and swallows every click on it. */
.ev-row { position: relative; }
.ev-row__title::after { content: ""; position: absolute; inset: 0; }
.ev-row form,
.ev-row button,
.ev-row .ui-btn,
.ev-row .ui-badge { position: relative; z-index: 1; }
.ev-date {
  flex: none;
  width: 46px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 6px 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--rail-bg);
}
/* The month keeps `.card-event__month` as well as its own name: that class is
   the product's date-block month — mono, `--fs-micro`, muted, sentence case
   since report 114 §3.1 — and it is one of the twelve names `type_probe.py`
   exempts from the 13px floor. Reusing it is one component and no edit to a
   shared script. `--muted-ink` on `--rail-bg` is 5.18:1. */
.ev-date__day {
  font-family: var(--font-mono);
  font-size: var(--fs-figure);
  font-weight: 500;
  line-height: 1;
  color: var(--ink-1);
}
.ev-row__text {
  flex: 1 1 260px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* §4's row title, 16/500. `.ui-table__link` carries 600, which is right on a
   roster of forty names and wrong on the eight things a broker came here to
   read. */
.ev-row__title {
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--ink-1);
  text-decoration: none;
}
.ev-row:hover .ev-row__title { color: var(--accent); }
.ev-row__when,
.ev-row__for,
.ev-row__places { font-size: var(--fs-meta); color: var(--muted-ink); }
.ev-row__act {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ev-row__act .ui-inline { display: flex; align-items: center; gap: var(--space-2); }

/* E3. THE EVENT'S FACTS ARE A LABEL/VALUE GRID, which is mockup lines
   378–383: a 130px column of labels and the values in the rest. They were
   `repeat(auto-fit, minmax(160px, 1fr))` — a responsive grid of stacked
   pairs, which on a card this wide put "When" above its own date and left
   two thirds of the row empty.

   NOT `display: contents` on the pair wrappers. That is the other way to
   turn `<div><dt><dd></div>` into two grid columns and it takes the wrapper
   out of the accessibility tree on browsers this project has not measured.
   Each pair is its own two-column grid instead, which needs nothing new. */
.ui-page .ev-facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* The mockup's own 22px under the list. `.prof-readonly` carries
     `margin: 18px 0 0` and nothing below it, so the button row sat on the
     last value. */
  margin-bottom: 22px;
}
.ui-page .ev-facts > div {
  display: grid;
  grid-template-columns: 130px minmax(0, 1fr);
  gap: var(--space-5);
  align-items: baseline;
}

/* E4. THE CLOCK SENTENCE IS A PANEL, mockup line 391: the rail fill, a 12px
   radius, and a measure short enough to read. It is the last thing said
   before somebody presses "Add to my calendar" and it was a grey line
   floating between two other grey lines.

   `.ui-page .ev-calendar-clock` and not `.ev-calendar-clock`, because §14
   caps `.ui-page p` at 72ch at (0,1,1) and a bare class is (0,1,0) — the
   same trap §14's own comment records one rule further down. */
.ui-page .ev-calendar-clock {
  margin-top: var(--space-4);
  padding: 14px 16px;
  border-radius: 12px;
  background: var(--rail-bg);
  color: var(--ink-3);
  max-width: 60ch;
}

/* E5. CONNECT IS ONE CARD, NOT FOUR. Mockup lines 402–417 draw the channel
   list as a single card whose rows are separated by hairlines; this was
   `.ui-grid-cards`, three or four boxes across, and §8's own argument about
   the roster applies unchanged — four channels as four cards is four
   paragraphs with air between them.

   THE ROW IS THE ANCHOR. `.ui-table__link` outside a table row keeps the
   weight and the ink and gets no overlay pseudo-element, which is exactly
   what §8's comment says that selector was scoped for, so the whole row is
   one link with nothing layered over it.

   AN EMPTY LIST DRAWS NO CARD, and that is decided in the template rather
   than by `:empty` — Selectors 4 made whitespace-only elements match `:empty`
   and browsers took it up at different versions, while a Jinja loop always
   leaves whitespace. `connect/index.html` says why. */
.connect-list {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.connect-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: 16px 22px;
  border-top: 1px solid var(--line-2);
  text-decoration: none;
  transition: background 140ms ease;
}
.connect-row:first-child { border-top: 0; }
.connect-row:hover { background: var(--tint-bg); }
/* REPORT 113'S HAZARD, IN A ROW THAT IS NOT A TABLE CELL: a flex item is
   floored at its own min-content width unless it is told otherwise. The text
   column has `min-width: 0` and a BASIS rather than a fixed width, so a long
   channel name wraps instead of pushing the counts off the card. */
.connect-row__text {
  flex: 1 1 240px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.connect-row__title { font-size: var(--fs-row); font-weight: 500; color: var(--ink-1); }
.connect-row__sub {
  font-size: var(--fs-meta);
  font-weight: 400;
  color: var(--muted-ink);
  line-height: 1.5;
}
.connect-row__meta {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.connect-row__count {
  font-size: var(--fs-meta);
  color: var(--muted-ink);
}
/* THE CHEVRON IS DARKER THAN THE MOCKUP DRAWS IT and that is the third time
   this redesign has made the same correction. The mockup's `#A8A093` is
   1.93:1 on a card; `scripts/contrast.py` walks every text node the app
   paints and a glyph is a text node, `aria-hidden` or not. `--muted-ink` is
   5.41 there and 4.67 on the tint the row hovers to. */
.connect-row__go {
  flex: none;
  font-size: var(--fs-row);
  color: var(--muted-ink);
}

/* A section label over a list card is §4's SECTION step, 18/600, not the
   13/600 that labels a field. The mockup's own headings over these lists —
   "Discussions", the month over a card — are 15–18px; `.ui-eyebrow` is the
   13px one and it stays that everywhere else in the app. */
.ui-page .ev-section {
  font-size: var(--fs-section);
  font-weight: 600;
  color: var(--ink-1);
  margin: 0 0 var(--space-3);
}

/* E6. THE DISCUSSION — the one screen in this product that renders words one
   broker wrote for another. Mockup lines 470–487.

   THE CARD HOLDS ITS ROWS EDGE TO EDGE, so the comments read as one
   conversation: a head row, one hairline-ruled row per comment, and the zone
   line as the card's own footer instead of a fourth grey sentence inside the
   last comment.

   NOTHING HERE RENDERS ANYBODY'S TEXT AS MARKUP. `.connect-body` is still a
   `<pre>` with `white-space: pre-wrap`, which is `app.css`'s own note: the
   newlines survive in CSS, so nobody ever needs the filter that turns them
   into `<br>`. What moves is the SIZE — `app.css` sets 13.5px and §4's body
   step is 16/1.55 — and the measure, because a paragraph across a 1180px
   column is not a paragraph. */
.ui-page .connect-comments { padding: 0; }
.ui-page .connect-comments__head {
  margin: 0;
  padding: 18px 24px 14px;
}
.connect-comment {
  display: flex;
  gap: 14px;
  margin: 0;
  padding: 18px 24px;
  border-top: 1px solid var(--line-2);
}
/* Overrides `app.css`'s rule for the same element, which drew the divider
   with a margin between comments instead of a rule across the card. */
.connect-comment + .connect-comment { margin-top: 0; }
/* THE MONOGRAM IS `.avatar`, and only its fill is this screen's own.

   It carries the product's avatar class because it IS one — which also means
   `type_probe.py` already knows it: `--fs-micro` is the named route under the
   13px floor and `avatar` is one of the twelve class names that file exempts,
   so this needed no edit to a shared script. Report 114 §3.2 counts three
   avatar monograms among its fifteen micro sites; this is the fourth.

   THE FILL IS THE MOCKUP'S OWN AND NOT §7'S. The spec's component table says
   an avatar is "999px, navy fill, white initials", and the header's is. The
   mockup's discussion (line 481) draws THIS one on `#EFEBE2` with `#55677D`
   — the chip — and the drawing is the more specific evidence: a column of
   navy discs down a conversation is the loudest object on a page whose
   subject is what somebody wrote. `--ink-3` on `--chip-bg` is 4.87:1.
   Recorded as a divergence rather than made quietly; §7 of report 117. */
.connect-comment__mark {
  align-self: flex-start;
  width: 34px;
  height: 34px;
  background: var(--chip-bg);
  color: var(--ink-3);
}
.connect-comment__text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.connect-comment__by {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* 4px, not 8: the timestamp is introduced by the same "·" the page head
     above it uses, and a wide gap in front of a separator reads as two
     spaces. */
  gap: var(--space-1);
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--ink-1);
}
.connect-comment__when {
  font-size: var(--fs-meta);
  font-weight: 400;
  color: var(--muted-ink);
}
/* `max-width: none` because §14 caps `.ui-page p` and `.ui-hint` at 72ch, and
   this <p> IS the card's last row — its own border-top is the rule across the
   card, so a 72ch element drew a rule that stopped two thirds of the way. It
   is §14's own `.ui-page-foot` correction, one card along. */
.ui-page .connect-comments__foot,
.ui-page .connect-comments__none {
  margin: 0;
  padding: 14px 24px 18px;
  border-top: 1px solid var(--line-2);
  max-width: none;
}
/* THE READING MEASURE. 66ch for the opening post, 62 for a comment — the
   mockup's own two numbers, and the reason they differ is that a comment
   starts 48px in behind its monogram. */
.ui-page .connect-body {
  font-size: var(--fs-body);
  line-height: 1.6;
  max-width: 66ch;
}
.ui-page .connect-comment .connect-body { max-width: 62ch; }

/* E7. ON A PHONE. §6: "the rail becomes a bottom bar and every row stacks."
   The events list stacks through §15 and nothing here touches it. What is
   new is the two lists that are not tables and the label/value grid, and
   each gives up its second column rather than squeezing it. */
@media (max-width: 720px) {
  .connect-row {
    padding: 14px 16px;
    gap: var(--space-2);
  }
  /* The counts and the chevron take their own line under the text, which is
     §6's row shape — title, meta, then any control. */
  .connect-row__meta { flex: 1 0 100%; }
  .connect-comment { padding: 14px 16px; gap: 10px; }
  .ui-page .connect-comments__head { padding: 16px 16px 12px; }
  .ui-page .connect-comments__foot,
  .ui-page .connect-comments__none { padding: 12px 16px 16px; }
  /* 130px of label beside a value in a 358px card is a value in a slot. */
  .ui-page .ev-facts > div {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }

  /* E8. A THIRD BOX THAT OVERFLOWS ON A PHONE, and nobody had reported it.
     `scripts/overflow_probe.py` drives eleven STATIC paths and an event's own
     page is not one of them — its address is a fixture row — so this was
     measured by pointing the probe's own `_MEASURE` at the pages it cannot
     name (report 117 §5.2).

     A row of the register at 390px, signed in as the organiser, is a
     checkbox, a name, an optional "added by …", an optional check-in stamp
     and up to three buttons in a `display: flex` `<li>` that cannot wrap:
     `button.ui-btn--ghost` holding "Remove" measured **76px in a 312px row,
     17px past its right edge**. It is report 113's finding in a list instead
     of a table cell, and §6's rule is the same one: every row stacks.

     THE NAME TAKES THE WHOLE LINE and the controls take the next, which is
     what makes the wrap tidy rather than ragged — without it the first button
     rides up beside a short name and the rest fall under it.

     SCOPED TO `.ev-register`. `.attendees` is also drawn by Messages and
     People, and this phase owns neither. */
  .ui-page .ev-register li { flex-wrap: wrap; row-gap: var(--space-2); }
  .ui-page .ev-register .attendee { flex: 1 0 100%; min-width: 0; }

  /* E7b. THE EVENTS ROW ON A PHONE, mockup 792–806. The date stops being a
     tile and becomes a mono short date on the title's own baseline; the
     when-line takes the next line; and the control takes the row.

     REPORT 115 §2.6'S RULE, RESTATED FOR MARKUP THAT IS NO LONGER A TABLE.
     §15 says `.ui-table td > form:has(> .ui-btn--primary)` and spells out why
     it names the FORM: Register sits inside a POST form with its CSRF token,
     so `width: 100%` on the button alone fills a form that is still
     shrink-to-fit. The form is now a direct child of `.ev-row__act`, and the
     rule that stretches it is the same rule one selector along. §15's own
     copy is untouched and still holds every other table in the product. */
  .ev-row { padding: 14px 16px; gap: 10px; align-items: flex-start; }
  .ev-row__text { row-gap: 6px; }
  .ev-date {
    flex-direction: row;
    align-items: baseline;
    width: auto;
    gap: 5px;
    padding: 0;
    border: 0;
    background: none;
  }
  .ev-date__day { font-size: var(--fs-meta); }
  .ev-row__places,
  .ev-row__act { flex: 1 0 100%; }
  .ev-row__act { flex-wrap: wrap; }
  .ev-row__act > form:has(> .ui-btn--primary) { flex: 1 0 100%; }
  .ev-row__act > .ui-btn--primary,
  .ev-row__act > form > .ui-btn--primary {
    display: flex;
    width: 100%;
    justify-content: center;
    padding-block: 12px;
  }

  /* E9. AND THE OTHER THREE CONTROLS THAT MATTER. §6's rule is "a control
     that matters goes full width", and §15 applies it to exactly one shape:
     a `.ui-btn--primary` inside a stacked table cell, which is the Register
     button on the events LIST. The three primaries on the screens this phase
     owns are not in a table — Post on a channel, Post on a discussion, and
     Register on an event's own page — and each sat as a 78px pill in the
     corner of a 358px card while the same act one screen back spanned the
     row. Measured at 390px.

     BY ID FOR TWO OF THEM. `.ui-page-head__actions` is the button row on
     twenty templates and this phase owns five; `#connectStart` and
     `#connectReply` are the two sections in `connect/`, and
     `.ev-detail-actions` is already events-only. */
  #connectStart .ui-btn--primary,
  #connectReply .ui-btn--primary,
  .ev-detail-actions > form > .ui-btn--primary {
    display: flex;
    width: 100%;
    justify-content: center;
    padding-block: 12px;
  }
  .ev-detail-actions > form { flex: 1 0 100%; }
}
/* ══ END SCREENS: events ═════════════════════════════════════════════════ */
/* ══ SCREENS: ce ══════════════════════════════════════════════════════════
   Continuing education and People — the September 2026 redesign, phase 4.
   Report 118. Mockup lines 547–620 (CE) and 498–546 (People); the spec is
   `planning/design/redesign-2026-09.md`, §7 for the component vocabulary.

   NINE CE TEMPLATES AND THE MOCKUP DRAWS ONE OF THEM. What is here is that
   one screen built as drawn, and the same vocabulary — figure, status chip,
   count, hairline row, tinted explanation — carried onto the eight it does
   not draw. Nothing invented; where a shape had to be inferred, report 118
   §3 says so and says from what.

   Every rule in this block belongs to `templates/ce/` or `templates/people/`.
   Nothing outside it was edited. */

/* ── A. The broker's page is two columns, because the mockup draws two ────
   Mockup 590: `minmax(0, 1.35fr) minmax(0, 1fr)`, "Where you stand" beside
   "Add a course". This page was three full-width cards stacked, so at 1440
   the standing card's part rows ran a label hard left and a figure hard
   right across 1020px of paper with nothing in between — measured before,
   `ce-before-ce_index-wide.png`. The form also started below the fold.

   ONE COLUMN UNTIL 1240px, AND THE NUMBER IS ARITHMETIC RATHER THAN TASTE.
   The content column is `viewport − 40 (the card's gutters) − 252 (the rail)`
   and `.ui-page` takes 40px of padding either side, so the 1fr track is
   `(inner − 24) / 2.35`: 359px at 1240, 443 at 1440, 460 at 1920. `.prof-grid`
   is `repeat(auto-fit, minmax(200px, 1fr))`, which needs 412px for two
   columns — so at 1240 the Add-a-course form would have gone to ONE field per
   row and been LONGER than the full-width card it replaced. The minimum drops
   to 170 inside the grid and only there; 2 × 170 + 12 = 352, which clears 359.
   Below 1240 nothing changes: one column, in document order, as before. */
.ce-columns {
  display: grid;
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-6);
}
.ce-columns > * { min-width: 0; }
@media (min-width: 1240px) {
  .ce-columns { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .ce-columns .prof-grid {
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  }
}
/* §14's `.ui-page > .ui-card + .ui-card` cannot see a card that is now a
   grandchild, so the rhythm below the grid is restated rather than lost. */
.ce-columns + .ui-card { margin-top: var(--space-6); }

/* ── B. A FIGURE IS MONO. §4: "Mono is reserved for figures and dates so
   numbers line up in a column." Every one of these is a number a reader
   compares against another number — hours against hours, a count against
   the count beside it — and all of them were proportional sans. */
.ce-owed__figure,
.ce-tally strong,
.roster-tally strong,
.req-part__hours .ui-input {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
/* `.ce-part__figure` IS NOT ON THAT LIST, and the mockup is why it looks as
   though it should be. Mockup 968 draws `0 / 12.0 hrs` there and puts
   "outstanding for years 1, 2 and 3" in the NOTE. This product does not: an
   annual part prints "4.0 hours each year · outstanding for years 1, 2, 3"
   in the figure slot, because ADE-33 found that a cycle total over a yearly
   requirement — "12.0 / 4.0 hours" — is not a fraction of anything. So two
   of R1's own four rows hold a SENTENCE there, and §4 reserves mono for
   figures and dates. Moving the phrase into the note to win the mono back
   would be reopening that decision to get a typeface. */
/* The owed figure is the first number on the page (ADE-171) and §4's figure
   step is what it is: mono 17/500. It was 14/600 sans. */
.ce-owed__figure {
  font-size: var(--fs-figure);
  font-weight: 500;
}

/* ── C. The requirement part — the row the mockup draws four of ───────────
   Mockup 613–620: a column of label over an optional note, then the figure,
   then a Met / Short chip. The product had label and note on ONE line and no
   chip: the state was carried by the figure's COLOUR alone, which is the one
   thing a reader who cannot separate green from amber does not have.

   ADE-185's wrap is untouched and is the reason this works at 390px: the
   row still wraps, the label still has a floor, and the figure still may
   wrap. The mockup's own `flex: 1; min-width: 160px` on the label column is
   the same shape ADE-185 arrived at by measuring. */
.ce-part, .ce-obligation {
  padding: 12px 0;
  font-size: var(--fs-meta);
}
.ce-part__label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 160px;
}
.ce-part__name {
  font-size: var(--fs-row);
  font-weight: 500;
  color: var(--ink-1);
}
/* The note takes its own line under the label rather than trailing it. It is
   the quietest thing on the row and it is what the mockup draws in the note
   slot — "Every year · outstanding for years 1, 2 and 3". */
.ce-part__period {
  margin-left: 0;
  font-size: var(--fs-label);
  font-weight: 400;
  color: var(--ink-4);
}
/* The figure and the chip are ONE flex item, and they right-align inside it.
   §27's note on `.ce-part__figure` says a `text-align` there moves nothing,
   because that box is `flex: 0 1 auto` and exactly as wide as its text. That
   is still true; what is aligned here is the PAIR, which does grow, so on a
   line of its own the figure ends where the chip ends and both sit against
   the card's right edge rather than against its left. */
.ce-part__state {
  flex: 1 1 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  min-width: 0;
}
/* The chip never shrinks and never wraps inside itself; it is the last thing
   on the row and the first thing a reader looks for. */
.ce-part .ce-state, .ce-obligation .ce-state { flex: none; }

/* ── D. The explanation under the summary is a tinted block ───────────────
   Mockup 605: "Property management applies to you, so those hours count
   inside the 36 rather than on top of it" is drawn on `#FBFAF6` at a 12px
   radius. In the product that sentence is `.ce-standing__because` (ADE-190)
   and it was a quiet line pulled 4px UP under the summary, which read as a
   continuation of it rather than as an answer to "why is my total this
   number". */
.ce-standing__because {
  margin: var(--space-2) 0 var(--space-4);
  padding: 14px 16px;
  border-radius: var(--radius);
  background: var(--rail-bg);
  color: var(--ink-3);
  line-height: 1.55;
}

/* ADE-239: WHY TWO ROWS ADD UP TO MORE THAN THE TOTAL.
   The same eight hours are printed on the core elective row and on the
   property management row, because New Mexico lets one course satisfy both.
   It sits UNDER the list it explains rather than over it — `because` above
   answers a question about the headline figure, this one answers a question
   about two rows — so it takes its margin at the top and carries no tint:
   two stacked tinted blocks in one card read as a warning, and this is an
   explanation. */
.ce-standing__shared {
  margin: var(--space-3) 0 0;
  font-size: var(--fs-meta);
  line-height: 1.55;
  color: var(--ink-3);
  max-width: 74ch;
}

/* ADE-241: NAR'S DEADLINE, WHICH IS NOT THE STATE'S.
   Outside the standings card on purpose — a measurement against a licence is
   what that card is for, and this is a date nobody here measures anybody
   against. Quiet, held to a readable measure, and separated from the card
   above it so it does not read as the last line of it. */
.ce-nar {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-meta);
  line-height: 1.55;
  color: var(--ink-3);
  max-width: 74ch;
}

/* ── E. The counts above the roster ───────────────────────────────────────
   Mockup 507–515: ONE card, four columns, a mono figure over a 14px caption,
   then a hairline and the sentence under it. ADE-156's four numbers were
   four separate grey pills of four different widths, so the first — the one
   that is the work — was the widest object on the page and looked like a
   banner rather than like a count.

   THE FOUR STAY, THE ORDER STAYS, AND THE MUTING STAYS. Never-asked first,
   all four the same colour, for report 36 §3's reason: none of these is a
   broker failing at anything.

   `auto-fit` and not `repeat(4, …)`: a supervising broker sees ONE tally,
   because she may not answer the property question for a supervisee, and a
   fixed four-column grid would put that number in a quarter of the card. */
.roster-counts {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-3);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
}
/* THE HAIRLINE IS ON THE COUNTS, NOT ON THE SENTENCE. The mockup rules the
   full width of the card (line 516); the sentence holds a 62ch measure, so a
   `border-top` on IT stopped two thirds of the way across — measured 980px of
   rule in a 1066px card. §24b of this file records the same trap on the
   notice box. */
.roster-counts .roster-tallies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-4);
  margin: 0 0 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line-2);
}
.roster-counts .roster-tally {
  margin: 0;
  padding: 0;
  background: none;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: var(--ink-3);
  font-size: var(--fs-meta);
  line-height: 1.4;
}
.roster-counts .roster-tally strong {
  font-size: var(--fs-h2);
  font-weight: 500;
  color: var(--ink-1);
}
/* The sentence belongs to the numbers, so it is inside their card with the
   hairline §7 puts between a header row and the rows under it. */
.roster-counts .roster-tallies__note {
  margin: 0;
  color: var(--ink-3);
  line-height: 1.55;
}

/* ── F. The roster row leads with a monogram ──────────────────────────────
   Mockup 525–531. `aria-hidden`: the name is the next thing in the row and a
   screen reader that read "BT Bea Trujillo" would be reading it twice.

   13px AND NOT THE MOCKUP'S 12. The floor beats the mockup — report 114 §3.2
   settled it — and a monogram at 13 in a 32px circle costs nothing, where
   `--fs-micro` would have meant editing `type_probe.py`'s allow-list to make
   room for a class that does not need it. */
.roster-name { display: flex; align-items: center; gap: 11px; }
.roster-name__text { min-width: 0; }
.roster-avatar {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--chip-bg);
  color: var(--ink-3);
  font-size: var(--fs-label);
  font-weight: 500;
  display: grid;
  place-items: center;
}
/* AND IT COMES OFF IN THE MIDDLE BAND, WHICH IS MEASURED RATHER THAN GUESSED.
   The roster is six columns and four of them are `<select>`s, so it is the
   widest table in the product. `scripts/overflow_probe.py --width 1100` went
   from 0 deliberate scrollers to 1 the moment the monogram landed:
   `div.ui-table-scroll 779 in 760` — nineteen pixels, and the price of them
   is a list Licensing has to drag sideways. The circle is `aria-hidden`
   decoration and the name beside it is the content, so the circle is what
   gives way. Above 1200 there is room for both; below 720 §15 has stacked the
   row into a block and the name cell is a heading with the width of the card,
   where the monogram costs nothing and reads as the mockup draws it. */
@media (max-width: 1199px) { .roster-avatar { display: none; } }
@media (max-width: 720px)  { .roster-avatar { display: grid; } }

/* ── G. The consoles' counts ──────────────────────────────────────────────
   `.ce-tally` keeps its colours, because on THIS block the colour is the
   meaning — short is the warning one — and it keeps its ring when active.
   What changes is the figure: it is a number beside three other numbers, so
   §4 says mono. §B sets the family; this sets the weight the scale asks for.

   NOT THE MOCKUP'S 22px. There is no step between 18 and 30 and §4 says six
   steps only, so the figure holds `--fs-h2` and gains the mono. Report 118
   §3 records the divergence. */
.ce-tally strong, .roster-tally strong { font-weight: 500; }

/* ── H. The state list and the requirement form ───────────────────────────
   `ce/requirements.html` and `ce/requirement.html` have no counterpart in the
   mockup. They get the row the mockup DOES draw: a name at the row step, a
   status chip on the right, the note under it, and hours as a figure. */
.req-name { font-size: var(--fs-row); font-weight: 500; }
.req-part__label { display: flex; flex-direction: column; gap: 2px; }
.req-part__period { color: var(--ink-4); }
.req-part__hours .req-field__label { color: var(--ink-4); }

/* ── I. Dates and hours in a console column ───────────────────────────────
   `.ui-table__num` is the product's own answer to a figure in a table —
   tabular numerals, no wrap in a COLUMN, and ADE-234's `white-space: normal`
   the moment §15 turns the cell into a BLOCK. The console's Renews and Hours
   cells carried neither, so "2026-10-01" broke after the month and "0 / 36"
   broke after the slash. The class is added in the templates; this is the
   family, which `.ui-table__num` deliberately does not set because most
   numeric cells in this app are counts rather than figures. */
.ui-table td.ui-table__num { font-family: var(--font-mono); }

/* ── J. What offboarding costs, one row per thing it costs ───────────────
   §7's row: a title, its meta under it, a top hairline except on the first.
   The eight consequences were eight running paragraphs with the badge inside
   the prose, so no two headings began at the same x and finding one meant
   reading all eight. The badge keeps its two colours and its four words —
   `walk_offboarding` reads them and mutation F3 in report 42 is about which
   one a sentence is filed under. */
.off-list { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.off-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px 0;
  border-top: 1px solid var(--line-2);
}
.off-row:first-child { border-top: 0; padding-top: 0; }
.off-row__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--fs-row);
  color: var(--ink-1);
}
.off-row__detail {
  font-size: var(--fs-meta);
  color: var(--ink-3);
  line-height: 1.55;
  max-width: 78ch;
}

/* ── K. A bare `.ui-btn` reads as a link, and five of them are mine ───────
   `.ui-btn` with no modifier is `border-color: transparent` on no fill, so
   inside `.ui-page` it inherits the anchor's copper and renders as underlined
   text — measured on `ce-before-people_offboard-wide.png` and on the
   requirement page, where "Confirm NM's requirements" is the act the whole
   screen exists for and looked like a footnote.

   THE FIX IS IN THE TEMPLATES, not here: the five are given the modifier
   that matches their weight. Changing `.ui-btn` itself would reach every
   screen in the product and two other branches. Report 118 §5. */
/* ══ END SCREENS: ce ══════════════════════════════════════════════════════ */

/* ══ THE ASK BOX ═════════════════════════════════════════════════════════════
   The home page's way into the assistant. One field and one button, sitting
   under the greeting and above the stat row, so the first thing on the page
   after the person's own name is a question they can ask.

   It is drawn as the header's search field is drawn — rail fill, card edge,
   10px — because it does the same job in the same product and two different
   treatments for one affordance would be two different products. */
.ui-ask {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 var(--space-5);
}
.ui-ask__input {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  font-family: inherit;
  font-size: var(--fs-body);
  color: var(--ink-1);
  background: var(--rail-bg);
  border: 1px solid var(--line-input);
  border-radius: 10px;
}
.ui-ask__input::placeholder { color: var(--muted-ink); }
.ui-ask__input:focus-visible {
  outline: 2px solid var(--anchor);
  outline-offset: 1px;
  border-color: var(--anchor);
}
.ui-ask__go { flex: none; }

/* ON A PHONE THE BUTTON GOES UNDER THE FIELD AND TAKES THE ROW, which is §6's
   rule for every control that matters: a 44px target beside a field that has
   already been squeezed is a target nobody hits. */
@media (max-width: 720px) {
  .ui-ask { flex-wrap: wrap; }
  .ui-ask__input { flex: 1 1 100%; }
  .ui-ask__go { flex: 1 1 100%; justify-content: center; }
}
/* ══ END THE ASK BOX ═════════════════════════════════════════════════════ */

/* An empty chevron slot: the same width, nothing drawn. See
   `dashboard/cards/my_tasks.html` — it keeps the due-pills in one column
   without putting an arrow on a row that opens nothing. */
/* IT STILL CONTAINS THE GLYPH, and that is the whole rule. `visibility:
   hidden` keeps the box an element occupies, and an EMPTY span occupies
   nothing — the first version of this rendered no character and the row was
   still 5px out of line on the deployed box. The arrow is there and unpainted,
   which is the only way the two widths are one width. */
.card-chev--empty { visibility: hidden; }
