/* ============================================================================
   Merestone — the platform's stylesheet, on the brand pack v2 (October 2026).

   "Ordnance" palette, Manrope, hard edges, settled motion, plain words. The file
   began as a port of Mavens' V1 "Quiet Authority" system and keeps that SHAPE —
   a token block every component reads, vanilla CSS, no build step — with the
   pack's values in the tokens and the pack's rules in the handful of places a
   brand decision is hard-coded (headings, the nav, buttons, badges, motion).

   Token NAMES are the V1 ones (--navy, --paper, --accent …) because ~1,700 lines
   read them; renaming is a separate mechanical change. Read them as: --navy is
   Ink, --paper is Ground, --accent is Survey red and is reserved for the one
   control per screen a person acts with (`.btn-accent`).

   The computable half of the pack — one family, radii 4–6px, hairline depth,
   150–200ms motion, weights 400–800, the wordmark, the banned words — is
   asserted by tests/test_brand_rules.py rather than remembered.
   ========================================================================== */

/* ── Design tokens — brand pack v2, "Ordnance" ────────────────────────────── */
:root {
  /* Core */
  --navy:       #101828;   /* Ink — text, dark surfaces, headings, primary actions */
  --navy-700:   #2A3446;   /* Ink, one step lighter — hover */
  --navy-900:   #101828;
  --navy-deep:  #101828;   /* the nav */
  --ink:        #101828;
  --steel:      #3D5578;   /* secondary surfaces, links, info */

  /* Surfaces */
  --paper:      #FBFAF8;   /* Ground — page background */
  --surface:    #FFFFFF;   /* cards, bars */
  --surface-2:  #F0F2F5;   /* soft fills, table heads */
  --surface-3:  #E4E7EC;   /* Chalk */

  /* Lines — Chalk, and one step darker where a line must hold */
  --border:        #E4E7EC;
  --border-strong: #C6CCD6;
  --hairline:      #E4E7EC;

  /* Text. #4A5568 is the muted colour: lighter greys fail contrast on Ground. */
  --text:       var(--ink);
  --text-soft:  #4A5568;
  --text-faint: #8A94A6;
  --text-onnavy: #E4E7EC; /* Chalk on Ink */
  --nav-text:    #E4E7EC; /* the nav is Ink in every theme, so its text is always Chalk */

  /* Survey red — ONE accent per screen, for the moment a person acts (§4) */
  --accent:      #B8431A;
  --accent-soft: #F4E3DC;
  --accent-ink:  #8F3414; /* Red dark */

  /* Status: Sage, Ochre, Red dark, Steel. Error is the darker red so it never
     competes with the accent. */
  --pos:       #5F7A6E;
  --pos-soft:  #DEE7E2;
  --warn:      #A8793A;
  --warn-soft: #F1E6D3;
  --neg:       #8F3414;
  --neg-soft:  #F3DED6;
  --info:      var(--steel);

  /* Data series run Steel, Slate blue, Dusk, Harbour, in that order; Survey red
     is the highlight series only. */
  --chart-1:    var(--steel);
  --chart-2:    #6B84A6;
  --chart-3:    #94A3B8;
  --chart-4:    #3F6670;
  --chart-hi:   var(--accent);
  --chart-fill: #E4E7EC;
  --chart-axis: #C6CCD6;

  /* Shape + depth: square corners on the mark, 4 to 6px everywhere else; no
     shadow deeper than a hairline. */
  --r-sm: 4px;
  --r:    6px;
  --r-lg: 6px;
  --r-pill: 6px;
  --shadow-sm: 0 0 0 1px rgba(16, 24, 40, 0.06);
  --shadow:    0 0 0 1px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 0 0 1px rgba(16, 24, 40, 0.10);

  /* Rhythm */
  --nav-h: 60px;
  --pad-x: 40px;
  --maxw: 1320px;

  /* A spacing scale. Existing rules keep their literals; new work uses these. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 28px;
  --s-7: 40px;

  /* Type — Manrope, one family, four weights (400/500/600/800). There is no mono
     and no serif in the brand; the three names stay so nothing has to be
     re-pointed, and numbers get tabular figures through .num. */
  --serif: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --sans:  "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --mono:  "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Motion: 150 to 200ms, ease-out, stops dead. */
  --ease: cubic-bezier(0.2, 0, 0, 1);
}

/* Density tweak (data-density="compact") */
:root[data-density="compact"] {
  --nav-h: 52px;
  --pad-x: 28px;
}

/* Dark surface — Chalk text on Ink, Steel for secondary (§2). Nothing sets
   data-theme="dark" today; this keeps the variant on-brand if something does. */
:root[data-theme="dark"] {
  --ink:        #E4E7EC;
  --navy:       #E4E7EC;
  --navy-700:   #C6CCD6;
  --navy-900:   #E4E7EC;
  --paper:      #101828;
  --surface:    #182030;
  --surface-2:  #2A3446;
  --surface-3:  #4A5568;
  --border:        #2A3446;
  --border-strong: #4A5568;
  --hairline:      #2A3446;
  --text:       var(--ink);
  --text-soft:  #C6CCD6;
  --text-faint: #8A94A6;
  --text-onnavy: #101828;
  --accent-soft: #4A2416;
  --pos-soft:  #26342E;
  --warn-soft: #3A2E1C;
  --neg-soft:  #3B1F17;
  --chart-fill: #2A3446;
  --chart-axis: #4A5568;
}
* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}

/* Headings sit heavy and close. The DISPLAY weight (800) only at 28px and above (§3);
   below that, 600. */
h1, h2, h3, h4 {
  font-family: var(--sans);
  font-weight: 600;
  color: var(--navy);
  line-height: 1.2;
  letter-spacing: -0.01em;
}
h1 { font-size: 34px; font-weight: 800; letter-spacing: -0.03em; margin: 0 0 8px; line-height: 1.1; text-wrap: balance; }
h2 { font-size: 18px; margin: 0 0 4px; }
h3 { font-size: 14px; margin: 20px 0 8px; }

a { color: var(--steel); text-decoration: none; }
a:hover { text-decoration: underline; }

code, .mono { font-family: var(--mono); font-size: 0.92em; }
.muted { color: var(--text-soft); }
.small { font-size: 12.5px; }
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }

::selection { background: var(--surface-3); color: var(--text); }

/* `hidden` MEANS HIDDEN.
   `[hidden] { display: none }` lives in the user-agent stylesheet, so ANY author rule that
   sets `display` on the same element beats it — and the shared button rule below sets
   `display: inline-block`. That is why the console's Stop button was offered at rest with
   nothing to stop: `el('statusStop').hidden = !v.canStop` was correct and had no effect
   (the strip is a flex container, which blockified the result to `display: block`). Found
   by pointing a browser at dev; reading the JS could not show it, because the JS was right.
   Declared once and globally, because the next `hidden` toggle would hit the same wall. */
[hidden] { display: none !important; }

/* Label — 12 / 600 / 0.12em caps (§3). */
.eyebrow {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
  display: block;
  margin-bottom: 12px;
}

/* ── Top navigation ──────────────────────────────────────────────────────── */
.nav {
  border-bottom: 1px solid var(--navy-700);
  background: var(--navy-deep);
  color: var(--nav-text);
  position: sticky;
  top: 0;
  z-index: 50;
}

.nav-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--pad-x);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  gap: 36px;
}

/* The wordmark: always lower case, display weight, tight (§1, §3). The TEXT is in
   base.html; this is only its setting. */
.nav-brand {
  font-weight: 800;
  font-size: 19px;
  letter-spacing: -0.04em;
  color: var(--nav-text);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}
.nav-brand:hover { text-decoration: none; }

/* The Merestone benchmark mark, masked so it takes whatever colour its surface
   needs rather than shipping a second asset per surface. One colour only, always. */
.nav-mark {
  width: 26px;
  height: 26px;
  background-color: var(--nav-text);
  -webkit-mask: url(merestone-mark.svg) center / contain no-repeat;
  mask: url(merestone-mark.svg) center / contain no-repeat;
  flex: none;
}

.nav-links { display: flex; gap: 4px; align-items: center; margin-left: auto; }

.nav-link {
  color: color-mix(in oklab, var(--nav-text) 72%, transparent);
  text-decoration: none;
  padding: 8px 12px;
  font-weight: 500;
  font-size: 13.5px;
  white-space: nowrap;
  border-radius: var(--r-sm);
  position: relative;
  transition: color 0.15s var(--ease), background 0.15s var(--ease);
}
.nav-link:hover { color: var(--nav-text); background: color-mix(in oklab, var(--nav-text) 8%, transparent); text-decoration: none; }
.nav-link.active { color: var(--nav-text); }
.nav-link.active::after {
  content: "";
  position: absolute;
  left: 12px; right: 12px; bottom: -1px;
  height: 2px;
  /* Chalk, not the accent: the accent is for the moment a person acts (§4). */
  background: var(--nav-text);
}
.nav-user { color: color-mix(in oklab, var(--nav-text) 72%, transparent); cursor: default; }
.nav-admin-pill {
  font-family: var(--mono);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--surface-3);
  color: var(--navy);
  border-radius: var(--r-sm);
  padding: 2px 7px;
  margin-left: 6px;
}
.nav-form { display: inline; margin: 0; }
.nav-link-button { background: transparent; border: 0; font: inherit; cursor: pointer; }

