/* aichat — admin & chat design system.
   Served statically by nginx (try_files hits the file before the front controller).

   VISUAL DIRECTION (2026-09-02). The surface was cold blue-grey on near-white with flat cards, a
   centred nav pill that ignored the 88rem content shell, and one shadow doing every job. It read as a
   scaffold rather than a tool. This pass moves to WARM PAPER + INK with a single indigo accent:
     · warm neutrals (a hint of ochre) instead of blue-tinted greys, so long reading sessions sit easier
     · one accent, used only for action and current-state — never for decoration
     · hairline borders carry structure; shadow is reserved for things that actually float (menus)
     · a real type scale with negative tracking on headings, and tabular figures on every number
   Semantic colours stay distinct from the accent, so "primary action" and "healthy" can never be
   confused with one another. */

:root[data-theme="light"] { color-scheme: only light; }
:root[data-theme="dark"]  { color-scheme: only dark; }

/* LIGHT/DARK IS A STAFF SURFACE FEATURE, AND ONLY THAT.
   A visitor's chat or ticket page must never go dark because the VISITOR's operating system is dark:
   it is pasted into somebody else's site, its whole job is to paint nothing of its own, and turning it
   dark would drop a dark card into a light page. The customer opts their embed into dark themselves,
   via body.is-embed.on-dark, and that stays the only way it happens.

   ON :root AND NOT ON body, which is the part that has to be right rather than merely reasonable.
   light-dark() resolves against the color-scheme of the element the DECLARATION sits on, and every
   token is declared on :root — so `body { color-scheme: only light }` changes nothing at all. The
   first version of this rule was on body, and the public chat rendered in the console's dark palette
   for a visitor whose OS was dark. It looked correct in the file and was wrong in the browser. */
:root:not(.themed) { color-scheme: only light; }

