/**
 * The console's stylesheet, and the design bundle's.
 *
 * These rules used to live in an inline <style> block inside each preview,
 * duplicated per file. Two things forced the change. The console's CSP is
 * `default-src 'none'; style-src 'self'` with no `unsafe-inline`, so an inline
 * block does not render at all. And a component whose CSS exists in two places
 * drifts — this repository has shipped that bug three times (a conflict target
 * that disagreed with its constraint, a guard that read a path that never
 * existed, design copy advertising a retry curve the code had stopped using).
 *
 * Preview-only chrome (.rule, h2, .note, swatch grids) stays inline in each
 * preview. The console never renders it, so it cannot drift into the console.
 *
 * Where two previews defined a moved class differently, the screens' version
 * won (the console mirrors screens/, not components/), and the loser now
 * carries a scoped `body .selector` override in its own remaining <style>
 * block if it needed to keep a legitimately different layout. Every such
 * conflict is inventoried in the Task 1 report, not just in code comments.
 */

/* ==== Shell: rail, nav, who, banner, page frame ====
 * Source: this task's own rotation from a horizontal masthead to a
 * collapsible left rail — see .superpowers/sdd/left-rail-report.md for the
 * full rationale. Nothing about the palette, radius, spacing scale or
 * status-glyph vocabulary changed; only the shell's STRUCTURE did. Values
 * (padding, letter-spacing) carry over unchanged from the masthead this
 * replaces, which itself settled screens/operations.html's edge-to-edge
 * numbers over components/app-shell.html's slightly different ones — see
 * this section's git history for that earlier tie-break if it matters again.
 *
 * The bare-chevron toggle, the eight nav icons, and the collapsed rail
 * showing icons instead of nothing are the owner's follow-up pass after
 * seeing the rail above live — see .superpowers/sdd/rail-icons-report.md for
 * that pass's own rationale (why line drawings and not filled shapes, what
 * carries the current-page distinction with labels clipped, what the brand
 * mark and who/sign-out block become collapsed).
 */
.shell {
  display: flex;
  border: 1px solid var(--rule);
  background: var(--surface-0);
}

.rail {
  display: flex; flex-direction: column; flex: none;
  width: 13rem; gap: var(--space-5);
  padding: var(--space-4) var(--space-3);
  background: var(--surface-1);
  border-right: 1px solid var(--rule);
}
.rail__top { display: flex; flex-direction: column; gap: var(--space-1); }
.rail__mark { margin: 0; font-family: var(--serif); font-size: var(--t-lead); }
.rail__product {
  margin: 0; font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--graphite);
}
/* Pushes .rail__bottom to the rail's own far edge, the same spacer trick the
   old .masthead__gap used in a horizontal bar — just turned to the column
   axis this file's flex-direction now runs on. */
.rail__gap { flex: 1; }
.rail__bottom { display: flex; flex-direction: column; gap: var(--space-3); }

/* Collapsed: src/console/layout.ts still renders the nav — icons, with
   their labels clipped, not removed (see .nav__label/.nav__tag below) — and
   a one-letter brand mark, but drops the product name and the who/sign-out
   block outright (layout.ts's own renderRailHtml() comment has the
   reasoning for each). Width is deliberately NOT fixed: it sizes to
   whichever visible row is widest — the icon column, once labels are
   clipped out of flow — rather than a guessed rem value, the same
   shrink-to-fit the toggle button's own width already relied on before this
   task. No transition on this rule or on .rail above: collapsing is a real
   navigation (POST, then a fresh page load), never a client-side state
   change, so animating the width would describe a motion that never
   actually happened. */
.rail--collapsed {
  width: auto; align-items: center;
  padding: var(--space-4) var(--space-2);
}

.rail__toggle-form { display: flex; justify-content: center; }
/* The owner's second look at this rail: the toggle recedes instead of
   announcing itself. Bare — no border, no fill at rest — because a full
   `.btn` rectangle is exactly the "announce itself" look this control is no
   longer meant to have; hover and focus are colour and the shared focus
   ring, the same two treatments `.nav a` above already uses for the same
   reason. The glyph itself (`.rail__toggle-glyph`, built in
   src/console/layout.ts's buildRailToggleFormHtml()) is aria-hidden, so
   nothing here needs its own text-related properties — the button carries
   no visible words any more. */
