/* 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 ───────────────────────────────────────────────────────── */
  --page-bg:     oklch(0.972 0.008 85);   /* the paper */
  --rail-bg:     oklch(0.985 0.006 85);   /* rail, tinted panels */
  --card-bg:     oklch(0.995 0.004 85);   /* cards, tiles, controls */

  --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:        oklch(0.90 0.012 85);
  /* THE CONCEPT'S MUTED INK, MOVED THREE HUNDREDTHS DARKER.
     Every quiet label in the concept is oklch(0.58 0.015 255) and lighter —
     0.60 on a tinted row, 0.66 in the footer, 0.68 on a keyboard hint. On
     this paper those measure 2.8:1 to 4.2:1, and all of them are body-size
     text: below AA, in a product where the quiet labels are the ones that
     say when a licence expires. 0.547 is the lightest value that clears
     4.5:1 on the page, the rail, a card and a tinted row alike, so ONE token
     replaces all five. The difference is not visible side by side; the
     failure was. */
  --muted-ink:   oklch(0.547 0.015 255);
  --line-1:      var(--line);
  --line-2:      oklch(0.93 0.010 85);
  --line-3:      oklch(0.88 0.012 85);
  --line-input:  oklch(0.86 0.012 85);

  /* ── Ink. Cool-leaning, which is what keeps warm paper from going muddy. */
  --ink-1:       oklch(0.26 0.025 255);
  --ink-navy:    var(--ink-1);
  --ink-2:       oklch(0.34 0.020 255);
  --steel-slate: var(--ink-2);
  --ink-3:       oklch(0.44 0.018 255);
  --ink-4:       oklch(0.54 0.015 255);

  /* ── The one dark anchor. Active nav, primary button. ─────────────── */
  --anchor:      oklch(0.26 0.025 255);
  --anchor-press: oklch(0.20 0.025 255);
  --on-anchor:   oklch(0.99 0 0);
  /* The old CTA names point here so every existing button follows. */
  --copper-cta:   var(--anchor);
  --copper-press: var(--anchor-press);

  /* ── Accent: R1's green. Links and the brand chip, never a surface. ─ */
  --accent:      oklch(0.45 0.08 155);
  --accent-hi:   oklch(0.36 0.08 155);
  --action-blue: var(--accent);
  --blue-deep:   var(--accent-hi);
  --peach:       var(--accent);
  --peach-deep:  var(--accent-hi);
  --copper-lift: var(--accent);
  --copper-deep: var(--accent-hi);

  /* ── Status. Amber is a DEADLINE, not a call to action. ───────────── */
  --warn-bg:     oklch(0.99 0.010 80);
  --warn-fg:     oklch(0.46 0.09 55);
  --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.

     Every weight used here — 600 for the greeting and page titles, 700 for
     section headings — is a real cut of Instrument Sans, which loads
     400/500/600/700. Nothing in the type is synthesised except the greeting's
     one italic word. IBM Plex Sans lost partly on this: it is loaded at
     400/500/600 only, so 700 headings would have been faked. */
  --font-display: 'Instrument Sans', Helvetica, Arial, sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;

  --fs-page-title: 46px;

  /* ── Form ─────────────────────────────────────────────────────────── */
  --radius:      14px;   /* cards, tiles */
  --radius-sm:   10px;   /* buttons, inputs */
  --radius-nav:   9px;   /* nav rows */
  --radius-pill: 999px;  /* chips, badges */

  --shadow-sm: none;
  --shadow-md: 0 12px 32px oklch(0.26 0.025 255 / 0.10);
  --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 ──────────────
   264px 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. */

.sidebar {
  width: 264px;
  background: var(--rail-bg);
  border-right: 1px solid var(--line);
  padding: 26px 18px 18px;
  gap: 28px;
}
.nav { gap: 2px; }
.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. */
.links-section { border-bottom: 0; padding: 0; overflow: visible; }
.side-group { display: flex; flex-direction: column; gap: 8px; }
.side-group-label {
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  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: 12.5px;
  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 ────────────────────────────── */
.main-col > header {
  background: var(--rail-bg);
  border-bottom: 1px solid var(--line);
  backdrop-filter: none;
  padding: 16px clamp(16px, 4vw, 40px);
}
.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. */
/* Instrument Sans is loaded without an italic axis, so this is a synthesised
   oblique — the one synthesised thing left in the type, and it earns it: 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: 12.5px; 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; }

.ui-eyebrow {
  font-weight: 400;                          /* was 500 */
  letter-spacing: 0.14em;                    /* was .08em — 1.54px at 11px */
}

.ui-stat__label { font-size: 12px; }         /* 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: 12.5px; }

.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: 700;
  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. */

.ui-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  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: #FFFFFF;
  border-color: var(--copper-cta);
}
.ui-btn--primary:hover:not(:disabled) {
  background: var(--copper-press);
  border-color: var(--copper-press);
  color: #FFFFFF;
}

.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-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  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;
}
.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. */
.ui-table td form,
.ui-table td button,
.ui-table td select,
.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(--copper-cta); }
.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: 700; }
.task-overdue { color: var(--due-past);   font-weight: 700; }
.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: 700; }