:root {
  color-scheme: light dark;

  /* Surfaces — warm paper, not blue-grey. */
  --bg: light-dark(#f5f3f1, #1a1712);
  --surface: light-dark(#ffffff, #25221d);
  --surface-sunken: light-dark(#faf8f6, #13110c);
  --surface-raised: light-dark(#ffffff, #2d2a25);

  /* Ink.
     --ink-light and --muted-light are the SOURCE of the light values, not copies of them. An embed on
     a dark host redefines --fg and --muted so the text on the customer's page can be read — but a
     form control keeps its own LIGHT background there, so its ink has to come from somewhere the
     override has not reached. Deriving --fg from --ink-light gives it one, without a second copy of
     the value to drift. (M39 T7) */
  --ink-light: #1e1a14;
  --muted-light: #736d64;
  --fg: light-dark(var(--ink-light), #f3f0ea);
  --fg-soft: light-dark(#4b4741, #d0cbc4);
  /* 4.51:1 on --bg. #7b7883 was 3.97:1 and this is the colour of every timestamp and
     every line of help text. The dark value clears 4.5:1 on --surface-raised, which is
     the LIGHTEST dark surface and therefore the hard case — the mirror of light mode,
     where --bg is the darkest and so the hard one. */
  --muted: light-dark(var(--muted-light), #989288);

  /* Structure */
  --border: light-dark(#e6e2dd, #36322c);
  --border-strong: light-dark(#d5d2cd, #4a463f);
  /* The visible boundary of an interactive control is held to 3:1 (WCAG 1.4.11), which
     --border-strong (1.40:1) never met. Kept separate on purpose: --border-strong also
     draws hover states, dividers and panel emphasis, and darkening all of that to clear
     a control rule would repaint half the UI to fix a form. */
  --control-border: light-dark(#8e8b86, #78746d);

  /* Text that sits ON a filled accent/warn/danger chip. One name, so a palette change
     does not have to find five hard-coded #fff. */
  --ink-on-color: light-dark(#ffffff, #0f0d09);

  /* THE ACCENT RAMP — PRIVATE (M42 T7, decision (e)).
   *
   * A 50–900 ramp is how a palette becomes something people PICK from, and picking is exactly what
   * M39 removed when it cut 20 font sizes to 6. So the steps exist to DERIVE from, and no component
   * may name one: every rule in this file uses `--accent`, `-hover`, `-active`, `-soft`, `-border`,
   * and those five are defined here in terms of the ramp rather than hand-picked one at a time.
   *
   * The values are the hues that were already in use — this is a reorganisation, not a repaint, and
   * the user's instruction was *"keep the accent as it is indigo"*. `ContrastTest` proves it: the
   * same ratios pass after the change as before.
   *
   * The ramp has a second job waiting, which is the reason it is worth having at all:
   * `EmbedAppearance` takes ONE hex from a tenant and derives only the ink on it, so a branded widget
   * is half-branded — hover, soft and border stay our indigo. A ramp generated from the tenant's own
   * hue is how their chat becomes properly theirs. That is a milestone of its own; this is the shape
   * it will need.
   */
  --accent-500: light-dark(#5c63cc, #808aec);   /* the action */
  --accent-600: light-dark(#4547ad, #939eff);   /* pointed at */
  --accent-700: light-dark(#373a8e, #a8b2ff);   /* pressed */
  --accent-100: light-dark(#ecefff, #242842);   /* a soft fill behind it */
  --accent-200: light-dark(#ccd3fc, #424873);   /* its edge */

  /* One accent: action and current state only. The five names every component uses, derived. */
  --accent: var(--accent-500);
  --accent-hover: var(--accent-600);
  --accent-ink: var(--ink-on-color);
  --accent-soft: var(--accent-100);
  --accent-border: var(--accent-200);

  /* PRESSED, and it did not exist (M42 T7). There were ZERO `:active` rules in this stylesheet, so
     every control in the product looked identical the instant you clicked it — on a slow request,
     which is exactly when a person wants to know their click landed. Darker than hover, because
     pressing is more than pointing. */
  --accent-active: var(--accent-700);

  /* DISABLED, as a colour rather than `opacity: .45`.

     MEASURED, because I got this wrong first. `opacity: .45` on a filled button gave a label at
     **1.93:1** against its own fill — not a signal, just unreadable. And it was `.45` in one rule
     and `.5` in another, so two disabled buttons on one page did not match.

     My first replacement measured 3.05:1 and its own test caught it. WCAG EXEMPTS inactive
     components from any contrast requirement at all — so 3:1 would have been defensible — but a
     value that clears 4.5:1 exists and costs nothing, and "off" is carried by the pale FILL and
     `cursor: not-allowed`, not by making the words hard to read. */
  --disabled-bg: light-dark(#eceae7, #2a2723);
  --disabled-ink: light-dark(#6b6761, #a09992);
  --disabled-border: light-dark(#dcd8d3, #3a362f);

  /* INFORMATIONAL — a fact, not a warning and not the action (M42 T7). Notices borrowed `--accent`,
     which quietly says "this is the thing to do" about something that is only telling you where you
     are. A blue-grey, deliberately cooler and quieter than the indigo the one action owns. */
  --info: light-dark(#31627f, #7fb4d2);
  --info-soft: light-dark(#e8f1f6, #14262f);
  --info-border: light-dark(#bcd6e4, #2d4d5e);

  /* THE LINK, split from the action (M42 T7). They are different promises — one navigates, one
     changes something — and one token could not say which. Close to the accent on purpose: this is a
     distinction of ROLE, not a second brand colour. */
  --link: light-dark(#4a51b8, #9aa4f0);

  /* Semantics, deliberately not the accent hue. */
  --ok: light-dark(#0a7e4f, #4ea375);
  /* TEXT THAT SITS ON `--ok` (M40 T57). Not white in both: the dark theme's green is a LIGHTER
     green — it has to be, against a dark page — and white on it measures 3.1:1, under the 4.5
     normal-text floor. A near-black reads 5.2:1 there, and white reads 5.1:1 on the light theme's
     darker green. The pair flips because the background does. */
  --ok-on: light-dark(#ffffff, #08261a);
  --ok-soft: light-dark(#e5f5eb, #1a3023);
  --ok-border: light-dark(#bee0cb, #315540);
  --warn: light-dark(#9d6000, #c3863c);
  --warn-soft: light-dark(#fbeee0, #372614);
  --warn-border: light-dark(#ecd0b4, #614627);
  --danger: light-dark(#bb4746, #de736e);
  --danger-soft: light-dark(#ffebe9, #3e211f);
  --danger-border: light-dark(#fac8c4, #6c3c3a);

  /* Type scale — six steps, and every font-size in this file is one of them.
     Before M39 there were 20 distinct sizes, 14 of them between .68rem and .95rem:
     sizes chosen one rule at a time. That is why the pages read flat — everything
     was almost the same size, so nothing was emphasised. */
  --step--2: .75rem;      /* badges, pills, table meta */
  --step--1: .8125rem;
  --step-0: .9375rem;
  --step-1: 1.0625rem;
  --step-2: 1.375rem;
  --step-3: 1.875rem;

  /* LEADING, PAIRED TO THE STEPS (M39 T3). There were TWELVE distinct line-heights, and --step--1
     alone appeared with six of them (1.5, 1.45, 1.4, 1.35, 1.3, 1). A step is meant to be a
     decision; a size with a different leading every time it is used is still a size chosen one rule
     at a time. Nothing here moves by more than .05, so this is a consolidation and not a re-set.
     `line-height: 1` stays a literal where it appears: on a chip, a badge or an icon button it is a
     layout primitive meaning "add no space", not a typographic choice. */
  --leading-tight: 1.2;      /* headings, big figures */
  --leading-snug: 1.35;      /* dense rows, table cells */
  --leading-normal: 1.5;     /* UI text */
  --leading-prose: 1.6;      /* paragraphs meant to be read */
  --font-ui: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Geometry */
  --radius: 12px;
  --radius-sm: 8px;
  /* hiox.com's controls are 6px (M42 T13). Its own token rather than changing `--radius-sm`,
     which draws half the product and is not what was asked about. */
  --radius-header: 6px;
  --radius-pill: 999px;

  /* THREE ELEVATIONS, AND EACH ONE MEANS SOMETHING (M39 T5).
       · none          — in the page. A card is a hairline border and nothing else; a drop shadow on
                         something that never moves is decoration pretending to be depth.
       · --shadow-card — RAISED. Sits on the page but belongs to it: a tab that has come forward, a
                         segment of a switch that is the active one. One pixel, barely there.
       · --shadow-menu — FLOATING. Genuinely above the page and dismissable: menus, tooltips.
       · --ring        — not elevation at all. Focus. Listed here because it is the fourth thing in
                         this block that happens to be a box-shadow, which is how it got mistaken for
                         one of the others.
     The tint is a token because a drop shadow on a dark ground reads as a smudge rather than a lift,
     so dark mode inverts the card's to a faint top highlight instead of deepening it. */
  --shadow-tint: light-dark(rgba(23, 23, 28, .16), rgba(0, 0, 0, .62));
  --shadow-tint-soft: light-dark(rgba(23, 23, 28, .06), rgba(0, 0, 0, .42));
  --shadow-card-tint: light-dark(rgba(23, 23, 28, .03), rgba(255, 255, 255, .045));
  --shadow-menu: 0 8px 28px -6px var(--shadow-tint), 0 2px 6px var(--shadow-tint-soft);
  --shadow-card: 0 1px 1px var(--shadow-card-tint);
  --ring: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);

  /* A readable measure for prose and form controls — an input 1300px wide is not a form. */
  --measure: 34rem;
  --shell: 88rem;
}

* { box-sizing: border-box; }

/* `hidden` MEANS HIDDEN, EVERYWHERE (M40 T57).
   The HTML `hidden` attribute is a UA rule of the LOWEST possible weight, so ANY author rule that
   sets `display` beats it — and this codebase sets `display` on a great many things. The result each
   time is the same and is unusually hard to see: the DOM property reads `true`, `element.hidden` is
   true, every test that asks the page whether it hid the thing agrees it did, and the element is
   still painted on screen.

   It had been fixed FOUR separate times as a local override (settings fields, chat handling, settings
   groups, the review alert) before the fifth — a Continue button held back until the crawl had read
   five pages, which `a.button-ish { display: inline-block }` kept fully visible beside the notice
   explaining why you could not continue yet.

   `!important` is the right tool here and nowhere else: the whole point is to outrank author rules
   that have no idea this attribute exists. A component that wants a hidden element to occupy space
   has `visibility` or `aria-hidden` instead. */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  font: var(--step-0)/1.6 var(--font-ui);
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Numbers line up in columns; prose does not need to. */
.num, .pill, .badge, td, .kb-rail-count, .stat .value { font-variant-numeric: tabular-nums; }

h1, h2, h3 { letter-spacing: -.018em; }

a { color: var(--accent); text-underline-offset: .18em; }
a:hover { color: var(--accent-hover); }

.wrap {
  width: 100%;
  max-width: 47rem;
  padding: 0 1.25rem;
  margin: clamp(1.5rem, 4vw, 3rem) auto clamp(3rem, 6vw, 5rem);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

/* Admin pages get the wide shell; public pages keep the reading measure (layout.php picks, on $adminNav).
   A listing row carrying a title, a url, five badges and four buttons does not belong in 47rem. */
.wrap-admin { max-width: var(--shell); }

/* ---- admin layout: category rail + content (M16 T1) --------------------- */
.admin-layout {
  display: grid;
  grid-template-columns: 14rem minmax(0, 1fr);
  gap: 1.75rem;
  align-items: start;
  width: 100%;
}
.admin-main {
  min-width: 0;                 /* lets long urls/tables shrink instead of blowing out the grid */
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.kb-sidebar { position: sticky; top: 4.25rem; }
.kb-rail {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: .5rem;
}
.kb-rail-head {
  list-style: none;
  cursor: default;
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  padding: .35rem .55rem .5rem;
}
.kb-rail-head::-webkit-details-marker { display: none; }
.kb-rail-list { display: flex; flex-direction: column; gap: .1rem; }
/* The name truncates; the count never does — a number that says "12" is useless as "1…" (M42 T10). */
.kb-rail-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kb-rail-count { flex: 0 0 auto; }
.kb-rail-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  /* THE NAME IS A CATEGORY SOMEBODY TYPED, and it may be long (M42 T10). Measured at 390px a rail
     item came out 466px and took the page with it. `min-width: 0` lets the flex child shrink — a
     flex item refuses to go below its content width without it — and the name then truncates. */
  min-width: 0;
  padding: .4rem .55rem;
  border-radius: var(--radius-sm);
  color: var(--fg-soft);
  text-decoration: none;
  font-size: var(--step-0);
}
.kb-rail-item:hover { background: var(--surface-sunken); color: var(--accent); }
.kb-rail-item.is-current { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.kb-rail-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-rail-count {
  flex: 0 0 auto;
  font-size: var(--step--2);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: var(--surface-sunken);
  border-radius: var(--radius-pill);
  padding: .05rem .45rem;
}
.kb-rail-item.is-current .kb-rail-count { background: var(--surface); }

/* ---- KB subnav ---------------------------------------------------------- */
/* Sibling surfaces, always visible, above the heading — steadier than a dropdown you must reopen. */
.kb-subnav {
  display: flex;
  gap: .25rem;
  flex-wrap: wrap;
  margin-bottom: .35rem;
}
.kb-subnav-item {
  padding: .3rem .7rem;
  border-radius: var(--radius-pill);
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border: 1px solid transparent;
}
.kb-subnav-item:hover { color: var(--fg); background: var(--surface-sunken); }
.kb-subnav-item.is-current {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-border);
}

/* ---- page header -------------------------------------------------------- */
/* Title, one line of orientation, and room for a page-level action on the right. */
/* Title over orientation text. Every page's markup is <h1> then <p class="tagline">, so this stays a
   block — laying it out as a row pushed the tagline off to the right of its own heading. */
.hero { margin-bottom: .35rem; }
.hero h1 {
  margin: 0;
  font-size: var(--step-3);
  font-weight: 700;
  letter-spacing: -.028em;
  line-height: var(--leading-tight);
}
.hero .tagline {
  margin: .35rem 0 0;
  color: var(--muted);
  font-size: var(--step-0);
  /* NO max-width (user, 2026-09-21, naming /internal/inbound-mail). It was capped at 68ch as a reading
     measure — sound typography in the abstract, and wrong here: the cap is invisible on a short page and
     on a wide admin table it draws the orientation text into a narrow column under a full-width heading,
     so the page looks like two different layouts stacked. Everything else on these pages runs to the
     content width; this now does too. */
}

/* ---- cards / sections --------------------------------------------------- */
/* Structure comes from a hairline and generous padding, not from a drop shadow. */
/* `.panel` IS A CARD, AND HAD NO RULE AT ALL (M42 T15).
 *
 * Eight uses, all on Operations, and nothing in this file defined it — so those sections painted
 * nothing while every other page's sections use `.group` and are white cards. That is the whole
 * reason Operations reads as a looser, flatter page than its neighbours: half its content was in
 * cards and half was lying on the background.
 *
 * A comment elsewhere in this file says the page was "built from what is already here — .panel,
 * .listing, .badge, .help". Two of those four did not exist. **This is the fifth class today used
 * across the templates with no definition** — after `.help` (144 uses), `.mono` (27), `.empty` (22)
 * and `.primary` (3). The pattern is always the same: somebody reaches for the obvious name, it does
 * nothing, and the page looks almost right.
 *
 * It is `.group`, not a sixth card. A second card component is how there come to be two.
 */
.panel,
.group {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 1.35rem 1.5rem;
}
/* A CARD'S PADDING IS A DESKTOP LUXURY (M42 T10). At 360px, `1.5rem` either side plus the page's own
   gutter put `.group` 8px past the edge — small, and the kind of 8px that makes a whole page rock
   sideways under a thumb. The card keeps its generosity where there is room for it. */
/* A CARD IS A FLEX ITEM, and a flex item will not shrink below its content without being told it
   may (M42 T10). With a form inside, "its content" included the browser's idea of how wide an input
   wants to be. */
.panel, .group { min-width: 0; }
@media (max-width: 30rem) {
  .panel, .group { padding: 1rem .9rem; }
  /* 2.3rem either side is 74px of a 320px screen spent on nothing (M42 T10). */
  .slide-body { padding: 1.2rem 1rem; }
}
.panel > h2, .group > h2 {
  margin: 0 0 1rem;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: .5rem;
}
.group > h2 + .tagline { margin-top: -.6rem; }
.group.readonly { background: var(--surface-sunken); }
.group + .group { margin-top: 0; }

/* A card that folds. Used where a page's primary content is a LIST and the creation forms are
   occasional — they should not push the list below the fold every single visit. */
details.group > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
details.group > summary::-webkit-details-marker { display: none; }
details.group > summary::after {
  content: "";
  width: .5rem; height: .5rem;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .15s ease;
}
details.group[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
details.group > summary:hover { color: var(--fg); }
details.group[open] > summary { margin-bottom: 1.1rem; }

/* Two creation forms side by side inside the folded panel. */
/* EVERY auto-fit GRID IN THIS FILE USES `minmax(min(Xrem, 100%), 1fr)` (M42 T10).
   `minmax(21rem, 1fr)` means the track is NEVER smaller than 21rem — 336px — so on a 320px screen
   the grid is wider than the window and the whole page slides sideways. Measured on Categories:
   a card 336px wide inside a 296px `main`. `min(Xrem, 100%)` makes the floor "21rem, or all the
   room there is, whichever is less", which is what the fixed value was always trying to say.
   Six grids carried the same latent fault; all six are fixed the same way. */
.add-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr)); gap: 1.5rem; }
.add-grid > section { min-width: 0; }
.add-grid h3 {
  margin: 0 0 .8rem;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--fg);
}

/* ---- form fields -------------------------------------------------------- */
.field { margin-bottom: 1.15rem; }
.field:last-child { margin-bottom: 0; }
.field > label {
  display: block;
  margin-bottom: .4rem;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
}
.field .help {
  margin: .4rem 0 0;
  font-size: var(--step--1);
  color: var(--muted);
  max-width: var(--measure);
}

/* `search`, `tel` and `date` were missing from this list, so an input of those types fell back to the
   browser's own control: a 21px-tall box with a 2px inset border and no radius, sitting beside fields
   that have none of those things. The settings filter was the first one on an admin page and it looked
   exactly as out of place as that sounds. Listing types rather than using a bare `input` selector is
   deliberate — checkboxes and radios must NOT get this treatment. */
input[type="text"], input[type="url"], input[type="password"], input[type="email"], input[type="number"],
input[type="search"], input[type="tel"], input[type="date"],
select, textarea {
  width: 100%;
  max-width: var(--measure);      /* a 1300px-wide text input is not a form */
  padding: .55rem .7rem;
  font: inherit;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  transition: border-color .12s ease, box-shadow .12s ease;
}
textarea { max-width: none; resize: vertical; line-height: var(--leading-normal); }

/* 16px ON TOUCH, AND THIS IS A BUG FIX RATHER THAN A PREFERENCE (M39 T3).
   iOS Safari ZOOMS THE WHOLE PAGE when a field smaller than 16px takes focus, and does not zoom
   back out afterwards — the visitor is left on a magnified page they have to pinch their way out
   of, mid-form. Our body step is .9375rem = 15px and controls inherit it, so EVERY text input,
   select and textarea in the product did this: the admin's search boxes, the settings form, and the
   customer's own ticket form.

   It was known, and it was handled ONCE — the chat composer carried a hand-written `font-size: 1rem`
   with a comment explaining exactly this. One field was protected and forty were not, which is what
   a fix living in a single rule instead of in the system looks like.

   Scoped to `pointer: coarse` so nothing on a desktop moves: the zoom only exists where there is a
   soft keyboard. 1rem rather than var(--step-0) deliberately — this number answers to the browser's
   threshold, not to our scale, and pinning it to a token means a future scale change silently
   reintroduces the bug. */
@media (pointer: coarse) {
  input[type="text"], input[type="url"], input[type="password"], input[type="email"],
  input[type="number"], input[type="search"], input[type="tel"], input[type="date"],
  select, textarea {
    /* !important, and this is the case it exists for. The rule above selects on element type, so
       ANY later class-scoped rule outranks it — `.kb-bulk-actions select` (13px) and `.inline
       select` (15px) both did, and both kept zooming after the fix looked complete on two other
       pages. The alternative is auditing every component rule that touches a control, for ever,
       against a threshold none of their authors will be thinking about. This is a floor imposed by
       browser behaviour, not a style choice, and a floor that any component can quietly drop below
       is not a floor. */
    font-size: 16px !important;
  }
}
input:hover, select:hover, textarea:hover { border-color: var(--muted); }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}
::placeholder { color: color-mix(in oklab, var(--muted) 72%, transparent); }

select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%237b7883' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .75rem center;
  padding-right: 2.1rem;
}
/* A FILE INPUT CARRIES THE BROWSER'S OWN BUTTON and sizes itself to it — about 290px in Chrome, which
   is wider than a 320px screen once the page has any padding (M42 T10). It is the one control whose
   intrinsic width nobody here chose. */
input[type="file"] { max-width: 100%; }

/* A CONTROL MUST NOT BE THE REASON A PAGE IS WIDER THAN THE SCREEN (M42 T10).
   An `<input>` has an intrinsic size of its own — roughly twenty characters — and that is its
   MIN-CONTENT width, which `width: 100%` does not override. A card holding one therefore could not
   shrink below it: measured at 320px, the `.group` on Categories came out 336px inside a 296px
   `main`. `min-width: 0` is what lets `width: 100%` mean 100% of whatever room there actually is. */
input, select, textarea { min-width: 0; }

input[type="checkbox"] {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--accent);
  vertical-align: -2px;
}

/* A form control should be about as wide as the value it holds. Only the listing filter bar (which is a
   grid of its own) and the instruction editor run full width. */
.add-form input[type="text"], .add-form input[type="url"] { max-width: 26rem; }
.filter-form input, .filter-form select,
.src-actions select, .kb-bulk select { max-width: none; }

/* ---- buttons ------------------------------------------------------------ */
/* One primary style, one quiet style, one destructive style. Nothing else. */
/* `a.button` — an anchor that IS the primary action. The stylesheet had `button` and `a.ghost` and no
   way to say "a link that looks like the main button", so `class="button"` on an anchor rendered as
   plain underlined text: the most important control on the customer's own page, styled as the least
   important thing on it. */
/* THREE WEIGHTS, AND EVERYTHING ELSE IS A RENAME (M42 T3).
   Measured across the stylesheet, eleven button-ish classes rendered as four distinct looks and three
   genuinely different KINDS. The weights are: the one action a page exists for (this rule), the
   ordinary ones (`.ghost`), and the quiet/destructive ones (`.ghost.quiet`, `.danger`).

   `.primary` is listed here because three templates reach for it and it HAD NO RULE — it rendered
   correctly only because the bare element is already the primary. A class that works by accident is
   a class that stops working when the accident does.

   `.kb-toolbar-action` was this same look under a ninth name, with 0.8px of different padding. */
/* `a.ghost` IS IN THIS LIST because eleven templates write it, and without the base shape it got
   the ghost COLOURS and none of the padding, radius or type — a ghost link and a ghost button did
   not match. Measured side by side, which is the only way that shows (M42 T3). */
button, .kb-toolbar-action, a.button, button.primary, a.primary, a.ghost {
  font: inherit;
  font-weight: 600;
  padding: .55rem 1rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  cursor: pointer;
  line-height: var(--leading-snug);
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
button:hover, .kb-toolbar-action:hover, a.primary:hover { background: var(--accent-hover); }
/* PRESSED (M42 T7). The product had no `:active` rule at all, so a click looked like nothing until
   the page changed — and on a slow request that is the whole wait. `translateY` as well as the
   colour, because the movement is what reads as "pressed" at a glance. */
button:active, .kb-toolbar-action:active, a.button:active {
  background: var(--accent-active);
  transform: translateY(1px);
}
@media (prefers-reduced-motion: reduce) {
  button:active, .kb-toolbar-action:active, a.button:active { transform: none; }
}
button:focus-visible, .kb-toolbar-action:focus-visible, a.primary:focus-visible { outline: none; box-shadow: var(--ring); }
/* DISABLED IS A COLOUR, NOT A FADE (M42 T7). `opacity: .45` dragged the label under the 4.5:1
   contrast floor — it made the control hard to READ rather than obviously off — and it was `.45`
   here and `.5` in the bulk-actions rule, so two disabled buttons on one page did not match. */
button:disabled, button:disabled:hover, button:disabled:active {
  background: var(--disabled-bg);
  color: var(--disabled-ink);
  border-color: var(--disabled-border);
  cursor: not-allowed;
  transform: none;
}

a.button { display: inline-block; text-decoration: none; text-align: center; }
/* ONE GHOST, ONE BORDER (M42 T3). There were two rules for this — this one using `--border-strong`
   and a later `button.ghost` using `--control-border` — so a ghost LINK and a ghost BUTTON did not
   match each other, and the later one won for buttons. The third time in this milestone that a
   second rule further down the file quietly replaced the first.

   `--control-border` is the right survivor, and not by seniority: it exists because the visible
   boundary of an interactive control is held to 3:1 (WCAG 1.4.11), which `--border-strong` has never
   met at 1.40:1. A link shaped like a control is a control. */
button.ghost, a.ghost, a.button-ish {
  background: var(--surface);
  color: var(--fg-soft);
  border-color: var(--control-border);
}
button.ghost:hover, a.ghost:hover {
  background: var(--surface-sunken);
  border-color: var(--muted);
  color: var(--fg);
}

button.danger {
  color: var(--danger);
  background: var(--surface);
  border-color: var(--danger-border);
}
button.danger:hover { background: var(--danger-soft); border-color: var(--danger); }

/* ---- connectivity actions ---------------------------------------------- */
.test-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: .9rem 1.25rem;
  margin-top: 1rem;
}
.test-actions .inline { display: flex; align-items: center; gap: .6rem; }
.test-actions label {
  font-size: var(--step--1);
  color: var(--muted);
  align-self: center;
  flex: 0 0 auto;
  white-space: nowrap;      /* it is a toolbar label; wrapping it to three lines reads as a broken control */
}
.test-actions select { width: auto; min-width: 12rem; }

/* ---- result + alert panels --------------------------------------------- */
.test-result {
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  padding: .9rem 1.1rem;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
.test-result.ok { border-left-color: var(--ok); background: var(--ok-soft); }
.test-result.fail { border-left-color: var(--danger); background: var(--danger-soft); }
.test-result strong { display: inline-block; margin-right: .5rem; }
.test-result .detail { margin: .6rem 0 0; color: var(--fg-soft); }
.test-result .detail.err { color: var(--danger); font-family: var(--font-mono); font-size: var(--step-0); }

.errors {
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  /* The same shape as every other alert (M42 T13) — only the tone differs. */
  border-radius: var(--radius-header);
  padding: .8rem 1.1rem;
  color: var(--danger);
}
.errors ul { margin: .5rem 0 0; padding-left: 1.1rem; }

/* Row actions are secondary to the row itself: smaller, quieter, and only Delete carries colour. */
.src-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-top: .55rem; }
.src-actions .inline { display: flex; align-items: center; gap: .35rem; }
.src-actions button, .src-actions a.ghost {
  padding: .33rem .7rem;
  font-size: var(--step--1);
  font-weight: 600;
}
.src-actions button:not(.danger) {
  background: var(--surface);
  color: var(--fg-soft);
  border-color: var(--border-strong);
}
.src-actions button:not(.danger):hover {
  background: var(--surface-sunken);
  border-color: var(--muted);
  color: var(--fg);
}
.src-actions select {
  width: auto;
  min-width: 8rem;
  padding: .3rem 1.9rem .3rem .55rem;
  font-size: var(--step--1);
}
.src-actions a.ghost {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* Semantic signal badges. Colour carries the meaning so the glyph doesn't have to. */
.badge.pill-ok, .pill.pill-ok { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-border); }
.badge.pill-warn, .pill.pill-warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-border); }
/* Past its service target (M18 T2). The only red in a queue row, so it means one thing. */
.badge.pill-bad, .pill.pill-bad { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }
.src-signals a.badge:hover { border-color: currentColor; }
/* This one carries a sentence of extraction notes, not a one-word state, so it is the only badge
   allowed to wrap — nowrap pushed it past the viewport at 390px. */
.src-extract {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent-border);
  white-space: normal;
  max-width: 100%;
  align-items: flex-start;
}
.src-help {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.05em; height: 1.05em; border-radius: 50%;
  background: currentColor; color: var(--surface);
  font-size: .72em; font-weight: 700; cursor: help;
}
.src-extract .src-help { background: var(--accent); color: var(--ink-on-color); }

/* Title and url */
.src-main { display: flex; flex-direction: column; gap: .15rem; }
.src-title { font-weight: 600; font-size: var(--step-0); }
.src-title a { text-decoration: none; }
.src-title a:hover { text-decoration: underline; }
.src-url {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.src-meta, .src-signals {
  display: flex; align-items: center; gap: .35rem; flex-wrap: wrap;
  margin: .45rem 0 0;
}
.src-signals { margin-top: .3rem; }

/* ---- settings accordion ------------------------------------------------- */
/* Settings is a long page of rarely-touched groups. One open at a time keeps the whole map visible. */
.settings-group > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg);
}
.settings-group > summary::-webkit-details-marker { display: none; }
.settings-group > summary::after {
  content: "";
  margin-left: auto;
  width: .5rem; height: .5rem;
  border-right: 1.6px solid var(--muted);
  border-bottom: 1.6px solid var(--muted);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .15s ease;
}
.settings-group[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.settings-group[open] > summary { margin-bottom: 1.1rem; padding-bottom: .9rem; border-bottom: 1px solid var(--border); }
.settings-group:not([open]):hover { border-color: var(--border-strong); }

/* A label that must be READ OUT but not seen — the classic clip pattern rather than `display:none`
   or `font-size:0`, both of which take it out of the accessibility tree as well as off the screen.
   (M07 T5 made exactly that mistake once; this is the rule that fixes it properly.) */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ---- how an embedded surface looks (M21 T9) ------------------------------ */
/* Set per tenant as custom properties on <body>; absent when nothing is configured, which is why every
   rule here falls back to what the page already looked like. `transparent` is a real choice for the
   INLINE mount — a background that is not there cannot fail to match the page around it. */
body[style*="--embed-bg"] { background: var(--embed-bg); }
body[style*="--embed-ink"],
body[style*="--embed-ink"] .chat-msg,
body[style*="--embed-ink"] .hero h1 { color: var(--embed-ink); }
/* The accent is always set, because the launcher and the send button are ours to draw.

   THE TICKET SURFACE WAS NOT IN THIS LIST. `--embed-accent` reached its <body> perfectly well — the
   rule simply never named `.stack-form`, so a tenant who had set their colour got a BLUE chat and an
   INDIGO "Send" on their ticket form, side by side on one page of their own site. The setting's own
   help text says "the launcher button and the send button"; on this surface it was not true. Found
   by embedding both snippets on a mock customer site and looking — the only place the two surfaces
   are ever seen together, since each one alone looks entirely consistent.

   `.stack-form > button` and nothing wider. Those four — "Send me a code", "Continue", "Send",
   "Send reply" — are the primary action of their form. The quiet ones ("Sign out", "Reopen this
   ticket", "I no longer need help", "Search") live in `.inline-form` and `.ticket-search` and stay
   quiet: painting every button in the brand colour gives a page four equal primary actions, which
   is the same as having none. */
body[style*="--embed-accent"] .chat-form button[type="submit"],
body[style*="--embed-accent"] .prechat-actions button[type="submit"],
body[style*="--embed-accent"] .stack-form > button[type="submit"] {
  background: var(--embed-accent);
  border-color: var(--embed-accent);
  color: var(--embed-accent-ink);
}

/* ---- an embedded page is not a page (M21 T11) ---------------------------- */
/* The complaint: "customer dont feel like iframe, its like page display". What gave it away was never
   the content — it was the SHELL around it. A standalone visit is a whole viewport: it stretches to
   100vh so a short page still fills the window, and it carries outer padding so the content is not
   against the glass. Inside a frame both are wrong. 100vh is the FRAME's height, so the content can
   never be shorter than the box it was given, and the padding draws a visible inset margin that reads
   as the edge of a widget. */
body.is-embed {
  min-height: 0;
  /* The host page's own background shows through wherever a tenant chose `transparent`; where they did
     not, this is still the background they configured. Either way the frame element itself is
     transparent now, so nothing of ours paints outside what the content occupies. */
  background: transparent;
}
body.is-embed > .wrap {
  /* Full bleed. The margin that centres a page inside a window is the same margin that makes a frame
     look like a picture hung on a wall. */
  max-width: none;
  width: 100%;
  padding: 0;
  margin: 0;
  /* And no tint of our own. This was the last thing giving the frame away: our page background is a
     pale blue-grey, and on a white host page it drew a visible band exactly the width and height of
     the iframe. Transparent means the host's own background IS the background. */
  background: transparent;
}
/* Which leaves one real hazard: our text colour is dark, and a dark host page would put dark on dark.
   The widget measures the background it is being dropped onto and says which it is, so the embedded
   page can pick ink that can be read on it (M21 T11). A tenant who sets `embed.background` explicitly
   overrides both — they have told us what is behind the frame and we no longer have to guess. */
/* The VARIABLES, not the selectors. Overriding `color` on a handful of elements looked right in one
   screenshot and was wrong everywhere else — every label in the app already says `color: var(--fg-soft)`,
   which is an explicit dark grey that inheritance cannot reach. Redefining the three ink tokens keeps the
   whole page's relationships intact and simply moves them to the other end of the scale. */
/* THESE SIX LITERALS STAY LITERAL, AND IT IS A DECISION (M39 T7).
   Everything below sits on the CUSTOMER'S page, whose background we do not know and cannot measure
   — so unlike every other colour in this file, no contrast test can be written for them and no
   palette of ours can be correct for them. Pulling them onto the console's dark tokens would look
   tidy and would quietly tie how an embed reads on somebody else's site to a choice made about our
   admin. The ink that lands on OUR OWN surfaces inside the embed — form controls, placeholders, the
   captcha — does follow the theme, via --ink-light and --muted-light above. That is the line: our
   surfaces are themed, their page is not ours to theme. */
body.is-embed.on-dark {
  --fg: #f4f6fb;
  --fg-soft: #d5dce8;
  --muted: #a3aebd;
  --border: #39414f;
  color: var(--fg);
}
body.is-embed.on-dark a { color: #9fc0ff; }
body.is-embed.on-dark a:hover { color: #c2d6ff; }
/* BUT NOT INSIDE THE CONTROLS. A form field keeps its own light background on a dark page — that is
   what a text box looks like everywhere — so the lightened ink would be pale grey on white, and the
   placeholders went from quiet to barely there. Caught by looking at the screenshot, not by the rule
   that produced them. */
body.is-embed.on-dark input,
body.is-embed.on-dark select,
body.is-embed.on-dark textarea { color: var(--ink-light); }
body.is-embed.on-dark ::placeholder { color: var(--muted-light); opacity: 1; }
/* AND NOT THE CAPTCHA. Its strokes are `currentColor` on a light card, so lightening the ink drew the
   puzzle in near-white on white and made the form impossible to complete — a change of colour that
   silently removed the only way to submit it. Nothing but looking at the rendered page found this. */
body.is-embed.on-dark .captcha-art { color: var(--ink-light); }
/* A configured background belongs to the CONTENT, not to the viewport: painted on body it fills the
   whole frame including the space auto-height is about to remove. */
/* A CHOSEN BACKGROUND MAKES IT A PANEL, AND A PANEL HAS TO LOOK LIKE ONE (M40 T53, reported live:
   *"embed chat widget visible not good. No padding and margin. improper background set (top portion
   only background color updated)"*).

   Everything above is right for the DEFAULT, which is transparent: no tint, no padding, no forced
   height, so the widget reads as part of the host's page rather than a picture hung on their wall.

   Setting `embed.background` is the opposite decision, and T36 half-implemented it. The colour reached
   `.wrap` only, and measured on a 380x560 frame that produced a coloured block 356 wide and 350 tall in
   the top-left corner: `body` keeps `padding: 0 12px` from the base sheet, so a strip down each side
   stayed the host's colour; `body.is-embed` had already forced `background: transparent`, so the body
   itself painted nothing; and with `min-height: 0` the content stopped 210px short of the frame. The
   user's words for it — "top portion only" — were exact.

   So: paint the BODY, fill the frame, and put the content back off the glass. Choosing a colour is
   choosing to have a surface, and a surface with text against its edge looks broken however good the
   colour is. */
body.is-embed[style*="--embed-bg"] {
  background: var(--embed-bg);
  /* THE FRAME'S OWN HEIGHT, and `vh` is the right unit for once. The comment above rejects `100vh`
     for the transparent case and is right: nothing of ours should paint past the content when the
     content is invisible. Here the opposite holds — the colour must cover the whole box — and inside
     an iframe `100vh` IS the frame, which is exactly the measurement wanted.
     `min-height: 100%` was the first attempt and does nothing: it resolves against `html`, whose
     height is auto, so the body stopped 178px short of a 560px frame. */
  min-height: 100dvh;
}
@supports not (min-height: 100dvh) {
  body.is-embed[style*="--embed-bg"] { min-height: 100vh; }
}
body.is-embed[style*="--embed-bg"] > .wrap {
  /* Inherits the body's colour rather than repainting it, so there is one surface and not two that
     could ever disagree. */
  background: transparent;
  padding: 1rem 0.25rem;
}

/* ---- an embedded CHAT fills the frame it was given (M40 T65) --------------
   T53 made the chosen BACKGROUND cover the whole frame. It did not make the CONTENT do so, and on a
   382x632 launcher panel that left **160px of empty colour under the composer** — a quarter of the
   panel. Measured on the live widget embedded on a customer page, not eyeballed: body 630, content
   470.

   A chat is ALWAYS in a fixed-height frame — `widget.js` gives the floating panel `70vh/max 640px`
   and the inline mount `data-height` or 520px, and its own comment says a chat is deliberately not
   auto-height because a growing transcript would walk the composer off the host's page. So there is
   always a box to fill, in both mounts, whether or not a colour was chosen.

   THE TICKET VIEW IS THE OPPOSITE and must not be caught by this: it measures itself and posts its
   height up to the host, so forcing it to fill the viewport would make it report the full frame for
   ever and grow once, permanently. That is why this is scoped to `.page-chat` — before M40 T65 the
   two surfaces reached the stylesheet indistinguishable. */
body.is-embed > .wrap.page-chat {
  /* A DEFINITE HEIGHT, not a minimum — and that distinction is the whole fix. `min-height` leaves
     the column content-driven, so `flex: 1` below has no free space to hand out and the children
     keep their natural heights: with thirty messages in the transcript the composer was pushed to
     699px inside a 632px frame and the visitor had to scroll the PANEL to reach the box they type
     in. Caught by putting a real conversation in it; a greeting fits either way and proves
     nothing. */
  height: 100dvh;
  display: flex;
  flex-direction: column;
}
@supports not (height: 100dvh) {
  body.is-embed > .wrap.page-chat { height: 100vh; }
}
/* The transcript takes the slack and the composer sits at the bottom. `min-height: 0` on both is the
   part that is easy to miss: a flex item's default `min-height: auto` refuses to shrink below its
   content, so without it a long conversation pushes the composer out of the frame instead of
   scrolling inside it — which is the very failure the fixed height exists to prevent. */
body.is-embed > .wrap.page-chat > .chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
body.is-embed > .wrap.page-chat > .chat > .chat-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* AND THE HEADING IS A PANEL'S, NOT A PAGE'S (M40 T65). Measured at 380x632: the title rendered at
   30px, wrapped to two lines, and the block above the transcript came to 231px — **37% of the panel
   spent before the conversation starts**, on a surface whose whole job is the conversation. The
   standalone `/chat` page keeps the big heading; it is a page and has the room.

   WIDENED FROM `.page-chat` TO EVERY EMBEDDED PANEL. The reasoning above is about being embedded,
   not about chat, and scoping it to the chat left the TICKET panel at 30px: on a 292px frame
   "Hosteasy — your support tickets" ran to three lines and 108px, directly underneath the host
   page's own "Your tickets" heading saying the same thing. Two surfaces of one product, embedded
   side by side, disagreeing about how big a heading is. */
body.is-embed > .wrap h1 {
  font-size: var(--step-1);
  line-height: var(--leading-snug);
  margin: 0;
}
body.is-embed > .wrap .tagline {
  font-size: var(--step--1);
  margin: .3rem 0 0;
}
body.is-embed > .wrap.page-chat .chat-head { margin-bottom: .35rem; }


/* ---- asking about something else (M04 T14) ------------------------------ */
/* The hint belongs UNDER the button, not beside it: side by side it wrapped into the line below and
   read as a sentence the button was sitting inside. */
.chat-new {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .25rem;
  margin: .9rem 0 .2rem;
  padding-top: .9rem;
  border-top: 1px solid var(--border);
}
.chat-new .hint { color: var(--muted); font-size: var(--step--1); }

/* ---- before the chat starts (M04 T11) ----------------------------------- */
/* A short form above the chat, not a wall in front of it: same card as the chat below so it reads as
   the first step of one thing rather than a gate on another. */
.chat-prechat {
  margin-bottom: 1rem;
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.chat-prechat > h2 { margin: 0 0 .25rem; font-size: var(--step-0); }
.chat-prechat > .help { margin: 0 0 .9rem; }
.prechat-form { display: grid; gap: .8rem; max-width: 30rem; }
.prechat-form label { display: flex; flex-direction: column; gap: .25rem; font-size: var(--step-0); }
.prechat-form input { width: 100%; }
/* A field we filled in for them is not one they should be fighting with. */
.prechat-form input[readonly] { background: var(--surface-sunken); color: var(--muted); }
.prechat-form .hint { color: var(--muted); font-size: var(--step--1); }
/* The question, on the same form (M04 T12). Given the weight the contact fields do not have: it is the
   thing the visitor came to type, and the fields above it are the part they are allowed to skip. */
.prechat-ask textarea {
  width: 100%;
  min-height: 4.6rem;
  padding: .55rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  line-height: var(--leading-normal);
  resize: vertical;
}
.prechat-ask textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.prechat-ask { margin-top: .2rem; }
.prechat-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

/* ---- guided setup (M32 T1) ---------------------------------------------- */
/* The first screen a new install meets. Four steps, numbered, each saying what is TRUE now rather than
   only whether it is finished — a step that says "not done" and nothing else gives nobody anything to
   act on. Numbers on the left so the eye can run down them; the body is what you read. */
.setup-count { margin: 0 0 1.1rem; color: var(--muted); font-size: var(--step--1); }
.setup-count strong { color: var(--fg); }

.setup-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
.setup-step {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  gap: .25rem 1rem;
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
/* A done step steps back rather than disappearing: it is still a thing you may want to go and change,
   and a list that shrinks as you work loses the sense of how far along you are. */
.setup-step.is-done { background: var(--surface-sunken); border-color: var(--border); }
.setup-step.is-done .setup-step-num { background: var(--ok-soft); color: var(--ok); border-color: transparent; }
.setup-step.is-working { border-left: 3px solid var(--warn); }

.setup-step-num {
  display: flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  font-weight: 700; font-size: var(--step-0); color: var(--muted);
  background: var(--surface-sunken);
}
.setup-step-body { min-width: 0; }
.setup-step-body > h2 {
  margin: .15rem 0 .25rem;
  font-size: var(--step-0);
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
}
.setup-step-blurb { margin: 0 0 .35rem; }
.setup-step-detail { margin: 0 0 .75rem; }
.setup-step-actions { margin: 0; }
/* Step one is typed in place: one field and one button, on one line where there is room for it. */
.setup-website-form { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: 0 0 .4rem; }
.setup-website-form input[type="text"] { flex: 1 1 18rem; min-width: 0; }
.setup-step-hint { margin: 0; }

/* What we read off their own site, offered for correction. Set apart from the steps, because it is a
   question rather than a task — and it disappears the moment it is answered or skipped. */
.setup-brand { border-color: var(--border-strong); margin-bottom: 1.4rem; }
.setup-brand > h2 { margin-top: 0; }
.setup-brand .stack-form { display: grid; gap: .85rem; max-width: 34rem; }
.setup-brand label { display: flex; flex-direction: column; gap: .25rem; font-size: var(--step-0); }

/* The line to paste, on the step that gives it. Set in the mono face and boxed, because it is a thing
   to be copied exactly rather than read. */
.setup-snippet { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 0 0 .4rem; }
/* WRAPS, never clips. This is a line somebody has to copy EXACTLY, and at phone width both snippets
   were cut off mid-URL with nothing to say so — `<script src="http://aichat.funfl` and then the box
   ended. Horizontal scrolling inside a code block is barely discoverable on a touch screen and
   impossible to notice on a screenshot; wrapping keeps every character selectable. `break-all` because
   the long token is a URL, which has no spaces to break at. */
.setup-snippet .secret,
.setup-ticket-embed .secret {
  min-width: 0;
  flex: 1 1 100%;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* Progress is a COUNT, and it is quiet: it reports, it does not demand. */
.setup-crawl { margin: .7rem 0 0; }
.setup-step .notice { margin: .8rem 0 0; }
.setup-step .notice p { margin: 0 0 .6rem; }

/* The ticket code, on the step that offers it. Set apart from the step's own text, because it is a
   thing to take away rather than a thing to read. */
.setup-ticket-embed { margin-top: 1rem; padding-top: .9rem; border-top: 1px dashed var(--border); }
.setup-ticket-embed h3 { margin: 0 0 .3rem; font-size: var(--step--1); }
.setup-ticket-embed .help { margin: 0 0 .7rem; max-width: 68ch; }
.setup-ticket-embed .secret { display: block; }
/* Two buttons on a step: the thing to do, and leaving it for later. The second is quiet but PRESENT —
   this is the step most likely to strand somebody, and a hidden way out is not a way out. */
.setup-step-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

.setup-next { margin: 1.1rem 0 1.6rem; }
.setup-dismiss { margin-top: 1.6rem; }
.setup-dismiss .help { margin: 0 0 .6rem; max-width: 68ch; }

/* A phone, because an operator setting this up on site is not at a desk. */
@media (max-width: 30rem) {
  .setup-step { grid-template-columns: 1fr; }
  .setup-step-num { width: 1.9rem; height: 1.9rem; font-size: var(--step--1); }
}

/* ---- settings: the explanation behind a "?" (M07 T9) --------------------- */
/* 115 settings each carrying a paragraph is a page somebody scrolls past rather than reads. The label
   is what you scan; the explanation is what you read once. `<details>` needs no JavaScript and is
   keyboard-reachable for free — a tooltip on a touch screen is text nobody can reach. */
/* The "?" itself: a small circle beside the label. `position: relative` is what the bubble below
   anchors to, so one declaration block rather than two — the first version had two and the second
   overrode `position` back to static, which put every tooltip in the top-left corner of the page. */
.field-help {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: .4rem;
  vertical-align: middle;
  width: 1.15rem;
  height: 1.15rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--step--2);
  font-weight: 700;
  line-height: 1;
  color: var(--muted);
  background: var(--surface);
  cursor: help;
  user-select: none;
}
.field-help:hover, .field-help:focus { color: var(--fg); border-color: var(--fg-soft); }
.field-help:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The bubble. Absolutely positioned so opening one never moves the form underneath it — a page that
   reflows when you ask what something means is a page that loses your place. */
.field-tip {
  position: absolute;
  left: 0;
  top: calc(100% + .35rem);
  z-index: 20;
  display: none;
  width: max-content;
  max-width: min(38rem, 60vw);
  padding: .6rem .75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-menu);
  font-size: var(--step--1);
  font-weight: 400;
  line-height: var(--leading-normal);
  color: var(--fg-soft);
  text-align: left;
  white-space: normal;
  cursor: auto;
}
/* Hover for a mouse, focus for a keyboard — and the same focus for a tap, because a touch screen has
   no hover and would otherwise be left with text nobody can reach. */
.field-help:hover > .field-tip,
.field-help:focus > .field-tip,
.field-help:focus-within > .field-tip { display: block; }
/* Near the right edge it would otherwise run off the page. */
.page-internal-settings .group .field:last-child .field-tip { top: auto; bottom: calc(100% + .35rem); }

/* ---- settings: start here, gaps, advanced (M07 T5) ---------------------- */
/* The page used to lead with a DIAGNOSTIC and then the two most technical groups in the product. These
   three blocks are what leads it now: what is missing, what to set first, and a line saying where the
   internals begin. */

/* Not an error and not a success — a list of things this install has not been told. Amber rather than
   red, because none of them is wrong, and the panel disappears entirely once they are answered. */
.setup-gaps {
  margin-bottom: 1.4rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--warn-border);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  background: var(--warn-soft);
}
.setup-gaps h2 { margin: 0 0 .35rem; font-size: var(--step-0); display: flex; align-items: center; gap: .5rem; }
.setup-gaps > .help { margin: 0 0 .75rem; }
.setup-gap-list { display: grid; gap: .7rem; }
/* Label on its own line above the explanation: the label is the scannable part and the detail is the
   part you read once. Side by side, neither reads well at a glance. */
.setup-gap-list > li { display: grid; gap: .15rem; }
.setup-gap-list > li > a { font-weight: 600; }
.setup-gap-list .help { margin: 0; }

/* The shortcut card. Raised rather than sunken — it is the thing to act on, not context. */
.group.starter { border-color: var(--border-strong); }
.group.starter > h2 { margin-top: 0; }
/* Two columns at most. Three put a 24rem label beside its control in a 23rem column, which is how the
   inputs ended up detached from their labels and clipped against the card's edge — see the override
   below, which is the actual fix; this just stops the columns getting that narrow again. */
/* ONE COLUMN, OR TWO. Never three: `auto-fit` gave a 1440px screen three 418px cells, and the field
   layout inside needs more than that — see the override below. An explicit count is also the only way
   to know what a wide monitor gets without owning one. */
.starter-fields { display: grid; gap: 1.25rem 2rem; grid-template-columns: 1fr; }
@media (min-width: 60rem) {
  .starter-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.starter-fields > .field { border: 0; padding: 0; }


/* A setting that lives in the card above, shown here so it is still findable where somebody looks for
   it — quieter than an editable row, because it is not one. */
.field-elsewhere { opacity: .85; }
/* `.label-text` carries `white-space: nowrap` for the toolbar labels it was written for, and that is
   right there and wrong here: these rows are a setting's NAME followed by its key, and at 390px the
   pair ran 25px past the window and gave every phone a horizontal scrollbar. Scoped rather than
   changed at source, because the toolbars still want their labels on one line. */
.field-elsewhere .label-text {
  font-weight: 600;
  display: block;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* A setting key is a long unbroken token — `assignment.ticket_batch` — and at 390px it pushed the page
   wider than the window. It wraps at the dots now rather than forcing a horizontal scrollbar on every
   phone that opens this page. */
.field-key { overflow-wrap: anywhere; word-break: break-word; max-width: 100%; }
.field-elsewhere .help { margin-top: .2rem; }

/* The line where the internals begin. A heading, not a collapsed box: the groups under it stay exactly
   as open-able as the ones above, which is the whole point of demoting rather than hiding. */
.settings-advanced-divider {
  margin: 2.2rem 0 1.1rem;
  padding-top: 1.3rem;
  border-top: 2px solid var(--border);
}
.settings-advanced-divider h2 {
  margin: 0 0 .3rem;
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
}
.settings-advanced-divider .help { margin: 0; max-width: 68ch; }
.settings-group.is-advanced > summary { color: var(--muted); }

/* The probe, now inside Provider rather than first on the page. Set off from the fields above it, since
   it is an action and they are values. */
.connectivity-inline {
  margin-top: 1.4rem;
  padding-top: 1.1rem;
  border-top: 1px dashed var(--border);
}
.connectivity-inline h3 { margin: 0 0 .2rem; font-size: var(--step--1); }
.connectivity-inline .help { margin: 0; }
.connectivity-inline .test-actions { margin-top: .8rem; }
/* The forms the buttons above belong to carry only a token. Nothing to show, and a stray empty block
   below the save button would be a puzzle. */
.probe-form { display: contents; }

.form-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.form-actions .help { margin: 0; }

/* ---- settings: two-column fields --------------------------------------- */
/* A settings page is a long list of one-line decisions. Stacked, each field costs three rows and the
   right two-thirds of an 88rem shell sits empty; side by side, the label and its explanation read as the
   question and the control reads as the answer. */
@media (min-width: 62rem) {
  .page-internal-settings .group > form > .field,
  .page-internal-settings .group .field {
    display: grid;
    grid-template-columns: minmax(0, 24rem) minmax(0, 1fr);
    column-gap: 2.5rem;
    align-items: start;
    padding-bottom: 1.15rem;
    border-bottom: 1px solid var(--border);
  }
  .page-internal-settings .group .field:last-child { border-bottom: 0; padding-bottom: 0; }
  /* The `hidden` attribute is honoured globally near the top of this sheet — a local override here
     was the first of five copies of the same fix. */
  .page-internal-settings .group .field > label { grid-column: 1; grid-row: 1; margin-bottom: .2rem; }
  .page-internal-settings .group .field > .help { grid-column: 1; grid-row: 2; margin-top: 0; }
  /* THE "?" TAKES THE PLACE THE PARAGRAPH HAD (M07 T9). Without this it is not `.help`, so the grid
     never placed it — it fell into an auto row underneath the control, a long way from the label it
     belongs to, which is not where anybody looks for a question mark and not what "tooltip" means. */
  .page-internal-settings .group .field > .field-help { grid-column: 1; grid-row: 2; margin: .1rem 0 0; }
  .page-internal-settings .group .field > input,
  .page-internal-settings .group .field > select,
  .page-internal-settings .group .field > textarea {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: start;
  }
  /* The connectivity probe is a toolbar, not a settings row (its controls sit outside .field). */
  .page-internal-settings .connectivity .field { display: flex; align-items: center; gap: .6rem; border: 0; padding: 0; }
}

/* THE START-HERE CARD DOES NOT USE THE PAGE'S TWO-COLUMN FIELD LAYOUT.
   That layout is a 24rem label beside its control, sized for the full width of the page. The card is a
   `.group`, so its fields inherited it inside cells a third that wide — and the control was squeezed to
   **nothing**: 24px at 1200px, and 0px at 1440px, where the card took three columns. The controls
   overlapped their labels at every width except the one I happened to screenshot.
   Stacked here, which is what a short field wants anyway.
   FOUR classes and AFTER the block above, both deliberately: the first attempt at this rule had three
   classes and sat earlier in the file, so it lost the cascade in silence while reading exactly like the
   fix. Whether a rule applies is a question for the browser's computed style, not for the eye. */
.page-internal-settings .group.starter .starter-fields > .field {
  display: block;
  padding-bottom: 0;
  border-bottom: 0;
}
.page-internal-settings .group.starter .starter-fields > .field > label {
  display: block;
  grid-column: auto;
  grid-row: auto;
  margin-bottom: .35rem;
}
.page-internal-settings .group.starter .starter-fields > .field > input,
.page-internal-settings .group.starter .starter-fields > .field > select,
.page-internal-settings .group.starter .starter-fields > .field > textarea {
  grid-column: auto;
  grid-row: auto;
  display: block;
  width: 100%;
  max-width: 20rem;
}
/* A checkbox sits at its natural size; stretching one to 20rem makes a tick the size of a text box. */
.page-internal-settings .group.starter .starter-fields > .field > input[type="checkbox"] {
  width: auto;
  max-width: none;
}
.page-internal-settings .group.starter .starter-fields > .field > .help {
  grid-column: auto;
  grid-row: auto;
  margin-top: .4rem;
}

/* ---- overview tiles ----------------------------------------------------- */
/* The number leads; the label explains it. A dashboard is scanned, not read. */
.status-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(9.5rem, 100%), 1fr));
  gap: .75rem;
  padding: 0;
  background: none;
  border: 0;
}
.tile {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .85rem .95rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.tile-value {
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: var(--leading-tight);
  color: var(--fg);
}
.tile-text { font-size: var(--step-1); }
.tile-label { font-size: var(--step--1); color: var(--muted); }
/* Outstanding work reads differently from a healthy zero. */
.tile-warn { background: var(--warn-soft); border-color: var(--warn-border); }
.tile-warn .tile-value { color: var(--warn); }
.tile-warn .tile-label { color: color-mix(in oklab, var(--warn) 78%, var(--fg)); }
.tile-health .tile-value { display: flex; align-items: center; gap: .4rem; min-height: 1.65rem; }
.tile-health .dot { width: .6rem; height: .6rem; }

/* ---- paired creation cards ---------------------------------------------- */
/* Two ways to do one job belong on one row, not stacked as two full-width cards. */
.create-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
  gap: 1rem;
  align-items: start;
}
.create-row .group { padding: 1.1rem 1.2rem; }
.create-row .panel > h2, .group > h2 { margin-bottom: .8rem; }
.create-row .field { margin-bottom: .85rem; }
.create-row .add-form { display: block; }

/* ---- add-pages panel ---------------------------------------------------- */
/* The toggle reads as a BUTTON, because it is the page's one creation affordance and a grey uppercase
   label did not look clickable. The two routes in behind it are genuinely different jobs — one fetches a
   URL now, the other runs a background crawl — so they are tinted apart rather than left as twin forms. */
.add-panel { padding: 0; border: 0; background: none; box-shadow: none; }
.add-panel > summary.add-toggle {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  width: auto;
  padding: .55rem 1.05rem;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
  list-style: none;
  margin: 0;
}
.add-panel > summary.add-toggle::-webkit-details-marker { display: none; }
.add-panel > summary.add-toggle::before { content: "+"; font-size: 1.05em; line-height: 1; }
.add-panel > summary.add-toggle::after { content: none; }
.add-panel > summary.add-toggle:hover { background: var(--accent-hover); }
.add-panel[open] > summary.add-toggle { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.add-panel[open] > summary.add-toggle::before { content: "×"; }

.add-panel .add-grid {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: 0 var(--radius) var(--radius) var(--radius);
  overflow: hidden;
  gap: 0;
}
.add-option { padding: 1.25rem 1.4rem; }
.add-option + .add-option { border-left: 1px solid var(--border); }
/* Tinted apart so the two routes read as different jobs at a glance, not as twin forms: one fetches a
   single URL now, the other kicks off a background crawl. */
.add-option-page { background: var(--surface); }
.add-option-crawl { background: color-mix(in oklab, var(--accent) 6%, var(--surface)); }
.add-option-crawl .help { color: color-mix(in oklab, var(--accent) 45%, var(--fg-soft)); }
.add-option h3 { margin-bottom: .2rem; }
.add-option > .help { margin: 0 0 1rem; }
.add-option input, .add-option select { max-width: none; }

/* ---- background job panels (crawl / ingest) ---------------------------- */
/* Progress readout, not a section. Slim, muted, and it gets out of the way of the listing. */
.crawl-status { padding: .8rem 1rem; background: var(--surface-sunken); }
.crawl-status > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--muted);
}
.crawl-status > summary::-webkit-details-marker { display: none; }
.crawl-status > summary:hover { color: var(--fg); }
.crawl-status[open] > summary { margin-bottom: .7rem; }
.crawl-status .help { margin-top: .6rem; font-size: var(--step--1); color: var(--muted); }
.job-list { display: flex; flex-direction: column; gap: .3rem; }
.job-row {
  display: flex;
  align-items: center;
  gap: .55rem;
  font-size: var(--step--1);
}
.job-url {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.job-state { margin-left: auto; color: var(--muted); white-space: nowrap; }

/* ---- small tokens: badge / pill ---------------------------------------- */
/* A listing row can carry five of these. They are annotations, so they whisper; only STATE gets colour. */
.badge, .pill {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .1rem .5rem;
  border-radius: var(--radius-pill);
  font-size: var(--step--2);
  font-weight: 600;
  line-height: var(--leading-prose);
  white-space: nowrap;
  /* It may be a pill, but it may not be wider than the thing it sits in (M42 T10). */
  max-width: 100%;
}
/* A BADGE HOLDING A SENTENCE IS NOT A BADGE, and one of them is: *"Not on the page right now — they
   will see this when they come back"* — 454px of it, `nowrap`, on a 320px screen. `nowrap` is right
   for "on the page" and wrong for a sentence, and the difference is how much room there is.

   So the pill shape is kept wherever it fits and given up where it does not. Two lines on a phone
   beats a page that slides under a thumb. */
@media (max-width: 34rem) {
  .badge, .pill { white-space: normal; }
}
/* AND THE SAME THING HAPPENS ON A DESKTOP, which the viewport query above cannot see (M28 T21).
   `/internal/review` at 961px: a signal reading "Content changed and the answer with it — regenerated
   draft a…" measured **624px wide in a 588px card**, `nowrap`, so the whole page got a horizontal
   scrollbar. The window was 961px — wide by any measure — and the CARD was narrow, because that page
   carries a sidebar. Exactly the mistake the row layout above had to be rewritten to stop making:
   what matters is the room this element has, not the size of the window.

   `max-width: 100%` was already here and cannot help on its own — with `nowrap` the text simply
   overflows the box it has been capped to.

   Scoped to the badge rows of a listing, which is where sentence-length signals actually appear, and
   harmless to the short ones: `normal` only changes anything for text that does not fit, so "enabled"
   and "1 chunk" keep their pill exactly as they are. */
.src-meta .badge, .src-meta .pill,
.src-signals .badge, .src-signals .pill { white-space: normal; }
.badge {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
}
.pill { background: var(--accent-soft); color: var(--accent); border: 1px solid transparent; }

/* ---- definition lists (meta / readonly panels) ------------------------- */
.meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .55rem 1.5rem;
  margin: 0;
}
.meta dt { color: var(--muted); }
.meta dd { margin: 0; color: var(--fg-soft); }
.meta a { color: var(--accent); text-decoration: none; }
.meta a:hover { text-decoration: underline; }
/* TWO COLUMNS NEED TWO COLUMNS' WIDTH (M28 T18). `max-content 1fr` gives the label whatever it asks
   for and the value the rest — which on a phone is the rest of 276px, and the thread's own `dd`
   cells measured 454px inside it. A definition list is the one layout that should simply stack when
   there is no room, so it does.

   `pointer: coarse` is wrong here and a width query is right: this is about how much room there is,
   not what is pointing at it — a narrow window on a desktop has the same problem. */
@media (max-width: 34rem) {
  .meta {
    /* `minmax(0, 1fr)`, NOT `1fr` (M42 T10). `1fr` is `minmax(auto, 1fr)`, and an `auto` minimum is
       MIN-CONTENT — so one unbreakable value blows the track out past its own container. Measured
       at 320px: the `dl` was correctly 225px wide and its single column was 454px. Stacking the
       list was not enough on its own, and the numbers said so. */
    grid-template-columns: minmax(0, 1fr);
    gap: .15rem 0;
  }
  .meta dt { margin-top: .45rem; }
  /* Both halves: a label can be long too, and a badge inside a value will not break on its own. */
  .meta dt, .meta dd { overflow-wrap: anywhere; min-width: 0; }
}
.readonly .help { margin: -.4rem 0 1rem; color: var(--muted); font-size: var(--step-0); }
/* CODE, THROUGH THE TOKEN (M42 T2, user: *"code display portions"*).

   `--font-mono` exists and this rule restated the stack by hand — as did `.secret`, with a third
   copy. Three places, three literals, one of which can drift from the other two. `.88em` and `5px`
   were likewise the only raw values in a file whose type and radius are otherwise entirely tokens. */
code {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .05rem .35rem;
}

/* ---- admin header (shared across /internal pages) ----------------------- */
.admin-header {
  /* Full-bleed bar. The inner content is aligned to the same 88rem shell the page uses, via padding
     rather than an extra wrapper element — so the nav sits over the content, not floating mid-viewport. */
  width: 100%;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem 1.75rem;
  /* MEASURED AGAINST hiox.com, where this tool is mainly used (M42 T13): its bar is 65px of SOLID
     white with a hairline and no shadow. Ours was 54px of 85%-opaque white with a blur behind it —
     a different object, and somebody moving between the two sees two products. */
  padding-block: .8rem;
  padding-inline: max(1.25rem, calc((100% - var(--shell)) / 2 + 1.25rem));
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 20;
}
.admin-brand {
  font-weight: 700;
  font-size: var(--step-1);
  letter-spacing: -.02em;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
  margin-right: .25rem;
}
.admin-brand span {
  font-weight: 500;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: .05rem .5rem;
  font-size: var(--step--2);
  letter-spacing: .06em;
  text-transform: uppercase;
  margin-left: .45rem;
  vertical-align: middle;
}
.admin-header-nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .2rem .1rem;
  /* hiox.com sets its nav at 16px/500 in slate. Ours was 13px/600 in a warm grey — small enough and
     different enough in colour that the two headers do not read as the same company. `--step-0` is
     the product's nearest step (15px); weight comes down to 500 because 600 at that size shouts. */
  font-size: var(--step-0);
  font-weight: 500;
}
/* Nav items are quiet targets that light up; the current one is inked, not merely bolder. */
.admin-header-nav a,
.admin-menu > summary {
  /* `--fg-soft`, not `--muted`: hiox's nav is near-ink (`#1e2d3d`), not grey. A nav is the one place
     a link should not whisper — it is the way around the product. */
  color: var(--fg-soft);
  text-decoration: none;
  white-space: nowrap;
  /* Eleven items plus a brand plus two controls have to share one row, the way hiox.com does.
     Raising the type from 13px to 15px spent the slack, so the padding gives some back — and it
     has to be MEASURED with the alert badge loaded, because that badge arrives by fetch and is
     what tips the row over. Measured before it landed, the bar looked fine and was not. */
  padding: .42rem .52rem;
  border-radius: var(--radius-header);
  transition: background-color .12s ease, color .12s ease;
}
.admin-header-nav a:hover,
.admin-menu > summary:hover { color: var(--fg); background: var(--surface-sunken); }
.admin-header-nav > [aria-current="page"],
.admin-menu > summary[aria-current="page"] {
  color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}

.admin-menu { position: relative; display: inline-block; }
.admin-menu > summary { list-style: none; cursor: pointer; }
.admin-menu > summary::-webkit-details-marker { display: none; }

/* THE MENUS ON THE RIGHT OPEN LEFTWARDS. `left: 0` is right for a menu near the left of the header
   and wrong for the last one in it, which is where the 51px came from (M42 T10). */
.admin-menu:nth-last-child(-n+3) > .admin-submenu { left: auto; right: 0; }
.admin-submenu {
  position: absolute;
  top: calc(100% + .45rem);
  left: 0;
  /* A MENU THAT OPENS OFF THE SCREEN IS A MENU WITH ITEMS NOBODY CAN REACH (M42 T10).
     Measured on a tablet: opening Review put the panel 51px past the right edge. `min-width: 12rem`
     with `left: 0` guarantees a width and promises nothing about where that width ends up.

     The cap is `100vw` minus a thumb's worth of gutter; `min-width` becomes a `min()` so the two
     cannot contradict each other on a phone, where a hard 12rem floor is the whole problem. */
  max-width: calc(100vw - 1.5rem);
  min-width: min(12rem, calc(100vw - 1.5rem));
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .35rem;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);   /* it floats, so it earns a shadow */
  z-index: 30;
}
.admin-submenu a { padding: .4rem .6rem; border-radius: var(--radius-sm); font-weight: 600; }
.admin-submenu a:hover { background: var(--surface-sunken); color: var(--fg); }
.admin-submenu [aria-current="page"] { background: var(--accent-soft); color: var(--accent); }

/* ---- admin data tables (Review pages, M12) ------------------------------ */
/* Restored and restyled in the 2026-09-02 design pass — this block was collateral damage when the header
   above it was rewritten, which flattened every table on the Review surfaces. */
/* THE FOOTER (M42 T7, decision (h)). Quiet by construction: it is the least important row on the
   page and must look it. Same shell width as the header and the content, so the three line up. */
.admin-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem 1.25rem;
  margin-top: 3rem;
  padding-block: 1.1rem;
  padding-inline: max(1.25rem, calc((100% - var(--shell)) / 2 + 1.25rem));
  border-top: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--step--1);
}
.admin-footer a { color: var(--muted); text-decoration: none; }
.admin-footer a:hover { color: var(--accent); text-decoration: underline; }
.admin-footer .mono { margin-left: auto; }
@media (max-width: 34rem) {
  /* `margin-left: auto` would strand the version on its own line against the right edge. */
  .admin-footer .mono { margin-left: 0; }
}
/* THE CARD LISTS SHARE A BASE (M42 T2).

   Fourteen of them, each opening with the same three declarations — `list-style: none; margin;
   padding` — because each was written on a different day and nobody had a base to extend. That is
   the real finding, and it is NOT the one my own audit wrote down: I called them "twenty containers
   for one job", which counted right and read wrong. Thirteen are `<ul>` CARD LISTS, which is a
   legitimate second pattern beside the table, not a table in disguise. Corrected when I did the work.

   Each list keeps whatever is genuinely its own below — a grid, a gap, a margin that differs — and
   stops restating the reset.
*/
.job-list,
.hub-list,
.cat-list,
.pick-list,
.src-list,
.quarantine-list,
.ms-list,
.request-list,
.task-list,
.file-list,
.setup-gap-list,
.kb-rail-list,
.ticket-list,
.ticket-cards {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A TABLE TOO WIDE FOR THE SCREEN SCROLLS ITSELF (M42 T10), rather than taking the page with it.
   Nine tables already used this and the twenty that did not are exactly the pages that scrolled
   sideways on a phone — so this is the product's own answer, applied everywhere rather than where
   somebody remembered.

   `-webkit-overflow-scrolling` for momentum on iOS, and a touch hint: on a coarse pointer the right
   edge fades, which is the only affordance a scrollable region gets when there is no scrollbar to
   see. `scrollbar-gutter` is deliberately NOT used — it would reserve a channel on every table that
   does not need one. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
@media (pointer: coarse) {
  .table-wrap {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
  }
  /* Once it is scrolled to the end there is nothing more to hint at, and the fade would just be
     eating the last column. */
  .table-wrap:not(:hover) { mask-position: 0 0; }
}
/* ONE TABLE LOOK (M42 T2, user: *"common table layout (color, font, background, …)"*).

   There were two, and they were subtly different: `.admin-table` padded cells at `.6rem` and spaced
   its headings at `.06em`, `.listing` used `.55rem` and `.04em`. Nobody would name the difference and
   everybody would feel it — a table on History and a table on the agent console were not the same
   object. `.admin-table`'s values win because they are on more pages; `.listing` keeps only what is
   genuinely ITS OWN below, which is a queue's column behaviour and not a look. */
.admin-table, .listing { width: 100%; border-collapse: collapse; font-size: var(--step--1); }
.admin-table th, .admin-table td, .listing th, .listing td {
  text-align: left;
  padding: .6rem .7rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.admin-table thead th, .listing thead th {
  font-size: var(--step--2);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}
.admin-table tbody tr:last-child td, .listing tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover { background: var(--surface-sunken); }
.admin-table .num { text-align: right; }
.admin-table .nowrap { white-space: nowrap; }
/* MONOSPACE, EVERYWHERE AND NOT ONLY IN A TABLE (M42 T2).

   `class="mono"` appears 27 times across the templates and was defined ONLY inside `.admin-table` —
   so the same class rendered as code in a table and as ordinary body text everywhere else. The same
   shape as `.help`: a class used everywhere, defined for one place. */
.mono, .ip { font-family: var(--font-mono); font-size: var(--step--1); color: var(--fg-soft); }

/* Incoming mail (M31 T6): the reason a message was refused sits UNDER its badge rather than beside it —
   it is the sentence an operator reads, and inline it pushes the badge and the ticket link off the row. */
/* `fixed` so the <colgroup> is AUTHORITATIVE. With the default `auto`, column widths are only hints and
   content wins — which gave the subject 82px and the empty ticket column 755px of a 1318px table. */
.inbound-table { table-layout: fixed; }
.inbound-table td .help { display: block; margin-top: .2rem; font-size: var(--step--1); line-height: var(--leading-snug); }
.inbound-table td { vertical-align: top; }
.inbound-table td.mono { overflow-wrap: anywhere; }

/* ---- email templates (M34) ---------------------------------------------- */
/* A form somebody WRITES in, not one they fill in: the message is the field that matters and it gets the
   full width, while the four short fields share a row above it. Laid out as a grid rather than a wrapping
   flex row, because flex put "Message" beside "Subject" and the hint for one under the other. */
.template-form { display: grid; gap: .85rem 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 80ch; }
.template-form > label { display: flex; flex-direction: column; gap: .25rem; font-size: var(--step-0); min-width: 0; }
.template-form > label.template-body,
.template-form > .template-actions,
.template-form > label.inline-check { grid-column: 1 / -1; }
.template-form textarea { width: 100%; min-height: 12rem; font-family: var(--font-mono); font-size: var(--step--1); line-height: var(--leading-normal); }
.template-form .hint { color: var(--muted); font-size: var(--step--1); line-height: var(--leading-snug); }
.template-form label.inline-check { flex-direction: row; align-items: center; gap: .45rem; }
.template-actions { display: flex; gap: .5rem; align-items: center; }

/* The attachment is ONE THING made of three controls — the file it has, a way to drop it, a way to
   replace it — so it is boxed as one rather than left as three loose rows that read as unrelated. It
   spans the full width for the same reason "Message" does: a file input with a long name in it is wider
   than half a form, and a truncated filename is the field nobody can check. */
.template-form > .template-file { grid-column: 1 / -1; display: flex; flex-direction: column;
  gap: .5rem; padding: .75rem .9rem; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-sunken); font-size: var(--step-0); }
.template-file > .label-text { font-weight: 600; }
.template-file-current { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; margin: 0; }
/* The filename is the thing being identified, so it is set in the face that will not re-space it. */
.file-chip { display: inline-block; max-width: 38ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: bottom; padding: .15rem .45rem; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--border); font-family: var(--font-mono);
  font-size: var(--step--1); }
.template-file-pick { display: flex; flex-direction: column; gap: .25rem; }
.template-file-pick input[type="file"] { font-size: var(--step--1); }
.template-file .hint { margin: 0; }

/* The preview is a RESULT, so it is set apart from the form that produced it rather than flowing on. */
.template-preview { margin-top: 1.1rem; padding: .9rem 1.1rem; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface-sunken); max-width: 80ch; }
.template-preview h3 { margin: 0 0 .5rem; font-size: var(--step--1); text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); }
.template-preview .preview-subject { margin: 0 0 .5rem; }
.template-preview .preview-body { margin: 0; white-space: pre-wrap; font-family: var(--font-mono);
  font-size: var(--step--1); line-height: var(--leading-normal); }
.placeholder-help { margin-top: 1rem; }
.placeholder-help summary { cursor: pointer; color: var(--muted); font-size: var(--step--1); }

/* Same lesson as the incoming-mail table: with `auto`, a <colgroup> is a hint and content wins — which
   gave "an agent picks it" a five-line column beside a subject squeezed into three. */
.group table.admin-table { table-layout: fixed; }
.admin-table .version-body { overflow-wrap: anywhere; }
tr.is-off td { opacity: .6; }

/* The template picker in the composer, beside AI Answer — the same shape, and neither one sends. */
.agent-template { display: flex; align-items: flex-end; gap: .6rem; flex-wrap: wrap; margin-top: .6rem; }
.agent-template .template-pick { display: flex; flex-direction: column; gap: .2rem; font-size: var(--step--1); }

/* Files on a thread (M37 T4). A refused one is listed as plainly as a kept one — the fact that it was
   refused is the useful half, and hiding it is how a customer sends the same invoice three times. */
.attachment-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.attachment-list li { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.attachment-refused { text-decoration: line-through; color: var(--muted); }

/* ── FIVE MODIFIERS THAT EXISTED IN THE MARKUP AND DID NOTHING (M42 T16) ──────────────────────────
   Found by checking every class the templates write against every class this file defines. Each was
   decided on its own merits rather than styled for the sake of it: a modifier that need not differ
   should be deleted, not given a rule to justify itself. */

/* A list inside a TABLE CELL, which is the only place this variant is used. A cell is a tight space
   and the default list is sized for a panel. */
.attachment-list.compact { gap: .2rem; font-size: var(--step--1); }

/* THE OFFERS BAND IS THE ONE THING ON THE CONSOLE WITH A CLOCK RUNNING, and it looked like every
   other card. An agent scanning the page has seconds to notice it; "same as everything else" is the
   wrong answer for the only item with a deadline. The accent is this product's "act on this". */
.group.offered-to-me {
  border-color: var(--accent-border);
  background: var(--accent-soft);
}

/* "NOBODY IS ON THIS DESK — nothing can be assigned here and no agent will ever be offered it." That
   is a consequence, not a note, and it was rendering in the same grey as the help text beside it. */
.help.assign-none {
  color: var(--warn);
  background: var(--warn-soft);
  border: 1px solid var(--warn-border);
  border-radius: var(--radius-header);
  padding: .55rem .75rem;
}
.help.assign-none a { color: inherit; }

/* Somebody else is working this thread. Informational and nothing to do about it, so it takes the
   informational tone rather than a warning one — the distinction `--info` was added for. */
.hint.held-by-other { color: var(--info); }

/* `.group.letterhead` was NOT given a rule. It is an ordinary card holding the letterhead form, and
   the tab above it already says which of the two things you are editing. A card variant that differs
   from its neighbour for no reason is how a page stops having a grammar. The class stays in the
   markup as a hook; it does not need a look. */

/* Files on a customer's own ticket (M36 T4). "You sent" / "We sent" leads each row, because which
   direction a file went is the first thing the reader needs and a list that merges them is worse than
   either alone. A refused one is not hidden — it is the row that most needs reading. */
.ticket-files { margin: 1.25rem 0; }
.ticket-files h2 { font-size: var(--step--1); text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); margin: 0 0 .5rem; }
.file-list { display: grid; gap: .4rem; }
.file-list li { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; font-size: var(--step-0); }
.file-way { font-size: var(--step--2); text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
  min-width: 4.5rem; }
.file-size { color: var(--muted); font-size: var(--step--1); }
.file-refused { text-decoration: line-through; color: var(--muted); }
.file-why { color: var(--danger); font-size: var(--step--1); }

/* The ticket list's sort and pager (2026-09-17). Both sit ON the heading or under the table they belong
   to — a control floating between two groups is one nobody can tell the owner of. */
.queue-order { margin-left: auto; font-size: var(--step--1); font-weight: 400; text-transform: none;
  letter-spacing: 0; display: inline-flex; gap: .6rem; }
.queue-order .is-current { color: var(--fg); font-weight: 600; }
.queue-order a { color: var(--muted); }
.pager { display: flex; gap: .4rem; align-items: baseline; flex-wrap: wrap; margin-top: .75rem;
  padding-top: .6rem; border-top: 1px solid var(--border); font-size: var(--step--1); }
.pager a, .pager .is-current { min-width: 1.8rem; text-align: center; padding: .15rem .4rem;
  border-radius: var(--radius-sm); }
.pager .is-current { background: var(--surface-sunken); font-weight: 600; }
.pager .help { margin-left: auto; }

/* "Check now" sits ON the Mailboxes heading, because that is the thing it acts on — a button floating
   above a card is one nobody can tell the owner of. */
.group > h2 .fetch-now { margin-left: auto; font-weight: 400; text-transform: none; letter-spacing: 0; }

/* A human's judgement on a queue row (M28 T7). Three colours and no more: urgent reads as a warning,
   high as a note, and `low` deliberately RECEDES — it is the one an agent should be able to skip over. */
.badge.urgency-urgent { background: var(--danger-soft); color: var(--danger); }
.badge.urgency-high   { background: var(--warn-soft); color: var(--warn); }
.badge.urgency-low    { background: var(--surface-sunken); color: var(--muted); }
.urgency-form { display: inline-flex; align-items: center; }
.urgency-form .urgency-label { display: inline-flex; align-items: center; gap: .4rem; margin: 0; }
.urgency-form .label-text { font-size: var(--step--1); color: var(--muted); }
.admin-table .muted-cell { color: var(--muted); }
/* SEARCH RESULTS (M28 T19). Six columns, and only one of them has anything to say about how wide it
   should be: the subject is a sentence and the rest are identifiers, states and clocks. So the
   identifiers take what they need and the subject takes the remainder — the same shape the queue
   tables were given in T18, for the same reason. */
.search-results th, .search-results td { white-space: nowrap; }
.search-results th:nth-child(4), .search-results td:nth-child(4) {
  white-space: normal;
  width: 100%;
  min-width: 14rem;
}
/* A LABEL ABOUT OUR SEARCH, NOT ABOUT THEIR THREAD. It sat inline after the subject, reading as part
   of what the customer wrote — "Domain renewal matched: name or subject". As a column under its own
   heading it is plainly ours, so it can also be quiet. */
.search-results .matched-on { color: var(--muted); font-size: var(--step--2); }
.admin-table .gap-fix { display: flex; gap: .35rem; flex-wrap: wrap; align-items: center; margin: 0; }
/* The message column is the one that should absorb the spare width. */
.admin-table td:last-child { width: 100%; color: var(--fg-soft); }
.admin-table a { text-decoration: none; font-weight: 600; }
.admin-table a:hover { text-decoration: underline; }

/* ---- admin nav ---------------------------------------------------------- */
.admin-nav {
  display: flex;
  gap: 1.25rem;
  font-size: var(--step-0);
  font-weight: 600;
  margin-bottom: -.25rem;
}
.admin-nav a { color: var(--muted); text-decoration: none; }
.admin-nav a:hover { color: var(--accent); }
.admin-nav [aria-current="page"] { color: var(--fg); }

/* ---- category taxonomy admin ------------------------------------------- */
.add-form { display: flex; align-items: end; gap: .9rem; flex-wrap: wrap; }
.add-form .field { margin-bottom: 0; flex: 1 1 16rem; }

/* AN ALERT IS NOT AN ACTION (M42 T13). `.notice` borrowed `--accent-soft`, which is the colour this
   product uses for "this is the thing to do" — so a panel merely telling you where you are wore the
   same blue as the button you are meant to press. `--info` exists for exactly this and was added in
   Task 8 without a caller.

   The whole alert family now shares one shape — same radius, same hairline weight, same padding — so
   `notice`, `notice-warn`, `notice-ok` and `errors` read as four tones of one thing rather than four
   components. The radius matches the header's, which is where this pass started. */
.notice {
  background: var(--info-soft);
  border: 1px solid var(--info-border);
  border-radius: var(--radius-header);
  padding: .8rem 1.1rem;
  color: var(--fg-soft);
}

/* A switched-off subsystem (M13 T6): warm, not alarming — nothing is broken, it is just off. */
.notice-warn {
  background: color-mix(in oklab, var(--warn) 10%, var(--surface));
  border-color: color-mix(in oklab, var(--warn) 34%, var(--border));
  margin-bottom: 1.4rem;
}

/* A finished background job OFFERING a refresh (M16 T8). The page never reloads itself — an admin with
   text in a form must not lose it because a crawl happened to finish — so this bar appears in place and
   waits for a click. Laid out as: message · Show · Dismiss. */
.notice-ok {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
  margin: 0 0 1.2rem;
  background: color-mix(in oklab, var(--ok) 10%, var(--surface));
  border-color: color-mix(in oklab, var(--ok) 34%, var(--border));
}
.notice-ok a { font-weight: 600; }
.notice-ok button { margin-left: auto; padding: .3rem .7rem; font-size: var(--step--1); }

/* Bootstrap-from-URL suggestion checklist (M03 T10). */
.suggest-picks { margin-top: 1.2rem; padding-top: 1.1rem; border-top: 1px solid var(--border); }
.suggest-lead { margin: 0 0 .7rem; color: var(--fg-soft); }
.pick-list { margin: 0 0 1rem; display: flex; flex-wrap: wrap; gap: .5rem .9rem; }
.pick-row label { display: inline-flex; align-items: center; gap: .45rem; cursor: pointer; }
.pick-row { background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: .3rem .8rem; }


.cat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: .85rem 0;
  border-top: 1px solid var(--border);
}
.cat-row:first-child { border-top: none; padding-top: 0; }
.cat-meta { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.cat-name { font-weight: 600; }
/* THE ALARMING STATE WAS THE ONLY ONE WITH NO COLOUR (M42 T16).

   `.badge.state-off` and `.badge.pill-ok` were defined; `.badge.state-warn` never was — so on the
   Operations page **stranded** (a desk with work on it and nobody to do it, the state that cost
   this install five days in September) rendered in the same neutral grey as a plain annotation,
   while *nobody assigned* was red and *covered* was green. Measured side by side: `badge
   state-warn` came out pixel-identical to a bare `badge`.

   A modifier that does nothing is worse than no modifier: the markup SAYS this is a warning, the
   page says it is routine, and the one reading it believes the page. Used in five templates. */
.badge.state-warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-border); }
/* And `quiet` on a badge, said out loud: it was falling back to the plain badge by accident, which
   happened to be right for a benign state. Now it is right on purpose. */
.badge.quiet { background: transparent; color: var(--muted); border-color: var(--border); }
.badge.state-off { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }
/* `min-width: 0`, for the reason `.group` and the raise form have it (M42 T10): a flex item will
   not shrink below its content unless told it may, and this one holds a text field and two
   buttons. Measured at 320px: 285px of actions inside a 225px row. */
.cat-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; min-width: 0; }
/* `max-width`, NOT `width` (M42 T10). A hard 11rem in a wrapping row is 176px that cannot give
   any of it back, and at 320px the row carried it past the edge. */
.cat-actions input[type="text"] { width: 11rem; max-width: 100%; padding: .4rem .55rem; }

/* Per-category instruction editor (M03 T7) — a full-width disclosure under the row. */
.cat-instruction { flex-basis: 100%; }
.cat-instruction > summary {
  cursor: pointer;
  width: fit-content;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--muted);
  list-style-position: inside;
}
.cat-instruction > summary:hover { color: var(--accent); }
.cat-instruction > summary .pill { margin-left: .35rem; }
.instruction-form { margin-top: .7rem; display: flex; flex-direction: column; align-items: flex-start; gap: .6rem; }
.instruction-form textarea { min-height: 4.5rem; }
/* ---- service targets (M18 T1) ------------------------------------------
   One row per desk: two small number boxes and their unit, wrapping to the help line on a narrow screen.
   The inputs are deliberately narrow — a target is three digits, and a full-width box invites a date. */
.targets-form { margin-top: .7rem; display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
.target-field { display: inline-flex; align-items: center; gap: .35rem; font-size: var(--step--1); color: var(--muted); }
.target-input { width: 5.5rem; }
.target-help { flex-basis: 100%; margin: 0; }

/* (the `button.ghost` copy that used to live here is folded into the one rule above — M42 T3) */
button.ghost:hover { background: var(--surface-sunken); filter: none; }

/* ---- form action rows (manual authoring: suggest + store) --------------- */
.form-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }

/* ---- build-status dashboard -------------------------------------------- */
.summary { display: flex; align-items: center; gap: 1.4rem; flex-wrap: wrap; }
.summary-pct {
  font-size: clamp(2.4rem, 7vw, 3.2rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--accent);
  line-height: 1;
}
.summary-pct span { font-size: .45em; font-weight: 600; color: var(--muted); margin-left: .1em; }
.summary-facts { flex: 1 1 16rem; min-width: 14rem; }
.progress {
  height: .55rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.progress > span { display: block; height: 100%; background: var(--accent); border-radius: var(--radius-pill); transition: width .3s ease; }
.summary-line { margin: .7rem 0 0; color: var(--fg-soft); }
.summary-line strong { color: var(--fg); font-weight: 600; }

.ms-list { counter-reset: none; }
.ms-row { padding: 1rem 0; border-top: 1px solid var(--border); }
.ms-row:first-child { border-top: none; padding-top: 0; }
.ms-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ms-title { font-weight: 600; }
.ms-count { margin-left: auto; font-size: var(--step--1); color: var(--muted); font-variant-numeric: tabular-nums; }

.badge.state-done { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-border); }
.badge.state-wip { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in oklab, var(--accent) 22%, var(--border)); }
.badge.state-defer, .badge.state-todo { background: var(--surface-sunken); color: var(--muted); border-color: var(--border); }

.task-list { margin: .7rem 0 0; padding: 0 0 0 .1rem; }
.task-row { display: flex; align-items: baseline; gap: .55rem; padding: .28rem 0; }
.task-name { color: var(--fg-soft); }
.task-meta { margin-left: auto; font-family: var(--font-mono); font-size: var(--step--1); color: var(--muted); }
.task-empty { margin: .55rem 0 0; color: var(--muted); font-size: var(--step-0); }

.dot { width: .55rem; height: .55rem; border-radius: var(--radius-pill); flex: 0 0 auto; align-self: center; background: var(--border-strong); }
.dot-done { background: var(--ok); }
.dot-wip { background: var(--accent); }
.dot-defer { background: var(--danger); opacity: .55; }
.dot-todo { background: var(--border-strong); }

/* ---- source / index manager (M03 T8) ----------------------------------- */
.filter-form {
  display: flex;
  align-items: end;
  gap: .9rem;
  flex-wrap: wrap;
  margin-bottom: 1.2rem;
  padding-bottom: 1.2rem;
  border-bottom: 1px solid var(--border);
}
.filter-form .field { margin-bottom: 0; flex: 1 1 12rem; }

button.danger {
  color: var(--danger);
  background: var(--surface);
  border-color: var(--danger-border);
}
button.danger:hover { background: var(--danger-soft); filter: none; }


.src-row {
  padding: .95rem .85rem;
  border: 1px solid transparent;
  border-top-color: var(--border);
  border-radius: var(--radius-sm);
  transition: background-color .12s ease, border-color .12s ease;
}
.src-row:first-child { border-top-color: transparent; }
/* Hovering lifts the whole row out of the list, which is how you tell where one document ends. */
.src-row:hover { background: var(--surface-sunken); border-color: var(--border); }
.src-row:hover + .src-row { border-top-color: transparent; }
.src-main { display: flex; flex-direction: column; gap: .15rem; }
.src-title { font-weight: 600; }
.src-title a { color: var(--accent); text-decoration: none; }
.src-title a:hover { text-decoration: underline; }
.src-url { font-size: var(--step--1); color: var(--muted); word-break: break-all; font-family: var(--font-mono); }
.src-meta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .55rem 0; font-variant-numeric: tabular-nums; }
.src-actions { display: flex; align-items: center; gap: .55rem; flex-wrap: wrap; }
.src-actions .inline { display: flex; align-items: center; gap: .4rem; }
.src-actions select { width: auto; min-width: 8rem; padding: .4rem 1.8rem .4rem .55rem; font-size: var(--step-0); }
.src-actions button { padding: .45rem .85rem; font-size: var(--step-0); }
/* Edit is a LINK (it opens the authoring page), not an action posted from here — styled to sit level with
   the ghost buttons beside it so the row does not read as one odd item out. */
.src-actions a.ghost {
  padding: .45rem .85rem;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--fg-soft);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  text-decoration: none;
}
.src-actions a.ghost:hover { background: var(--surface-sunken); }

.pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: var(--step-0); }
.pager a { color: var(--accent); text-decoration: none; font-weight: 600; }
.pager a:hover { text-decoration: underline; }
.pager-off { color: var(--border-strong); }
.pager-at { color: var(--muted); font-variant-numeric: tabular-nums; }

/* crawl-job status panel (M03 T12) */
.crawl-status { background: var(--surface-sunken); }
.job-list { list-style: none; margin: 0 0 .6rem; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.job-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; font-size: var(--step-0); }
.job-row .dot { align-self: center; }
.job-url { color: var(--fg-soft); font-family: var(--font-mono); font-size: var(--step--1); word-break: break-all; }
.job-state { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; }
.dot-wip { animation: pulse 1.2s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: .35; } }

/* ---- internal admin hub ------------------------------------------------- */
/* The strip itself is styled with the overview tiles further up; it is a bare grid, so it must not carry
   a card of its own — a border around a row of bordered tiles is a box in a box. */


.hub-row {
  display: flex;
  align-items: baseline;
  gap: .8rem;
  flex-wrap: wrap;
  padding: .7rem 0;
  border-top: 1px solid var(--border);
}
.hub-row:first-child { border-top: none; padding-top: 0; }
.hub-link { font-weight: 600; color: var(--accent); text-decoration: none; flex: 0 0 auto; min-width: 9rem; }
.hub-link:hover { text-decoration: underline; }
.hub-desc { color: var(--muted); font-size: var(--step-0); }

/* knowledge-gap review (M04 T9.3) */
.gap-list { list-style: none; margin: 0; padding: 0; }
.gap-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; padding: .7rem 0; border-top: 1px solid var(--border); }
.gap-row:first-child { border-top: none; padding-top: 0; }
.gap-q { font-weight: 600; color: var(--fg); }
.gap-meta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-left: auto; }
.gap-fix { flex-basis: 100%; display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-top: .5rem; }
.gap-fix .gap-note { flex: 1 1 14rem; }
.gap-fix select { flex: 0 0 auto; }
/* AI Fix Assistant option cards (M12 T5/T6) */
.fix-form { display: flex; flex-direction: column; gap: 1.5rem; }
.fix-option { border-left: 3px solid var(--border); }
.fix-option:focus-within { border-left-color: var(--accent); }
.fix-pick { display: block; margin-bottom: .5rem; font-weight: 500; }
.fix-form label { display: block; font-weight: 600; color: var(--fg-soft); }
.fix-form .fix-pick { font-weight: 500; color: var(--fg); }
.fix-form textarea, .fix-form select { width: 100%; margin-top: .35rem; }
.fix-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.fix-sources li { display: flex; align-items: center; gap: .5rem; }

/* Q&A review queue (M05 T3) */
.qa-list { list-style: none; margin: 0; padding: 0; }
.qa-row { padding: 1rem 0; border-top: 1px solid var(--border); }
.qa-row:first-child { border-top: none; padding-top: 0; }
.qa-q { font-weight: 600; margin-bottom: .5rem; }
/* Old-vs-regenerated comparison (M13 T4): two columns so the decision is a glance, stacking on narrow screens
   because a side-by-side diff squeezed into a phone width is worse than reading them in order. */
.qa-diff { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; margin: .6rem 0 .8rem; }
.qa-diff h3 { margin: 0 0 .4rem; font-size: var(--step--1); font-weight: 600; color: var(--muted); }
.qa-diff h3 .badge { margin-left: .35rem; font-weight: 500; }
.qa-side { display: flex; flex-direction: column; min-width: 0; }
.qa-side textarea { flex: 1; }
.qa-old {
  padding: .7rem .85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--fg-soft);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.qa-links { margin: -.15rem 0 .6rem; font-size: var(--step--1); }
@media (max-width: 46rem) { .qa-diff { grid-template-columns: 1fr; } }

/* Demand count + staleness reason (M13 T2/T1): secondary to the category pill, not competing with it. */
.pill.demand { background: var(--surface-sunken); color: var(--muted); border: 1px solid var(--border); }
.qa-why { margin: -.25rem 0 .55rem; font-size: var(--step--1); color: var(--muted); }
.qa-a { color: var(--fg-soft); background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: .6rem .8rem; margin-bottom: .5rem; }
.qa-edit { display: flex; flex-direction: column; align-items: flex-start; gap: .5rem; margin-bottom: .5rem; }
.qa-actions { display: flex; gap: .55rem; flex-wrap: wrap; }

/* ---- visitor support chat (M04 T2) ------------------------------------- */
.chat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 1.25rem 1.35rem 1.35rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.chat-log {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .85rem;
  min-height: 16rem;
  max-height: 60vh;
  overflow-y: auto;

  /* M25 T1. The complaint was "the right scroll bar inside the chat" — a chunky default bar hard against
     the bubbles, which made the transcript read as a frame nested inside the card rather than as the card's
     own content.
     · `scrollbar-width: thin` + a styled webkit bar: present when it is needed, quiet when it is.
     · `scrollbar-gutter: stable` reserves the channel so the whole transcript does NOT shift sideways the
       moment a chat grows past the fold — the jump is more distracting than the bar ever was.
     · the padding keeps text off the bar instead of touching it. */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
  scrollbar-gutter: stable;
  padding-right: .35rem;
}
.chat-log::-webkit-scrollbar { width: 8px; }
.chat-log::-webkit-scrollbar-track { background: transparent; }
.chat-log::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--radius-pill);
  /* Inset by a transparent border so the thumb reads as a slim pill in a channel, not a full-width slab. */
  border: 2px solid transparent;
  background-clip: content-box;
}
.chat-log:hover::-webkit-scrollbar-thumb { background-color: var(--muted); background-clip: content-box; }

/* The CUSTOMER's chat only (M25 T1): start small and grow. The 16rem floor above is right for the agent
   console, where the pane is a fixed working area that M23 T3 pins to the newest turn — but on the public
   page it reserved a tall empty box before anybody had said anything, which is what made a two-message
   conversation look like a viewport. Scoped, so the console keeps the floor it needs. */
.chat .chat-log { min-height: 0; }
.chat .chat-log:empty { display: none; }
/* What is happening on the human side, above the transcript (M23 T4/T6). Quiet on purpose: it is standing
   context, not a message, and styling it like a reply would put a second voice in the conversation. */
.chat-handling {
  margin: 0 0 .6rem;
  padding: .5rem .7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-sunken);
  color: var(--muted);
  font-size: var(--step--1);
  line-height: var(--leading-normal);
}
.chat-empty { color: var(--muted); text-align: center; padding: 2.5rem 1rem; }
.msg { display: flex; flex-direction: column; gap: .2rem; max-width: 82%; }
.msg-who { font-size: var(--step--2); font-weight: 600; color: var(--muted); letter-spacing: .02em; padding: 0 .3rem; }
.msg .bubble {
  padding: .6rem .85rem;
  border-radius: var(--radius-sm);
  line-height: var(--leading-normal);
  word-wrap: break-word;
  overflow-wrap: anywhere;
  /* A READING MEASURE (M27 T4). Measured on the live agent thread page, which runs the wide shell:
     assistant bubbles were rendering 1068px across, which is about 142 characters a line. Comfortable
     is 60-80, so every reply was roughly double the width the eye can track, and the transcript read
     as an undifferentiated wall — most of what "looks like junk content pasted all places" is this one
     number. `ch` rather than `rem` because the limit being expressed is a count of characters. */
  max-width: 68ch;
  /* Keep the model's own line breaks and blank lines. The live reply arrives as a stream of text
     set via textContent (no HTML), so the paragraphing has to come from CSS — and the templates
     print the same raw text, so a reloaded thread reads exactly like the one that just streamed. */
  white-space: pre-wrap;
}
.msg-visitor { align-self: flex-end; align-items: flex-end; }
.msg-visitor .bubble { background: var(--accent); color: var(--accent-ink); border-bottom-right-radius: 3px; }
.msg-assistant, .msg-human { align-self: flex-start; align-items: flex-start; }
.msg-assistant .bubble, .msg-human .bubble {
  background: var(--surface-sunken);
  color: var(--fg);
  border: 1px solid var(--border);
  border-bottom-left-radius: 3px;
}
/* Development tenant selector (M17 T1) — deliberately marked out from the rest of the nav. This is an
   unauthenticated stand-in for HIOX SSO and only ever renders in development, so it should look like a
   development tool rather than a product control.

   SINCE M44 T3 IT IS BOTH THINGS, and the markup says which: `.admin-tenant-standin` is the
   development selector, and the bare class is a real customer administrator choosing between the
   sites they administer. The dashed-warning framing belongs only to the first — left on the real
   control it would be the same mistake pointing the other way, telling somebody their own product is
   a development tool. */
/* THE CONTROLS ON THE RIGHT ARE BORDERED PILLS (M42 T13), measured off hiox.com: 42px tall, a
   `#d1d9e0` hairline, 6px radius, 14px/500. Ours had no border at all, so the tenant picker and the
   theme switcher read as text that happened to be clickable rather than as controls. */
.admin-tenant > summary {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-header);
  background: var(--surface);
}
.admin-tenant > summary { display: inline-flex; align-items: center; gap: .4rem; }
.tenant-dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--accent); flex: 0 0 auto;
}
.admin-tenant .tenant-note {
  margin: 0 0 .4rem; padding: 0 .1rem .4rem;
  border-bottom: 1px solid var(--border);
  font-size: var(--step--2); color: var(--fg-soft); max-width: 15rem;
}
/* Console access (M45 T3). One block per customer, because the question somebody arrives with is
   "who can get into Acme?" — and a container query again, not a viewport one: this sits in the
   console's content column, so the card's width is what decides whether a row is one line or two. */
.access-block {
  margin: 0 0 2rem;
  padding: .9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  container-type: inline-size;
  container-name: access;
}
.access-block > h2 { display: flex; align-items: baseline; gap: .5rem; margin: 0 0 .5rem; font-size: var(--step-1); }
.access-slug { font-size: var(--step--2); font-weight: 400; color: var(--fg-soft); }
.access-list { list-style: none; margin: 0 0 .8rem; padding: 0; display: grid; gap: .3rem; }
.access-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto);
  align-items: center;
  gap: .5rem .75rem;
  padding: .4rem .5rem;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}