.rail__toggle {
  padding: var(--space-2);
  border: 0; border-radius: 0; background: transparent;
  color: var(--graphite); cursor: pointer; line-height: 1;
  transition: color var(--transition);
}
.rail__toggle:hover { color: var(--ink); }
.rail__toggle:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
}
.rail__toggle-glyph { display: block; }

.nav { display: flex; flex-direction: column; gap: var(--space-1); }
.nav a {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2);
  border-left: 2px solid transparent;
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--graphite); text-decoration: none;
  transition: color var(--transition), border-color var(--transition);
}
/* Same vocabulary as the old horizontal nav (a 2px line in --mark marks
   "here", --rule marks hover), turned ninety degrees for the rail's own
   axis: border-left instead of border-bottom. Colour is inherited by each
   row's icon too (`stroke="currentColor"` on every `.nav__icon`), so
   current-vs-not reads the same way with the rail collapsed as it does
   expanded, with no extra rule needed — see .nav__icon below. */
.nav a[aria-current="page"] { color: var(--ink); border-left-color: var(--mark); }
.nav a:hover { color: var(--ink); border-left-color: var(--rule); }
/* The shell composes interactive elements, so it carries the focus rule too.
   Without it the nav falls back to the browser's default ring. */
.nav a:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
}

/* A destination on the map that has no page behind it yet — src/console/layout.ts
   renders it as this <span>, never as an <a>. No :hover/:focus-visible rule is
   defined for it on purpose: it is not interactive, so it must not react like
   something that is. Colour stays --graphite, the same as an unvisited .nav a,
   because a roadmap entry is meant to be read, not dimmed into illegibility —
   .nav__tag (not .pg:disabled's washed-out --rule) is what marks it as
   unbuilt, borrowing .chip--default's "not a choice you can make" dashed
   border instead of the disabled-control look, for the reason layout.ts's own
   comment on renderNavHtml() gives: this was never a control to begin with. */
.nav__todo {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2);
  border-left: 2px solid transparent;
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--graphite);
}
.nav__tag {
  padding: 0 var(--space-1);
  border: 1px dashed var(--rule); border-radius: 0;
  font-size: var(--t-micro); letter-spacing: 0.08em; text-transform: none;
  color: var(--graphite);
}

/* One line drawing per nav item — deliberately a SECOND glyph vocabulary
   next to the status pills' filled shapes (`.pill__glyph` above), and kept
   from competing with it by the one axis that matters: a status glyph is
   filled (or a filled-looking border-only ring for "queued"), a nav icon
   never is. `fill="none"`/`stroke="currentColor"` are set on every
   `<svg class="nav__icon">` in src/console/layout.ts itself, not here — this
   class only carries layout (fixed square, never shrunk by a cramped row).
   Colour is inherited from whichever `.nav a`/`.nav__todo` rule currently
   applies (current page vs. not, hover), which is also what keeps Operations
   visually distinct from the seven unbuilt items once the rail is collapsed
   and the text label beside each icon is clipped away. */
.nav__icon { width: 1rem; height: 1rem; flex: none; }

/* Collapsed rail: the label and (for an unbuilt item) the "not built" tag
   are hidden VISUALLY only — clipped to a 1px box, never `display:none` —
   so a screen reader still reads the item's full accessible name, "not
   built" included, exactly as it would expanded. This is the standard
   visually-hidden/"clip" technique, not a bespoke one: absolutely
   positioned out of flow (so it does not leave a blank gap where the text
   used to sit), 1px×1px and clipped so nothing paints, but still present
   and readable to assistive tech. A hovering mouse user gets the same
   "not built" fact from the `title` src/console/layout.ts's renderNavHtml()
   sets on each unbuilt item — the one fact this clip costs a sighted user
   that a screen reader user never loses. */
.rail--collapsed .nav__label,
.rail--collapsed .nav__tag {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
  border: 0;
}

.who { display: block; font-family: var(--mono); font-size: var(--t-micro); color: var(--graphite); overflow-wrap: anywhere; }

/* min-width: 0 is load-bearing, not decoration: .content is now a flex child
   (of .shell) holding a table (.frame { overflow-x: auto }) that can be
   wider than the viewport. A flex item's default min-width is auto, which
   floors it at its content's intrinsic width and defeats .frame's own
   overflow scrolling — the table would push the whole page wider instead of
   scrolling inside its own box. */