/* 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-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  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; }
  /* 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 */
  }
  .tabbar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 120;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    background: var(--rail-bg);
    border-top: var(--tabbar-rule) solid var(--line);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .tabbar__tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: var(--tabbar-tap);
    padding: 6px 4px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--ink-3);
    text-decoration: none;
  }
  .tabbar__tab .ico { width: 20px; height: 20px; color: currentColor; }
  .tabbar__tab.is-on { color: var(--ink-1); font-weight: 700; }
  /* The marker is on the TOP edge, so a thumb resting on the bar never
     covers the one thing that says where you are. */
  .tabbar__tab.is-on::before {
    content: "";
    position: absolute;
    margin-top: -62px;
    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; }
}


/* ── 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 oklch(0.91 0.012 85);
  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: oklch(0.36 0.08 155); }

/* 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: 11px;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 6px;
  background: oklch(0.94 0.01 85);
  color: oklch(0.45 0.02 255);
  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 oklch(0.94 0.01 85);
}
.card-row:hover { background: oklch(0.975 0.008 85); }
.card-row__avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: oklch(0.94 0.01 85);
  color: oklch(0.4 0.02 255);
  font-size: 11.5px;
  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: 12.5px; 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(--copper-cta); }

/* 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: 12.5px;
  font-weight: 500;
  padding: 5px 11px;
  border-radius: 999px;
  white-space: nowrap;
  background: oklch(0.945 0.01 85);
  color: oklch(0.45 0.02 255);
}
.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: oklch(0.975 0.008 85);
  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 oklch(0.92 0.012 85);
}
.card-event__month {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  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: 12.5px; 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: oklch(0.955 0.03 80);
  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-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  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: 11.5px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  background: oklch(0.99 0.01 80);
  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: oklch(0.99 0.01 80);
  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: oklch(0.99 0.01 80);
}

/* 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: oklch(0.91 0.012 85);
  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: 12px; font-weight: 500; color: var(--muted-ink); }
.ui-stat__sub { font-size: 12px; 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: #fff;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.02em;
  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: 11px;
  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. */
.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: 11px;
  font-weight: 500;
  padding: 1px 6px;
  border-radius: 5px;
  background: oklch(0.9 0.05 75);
  color: oklch(0.42 0.08 60);
}
/* 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: #fff;
}

/* 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;
}
.topsearch__form {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  max-width: 380px;
  padding: 9px 14px;
  border-radius: 10px;
  background: var(--card-bg);
  border: 1px solid var(--line);
}
/* The magnifier is drawn, not fetched: one ring and one handle, so the field
   needs no icon sprite and no extra request. */
.topsearch__form::before {
  content: "";
  flex: none;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1.5px solid var(--muted-ink);
  box-shadow: 1.5px 1.5px 0 -0.25px var(--muted-ink);
}
.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 oklch(0.45 0.08 155 / 0.12);
}
.topsearch__key {
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted-ink);
}

/* 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: oklch(0.86 0.012 85); }
.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;
  background: oklch(0.62 0.16 35);
  color: #fff;
  font-size: 10.5px;
  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: oklch(0.86 0.012 85); }
.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: 11.5px;
  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: 11px;
  font-weight: 400;
  color: var(--muted-ink);
}
.main-col > header .user-menu-caret { color: var(--muted-ink); font-size: 11px; }

/* 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. */

/* Where you are gets a dot as well as the filled pill — small, green, and the
   only spot of accent in the rail. */
.nav-item.active { justify-content: space-between; }
.nav-item__dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: oklch(0.72 0.13 145);
  flex: none;
}
.nav-item:not(.active) .nav-item__dot { display: none; }