/* ── Page shell ──────────────────────────────────────────────────────────── */
.main { max-width: var(--maxw); margin: 0 auto; padding: 44px var(--pad-x) 120px; }
.main--narrow { max-width: 760px; }

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  padding-bottom: 22px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
/* The heading block SHRINKS rather than forcing the right-hand control onto its own line.
   `.page-head` wraps, so at 420px the applications page's 34px plus wrapped to a second line
   and — `space-between` with a single item — landed on the LEFT, reading as a stray button
   under the text. Found by looking at a screenshot; asserted at 420px in
   tests/_catalogue_driver.mjs. Harmless on the heads with no right-hand content: a lone
   flex child already spans the row. */
.page-head > div:first-child { flex: 1; min-width: 0; }
.page-head h1 { margin: 0; }
.page-head-lead { margin: 10px 0 0; color: var(--text-soft); max-width: 64ch; }

/* ── Cards ───────────────────────────────────────────────────────────────── */
.card {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 22px 24px;
  background: var(--surface);
  margin: 16px 0;
  box-shadow: var(--shadow-sm);
}
.card h2 { margin-top: 0; }

/* ── The application catalogue ───────────────────────────────────────────── */
.app-catalogue {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
  margin: 20px 0 28px;
}
.app-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 20px 22px 18px;
  text-decoration: none;
  color: var(--text);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s var(--ease), box-shadow 0.15s var(--ease), transform 0.15s var(--ease);
  position: relative;
  overflow: hidden;
}
/* The card's own menu — on the name's line, at its right end (PRIMER-525).
 *
 * ⚠️ A FLEX ITEM, NOT ABSOLUTELY POSITIONED. `.app-card` sets `overflow: hidden` for the
 * hover edge below, which would clip an absolutely positioned dropdown the moment it left
 * the card's box. Sitting in the flow beside the name, its dropdown opens downward INTO the
 * card, where nothing clips it.
 */
/* The name and the menu on ONE line, the menu at its right end. `baseline` rather than
 * `center` so the dots sit on the name's own line rather than floating beside a two-line
 * title; `space-between` puts them at the right edge without the menu needing a width. */
.app-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
/* The name is what gives, so a long one wraps instead of pushing the dots off the card. */
.app-card-head .app-card-name {
  min-width: 0;
}
.app-card-menu {
  position: relative;
  flex: 0 0 auto;
}
.app-card-dots {
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  padding: 2px 6px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.app-card-dots:hover,
.app-card-dots:focus-visible {
  color: var(--text);
  background: var(--surface-2, rgba(0, 0, 0, 0.04));
}
.app-card-menu-items {
  position: absolute;
  top: 100%;
  /* ⚠️ RIGHT-ANCHORED, to match the control. Left-anchored from a control on the right edge
   * would push a 220px dropdown past the card, and `.app-card` sets `overflow: hidden` — so
   * it would be silently clipped rather than visibly wrong. */
  right: 0;
  z-index: 5;
  min-width: 220px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--r-md, 6px);
  background: var(--surface);
  box-shadow: var(--shadow-md, 0 6px 18px rgba(0, 0, 0, 0.12));
}
.app-card-menu-items button {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.app-card-menu-items button:hover,
.app-card-menu-items button:focus-visible {
  background: var(--surface-2, rgba(0, 0, 0, 0.05));
}

/* The navy edge that appears on hover — V1's signature on this card. */
.app-card::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--navy);
  opacity: 0;
  transition: opacity 0.15s var(--ease);
}
.app-card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
  transform: translateY(-2px);
  text-decoration: none;
}
.app-card:hover::before { opacity: 1; }
.app-card-name { font-weight: 600; font-size: 16px; letter-spacing: -0.01em; line-height: 1.3; }
/* The card stopped being a link when it gained buttons (a <button> inside an <a> is invalid
   and breaks both), so the NAME carries the link now and has to look like the card did. */
.app-card-name a { color: var(--text); text-decoration: none; }
.app-card-name a:hover { color: var(--navy); text-decoration: none; }
/* Open the builder, or start the app. Two things, under every app, because those are the two
   things you do with one — and starting was previously only possible from inside the
   builder's sidebar. */
.app-card-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.app-card-actions .btn { padding: 7px 14px; font-size: 13px; }
.app-card-note { margin: 10px 0 0; color: var(--text-soft); }
.app-card-meta {
  font-size: 12px;
  color: var(--text-soft);
  display: flex;
  gap: 10px;
  align-items: center;
  margin-top: auto;
}
.app-card-category {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 3px 9px;
  border-radius: var(--r-pill);
}

/* A square icon button — the applications page's `+`. Kept in the same navy border/hover
   language as `.btn` rather than inventing a second one.

   `align-self: flex-start` because `.page-head` aligns its children to `flex-end`, which
   would drop the plus to the bottom of a three-line heading block. It was asked for in the
   TOP right. */
.icon-btn {
  flex: none;
  align-self: flex-start;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 20px;
  line-height: 1;
  color: var(--navy);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.icon-btn:hover { border-color: var(--navy); background: var(--surface-2); }

/* The modal the plus opens. A native <dialog>, so this is styling only — no positioning, no
   z-index, no scroll locking, none of the things a hand-rolled overlay gets wrong. */
dialog.modal {
  max-width: 440px;
  width: calc(100vw - 32px);
  padding: 22px 24px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
}
dialog.modal::backdrop { background: color-mix(in oklab, var(--navy) 45%, transparent); }
dialog.modal h2 { margin: 0 0 4px; font-size: 18px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
/* Three actions instead of two (PRIMER-542: cancel, and the two ways to copy). They wrap
   rather than shrink, because "Copy structure only" losing its last word is how a choice
   about customer data gets misread. */
.modal-actions-stack { flex-wrap: wrap; }
.modal-actions-stack .btn { white-space: nowrap; }

.new-app-card {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 24px 26px;
  margin: 20px 0 8px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}
.new-app-card h2 { margin: 0 0 4px; font-size: 18px; }
.new-app-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-top: 16px; }
.new-app-row .grow { flex: 1; min-width: 200px; }
.new-app-row label {
  display: flex; flex-direction: column; gap: 6px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-faint);
}

/* ── Fields and buttons ──────────────────────────────────────────────────── */
.new-app-row input,
.login-card input,
.field input,
.console-form textarea {
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: 14px/1.5 var(--sans);
  text-transform: none;
  letter-spacing: 0;
  background: var(--surface);
  color: var(--text);
}
.new-app-row input:focus,
.login-card input:focus,
.field input:focus,
.console-form textarea:focus {
  outline: none;
  border-color: var(--steel);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--steel) 30%, transparent);
}

/* Shared primary look, as in V1: navy fill, on-navy text, brightness on hover. */
.new-app-row button,
.login-card button,
.console-form button,
.btn {
  padding: 9px 18px;
  white-space: nowrap;
  border: 1px solid var(--navy-900);
  border-radius: var(--r-sm);
  background: var(--navy);
  color: var(--text-onnavy);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
  transition: filter 0.12s var(--ease), transform 0.05s var(--ease);
}
.new-app-row button:hover,
.login-card button:hover,
.console-form button:hover,
.btn:hover { filter: brightness(1.08); text-decoration: none; }
.new-app-row button:active,
.login-card button:active,
.console-form button:active,
.btn:active { transform: translateY(1px); }

/* Secondary: the same shape, no fill. For actions that are not the point of the page. */
.btn-quiet {
  background: var(--surface);
  color: var(--navy);
  border-color: var(--border-strong);
  font-weight: 500;
}
.btn-quiet:hover { background: var(--surface-2); filter: none; }
.btn-danger { background: var(--surface); color: var(--neg); border-color: var(--border-strong); font-weight: 500; }
.btn-danger:hover { background: var(--neg-soft); filter: none; }

/* THE accent. Survey red, on exactly one control per screen — the moment a person acts
   (§4): Sign in, Create, Send, Publish. Everything else is Ink or outlined.
   tests/_catalogue_driver.mjs asserts the create dialog carries one and only one. */
.login-card button.btn-accent,
.console-form button.btn-accent,
.btn-accent { background: var(--accent); color: #FFFFFF; border-color: var(--accent-ink); }
.login-card button.btn-accent:hover,
.console-form button.btn-accent:hover,
.btn-accent:hover { background: var(--accent-ink); filter: none; }

button.link {
  background: transparent; border: 0; color: var(--navy);
  font: inherit; cursor: pointer; padding: 2px 4px;
}
button.link:hover { text-decoration: underline; }

.field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 14px; }
.field > span {
  font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-soft);
}

/* ── Auth ────────────────────────────────────────────────────────────────── */
.login-shell { max-width: 400px; margin: 8vh auto; padding: 0 20px; }
.login-card {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 36px 34px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 16px;
  box-shadow: var(--shadow);
}
/* The stacked lockup: mark above, wordmark below — lower case, 800, tight (§1). */
.login-card h1 { margin: 0; font-size: 28px; font-weight: 800; letter-spacing: -0.04em; text-align: center; }
.login-card .muted { color: var(--text-soft); margin: -10px 0 4px; font-size: 13px; }
.login-card label {
  display: flex; flex-direction: column; gap: 7px;
  font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-soft);
}
.login-card button { margin-top: 6px; padding: 12px 14px; }
.login-brand {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-bottom: 6px;
}
.login-brand .nav-mark { background-color: var(--navy); width: 40px; height: 40px; }
/* The bare pages a shared link may show (PRIMER-578): the same card, its form stacked the
   way the sign-in card stacks its own children. */
.bare-form { display: flex; flex-direction: column; gap: 16px; }
.bare-app { margin: 0; text-align: center; font-weight: 600; color: var(--text); }
.login-card .bare-note { margin: 0; text-align: center; font-size: 13px; }