.content { flex: 1; min-width: 0; padding: var(--space-5) var(--space-4); }
.content__title {
  margin: 0 0 var(--space-4);
  font-size: var(--t-title); font-weight: 400; letter-spacing: -0.01em;
}

/* Narrow screens: the rail returns to a horizontal strip above the content,
   reusing the SAME 60rem breakpoint screens/operations.html already declares
   for its own table/detail split, rather than inventing a second magic
   number for the bundle to keep in sync. */
@media (max-width: 60rem) {
  .shell { flex-direction: column; }
  .rail {
    flex-direction: row; flex-wrap: wrap; align-items: center;
    width: auto; border-right: 0; border-bottom: 1px solid var(--rule);
  }
  .rail--collapsed { width: auto; }
  .rail__top { flex-direction: row; align-items: baseline; gap: var(--space-2); }
  .rail__gap { display: none; } /* a wrapping horizontal strip has no single trailing edge left to push toward */
  .rail__bottom { flex-direction: row; align-items: center; gap: var(--space-3); }
  .nav { flex-direction: row; flex-wrap: wrap; }
  /* The active mark rotates back with the axis it is drawn on — the same
     "turned ninety degrees" rule as the rail/nav split above, run in
     reverse. */
  .nav a, .nav__todo { border-left: 0; border-bottom: 2px solid transparent; }
  .nav a[aria-current="page"] { border-bottom-color: var(--mark); }
  .nav a:hover { border-bottom-color: var(--rule); }
}

.banner {
  display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) var(--space-5);
  background: var(--surface-2);
  border-bottom: 1px solid var(--rule);
  border-left: 3px solid var(--status-degraded);
  font-size: var(--t-body);
}
.banner__glyph {
  width: 0.625rem; height: 0.625rem; align-self: center; flex: none;
  background: var(--status-degraded); clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.banner__tag {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--status-degraded);
}
.banner__text { flex: 1; min-width: 14rem; }

/* ==== Data table ====
 * Source: components/data-table.html — the richest and most-repeated version
 * (six screens agree with it on td's vertical-align:baseline; it is also the
 * one file with tbody tr:focus-visible, needed once a row is keyboard-reachable).
 */
.frame { border: 1px solid var(--rule); background: var(--surface-1); overflow-x: auto; }

table { width: 100%; border-collapse: collapse; font-size: var(--t-body); }
th {
  padding: var(--space-3);
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 400;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--graphite); text-align: left; white-space: nowrap;
}
td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--rule);
  vertical-align: baseline;
}
tr:last-child td { border-bottom: 0; }

/* Identifiers, counts and times are set in mono and pushed right so the digits
   stack. In a proportional face a column of ids is a ragged edge, and a ragged
   edge is where a wrong number hides. */
.num { font-family: var(--mono); text-align: right; white-space: nowrap; }
th.num { text-align: right; }
/* The external-key column: same mono treatment, one size down, because a
   composite key is longer than any other cell in the row. */
.key { font-family: var(--mono); font-size: var(--t-small); }

tbody tr:hover { background: var(--surface-2); }
tbody tr.is-selected { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--mark); }
tbody tr:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: calc(var(--focus-offset) * -1);
}

/* ==== Status pill ====
 * Source: components/status-pill.html for the full six-state set (queued,
 * claimed, done, failed, skipped, degraded); data-table.html and
 * screens/operations.html only ever render the four a job can carry, and
 * agree with status-pill.html wherever they overlap.
 */