.access-who { display: grid; gap: .1rem; min-width: 0; }
.access-email { overflow-wrap: anywhere; }
.access-meta { font-size: var(--step--2); color: var(--fg-soft); }
.access-form { margin: 0; }
.access-grant { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0; }
.access-grant label { font-size: var(--step--1); font-weight: 600; }
.access-grant input[type="email"], .access-grant select {
  /* 16px minimum, or every phone zooms the moment the field is focused (M39 T4). */
  font-size: max(1rem, var(--step-0));
  padding: .4rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.access-grant input[type="email"] { flex: 1 1 14rem; }
.access-grant button, .access-revoke {
  /* 44px, which the theme-switcher icons still fail (M42 T10). */
  min-height: 2.75rem;
  padding: 0 .75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.access-grant button:hover, .access-revoke:hover { color: var(--fg); background: var(--surface-sunken); }
.access-grant button:focus-visible, .access-revoke:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Revoking is destructive, so it does not look like Grant. Not red-and-shouting either — it is
   reversible in one click, and alarm for a reversible act is noise that gets ignored. */
.access-revoke { border-color: var(--border); color: var(--fg-soft); }

@container access (max-width: 26rem) {
  .access-row { grid-template-columns: minmax(0, 1fr); }
  .access-grant input[type="email"], .access-grant select, .access-grant button { width: 100%; }
}

/* What a role may see (M45 T2). A dense multi-column list, because fifty paths in one column is a
   page nobody scrolls to the bottom of — and `auto-fill` with a `min()` floor so a narrow card gets
   one column instead of a crushed two (the `minmax` lesson the project's own guard enforces). */
.access-pages {
  list-style: none;
  margin: .4rem 0 .8rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: .1rem .9rem;
}
.access-pages label { display: flex; align-items: center; gap: .45rem; padding: .2rem 0; font-size: var(--step--1); }
.access-pages code { overflow-wrap: anywhere; }
.access-perms-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.access-perms-actions button {
  min-height: 2.75rem;
  padding: 0 .75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.access-perms-actions button:hover { color: var(--fg); background: var(--surface-sunken); }
.access-perms-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Removed items (M07 T11). A CONTAINER query, not a viewport one: this page sits in the console's
   content column, so what decides whether a row is one line or two is the card's width — the lesson
   `container-not-viewport` cost twice. */
.removed-group { margin: 0 0 2rem; container-type: inline-size; container-name: removed; }
.removed-group > h2 { display: flex; align-items: baseline; gap: .5rem; font-size: var(--step-1); }
.removed-count {
  font-size: var(--step--2);
  font-weight: 600;
  padding: .05rem .4rem;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--fg-soft);
}
.removed-list { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .3rem; }
.removed-row {
  display: grid;
  /* The actions track may shrink below max-content so the button wraps inside its own column
     instead of squeezing the title to nothing — the M28 T20 listing fix, same shape. */
  grid-template-columns: minmax(0, 1fr) minmax(0, auto);
  align-items: center;
  gap: .5rem .75rem;
  padding: .5rem .6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.removed-what { display: grid; gap: .1rem; min-width: 0; }
.removed-title { overflow-wrap: anywhere; }
.removed-meta { font-size: var(--step--2); color: var(--fg-soft); }
.removed-form { margin: 0; }
.removed-form button {
  padding: .3rem .7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  font: inherit;
  /* 44px touch target, which the theme-switcher icons still fail (M42 T10). */
  min-height: 2.75rem;
  cursor: pointer;
}
.removed-form button:hover { color: var(--fg); background: var(--surface-sunken); }
.removed-form button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@container removed (max-width: 26rem) {
  .removed-row { grid-template-columns: minmax(0, 1fr); }
  .removed-form button { width: 100%; }
}

/* "My desk" (M44 T5). A bordered pill like the tenant picker and the theme switcher beside it — the
   same 42px/hairline/6px measurements M42 T13 took off hiox.com, because three controls in a row that
   do not match each other read as three accidents. */
.admin-desk-form { margin: 0; display: inline-flex; }
.admin-desk-form button {
  display: inline-flex;
  align-items: center;
  min-height: 2.625rem;
  padding: 0 .7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-header);
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}
.admin-desk-form button:hover { color: var(--fg); background: var(--surface-sunken); }
.admin-desk-form button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The development selector keeps the warning tint; the real one is an ordinary control. */
.admin-tenant-standin > summary {
  border-style: dashed;
  border-color: var(--warn);
  background: var(--warn-soft);
}

.tenant-form { margin: 0; }
.tenant-form button {
  display: flex; align-items: baseline; gap: .4rem; width: 100%;
  padding: .35rem .5rem; border: 0; border-radius: var(--radius-sm);
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.tenant-form button:hover { background: var(--surface-sunken); }
.tenant-form button[aria-current] { font-weight: 600; }
.tenant-slug { font-size: var(--step--2); color: var(--fg-soft); font-family: var(--font-mono); }

/* The composer (M25 T1). It sat as a plain field and a plain button, visually unrelated to the rounded
   bubbles above it. Matching the bubble radius and giving the pair a little more height makes the box the
   obvious place to type rather than a form stuck under a transcript. */
.chat-form { display: flex; gap: .6rem; align-items: stretch; }
.chat-form input[type="text"] {
  flex: 1 1 auto;
  padding: .7rem .9rem;
  border-radius: var(--radius-sm);
  font-size: var(--step-1);   /* 17px — the composer should read bigger than the page around it */
}
.chat-form button {
  flex: 0 0 auto;
  padding-inline: 1.15rem;
  border-radius: var(--radius-sm);
  font-weight: 600;
}

@media (max-width: 30rem) {
  /* On a narrow screen the Send label costs more than it earns beside a full-width field. */
  .chat-form { gap: .45rem; }
  .chat-form button { padding-inline: .9rem; }
}

/* per-turn retrieval trace on the Chat Review page (M12 T2) */
.trace {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem;
  margin: .2rem .3rem 0;
  padding: .4rem .55rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--step--1);
}
.trace-flag { border-color: var(--warn); background: color-mix(in oklab, var(--warn) 8%, var(--surface)); }
.trace .hub-desc { flex-basis: 100%; margin: 0; }
.trace-scores { flex-basis: 100%; margin: .15rem 0 0; }
.trace-scores summary { cursor: pointer; color: var(--muted); }
.trace-scores ol { margin: .3rem 0 0 1.1rem; padding: 0; color: var(--fg-soft); }
.pill-warn { background: color-mix(in oklab, var(--warn) 16%, var(--surface)); color: var(--warn); }
.pill-ok { background: var(--accent-soft); color: var(--accent); }

/* interactive choice buttons (M04 T5) */
.choices { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .5rem; align-self: flex-start; max-width: 100%; }
.choice-form { margin: 0; }
.choice-btn {
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  padding: .4rem .9rem;
  color: var(--accent);
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  transition: background .15s ease;
}
.choice-btn:hover { background: var(--accent-soft); }

/* per-answer feedback (M04 T6) */
.feedback { display: flex; gap: .3rem; margin-top: .35rem; align-self: flex-start; }
.feedback .fb-form { margin: 0; }
.fb-btn {
  font-size: var(--step-0);
  line-height: 1;
  cursor: pointer;
  padding: .25rem .45rem;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  opacity: .7;
  transition: opacity .15s ease, background .15s ease;
}
.fb-btn:hover { opacity: 1; background: var(--surface-sunken); }
.feedback[data-voted="up"] .fb-btn:first-child,
.feedback[data-voted="down"] .fb-btn:last-child { opacity: 1; background: var(--accent-soft); border-color: var(--accent); }

/* M11 T3 — AI-distilled extraction indicator under a source row */
.src-extract { display: inline-flex; align-items: center; gap: .4em; margin-top: .25rem;
  font-size: .82em; color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
  border-radius: 6px; padding: .1rem .5rem; cursor: help; }
.src-help { display: inline-flex; align-items: center; justify-content: center;
  width: 1.1em; height: 1.1em; border-radius: 50%; background: var(--accent); color: var(--accent-ink);
  font-size: .78em; font-weight: 700; cursor: help; }

/* ---- shared KB listing (M16 T2) ----------------------------------------
   The one anatomy behind Crawl Pages, Manual Content and Q&A. Each of those had
   grown its own toolbar and row markup; these rules exist so they cannot drift
   apart again. Row internals (.src-*) are reused rather than renamed — the shape
   was already right, it just had no single owner. */
.kb-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  flex-wrap: wrap;
  margin-bottom: .9rem;
}
.kb-toolbar-title { margin: 0; display: flex; align-items: center; gap: .5rem; }
.kb-toolbar-action {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  /* Padding comes from the base button rule (M42 T3). Its own `.5rem .95rem` was 0.8px away
     from it — close enough that nobody would see it beside anything, and far enough to make
     this a ninth distinct look in a measurement. */
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--ink-on-color);
  font-size: var(--step-0);
  font-weight: 600;
  text-decoration: none;
}
.kb-toolbar-action:hover { filter: brightness(1.06); }