/* ── Flashes ─────────────────────────────────────────────────────────────── */
.flash { padding: 10px 14px; border-radius: var(--r-sm); font-size: 13px; border: 1px solid transparent; }
.flash-error { background: var(--neg-soft); color: var(--neg); border-color: color-mix(in oklab, var(--neg) 35%, transparent); }
.flash-ok { background: var(--pos-soft); color: var(--pos); border-color: color-mix(in oklab, var(--pos) 35%, transparent); }
.flash-warn { background: var(--warn-soft); color: var(--warn); border-color: color-mix(in oklab, var(--warn) 35%, transparent); }
.flash-info { background: color-mix(in oklab, var(--info) 8%, var(--surface)); color: var(--info); border-color: color-mix(in oklab, var(--info) 25%, transparent); }
/* The person-wide budget banner (PRIMER-602 B): between the nav and the page, at the page's
   own width and gutters, with Dismiss on the same line as the sentence. The wrap carries the
   gutters so a dismissed banner (hidden) leaves no gap behind. */
.budget-banner-wrap, .sticky-notice-wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad-x); }
.sticky-notice { margin-top: 16px; }
.budget-banner { display: flex; align-items: center; gap: 10px; margin-top: 16px; }
.budget-banner p { margin: 0; flex: 1; min-width: 0; }
.budget-banner button { padding: 4px 12px; flex: 0 0 auto; }

.empty {
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-lg);
  padding: 34px 24px;
  text-align: center;
  color: var(--text-soft);
  background: var(--surface);
}

/* ── The builder console ─────────────────────────────────────────────────── */
/* No V1 equivalent — V1's builder is a floating widget, this is a full surface.
   Built from the same tokens: navy structure, off-white paper, mono for machine
   output and Manrope for anything a person wrote. */
.console {
  display: grid;
  grid-template-columns: 1fr 21rem;
  /* `dvh`, not `vh`. On mobile Safari `100vh` is the height WITHOUT the address bar, so a
     `vh` frame puts the composer underneath the browser's own chrome — which is the one
     place a pinned composer must never be. `dvh` tracks the visible viewport. */
  height: calc(100dvh - var(--nav-h));
  /* The frame does not scroll; the conversation inside it does. Without this a tall child
     produces a second scrollbar beside the conversation's own. */
  overflow: hidden;
}
.console-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* min-height:0 is what actually lets the conversation scroll. A flex item's default
     min-height is auto, so a tall child makes the ITEM grow instead of scrolling, and the
     composer gets pushed off the bottom of the frame. */
  min-height: 0;
  background: var(--surface);
}
.console-log {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  padding: 18px 26px 26px;
  margin: 0;
  color: var(--text);
  background: var(--surface);
  /* Anchored to the bottom while a build streams, without the jump that scroll-anchoring
     otherwise produces when an older chunk collapses above the viewport. */
  overflow-anchor: none;
}
.console-log:empty::before {
  content: "Ask for a change and the work appears here as it happens.";
  color: var(--text-faint);
  font-family: var(--sans);
}

/* ── One iteration ──────────────────────────────────────────────────────────
   A <details> per turn, so a finished one folds to a single line. Before this the whole
   session was one <pre> and a dozen prompts meant one enormous scroll. */
.console-turn {
  border-bottom: 1px solid var(--hairline);
}
.console-turn > summary {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 10px 4px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--text);
  /* The default triangle is replaced below; `list-style: none` alone leaves it in Safari. */
  list-style: none;
}
.console-turn > summary::-webkit-details-marker { display: none; }
.console-turn > summary::before {
  content: "▸";
  color: var(--text-faint);
  font-size: 10px;
  line-height: 1.6;
  flex: none;
}
.console-turn[open] > summary::before { content: "▾"; }
.console-turn > summary:hover { background: var(--paper); }
.console-turn > summary:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: -2px;
}
.turn-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
}
.turn-meta {
  flex: none;
  font-size: 11.5px;
  color: var(--text-faint);
}
.console-turn.is-running > summary .turn-title { color: var(--navy); }
.console-turn.is-failed > summary .turn-title { color: var(--neg); }
/* The machine's output. Mono, wrapped, and never a horizontal scrollbar — a wide stack
   trace should fold rather than hide the rest of the line. */
.turn-body {
  margin: 0 0 14px;
  padding: 0 4px 0 22px;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-soft);
}
.turn-body:empty { display: none; }
/* The status strip. Above the log and outside it, so "what is happening now" does not
   scroll away from the work it describes. */
.status-strip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 26px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  font-family: var(--sans);
  font-size: 12.5px;
  color: var(--text);
}
.status-strip { flex: none; }
/* At rest the strip still says something — the application's own state — but it should not
   compete with the conversation for attention the way an active turn does. */
.status-strip.is-idle { color: var(--text-soft); background: var(--paper); }
.status-strip.is-idle .status-label { font-weight: 400; }
.status-label { font-weight: 500; }
.status-detail {
  color: var(--text-faint);
  font-family: var(--mono);
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}
.status-elapsed { color: var(--text-faint); font-size: 11.5px; }
/* Who is building (PRIMER-593). Present only while somebody is; at rest it takes no space. */
.status-driver {
  color: var(--text-soft);
  font-size: 11.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 40%;
}
.status-driver:empty { display: none; }
.status-stop { padding: 3px 10px; font-size: 11.5px; }
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-faint);
  flex: none;
}
/* Working pulses; stalled and failed deliberately do NOT. Animation that continues through
   a stall is what made the product read as hung in the first place — movement here has to
   mean progress, or the strip is just a nicer spinner. */
.status-dot.is-working {
  background: var(--navy);
  animation: status-pulse 1.6s var(--ease) infinite;
}
.status-dot.is-stalled { background: var(--warn); }
.status-dot.is-failed { background: var(--neg); }
@keyframes status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}
@media (prefers-reduced-motion: reduce) {
  .status-dot.is-working { animation: none; }
}
.console-form {
  display: flex;
  /* Wraps so the dictation note can take a full row above the composer rather than
     competing with it for width. */
  flex-wrap: wrap;
  gap: 10px;
  /* `flex-end`, so the Send button stays on the last line as the textarea grows upwards
     rather than floating in the middle of a four-line prompt. */
  align-items: flex-end;
  padding: 14px 26px 18px;
  border-top: 1px solid var(--border);
  background: var(--surface);
  flex: none;
}
/* The composer. One row to start, grown by the script from `scrollHeight`, and capped here
   rather than in JavaScript so the ceiling lives with the rest of the layout. 40% of the
   frame: a long prompt must never squeeze the conversation out of existence. */
.console-form textarea {
  flex: 1;
  min-width: 0;
  resize: none;
  max-height: 40dvh;
  overflow-y: auto;
  line-height: 1.5;
  font-family: var(--sans);
  font-size: 13.5px;
}
.console-form button { flex: none; }

/* Dictation. The mic is a QUIET button: the shared rule above gives every button in the form
   the navy primary fill, and two navy buttons side by side read as two ways to send.
   ⚠️ SELECTOR SPECIFICITY, not source order. `.console-form button` is (0,1,1) — a class and
   a type — so a bare `.mic-btn` at (0,1,0) LOSES however late it is declared, and the mic
   rendered navy-on-navy, indistinguishable from Send. Measured in a browser
   (`backgroundColor` identical to the submit button's) after the screenshot looked wrong;
   the recording state got away with it only because `.mic-btn.is-recording` is (0,2,0).
   `.console-form .mic-btn` is (0,2,1) and wins. Asserted now in
   tests/_console_driver.mjs — "it does not look like a second Send" is a visual claim, and
   this one was wrong. */
.console-form .mic-btn {
  width: 38px;
  padding: 0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: flex-end;
  height: 38px;
  color: var(--navy);
  background: var(--surface);
  border-color: var(--border-strong);
}
.console-form .mic-btn:hover { border-color: var(--navy); background: var(--surface-2); filter: none; }
/* Recording. Colour AND a pulse AND the announced note in the markup — a live microphone
   that only differs by hue is one a colour-blind builder cannot tell from an idle one. */
.console-form .mic-btn.is-recording {
  color: var(--text-onnavy);
  background: var(--neg);
  border-color: var(--neg);
  animation: mic-live 1.6s var(--ease) infinite;
}
/* Opacity, not a growing ring: nothing bounces or lingers (§4). */
@keyframes mic-live {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
  .console-form .mic-btn.is-recording { animation: none; }
}
.console-mic-note {
  flex: 0 0 100%;
  margin: 0;
  color: var(--text-soft);
}
/* The budget notice (PRIMER-602): a full row above the composer, like the mic note, with
   its Dismiss on the same line. `.console-form .console-budget button` and not `.btn-quiet`
   alone, for the specificity reason the mic button above records: `.console-form button`
   (0,1,1) would otherwise paint Dismiss navy, a second Send. */
.console-budget {
  flex: 0 0 100%;
  display: flex;
  align-items: center;
  gap: 10px;
}
.console-budget p { margin: 0; flex: 1; min-width: 0; }
.console-form .console-budget button {
  padding: 4px 12px;
  color: var(--navy);
  background: var(--surface);
  border-color: var(--border-strong);
}

/* ── the panel's files and history ──────────────────────────────────────────── */
.side-count { color: var(--text-faint); font-weight: 400; font-size: 11.5px; margin-left: 6px; }
.side-filter {
  width: 100%;
  margin-bottom: 8px;
  padding: 6px 9px;
  font: 12.5px/1.4 var(--sans);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
}
.side-filter:focus {
  outline: none;
  border-color: var(--steel);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--steel) 30%, transparent);
}
/* A file is a button because it opens something. Styled to read as the list item it replaced
   rather than as a control — a sidebar of forty buttons is noise. */