.pill {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap;
}
.pill__glyph { width: 0.625rem; height: 0.625rem; flex: none; }
.pill--queued { color: var(--status-queued); }
.pill--queued .pill__glyph { border: 1.5px solid currentColor; border-radius: 50%; }
.pill--claimed { color: var(--status-claimed); }
.pill--claimed .pill__glyph {
  border: 1.5px solid currentColor; border-radius: 50%;
  background: linear-gradient(to right, currentColor 50%, transparent 50%);
}
.pill--done { color: var(--status-done); }
.pill--done .pill__glyph { background: currentColor; border-radius: 50%; }
.pill--failed { color: var(--status-failed); }
.pill--failed .pill__glyph { background: currentColor; }
.pill--skipped { color: var(--status-skipped); }
.pill--skipped .pill__glyph { border: 1.5px solid currentColor; border-radius: 0; }
.pill--degraded { color: var(--status-degraded); }
.pill--degraded .pill__glyph {
  background: currentColor;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

/* ==== Stat strip ====
 * Source: screens/operations.html. Screens with a different tile count carry
 * their own grid-template-columns/margin-bottom as a scoped `body .strip`
 * override in their remaining <style> block — fewer, wider tiles need a wider
 * minmax() than operations.html's nine-tile row.
 */
.strip {
  display: grid; gap: 1px; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  background: var(--rule); border: 1px solid var(--rule);
  margin-bottom: var(--space-4);
}
.tile { background: var(--surface-1); padding: var(--space-4); }
.tile__value {
  margin: 0; font-family: var(--mono); font-size: var(--t-stat);
  line-height: 1; letter-spacing: -0.03em;
}
.tile__unit { font-size: 0.5em; letter-spacing: 0; color: var(--graphite); }
.tile__label {
  margin: var(--space-3) 0 0; font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--graphite);
}
.tile__why { margin: var(--space-2) 0 0; font-size: var(--t-micro); color: var(--graphite); }
.tile--alarm .tile__value, .tile--alarm .tile__label { color: var(--status-failed); }
.tile--warn .tile__value, .tile--warn .tile__label { color: var(--status-degraded); }
.tile--unknown .tile__value { color: var(--graphite); }

/* ==== Buttons ====
 * Source: screens/operations.html. This is the one real inconsistency the
 * Step 1 inventory turned up: components/button.html treated the filled,
 * ink-coloured look as the unmodified `.btn` and demoted the outline look to
 * a `.btn--secondary` modifier. Every screen does the opposite — `.btn` bare
 * is the small outline control, and `.btn--primary` is the filled modifier
 * for the one bold action a screen is allowed. Screens win per the tie-break
 * rule; button.html, auth-error.html and sign-in.html's old markup are
 * corrected to say `.btn--primary` where they meant the filled button (see
 * the Task 1 report for the full list of touched files).
 */
.btn {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--rule); border-radius: 0;
  background: transparent; color: var(--ink);
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer;
  transition: border-color var(--transition);
}
.btn:hover { border-color: var(--ink); }
.btn:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
}
.btn--primary {
  background: var(--ink); border-color: var(--ink); color: var(--surface-0);
  font-size: var(--t-body); padding: var(--space-3) var(--space-4);
}
.btn--primary:hover { background: var(--mark); border-color: var(--mark); }
.btn--danger {
  color: var(--status-failed); border-color: var(--status-failed);
  font-size: var(--t-body); padding: var(--space-3) var(--space-4);
}
.btn--danger:hover { background: var(--status-failed); color: var(--surface-0); }

/* ==== Filter bar ====
 * Source: screens/operations.html for `.filters`/`.filters__controls` (the
 * bordered bar with a flex row inside it) and the chip family; the removable,
 * interactive `.chip` itself and its dashed `.chip--default` modifier come
 * from components/filter-bar.html, the one file that demonstrates both.
 * Screens that put selects directly under `.filters` (no `.filters__controls`
 * child) carry a scoped `body .filters` override restoring that flex-on-self
 * layout, so their existing markup does not need to change.
 */
.field { display: flex; flex-direction: column; gap: var(--space-1); }
.field__label {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--graphite);
}
.field select, .field input {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--rule); border-radius: 0;
  background: var(--surface-2); color: var(--ink);
  font-family: var(--sans); font-size: var(--t-body);
  transition: border-color var(--transition);
}
.field select:focus-visible, .field input:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
  border-color: var(--mark);
}

.filters {
  border: 1px solid var(--rule); background: var(--surface-1);
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4);
}
.filters__controls { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-end; }

.chips {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}
.chips__label {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--graphite);
}
.chips__result { margin-left: auto; font-family: var(--mono); font-size: var(--t-micro); color: var(--graphite); }

.chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--rule); border-radius: 0;
  background: var(--surface-2);
  font-family: var(--mono); font-size: var(--t-micro);
  color: var(--ink); cursor: pointer;
  transition: border-color var(--transition);
}
.chip:hover { border-color: var(--ink); }
.chip:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
}
/* The one chip that cannot be removed: the console's own default filter,
   restated so it is never invisible. Dashed border marks "not a choice you
   made" the same way the sample-data chip and the dry-run band do. */
.chip--default { border-style: dashed; color: var(--graphite); }