/* 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: oklch(0.945 0.01 85); }
.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: 11.5px;
  font-weight: 400;
  color: var(--muted-ink);
}
.product-link.current {
  background: oklch(0.93 0.045 155);
  color: oklch(0.32 0.06 155);
}
.prod-here {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: oklch(0.38 0.07 155);
}

/* 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 oklch(0.92 0.012 85);
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  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: oklch(0.975 0.008 85);
  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: 12.5px; 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 oklch(0.92 0.012 85);
  font-size: 11.5px;
  color: var(--muted-ink);
}
.powered__name { font-weight: 600; color: oklch(0.4 0.02 255); }

/* ── 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: oklch(0.965 0.025 80);
  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 oklch(0.9 0.012 85);
  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 oklch(0.45 0.08 155 / 0.12);
}
/* 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: 12px;
  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: oklch(0.2 0.025 255);
  border-color: oklch(0.2 0.025 255);
  color: var(--on-anchor);
}
.ui-btn--secondary, .card-btn {
  background: var(--card-bg);
  color: oklch(0.3 0.02 255);
  border: 1px solid oklch(0.88 0.012 85);
}
.ui-btn--secondary:hover:not(:disabled), .card-btn:hover {
  background: var(--card-bg);
  border-color: oklch(0.7 0.02 255);
}
.ui-btn--sm { padding: 7px 12px; font-size: 12.5px; }

/* 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 oklch(0.91 0.012 85);
  border-radius: 16px;
}
.ui-table th {
  padding: 14px 22px;
  background: var(--card-bg);
  color: var(--muted-ink);
  border-bottom: 1px solid oklch(0.91 0.012 85);
}
.ui-table td {
  padding: 14px 22px;
  border-bottom: 1px solid oklch(0.94 0.01 85);
  font-size: 14px;
  color: oklch(0.36 0.02 255);
}
.ui-table tbody tr:hover { background: oklch(0.975 0.008 85); }
.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: 12.5px; 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 oklch(0.87 0.012 85);
  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 oklch(0.91 0.012 85);
  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: oklch(0.3 0.02 255);
  border: 1px solid oklch(0.82 0.014 85);
}
.auth-btn.auth-btn--quiet:hover { background: var(--surface-2); }
.auth-hint { font-size: 12.5px; color: var(--muted-ink); }
.auth-foot {
  margin-top: 22px;
  padding-top: 14px;
  border-top: 1px solid oklch(0.92 0.012 85);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-size: 11.5px;
  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: 11px;
  padding: 2px 8px;
  border-radius: 6px;
  background: oklch(0.94 0.01 85);
  color: oklch(0.45 0.02 255);
}
.lib-cards {
  gap: 0;
  background: var(--card-bg);
  border: 1px solid oklch(0.91 0.012 85);
  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 oklch(0.94 0.01 85); }
.lib-item-head { padding: 14px 22px; gap: 12px; }
.lib-item-head:hover { background: oklch(0.975 0.008 85); }
.lib-item-name { font-size: 14.5px; }
.lib-item-body { padding: 0 22px 18px; }
.lib-item.open { background: oklch(0.978 0.008 85); }

/* 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 oklch(0.91 0.012 85);
  border-radius: 14px;
}
.lib-upload-hint { font-size: 11.5px; 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 oklch(0.9 0.012 85);
  border-radius: 12px;
}
.lib-head input[type="search"]:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px oklch(0.45 0.08 155 / 0.12);
}
.lib-filters { gap: 8px; }
.filter {
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  background: var(--card-bg);
  border: 1px solid oklch(0.9 0.012 85);
  color: oklch(0.36 0.02 255);
}
.filter:hover { border-color: oklch(0.78 0.02 255); }
.filter.active {
  background: var(--anchor);
  border-color: var(--anchor);
  color: var(--on-anchor);
}
.filter .lib-count { font-family: var(--font-mono); font-size: 11px; opacity: 0.75; }

/* 20j. The explanatory blocks, on paper. */
.ce-stub, .ce-hint {
  background: var(--info-bg);
  border: 1px solid oklch(0.92 0.012 85);
  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: 12.5px; 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 oklch(0.91 0.012 85);
  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-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  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: 12px; }
.field-hint { font-size: 12.5px; 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-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-ink);
  border-bottom: 1px solid oklch(0.91 0.012 85);
}
table.grid td {
  padding: 12px 0;
  border-bottom: 1px solid oklch(0.94 0.01 85);
  color: oklch(0.36 0.02 255);
}
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: 11px;
  padding: 2px 8px;
  border-radius: 6px;
  background: oklch(0.94 0.01 85);
  color: oklch(0.45 0.02 255);
}
.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: 12.5px;
  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 oklch(0.91 0.012 85);
  }
  .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;
}
.msg.user .bubble a { color: oklch(0.86 0.09 155); }
.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 oklch(0.91 0.012 85);
  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 oklch(0.9 0.012 85);
  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: oklch(0.945 0.01 85); color: var(--ink-1); }
.conv-item.active { background: oklch(0.93 0.045 155); color: oklch(0.32 0.06 155); }
.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: 12.5px; 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: oklch(0.955 0.008 85); 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 oklch(0.91 0.012 85);
}

/* 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: 650; }
.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: 12.5px;
  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: 12.5px;
  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: oklch(0.975 0.008 85);
  border: 1px solid oklch(0.93 0.012 85);
  font-size: 13.5px;
}
.ce-evidence--query {
  background: oklch(0.965 0.025 80);
  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: 11.5px;
  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 oklch(0.94 0.01 85);
}
.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: oklch(0.93 0.012 85);
  overflow: hidden;
  margin-bottom: 12px;
}
.ce-bar__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
}

.ce-parts { 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 {
  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 oklch(0.96 0.008 85);
}
.ce-part: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. */
.ce-part__period {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  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 { color: oklch(0.42 0.08 155); }
.ce-part--short .ce-part__figure { color: oklch(0.45 0.10 55); font-weight: 500; }
.ce-part--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: 12.5px;
  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: oklch(0.965 0.025 80);
  border: 1px solid oklch(0.9 0.04 78);
  font-size: 12.5px;
  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. */
.ce-tallies {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.ce-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 {
  display: block;
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 1.1;
}
.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 oklch(0.94 0.008 85);
}
.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. */
.req-state {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  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: oklch(0.97 0.004 85);
  border-color: oklch(0.92 0.006 85);
}

.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: 12.5px; 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: 12.5px;
  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: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  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 oklch(0.96 0.008 85);
}
.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-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  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: 12px;
  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: 12.5px; }