/* Signals sit on their own line: what a document MEANS (canonical answers it backs,
   review it needs) reads separately from the factual meta badges above it. */
.src-signals {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  margin: -.15rem 0 .55rem;
}
.src-signals a.badge { text-decoration: none; }
.src-signals a.badge:hover { filter: brightness(.97); text-decoration: underline; }

/* ---- bulk selection + chunk inspection (M16 T7) ------------------------
   50 of 77 crawled pages sit pending; working that one round-trip at a time is
   not work anyone finishes. The bar is inert until something is selected. */
.kb-bulk {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .55rem .7rem;
  margin-bottom: .6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  position: sticky;
  top: 3.6rem;
  z-index: 2;
}
.kb-bulk.has-selection { border-color: var(--accent); background: var(--accent-soft); }
.kb-bulk-all { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--step-0); }
.kb-bulk-count { font-size: var(--step--1); color: var(--muted); font-variant-numeric: tabular-nums; }
.kb-bulk.has-selection .kb-bulk-count { color: var(--fg); font-weight: 600; }
.kb-bulk-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-left: auto; }
.kb-bulk-actions button { padding: .35rem .7rem; font-size: var(--step--1); }
.kb-bulk-actions select { width: auto; min-width: 8rem; padding: .32rem 1.6rem .32rem .5rem; font-size: var(--step--1); }
/* The same treatment as every other disabled control (M42 T7) — this was `opacity: .5` against
   the base rule's `.45`, so two disabled buttons on one page did not match each other. */
.kb-bulk-actions select:disabled {
  background: var(--disabled-bg);
  color: var(--disabled-ink);
  border-color: var(--disabled-border);
  cursor: not-allowed;
}

/* The row checkbox sits in a gutter so the title still lines up down the list. */
.src-row.is-selectable { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr); gap: .1rem .5rem; }
.src-row.is-selectable > .kb-pick { grid-row: 1 / span 5; align-self: start; margin-top: .2rem; }

/* ---- A LISTING ROW IS TWO COLUMNS WHEN ITS CARD IS WIDE (M28 T21) ---------------------------------
   User, 2026-10-01, choosing between a roster-only change and this one: *"go to 2"*.

   The row was a vertical stack of full-width blocks — name, then badges, then whatever the page adds,
   then the buttons. Measured on the roster at 1440x900: each row 233px tall with its ink reaching
   about 591px of a 1318px card, so roughly 60% of every row was empty and the page ran three screens
   for four people. Every listing in the product is this same partial, so every one of them paid it.

   The actions move to a second column, level with the row's NAME, because they are what you do TO the
   row rather than something that follows everything the row contains. `/internal/departments` had had
   exactly this since 2026-09-18 ("Disable and Delete belong at the TOP RIGHT of a desk"), scoped to
   that page with a note saying the shared shape was deliberate and not mine to change. It is the
   shared shape now, and the page-scoped copy is DELETED rather than left underneath — a second rule
   saying the same thing is how two rules quietly drift apart.

   A CONTAINER QUERY, NOT A MEDIA QUERY, and that was not the first thing I tried. A viewport
   breakpoint at 60rem split the row by how wide the WINDOW is, and these lists do not all get the
   same share of it: `/internal/sources` carries a category sidebar, so at 961px its card is ~700px
   and the 550px of actions on a crawl row squeezed `minmax(0, 1fr)` down to **1.5px** — twenty rows
   with no readable title. `/internal/review` overflowed into a horizontal scrollbar at the same
   width. Caught by measuring at 959 and 961 rather than only at the sizes a design is drawn for.

   What decides the layout is how much room THIS LIST has, so that is what is asked. Two more things
   earn their place in the same fix: the main column keeps an 18rem floor so it can never be crushed
   to nothing again, and the actions track may shrink below its max-content width, which lets the
   buttons wrap inside their own column instead of pushing the title out of the row. */
.src-list {
  container-type: inline-size;
  container-name: listing;
}

@container listing (min-width: 46rem) {
  .src-row {
    display: grid;
    grid-template-columns: minmax(min(18rem, 100%), 1fr) minmax(0, max-content);
    column-gap: 1.25rem;
    align-items: start;
  }
  .src-main { grid-column: 1; grid-row: 1; }
  /* `justify-content: flex-end` so a row with four actions and a row with five end at the same x.
     A ragged right edge down a list is what made the roster's buttons read as a different control in
     every row. */
  .src-actions {
    grid-column: 2;
    grid-row: 1;
    margin-top: 0;
    align-self: start;
    justify-content: flex-end;
  }
  /* Everything else the page puts in the row — badges, signals, the per-row editor — keeps the full
     width underneath. They are what the row SAYS; only the verbs move. */
  .src-row > *:not(.src-main):not(.src-actions) { grid-column: 1 / -1; }

  /* With a bulk checkbox there are three columns, and the checkbox spans all of them vertically so
     the title still lines up down the list. */
  .src-row.is-selectable { grid-template-columns: 1.4rem minmax(min(18rem, 100%), 1fr) minmax(0, max-content); }
  .src-row.is-selectable > .kb-pick { grid-column: 1; grid-row: 1 / -1; }
  .src-row.is-selectable > .src-main { grid-column: 2; grid-row: 1; }
  .src-row.is-selectable > .src-actions { grid-column: 3; grid-row: 1; }
  .src-row.is-selectable > *:not(.kb-pick):not(.src-main):not(.src-actions) { grid-column: 2 / -1; }
}

.kb-chunks { margin: .5rem 0 .2rem; font-size: var(--step-0); }
.kb-chunks > summary { cursor: pointer; color: var(--muted); }
.kb-chunks > summary:hover { color: var(--accent); }
.kb-chunks-body {
  margin-top: .5rem;
  max-height: 22rem;
  overflow-y: auto;
  border-left: 2px solid var(--border);
  padding-left: .7rem;
}
.kb-chunk { margin-bottom: .6rem; }
.kb-chunk p { margin: .2rem 0 0; white-space: pre-wrap; color: var(--fg-soft); font-size: var(--step--1); }

/* ---- responsive (M16 T1) -------------------------------------------------
   The stylesheet had exactly ONE media query before this block; everything else
   survived on flex-wrap alone, which is why the admin surfaces were unusable on a
   narrow screen. Breakpoints are in rem so they track the user's text size. */

/* Tablet and below: the category rail stops being a column and becomes a horizontal
   filter strip above the content. Collapsing it into a <details> would be the obvious
   move, but the rail is server-rendered `open` and CSS cannot close it — a strip needs
   no JS, wastes no vertical space, and keeps every category one tap away. */
