/* ============================================================================
   Primer Platform — "Quiet Authority", the design system from Primer V1.

   PORTED, not reinvented. The token block below is lifted VERBATIM from
   primer-local's `tenant_plane/static/style.css` (Mavens Cloud Brand Guidelines
   v1, PRIMER-185), so the palette is identical rather than eyeballed — the same
   Deep Blue #253551, the same Off-White paper, the same gold accent.

   What is NOT ported is the ~1300 lines of component CSS for surfaces this
   platform does not have: SDK tables, KPI tiles, chart adapters, the admin
   shell, layout editors. Copying those would be dead weight that reads as
   supported. The components below are the ones these four pages actually use,
   written in the same language: navy nav, serif headings in Deep Blue, white
   cards on off-white paper, a 3px navy edge that appears on hover.

   The one genuinely new surface is the builder console (`.console-*`), which V1
   has no equivalent of. It is built from the same tokens.

   Vanilla CSS, no framework, no build step — same as V1.
   ========================================================================== */

/* ── Design tokens — Mavens Cloud brand (PRIMER-185), verbatim from V1 ─────── */
:root {
  /* Brand navy + ink */
  --navy:       #253551;   /* Deep Blue — actions, active states, headings, brand */
  --navy-700:   #1d2a40;   /* darker navy — hover */
  --navy-900:   #060644;   /* deepest navy (brand secondary) */
  --navy-deep:  #060644;   /* structural nav / hero surface */
  --ink:        #363D46;   /* Dark Grey — body text */

  /* Surfaces */
  --paper:      #F6F6F6;   /* Off-White — app background */
  --surface:    #FFFFFF;   /* cards, bars */
  --surface-2:  #E9ECE6;   /* soft fills, table heads (mist) */
  --surface-3:  #E3E6E0;

  /* Lines */
  --border:        #D5D8DC;
  --border-strong: #C0C0C0;  /* Soft Silver */
  --hairline:      #E6E8E3;

  /* Text */
  --text:       var(--ink);
  --text-soft:  #5B6470;
  --text-faint: #8A94A0;
  --text-onnavy: #F6F6F6;

  /* Gold — restrained premium accent only (callouts, active markers) */
  --accent:      #B89F51;
  --accent-soft: #EEE8D6;
  --accent-ink:  #6E5E2E;

  /* Financial deltas (positive = brand green; negative = signed-off #A4453A) */
  --pos:      #3F7A43;
  --pos-soft: #A2D3A6;
  --neg:      #A4453A;
  --neg-soft: #F0DAD6;

  /* Chart palette */
  --chart-1: var(--navy);
  --chart-fill: #DCE2E8;
  --chart-axis: #C0C0C0;

  /* Shape + depth */
  --r-sm: 6px;
  --r:    8px;
  --r-lg: 11px;
  --r-pill: 999px;
  --shadow-sm: 0 1px 2px rgba(37, 53, 81, 0.06);
  --shadow:    0 1px 2px rgba(37, 53, 81, 0.06), 0 6px 22px rgba(37, 53, 81, 0.06);
  --shadow-lg: 0 18px 50px rgba(37, 53, 81, 0.16);

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

  /* A spacing scale. Spacing in this file was literals per rule — `22px 24px` here, `16px 0`
     there — which is why the vertical rhythm reads as uneven even though the palette does
     not. ADDED, not substituted: existing rules keep their literals, and new work uses these.
     Nothing above this comment changes value, which is what the palette checks in
     `tests/test_web.py` assert. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 28px;
  --s-7: 40px;

  /* Type — FF Meta Serif Pro headings (Source Serif 4 fallback until licensed)
     + Open Sans body/subheads. No mono in the brand; --mono → Open Sans and
     numerics get tabular figures via the .num rule. */
  --serif: "Source Serif 4", Georgia, "Times New Roman", serif;
  --sans:  "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --mono:  "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
}

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

/* Radius tweak (data-radius="sharp") */
:root[data-radius="sharp"] {
  --r-sm: 0px;
  --r: 0px;
  --r-lg: 2px;
}

/* Accent variants */
:root[data-accent="slate"] {
  --accent:      oklch(0.6 0.06 240);
  --accent-soft: oklch(0.95 0.022 240);
  --accent-ink:  oklch(0.42 0.06 245);
}
:root[data-accent="none"] {
  --accent:      var(--navy);
  --accent-soft: oklch(0.95 0.02 256);
  --accent-ink:  var(--navy-700);
}