.side-file {
  display: block;
  width: 100%;
  padding: 0;
  font: inherit;
  color: var(--text-soft);
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  word-break: break-all;
}
.side-file:hover { color: var(--navy); text-decoration: underline; }
/* A large upload lives in the object store, which the viewer cannot read, so its name is
   text rather than a button — see paintUploads. Same shape as its neighbours, no affordance
   it cannot honour. */
.side-file-static { cursor: default; }
.side-file-static:hover { color: var(--text-soft); text-decoration: none; }
/* Who sent it, when, and how big. Under the name rather than beside it: a sidebar is 300px
   and a filename plus a timestamp does not fit on one line. */
.upload-meta {
  display: block;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: 1.3;
}
/* A history entry that expands to the whole prompt.
   `word-break: normal` overrides `.console-list li`, which uses `break-all` — right for a
   long file path, wrong for prose: a summary is now a sentence, and it was breaking as
   "so it han / dles overpayments". `anywhere` still rescues a single unbroken long token. */
.side-turn > summary,
.side-turn .side-prompt { word-break: normal; overflow-wrap: anywhere; }
.side-turn > summary { cursor: pointer; color: var(--text-soft); }
.side-turn > summary:hover { color: var(--navy); }
.side-turn[open] > summary { color: var(--navy); font-weight: 500; }

/* ── a panel section, and what a turn changed (PRIMER-473) ──────────────────── */
/* `<details>` carrying the same weight the `h3` did, so a collapsed panel reads as the same
   product rather than as a different one. The section headings were `h3`; these summaries
   take that rule's typography verbatim. */
.side-section > summary {
  font-family: var(--sans);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin: 26px 0 10px;
  cursor: pointer;
  list-style-position: outside;
}
.side-section:first-of-type > summary { margin-top: 22px; }
.side-section > summary:hover { color: var(--navy); }
.side-section-head { display: flex; justify-content: flex-end; margin: -4px 0 6px; }
/* The danger zone sits at the foot of the panel with air above it, so the gap itself says
   that what follows is not part of the routine controls. */
.side-danger { margin-top: 28px; border-top: 1px solid var(--border); padding-top: 4px; }
.side-danger > summary { color: var(--neg); }
.side-danger form { margin: 0 0 8px; }
.side-danger .btn-danger { width: 100%; }

.side-turn-actions { display: flex; align-items: baseline; gap: 10px; margin: 4px 0 2px; }
/* "3 files +42 −7" — a button when there is a diff behind it, flat text when there is not
   (a checkpoint from before the data existed). Same size either way, so the list does not
   jump between applications. */
.side-change {
  padding: 0;
  font: 11px/1.4 var(--mono);
  color: var(--text-faint);
  background: none;
  border: 0;
  cursor: pointer;
}
.side-change:hover { color: var(--navy); text-decoration: underline; }
.side-change-flat { cursor: default; }
.side-change-flat:hover { color: var(--text-faint); text-decoration: none; }
/* Who made the checkpoint (PRIMER-593) — or that the platform cannot say, which is quieter
   than a name because it is the absence of one. */
.side-who { font: 11px/1.4 var(--sans); color: var(--text-soft); }
.side-who.is-unverified { color: var(--text-faint); }
/* Whose work a restore undoes (PRIMER-593 E): up to 500 rows, so it scrolls inside the dialog. */
.restore-undoes { max-height: 14rem; overflow: auto; }
.restore-undoes .restore-who { margin: 8px 0 2px; font-weight: 600; }
.restore-undoes ul { margin: 0 0 4px 18px; padding: 0; }
.side-restore {
  padding: 0;
  font: 11px/1.4 var(--sans);
  color: var(--text-faint);
  background: none;
  border: 0;
  cursor: pointer;
}
.side-restore:hover { color: var(--navy); text-decoration: underline; }
.side-prompt {
  margin: 6px 0 10px;
  padding: 8px 10px;
  max-height: 40dvh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font: 12px/1.55 var(--mono);
  color: var(--text);
  background: var(--surface-2);
  border-radius: var(--r-sm);
}

/* The agent's reply under a checkpoint's prompt (PRIMER-587). The same box as the prompt,
   set apart by a rule on the left so "what was asked" and "what was said" do not read as one
   block. Plain text in a <pre>: Markdown is deliberately not rendered (spec D11). */
.side-turn .side-reply { word-break: normal; overflow-wrap: anywhere; }
.side-reply-label {
  margin: 8px 0 0;
  font: 600 10.5px/1.4 var(--sans);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.side-reply {
  margin: 4px 0 10px;
  padding: 8px 10px;
  max-height: 40dvh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font: 12px/1.55 var(--mono);
  color: var(--text);
  background: var(--surface-2);
  border-left: 2px solid var(--border);
  border-radius: var(--r-sm);
}
.side-reply-outcome { margin: 8px 0 0; font: 500 12px/1.4 var(--sans); color: var(--neg); }
.side-reply-none,
.side-reply-cut { margin: 4px 0 8px; font: 11.5px/1.4 var(--sans); color: var(--text-faint); }

/* The Data section: shared datasets this app publishes and consumes. */
.icon-btn-sm { width: 22px; height: 22px; font-size: 15px; margin-left: 8px; vertical-align: middle; }
.btn-small { padding: 3px 9px !important; font-size: 11.5px; margin-top: 6px; }
/* One grant, as a reviewable line. Laid out so the principal and capability read first and
   the revoke sits at the end — a grant list is something a person audits. */
.grant-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 0;
  font-size: 12px;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}
.grant-row .btn-small { margin-top: 0; flex: none; }
.grant-row .badge { flex: none; }

/* A live share's write history (PRIMER-564): what other applications wrote into this table.
   Scrolls sideways inside the panel rather than widening it. A row that did not apply — or
   whose outcome could not be settled — is marked, never hidden. */
.write-history-box { overflow-x: auto; margin: 6px 0; }
.write-history { border-collapse: collapse; font-size: 11.5px; width: 100%; }
.write-history th, .write-history td {
  text-align: left;
  padding: 3px 6px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.write-history th { color: var(--text-soft); font-weight: 500; }
.write-history tr.is-aborted td { color: var(--text-soft); text-decoration: line-through; }
.write-history tr.is-unknown td, .write-history tr.is-pending td { color: var(--warn); }

/* The owner's actions on their own dataset: share, reassign, delete. Deliberately a
   different class from `.grant-row` even though it looks the same — the browser driver
   counts `.grant-row` elements to prove every grant is listed and revocable, and a row of
   buttons wearing that class would inflate the count it checks. */
.ds-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 4px 0;
}
.ds-actions .btn-small { margin-top: 0; flex: none; }

/* A deleted dataset still inside its grace window. Dimmed, because it is not usable — but
   present, because the whole point is that the undo is discoverable. Same reasoning as the
   admin card's amber border, and again a distinct class from `.grant-row`. */
.ds-pending {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex: 1 1 100%;
  padding: 4px 0;
  font-size: 12px;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}
.ds-pending .btn-small { margin-top: 0; flex: none; }

/* The platform's own feedback button (PRIMER-440). Fixed, bottom-right, deliberately the
   same position and shape as the one the gateway injects into applications — a builder
   moving between the console and a running app should not have to look for it twice. */
.feedback-fab {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 900;
  padding: 8px 14px;
  border: 1px solid var(--navy);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--navy);
  font: 600 12px/1.2 var(--sans);
  cursor: pointer;
  box-shadow: var(--shadow);
}
.feedback-fab:hover { background: var(--surface-2); }
#pfDialog textarea { width: 100%; box-sizing: border-box; font: inherit; }

/* The assistant (PRIMER-583): above the feedback button, same shape, so the corner reads as one
   place to ask for help. */