/* ==== Pagination ====
 * Source: components/pagination.html. `.pg`, `.count` and `.pages` are
 * shared class vocabulary: the console's operations screen renders all
 * three identically (Task 6b's fix pass — the pager was the first screen to
 * actually need this layout, not merely demonstrate it).
 *
 * `.pages` was written for pagination.html's INNER button group only — the
 * child that `.bar` (that preview's own local wrapper) pushes to the right
 * end of the bar with `margin-left: auto`. The console's operations.ts
 * originally put `.count` and the buttons directly inside `<nav class="pages">`
 * with no outer wrapper at all, which cannot be what `.pages` was written
 * for: `margin-left: auto` on the outermost element of the pager has nothing
 * to push against, so the top border, the padding and the roomier gap
 * between the count and the buttons that `.bar` supplies in the preview were
 * simply missing from the console's own pager, which read as jammed against
 * the last table row. `.pager` below is that missing outer wrapper, restated
 * under its own name rather than as `.bar` itself: pagination.html already
 * defines a bare `.bar` in its own <style> block, and this file's checker
 * forbids a preview from redefining a class console.css already owns (the
 * same reasoning that keeps `.mark` out of this file in favour of
 * `.authmark`, and `.screen` out of it in favour of `.authpage` — see each
 * section below). `.pages` now goes back to being exactly what its own rule
 * says: the inner group, nested inside `.pager`, with `.count` beside it.
 *
 * `.gap` was believed to collide with screens/campaigns.html's own
 * unrelated `.gap` (a "data is missing here" callout box) and was left out
 * on that basis — wrongly: campaigns.html links this file and THEN defines
 * its own `.gap` in a later <style> block, so its local rule already wins
 * the cascade on that page regardless of what this file declares. There
 * was never a real collision to avoid, only an unstyled console ellipsis to
 * fix. Scoped to `.pages .gap` below rather than a bare `.gap` for a
 * different, still-real reason: campaigns.html's callout is never nested
 * inside a `.pages` pager, so the scoped selector cannot reach it even by
 * accident, and this file still never redefines the bare name `.gap`
 * anywhere (Task 1's no-redefinition rule, checked by check-design.mjs by
 * exact selector string) — the same reasoning that keeps `.mark` out of
 * this file in favour of `.authmark` (see that section below) still holds
 * for the bare name, just not for a scoped one.
 */
.pager {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  border: 1px solid var(--rule); background: var(--surface-1);
  padding: var(--space-3) var(--space-4);
}
.pg {
  min-width: 2rem; padding: var(--space-2);
  border: 1px solid transparent; border-radius: 0;
  background: transparent; color: var(--graphite);
  font-family: var(--mono); font-size: var(--t-body);
  text-align: center; cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.pg:hover { color: var(--ink); border-color: var(--rule); }
.pg:focus-visible {
  outline: var(--focus-width) solid var(--mark);
  outline-offset: var(--focus-offset);
}
.pg[aria-current="page"] { color: var(--ink); border-color: var(--ink); }
/* Disabled rather than removed. A control that vanishes on the first page
   shifts every other control sideways, and the reader relearns the row each
   time they page. */
.pg:disabled { color: var(--rule); cursor: not-allowed; border-color: transparent; }
.pg:disabled:hover { color: var(--rule); border-color: transparent; }

.count {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.08em; color: var(--graphite);
}
.pages { display: flex; gap: var(--space-1); margin-left: auto; }
/* Scoped to a pager, not the bare name — see this section's own note above
   for why that is what keeps campaigns.html's unrelated `.gap` untouched. */
.pages .gap { color: var(--rule); padding: var(--space-2) 0; font-family: var(--mono); }

/* ==== Timestamp ====
 * Source: components/timestamp.html. data-table.html reasserts `.ts__rel`
 * identically, which is why it showed up in the Step 1 inventory — no real
 * conflict, just a second file confirming the same value.
 *
 * One shape everywhere: the clock time carries the fact, the relative phrase
 * carries the judgement. Showing only the relative phrase loses the fact;
 * showing only the clock time makes every reader do the arithmetic.
 */
.ts { font-family: var(--mono); font-size: var(--t-body); }
.ts__abs { color: var(--ink); }
.ts__sep { color: var(--rule); margin: 0 var(--space-2); }
.ts__rel { color: var(--graphite); }
.ts--future .ts__rel { color: var(--status-claimed); }
.ts--unknown { color: var(--graphite); }

/* ==== Key-value ====
 * Source: screens/operations.html — the job detail panel's key-value rows, in
 * the narrower measure that panel actually has. components/key-value.html and
 * components/unknown-value.html demonstrate the same rows at a wider,
 * more-generous column width because their gallery card has the room a
 * side panel does not; each keeps that width as a scoped `body .kv__row`
 * override rather than redefining the base rule. screens/score-distribution.html
 * reuses .kv__row/.kv__key for an unrelated three-column bar-inspector row,
 * scoped the same way.
 */
.kv__row {
  display: grid; gap: var(--space-3);
  grid-template-columns: 7rem 1fr;
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--rule);
}
.kv__key {
  margin: 0;
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--graphite);
}
/* A long value wraps rather than truncating. An email cut short is an email
   nobody can retype, and this panel is what an advisor reads before calling. */