/* Dark mode */
:root[data-theme="dark"] {
  --ink:        oklch(0.95 0.008 250);
  --navy:       oklch(0.72 0.075 250);
  --navy-700:   oklch(0.66 0.07 250);
  --navy-900:   oklch(0.8 0.06 250);

  --paper:      oklch(0.19 0.018 257);
  --surface:    oklch(0.225 0.02 257);
  --surface-2:  oklch(0.255 0.021 257);
  --surface-3:  oklch(0.29 0.022 257);

  --border:        oklch(0.32 0.018 257);
  --border-strong: oklch(0.4 0.02 257);
  --hairline:      oklch(0.3 0.016 257);

  --text:       var(--ink);
  --text-soft:  oklch(0.7 0.018 256);
  --text-faint: oklch(0.58 0.016 256);
  --text-onnavy: oklch(0.16 0.02 257);

  --accent:      oklch(0.78 0.1 73);
  --accent-soft: oklch(0.32 0.05 70);
  --accent-ink:  oklch(0.85 0.09 75);

  --pos:      oklch(0.74 0.12 158);
  --pos-soft: oklch(0.32 0.06 158);
  --neg:      oklch(0.7 0.15 28);
  --neg-soft: oklch(0.33 0.08 27);

  --chart-1: var(--navy);
  --chart-fill: oklch(0.32 0.04 255);
  --chart-axis: oklch(0.4 0.02 256);

  --shadow-sm: 0 1px 2px oklch(0 0 0 / 0.3);
  --shadow:    0 1px 2px oklch(0 0 0 / 0.3), 0 6px 22px oklch(0 0 0 / 0.35);
  --shadow-lg: 0 18px 50px oklch(0 0 0 / 0.5);
}
* { 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.25s var(--ease), color 0.25s var(--ease);
}

/* Brand type: serif headings in Deep Blue. */
h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 700;
  color: var(--navy);
  line-height: 1.2;
}
h1 { font-size: 32px; margin: 0 0 8px; line-height: 1.12; text-wrap: balance; }
h2 { font-size: 18px; margin: 0 0 4px; }
h3 { font-size: 14px; margin: 20px 0 8px; }

a { color: var(--navy); 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(--accent-soft); color: var(--accent-ink); }

/* `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; }

/* Subhead / eyebrow — Open Sans 600 caps. */
.eyebrow {
  font-family: var(--sans);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  display: block;
  margin-bottom: 12px;
}

/* ── Top navigation ──────────────────────────────────────────────────────── */
.nav {
  border-bottom: 1px solid var(--navy-900);
  background: var(--navy-deep);
  color: var(--text-onnavy);
  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;
}

.nav-brand {
  font-weight: 600;
  font-size: 17px;
  letter-spacing: 0.01em;
  color: #fff;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}
.nav-brand:hover { text-decoration: none; }

/* The Merestone mark (a boundary stone with the mere-line through it), masked so
   it takes the on-navy colour rather than shipping a second asset per surface. */
.nav-mark {
  width: 26px;
  height: 26px;
  background-color: var(--text-onnavy);
  -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: rgba(246, 246, 246, 0.72);
  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: #fff; background: rgba(255, 255, 255, 0.08); text-decoration: none; }
.nav-link.active { color: #fff; }
.nav-link.active::after {
  content: "";
  position: absolute;
  left: 12px; right: 12px; bottom: -1px;
  height: 2px;
  background: var(--accent);
}
.nav-user { color: rgba(246, 246, 246, 0.72); cursor: default; }
.nav-admin-pill {
  font-family: var(--mono);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-radius: var(--r-pill);
  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 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; }

.new-app-card {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 24px 26px;
  margin: 20px 0 8px;
  background: linear-gradient(180deg, var(--surface-2), 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(--navy);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--navy) 18%, 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; }

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: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-faint);
}

/* ── 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);
}
.login-card h1 { margin: 0; font-size: 23px; }
.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: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-faint);
}
.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; }

/* ── 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-info { background: color-mix(in oklab, var(--navy) 8%, var(--surface)); color: var(--navy-700); border-color: color-mix(in oklab, var(--navy) 25%, transparent); }

.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 Open Sans 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: #b45309; }
/* 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; }
.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.4s ease-in-out infinite;
}
.status-dot.is-stalled { background: #f59e0b; }
.status-dot.is-failed { background: #dc2626; }
@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.4s var(--ease) infinite;
}
@keyframes mic-live {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--neg) 55%, transparent); }
  70% { box-shadow: 0 0 0 6px transparent; }
}
@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 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(--navy);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--navy) 18%, 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 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; }
.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 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; }

/* 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; }
#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);
}

/* ── 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(--navy) 12%, 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: #8a5a00; background: #fdf3dd; border-color: #f0dcae; }
.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(--navy) 12%, 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: 999px;
  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; }
.edit-person-budget { display: flex; align-items: center; gap: 6px; font-size: 13.5px; }
.edit-person-budget input { width: 8em; }
#editPerson { min-width: 22rem; }
#editPerson .admin-section-head { margin-bottom: 6px; }