.assistant-fab {
  position: fixed;
  right: 16px;
  bottom: 56px;
  z-index: 900;
  padding: 8px 14px;
  border: 1px solid var(--navy);
  border-radius: var(--r-pill);
  background: var(--navy);
  color: var(--surface);
  font: 600 12px/1.2 var(--sans);
  cursor: pointer;
  box-shadow: var(--shadow);
}
.assistant-panel {
  position: fixed;
  right: 16px;
  bottom: 96px;
  z-index: 901;
  width: min(420px, calc(100vw - 32px));
  max-height: min(70vh, 640px);
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--line, #ccc);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.assistant-panel[hidden] { display: none; }
.assistant-head { display: flex; justify-content: space-between; align-items: center; }
.assistant-note { margin: 0; }
.assistant-log { flex: 1 1 auto; overflow-y: auto; min-height: 120px; }
.assistant-line { margin: 0 0 8px; white-space: pre-wrap; overflow-wrap: anywhere; }
.assistant-line.from-person { font-weight: 600; }
.assistant-line.changed { font-weight: 600; color: var(--navy); }
.assistant-line.tool { color: var(--muted, #667); font-size: 12px; }
.assistant-line.problem { color: var(--danger, #a33); }
.assistant-form textarea { width: 100%; box-sizing: border-box; font: inherit; }
.assistant-earlier { margin: 0; padding: 0; list-style: none; max-height: 30vh; overflow-y: auto; }
.assistant-earlier li { margin: 0 0 4px; }
.assistant-earlier button { width: 100%; text-align: left; }
/* Only the conversation log grows and shrinks; everything else keeps its height, and the panel
   scrolls if together they are taller than it. Measured: with two proposal cards, flex-shrink
   squashed the Earlier list to nothing and its buttons could not be clicked. */
.assistant-panel { overflow-y: auto; }
.assistant-panel > :not(.assistant-log) { flex: none; }
.assistant-proposals { max-height: 35vh; overflow-y: auto; }
.assistant-proposals:empty { display: none; }
.assistant-card { border: 1px solid var(--navy); border-radius: var(--r-sm); padding: 8px; margin: 0 0 8px; }
.assistant-card p { margin: 0 0 6px; overflow-wrap: anywhere; }
.assistant-card code { font-size: 11px; overflow-wrap: anywhere; }
.assistant-card .assistant-card-warning { color: var(--danger, #a33); }
.assistant-card input { width: 100%; box-sizing: border-box; font: inherit; margin-bottom: 6px; }
/* A proposal that has been answered. No border and no padding: it is a RECORD of what the
   person chose, not a thing they can still act on, and leaving it looking like a card is what
   made a confirmed proposal read as a stuck one. */
.assistant-settled { margin: 0 0 8px; }
.assistant-settled p { margin: 0 0 2px; overflow-wrap: anywhere; }
.assistant-settled .assistant-card-warning { color: var(--danger, #a33); }
.assistant-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 6px; }
#datasets li { display: flex; flex-wrap: wrap; gap: 6px; justify-content: space-between; }
/* A <details> in the list has to span it, or the summary and the grants sit side by side. */
#datasets li > details { flex: 1 1 100%; }

/* The file viewer. Wide, because code in a 440px dialog is as unreadable as code in a 300px
   sidebar — which is the reason it is a dialog at all. */
dialog.modal-wide {
  max-width: min(980px, calc(100vw - 48px));
  width: min(980px, calc(100vw - 48px));
}
.modal-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.modal-head h2 { font-size: 14px; font-family: var(--mono); word-break: break-all; }
.file-body {
  margin: 12px 0 0;
  padding: 12px 14px;
  max-height: 70dvh;
  /* BOTH axes. Code has long lines and wrapping them silently changes what the file looks
     like; scrolling inside the viewer keeps the console's own frame still. */
  overflow: auto;
  white-space: pre;
  font: 12.5px/1.6 var(--mono);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

.console-side {
  border-left: 1px solid var(--border);
  padding: 22px 22px 40px;
  overflow: auto;
  background: var(--paper);
}
.console-side h3 {
  font-family: var(--sans);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin: 26px 0 10px;
}
.console-side h3:first-of-type { margin-top: 22px; }
.console-actions { display: flex; flex-direction: column; gap: 8px; }
.console-actions form { margin: 0; }
.console-actions .btn, .console-actions button { width: 100%; text-align: center; }
.console-list { list-style: none; margin: 0; padding: 0; }
.console-list li {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text-soft);
  padding: 5px 0;
  border-bottom: 1px solid var(--hairline);
  word-break: break-all;
}
.console-list li:last-child { border-bottom: 0; }

@media (max-width: 900px) {
  /* STILL A FIXED FRAME. This used to set `height: auto`, which abandoned the frame on a
     narrow window: the whole document scrolled and the composer ended up wherever the page
     happened to end, which is the opposite of a pinned composer. It becomes one column
     instead — the sidebar ABOVE the conversation, scrolling with the frame rather than
     beside it — so the composer stays on the bottom edge at every width. */
  .console {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
  }
  .console-side {
    border-left: 0;
    border-bottom: 1px solid var(--border);
    /* Beside the conversation it is a full-height column; above it, it must not eat the
       conversation, so it is capped and scrolls itself. */
    max-height: 38dvh;
    order: -1;
    padding: 14px 20px 18px;
  }
  :root { --pad-x: 20px; }
}

/* A label that is read but not seen. The composer needs one — a placeholder is not a label,
   and it disappears the moment anything is typed. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The guidance an application was built with, in its console panel (PRIMER-406/D4). */
.kb-pinned { margin: 0 0 var(--s-2); padding-left: 1.1rem; font-size: 13px; }
.kb-pinned li { margin: 2px 0; }
.kb-upgrade {
  margin: 0 0 var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  color: var(--accent-ink);
}
/* Adding or taking guidance on an existing application (PRIMER-634 D14). ⚠️ The select's
   options carry a name and a size, and a native <select> sizes to its WIDEST option — at a
   420px window that pushed the page sideways and its horizontal scrollbar added 15px of
   height, breaking the console's "composer pinned, page unscrolled" layout. So it may shrink. */
#kbAddForm, .kb-take-form { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-2); margin: 0 0 var(--s-2); min-width: 0; max-width: 100%; }
#kbAddForm select { flex: 1 1 10rem; min-width: 0; max-width: 100%; }
#kbAddForm p { flex-basis: 100%; margin: 0; }

/* ── choosing guidance when an application is created (PRIMER-406) ────────────── */
/* Inside the create dialog, which until now held one field. Kept quiet: the selection is
   optional and the dialog's premise is still "a name is enough to start". */
.kb-choices {
  margin: var(--s-4) 0 0;
  padding: var(--s-3) var(--s-4) var(--s-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--paper);
}
.kb-choices legend {
  padding: 0 var(--s-2);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-soft);
}
.kb-choice {
  display: flex;
  gap: var(--s-2);
  align-items: baseline;
  padding: var(--s-1) 0;
  font-size: 13.5px;
  cursor: pointer;
}
.kb-choice input { margin: 0; }
.kb-note { margin: var(--s-2) 0 0; }
.kb-group { margin: var(--s-3) 0 var(--s-1); font-weight: 600; }
/* Authoring a knowledge base: a plain textarea, because the body is never rendered as HTML
   (see the comment in admin.html). Monospace-ish line height so Markdown structure is legible
   while it is being written. */
.kb-body {
  width: 100%;
  min-height: 18rem;
  padding: var(--s-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: 13px/1.6 var(--sans);
  background: var(--surface);
  color: var(--text);
  resize: vertical;
}
.kb-history { margin: 0 0 var(--s-3); font-size: 13px; }
.kb-history ul { margin: var(--s-2) 0 0; padding-left: 1.1rem; }
.kb-body:focus {
  outline: none;
  border-color: var(--navy);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--steel) 22%, transparent);
}

/* ── the admin page (PRIMER-474) ─────────────────────────────────────────────── */
/* Eight zones stacked with no way to find anything. Sections with a sticky index rather
   than tabs: every action here is a form POST that re-renders the page, and a tab's
   selected state would have to survive that. An anchor has no state to lose. */

/* The tabs. LINKS, with the selected one marked by the server — see admin.html. Sticky,
   because they are the page's navigation and scrolling a long table should not scroll them
   away. */
.admin-tabs {
  position: sticky;
  top: var(--nav-h);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: 22px 0 4px;
  padding: 6px 0 0;
  background: var(--paper);
  border-bottom: 1px solid var(--border);
}
.admin-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border: 1px solid transparent;
  border-bottom: 2px solid transparent;
  font-size: 13px;
  color: var(--text-soft);
}
.admin-tab:hover { color: var(--navy); text-decoration: none; }
/* The selected tab is marked by COLOUR AND AN UNDERLINE, not colour alone — colour is not
   available to every reader, and `aria-current="page"` carries it for the rest. */
.admin-tab.is-selected {
  color: var(--navy);
  border-bottom-color: var(--navy);
  font-weight: 600;
}

.admin-section-head { margin-top: var(--s-7); margin-bottom: var(--s-2); }
.admin-section-head + p,
.admin-section-head + .small { margin-top: 0; color: var(--text-soft); }
.admin-filter { max-width: 22rem; margin-bottom: var(--s-3); }
.admin-table-card { padding: 0; overflow-x: auto; }
/* ⚠️ `overflow-x` ONLY, never `overflow-y`, and never a `max-height`. Either one makes the
   other axis clip too, which is the PRIMER-474 defect the in-flow row menu below exists to
   avoid. A sticky table head would need exactly that, which is why there isn't one. */
.admin-table { margin: 0; min-width: 720px; }

/* ── the tables themselves ─────────────────────────────────────────────────────
   NOTE the selectors do not say `thead`/`tbody`: the Costs panel builds its table in
   JavaScript and appends `<tr>` straight to `<table>`, so its heading row ends up in an
   implicit `<tbody>` and a `thead th` rule would miss it entirely. Every `<th>` in this
   template is a heading cell, so matching on the element is both simpler and complete.
   `.demo-table` and `.table-wrap` had NO RULE ANYWHERE. Every table on this page was
   rendering at browser defaults — headings centred over left-aligned cells, no header fill,
   no row separators, no cell padding — inside a carefully styled card. That mismatch, not
   the palette, is what made the page look unfinished.

   Both class names are admin-only (`.demo-table` appears in no other template), so this
   reaches nothing else. The two tables that carry only `.admin-table` are covered as well. */
.admin-table,
.demo-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
/* LEFT, to agree with the cells underneath. Centred headings over left-aligned content was
   the browser's default, not a decision. */
.admin-table th,
.demo-table th {
  text-align: left;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-soft);
  white-space: nowrap;
}
.admin-table td,
.demo-table td {
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--hairline);
  vertical-align: middle;
}
/* A quiet row tint on hover: enough to follow a row across eight columns, not enough to read
   as selection — the checkbox is what means selected. */
.admin-table tr:hover > td,
.demo-table tr:hover > td { background: color-mix(in oklab, var(--surface-2) 40%, transparent); }
/* Figures line up under each other. The brand has no mono, so `.num` carries tabular figures
   and this extends the same treatment to every numeric cell. */
.admin-table td .num,
.demo-table td .num,
.admin-table td.num,
.demo-table td.num { font-variant-numeric: tabular-nums; }

/* The selection and the actions columns take only the width they need, which lets the
   content columns have the rest. */
.bulk-cell { width: 1%; }
.admin-table .row-actions-cell,
.demo-table .row-actions-cell { width: 1%; text-align: right; }

/* An empty table's explanation is prose, not a data row. */
.admin-table td[colspan],
.demo-table td[colspan] {
  padding: var(--s-6) var(--s-4);
  color: var(--text-soft);
  text-align: center;
}
.admin-table tr:hover > td[colspan],
.demo-table tr:hover > td[colspan] { background: none; }

/* `.table-wrap` had no rule either, so the two tables using it — the dataset activity log and
   the handle register — pushed the WHOLE DOCUMENT into horizontal scroll on a narrow screen,
   because their 720px minimum had nothing to scroll inside. */
.table-wrap { overflow-x: auto; }

/* A row's actions behind one disclosure, rather than several controls a mis-click apart —
   which the old template's own comment admitted was a risk. */
.row-actions-cell { white-space: nowrap; }
.row-menu { display: inline-block; position: relative; }
.row-menu > summary {
  cursor: pointer;
  padding: 2px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font-size: 12px;
  color: var(--text-soft);
  list-style: none;
}
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu > summary:hover { border-color: var(--navy); color: var(--navy); }
/* ⚠️ THIS SUMMARY IS THE KEYBOARD ENTRY POINT TO EVERY ROW ACTION and had no focus style at
   all — a keyboard reader tabbing through the table could not see where they were. Same idiom
   as `.console-turn > summary` above, so the two disclosures behave alike. */
.row-menu > summary:focus-visible,
.admin-tab:focus-visible,
.row-menu-items .link:focus-visible,
.bulk-bar button:focus-visible,
.bulk-bar .link:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
/* ⚠️ IN FLOW, NOT ABSOLUTE. It was absolute, and the card it sits in scrolls horizontally —
   `overflow-x: auto` makes the other axis `auto` as well, so the last row's menu was cut off
   by 79px while every click check still passed (a clipped element keeps its bounding box).
   Opening in flow grows the row instead, which cannot be clipped by the scroller and needs
   no z-index. `tests/_admin_driver.mjs` measures the overhang so this cannot come back. */
.row-menu-items {
  min-width: 11rem;
  margin-top: 4px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
}
.row-menu-items form { margin: 0; }
.row-menu-items .link { display: block; width: 100%; padding: 5px 8px; text-align: left; }
.row-menu-items .link:hover { background: var(--surface-2); }
/* The destructive entry reads as destructive, and sits last. */
.link-danger { color: var(--neg); }

/* ── scoping a grant: columns and row conditions (PRIMER-475a) ────────────────── */
/* Wider than the default 440px modal, and narrower than `modal-wide`'s 980: a condition is
   column + operator + value on one line, and at 440 the value box wrapped under the other
   two, which reads as a second condition rather than the rest of the first. */
dialog.modal-mid { max-width: min(600px, calc(100vw - 32px)); }
/* The share dialog could say who and what capability, but not WHICH ROWS — though the
   grant machinery has enforced row predicates since the data plane shipped. These are the
   controls that make a scoped grant authorable by a person rather than only by an API
   client. Structured inputs, never a SQL box. */
.scope-block {
  margin: 12px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.scope-block > legend {
  padding: 0 6px;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.scope-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}
.scope-column {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.scope-column input { width: auto; }
.scope-condition {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-bottom: 6px;
}
.scope-condition .cond-column { flex: 1 1 9rem; }
.scope-condition .cond-op { flex: 0 1 8rem; }
.scope-condition .cond-value { flex: 1 1 8rem; }

/* ── state badges (admin, slice B) ────────────────────────────────────────────── */
/* State was plain text — "Active", "Live", "Present" — in columns a reader has to parse
   word by word. A badge is scannable, and the research on operational dashboards is
   specific about it: make state visible, with counters for what is pending.

   ⚠️ COLOUR IS NEVER THE ONLY SIGNAL. Each badge carries its word as well as its colour, so
   nothing here depends on distinguishing green from amber. Built on the same pill as
   `.app-card-category` so the page keeps one vocabulary for small labels. */
.badge {
  display: inline-block;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  white-space: nowrap;
  color: var(--text-soft);
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.badge-ok   { color: var(--pos); background: color-mix(in oklab, var(--pos-soft) 45%, white); border-color: var(--pos-soft); }
.badge-warn { color: var(--warn); background: var(--warn-soft); border-color: color-mix(in oklab, var(--warn) 35%, transparent); }
.badge-bad  { color: var(--neg); background: var(--neg-soft); border-color: var(--neg-soft); }
/* Deliberately the quiet one: "deactivated" and "archived" are resting states, not alarms. */
.badge-off  { color: var(--text-faint); }

/* A time that says how long ago, with the exact moment on hover. */
.ago { white-space: nowrap; }

/* A sortable heading looks clickable and says which way it is sorted. The arrow is drawn
   from `aria-sort`, so the visual state and the announced state cannot disagree. */
th.is-sortable { cursor: pointer; user-select: none; }
th.is-sortable:hover { color: var(--navy); }
th.is-sortable[aria-sort="ascending"]::after { content: " ↑"; }
th.is-sortable[aria-sort="descending"]::after { content: " ↓"; }
th.is-sortable:focus-visible { outline: 2px solid var(--navy); outline-offset: -2px; }

/* The undo sits in the outcome it reverses, on the same line. */
.flash-undo { display: inline; margin-left: 10px; }
.flash-undo .link { font-weight: 600; }

/* ── bulk selection (admin, slice D) ──────────────────────────────────────────── */
/* The bar is hidden until something is selected — a destructive control on screen at rest is
   what the earlier slices removed, and bulk is the version of that with a bigger radius. */
.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0 0 10px;
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.bulk-count { font-size: 12.5px; font-weight: 600; color: var(--navy); }
.bulk-confirm {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.bulk-confirm input { width: 4.5rem; }
.bulk-cell { width: 34px; text-align: center; }
.bulk-cell input { width: auto; }

/* The activity filters sit on one line where there is room, and wrap where there is not. */
.activity-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin: 0 0 12px;
}
.activity-filters label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.activity-filters input { width: 11rem; }
/* The submit sat a few pixels shorter than the fields beside it, which reads as misalignment
   rather than as a smaller control. Scoped to this bar — `.btn` itself is shared with the
   catalogue, the console and the nav. */
.activity-filters .btn { padding: 8px 16px; font-size: 13px; }

/* ── admin inputs look like the rest of the product ───────────────────────────── */
/* The Add-someone fields are styled (`.field input`, above); the Activity filters, the
   people/application filter and the bulk confirmation were raw browser defaults sitting a few
   centimetres away from them. One page, two input languages. These adopt the existing idiom
   rather than inventing a third. */
.activity-filters input,
.admin-filter,
.bulk-confirm input {
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: 13.5px/1.5 var(--sans);
  text-transform: none;
  letter-spacing: 0;
  background: var(--surface);
  color: var(--text);
}
.activity-filters input:focus,
.admin-filter:focus,
.bulk-confirm input:focus {
  outline: none;
  border-color: var(--navy);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--steel) 22%, transparent);
}

/* ── the admin page on a narrow screen ────────────────────────────────────────── */
/* There was NO responsive rule for this page. Two consequences a phone showed immediately:
   the six sticky tabs wrapped onto three rows and pinned all of them under the nav, eating
   most of the viewport; and `.table-wrap`'s 720px-minimum tables pushed the document itself
   into horizontal scroll. */
@media (max-width: 900px) {
  /* ⚠️ THE TOP NAVIGATION, not the tables, is what pushed this page sideways. Measured on a
     390px screen: `.nav-links` is 714px wide and overflows the document by 324px. I had
     assumed the 720px-minimum tables were the cause and was wrong — the diagnostic in
     `_admin_driver.mjs` named the real element in one run.
     
     It is in `base.html`, so it affects EVERY page at this width, not only this one — and it
     only bites for an administrator, who has two more links in the bar than a builder does.
     Scoped to the narrow query so nothing changes at desktop width; the console's own
     narrow-width checks prove it did not break that page. */
  .nav-links {
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .nav-links::-webkit-scrollbar { display: none; }
  /* The account's own email is the first thing to go: it is the one item that is already on
     screen in the page itself, and it is by far the widest. */
  .nav-user { display: none; }

  /* One row that scrolls sideways, rather than three rows that stay on screen. */
  .admin-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    margin-top: var(--s-4);
  }
  .admin-tabs::-webkit-scrollbar { display: none; }
  .admin-tab { white-space: nowrap; }

  .admin-section-head { margin-top: var(--s-6); }
  .admin-filter { max-width: none; width: 100%; }

  /* Each filter takes the full width rather than squeezing four onto a 390px line. */
  .activity-filters { flex-direction: column; align-items: stretch; }
  .activity-filters label { width: 100%; }
  .activity-filters input { width: 100%; }

  /* The tables keep their minimum and scroll inside their own card, which is what stops the
     document scrolling. `overflow-x` only — see the note on `.admin-table-card`. */
  .table-wrap, .admin-table-card { overflow-x: auto; }
  /* AND the ones in a PLAIN card — live shares, feedback, the two pending lists — which have
     no scrolling wrapper at all because their card also holds prose and a form. Measured: the
     Data tab pushed the document 324px sideways on a 390px screen without this. Scoped to the
     narrow query, since nothing overflows at desktop width and `display: block` on a table is
     a bigger hammer than this needs the rest of the time. */
  .card:not(.admin-table-card):not(.table-wrap) .admin-table,
  .card:not(.admin-table-card):not(.table-wrap) .demo-table {
    display: block;
    overflow-x: auto;
  }
  .admin-table th,
  .demo-table th,
  .admin-table td,
  .demo-table td { padding: var(--s-2) var(--s-3); }

  /* An empty table's explanation was scrolling off the right edge with the columns it spans —
     and on a narrow screen that sentence is the single most useful thing on the card, because
     there is no data to look at instead.
     
     An EMPTY table does not need the 720px minimum: there are no columns to keep apart. In
     this template `colspan` is used for exactly one thing, the empty-state row (six of them,
     and nothing else), so `:has()` identifies that case precisely rather than guessing at a
     max-width — which I tried first, and which still clipped the two cards that carry more
     padding than the others. */
  .admin-table:has(td[colspan]),
  .demo-table:has(td[colspan]) { min-width: 0; }
  /* And its headings go: eight uppercase labels still force a width wider than a phone, and
     on an empty table they label nothing — "DATASET HELD BY TABLE ROWS STATE…" above "No
     copies exist" is noise in the one place where the sentence beneath it is the content.
     Narrow screens only; at desktop width the headings cost nothing and keep the shape of
     the table visible. */
  .admin-table:has(td[colspan]) thead,
  .demo-table:has(td[colspan]) thead { display: none; }
  /* ...and with no heading above it, the row separator has nothing to separate. */
  .admin-table:has(td[colspan]) td,
  .demo-table:has(td[colspan]) td { border-top: 0; }
  .admin-table td[colspan],
  .demo-table td[colspan] {
    white-space: normal;
    text-align: left;
    padding: var(--s-5) var(--s-3);
  }
}

/* Choosing where a shared dataset's data comes from (PRIMER-475b). */
.source-choice {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 10px 0 4px;
}
.source-choice label {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.source-choice input { width: auto; }

/* The live-sharing policy: a state, the action that changes it, and its consequence. */
.live-toggle {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

/* ── attachments on a message (PRIMER-446) ─────────────────────────────────────
   The list sits under the composer rather than inside it: a row that grows inside the
   input pushes the textarea around as files are added, and the thing that moves while you
   type is the thing you mis-click. */
.attach-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 2px 0;
}
.attach-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-size: 12px;
  color: var(--text);
}
.attach-chip .size { color: var(--muted); }
.attach-chip button {
  all: unset;
  cursor: pointer;
  color: var(--muted);
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}
.attach-chip button:hover { color: var(--text); }
.attach-chip button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The image caveat. Muted, but present BEFORE the send rather than after a disappointing
   result — the agent can read an image's bytes and cannot see it (PRIMER-446 spec §2). */
.attach-note { flex-basis: 100%; color: var(--muted); font-size: 11.5px; margin-top: 2px; }

/* The download affordance on each file row (PRIMER-446). Muted until the row is hovered or
   the link is focused: every file has one, and forty bright arrows is a list you stop
   reading. Focus-visible keeps it reachable by keyboard, where hover never happens. */
.side-file-save {
  flex: 0 0 auto;
  margin-left: 6px;
  padding: 0 4px;
  color: transparent;
  text-decoration: none;
  font-size: 13px;
  line-height: 1;
}
li:hover > .side-file-save,
.side-file-save:focus-visible { color: var(--muted); }
.side-file-save:hover { color: var(--text); }

/* A published knowledge base, shown as text. `pre` so the markdown headings and lists keep
   the shape they were written in, wrapped so a long line does not scroll the card sideways
   (PRIMER-511). Never rendered as HTML — see the comment in admin.html. */
.kb-read {
  margin: 0;
  padding: 16px;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 60vh;
  overflow-y: auto;
}

/* The guidance picker: a choice, and a disclosure to read it before choosing. The
   disclosure is a SIBLING of the label, never a child — see the comment on `kb_choice` in
   catalogue.html (PRIMER-511). */
/* ⚠️ `flex-wrap: wrap` IS LOAD-BEARING, not tidiness. The open disclosure below asks for
   `flex: 1 0 100%` so it takes its own row — and a 100% basis cannot move to a new line in
   a nowrap row. Without this it stayed beside the checkbox, crushed the label to 97px with
   the base's name wrapping over three lines, and spilled 63px past the right edge of the
   dialog. Measured; `tests/_catalogue_driver.mjs` asserts all three. */
.kb-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
/* `min-width: 0` on both, because a flex item defaults to `min-width: auto` and so refuses
   to shrink below its content — which is what let a long unbroken line in the guidance
   push the panel out of the dialog rather than wrap inside it. */
.kb-item .kb-choice { flex: 1 1 auto; min-width: 0; }
.kb-peek { flex: none; min-width: 0; }
.kb-peek > summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--navy-700);
  list-style: none;
  padding: 2px 6px;
  border-radius: var(--r-sm);
}
.kb-peek > summary:hover { background: color-mix(in oklab, var(--navy) 8%, transparent); }
.kb-peek > summary::-webkit-details-marker { display: none; }
/* Open, the body spans the whole fieldset rather than the narrow actions column. */
.kb-peek[open] { flex: 1 0 100%; margin-top: 6px; min-width: 0; }
.kb-peek[open] > .kb-read {
  border: 1px solid var(--line-soft, rgba(0, 0, 0, 0.1));
  border-radius: var(--r-sm);
  max-height: 40vh;
}

/* ── the row menu's separator, and the person editor ────────────────────────────────
   The rule above the destructive action: `link-danger` carried the whole distinction
   before, and colour alone fails a colour-blind reader and a screenshot (spec D5). */
.row-menu-rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 4px 0;
}
/* The roles and the budget, in a dialog rather than a 156px dropdown — which is where the
   currency field used to overflow its own label line by 3px and wrap. */
.edit-person-roles { display: flex; flex-direction: column; gap: 2px; }
#editPerson { min-width: 22rem; }
#editPerson .admin-section-head { margin-bottom: 6px; }

/* ── the marketplace shelf (PRIMER-507) ─────────────────────────────────────── */
/* The publish card borrows the Knowledge tab's authoring shape: `.field` blocks, full width.
   The two-up rows are a grid rather than the flex `.new-app-row`, so a select and an input
   sit on one baseline at the same height — the first version put a bare <select> beside a
   padded input and the pair read as two different forms. */
.mp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.mp-grid-wide { grid-template-columns: 2fr 1fr; }
@media (max-width: 720px) { .mp-grid, .mp-grid-wide { grid-template-columns: 1fr; } }
.field select {
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font: 14px/1.5 var(--sans);
  text-transform: none;
  letter-spacing: 0;
  background: var(--surface);
  color: var(--text);
  appearance: none;
  -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-soft) 50%),
                    linear-gradient(135deg, var(--text-soft) 50%, transparent 50%);
  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 36px;
}
.field select:focus {
  outline: none;
  border-color: var(--steel);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--steel) 30%, transparent);
}
.field .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }
/* The seed-table picker: a bordered box like an input, holding a wrapping row of checkboxes.
   `min-width: 0` on the labels for the same reason `.kb-item` needed it — a long table name
   must shrink rather than push its row sideways. */