@media (max-width: 60rem) {
  .admin-layout { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
  .kb-sidebar { position: static; }
  .kb-rail { padding: .5rem; }
  .kb-rail-head { padding: .2rem .35rem .4rem; }
  .kb-rail-list {
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: .35rem;
    padding-bottom: .2rem;
    scrollbar-width: thin;
  }
  .kb-rail-list > li { flex: 0 0 auto; }
  /* The name truncates; the count never does — a number that says "12" is useless as "1…" (M42 T10). */
.kb-rail-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kb-rail-count { flex: 0 0 auto; }
.kb-rail-item { border: 1px solid var(--border); white-space: nowrap; }
  .kb-rail-item.is-current { border-color: transparent; }
}

/* Phone: stack the row internals that were laid out side by side, and let action
   buttons fill the width rather than crushing into an unreadable strip. */
@media (max-width: 40rem) {
  body { padding-left: .75rem; padding-right: .75rem; }
  .admin-header { padding: .6rem .85rem; gap: .4rem .9rem; }
  .admin-header-nav { gap: .3rem .75rem; font-size: var(--step--1); }

  .kb-bulk { position: static; }
  .kb-bulk-actions { margin-left: 0; width: 100%; }
  .kb-bulk-actions button, .kb-bulk-actions select { flex: 1 1 auto; }

  .kb-toolbar { align-items: stretch; }
  .kb-toolbar-action { justify-content: center; }

  .src-row { flex-direction: column; align-items: stretch; }
  /* A job row is four short tokens; wrapping keeps them readable, stacking gave each its own line
     and turned a three-line panel into fifteen. */
  .job-row { flex-wrap: wrap; row-gap: .15rem; }
  .job-row .job-url { flex: 1 1 100%; order: 3; }
  .job-row .job-state { margin-left: 0; order: 2; }
  .src-actions, .form-actions, .filter-form, .add-form { flex-wrap: wrap; }
  /* THE TWO KINDS OF ACTION HAVE DIFFERENT MARKUP DEPTHS, and this did not account for it.
     A posting action is a <form class="inline"> wrapping its button; a navigating action is a bare
     <a class="ghost">. `width: 100%` is right for the button INSIDE a form — it fills the flex slot
     its wrapper was given — and wrong for a link that IS the flex item, because a flex basis of
     `auto` then resolves to 100% of the row and the link claims a whole line to itself.

     Measured on the roster at 390px: "Workload" and "View chats" took a full-width line EACH while
     "New password" and "Disable" shared one, so four actions came to three lines in an order nothing
     chose. Every listing on the product shares this partial, so every one of them had it. */
  .src-actions > form, .src-actions > a, .src-actions > button { flex: 1 1 auto; }
  .src-actions > form > button, .src-actions > form > select, .src-actions > button { width: 100%; }
  .src-actions > a.ghost { width: auto; justify-content: center; }
  .src-actions select { width: 100%; min-width: 0; }
  /* And a thumb needs 44px. These are 37px tall on a phone — the padding is tuned for a dense desktop
     row, where it is right, and nothing was giving it back on the surface that is touched. */
  .src-actions button, .src-actions a.ghost, .src-actions select { min-height: 44px; }

  .admin-table { font-size: var(--step--1); }
  .admin-table th, .admin-table td { padding: .4rem .35rem; }

  .pager { flex-wrap: wrap; gap: .4rem; }
  .msg { max-width: 92%; }
}

/* A submenu that would hang off the right edge on a narrow screen is pinned instead. */
@media (max-width: 30rem) {
  /* THE MENUS ON THE RIGHT OPEN LEFTWARDS. `left: 0` is right for a menu near the left of the header
   and wrong for the last one in it, which is where the 51px came from (M42 T10). */
.admin-menu:nth-last-child(-n+3) > .admin-submenu { left: auto; right: 0; }
.admin-submenu { left: auto; right: 0; min-width: 11rem; }
}

/* ---- agent sign-in (M17 T3) --------------------------------------------- */
/* A narrow, centred card. This is the only surface in the app that is neither the admin shell nor the
   chat column: nobody is signed in yet, so there is no header, no nav and nothing to navigate to. It
   says as little as possible by design — the page must look the same for a tenant that does not
   exist — so the styling carries the whole of the welcome. */
.auth {
  max-width: 22rem;
  margin: 3rem auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.75rem 1.6rem 1.4rem;
}
.auth-head { margin-bottom: 1.25rem; }
.auth-head h1 {
  font-size: var(--step-2);
  letter-spacing: -.015em;
  margin: 0 0 .2rem;
}
.auth-head .tagline { color: var(--muted); font-size: var(--step--1); margin: 0; }

.auth-form { display: grid; gap: .3rem; }
.auth-form label {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
  margin-top: .6rem;
}
.auth-form input { max-width: none; }
.auth-form button { margin-top: 1.1rem; width: 100%; }

/* One message, one place, whatever went wrong — see AgentAuthController on why it is never specific. */
.auth-error {
  margin: 0 0 1rem;
  padding: .6rem .7rem;
  font-size: var(--step--1);
  color: var(--danger);
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  border-radius: var(--radius-sm);
}
.auth-note {
  margin: 1.1rem 0 0;
  font-size: var(--step--1);
  color: var(--muted);
  line-height: var(--leading-normal);
}
.auth-note a { color: var(--accent); }

.auth-actions {
  display: flex;
  align-items: center;
  gap: .9rem;
  margin-top: 1.5rem;
}
.auth-actions form { margin: 0; }
.auth-actions a { color: var(--accent); font-size: var(--step--1); }

/* ── Department routing tags (M17 T6) ──────────────────────────────────────────────────────────────
   Replaced a 40-cell intent × category grid (user, 2026-09-03). The grid asked "for each combination,
   where does it go?"; nobody thinks that way, and its hole-counter called a complete configuration
   87% unfinished. This states the question an admin actually has, on the desk it is about: what
   arrives here, and is it narrowed. "Any topic" is prose rather than an empty control, because it is
   the normal and complete state, not a blank waiting to be filled. */
.routes { flex-basis: 100%; margin: .55rem 0 .1rem; display: flex; flex-direction: column; gap: .35rem; }
.routes-line { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.routes-label {
  flex: 0 0 5.5rem;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.routes-none { font-size: var(--step--1); color: var(--muted); font-style: italic; }
.tag-set { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.tag {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  padding: .18rem .3rem .18rem .6rem;
  font-size: var(--step--1);
  font-weight: 500;
}
/* An assistant-derived intent is a PROPOSAL: outlined rather than filled, so "what I chose" and "what
   it worked out" are never confused at a glance. */
.tag-ai { background: transparent; border-color: var(--border); color: var(--muted); border-style: dashed; }
.tag-stale { text-decoration: line-through; opacity: .65; }
.tag-form { display: inline; margin: 0; }
.tag-btn {
  background: none;
  border: none;
  padding: 0 .2rem;
  cursor: pointer;
  font: inherit;
  font-size: var(--step--1);
  color: inherit;
  opacity: .65;
  line-height: 1;
}
.tag-btn:hover { opacity: 1; color: var(--danger); }
.tag-ai .tag-btn:hover { color: var(--accent); }
.tag-add { margin: 0; }
.tag-add select { padding: .2rem .35rem; font-size: var(--step--1); border-radius: var(--radius-pill); }
/* A TYPED tag (M28 T7), beside the chosen-from-a-list one above. Same pill geometry so the two read as
   one control, and `min-width: 0` because a flex item will not shrink below its content and an input
   carries a ~20-character intrinsic minimum that `width` alone does not override — the trap that made
   three admin layouts overflow before it was written down. */
.tag-add { display: inline-flex; align-items: center; gap: .35rem; min-width: 0; }
.tag-add input[type="text"] {
  min-width: 0;
  width: 14rem;
  max-width: 100%;
  padding: .2rem .6rem;
  font-size: var(--step--1);
  border-radius: var(--radius-pill);
}

/* ---- tags on a thread (M28 T7) ------------------------------------------- */
/* ABOVE the Details fold, deliberately: a control behind a disclosure is one that does not exist in
   practice (M19 T5 shipped settings two levels deep and they were reported missing), and a tag is the
   one thing somebody adds while skim-reading a thread they are triaging. */
.thread-tags {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  min-width: 0;
  margin: .5rem 0 .75rem;
}
.tag-label {
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}


/* ---- agent console: offers (M17 T14.10) -------------------------------------
   A chat offered to an agent has a clock on it, and until this the console showed
   neither the offer nor the clock — so the one person who could stop it running out
   could not see it. The band is deliberately louder than a queue heading: at
   thirty seconds, a row indistinguishable from the 34 below it is not an answer. */
.queue-band {
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
  border-radius: var(--radius);
  padding: .9rem 1rem 1rem;
}
.queue-band > h2 { margin-top: 0; }
.queue-band .listing { background: var(--surface); border-radius: calc(var(--radius) - 2px); }

/* The row a countdown belongs to, so the eye lands on it before reading the column. */
tr.row-offered > td { background: color-mix(in oklab, var(--accent-soft) 55%, transparent); }

/* Its clock has run out under them. Dimmed rather than removed: a row vanishing mid-click
   is how the wrong thread gets answered, and the buttons will refuse anyway. */
tr.row-expired > td { opacity: .55; }
tr.row-expired .offer-left { color: var(--danger); }

/* Accept / Decline sit together on one line; the row is already narrow. */
.row-actions .inline-form { display: flex; gap: .4rem; margin: 0; }
.row-actions button { white-space: nowrap; }

/* The countdown itself — tabular so the row does not twitch as digits change. */
.offer-left { font-variant-numeric: tabular-nums; }


/* ---- agent console: orientation strip + folded queue (M17 T14.10) -----------
   Three numbers, each a different decision, so the answer to "what now" is not
   "scroll four tables and count". Sized to be read from across a desk. */
.console-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: .75rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.console-summary li {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  padding: .7rem .9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.console-summary .value {
  font-size: var(--step-2);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.console-summary .label { color: var(--fg-soft); font-size: var(--step--1); line-height: var(--leading-snug); }
/* Colour only where there is something to do. A dashboard where every tile is lit
   is a dashboard nobody reads. */
.console-summary li.is-urgent { border-color: var(--accent-border); background: var(--accent-soft); }
.console-summary li.is-urgent .value { color: var(--accent); }
.console-summary li.is-live { border-color: var(--ok-border); background: var(--ok-soft); }
.console-summary li.is-live .value { color: var(--ok); }

/* THE QUEUE TABLES (M28 T18, user: *"\"acme/agent\" pages need to improve design"*).

   `.listing` HAD ONE RULE — the `td.num` line below — and nothing else. Every admin page in this
   product renders `.admin-table`, which is a designed table; the agent console renders `.listing`,
   which was a bare HTML table wearing the browser's defaults. Measured: on a 1440px screen the queue
   occupied 461px of its 1368px card, columns bunched at the left with two thirds of the card empty
   and `Latest` — the message you are scanning for — squeezed into 182px.

   So this is `.admin-table`'s bones rather than a new invention, plus the one thing a queue needs that
   a settings table does not: the LAST column takes the slack. `width: 100%` on a cell in an
   auto-layout table claims the leftover space, and `max-width: 0` is what lets it then truncate
   instead of forcing the table wider than its card — the pair only works together. */
/* THE BONES ARE `.admin-table`'s, SHARED — see that rule. They were copied here with two values
   changed, `.55rem` for `.6rem` and `.04em` for `.06em`, which nobody could name and everybody would
   feel. Deleting the copy is what actually makes the two tables one object: adding `.listing` to the
   shared selector and LEAVING this below it changed nothing at all, because this came later and won.
   Measured both times — the console still read 8.8px after the first attempt. */
/* The columns either side of the name are counts and clocks: they should take what they need and no
   more, so the name and the message get the rest. */
.listing td.num, .listing th:nth-child(n+2):not(:last-child) { white-space: nowrap; width: 1%; }
/* THE MESSAGE COLUMN ABSORBS THE CARD. One line, truncated — a queue is scanned, and a row that grows
   to three lines because somebody pasted a stack trace pushes the next five rows off the screen. */
.listing td:last-child, .listing th:last-child {
  width: 100%;
  max-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-soft);
}
.listing tbody tr:last-child td { border-bottom: none; }
.listing tbody tr:hover { background: var(--surface-sunken); }
/* A MESSAGE PREVIEW NEEDS A COLUMN, NOT A SLIVER (M28 T18).

   On a 390px screen the queue's four columns left `Latest` about 20px, so the header read "LA" and
   the cell showed "do" — cut without even an ellipsis, because there was not room for one. Twenty
   pixels of somebody's sentence is not a preview; it is a column pretending to be one, taking width
   from the name, which is what an agent actually scans on a phone.

   So it goes, and only on the queue tables — `.queue` rather than `.listing`, because the same base
   class carries the Operations page's tables and their last column is not a preview. */
@media (max-width: 34rem) {
  .listing.queue th:last-child, .listing.queue td:last-child { display: none; }
}
/* THE NAME CELL, AND THE MISTAKE THE FIRST VERSION MADE. Giving the last column `width: 100%` makes
   every other column collapse to its MIN-content — which for a name is its longest word, so "Anonymous
   visitor" broke across two lines on a 1440px screen with two thirds of the table empty beside it.
   Seen in the screenshot, not in the numbers: the table measured correctly at 1318px the whole time.

   So the name itself does not wrap, which makes the column ask for the width of a whole name; the
   cap and the ellipsis are what stop one enormous name from doing to the table what the last column
   was just stopped from doing. The lines UNDER it — address, reference, subject — still stack. */
/* THE NAME COLUMN, ON BOTH TABLES (M42 T15). This was scoped to `.listing`, so when Operations moved
   to `.admin-table` it lost the protection and "General Support" wrapped onto two lines in a 74px
   column — beside a STATE column holding 1,119px. The last-column-takes-the-slack rule squeezes
   everything else to min-content, and a name's min-content is its longest word. Measured, not seen:
   at a glance it just looks like a slightly tall row. */
.listing td:first-child,
.admin-table td:first-child { white-space: normal; max-width: 22rem; min-width: 9rem; }
.listing td:first-child .who {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
.listing td.num { white-space: nowrap; }

/* ---- agent console: tabs (M17 T14.10) ---------------------------------------
   A chat and a ticket are different promises to different people, and an agent
   working one is not doing the other. `New` is a tab of its own because it is the
   only thing on the page with a clock running. Links, not JavaScript: nothing to
   fail, and a tab is an address an agent can keep. */
.console-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0;
  margin-bottom: -.5rem;
}
.console-tab {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .55rem .95rem;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  color: var(--fg-soft);
  text-decoration: none;
  font-weight: 600;
}
.console-tab:hover { background: var(--surface-sunken); color: var(--fg); }
.console-tab .count {
  font-variant-numeric: tabular-nums;
  font-size: var(--step--1);
  color: var(--muted);
  background: var(--surface-sunken);
  border-radius: var(--radius-pill);
  padding: .05rem .45rem;
}
.console-tab.is-active {
  background: var(--surface);
  border-color: var(--border);
  color: var(--fg);
  box-shadow: 0 1px 0 0 var(--surface);
}
.tab-note { margin-top: 1.1rem; }

/* Accept, or send it somewhere it fits better. Nothing else: an agent who has not
   accepted a thread has not started working it. */
.offer-form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0; }
.offer-transfer > summary { cursor: pointer; font-size: var(--step--1); color: var(--fg-soft); }
.offer-transfer[open] {
  display: grid;
  gap: .4rem;
  margin-top: .4rem;
  padding: .6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  min-width: 15rem;
}
.offer-transfer label { display: grid; gap: .2rem; font-size: var(--step--1); color: var(--fg-soft); }
.offer-transfer select, .offer-transfer input { max-width: none; }
.col-answer { width: 1%; }

/* Who passed this to you, and what they said — New-tab rows only. An auto-assignment
   is not a message from a person and gets no line. */
.offer-from {
  margin: .3rem 0 0;
  font-size: var(--step--1);
  color: var(--fg-soft);
  max-width: 34rem;
}
.offer-from q { color: var(--fg); }

/* A row that just arrived: a brief wash, then it settles into the table. Long enough to
   catch the eye moving back to the screen, short enough not to be a decoration. */
@keyframes offer-arrived {
  from { background: var(--accent-soft); }
  to   { background: transparent; }
}
tr.row-arrived > td { animation: offer-arrived 2.4s ease-out 1; }
@media (prefers-reduced-motion: reduce) {
  tr.row-arrived > td { animation: none; background: var(--accent-soft); }
}

/* The sound is a courtesy and an agent may not want it; the toggle is remembered locally. */
.sound-note { margin-bottom: -.5rem; }
.sound-note label { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer; }

/* A poll-built row links to the page rather than rebuilding a CSRF-bearing form. */
/* A LINK SHAPED LIKE A BUTTON MUST BE SHAPED LIKE THE BUTTON (M42 T3).
   Measured, this matched nothing: 12px corners where every button has 8, 13px/400 where every button
   has 15px/600, and the soft `--border` where `.ghost` uses `--border-strong`. Three differences, none
   of them decided — it was simply written on a different day. It is `.ghost` now, in `<a>` clothing. */
a.button-ish {
  display: inline-block;
  text-decoration: none;
  text-align: center;
  font: inherit;
  font-weight: 600;
  font-size: var(--step-0);
  line-height: var(--leading-snug);
  padding: .55rem 1rem;
  border: 1px solid var(--control-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg-soft);
  cursor: pointer;
}
a.button-ish:hover { border-color: var(--accent); color: var(--accent); }
a.button-ish:active { transform: translateY(1px); }
a.button-ish:hover { border-color: var(--accent); color: var(--accent); }

/* The customer leads the row; the thread id is small print beside them. */
a.who { font-weight: 600; }
.who-sub {
  margin-left: .45rem;
  font-size: var(--step--1);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}


/* ---- agent console: the new-request widget (M17 T14.10) ---------------------
   Not a tab. Offers are the one thing on this page with a clock running, and a tab
   is somewhere you have to BE to see what is in it — an agent reading their ticket
   list would miss a chat arriving with thirty seconds on it. So it sits above
   everything, on every tab, and fills itself in. */
.new-requests {
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
  border-radius: var(--radius);
  padding: .9rem 1rem 1rem;
}
/* WITH NOTHING WAITING IT IS NOT AN ALERT. The accent panel shouted the same way whether four people
   were waiting or none were, so the one state an agent should be able to read across the room looked
   identical to the one they can ignore. */
.new-requests.is-empty {
  border-color: var(--border);
  background: var(--surface-sunken);
}
.new-requests.is-empty > h2 .count { background: var(--border-strong); color: var(--fg-soft); }
.new-requests > h2 { margin-top: 0; }
.new-requests > h2 .count {
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: var(--radius-pill);
  padding: .05rem .5rem;
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}
/* Quiet when there is nothing in it: a permanently loud panel is one nobody looks at. */
/* QUIET WHEN THERE IS NOTHING (2026-09-21). Empty, this block still took the most prominent card on
   the page — a heading, two lines explaining what happens to requests that do not exist, and a third
   saying there are none. Three ways of saying nothing, above the queues an agent actually came to read.
   The explanation is for items; with no items it is noise. */
.new-requests.is-empty {
  border-color: var(--border);
  background: var(--surface);
  padding: .8rem 1rem;
}
/* ONE ROW when empty: the heading, the fact, and the sound toggle at the far end. Stacked, three
   short lines each said a version of "nothing", which is two more than the fact needs. */
.new-requests.is-empty {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  flex-wrap: wrap;
}
.new-requests.is-empty > h2 { font-size: var(--step--1); color: var(--fg-soft); margin-bottom: 0; }
.new-requests.is-empty > .hint { margin-left: auto; order: 3; }
.new-requests.is-empty > h2 .count { background: var(--surface-sunken); color: var(--muted); }
/* The hint's SENTENCE is not rendered when empty (see the template — a screen reader would otherwise
   read it to somebody with nothing waiting); the paragraph survives to carry the sound toggle. */
.new-requests.is-empty > .hint { margin: 0; font-size: var(--step--1); }
.new-requests.is-empty > .hint .sound-toggle { margin-left: 0; }
.new-requests.is-empty > .empty { margin: 0; font-size: var(--step--1); order: 2; }
.new-requests .sound-toggle { margin-left: .6rem; white-space: nowrap; }

.request-list { margin: .6rem 0 0; display: grid; gap: .5rem; }
.request {
  display: grid;
  grid-template-columns: 5.5rem 1fr auto auto;
  align-items: start;
  gap: .75rem;
  padding: .7rem .85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* CHAT or TICKET, said outright — the two are different work and the answer differs. */
.request-kind {
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: .2rem .5rem;
  border-radius: var(--radius-pill);
  text-align: center;
}
.request-kind.is-chat { background: var(--ok-soft); color: var(--ok); }
.request-kind.is-ticket { background: var(--surface-sunken); color: var(--fg-soft); }

.request-who { display: grid; gap: .2rem; min-width: 0; }
.request-who > a { font-weight: 600; }
.request-from { font-size: var(--step--1); color: var(--fg-soft); }
.request-from q { color: var(--fg); }
/* A hand-off that came back (M23 T13). Warn-toned and given a rule of its own so it does not read as
   another grey sub-line: every other line on the row describes the customer, and this one describes a
   thing the agent believed was off her desk and is not. */
.request-back {
  display: block;
  font-size: var(--step--1);
  color: var(--warn);
  background: var(--warn-soft);
  border-left: 3px solid var(--warn-border);
  border-radius: 3px;
  padding: .15rem .45rem;
  margin: .15rem 0;
}
.request-back strong { font-weight: 600; }
.request-latest {
  font-size: var(--step--1);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.request-clock {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--warn);
  white-space: nowrap;
}
.request-actions .offer-form { justify-content: flex-end; }

@keyframes request-arrived {
  from { background: var(--accent-soft); }
  to   { background: var(--surface); }
}
.request-arrived { animation: request-arrived 2.4s ease-out 1; }
@media (prefers-reduced-motion: reduce) {
  .request-arrived { animation: none; background: var(--accent-soft); }
}
@media (max-width: 52rem) {
  .request { grid-template-columns: 1fr auto; }
  .request-kind { grid-column: 1; justify-self: start; }
  .request-who { grid-column: 1 / -1; }
}


/* ---- agent console: one top bar (M17 T14.10) --------------------------------
   Was a hero block plus a nav below it, which pushed the thing with a clock on it
   below the fold. Identity left, controls right, one line. */
/* A BAR, not a line of text with a rule under it (M40 T23).
   It was four pieces of near-identical prose — brand, name, tenant, desks — followed by two
   underlined links, on a page an agent has open all day. Giving it a surface separates the thing
   they act ON from the thing they act WITH, and sticking it to the top means the presence toggle and
   the sign-out do not scroll away under a long queue. */
.agent-topbar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: -1rem 0 1.2rem;
  padding: .85rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
/* The NAME leads and the rest is context beneath it (2026-09-21). Brand, name, tenant and three desk
   names used to run together as one line of similar-weight text, so who you are signed in as read no
   louder than which desks you happen to staff. */
.agent-identity {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: .75rem;
  min-width: 0;
}
/* The product name is a MARK, not a heading. It was set at the same weight as the agent's own name,
   so the first thing the eye landed on was the word "aichat" — which is the one thing on this bar
   nobody needs. */
.agent-home {
  grid-row: 1 / span 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 700;
  text-decoration: none;
  overflow: hidden;
  text-indent: -99em;   /* the mark is the square; the word is for a screen reader */
}
.agent-home::after {
  content: "\2022";
  text-indent: 0;
  float: left;
  width: 100%;
  text-align: center;
  font-size: var(--step-1);
}
.agent-name { font-weight: 700; font-size: var(--step-0); letter-spacing: -.01em; line-height: var(--leading-tight); }
.agent-desks { grid-column: 2; color: var(--muted); font-size: var(--step--2); line-height: var(--leading-snug); }
/* The desks are the least-read thing on this bar and were taking the most room. */
.agent-desk-list { color: var(--fg-soft); opacity: .75; }
.agent-desk-list::before { content: " · "; }
/* It wraps, but its CONTENTS did not: measured at 390px the Sign-out control sat 2px past the card's
   own edge, which is the sort of thing that reads as the page being slightly broken (M28 T18). */
.agent-controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; min-width: 0; }

/* The two text links beside the presence pill were underlined prose sitting among controls. They do
   things; they should look like it. */
.agent-controls .link-ish {
  padding: .35rem .7rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 600;
  text-decoration: none;
  background: none;
  cursor: pointer;
}
.agent-controls .link-ish:hover { color: var(--fg); background: var(--surface-sunken); }
.agent-controls .link-ish:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Presence reads as a state, not a sentence: the dot carries it and the word confirms it. */
.presence {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .4rem .85rem;
  border-radius: var(--radius-pill);
  font-size: var(--step--1);
  font-weight: 700;
}
.presence-dot { width: .55rem; height: .55rem; border-radius: 50%; background: currentColor; }
.presence-on { color: var(--ok); border-color: var(--ok-border); background: var(--ok-soft); }
.presence-off { color: var(--warn); border-color: var(--warn-border); background: var(--warn-soft); }
button.link-ish {
  background: none;
  border: none;
  color: var(--fg-soft);
  text-decoration: underline;
  text-underline-offset: .18em;
  padding: .35rem .2rem;
  cursor: pointer;
}
button.link-ish:hover { color: var(--accent); }


/* Threads that came back: accepted, and the customer has spoken since. Calmer than the
   new-request panel — nothing here has a clock, it is simply owed a reply. */
.awaiting {
  border: 1px solid var(--warn-border);
  background: var(--warn-soft);
  border-radius: var(--radius);
  padding: .8rem 1rem .9rem;
}
.awaiting > h2 { margin-top: 0; }
.awaiting > h2 .count {
  background: var(--warn);
  color: var(--ink-on-color);
  border-radius: var(--radius-pill);
  padding: .05rem .5rem;
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}
.awaiting .request-clock { color: var(--fg-soft); }

/* The second number on a tab: how much of it wants this agent. Coloured, because
   the first number is a size and this one is a task. */
.console-tab .count-waiting {
  background: var(--warn);
  color: var(--ink-on-color);
}
.console-tab.is-active .count-waiting { background: var(--warn); }

/* Landed work is owed a reply, not counting down: the number is how long they have
   waited, and it must not read as a threat. */
.request-clock.is-waiting { color: var(--fg-soft); font-weight: 600; }


/* ── Agents roster (M17 T6b/T10 · design pass 2026-09-05) ──────────────────────────────────────────
   The page answers "who is at their desk right now". These rules exist so it answers it BEFORE the
   reader has to work through the rows, and so a list of people stops reading as a page of forms. */

/* The strip is the tenant's answer; the tiles below it are the per-person one. Available is the only
   number here that is a state of the SERVICE, so it is the only one that carries colour. */
.agent-strip { margin-bottom: 1.4rem; }
.tile-ok { background: var(--ok-soft); border-color: var(--ok-border); }
.tile-ok .tile-value { color: var(--ok); }
.tile-ok .tile-label { color: color-mix(in oklab, var(--ok) 74%, var(--fg)); }

/* A desk nobody staffs. Warm rather than red: nothing is broken, but every escalation routed there
   is waiting on a person who does not exist. */
.agent-gap { margin: 0 0 1.4rem; }
.agent-gap a { font-weight: 600; }

/* "since when", beside the badge that says "where". Plain text, not a badge — it is the sentence the
   badge needs, and giving it a border would make five signals look like six facts. */
.src-note { font-size: var(--step--1); color: var(--muted); }

/* A disabled agent is roster history, not staffing: still readable, still actionable, visibly not
   part of the rota. The row keeps full contrast on hover so it can still be worked with. */
.src-row.is-off { opacity: .62; }
.src-row.is-off:hover { opacity: 1; }

/* Name and email are typed once and never again, so they fold. Styled as the quiet disclosure the
   chunk panel already uses — same affordance, same weight, one page's worth of consistency. */
.row-edit { margin: .35rem 0 .1rem; font-size: var(--step-0); }
.row-edit > summary { cursor: pointer; color: var(--muted); width: max-content; }
.row-edit > summary:hover { color: var(--accent); }
.row-edit-form {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  margin: .5rem 0 0;
}
.row-edit-form input { width: auto; min-width: 14rem; padding: .38rem .55rem; font-size: var(--step-0); }
.row-edit-form button { padding: .4rem .8rem; font-size: var(--step-0); }
.row-edit .help { margin: .4rem 0 0; }

/* The one-time password. It is readable for exactly ONE page load and is the most important thing on
   the page while it exists, so it is drawn as a credential to be copied rather than as a sentence to
   be read past. */
.notice-strong {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  margin: 0 0 1.4rem;
  padding: 1rem 1.2rem;
  border-color: var(--accent);
  border-left-width: 4px;
}
.secret-label { font-size: var(--step-0); }
.secret-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.secret {
  display: inline-block;
  padding: .45rem .8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg);
  /* The token, not a fourth copy of the stack (M42 T2). */
  font-family: var(--font-mono);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: .06em;
  /* A 64-CHARACTER CREDENTIAL IS ONE UNBREAKABLE WORD (M42 T10). Measured at 390px it rendered 576px
     wide and pushed the whole page sideways — the one page whose entire job is to be copied FROM.
     `anywhere` rather than `break-word` because a monospace secret has no break opportunities at all
     for the softer rule to find. */
  max-width: 100%;
  overflow-wrap: anywhere;
  user-select: all;      /* one click selects the whole credential, for anyone without the clipboard API */
}

/* Ships hidden and is revealed by script only where the clipboard API exists — a copy button that
   cannot copy is worse than no button. */
.copy-btn {
  padding: .3rem .7rem;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.copy-btn:hover { background: var(--surface-sunken); color: var(--accent); }
.copy-btn.is-copied { color: var(--ok); border-color: var(--ok-border); background: var(--ok-soft); }
/* When even the fallback is refused the button says what to press instead of pretending it worked. */
.copy-btn.is-copy-failed { color: var(--warn); border-color: var(--warn-border); background: var(--warn-soft); }

.agent-links { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

@media (max-width: 46rem) {
  .row-edit-form input { width: 100%; min-width: 0; }
  .row-edit-form button { width: 100%; }
}

/* ── Customer ticket surface (M21) ────────────────────────────────────────────────────────────────
   Deliberately built from what is already here — .hero, .notice, .listing, .badge, .chat-log and the
   message bubbles are all the visitor chat's, because this page IS the other half of that
   conversation and a customer who has seen one should recognise the other. Only the few things with
   no existing equivalent are new below. */
.stack-form { display: flex; flex-direction: column; gap: .4rem; max-width: 32rem; margin: 1rem 0; }
.stack-form label { font-size: var(--step--1); font-weight: 600; color: var(--fg-soft); }
.stack-form input, .stack-form textarea {
  padding: .55rem .7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg);
  font: inherit;
}
.stack-form button { align-self: flex-start; margin-top: .3rem; }

/* A submit that has to read as a link: signing out is not a primary action and a full button beside
   the address it belongs to reads like the page's purpose. */
.linkish {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
}

.ticket-auth p, .ticket-list .empty { color: var(--fg-soft); max-width: 42rem; }
.ticket-thread .crumbs { margin: 0 0 .6rem; font-size: var(--step-0); }
.ticket-thread h2 { display: flex; align-items: center; gap: .6rem; margin: 0 0 .8rem; }
.ticket-thread .hint { color: var(--fg-soft); font-size: var(--step-0); }
/* ── Raising a query (M21 T7) ──────────────────────────────────────────────────────────────────────
   The form is the first thing a visitor with no session sees, so it gets the room: one column, labels
   above their fields, and the two required ones marked. */
.ticket-new { margin: 1.5rem 0; }
.ticket-new h2, .ticket-raised h2 { margin: 0 0 .75rem; }
.ticket-new .stack-form { max-width: 34rem; }
.ticket-new select { width: 100%; }

/* Show / hide on a password field. The button sits INSIDE the field's box so the pair reads as one
   control — a separate button beside it looks like a second thing to fill in. */
.pw-wrap { position: relative; display: block; max-width: var(--measure); }
.pw-wrap input[type="password"], .pw-wrap input[type="text"] { width: 100%; padding-right: 2.9rem; }
/* `.pw-wrap .pw-toggle` rather than `.pw-toggle`, and the extra class is load-bearing: `.auth-form
   button` is (0,1,1) and beat a single class, so the toggle came out 299px wide with 1.1rem of margin
   pushing it below the field — a form's own button styling reaching an element that is not one of its
   buttons. Measured before assuming: button 565→864 against a field 571→869. */
.pw-wrap .pw-toggle {
  position: absolute;
  top: 50%;
  right: .35rem;                 /* the right-hand side: where every browser and password manager puts it */
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* `width: auto` because a form can style its own buttons full-width — `.auth-form` does, and this one
     came out 299px wide, the whole field, anchored to nothing. An absolutely positioned element does not
     escape a width rule that names it. */
  width: auto;
  min-width: 0;
  margin: 0;
  padding: .3rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
  line-height: 1;
}
/* The stroke through the eye is present only while the password is visible: closed eye = hidden,
   open eye = you can read it. The icon therefore reports the STATE, and its label says the action. */
.pw-wrap .pw-toggle .pw-slash { opacity: 1; }
.pw-wrap .pw-toggle.is-shown .pw-slash { opacity: 0; }
.pw-wrap .pw-toggle:hover { background: var(--bg); }
.pw-wrap .pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Our own captcha. Deliberately plain: the picture is the only demand this form makes of a person, so
   it should look like a question and not like a security apparatus. */
.captcha { margin: .4rem 0 .2rem; }
.captcha-art {
  display: inline-block;
  padding: .25rem .5rem;
  margin-bottom: .45rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  line-height: 0;
}
.captcha input[type=text] { max-width: 7rem; }
.captcha-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .45rem; }
.captcha-new { font-size: var(--step--1); }
.captcha-new.is-busy { opacity: .5; pointer-events: none; }

/* The honeypot. Off-screen rather than `display:none`, because some form-fillers skip hidden inputs and
   the whole value of the field is that an automated one fills it. Never focusable, never announced. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The receipt. The reference is the one thing on this screen that matters, so it is the one thing sized
   to be read across a desk and copied without squinting. */
.ticket-raised { margin: 1.5rem 0; }
.raised-ref {
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  font-weight: 700;
  letter-spacing: .06em;
  margin: .25rem 0 .6rem;
  font-family: var(--font-mono);
}

.ticket-subject { font-weight: 600; margin: 0 0 .4rem; }
/* The business's own field. Labelled in the admin's words, because the label is stored on the ticket. */
.ticket-extra { margin: 0 0 .8rem; font-size: var(--step-0); }
.ticket-extra .extra-label {
  color: var(--fg-soft);
  text-transform: uppercase;
  font-size: var(--step--2);
  letter-spacing: .06em;
  margin-right: .4rem;
}

/* Four words, and they should not all look the same: "Answered" means the customer has to act. */
.badge.status-open { background: var(--warn-soft); color: var(--warn); }
.badge.status-answered { background: var(--ok-soft); color: var(--ok); }
.badge.status-closed { background: var(--border); color: var(--fg-soft); }

/* Disable and Delete at the TOP RIGHT of a desk (user, 2026-09-18) is no longer a rule of this page:
   M28 T21 made it the shape of every listing row, on the user's call, so the page-scoped copy that
   lived here is gone rather than sitting underneath saying the same thing more loudly. */

/* A "?" beside a label, holding what used to be a paragraph under it. The text is still in the DOM and
   still reachable by keyboard — this hides it until asked for, it does not throw it away. */
.tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05rem;
  height: 1.05rem;
  margin-left: .35rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg-soft);
  font-size: var(--step--2);
  font-weight: 700;
  cursor: help;
  position: relative;
  vertical-align: middle;
}
.tip:hover, .tip:focus-visible { border-color: var(--accent); color: var(--accent); }
.tip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tip-body {
  display: none;
  /* FIXED, not absolute, and placed by script (2026-09-18).
     `.desk-panels` scrolls, so ANY absolutely-positioned child is clipped by it — above the marker the
     tooltip lost its top, below it lost its bottom, and near the right-hand column it lost its right.
     A fixed element is positioned against the viewport and cannot be clipped by an ancestor's overflow;
     the script then clamps it so it cannot leave the screen either. */
  position: fixed;
  z-index: 60;
  width: max-content;
  max-width: min(28rem, 70vw);
  padding: .6rem .75rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--fg);
  font-size: var(--step--1);
  font-weight: 400;
  line-height: var(--leading-normal);
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  cursor: auto;
  white-space: normal;
}
.tip-body.is-open { display: block; }
/* No script, no tooltips at all — the help text is never converted in the first place — so there is no
   CSS-only path to keep working here. */