.kv__val { margin: 0; font-size: var(--t-body); overflow-wrap: anywhere; }
.kv__val--mono { font-family: var(--mono); }

/* ==== Unknown-value marker ====
 * Two names for the same doctrine. components/key-value.html and
 * components/user-menu.html call it `.unknown`; components/unknown-value.html
 * — the component that documents the doctrine itself — and four screens
 * (enrichment, leads, search, sequences) call it `.unk`. Both still render
 * the identical "missing, with a stated reason" mark, so both stay defined
 * here rather than this task forcing a rename it was not asked to make.
 * New work should prefer `.unk`, since that is what the doctrine component
 * and the majority of screens already settled on.
 *
 * One way to say "no data", everywhere. The doctrine only survives if saying
 * nothing is easier than inventing something.
 */
.unknown { color: var(--graphite); }
.unknown abbr { text-decoration: none; border-bottom: 1px dotted var(--rule); cursor: help; }
.unknown__why { display: block; margin-top: var(--space-1); font-size: var(--t-micro); color: var(--graphite); }
.unk { color: var(--graphite); }
.unk abbr { text-decoration: none; border-bottom: 1px dotted var(--rule); cursor: help; }

/* ==== Empty state ====
 * Source: components/empty-state.html. max-width on .empty__body settled at
 * 32rem, the value four of six sources already agreed on.
 */
.empty { padding: var(--space-7) var(--space-4); text-align: center; }
.empty__title { margin: 0 0 var(--space-2); font-size: var(--t-lead); font-weight: 400; }
.empty__body { margin: 0 auto; max-width: 32rem; font-size: var(--t-body); color: var(--graphite); }
.empty__action { margin-top: var(--space-4); }

/* ==== Broken panel ====
 * Source: components/data-table.html, which already named this pattern
 * `.broken` (paired with `.empty` as the two full-panel states — "no rows"
 * versus "can't tell you how many rows"). components/error-state.html and
 * four screens (enrichment, score-distribution, search, sequences) had
 * independently built the identical panel under the name `.err`, which
 * collided head-on with screens/operations.html's unrelated, much smaller
 * `.err` — the inline verdict/message/next-step block inside the job detail
 * panel. That collision is the reason for the rename: `.err` now belongs to
 * operations.html alone (it stays inline there, single-sourced), and every
 * "the whole panel failed to load" state uses `.broken`, matching data-table.html.
 */
.broken { padding: var(--space-7) var(--space-4); text-align: center; }
.broken__title { margin: 0 0 var(--space-2); font-size: var(--t-lead); font-weight: 400; color: var(--status-failed); }
.broken__body { margin: 0 auto var(--space-4); max-width: 32rem; font-size: var(--t-body); color: var(--graphite); }
.broken__actions { display: flex; gap: var(--space-2); justify-content: center; }

/* ==== Auth pages: message block, form, fine print ====
 * Source: components/sign-in.html and components/auth-error.html — the two
 * previews `src/console/auth.ts` actually renders. `.product`/`.form`/`.msg`/
 * `.msg__title`/`.msg__body`/`.error`/`.fine` are the same visual component
 * in the preview's boxed `.screen` card and on the console's full-viewport
 * page: nothing about any of them depends on being inside a bordered box, so
 * both places must read from one definition, same as every other component
 * in this file.
 *
 * `.mark` (the wordmark) and `.screen` (the frame) are deliberately NOT
 * here — see each one's own note below for why.
 *
 * `.product` and `.divider` disagreed between the two previews (auth-error's
 * three stacked cases used tighter spacing than sign-in's single wordmark).
 * screens/sign-in.html — the actual full-page reference, not a gallery card —
 * agrees with components/sign-in.html on both, so that version wins per this
 * file's existing tie-break rule; auth-error.html keeps its tighter spacing
 * as a scoped `body .selector` override in its own remaining <style> block.
 */