.mp-tables {
  border: 1px solid var(--border-strong); border-radius: var(--r-sm); padding: 10px 12px; margin: 0;
  display: flex; flex-wrap: wrap; gap: 8px 18px; background: var(--surface);
  font: 14px/1.5 var(--sans); text-transform: none; letter-spacing: 0; color: var(--text);
}
.mp-tables[disabled] { background: var(--surface-2); color: var(--text-soft); }
.mp-tables > p { flex: 1 0 100%; margin: 0; }
.mp-tables label { display: inline-flex; align-items: center; gap: 6px; min-width: 0; font-family: var(--mono); font-size: 13px; }
.mp-tables input { margin: 0; width: auto; }
.mp-needs-note { margin: 6px 0 0; text-transform: none; letter-spacing: 0; }
/* A description is a paragraph or two, not a knowledge base: shorter than .kb-body's floor. */
.mp-desc { min-height: 9rem; }
.mp-close { margin-left: 12px; }
/* The attestation and the button share a row: the tick gates the act, so they belong together. */
.mp-publish-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-top: 6px; }
.mp-attest { display: inline-flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--text); max-width: 60ch; }
.mp-attest input { margin: 3px 0 0; width: auto; flex: none; }
/* The reader: description as text, then the facts a builder needs before installing. */
.mp-read .kb-read { margin: 0 0 14px; }
.mp-needs { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; margin: 0; font-size: 13px; }
.mp-needs dt { color: var(--text-soft); }
.mp-needs dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* Starting from the shelf (PRIMER-507, slice 2). A radio shares the knowledge picker's row
   layout; the summary sits under the name on its own line so a long one wraps there rather
   than pushing the Read control off the row. */