/* ── A desk's configuration, in a dialog with tabs (2026-09-18) ────────────────────────────────────
   Folding it shortened the page without making it a LIST: everything a desk owns still arrived as one
   continuous column the moment it opened. The tabs are RADIO BUTTONS and no script — each panel holds
   its own `<form>` and forms cannot nest, so the tab state has to live outside them. */
.desk-edit {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
  margin: .9rem 0 0;
  padding: .55rem .8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color .12s, background .12s, transform .12s;
}
.desk-edit:hover, .desk-edit:focus-visible {
  border-color: var(--accent);
  background: var(--surface);
  transform: translateY(-1px);
}
.desk-edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.desk-edit::after { content: '▸'; color: var(--accent); font-size: var(--step--1); }
/* The word itself carries the accent, because a bar that only reveals itself on hover is a bar nobody
   hovers: on a touch screen there is no hover at all. */
.desk-edit .desk-config-label { color: var(--accent); font-weight: 600; }

.desk-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0;
  width: min(62rem, 94vw);
  max-height: 88vh;
  background: var(--surface);
  color: inherit;
}
.desk-dialog::backdrop { background: rgb(0 0 0 / .45); }
/* WITHOUT JAVASCRIPT the dialog is still reachable: the Edit link is an anchor, and `:target` opens it
   in place. A modal nobody can open is worse than no modal. */
.desk-dialog:target { display: block; position: static; margin: 1rem 0; width: auto; max-height: none; }
.desk-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 1;
}
.desk-dialog-head h3 { margin: 0; }
.desk-dialog-close {
  text-decoration: none;
  font-size: var(--step-2);
  line-height: 1;
  color: var(--fg-soft);
  padding: .1rem .5rem;
  border-radius: var(--radius);
}
.desk-dialog-close:hover { background: var(--bg); color: var(--fg); }

.desk-tabs { display: block; }
.desk-tab-radio { position: absolute; opacity: 0; pointer-events: none; }
.desk-tablist {
  display: flex;
  gap: .25rem;
  flex-wrap: wrap;
  padding: .75rem 1.25rem 0;
  border-bottom: 1px solid var(--border);
}
.desk-tablist label {
  padding: .45rem .8rem;
  border-radius: var(--radius) var(--radius) 0 0;
  cursor: pointer;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
  border: 1px solid transparent;
  border-bottom: 0;
  margin-bottom: -1px;
}
.desk-tablist label:hover { color: var(--fg); }
.desk-panels { padding: 1.25rem; overflow-y: auto; max-height: 68vh; }
.desk-dialog:target .desk-panels { max-height: none; }
.desk-panel { display: none; }

/* One rule per tab: the checked radio lights its label and shows its panel. Five tabs, five pairs —
   verbose, and the price of tabs that work with the script switched off. */
.desk-tabs > input:nth-of-type(1):checked ~ .desk-tablist label:nth-of-type(1),
.desk-tabs > input:nth-of-type(2):checked ~ .desk-tablist label:nth-of-type(2),
.desk-tabs > input:nth-of-type(3):checked ~ .desk-tablist label:nth-of-type(3),
.desk-tabs > input:nth-of-type(4):checked ~ .desk-tablist label:nth-of-type(4) {
  background: var(--surface);
  color: var(--fg);
  border-color: var(--border);
}
.desk-tabs > input:nth-of-type(1):checked ~ .desk-panels > .desk-panel:nth-of-type(1),
.desk-tabs > input:nth-of-type(2):checked ~ .desk-panels > .desk-panel:nth-of-type(2),
.desk-tabs > input:nth-of-type(3):checked ~ .desk-panels > .desk-panel:nth-of-type(3),
.desk-tabs > input:nth-of-type(4):checked ~ .desk-panels > .desk-panel:nth-of-type(4) { display: block; }

/* A named division INSIDE a tab. "Outgoing mail" is three separate things — the mailbox, how replies
   are addressed, and a test send — and three unlabelled forms in a column read as one long form with
   buttons scattered through it. */
.desk-subhead {
  margin: 1.6rem 0 .8rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--fg-soft);
}
.desk-panel > .desk-subhead:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* Labels ABOVE their fields inside the dialog, at a readable size. The mail forms were laid out as a
   wrapping flex row with the label inline, which is compact on a wide admin page and cramped in a modal
   — the label and the box it belongs to could end up on different lines. */
.desk-panel .mail-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); gap: .9rem 1.1rem; align-items: start; max-width: none; }
.desk-panel .mail-fields > label { display: flex; flex-direction: column; gap: .3rem; }
.desk-panel .mail-fields .label-text { font-weight: 600; font-size: var(--step--1); }
.desk-panel .mail-fields input, .desk-panel .mail-fields select, .desk-panel .mail-fields textarea { width: 100%; }
.desk-panel .mail-fields .mail-signature, .desk-panel .mail-fields .mail-inherit,
.desk-panel .mail-fields .mail-actions, .desk-panel .mail-fields > .help,
.desk-panel .mail-fields > .hint { grid-column: 1 / -1; }
.desk-panel .mail-actions { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }

/* The name field: one row, box and button together, so "Save name" cannot drift away from it. */
.desk-field-form { margin-bottom: 1.4rem; }
.desk-field-form > label { display: block; font-weight: 600; font-size: var(--step--1); margin-bottom: .3rem; }
.desk-field-row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
.desk-field-row input { flex: 1 1 18rem; }

/* Inside the dialog the mail panels are already the whole tab — they do not need a card of their own,
   and their heading repeats the tab it is under. De-emphasised rather than removed: the BADGE and the
   address beside it are the useful part of that line, and they are what somebody opening the tab is
   checking. */
.desk-panel > .group { box-shadow: none; border: 0; padding: 0; background: transparent; }
.desk-panel > .group > h3 {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 0 0 .9rem;
}

/* (superseded) One desk's configuration, folded. The summary answers what an admin usually came to check — does it
   send, does it receive, who is on it — so opening the panel is a choice rather than the only way to
   find out. */
.desk-config { margin: .9rem 0 0; border-top: 1px solid var(--border); padding-top: .8rem; }
.desk-config > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
}
.desk-config > summary::-webkit-details-marker { display: none; }
.desk-config > summary::after {
  content: '▾';
  color: var(--fg-soft);
  font-size: var(--step--1);
  transition: transform .12s;
}
.desk-config[open] > summary::after { transform: rotate(180deg); }
.desk-config-label { font-weight: 600; font-size: var(--step--1); }
.desk-facts { display: flex; gap: .4rem; flex-wrap: wrap; margin-left: auto; }
.desk-config-body { padding-top: .9rem; }

/* An agent's queue row: the subject is what the thread is ABOUT, and it reads before the last thing
   said. Quiet enough not to compete with the customer's name above it. */
.who-subject {
  display: block;
  font-size: var(--step--1);
  color: var(--fg);
  margin-top: .15rem;
}
/* When there is nothing offered, nobody on the page and nothing open, say so in one line instead of
   three tiles — the queue below is what the agent came for. */
/* THE SUMMARY SITS WITH WHAT IT SUMMARISES (M40 T23). It was a line of grey text floating between
   the topbar and the requests strip, belonging to neither — the one sentence on this page that tells
   an agent whether they have anything to do, styled as an aside. */
.console-summary-quiet {
  margin: 0 0 .9rem;
  padding: .75rem 1.1rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--ok-border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--fg-soft);
  font-size: var(--step--1);
}

/* The settings filter. Sticky, because on a 2,000px page the box you typed in should not scroll away
   from the results it is filtering. */
.settings-filter {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .7rem 0 .9rem;
  background: var(--bg);
  flex-wrap: wrap;
}
.settings-filter label { font-weight: 600; }
.settings-filter input[type=search] { flex: 1 1 22rem; max-width: 34rem; }
/* Safari draws its own rounded capsule over ours unless this is said. */
.settings-filter input[type=search] { -webkit-appearance: none; appearance: none; }
.settings-filter input[type=search]::-webkit-search-cancel-button { cursor: pointer; }
.settings-filter .hint { margin: 0; white-space: nowrap; }

/* The key, beside its label: an admin who has read a plan file knows `tickets.form_extra_label`, not
   "One extra question (its label)". Quiet enough to ignore when you are not looking for it. */
.field-key {
  display: inline-block;
  margin-left: .45rem;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 400;
  color: var(--fg-soft);
}

/* A tile that is a way in, not a poster. It keeps the tile's shape exactly — only the affordances are
   added — because the strip must not become a row of buttons; the numbers are still the content. */
a.tile-link { text-decoration: none; color: inherit; display: flex; transition: border-color .12s, transform .12s; }
a.tile-link:hover, a.tile-link:focus-visible { border-color: var(--accent); transform: translateY(-1px); }
a.tile-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
a.tile-link .tile-label { text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 2px; }

/* Development sign-in (dev only). Deliberately UGLY and unmistakable: a dashed warning border and the
   word development in the heading, because the one thing this panel must never do is look like a feature
   somebody could screenshot as production behaviour. */
.dev-signin {
  margin: 2.5rem 0 0;
  padding: .9rem 1rem;
  border: 2px dashed var(--warn);
  border-radius: var(--radius);
  background: var(--warn-soft);
}
.dev-signin h2 { margin: 0 0 .35rem; font-size: var(--step--1); text-transform: uppercase; letter-spacing: .06em; }
.dev-signin p { margin: 0 0 .6rem; font-size: var(--step--1); }
.dev-signin .inline-form { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* The credential form (M43 follow-up). A label column that COLLAPSES rather than squeezing its field:
   this panel sits inside the console's content column, so the width that matters is the container's,
   not the window's — the lesson `container-not-viewport` cost twice already. */
.dev-signin-form {
  display: grid;
  gap: .55rem;
  max-width: 34rem;
  /* ITS OWN containment context, named, exactly as .src-list does it. An unnamed @container with no
     ancestor container is a rule whose behaviour I would be guessing at; a named one cannot be
     resolved against anything but this. */
  container-type: inline-size;
  container-name: devsignin;
}
.dev-signin-row {
  display: grid;
  grid-template-columns: minmax(min(9rem, 100%), auto) minmax(0, 1fr);
  align-items: center;
  gap: .5rem .75rem;
}
.dev-signin-row > label, .dev-signin-as > legend {
  font-size: var(--step--1);
  font-weight: 600;
}
.dev-signin-row input[type="text"], .dev-signin-row input[type="password"], .dev-signin-row input[type="email"] {
  /* 16px minimum, or every phone zooms the moment the field is focused (M39 T4). */
  font-size: max(1rem, var(--step-0));
  padding: .4rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.dev-signin-as { border: 0; margin: 0; padding: 0; display: grid; gap: .35rem; }
.dev-signin-sites { display: grid; gap: .2rem; padding-left: 1.3rem; }
.dev-signin-as > legend { padding: 0; }
.dev-signin-choice { display: flex; align-items: center; gap: .4rem; font-size: var(--step--1); }
.dev-signin-form > button { justify-self: start; }
.dev-signin-denied {
  padding: .45rem .6rem;
  border-left: 3px solid var(--danger);
  background: #fff;
  font-weight: 600;
}
.dev-signin-keys {
  margin: 0 0 .6rem;
  padding: .5rem .6rem;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-size: var(--step--1);
}

@container devsignin (max-width: 28rem) {
  .dev-signin-row { grid-template-columns: minmax(0, 1fr); }
}

/* Which way a turn arrived. Quiet — it is context, not content, and a customer reading their own thread
   should notice it only when they look for it. */
.ticket-turn-when {
  font-weight: 400;
  color: var(--fg-soft);
  font-size: var(--step--2);
  margin-left: .35rem;
}
.ticket-turn-via {
  font-weight: 400;
  color: var(--fg-soft);
  font-size: var(--step--2);
  margin-left: .35rem;
}

/* ── The customer's own ticket list (rebuilt 2026-09-19) ───────────────────────────────────────────
   Cards rather than table rows: this is the page a customer lands on after signing in, and the thing
   they are looking for is "which one is mine and what is happening to it" — a subject they recognise,
   then the state. A four-column table made the reference the first thing, which is the one part they
   have to look up rather than read. */
.ticket-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: .9rem;
}
.ticket-list-head h2 { margin: 0; display: flex; align-items: center; gap: .5rem; }

/* Whose turn it is, beside the status (2026-09-21). Coloured only when it is OURS: a queue is read to
   find what still needs doing, and everything else on the row is already quiet. */
.inbound-table .help.turn-ours { color: var(--danger); font-weight: 600; }
.inbound-table .help.turn-theirs { color: var(--fg-soft); }
.inbound-table .help { display: block; }

/* ── The manuals (M37) ────────────────────────────────────────────────────────────────────────────
   Prose, so it gets a reading measure — the one place in this product where a long line genuinely
   hurts, because these paragraphs are read rather than scanned. */
.manual { max-width: 52rem; }
.manual .lede { font-size: var(--step-0); color: var(--fg-soft); margin: 0 0 1.4rem; }
.manual h3 {
  margin: 1.8rem 0 .5rem;
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -.01em;
}
.manual h3:first-of-type { margin-top: .6rem; }
.manual p, .manual li { line-height: var(--leading-prose); }
.manual ul, .manual ol { margin: .5rem 0 .9rem; padding-left: 1.2rem; }
.manual li { margin-bottom: .35rem; }
.manual .admin-table { margin: .6rem 0 1rem; }
/* Where to go and do the thing just described. Quiet, and at the end of the section it belongs to. */
.manual .where { font-size: var(--step--1); color: var(--fg-soft); margin: .2rem 0 0; }
/* The settings reference is wider than prose: it is a table, and a 52rem measure would wrap the help
   text into a column. */
.manual:has(> h2 + .lede + h3) .admin-table { max-width: none; }

.manual-index { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); gap: 1rem; }
.manual-card {
  display: block;
  padding: 1.1rem 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
}
.manual-card:hover, .manual-card:focus-visible { border-color: var(--accent); }
.manual-card h2 { margin: 0 0 .35rem; font-size: var(--step-0); text-transform: none; letter-spacing: 0; }
.manual-card p { margin: 0; font-size: var(--step--1); color: var(--fg-soft); line-height: var(--leading-normal); }

/* The finished email, in a dialog (M34 T9, 2026-09-21). Wide enough for a 600px email plus its
   margins, and tall enough that a two-paragraph template needs no scrolling. */
.preview-dialog { max-width: 46rem; width: 92vw; }
.preview-dialog .preview-subject { margin: 1rem 1.25rem .6rem; display: flex; gap: .5rem; align-items: baseline; }
.preview-frame {
  display: block;
  width: calc(100% - 2.5rem);
  height: 26rem;
  margin: 0 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.preview-dialog > .help { margin: .7rem 1.25rem 1.25rem; }
.preview-dialog .errors { margin: 0 1.25rem; }

/* The reuse controls, folded per reply (2026-09-21). Quiet, because marking a reply reusable is the
   rare act — most replies are about one customer's account and stay that way. */
.msg > details.reuse > summary {
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--fg-soft);
  padding: .1rem 0;
}
.msg > details.reuse > summary:hover { color: var(--fg); }
.msg > details.reuse[open] > summary { margin-bottom: .4rem; }

/* What else this customer has asked (M28 T7). Folded, because most customers have no history and a
   panel that is usually empty is one people learn to skip. */
.customer-history { margin: 0 0 .8rem; }
.customer-history > summary { cursor: pointer; font-size: var(--step--1); color: var(--fg-soft); }
.customer-history > summary:hover { color: var(--fg); }
.hist-threads { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .3rem; }
.hist-threads li {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  flex-wrap: wrap;
  font-size: var(--step--1);
}
/* The subject takes the room left over: it is the part an agent reads to decide whether to open it. */
.hist-subject { flex: 1 1 12rem; min-width: 0; color: var(--fg-soft); }
.hist-threads .hist-when { color: var(--fg-soft); white-space: nowrap; }

/* The three facts an agent acts on, on one line, with the rest behind a disclosure (2026-09-21).
   Eleven label/value rows used to open this page and the conversation began below them. */
.thread-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .4rem;
}
.thread-head h2 { margin: 0; }
.thread-ref { font-family: var(--font-mono); font-size: var(--step--1); color: var(--fg-soft); }
.thread-facts { margin: 0 0 1rem; }
.thread-facts > summary {
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--fg-soft);
  list-style-position: outside;
}
.thread-facts > summary:hover { color: var(--fg); }
.thread-facts .meta { margin-top: .7rem; }

/* ── The letterhead editor (M34 T9) ───────────────────────────────────────────────────────────────
   Form and preview side by side, because the whole point is watching one change the other. Stacked on a
   narrow screen: a 600px email beside a form does not fit anywhere smaller and the preview is the half
   that can wait. */
.letterhead-split { display: grid; grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); gap: 1.5rem; }
@media (max-width: 62rem) { .letterhead-split { grid-template-columns: 1fr; } }
.letterhead-form { max-width: none; margin: 0; }
.letterhead-form .help { margin: -.1rem 0 .55rem; }
/* `.accent-row` lived here — the setup wizard's own colour-picker layout, replaced by the shared
   `.color-field` + `.color-swatch` pair (M40 T58). It had no focus ring and no appearance reset,
   on a control its own script made keyboard-focusable. */
.accent-value { font-family: var(--font-mono); font-size: var(--step--1); color: var(--fg-soft); }
.letterhead-preview { margin: 0; }
.letterhead-preview figcaption {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--fg-soft);
  margin-bottom: .5rem;
}
/* The email renders in its OWN document: its styles cannot reach the admin page, and the admin page's
   cannot flatter the email into looking better than it will in an inbox. */
.letterhead-preview iframe {
  width: 100%;
  height: 30rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

/* A rating count that does not depend on an emoji font being installed (2026-09-21). */
.vote { font-variant-numeric: tabular-nums; }
.vote-up { color: var(--ok); }
.vote-down { color: var(--danger); }

/* ── What happened to a thread (M28 T12) ──────────────────────────────────────────────────────────
   A list, not a table: every row is one short sentence, and a four-column grid for "Priya replied" would
   be furniture around six words. The time leads, because the question this answers is "when did somebody
   do something", and a reader scanning for the last human act reads down one edge. */
.thread-history .hist { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.thread-history .hist li {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: .1rem .8rem;
  align-items: baseline;
  font-size: var(--step--1);
}
.hist-when { color: var(--fg-soft); font-variant-numeric: tabular-nums; }
.hist-act { grid-column: 2; }
.hist-who { font-weight: 600; }
.hist-verb { color: var(--fg-soft); }
/* The reason, on its own line under the sentence it belongs to — a re-escalation's detail runs to a
   full clause and putting it inline would wrap the column into a paragraph. */
.hist-detail { grid-column: 2; color: var(--fg-soft); font-size: var(--step--1); }
.thread-history .hist li.empty { grid-template-columns: 1fr; color: var(--fg-soft); }
.hist-system { margin-top: .9rem; }
.hist-system > summary { cursor: pointer; color: var(--fg-soft); font-size: var(--step--1); }
.hist-system > summary:hover { color: var(--fg); }
.hist-system .hist { margin-top: .6rem; padding-left: .2rem; }

/* Who dealt with a message, and who has it now — two facts stacked, the second quiet because it is
   only there when it DIFFERS from the first (M31, 2026-09-21). */
/* The name is a LINK to the page that can change it (2026-09-21), so it carries an underline on hover
   and nothing else: a row of blue names would make the column shout where it should report. */
.handled-by { display: block; color: inherit; text-decoration: none; }
.handled-by:hover, .handled-by:focus-visible { text-decoration: underline; }
.inbound-table a.badge { text-decoration: none; }
.inbound-table a.badge:hover, .inbound-table a.badge:focus-visible { text-decoration: underline; }
.inbound-table .handled-by + .help { display: block; margin-top: .1rem; }

/* ── The customer's own ticket, as correspondence (rebuilt 2026-09-19) ─────────────────────────────
   The thread used the live chat's bubbles, where alternating alignment tells you who is speaking in a
   fast exchange. On a ticket with one message that is a narrow box floating against the right margin
   with a page of white space beside it. */
.ticket-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: .25rem;
}
.ticket-head h2 { margin: 0; }
.ticket-meta {
  display: flex;
  gap: .75rem;
  flex-wrap: wrap;
  margin: 0 0 1.1rem;
  font-size: var(--step--1);
  color: var(--fg-soft);
}
.ticket-meta-ref { font-family: var(--font-mono); }
/* A separator, because "AC-P92KWF with our team" is two facts reading as one sentence. */
.ticket-meta span + span::before { content: "·"; margin-right: .75rem; }

.ticket-turns { list-style: none; margin: 0 0 1.4rem; padding: 0; display: grid; gap: .75rem; }
.ticket-turn {
  padding: .8rem 1rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
/* Which side wrote it, said with an edge rather than with alignment — so both read left to right and a
   thread of one message does not look broken. */
.ticket-turn-visitor { border-left-color: var(--accent); }
.ticket-turn-human { border-left-color: var(--ok); background: var(--bg); }
.ticket-turn-assistant { border-left-color: var(--border); background: var(--bg); }
.ticket-turn-who {
  display: block;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--fg-soft);
  margin-bottom: .35rem;
}
.ticket-turn-body { white-space: pre-wrap; line-height: var(--leading-normal); }

/* The reply spans the thread, so the page has ONE right edge instead of three: the turns ended at 712px
   and the box you answer in stopped 100px short of them, which reads as a box that failed to stretch. */
.ticket-thread .stack-form { max-width: none; }

/* CLOSING IS NOT WHAT THIS PAGE IS FOR. "I no longer need help" was rendering at the same weight and
   size as "Send reply" directly beneath it, so the two competed — on a page whose whole purpose is the
   one above. Below a rule, at the size of a footnote, and still perfectly clickable. */
.ticket-verbs {
  margin-top: 1.3rem;
  padding-top: .85rem;
  border-top: 1px solid var(--border);
}
.ticket-verbs button { font-size: var(--step--1); font-weight: 500; padding: .3rem .5rem; }
.ticket-verbs .hint { font-size: var(--step--1); color: var(--fg-soft); }

/* Tabs on the customer's own list. The same shape the agent console's tabs use, because one product
   should not have two kinds of tab — and plain links, so each one is bookmarkable. */
.ticket-tabs {
  display: flex;
  gap: .3rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
.ticket-tab {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .5rem .85rem;
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
  margin-bottom: -1px;
  text-decoration: none;
  color: var(--fg-soft);
  font-weight: 600;
  font-size: var(--step--1);
}
.ticket-tab:hover { color: var(--fg); background: var(--bg); }
.ticket-tab.is-current {
  background: var(--surface);
  border-color: var(--border);
  color: var(--fg);
}
/* An empty tab stays, quietly: "Closed 0" is an answer, and a tab that vanishes when empty makes the
   row jump about as tickets move between states. */
.ticket-tab.is-empty { opacity: .55; }

.ticket-search {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.ticket-search label { font-weight: 600; font-size: var(--step--1); }
.ticket-search input[type=search] { flex: 1 1 18rem; max-width: 26rem; }
.ticket-search-clear { font-size: var(--step--1); }

.ticket-cards { display: grid; gap: .55rem; }
.ticket-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: .85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color .12s, transform .12s;
}
.ticket-card:hover { border-color: var(--accent); transform: translateY(-1px); }
/* The whole card is the target, not a five-character reference — this page is read on phones. */
.ticket-card-link { flex: 1 1 18rem; text-decoration: none; color: inherit; display: block; }
.ticket-card-subject { display: block; font-weight: 600; margin-bottom: .2rem; }
.ticket-card-meta { display: flex; gap: .6rem; flex-wrap: wrap; font-size: var(--step--1); color: var(--fg-soft); }
.ticket-card-ref { font-family: var(--font-mono); }
.ticket-card-via::before, .ticket-card-when::before { content: '· '; }
.ticket-card-status { display: flex; flex-direction: column; align-items: flex-end; gap: .2rem; text-align: right; }
.ticket-card-status .hint { margin: 0; font-size: var(--step--2); }

/* (superseded by the cards above) The customer's own ticket list. `.listing` carries no styling of its own outside the admin pages, so
   the rows ran together and the status hint collided with the date — this is the one table a customer
   ever reads, and it was the only unstyled one in the product. */
.ticket-list .listing { width: 100%; border-collapse: collapse; }
.ticket-list .listing th, .ticket-list .listing td {
  text-align: left;
  padding: .6rem .7rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.ticket-list .listing thead th {
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--fg-soft);
  border-bottom: 2px solid var(--border);
}
.ticket-list .listing tbody tr:last-child td { border-bottom: 0; }
/* Under the badge, not beside it: inline, "with our team" ran into the timestamp in the next column. */
.ticket-list .listing td .hint { display: block; margin: .25rem 0 0; font-size: var(--step--1); }
.ticket-list .listing td:last-child { white-space: nowrap; color: var(--fg-soft); }

.ticket-verbs { margin-top: 1rem; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ticket-verbs .hint { margin: 0; }

/* A pasteable block, on the same plate as a revealed secret but multi-line and left-aligned. */
pre.secret {
  display: block;
  white-space: pre;
  overflow-x: auto;
  letter-spacing: 0;
  font-size: var(--step--1);
  font-weight: 500;
  line-height: var(--leading-normal);
  max-width: 100%;
}


/* ── Supervisor dashboard (M18 T4) ──────────────────────────────────────────────────────────────────
   Built from what is already here — .panel, .listing, .badge, .help — so it reads as the same product
   rather than a dashboard bolted on. The only new thing is the stat tile, because nowhere else in the app
   shows a number as the primary content. */
.window-picker { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 0; }
.window-picker .is-current { background: var(--accent); color: var(--accent-ink); }

.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: .8rem;
  margin: 0 0 .9rem;
}
.stat {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-width: 9.5rem;
  flex: 1 1 9.5rem;
  padding: .8rem .9rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-sunken);
}
/* Tabular figures so a column of numbers lines up on the decimal rather than shimmering as it updates. */
.stat-n { font-size: var(--step-2); font-weight: 600; line-height: var(--leading-tight); font-variant-numeric: tabular-nums; }
.stat-label { font-size: var(--step--1); color: var(--muted); }
.stat-sub { font-size: var(--step--2); color: var(--muted); }

@media (max-width: 40rem) {
  .stat { min-width: 100%; }
}


/* The Review menu's attention badge (M25 T2). Small, red, and only present when it has a number — a badge
   that is always there is one people stop seeing, which is the failure mode it exists to avoid. */
.review-alert {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 .3rem;
  margin-left: .15rem;
  border-radius: var(--radius-pill);
  background: var(--danger);
  color: var(--ink-on-color);
  font-size: var(--step--2);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Inside the open submenu the badge sits after the label rather than beside a caret, so it needs its own
   breathing room and must not stretch the row. */
.admin-submenu a .review-alert { margin-left: .4rem; vertical-align: middle; }


/* ── `.inline` and the action rows (M25 T7) ─────────────────────────────────────────────────────────
   Reported live: the buttons on /internal/agent?id=N "display its overlap one to another".

   The cause is that `.inline` was never DEFINED. Twenty-eight forms across ten templates carry the class,
   and every one of them was a plain block-level <form> falling back to the browser's default margins — so
   controls that were written to sit side by side stacked, collided with whatever followed, and depended
   entirely on whether a parent happened to be flex. Three scoped rules (`.test-actions .inline`,
   `.src-actions .inline`) had already patched the symptom in the three places somebody noticed.

   This is the base the name always promised. The scoped rules above still win where they set a different
   gap; nothing here overrides them. */
.inline {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: 0;
  /* A SHARED FORM CLASS IS A FLEX ITEM WHEREVER IT LANDS (M42 T10), and a flex item will not go
     below its content width unless told it may. Every fix in this pass chased the same cause one
     level at a time — the card, then the row, then this — so it belongs on the component rather
     than on the fourth page that trips over it. It only ALLOWS shrinking; nothing is forced. */
  min-width: 0;
  flex-wrap: wrap;
  vertical-align: middle;
}

/* The thread's action row (Claim · Mark resolved · Reopen). A wrapping flex row, so a narrow window drops a
   button to the next line with its spacing intact rather than crushing the three together. */



/* ── Agent thread page: grouping and weight (M27 T4) ────────────────────────────────────────────────
   Reported live as "not look professional ... like junk content pasted all places", and with the page
   finally on screen the complaint resolves into two things: no reading measure (fixed on `.msg .bubble`
   above) and no hierarchy below the reply box. Send reply, AI Answer, Claim, Mark resolved and Transfer
   were all the same primary indigo, stacked in four separate blocks with a bordered card among them, so
   nothing said which of the five an agent was meant to reach for. There is exactly one primary action on
   this page. */
.agent-assist { display: flex; align-items: flex-start; gap: .5rem; flex-wrap: wrap; margin: .4rem 0 .9rem; }
.help-block { display: block; flex: 1 1 100%; margin: .15rem 0 0; }
/* The state-changing verbs read as one row of alternatives, not as four unrelated forms that happen to
   sit next to each other. */
/* THE THREAD ACTION BAR (M28 T18, user: *"\"acme/agent\" pages need to improve design"*).

   THERE WERE TWO `.agent-actions` RULES, forty lines apart, and the second silently won. Whichever
   one you found and edited, half the time nothing moved. Now there is one.

   What measuring showed in the row itself: buttons 40px tall beside buttons 60px tall, because
   "Raise it" and "Close without answering" were breaking their labels across two lines under flex
   pressure. A row of controls at two different heights is most of why the bar read as a jumble
   rather than as a set of choices. `white-space: nowrap` stops the break — the row already wraps, so
   a control that will not fit moves to the next line whole, which is the honest failure. */
.agent-actions { display: flex; flex-wrap: wrap; gap: .45rem; align-items: stretch; margin: .75rem 0; }
.agent-actions form { margin: 0; }
/* NOWRAP BELONGS ON THE LABEL, NOT ON THE FORM (M42 T10, correcting my own M28 T18).
   The intent was that a BUTTON should not break "Close without answering" across two lines. Putting
   it on `.inline` as well stopped the form's own children wrapping, so at 320px a reason field and
   its button sat side by side at 327px and took the page with them. A form must be free to stack.
   `min-width: 0` for the reason `.group` has it: a flex item holding a text field cannot shrink past
   the browser's idea of how wide that field wants to be unless it is allowed to. */
.agent-actions button { white-space: nowrap; }
.agent-actions .inline { flex-wrap: wrap; min-width: 0; }
/* Stretch alone would make a 32px control as tall as a 40px neighbour; this sets the floor they all
   meet at, so the row is one band rather than a skyline. */
.agent-actions button, .agent-actions input, .agent-actions select { min-height: 2.5rem; }
/* AN INPUT THAT CANNOT SHOW ITS OWN PLACEHOLDER IS NOT ASKING A QUESTION. Both reason fields were
   squeezed to the point of cutting their prompt mid-word — "Why does this need someon". They get a
   floor and are allowed to take slack when the row has it. */
.agent-actions input[type="text"] { flex: 1 1 14rem; min-width: 11rem; max-width: 22rem; }
/* THE SECONDARY ACTIONS WERE READING AS HEADINGS. "Waiting on someone else" and "Close without
   answering" are `button.quiet` — muted text, transparent border, transparent fill — in a row of
   white bordered buttons. On a phone they looked like section titles with a stray input underneath
   them, which is a control nobody presses.

   I first wrote this rule against `.link-ish`, which is what they LOOKED like. They are `.ghost
   .quiet`; the rule did nothing and the screenshot was unchanged. Found by reading the computed
   class off the element rather than inferring it from the picture.

   `.quiet` is deliberate and it stays quiet — that hover turning DANGER red says what these are.
   The border is `--border`, not `--border-strong`, so they read as controls of lower weight rather
   than as controls of equal weight, which is the distinction the modifier exists to make. */
.agent-actions button.quiet { border-color: var(--border); }
.agent-actions button.quiet:hover, .agent-actions button.quiet:focus-visible {
  border-color: var(--danger-border);
}
/* Transfer keeps its explanation — the reason field is required and the copy says why — but it stops
   outweighing the reply box it sits under. */
.group.transfer.is-secondary {
  background: var(--surface-sunken);
  border-color: var(--border);
  padding: .75rem .9rem;
  margin-top: .75rem;
}
.group.transfer.is-secondary h3 { margin-top: 0; font-size: var(--step-0); }

/* The per-message reuse controls (M27 T4). They live inside a message row, which now carries the 68ch
   reading measure, so they were inheriting a 350px column and stacking one under the other. They are a
   row of controls about the reply above them, not a second column of content. */
.msg .reuse-form { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .4rem; max-width: 68ch; width: 100%; }
.msg .reuse-form textarea { flex: 1 1 100%; min-width: 0; }

/* Close-without-answering (M28 T6). The rarest verb on the thread page and the only one that removes
   something, so it is findable rather than prominent — quiet until hovered, and its reason field is
   narrow so the row still reads as one control rather than a second form. */
/* The reason belongs to the BUTTON BESIDE IT, and must look like it. It is already the same form;
   the trouble was that `flex-wrap` let the field drop onto its own line at this width, where it read as
   belonging to "Mark resolved" on the line above — an agent typing a spam reason into what they think
   is a resolution note. Narrower field, no wrap, so the two stay one control. */
.close-form { display: flex; align-items: center; gap: .35rem; flex-wrap: nowrap; }
.close-reason { max-width: 16rem; min-width: 0; font-size: var(--step--1); }
@media (max-width: 44rem) { .close-form { flex-wrap: wrap; } }
/* `.quiet` ON LINKS TOO (M42 T3). It was a `button.` rule, so `<a class="ghost quiet">` — which the
   agent console now writes for Manual and Sign out — got the ghost treatment and none of the quiet,
   and sat in the header looking like an ordinary button. */
button.quiet, a.quiet { color: var(--muted); border-color: transparent; background: transparent; }
button.quiet:hover, button.quiet:focus-visible,
a.quiet:hover, a.quiet:focus-visible { color: var(--danger); border-color: var(--danger-border); background: var(--danger-soft); }

/* A reply in flight (M29 T2). The input stays live — a customer must always be able to keep typing, and a
   box that goes dead under your fingers reads as a broken page — but the button greys and the form says
   quietly that the queue is working. Only SENDING waits. */
.chat-form[data-busy="1"] button[type="submit"] { opacity: .55; }
.chat-form[data-busy="1"]::after {
  content: "sending…";
  font-size: var(--step--2);
  color: var(--muted);
  align-self: center;
}

/* Connection banners (M29 T1). Two states with deliberately different weight: `retrying` is amber and says
   we are working on it; `lost` is red and asks the reader to do something. Nothing else on either page
   uses red for a transient state, so the second one means exactly one thing. */
.conn-state, .chat-handling[data-state="retrying"], .chat-handling[data-state="lost"] {
  border-radius: var(--radius-sm);
  padding: .45rem .7rem;
  font-size: var(--step--1);
}
.conn-state[data-state="retrying"], .chat-handling[data-state="retrying"] {
  background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-border);
}
.conn-state[data-state="lost"], .chat-handling[data-state="lost"] {
  background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger-border);
}
.conn-state { margin: 0 0 .6rem; }