.product {
  margin: var(--space-3) 0 0;
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--graphite);
}
.divider { width: 3rem; height: 1px; background: var(--rule); margin: var(--space-6) 0; }

.form { display: flex; flex-direction: column; gap: var(--space-3); width: 100%; max-width: 18rem; text-align: left; }

.msg { max-width: 28rem; }
.msg__title {
  margin: 0 0 var(--space-2);
  font-size: var(--t-lead); font-weight: 400; color: var(--status-failed);
}
.msg__body { margin: 0; font-size: var(--t-body); color: var(--graphite); }

/* Deliberately not attached to either field. Naming the field would tell an
   attacker which half of the guess was right — the one thing this message
   must never do. */
.error {
  margin: 0; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--status-failed); background: var(--surface-2);
  font-size: var(--t-micro); color: var(--status-failed); text-align: left;
}

.fine {
  margin: var(--space-5) 0 0; max-width: 24rem;
  font-size: var(--t-micro); color: var(--graphite);
}

/* ==== Auth wordmark ====
 * `.mark` itself stays OUT of this file on purpose, unlike everything else in
 * the section above: screens/enrichment.html already owns a bare `.mark` for
 * an unrelated component (an outcome glyph — resolved/no-match/failed), and
 * this file's checker (scripts/check-design.mjs) forbids a preview from
 * redefining a class console.css already owns, with no carve-out for "same
 * name, unrelated meaning". Reusing `.mark` here would make enrichment.html
 * fail that check for a component that has nothing to do with sign-in.
 *
 * `.authmark` is the console's own name for the identical wordmark rule
 * (byte-for-byte what `.mark` would have been, winning tie-break included).
 * The two sign-in/auth-error previews keep rendering `<p class="mark">` and
 * keep their own inline `.mark` rule unchanged — this is the one place in
 * the auth pages where the preview and the console genuinely use two
 * separate rules, and it is a naming accident, not a rendering difference:
 * both rules must be kept in sync by hand if the wordmark treatment ever
 * changes, same as `.screen`/`.authpage` below.
 */
.authmark {
  font-family: var(--serif); font-size: var(--t-stat);
  line-height: 1; letter-spacing: 0.01em; margin: 0;
}

/* ==== Auth page container ====
 * Console-only — NOT a rename of `.screen` and not shared with it. `.screen`
 * (design/components/sign-in.html, auth-error.html) is a documentation
 * frame: 18-22rem tall, bordered, sized to sit beside other cards in the
 * gallery. The sign-in and auth-error routes ARE the page, not a card inside
 * one, so this fills the viewport instead of being boxed inside it. Named
 * apart from `.screen` for the same reason `.authmark` is named apart from
 * `.mark`: this file's checker forbids a preview from redefining a class
 * already defined here, and `.screen` already means the boxed frame in every
 * preview that uses it.
 *
 * Colour, type and line-height are carried here rather than left to a global
 * `body` rule, because there is no such rule — every preview supplies its own
 * `body { font-family: var(--sans); ... }` as gallery chrome (see e.g.
 * sign-in.html's own remaining <style> block), and the console has nothing
 * equivalent. Without it, `.fine`/`.error`/`.msg__body` — none of which sets
 * its own font-family — would inherit the browser's serif default instead of
 * the token.
 */
.authpage {
  min-height: 100vh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: var(--space-7) var(--space-5);
  background: var(--surface-0); color: var(--ink);
  font-family: var(--sans); font-size: var(--t-ui); line-height: 1.5;
  text-align: center;
}

/* New, not moved: auth-error.html's three states put their primary button
   directly under `.screen`, with no form to give it a gap via `.form`'s own
   flex `gap`. `.screen .btn--primary { margin-top: ... }` stays inline there
   as preview chrome (that rule is about `.screen`, not `.authpage`), so the
   real page needs its own equivalent. `>` rather than a bare descendant
   combinator because the button genuinely is a direct child there, and
   because it is what keeps this from also matching sign-in's primary
   button — the other `.btn--primary` under `.authpage` — which sits inside
   `.form` instead and does not want this extra gap. */
.authpage > .btn--primary { margin-top: var(--space-5); }