.start-from .kb-choice input[type=radio] { margin: 0; flex: none; }
.kb-summary { display: block; margin-top: 2px; }
.app-card-from { display: inline-block; margin-left: 8px; font-size: 0.85em; color: var(--muted, #666); }

/* ── the Marketplace page and Account roles (PRIMER-514) ────────────────────── */
/* Cards on the same grid as the application catalogue, so the two pages read as one
   product rather than two layouts. */
.mp-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px;
  margin: 20px 0 28px;
}
.mp-card { display: flex; flex-direction: column; gap: 10px; margin: 0; }
.mp-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.mp-card-name { font-weight: 600; font-size: 15px; color: var(--navy); }
.mp-card-summary { margin: 0; color: var(--text-soft); font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
.mp-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: auto; }
.mp-entry-summary { margin: 0 0 16px; font-size: 15px; color: var(--text-soft); max-width: 70ch; }
/* The name field and its button on one baseline: the button's field has an empty label so
   both columns start at the same line (the same fix the publish card needed). */
.mp-start-action { justify-content: flex-end; }
.mp-start-action > span { visibility: hidden; }
.account-roles { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; margin: 0; font-size: 14px; }
.account-roles dt { font-weight: 600; color: var(--navy); }
.account-roles dd { margin: 0; color: var(--text-soft); min-width: 0; }
/* The composer's "this will be sent as n parts" line (PRIMER-515): full width under the
   textarea, inside the form's flex row. */
.console-parts-note { flex: 1 0 100%; margin: 6px 0 0; }

/* ── The egress matrix: applications × destinations ───────────────────────────
   ⚠️ THE MARKS DIFFER IN LIGHTNESS AND SHAPE, NOT HUE ALONE. A reader who cannot
   separate the two blues must still find a standing outbound channel, so `serve`
   is the darkest fill AND the only one with a ring, and `all` is the only square.

   ⚠️ NOT `--accent`. That token is reserved for the one control per screen a
   person acts with (`.btn-accent`), and a grid of twelve dots is not that. The
   ring uses `--warn`, which is the "look at this" colour everywhere else.

   50% is a permitted radius here — tests/test_brand_rules.py §4 allows it for a
   dot — and the square mark keeps to the 4px token like everything else. */
.egress-mark {
  display: inline-block;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  vertical-align: -1px;
}
.egress-mark + .egress-mark { margin-left: 5px; }
.egress-mark-install { border: 1.5px solid var(--steel); background: transparent; }
.egress-mark-build   { background: var(--steel); }
/* An OUTLINE rather than a shadow. §5 of tests/test_brand_rules.py permits only
   tokenised shadows and focus rings, and this ring is neither; an outline draws it
   honestly. (That rule reads the file as text, comments included, so the property
   name is deliberately not written out here — spelling it cost one red run.) */
.egress-mark-serve   { background: var(--navy); outline: 2.5px solid var(--warn); outline-offset: 1px; margin: 0 3px; }
.egress-mark-all     { background: var(--text-soft); border-radius: 4px; }

.egress-matrix td.egress-cell { text-align: center; white-space: nowrap; }
/* The destination headings are long and the grid must not force a horizontal
   scroll on an already-dense page; `.admin-table-card` keeps its own overflow. */
.egress-matrix th.egress-col { word-break: break-word; max-width: 140px; }

/* ── A turn as a stacked bar, and spend against its ceiling ───────────────────
   The three turn segments SUM TO THE TOTAL by construction (cold start, tools
   with the boot taken out, model), which is what let the paragraph explaining
   that relationship be deleted from the page rather than restated under a chart.

   Colours differ in lightness as well as hue: --warn is the lightest, --navy the
   darkest, so the three are separable without colour vision. Not --accent, which
   style.css reserves for the one control per screen a person acts with. */
.turn-row { display: flex; align-items: center; gap: 12px; padding: 5px 0; }
.turn-row + .turn-row { border-top: 1px solid var(--hairline); }
.turn-row-app { flex: 0 0 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.turn-row-total { flex: 0 0 56px; text-align: right; }
.turn-bar {
  flex: 1 1 auto;
  display: flex;
  height: 18px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--paper);
}
.turn-seg { display: block; height: 100%; }
.turn-seg-key { display: inline-block; width: 12px; height: 12px; border-radius: 4px; vertical-align: -1px; }
.turn-seg-cold  { background: var(--warn); }
.turn-seg-tools { background: var(--steel); }
.turn-seg-model { background: var(--navy); }

.spend-track {
  display: block;
  height: 10px;
  border-radius: 4px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  overflow: hidden;
  margin-bottom: 4px;
}
.spend-fill { display: block; height: 100%; background: var(--steel); }
/* Over the ceiling is said in WORDS beside the bar as well — a reader who cannot
   separate the two fills still gets the fact. */
.spend-fill-over { background: var(--warn); }
.spend-over-word { color: var(--warn); }

/* ── People against capabilities ──────────────────────────────────────────────
   A single-held capability is marked in the HEADER as words ("one holder") and
   tinted down the column, so the fact survives both a reader who skims headings
   and one who cannot separate the tints. --warn, not --accent: that token is
   reserved for the one control per screen a person acts with. */
.cap-matrix th.cap-col { text-align: center; vertical-align: bottom; }
.cap-matrix td.cap-cell { text-align: center; }
.cap-col-sole, .cap-cell-sole { background: var(--warn-soft); }
.cap-flag {
  display: block;
  margin-top: 2px;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--warn);
}
.cap-flag-none { color: var(--text-faint); }
.cap-tick { display: inline-block; color: var(--navy); vertical-align: -3px; }
.cap-cell-sole .cap-tick { color: var(--warn); }