/* The feedback confirmation (M30 T2). A quiet line rather than a toast: it belongs beside the thumbs it
   is about, and a floating notification would cover the conversation to say something small. */
.fb-note { font-size: var(--step--2); color: var(--muted); margin-left: .4rem; }

/* Unattended work that has been waiting more than two days (M28 T1). The only badge in the header that
   changes colour, so it means exactly one thing: this is not a busy afternoon, it is a backlog. */
.review-alert.is-stale { background: var(--danger-soft); color: var(--danger); }

/* The department mailbox panel (M19 T5/T8). A SECTION, not a disclosure: the first version nested a
   `<details>` inside an already-expandable row and the user went looking for it. Two levels of disclosure
   is functionally present and effectively hidden. */
.mailbox-form { margin-top: .9rem; }
.mailbox-form h3 { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 0 0 .6rem; font-size: var(--step-0); }
.mail-fields { display: flex; flex-wrap: wrap; gap: .6rem 1rem; align-items: flex-end; max-width: 68ch; }
.mail-fields label { display: flex; flex-direction: column; gap: .2rem; font-size: var(--step--1); }
/* The label and its required marker are ONE flex item, or the asterisk drops to its own line under the
   word — which is how it first rendered, and reads as a bullet rather than a marker. */
.label-text { white-space: nowrap; }
.mail-fields input, .mail-fields select { min-width: 12rem; }
.mail-actions { display: flex; gap: .4rem; flex-basis: 100%; }
/* Required marker. Named on the field AND explained once underneath, because an asterisk nobody has
   defined is decoration. */
.req { color: var(--danger); font-weight: 600; }
/* THE HINT UNDER A FIELD — and it had no base rule at all (M41).

   Three scoped variants existed (`.chat-new`, `.prechat-form`, `.template-form`) and they all agree:
   muted, `--step--1`. Everywhere else `small.hint` got nothing but `<small>`'s browser default — so
   it rendered at **10.8px in bold, in the body colour**, inheriting the weight from the `<label>` it
   sits in. Measured on the setup slides and on this milestone's own form.

   That is backwards twice over: a hint is the least important text on the line and was the heaviest,
   and 10.8px is below what this design system considers readable anywhere else. The base rule says
   what the three scoped ones already said, so they stop being exceptions and start being the rule. */
/* THE THREE TEXT ROLES, WHICH HAD NO BASE RULES (M42 T1).

   `.help` is used 144 times in the templates and had NO definition — only ELEVEN scoped margin
   patches (`.field .help`, `.setup-dismiss .help`, `.connectivity-inline .help`, …), so its colour and
   size came from whatever happened to surround it and every new page added a twelfth. `.empty` was
   used 22 times with nothing but an `li.empty`. `.hint` was the same until M41 — rendering at 10.8px
   in BOLD, because `<small>` halves the size and the label it sits in supplies the weight.

   Three roles, three rules. They say what the scoped variants already said, so those stop being
   exceptions and become the rule; the margin patches can go as each page is touched.

   `.help` keeps `--fg-soft` rather than `--muted`: it is a SENTENCE somebody is meant to read, where a
   hint is a note beside a field and an empty state is an absence. Three roles, three weights. */
.help {
  color: var(--fg-soft);
  font-size: var(--step--1);
  font-weight: 400;
  line-height: var(--leading-snug);
  max-width: 68ch;
}
p.empty, .empty {
  color: var(--muted);
  font-size: var(--step--1);
  font-style: italic;
  margin: .4rem 0;
}
.hint {
  display: block;
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 400;
  line-height: var(--leading-snug);
  margin-top: .2rem;
}
/* The test form is separate from the save form on purpose — a test must never be confusable with a save,
   and a half-typed change must not be sendable by pressing the wrong button. */
.mail-test {
  display: flex; align-items: flex-end; gap: .5rem; flex-wrap: wrap;
  margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--border); max-width: 68ch;
}
.mail-test label { display: flex; flex-direction: column; gap: .2rem; font-size: var(--step--1); }
.mail-test input[type="email"] { min-width: 16rem; }

/* The handoff token tester (M21 T9). A checklist, not a wall of prose: the integrator is scanning for
   the one line with a cross beside it, so the mark carries the meaning and the colour only reinforces
   it — a red/green pair alone fails anyone who cannot tell them apart. */
.token-input {
  display: block; width: 100%; max-width: 68ch; margin: .35rem 0 .75rem;
  padding: .5rem .6rem; border: 1px solid var(--border); border-radius: 6px;
  font-family: var(--font-mono); font-size: var(--step--1);
  line-height: var(--leading-normal); resize: vertical; word-break: break-all;
}
/* Same measure as the checklist it heads — a verdict running the full page width reads as a page-level
   banner rather than the answer to the form directly above it. */
.token-verdict { max-width: 68ch; }
.token-checks { list-style: none; margin: .75rem 0 .5rem; padding: 0; max-width: 68ch; }
.token-checks li {
  display: flex; gap: .6rem; align-items: baseline;
  padding: .5rem 0; border-top: 1px solid var(--border);
}
.token-checks li:first-child { border-top: 0; }
.token-checks .check-mark { flex: 0 0 1.1rem; font-weight: 700; line-height: var(--leading-snug); }
.token-checks .check-ok .check-mark { color: var(--ok); }
.token-checks .check-bad .check-mark { color: var(--danger); }
.token-checks .check-body { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.token-checks .check-body strong { font-size: var(--step-0); }
.token-checks .check-body .help { margin: 0; }
/* The failing line is the one being read — give it the weight its cross implies. */
.token-checks .check-bad .check-body strong { color: var(--danger); }

/* ---- appearance switch (M39 T7) -----------------------------------------
   Three segments in the header, showing which is active rather than only offering the alternative.
   A two-state toggle that says "Dark" cannot tell you whether you are currently following the system
   or have pinned light — which is the one thing somebody checking this control wants to know. */
.theme-switch {
  display: inline-flex;
  margin-left: .35rem;
  padding: 2px;
  gap: 2px;
  /* The same hairline and the same 6px as every other control on this bar (M42 T13). It was a
     999px pill in a sunken fill, which made it the only thing in the header shaped like nothing
     else up there. ADDING a rule above this one changed nothing — this came later and won, which
     is the specificity trap the two table looks already taught me. */
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-header);
  background: var(--surface);
}
.theme-switch button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.65rem;
  height: 1.45rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.theme-switch button:hover { color: var(--fg); }
.theme-switch button[aria-pressed="true"] {
  background: var(--surface);
  color: var(--fg);
  box-shadow: var(--shadow-card);
}
.theme-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }


/* ---- one empty state (M39 T5) -------------------------------------------
   There were three: `.task-empty` (a bare muted paragraph, used by the shared KB listing and so by
   Departments, Gaps, Chats and the dashboard), `.chat-empty`, and a bare `li.empty` in the agent
   thread. All three said the same thing and looked like three different pages had said it.

   An empty list is not an error and must not be dressed as one — no icon, no panel, no colour. It is
   a sentence with enough room around it to read as a deliberate state rather than as a list that
   failed to render, which is exactly what one unspaced grey line looks like. */
.task-empty,
.chat-empty,
li.empty {
  margin: 0;
  padding: 2rem 1rem;
  color: var(--muted);
  font-size: var(--step-0);
  text-align: center;
  list-style: none;
  text-wrap: balance;
}
/* Inside an already-padded panel the outer padding is somebody else's job. */
.kb-list .task-empty { padding: 2.5rem 1rem; }

/* ---- one loading state (M39 T5) -----------------------------------------
   Exactly ONE thing in this application loads after its page has rendered: expanding "View indexed
   chunks" fetches them. It said `Loading…`, which is honest but reads as a message rather than as
   the shape of what is coming — and on a slow connection a line of text is indistinguishable from an
   answer that says there is nothing.

   Deliberately NOT a general-purpose skeleton system. The admin is server-rendered: a slow query
   makes the page take longer, it does not produce an empty page that fills in later. A skeleton for
   a state that cannot happen is dead CSS that the next person has to reason about. */
.skeleton {
  display: block;
  width: 100%;
  height: .72rem;
  margin: .5rem 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--surface-sunken) 0%,
    color-mix(in oklab, var(--muted) 18%, var(--surface-sunken)) 50%,
    var(--surface-sunken) 100%
  );
  background-size: 300% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
}
/* Three bars of decreasing width read as "a list is coming"; equal bars read as a barcode.
   SIBLING selectors, not :nth-child — the first child of this region is the screen reader's
   "Loading…" text, so :nth-child(2) was the FIRST bar and the third bar matched nothing at all and
   ran full width past the two above it. Counting from the element you mean is the only version of
   this that survives somebody adding a label. */
.skeleton + .skeleton { width: 82%; }
.skeleton + .skeleton + .skeleton { width: 64%; }

@keyframes skeleton-sweep {
  from { background-position: 100% 0; }
  to   { background-position: -100% 0; }
}

/* A sweeping gradient is motion, and motion is the thing some people have asked their machine not to
   do. Without this the animation is the ONE piece of movement on the page, which is the worst case. */
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background-position: 50% 0; }
}


/* ---- table column widths (M39 T6) ---------------------------------------
   These were 49 inline `style="width:12rem"` attributes on <col> elements. They had to move, and
   not for tidiness: CSP can allow an inline <script> with a nonce, but there is NO way to allow one
   `style=` ATTRIBUTE and refuse another — it is `unsafe-inline` for every one of them or
   `style-src-attr 'none'` for none. Inline widths were the single thing standing between the console
   and a strict style policy, and the one thing a nonce could not have rescued.

   Scoped to `col` on purpose. A general `.w-12` invites a utility framework to grow here one class
   at a time; on a <col> it is unambiguously a column hint and cannot be reached for anywhere else.
   Three fractional widths (5.5, 7.5, 12.5rem) were rounded to whole rem — half a rem on a column
   hint is noise, and it is the difference between sixteen classes and nineteen. */
col.w-5 { width: 5rem; }
col.w-6 { width: 6rem; }
col.w-7 { width: 7rem; }
col.w-8 { width: 8rem; }
col.w-9 { width: 9rem; }
col.w-10 { width: 10rem; }
col.w-11 { width: 11rem; }
col.w-12 { width: 12rem; }
col.w-13 { width: 13rem; }
col.w-14 { width: 14rem; }
col.w-15 { width: 15rem; }
col.w-16 { width: 16rem; }
col.w-18 { width: 18rem; }
col.w-19 { width: 19rem; }
col.w-20 { width: 20rem; }
col.mw-16 { min-width: 16rem; }

/* ---- first run, one question per screen (M40) ---------------------------
   A staff surface with no chrome. The width is NOT --shell: a page whose job is to ask one question
   reads badly at 88rem, because the eye has to travel the whole width to find the single thing it is
   meant to do. */
/* A BACKDROP, so the card has something to float above (M40 T12).
   Two very quiet washes rather than a flat field: one warm, one on the accent, both well under a
   tenth of their own strength. The point is not that anybody notices them — it is that the card
   stops looking like a box drawn on a wall and starts looking like something laid on top of a
   surface, which is the whole of what "popup" means here. Both derive from theme tokens, so a
   palette change moves them too. */
body.is-bare {
  background:
    radial-gradient(70rem 40rem at 50% -12rem,
      color-mix(in oklab, var(--accent) 7%, transparent), transparent 70%),
    radial-gradient(52rem 34rem at 90% 108%,
      color-mix(in oklab, var(--ok) 5%, transparent), transparent 70%),
    var(--bg);
  background-attachment: fixed;
}

.wrap-bare {
  max-width: 60rem;
  /* MINUS the shell's own padding. At a flat 100vh the wrap is a viewport tall PLUS its padding, so
     the bottom-anchored Continue sat just off the bottom edge — present, reachable by scrolling, and
     looking exactly like a button that had been cut off. */
  min-height: calc(100vh - 3.2rem);
  display: flex;
  flex-direction: column;
  /* THE WHOLE BLOCK IS CENTRED, AND THE CARD HUGS ITS CONTENT. Stretching the card to fill the
     viewport and centring the text inside it traded one problem for another: no dead zone below the
     content any more, but a 630px card holding 400px of content, which reads as a panel that did not
     know how big it wanted to be. Centring the block instead gives the same calm and sizes the card
     to what is actually on it. */
  justify-content: center;
  padding-top: 1.6rem;
  padding-bottom: 1.6rem;
}
/* THE ENTRANCE ANIMATION MUST NOT MAKE THE PAGE SCROLLABLE (M42 T10).

   `slide-in` translates the card 26px, and on a 390px phone that pushed the document 11px wider than
   the window. MEASURED, and the measurement is why this is a fix and not a shrug: the transform
   settles back to identity after 1.5s and **the horizontal scroll stays at 11px**, because the
   scrollable overflow region does not shrink back. With `prefers-reduced-motion: reduce` — where the
   animation never runs — it is 0. So a decorative flourish left every setup step able to slide under
   a thumb, for as long as the page was open.

   `clip` and not `hidden`: `hidden` would make this a scroll container and force the other axis to
   `auto`, which would cut off anything that legitimately escapes downwards. `overflow-x: clip` leaves
   `overflow-y` genuinely visible, which is the whole reason the property exists. */
.slide { display: flex; flex-direction: column; flex: 0 1 auto; gap: 1.2rem; overflow-x: clip; }

/* ONE ROW WHERE THERE IS ROOM, TWO WHERE THERE IS NOT — and the threshold is measured, not guessed.
   The rail is 614px and the controls 293px: 907px of content. At the old 52rem shell (832px) they
   could not share a row, so the header stacked; at 60rem (960px) they can. Below 64rem the controls
   take a thin row of their own and the rail spans the width beneath them, which is what a progress
   track wants anyway. */
.slide-head {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: .9rem;
}
/* THE ONE-ROW VARIANT WAS TRIED AND REMOVED. 614px of rail plus 293px of controls is 907px, which
   fits 960px on paper — and did not in practice, because the tenant name is part of those controls
   and "Acme Retail" is longer than the name I measured with. A layout that depends on how long a
   customer's name is will break on the customer whose name is long. Stacked always: the controls
   take a thin row, the rail spans the width beneath them. */
/* The exit and the tenant switcher are ONE group on the right. Left as three siblings under
   `space-between` the switcher wrapped onto its own row and its open menu landed across the page
   heading — three things that each fit, arranged so that they do not. */
.slide-head-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: .5rem;
  /* Above the rail: these are the ways OUT, and the rail is the way through. */
  order: -1;
}
.slide-exit { margin: 0; }
.slide-exit button { white-space: nowrap; }

/* ---- the rail ------------------------------------------------------------
   SIX PILLS IN A ROW ARE NOT A JOURNEY. They were six separate things you could click, and nothing
   said they were a sequence or how far along it you were. A track through them does, and it does it
   without a number, a percentage or a word.

   The connector is drawn per step rather than as one bar with a width, because a width would have to
   be computed and handed over in a `style` attribute — and `style-src-attr 'none'` forbids exactly
   that (M39 T6). A class per step says the same thing and survives the policy. */
.slide-rail {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  row-gap: .4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.slide-pip { display: flex; align-items: center; }
.slide-pip > a,
.slide-pip > span {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .3rem .5rem;
  border-radius: var(--radius-pill);
  color: var(--muted);
  text-decoration: none;
  font-size: var(--step--2);
  font-weight: 600;
  line-height: var(--leading-normal);
  transition: color .16s ease, background-color .16s ease;
}
.slide-pip > a:hover { color: var(--fg); background: var(--surface-sunken); }
.slide-pip > a:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The connector, between each step and the next. */
.slide-pip:not(:last-child)::after {
  content: "";
  width: 1.4rem;
  height: 2px;
  flex: 0 0 auto;
  border-radius: 2px;
  background: var(--border-strong);
  transition: background-color .24s ease;
}
.slide-pip.is-done:not(:last-child)::after { background: var(--ok-border); }

.slide-pip-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font-variant-numeric: tabular-nums;
  transition: border-color .18s ease, background-color .18s ease, color .18s ease,
              box-shadow .18s ease;
}
.slide-pip.is-done .slide-pip-dot {
  background: var(--ok-soft);
  border-color: var(--ok-border);
  color: var(--ok);
}
.slide-pip.is-current > a { color: var(--fg); }
.slide-pip.is-current .slide-pip-dot {
  border-color: var(--accent);
  color: var(--accent);
  /* A halo rather than a heavier border: the ring says "you are here" without the dot changing size
     and nudging every pip after it sideways. */
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 14%, transparent);
}
/* Greyed AND not a link — the two together, because a greyed thing that is still clickable is the
   worst of both: it looks unavailable and behaves as if it is not. */
.slide-pip.is-locked > span { color: color-mix(in oklab, var(--muted) 55%, transparent); cursor: default; }
.slide-pip.is-locked .slide-pip-dot { border-style: dashed; background: transparent; }
/* The labels are the first thing to go when the row runs out of width — the dots still carry the
   position, and the slide's own heading already names where you are. */
@media (max-width: 46rem) { .slide-pip-label { display: none; } }

/* ---- the slide itself ----------------------------------------------------
   A CARD, AND THE CONTENT CENTRED IN IT. Before this the body clung to the top of the viewport with
   three hundred pixels of nothing between it and the navigation, which read as a page that had
   failed to finish loading rather than as one question asked deliberately. */
/* A URL IS ONE UNBREAKABLE WORD, and a setup step lists several of them — the sites allowed to embed
   the chat. At 320px a `<strong>` holding them measured 310px inside a 292px card (M42 T10). */
.slide-body strong, .slide-body code, .slide-body .mono { overflow-wrap: anywhere; }

.slide-body {
  flex: 0 1 auto;
  padding: 2rem 2.3rem;
  /* The card must not be wider than the window it is centred in (M42 T10). */
  max-width: 100%;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) * 1.6);
  background: var(--surface);
  /* --shadow-menu is the FLOATING elevation (M39 T5) and this is the one place outside a real menu
     that earns it: the slide is the only thing on the screen and everything else is backdrop. */
  box-shadow: var(--shadow-menu);
  /* SIDEWAYS, NOT UP (M40 T12). Rising eight pixels says "this appeared"; coming in from the side
     says "this is the next one", which is the difference between an animation and a slider. The
     scale is what makes it read as a panel rather than a page: a hair under full size on the way in,
     so it settles rather than arrives. */
  animation: slide-in-next .3s cubic-bezier(.22, .68, .3, 1) both;
}
.slide-body.is-back { animation-name: slide-in-prev; }

@keyframes slide-in-next {
  from { opacity: 0; transform: translateX(26px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
/* Going back comes BACK. A slider that always enters from the same side is not a slider. */
@keyframes slide-in-prev {
  from { opacity: 0; transform: translateX(-26px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* Motion is the thing some people have asked their machine not to do, and a page that animates on
   every navigation is the worst case for them. */
@media (prefers-reduced-motion: reduce) {
  .slide-body { animation: none; }
  .slide-pip > a,
  .slide-pip-dot,
  .slide-pip:not(:last-child)::after { transition: none; }
}

.slide-step-of {
  margin: 0 0 .5rem;
  color: var(--accent);
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.slide-body h1 {
  margin: 0 0 .5rem;
  font-size: var(--step-3);
  line-height: var(--leading-tight);
  letter-spacing: -0.015em;
}
.slide-blurb {
  margin: 0 0 1.3rem;
  color: var(--fg-soft);
  font-size: var(--step-1);
  line-height: var(--leading-normal);
}
.slide-sub { margin: 1.4rem 0 .4rem; font-size: var(--step-1); }
/* NO MEASURE CAP ON THE STEPS (M40 T63, user: *"in steps remove para max-width"*).
   T59 measured this and reported it: capped, a hint holds ~65 characters a line; uncapped it runs
   the card's full width, ~115. Asked for it gone, and on the steps that is the better call —
   the slide card is ALREADY a narrow column inside a wide shell, so the cap was a second
   constraint inside the first. What it actually produced was prose stopping two-thirds of the way
   across while the snippet box, the build bar and the notices beside it ran the full width: a
   ragged right edge that reads as a layout fault rather than as a reading aid.
   Still capped where it is doing its job and nothing else is: `.field .help`, form controls and
   `.pw-wrap`, none of which are steps. */
.slide-hint { margin: .75rem 0 0; color: var(--muted); font-size: var(--step--1); }
.slide-actions { display: flex; gap: .6rem; flex-wrap: wrap; margin: 1.2rem 0 0; }

/* ---- "For your developer" (M21 T19) -------------------------------------
   User: *"the non technical persion can also understand"*. The page was 1,810 words and 1,140 of
   them were a secret to install, four language samples and a token tester. Folded rather than cut:
   an operator should not scroll past a PHP 5.6 example to find the line they came for, and the
   developer who needs it should find it under a heading naming them. */
.dev-fold {
  margin: 1.6rem 0 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.dev-fold > summary {
  display: block;
  padding: .85rem 1rem;
  cursor: pointer;
  list-style: none;
}
.dev-fold > summary::-webkit-details-marker { display: none; }
.dev-fold > summary:hover { background: var(--surface); }
.dev-fold-title { display: block; font-weight: 700; font-size: var(--step-0); }
/* The subtitle is the part that stops somebody worrying they have missed a step. */
.dev-fold-sub { display: block; margin-top: .2rem; color: var(--muted); font-size: var(--step--1); }
.dev-fold[open] > summary { border-bottom: 1px solid var(--border); }
.dev-fold > section { margin: 0; padding: 0 1rem; }
.dev-fold > section:last-of-type { padding-bottom: 1rem; }

/* A STATE AND THE ONE ACTION THAT CHANGES IT (M40 T64, user: *"default to enable the ticket. near
   by disable button if he want disable it ... very simple display"*). Not a switch to set and then
   save: the screen states what is true, and the button beside it is the change. `flex-wrap` so the
   pair stacks on a phone rather than squeezing the sentence into a column. */
.slide-state {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8rem 1.2rem;
  flex-wrap: wrap;
  margin: 0;
  padding: .85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.slide-state-is { margin: 0; font-size: var(--step-0); line-height: var(--leading-normal); }
/* The dot carries the state at a glance; the sentence carries it for everybody else. Grey when off
   rather than red — tickets switched off is a choice, not a fault. */
.slide-state-dot {
  display: inline-block;
  width: .6rem;
  height: .6rem;
  margin-right: .45rem;
  border-radius: 50%;
  background: var(--muted);
  vertical-align: baseline;
}
.slide-state-dot.is-on { background: var(--ok); }
.slide-state button { flex: 0 0 auto; }

/* WHAT WILL NOT HAPPEN, SAID LIKE IT MATTERS (M40 T61, user: *"if any important to notify to
   customer make highlight"*). A step's `detail` is a consequence while the step is undone — "no
   email leaves or arrives", "nobody can pick up a chat" — and it rendered in the same muted grey as
   a passing remark. It borrows the console's warning panel rather than inventing a look, and stays a
   `<p>`: it is one sentence, and wrapping it in a div to make it a "component" would be the only
   reason for the div to exist. */
.slide-consequence {
  margin: 0 0 1.2rem;
  font-size: var(--step-0);
  line-height: var(--leading-normal);
}

/* The one field a slide asks for, at the size that says it is the point of the screen. */
.slide-field { display: flex; gap: .6rem; align-items: stretch; margin: 0; flex-wrap: wrap; }
.slide-field input[type="text"] {
  flex: 1 1 18rem;
  max-width: none;
  padding: .85rem 1.05rem;
  font-size: var(--step-1);
  border-radius: var(--radius-sm);
}
.slide-field button { padding-inline: 1.35rem; font-size: var(--step-0); }

.slide-suggests { display: flex; gap: .45rem; flex-wrap: wrap; margin: .85rem 0 0; }
/* CHIPS, not loose words. They were rendering as three pieces of bold text with a plus in front —
   readable, and giving no sign that they were the fastest thing on the screen to press. */
.slide-suggests button {
  padding: .35rem .8rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--fg-soft);
  font-size: var(--step--1);
  font-weight: 600;
}
.slide-suggests button:hover {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

/* The two navigation buttons carry the flow; they should not read as an afterthought under it. */
.slide-nav .button-ish,
.slide-nav .button { padding: .55rem 1.1rem; font-size: var(--step-0); }
/* FORWARD IS THE PRIMARY ACTION (M40 T62). Back stays a ghost; the way on is filled, so the
   pair no longer reads as two equal choices. */
.slide-nav .slide-next { margin-left: auto; }

.slide-snippet { position: relative; margin: 0 0 .2rem; }
/* THE ONE THING TO DO ON THIS STEP, weighted like it (M40 T57, user: *"what are the things need to
   focus highlight"*). */
.slide-snippet.is-primary .secret {
  border-color: var(--accent-border);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Wraps rather than clips: a snippet that runs off the edge of a phone is a snippet nobody can copy.
   (It used to add "and every Copy button on an HTTP host is hidden, so selecting the text is the real
   path" — true until M40 T26 added the `execCommand` fallback, after which the button works here.
   A comment that describes a fixed limitation is how the next person re-fixes it.) */
.slide-snippet .secret {
  display: block;
  padding: .85rem .95rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: var(--leading-normal);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.slide-learning {
  margin: 1.4rem 0 0;
  padding: .95rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.slide-learning-pages { margin: 0; font-size: var(--step-0); }
.slide-cats { display: flex; gap: .35rem; flex-wrap: wrap; margin: .75rem 0 0; }

/* ---- the knowledge build, at the top of step 2 (M40 T57) -----------------
   User: *"In Top Build KB 3/25 ... bellow this 'range bar' - highlight"*. The count used to sit at
   the bottom of the slide inside a bordered panel, which on a phone put the only moving thing on the
   screen below the fold. It leads now, and it is the one element here allowed to be loud. */
.kb-build {
  margin: 0 0 1.3rem;
  padding: .85rem 1rem .95rem;
  border: 1px solid var(--accent-border);
  border-radius: var(--radius);
  background: var(--accent-soft);
}
.kb-build-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .6rem;
  margin: 0 0 .5rem;
}
.kb-build-label {
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--fg-soft);
}
/* The number is the fact; tabular figures so it does not jitter sideways as it counts up. */
.kb-build-count {
  font-size: var(--step-2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--accent);
}
/* RESET FIRST. `<progress>` carries a heavy default appearance that differs on every engine, and
   styling it without `appearance: none` gives three different widgets on three browsers. */
.kb-build-bar {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  height: .55rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  overflow: hidden;
}
.kb-build-bar::-webkit-progress-bar { background: var(--surface-sunken); border-radius: var(--radius-pill); }
.kb-build-bar::-webkit-progress-value {
  background: var(--accent);
  border-radius: var(--radius-pill);
  transition: width .4s ease;
}
.kb-build-bar::-moz-progress-bar { background: var(--accent); border-radius: var(--radius-pill); }
.kb-build-note { margin: .5rem 0 0; font-size: var(--step--1); color: var(--fg-soft); }
.kb-build-note:empty { display: none; }
/* A slow pulse while it runs, and none when it stops — the difference between "working" and
   "finished" should be visible without reading the words. */
@media (prefers-reduced-motion: no-preference) {
  .kb-build.is-running .kb-build-count { animation: kb-pulse 2.4s ease-in-out infinite; }
}
@keyframes kb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* ---- "change this" folds (M40 T57) --------------------------------------
   A step whose defaults are already applied should not look like a step with work in it. Shut, this
   is one line stating what was set; open, it is the form that corrects it. */
.slide-fold { margin: 1.3rem 0 0; }
.slide-fold-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .6rem .85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  cursor: pointer;
  list-style: none;
}
.slide-fold-head::-webkit-details-marker { display: none; }
.slide-fold-head:hover { border-color: var(--border-strong); }
.slide-fold-head .slide-sub { margin: 0; font-size: var(--step-0); }
.slide-fold-state { display: flex; align-items: center; gap: .5rem; font-size: var(--step--1); }
.slide-fold-cue { color: var(--accent); font-weight: 600; }
.slide-fold[open] .slide-fold-cue { color: var(--muted); }
.swatch-dot {
  width: .85rem;
  height: .85rem;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  /* Painted by script through the CSSOM — this page's CSP forbids inline style attributes, so a
     `style="background:…"` in the markup is dropped and the dot renders grey. */
  background: var(--surface);
}
/* THE TICKET-NUMBER PREFIX (M40 T68).

   A FOUR-CHARACTER FIELD IS NOT A FULL-WIDTH ONE. Left at the form's width it reads as a box wanting
   a sentence, and the `maxlength="4"` only announces itself by refusing the fifth keystroke. `ch` is
   the advance of "0", not an average character, so 4ch is narrow for four capitals — 7ch plus the
   padding is about right and the cap stops it growing on a wide screen.

   `text-transform` only changes what is DRAWN; the value posted stays as typed and the server
   uppercases it. That is deliberate: a field that silently rewrote the value would disagree with
   what a screen reader announces. */
/* THE SEARCH BOX (M28 T15, given a rule in T17).

   It had no rule at all, and the honest account of what that looked like is narrower than my first
   guess: `input[type="search"]` is `width: 100%` capped at `--measure`, so on a desktop the box came
   out 544px and the button sat beside it on the same line by ordinary inline layout. Nothing was
   broken there. I wrote a comment claiming the button wrapped, MEASURED it, and it did not.

   What measuring did find is the PHONE. At 390px the input still took the full 326px available and
   the button dropped to its own line — a search box and its button are one control, and splitting
   them costs a line of a screen that has few. So the pair is a flex row that stays a row: the input
   may shrink (`min-width: 0`, which a flex item does not do by default), because a narrower box you
   can still type into beats a wider one whose button is somewhere else. */
.search-form {
  display: flex;
  gap: .5rem;
  align-items: center;
  margin: 0 0 1rem;
}
.search-form input[type="search"] {
  /* `min-width: 0` overrides the flex default of `auto`, which is what pins an input to its
     intrinsic size and pushes the button out of the row. */
  flex: 1 1 auto;
  min-width: 0;
  /* Its own measure, not the form's: a search box is one field on one line, and `--measure` is
     sized for a column of them. */
  max-width: 34rem;
}
.search-form button { flex: 0 0 auto; }
/* Dropped into a page that is ABOUT something else (M28 T17) — History and the handoff queue. It sits
   under the hero, so it takes the space a section would; the hero's own margin is above it. */
.search-inline { margin-top: -.25rem; margin-bottom: 1.25rem; }.prefix-text {
  width: 100%;
  max-width: calc(7ch + 1.6rem);
  text-transform: uppercase;
  letter-spacing: .08em;
}
/* The sample and the collapsed chip are the SAME fact in two places, so they read the same way. */
.prefix-sample,
.prefix-chip {
  font-family: var(--font-mono);
  letter-spacing: .04em;
}
.prefix-chip {
  color: var(--muted);
  font-size: var(--step--1);
}
.slide-fold > .stack-form { padding: .9rem .85rem 0; }

/* ---- Continue, held until the assistant can answer something (M40 T57) ---
   User: *"the continue button at starting grey color the KB increase it turn to green if 5 pages to
   crawled allow to continue"*. Held is a span, not a disabled link: an `<a>` styled to look dead is
   still a link and still navigates. */
.slide-next-hold {
  margin-left: auto;
  padding: .55rem .9rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--muted);
  background: var(--surface-sunken);
  font-size: var(--step--1);
}
.slide-next-hold strong { color: var(--fg-soft); font-variant-numeric: tabular-nums; }
/* GREEN when it opens, because that is the word the request used and because this is the only
   Continue in the flow that had to be earned. `--ok` is the token the rest of the console already
   uses for "this is fine", so it needs no new colour. */
.slide-nav .slide-next.is-gated {
  background: var(--ok);
  border-color: var(--ok);
  color: var(--ok-on);
}
.slide-nav .slide-next.is-gated:hover { filter: brightness(1.06); }

.slide-summary { list-style: none; margin: 1.2rem 0 0; padding: 0; display: grid; gap: .5rem; }
.slide-summary li {
  display: grid;
  grid-template-columns: 1.2rem 1fr auto;
  gap: .2rem .6rem;
  align-items: baseline;
  padding: .75rem .95rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}
.slide-summary .help { grid-column: 2; font-size: var(--step--1); }
.slide-summary-mark { color: var(--muted); }
.slide-summary li.is-done { background: var(--ok-soft); border-color: var(--ok-border); }
.slide-summary li.is-done .slide-summary-mark { color: var(--ok); }
/* PLACED, not auto-flowed. Left to find its own cell it landed in the 1.2rem marker column on a
   third row and wrapped "Do it now" onto three lines. It belongs beside the title it acts on. */
.slide-summary-link {
  grid-column: 3;
  grid-row: 1;
  align-self: center;
  white-space: nowrap;
  font-size: var(--step--1);
  font-weight: 600;
}

.slide-nav {
  display: flex;
  gap: .6rem;
  align-items: center;
  justify-content: space-between;
  padding: .2rem 0 0;
}
/* With no Back, Continue still sits on the right where it always is. */
.slide-nav > :only-child { margin-left: auto; }
.slide-next-locked { margin-left: auto; color: var(--muted); font-size: var(--step--1); }

/* The tenant switcher inside the flow sits beside the exit, not in place of it. */
.slide-tenant { margin-left: .2rem; }

/* ---- setup, skipped and waiting (M40 T9) --------------------------------
   A door, not a scolding: nothing was switched off, and the wording says so before it offers the
   button. Sits directly under the page heading because that is where somebody looks first when they
   have come back wondering where the guide went. */
.setup-resume {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 1.4rem;
}
.setup-resume p { margin: 0; }

/* The exit's placeholder before step one is done: says what unlocks it rather than showing a
   control that would immediately be refused. */
.slide-exit-locked {
  margin: 0;
  color: var(--muted);
  font-size: var(--step--1);
}


/* What is already in the knowledge base, on a step that is done (M40 T14). Not a notice and not a
   warning — a statement of fact with a tick, because coming back to a finished step should feel
   like confirmation rather than like something needs doing. */
.slide-known {
  display: flex;
  gap: .7rem;
  align-items: flex-start;
  padding: .9rem 1.1rem;
  border: 1px solid var(--ok-border);
  border-radius: var(--radius);
  background: var(--ok-soft);
}
.slide-known-mark { margin: 0; color: var(--ok); font-weight: 700; }
.slide-known-title { margin: 0 0 .2rem; }
.slide-known .help { margin: 0; }

/* The sentence or two that says what this slide is doing, before it starts doing it. */
.slide-lead { margin: 0 0 1.2rem; }
.slide-lead p { margin: 0 0 .5rem; color: var(--fg-soft); }
.slide-lead p:last-child { margin-bottom: 0; }
.slide-brand { margin: 1.3rem 0 0; }

/* A credential shown exactly once (M40 T19). Big enough to read off a screen and select cleanly,
   because every Copy button on this host is hidden — navigator.clipboard needs a secure context —
   so selecting the text IS the path. */
.slide-credential { display: block; }
.slide-credential p { margin: 0 0 .5rem; }
.slide-credential p:last-child { margin-bottom: 0; }
/* THE COPY BUTTON SITS BESIDE A SHORT SECRET (M40 T67), not over it. `.slide-snippet` positions its
   button absolutely because an embed snippet is a long wrapping block with room underneath; a
   one-line password has none, and an overlay there would cover the characters somebody is reading. */
.slide-credential-value { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
/* `.notice-ok button { margin-left: auto }` applies here — this credential block IS a `notice-ok` —
   and it threw the button 484px away from the password it belongs to, hard against the far edge of
   the panel. The flex row was correct and something else was moving the item inside it. Measured,
   because on the screenshot that just looks like a right-aligned button someone chose. */
.slide-credential-value .copy-btn { flex: 0 0 auto; margin-left: 0; }
.slide-credential-value .secret {
  display: inline-block;
  padding: .5rem .8rem;
  border: 1px solid var(--ok-border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: var(--step-1);
  letter-spacing: .04em;
  user-select: all;
}
.slide-agent-form { margin: 1.1rem 0 0; }


/* ---- the first-run flow on a small screen (M40 T20) ---------------------
   Measured at 320–414px before any of this was written, which is the only way to find out that
   2.6rem of card padding either side leaves 256px of a 320px phone for the actual question. */
@media (max-width: 46rem) {
  .wrap-bare { padding-inline: 0; padding-top: 1rem; }
  .slide { gap: 1.1rem; }

  /* The card keeps its shape but stops eating the screen. */
  .slide-body {
    padding: 1.5rem 1.25rem;
    border-radius: var(--radius);
  }

  /* --step-3 is 30px, which turns a six-word heading into three lines on a phone. One step down is
     still the largest thing on the screen and still on the scale — a hand-written size here would
     be the twenty-first font-size all over again. */
  .slide-body h1 { font-size: var(--step-2); }
  .slide-blurb { font-size: var(--step-0); margin-bottom: 1.2rem; }

  /* Six dots and five connectors do not fit beside each other at 320px, and a rail that wraps
     leaves a connector pointing at nothing. Shorter links, and it stays one line. */
  .slide-pip:not(:last-child)::after { width: .5rem; }
  .slide-pip > a,
  .slide-pip > span { padding: .25rem .2rem; }

  /* Stacked, and full width. Two half-width buttons side by side on a phone are two things nobody
     can hit; one per line is the shape every mobile form has for a reason. */
  /* FLEX-BASIS FOLLOWS THE MAIN AXIS, and turning this row into a column turned the input's
     `flex: 1 1 18rem` into an eighteen-REM-TALL text box — a 288px input on a 390px phone. Correct
     CSS, catastrophic layout, and invisible in the markup: the rule that broke it is three hundred
     lines away from the one that set it. */
  .slide-field { flex-direction: column; }
  .slide-field input[type="text"],
  .slide-field button { flex: 0 0 auto; width: 100%; }
  /* BOTH nav links, not just the ghost one. This named `.button-ish` only, and when the forward
     link became `a.button` (M40 T62) it silently stopped stretching — a full-width Back beside a
     shrink-wrapped Continue, on the width where the difference is most obvious. */
  .slide-nav a.button-ish,
  .slide-nav a.button { flex: 1 1 auto; text-align: center; }
  .slide-summary li { grid-template-columns: 1.2rem 1fr; }
  .slide-summary-link { grid-column: 2; grid-row: auto; justify-self: start; }
}

/* Between a phone and a laptop the card can breathe again but the shell should not waste the edges. */
@media (min-width: 46rem) and (max-width: 64rem) {
  .slide-body { padding: 2rem 1.9rem; }
}

/* ---- a decision made on the slide (M40 T22) -----------------------------
   A row you can hit anywhere rather than a bare checkbox with a label beside it: the whole point of
   deciding here is that it should be faster than going to the settings page, and a 13px tick box is
   not faster than anything. */
.slide-switches { display: grid; gap: .5rem; margin: 1.1rem 0 0; }
.slide-switch {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .75rem;
  align-items: start;
  padding: .7rem .9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  cursor: pointer;
  transition: border-color .14s ease, background-color .14s ease;
}
.slide-switch:hover { border-color: var(--control-border); }
.slide-switch:has(input:checked) {
  border-color: var(--accent-border);
  background: var(--accent-soft);
}
.slide-switch input { width: 1.1rem; height: 1.1rem; margin-top: .15rem; accent-color: var(--accent); }
.slide-switch strong { display: block; font-size: var(--step-0); }
.slide-switch small { display: block; margin-top: .15rem; color: var(--muted); font-size: var(--step--1); }
.slide-switch:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }


/* ---- the agent console, tightened (M40 T23) -----------------------------
   These queues are the page an agent has open all day. The section cards carried heading-sized
   padding for what is usually one line, so an empty console filled a screen and a busy one pushed
   the third queue below the fold. */
.queue-group, .console-panel, section.group.queue { padding: 1.1rem 1.25rem; }

/* The line that explains a queue is guidance, not content: it should read quieter than the rows it
   sits above, and it was set at body weight and body size. */
.console-tabs + .hint,
.console-tabs + p { color: var(--muted); font-size: var(--step--1); margin: .9rem 0 1rem; }

/* On a phone the topbar's three controls need room to wrap without the identity collapsing. */
@media (max-width: 46rem) {
  .agent-topbar { position: static; margin: -.5rem 0 1rem; padding: .8rem .9rem; }
  .agent-controls { width: 100%; justify-content: space-between; }
}

/* ---- the manual, read mid-shift (M40 T24) -------------------------------
   Sticky because that is how this page is used: an agent opens it with a customer waiting, finds the
   paragraph, and wants out. The way back should be where their eye already is, not at the top of a
   page they have scrolled down. */
.help-back {
  position: sticky;
  top: .6rem;
  z-index: 4;
  margin: 0 0 1rem;
}
.help-back .button-ish { box-shadow: var(--shadow-card); }

/* The copy control on a snippet: an icon for finding, the word for certainty (M40 T26). */
.slide-snippet .copy-btn {
  position: absolute;
  top: .45rem;
  right: .45rem;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}
.slide-snippet .secret { padding-right: 5.5rem; }

/* ---- one question, one row (M40 T28) ------------------------------------
   A name and the address that name signs in with are one thought; stacking them made a
   three-storey form out of a single question. The button sits on the row because it completes it.
   `align-items: end` so the three bottom edges line up whatever the labels do above them. */
.slide-agent-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto;
  gap: .6rem;
  align-items: end;
}
.slide-agent-row label { display: grid; gap: .25rem; margin: 0; min-width: 0; }
.slide-agent-row .label-text { font-size: var(--step--1); font-weight: 600; color: var(--fg-soft); }
.slide-agent-row input { max-width: none; width: 100%; }
.slide-agent-row button { white-space: nowrap; }
.slide-agent-form { margin: 1.1rem 0 0; }
.slide-agent-form .hint { display: block; margin-top: .5rem; }

/* Below the point where three columns stop being readable, it stacks — and the button goes full
   width rather than sitting orphaned at one end. */
@media (max-width: 52rem) {
  .slide-agent-row { grid-template-columns: 1fr; }
  .slide-agent-row button { width: 100%; }
}

/* Spacing is tightened AT THE DECLARATION, not appended here (M40 T28).
   Appending `.slide-body { padding: … }` to the end of this file silently beat the phone media
   query three hundred lines above it — 36.8px of padding on a 320px screen, measured — because
   later source order wins at equal specificity. That is the second time an append has quietly
   overridden an earlier rule in this stylesheet. Edit where the value lives. */

/* ── Colour fields (M40 T32) ──────────────────────────────────────────────────────────────────────
   A swatch, the hex, and the words that are not colours, on one line.

   The text input keeps the `name` and does the posting; the swatch is an assistant that writes into
   it. That is not a stylistic choice — `<input type="color">` always reports a colour, so a picker
   alone cannot express "leave it at the default" and would quietly store black.

   The swatch is sized and stripped by hand because the native control ships with a border and inner
   padding that make it look like a small button rather than the colour it is showing. */
.color-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.color-field .color-text {
  flex: 1 1 11rem;   /* basis follows the MAIN axis — a row, so this is a width */
  min-width: 0;      /* without it a flex item refuses to shrink below its content and overflows */
  font-family: var(--font-mono);
}
.color-swatch {
  flex: 0 0 auto;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}
/* TRIED AND REVERTED, recorded so nobody spends the hour again (M40 T58): Chrome 152 draws the inner
   chip at its own intrinsic size — a rounded pill about 24px wide inside this 40x40 control — and
   sizing `::-webkit-color-swatch` and its wrapper to 100% changes nothing. Verified the rule was
   served and in the cascade before concluding it. The chip is the browser's, the bordered square
   around it is ours, and that reads fine; leaving dead declarations here to prove we tried would be
   the same stale-rule problem the design suite exists to catch. */
.color-swatch::-webkit-color-swatch-wrapper { padding: 2px; }
.color-swatch::-webkit-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 3px); }
.color-swatch::-moz-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 3px); }
.color-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.color-field .color-keyword { flex: 0 0 auto; white-space: nowrap; }

/* Narrow screens: the swatch and hex stay paired and the keywords wrap beneath them, rather than
   three controls each taking a line. */
@media (max-width: 30rem) {
  .color-field .color-text { flex-basis: 8rem; }
}

/* A fragment link into a settings group must not land under the sticky header (M40 T34). Chrome
   auto-expands a closed <details> when you navigate to something inside it, then scrolls — and
   scrolls it to y=0, which is where the header is. The JS handles a click; this handles the link. */
details.settings-group,
.field[id^="setting-"] {
  scroll-margin-top: 5.5rem;
}

/* THE WHOLE ROW IS THE BUTTON (M40 T38, user: *"/internal/settings in tab any where click it must
   open"*).

   The summary sat INSIDE the card's padding — 24px of dead strip left and right, 21.6px above and
   below — all of which reads as part of the row and none of which opened it. Measured: the card
   spans 20→1260 and the summary only 45→1235. The row was also 24px tall, well under the 44px
   the rest of this design system holds itself to for a touch target.

   Pulled out to the card's edges and given the padding back on the inside, so nothing moves and
   everything in the row is clickable. `border-radius: inherit` keeps the hover tint from squaring
   off the card's corners. */
details.settings-group > summary {
  margin: -1.35rem -1.5rem;
  padding: 1.35rem 1.5rem;
  min-height: 2.75rem;
  border-radius: inherit;
}

/* Open, the bottom edge is no longer the card's edge — it is the seam before the fields, so the
   pulled-in margin goes back to nothing and the padding does the spacing. */
details.settings-group[open] > summary {
  margin-bottom: 0;
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

/* Something under the pointer should say so. The row is a control and never looked like one. */
details.settings-group > summary:hover {
  background: color-mix(in oklab, var(--accent) 6%, transparent);
}
details.settings-group > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Who a reviewed conversation was with (M40 T39). Label above value, wrapping into as many columns
   as fit — the fields are few and short, and a table for four rows is heavier than the content. */
.chat-customer {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem 2.2rem;
  margin: 0 0 1.2rem;
  padding: .85rem 1.1rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.chat-customer > div { min-width: 0; }
.chat-customer dt {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
}
.chat-customer dd {
  margin: .15rem 0 0;
  /* An address is long and must not push the panel wider than the page on a phone. */
  overflow-wrap: anywhere;
}

/* The embed-code converter (M40 T40). Two fields to a row where there is room, one where there is
   not — the pairs belong together (what/where, email/name) and splitting them reads as four
   unrelated questions. */
.converter-form { margin-bottom: 1rem; }
.converter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.4rem;
}
.converter-row > .field {
  flex: 1 1 18rem;   /* a row, so this basis is a WIDTH */
  min-width: 0;      /* without it a flex item will not shrink below its content and overflows */
}
.converter-form .help { margin-top: .25rem; }

/* End chat (M40 T47). The title and the way out on one line, the way out small and quiet — it is a
   door, not an action anybody came here to take. */
.chat-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.chat-head > h1 { margin: 0; }
.chat-end { flex: 0 0 auto; }
.chat-end button {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  /* 44px of target on a phone, where this sits beside a heading and is easy to miss or to hit by
     accident — the design audit's floor, and it applies to a small control most of all. */
  min-height: 2.75rem;
  padding-inline: .7rem;
}

/* NOTHING WAITING ⇒ NOT A PANEL (M40 T50, reported live: *"acme/agent?tab=tickets still its new
   request shows"*).

   Quieting it was not enough. An agent working their ticket list had a bordered box above it on every
   load saying "New requests 0 — Nothing is waiting on you", which is a sentence about the screen
   rather than about their work, and it is on EVERY tab because offers are deliberately not a tab of
   their own (an offer has a clock; a tab you have to visit would hide it).

   So the empty state keeps its element — the JS fills it, and it is the aria-live region that
   announces an arrival — and loses everything that makes it look like something to read. What stays
   is the sound toggle, because it is a SETTING, and idle is exactly when somebody sets it. */
.new-requests.is-empty {
  border-color: transparent;
  background: none;
  padding: 0 0 .4rem;
}
.new-requests.is-empty > h2,
.new-requests.is-empty > .empty {
  /* Not display:none on the heading — `position:absolute` keeps it for a screen reader, which should
     still be able to find where offers will appear. Sighted agents get the space back. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.new-requests.is-empty > .hint { margin: 0; }

/* Which desks a new agent answers, on the slide that creates them (M40 T51). */
.slide-desk-pick {
  border: 0;
  margin: .6rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem 1.1rem;
}
.slide-desk-pick > legend { padding: 0; margin-bottom: .35rem; }
.desk-pick {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  /* A checkbox and its word are one target, and 44px of it — this sits among text on a phone. */
  min-height: 2.75rem;
}

/* The quarantine bin (M33 T7). A list of judgements to check, so each row leads with who and the rule
   and then shows the words the rule acted on — the evidence, not a summary of it. */
.quarantine-list { margin: 1rem 0 0; }
.quarantine-row {
  padding: .9rem 0;
  border-top: 1px solid var(--border);
}
.quarantine-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .4rem .8rem;
}
.quarantine-head .who { font-weight: 600; }
.quarantine-said {
  margin: .55rem 0;
  padding: .55rem .9rem;
  border-left: 3px solid var(--border-strong);
  background: var(--surface-sunken);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--fg-soft);
  /* A spam message is often one very long unbroken string of a URL. */
  overflow-wrap: anywhere;
}
.quarantine-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .6rem 1.1rem;
}
.quarantine-actions .inline-form { margin: 0; }

/* "and always allow this sender", beside the release button (M33 T1). */
.allow-always {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 2.75rem;   /* the same 44px floor every other checkbox here holds to */
  color: var(--fg-soft);
  font-size: var(--step--1);
}

/* THE ONE THING ON THE SLIDE (M40 T55, user: *"less content more attractive, easy to point out
   ... what are the things need to focus highlight"*).

   Each step has exactly one action that matters — the line to paste, the desk to name, the switch to
   turn on — and it was set in the same weight as the explanation around it. Cutting the words helped;
   this is the other half, which is making the eye land in the right place before any of them are read.

   A tinted edge rather than a box: the slide is already a card, and a card inside a card is the kind
   of nesting that makes a simple step look like a form. */
.slide-snippet .secret { border-left: 3px solid var(--accent); }

/* The heading immediately above the thing you must do, so it reads as a label on it rather than as
   another section. */
.slide-sub {
  font-size: var(--step-0);
  margin-bottom: .35rem;
}

/* The two switches on the tickets step: the label carries the decision, the small print explains it,
   and they were the same size. */
.slide-switch > span > strong {
  display: block;
  font-size: var(--step-1);
}
.slide-switch > span > small {
  display: block;
  margin-top: .15rem;
  color: var(--fg-soft);
}

/* ---- internal notes (M28 T7) --------------------------------------------- */
/* THE WHOLE POINT OF THIS BLOCK IS THAT A NOTE NEVER LOOKS LIKE A REPLY.

   The one risk this feature carries is an agent typing a note into the reply box or a reply into the
   note box, and the consequence is asymmetric: a reply written as a note is a message nobody receives,
   while a note written as a reply is staff commentary sent to the customer. So the panel is given the
   WARNING palette rather than the neutral surface every other section uses — not because a note is an
   error, but because the one thing a half-reading agent must register before typing is that this box is
   not the other box. Markup alone cannot say that; colour at a glance can. */
.thread-notes {
  background: var(--warn-soft);
  border: 1px solid var(--warn-border);
}
.thread-notes > h2 {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}
/* The note list is a list of statements, not of links or rows — no bullets, no hanging indent. */
.note-list {
  list-style: none;
  margin: .75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .6rem;
}
.note {
  padding: .6rem .75rem;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--warn-border);
}
/* `pre-wrap` so the line breaks somebody typed survive, and `overflow-wrap` so one long unbroken
   token — a URL, an order reference — wraps instead of widening the whole panel. The second half is
   what the listing rows had to learn the hard way at 961px. */
.note-body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.note-by {
  margin: .3rem 0 0;
  font-size: var(--step--1);
  color: var(--muted);
}
.note-form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .5rem;
  margin-top: .75rem;
}
.note-form textarea { width: 100%; }

/* ---- bulk on a queue (M28 T7) -------------------------------------------- */
/* The script toggles the `hidden` ATTRIBUTE and nothing here writes an inline `display`, so the
   stylesheet's one global `[hidden] { display: none !important }` does the hiding. I wrote a
   component-scoped copy of that rule first and `PatternTest` refused it — correctly: the global rule
   carries `!important` precisely so author `display` cannot beat it, which is the trap this project
   has been caught by three times, most recently the widget's "Chat with us" bar painted under a chat
   that was already open. A second copy without the `!important` would be the weaker of the two. */
.queue-bulk {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  min-width: 0;
  margin-top: .6rem;
  padding: .6rem .75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}
.queue-bulk-count { font-weight: 600; font-size: var(--step--1); }
/* Each verb keeps its own field beside it. A single shared box would make "why?" look like it applied
   to Tag as well, and the reason a thread was closed is what the next agent reads. */
.queue-bulk-group {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-width: 0;
}
/* `min-width: 0` because a flex item will not shrink below its content, and an input carries a
   ~20-character intrinsic minimum that `width` alone does not override. */
.queue-bulk-group input[type="text"] {
  min-width: 0;
  width: 12rem;
  max-width: 100%;
  padding: .25rem .6rem;
  font-size: var(--step--1);
}
/* The checkbox column is a gutter, not a column of data — it should not take width from the customer
   name beside it. */
.pick-col { width: 1.6rem; padding-right: 0; }

/* ---- merge as a duplicate (M28 T7) --------------------------------------- */
/* Sits in the thread-verb row beside Resolve and Close, so it keeps their geometry. `min-width: 0`
   for the reason it is needed everywhere a text input lives in a flex row: the item will not shrink
   below its content, and an input carries a ~20-character intrinsic minimum that `width` alone does
   not override. */
.merge-form {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-width: 0;
}
.merge-form input[type="text"] {
  min-width: 0;
  width: 13rem;
  max-width: 100%;
  padding: .25rem .6rem;
  font-size: var(--step--1);
}

/* ---- redaction (M07 T2) --------------------------------------------------- */
/* A redacted turn keeps its place and says so. Dropping it would leave an answer replying to a question
   nobody can see, and a transcript with silent holes stops being evidence — the reader cannot tell a
   removal from a bug. So it is dimmed rather than hidden, and the note underneath carries the reason. */
.msg-redacted .bubble {
  color: var(--muted);
  font-style: italic;
  background: var(--surface-sunken);
  border: 1px dashed var(--border);
}
.redaction-note {
  margin: .3rem 0 0;
  font-size: var(--step--1);
  color: var(--muted);
}
/* Behind a fold on purpose, and this is the one place on the product where that is right: the act is
   irreversible, it is rare, and a button sitting open beside every turn of every transcript is one
   somebody eventually clicks by accident. It is on EVERY turn, so it is not hard to find. */
.redact {
  margin-top: .35rem;
  font-size: var(--step--1);
}
.redact > summary {
  cursor: pointer;
  color: var(--muted);
}
.redact > summary:hover { color: var(--danger); }
.redact form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .4rem;
  margin-top: .5rem;
  padding: .6rem .75rem;
  border: 1px solid var(--danger-border);
  border-radius: var(--radius-sm);
  background: var(--danger-soft);
}
/* `min-width: 0` for the reason it is needed wherever an input lives in a flex container: the item
   will not shrink below its content, and an input carries a ~20-character intrinsic minimum. */
.redact input[type="text"] {
  min-width: 0;
  width: 100%;
  max-width: 28rem;
}
.redact .help { margin: 0; }

/* A deleted thread's control is REVERSIBLE, so it must not borrow the danger palette the per-turn
   redaction uses. Saying "cannot be undone" about something that can is how a product teaches people
   to click through the warnings that matter. */
.delete-thread form {
  border-color: var(--border);
  background: var(--surface-sunken);
}