/* ── Cost bars, and the weekly spend shape ────────────────────────────────────
   ⚠️ AI AND COMPUTE SIT SIDE BY SIDE AND ARE NEVER STACKED. Stacking means
   addition, and this panel exists to refuse that sum: AI spend is measured,
   compute is an estimate against a list price no invoice matches. Different
   hues AND different lightness, so the two are separable without colour. */
.cost-track {
  display: block;
  height: 8px;
  border-radius: 4px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  overflow: hidden;
  margin-bottom: 3px;
  min-width: 70px;
}
.cost-fill { display: block; height: 100%; }
.cost-fill-ai { background: var(--navy); }
.cost-fill-compute { background: var(--steel); }
.cost-fill-storage { background: var(--pos); }

.week-chart { display: flex; align-items: flex-end; gap: 10px; height: 150px; }
.week-col { flex: 1 1 0; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
.week-bar {
  display: block;
  width: 100%;
  background: var(--navy);
  border-radius: 4px;
  min-height: 2px;
}
/* The current week is incomplete by definition; hatching says so without a legend, and the
   label says it too for anyone who cannot see the hatch. */
.week-bar-partial {
  background: repeating-linear-gradient(
    45deg, var(--steel), var(--steel) 4px, var(--paper) 4px, var(--paper) 8px
  );
}
.week-amt { display: block; text-align: center; margin-top: 4px; }
.week-lbl { display: block; text-align: center; }

/* ── Connections against applications ─────────────────────────────────────────
   ⚠️ READABLE IS THE ONE TO SEE. A mediated grant keeps the credential on the
   platform; a readable grant hands the raw value to an application whose builder
   can print it. So readable is the darkest fill AND the only ringed mark, the
   same distinction the egress matrix draws for `serve`, and the count is stated
   in words beside the legend. Not --accent: reserved for the acting control. */
.conn-mark {
  display: inline-block;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  vertical-align: -1px;
}
.conn-mark-mediated { background: var(--steel); }
.conn-mark-owner    { border: 1.5px solid var(--steel); background: transparent; }
.conn-mark-readable {
  background: var(--navy);
  outline: 2.5px solid var(--warn);
  outline-offset: 1px;
  margin: 0 3px;
}
.conn-matrix td.conn-cell { text-align: center; }
.conn-matrix th.conn-col { word-break: break-word; max-width: 140px; }

/* ── How long before a deleted application is destroyed for good ──────────────
   The queue printed a date, so "is this one nearly gone" was a subtraction the
   reader did per row. The bar DRAINS as the window closes. The last fifth and
   the overdue state are said in words beside it as well — purging cannot be
   undone, so this must not depend on seeing a colour. */
.purge-track {
  display: block;
  height: 8px;
  border-radius: 4px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  overflow: hidden;
  margin-bottom: 3px;
}
.purge-fill { display: block; height: 100%; background: var(--steel); }
.purge-fill-soon { background: var(--warn); }
.purge-fill-due { background: var(--warn); width: 100%; }
.purge-word { color: var(--warn); font-weight: 600; }

/* ── Who publishes, who consumes ──────────────────────────────────────────────
   One row per edge, publisher on the left and consumer on the right, so the
   direction is the layout rather than a column heading. A write flows back
   toward the publisher and is a different relationship from a read: thicker,
   darker, AND labelled in words, because direction is the one thing a reader
   must not have to infer from a tint. */
.flow-list { display: flex; flex-direction: column; gap: 10px; }
.flow-row { display: flex; align-items: center; gap: 12px; }
.flow-row + .flow-row { border-top: 1px solid var(--hairline); padding-top: 10px; }
.flow-node {
  flex: 0 0 190px;
  padding: 5px 10px;
  border-radius: 4px;
  background: var(--paper);
  border: 1px solid var(--hairline);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flow-row .flow-node:last-child { text-align: right; }
.flow-mid { flex: 1 1 auto; display: flex; align-items: center; gap: 7px; min-width: 0; }
.flow-line { flex: 0 0 28px; height: 0; border-top: 2px solid var(--steel); }
.flow-line-writes { border-top: 3px solid var(--navy); }
.flow-arrow { color: var(--steel); flex: 0 0 auto; }
.flow-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.flow-writes-word { color: var(--navy); margin-left: 6px; }

/* ── Direction in the console's data list ─────────────────────────────────────
   Owned and granted datasets were two consecutive groups with nothing saying
   which way the data moves. The arrow points out of the application for what it
   shares and into it for what it reads — and the heading says the same in
   words, so the direction never rests on the glyph alone. */
.ds-arrow { flex: 0 0 auto; vertical-align: -1px; }
.ds-arrow-out { color: var(--navy); }
.ds-arrow-in { color: var(--steel); }
