/* Layout and components. Geometry follows the design handoff: sidebar 196px,
 * top bar 46px, page padding 13px 14px, card gap 11px (20px in Ledger), cell
 * padding 9px, control height 28px, grid rows 27/23px with a 26px header.
 *
 * The AG Grid block near the bottom restyles the vendored Quartz theme through
 * the same custom properties, so the four themes carry the grid with them
 * instead of each needing its own grid theme. That block now reads the
 * --grid-* names rather than the underlying theme tokens: themes.css declares
 * those on :root as indirections through the very tokens this file used to
 * name, so the mapping is one hop longer and resolves identically -- and the
 * Nocturne theme can re-point a single --grid-* member without this file
 * growing a theme-scoped rule. See themes.css, "the --grid-* NAMES".
 */

* { box-sizing: border-box; }

/* `hidden` is a UA style (`display: none`), and ANY author display rule beats
 * it. `.ctl` is `display: inline-flex`, so every `node.hidden = true` in
 * shell.setControls() and in the screens was silently a no-op: the top bar
 * showed Search, Filters, the columns count and Export on screens that had
 * declared none of them, and the back caret on screens with nothing to go back
 * to. It cost nothing on a wide bar and was invisible; at 375px the dead
 * controls pushed Refresh off the end of the scrolling strip. Found while
 * implementing the narrow control set, which cannot work without it. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* The bare checkboxes -- Row stripes in Settings, the metric list in the
   * report builder -- are the browser's blue otherwise, in all three themes. */
  accent-color: var(--color-accent);
}

/* The dropdown list is a UA-painted surface and the option has no background of
 * its own, so it took the light canvas while its text inherited the theme's
 * near-white from the select: white on white, legible only under the OS
 * highlight bar. `color-scheme` in themes.css is what actually fixes that;
 * these two declarations make the result the SAME on the engines that do honour
 * author colours in a popup (Chrome on Windows, Firefox) instead of leaving it
 * to each UA's idea of dark. On macOS they are ignored and the scheme carries
 * it alone. Both mechanisms ship. See phase7-repairs/004. */
option, optgroup {
  background-color: var(--pop);
  color: var(--color-text);
}

a { color: var(--color-accent); text-decoration: none; }

/* The design's rule too: 2px solid accent at a 2px offset, on every
 * interactive element, never the browser default. --focus-ring resolves the
 * accent per theme, so this is the same paint it always was. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.mono { font-family: var(--font-mono); font-size: 12px; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.muted { color: var(--color-neutral-500); }
.wrap { overflow-wrap: anywhere; }

/* THE FOLD. One line, cut with an ellipsis, with the whole of it in the
 * element's own `title`. It is the counterpart of .wrap: .wrap is for a value
 * that must be read in full and may take three lines to do it (an outbound URL
 * in a simulator result); .fold is for a value in a row of values, where a
 * third line would push everything under it down and a long one would run
 * under its neighbour.
 *
 * The grid has had this since `.ag-cell.fold`, which is a MORE specific
 * selector and keeps winning inside a grid; this is the same rule for
 * everything that is not a grid cell. min-width:0 is the half that is easy to
 * forget: a flex or grid item refuses to shrink below its own content, so
 * overflow:hidden on it would never have anything to hide. */
.fold { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tone-ok { color: var(--ok); }
.tone-warn { color: var(--warn); }
.tone-bad { color: var(--bad); }
.tone-muted { color: var(--color-neutral-500); }

.micro {
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-neutral-500);
}

/* --- shell --------------------------------------------------------------- */

/* Three tracks: sidebar, work area, inspector rail. The rail's own track is
 * `auto` and the rail sizes itself, so collapsing it to the 26px strip is a
 * class on ONE element rather than a re-templated grid -- and the narrow rule
 * below still collapses the whole thing to a single column, which is the shape
 * tests/narrow-shell.test.js pins. */
.shell {
  display: grid;
  grid-template-columns: 196px 1fr auto;
  height: 100%;
  overflow: hidden;
}

.sidebar {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--panel);
  border-right: 1px solid var(--color-neutral-900);
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 13px;
}

.brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 21px;
  height: 21px;
  border: 1px solid var(--color-accent);
  border-radius: var(--r2);
  color: var(--color-accent);
  font-size: 13px;
}

.brand-word { font-size: 13px; font-weight: 500; }

/* The nav is its own scroll region so brand, theme switcher and account stay
 * pinned at any viewport height. */
.nav {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 8px;
}

.nav-group {
  padding: 12px 13px 4px;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5.5px 13px;
  font-size: 12.5px;
  color: var(--color-neutral-300);
  border-left: 2px solid transparent;
}

.nav-item i { font-size: 15px; }
.nav-item:hover { background: rgba(255, 255, 255, 0.04); }
.th-day .nav-item:hover { background: rgba(0, 0, 0, 0.04); }

.nav-item.on {
  border-left-color: var(--color-accent);
  background: var(--color-accent-200);
  color: var(--color-text);
}

.theme-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 8px 10px;
  border-top: 1px solid var(--color-neutral-900);
}

.theme-switch .ctl { flex: 1; padding: 0 6px; font-size: 11px; }

/* `flex-wrap: wrap` above arrived with the pane pill and repairs something
 * older. These pills are `white-space: nowrap` flex items with the default
 * `min-width: auto`, so `flex: 1` cannot shrink them below their text: four
 * theme names already need more than the 176px this strip has, and without a
 * wrap the last one is painted past the sidebar's right edge and over the work
 * area. Measured in a browser at 196px with Inter loaded, and visible in the
 * 2026-08-08 operator screenshot.
 *
 * The pane pill takes a row of its own -- a full basis rather than a fifth
 * share -- and the container's own 4px gap spaces the rows without a margin. */
.theme-switch .pane-pick { flex-basis: 100%; justify-content: center; }

.account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-top: 1px solid var(--color-neutral-900);
}

.account-name { font-size: 12px; color: var(--color-neutral-400); overflow: hidden; text-overflow: ellipsis; }

.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 46px;
  padding: 0 14px;
  background: var(--bar);
  border-bottom: 1px solid var(--color-neutral-900);
}

.topbar-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 0 1 auto; }
/* ⚠ THE TITLE FOLDS RATHER THAN PUSHING THE STRIP (lane P2, e2uipolish P5.8).
 * `white-space: nowrap` with no overflow rule made a long screen title -- a
 * campaign name in a crumb-bearing detail tab -- grow the title box past its
 * share of a 46px bar and shove `.controls` off the right edge, which is where
 * the horizontal scrollbar R2 removes came from in the first place. min-width:0
 * is the half that is easy to forget: a flex item's automatic minimum size is
 * its content, so overflow:hidden alone would clip nothing. The `.narrow` copy
 * of this rule at the bottom of the file is now redundant and left where it is,
 * because it belongs to the mobile block and says the same thing.
 * shell.setTitle writes the full text into the h1's `title`, so a folded title
 * is still readable on hover. */
.topbar h1 { margin: 0; font-size: 15px; font-weight: 500; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.crumb { font-size: 11.5px; color: var(--color-neutral-500); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The control strip owns all remaining width and FOLDS rather than scrolling.
 *
 * ⚠ `overflow: hidden` REPLACES `overflow-x: auto` (lane P2, e2uipolish P5.6,
 * ruling R2 -- the user called the horizontal scrollbars "sliders" and asked
 * for them gone). The reason the strip used to scroll was that the last
 * controls would otherwise be unreachable on a phone; shell.js's foldControls()
 * answers that better, by moving whatever does not fit into a `⋯` menu where it
 * is one click away instead of one drag of a 28px-tall scrollbar.
 *
 * It is still a clip in BOTH axes -- CSS will not scroll or clip one axis and
 * leave the other visible -- so the range menu and the ⋯ menu both still need
 * `menu-float` to escape it. tests/topbar.test.js asserts that as an invariant
 * ("a clipping control strip implies a floating menu") rather than as a
 * snapshot of either value.
 *
 * `flex-shrink: 0` on the members is the other half of the fold, and without it
 * the fold would never fire: flex items shrink to their min-content before a
 * container overflows, so the strip would crush the 196px search box to a
 * sliver and the whole bar to unreadable before scrollWidth ever exceeded
 * clientWidth. That crushing IS the "very cramped" the ask names. A control now
 * keeps the size it was designed at, or it leaves the bar.
 *
 * `margin-left: auto` on the first child stays, and must NOT become
 * `justify-content: flex-end`: an auto margin resolves to zero once free space
 * goes negative, so the overflow lands past the RIGHT edge where scrollWidth
 * measures it, while flex-end would push it past the left edge where nothing
 * does -- and foldControls() would then never see the overflow it exists to
 * remove.
 *
 * ⚠ `flex: 1 1 auto` RATHER THAN `flex: 1`, and the difference is the whole
 * third digit. `flex: 1` is `1 1 0%`: the strip's base size is ZERO, so it is
 * fed only out of the space LEFT OVER after the title's base, and a title long
 * enough to fill the bar on its own left the strip at literally zero pixels --
 * with the ⋯ button itself clipped, which is the one control that must never be
 * unreachable. A base of `auto` is the strip's own content, so a bar with too
 * little room shrinks the title and the strip in proportion to what each of them
 * asked for: the title folds to an ellipsis, the strip folds into the ⋯ menu,
 * and both stay usable. A bar with room to spare behaves exactly as before,
 * because `flex-grow: 1` still takes every spare pixel and the auto margin above
 * still pushes the controls to the right edge with it. */
.controls {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  /* ⚠ ONE CONTROL WIDE, NEVER ZERO. This was `min-width: 0`, which only means
   * "you may shrink past your content" -- and between the narrow breakpoint
   * (760px) and about 1000px the bar IS over-subscribed: the live-ness wrap
   * (143px), the draft pill (122px) and the language switch (70px) sit outside
   * this strip and all three refuse to shrink, so the strip was handed zero
   * pixels and clipped every control it held. 30px is one `.ctl.ctl-icon` --
   * the ⋯ button's own border box -- and it is the floor because that button is
   * the ONLY way to the controls that folded into it. A clipped ⋯ is a console
   * with no Refresh, no Export and no date range at all. The title (min-width:0)
   * gives up the difference, which is what P5.8's ellipsis is for. */
  min-width: 30px;
  justify-content: flex-start;
  overflow: hidden;
}

.controls > :first-child { margin-left: auto; }
.controls > * { flex-shrink: 0; }

/* The folded controls, stacked. They are the SAME NODES the strip held, so they
 * keep their handlers, their per-screen `hidden` state and -- in the range's
 * case -- their own menu, which opens on top of this one because both are
 * `position: fixed` and neither clips the other. */
.controls-menu { display: flex; flex-direction: column; align-items: stretch; gap: 4px; }
.controls-menu > .search { width: 100%; }
.controls-menu > .menu-wrap { width: 100%; }
.controls-menu > .ctl,
.controls-menu > .menu-wrap > .ctl { width: 100%; justify-content: flex-start; }

/* The todo drawer: description paragraphs, and a gallery whose images are
 * width-constrained so a 4 MB screenshot cannot push the drawer sideways. */
.todo-detail { display: flex; flex-direction: column; gap: 12px; }
.todo-description p { margin: 0 0 8px; font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; }
.todo-gallery { display: flex; flex-wrap: wrap; gap: 8px; }
.todo-image { max-width: 100%; max-height: 220px; border: 1px solid var(--color-neutral-900); border-radius: var(--r); }
.todo-textarea { min-height: 96px; resize: vertical; font-family: inherit; line-height: 1.5; }

/* The language switch is OUTSIDE .controls on purpose -- setControls() shows
 * and hides every member of that strip per screen, and this must be present on
 * all of them. flex-shrink: 0 keeps it off the control strip's scroll axis. */
.lang-switch { display: flex; gap: 4px; flex-shrink: 0; }
/* The selected language uses the shared .ctl.on accent rule, the same one the
 * theme picker uses, so "which one am I on" reads identically in both. */
.lang-switch .ctl { padding: 0 8px; font-size: 11px; }
.login-card .lang-switch { justify-content: center; }

.ctl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 9px;
  font-family: inherit;
  font-size: 12px;
  color: var(--color-neutral-300);
  background: transparent;
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--r2);
  cursor: pointer;
  white-space: nowrap;
}

.ctl:hover:not(:disabled) { border-color: var(--color-neutral-700); color: var(--color-text); }
.ctl:disabled { opacity: 0.45; cursor: not-allowed; }
.ctl.on { color: var(--color-accent); border-color: var(--color-accent); background: var(--color-accent-200); }
.ctl-icon { padding: 0 7px; }
.ctl-quiet { border-color: transparent; }
.ctl-primary { color: var(--color-accent); border-color: var(--color-accent); }
.ctl-danger { color: var(--bad); border-color: var(--bad); margin-right: auto; }
.ctl-static { cursor: default; }

.search { width: 196px; }
.bar-compact .search { width: 96px; }

.input { width: 100%; height: 28px; background: var(--fibg); border-color: var(--filine); color: var(--color-text); }
select.input { padding-right: 4px; }

.menu-wrap { position: relative; }

.menu {
  position: absolute;
  right: 0;
  top: 32px;
  z-index: 40;
  min-width: 160px;
  padding: 4px;
  background: var(--pop);
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--r2);
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.28);
}

.menu-item {
  display: block;
  width: 100%;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--color-text);
  background: transparent;
  border: 0;
  cursor: pointer;
}

.menu-item:hover { background: var(--row-hover); }

/* --- lane P2 (e2uipolish P5.7): the custom range, inside the range menu ----
 *
 * The form expands INSIDE the `.menu` the five presets live in rather than
 * opening a second popover, so the menu grows downward and shell.js re-anchors
 * it (anchorRangeMenu) after the expansion -- a menu measured as a five-item
 * list and then widened by a date input would hang off the right edge of a
 * phone. The rule that stops that is the re-anchor, not a width here.
 *
 * No `color-scheme` declaration: themes.css already sets it on every theme, so
 * the native date picker and its calendar glyph follow the console's theme. */
.range-custom {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 4px;
  padding: 8px 8px 4px;
  border-top: 1px solid var(--color-neutral-800);
}

.range-field { display: flex; align-items: center; justify-content: space-between; gap: 10px; cursor: pointer; }
.range-date { width: 138px; flex: 0 0 auto; }
.range-apply { align-self: flex-end; }

/* The refusal is a SENTENCE, so it drops `.micro`'s uppercase and its tracking
 * and it wraps -- everything else wearing `.micro` in this console is a two-
 * word label. It keeps `.micro`'s size and `.tone-bad`'s colour. */
.range-error {
  max-width: 190px;
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.4;
  white-space: normal;
}

/* The `Custom...` entry is the only menu item with an open/shut state, and it
 * says which it is in the accent the rest of the console uses for `on`. */
.menu-item.range-custom-open.on { color: var(--color-accent); }

/* ⚠ THE FIX FOR THE DEAD RANGE BUTTON (v2fe lane W6). Read this before
 * "simplifying" it back to `position: absolute`.
 *
 * `.controls` above declared `overflow-x: auto`, and CSS will not scroll or clip
 * one axis while leaving the other visible: with `overflow-x` set to anything
 * but `visible`, `overflow-y: visible` COMPUTES TO `auto`. The strip was
 * therefore a 28px-tall scroll container in both directions, and an absolutely
 * positioned menu whose containing block (`.menu-wrap`) sits inside it is
 * clipped away -- the range dropdown opened at `top: 32px`, four pixels below
 * the strip's bottom edge, and never appeared. The click handler worked the
 * whole time.
 *
 * ⚠ THE DECLARATION CHANGED, THE CLIP DID NOT. Lane P2 (P5.6) made it
 * `overflow: hidden` so the strip folds into a `⋯` menu instead of scrolling,
 * and `hidden` clips in both axes for exactly the same reason `auto` did. Both
 * menus in the strip -- the range's and the ⋯ -- therefore carry this class.
 *
 * `position: fixed` moves the containing block to the VIEWPORT, which is
 * outside `.controls`, and a scroll container does not clip a descendant whose
 * containing block is outside it. shell.js writes `left`/`top` from the
 * trigger's rect on open -- see menuAnchor there. `right: auto` cancels the
 * `right: 0` above, which would otherwise fight the computed `left`.
 *
 * Only the TOP BAR's menus carry this class. The pane head's copy stays
 * absolute: `.pane-head` scrolls in neither axis, so it clips nothing. */
.menu.menu-float { position: fixed; right: auto; }

/* --- scrollbars (v2fe lane W6) --------------------------------------------
 *
 * The console was showing the OS scrollbar -- a 16px light-grey trough down the
 * nav and across the grid -- inside a black theme. These are the design pack's
 * own rules (handoff-v2/assets/console.css:440-443) with its own tokens, which
 * themes.css declares on :root as indirections through each theme's neutral
 * ramp: so Nocturne gets exactly the pack's #26262b thumb and the other three
 * get their own, and none of them needs a rule of its own here.
 *
 * RESTYLED, NEVER REMOVED. Nothing below hides a scrollbar or suppresses
 * scrolling: `thin` is a width, not a disappearance, and the track stays a
 * track. A `::-webkit-scrollbar { display: none }` anywhere in this file would
 * take the grid's horizontal scroll with it.
 *
 * Both syntaxes are declared on purpose. Chrome 121+ implements the standard
 * `scrollbar-width` / `scrollbar-color` and ignores the -webkit- pseudos once
 * either is set; older Chromium and Safari only know the pseudos. Firefox only
 * knows the standard pair. Declaring both covers all three with the same two
 * colours rather than leaving one engine on the OS default. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--grid-scrollbar-thumb) var(--grid-scrollbar-track);
}

::-webkit-scrollbar { width: var(--grid-scrollbar-size); height: var(--grid-scrollbar-size); }
::-webkit-scrollbar-track { background: var(--grid-scrollbar-track); }
::-webkit-scrollbar-corner { background: var(--grid-scrollbar-track); }

/* The 2px transparent border plus background-clip is what makes a 9px bar read
 * as a 5px thumb with breathing room, instead of a slab filling the gutter. */
::-webkit-scrollbar-thumb {
  background: var(--grid-scrollbar-thumb);
  background-clip: content-box;
  border: 2px solid transparent;
  border-radius: var(--radius-lg);
}

::-webkit-scrollbar-thumb:hover { background: var(--grid-scrollbar-thumb-hover); background-clip: content-box; }

.page {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--page-gap);
  padding: 13px 14px;
  overflow-y: auto;
}

/* The head is one line that must survive a long config version. Without
 * min-width:0 a flex item refuses to shrink below its content, so the muted
 * "N campaigns · config <version>" span forced the row to wrap one word per
 * line at phone widths. The muted span owns the slack and ellipsises; the
 * label and the action button keep their size. */
.screen-head { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
.screen-head > * { min-width: 0; }
.screen-head .muted { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.screen-head .source-label, .screen-head .ctl { flex: none; }
.screen-head .ctl:last-child { margin-left: auto; }

/* --- cards, KPIs --------------------------------------------------------- */

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-neutral-900);
  border-radius: var(--r);
}

/* Ledger: rules instead of boxes. */
.th-ledger .card { border: 0; border-top: 1px solid var(--hair); background: transparent; }

.card-head { display: flex; align-items: center; justify-content: space-between; padding: 9px 11px 0; }
.card-body { display: flex; flex-direction: column; gap: 9px; padding: 9px 11px 11px; }

/* `card(null, ...)` -- a card with no header -- had a 9px top inset against an
 * 11px bottom one, because the 9px is the HEAD's top padding and the body's own
 * top padding was written to sit UNDER a head rather than to be the card's top
 * inset. With no head to sit under, the box is lopsided; beside a headed card
 * in a `.row-2` the two read as two different cards. */
.card > .card-body:first-child { padding-top: 11px; }

/* minmax(0, ...), never a bare 1fr -- the lesson .mini-row below is already
 * written out for. A bare `1fr` is `minmax(auto, 1fr)`, and that auto MINIMUM
 * is the track's min-content width: one card holding an unbroken token (a click
 * id, a campaign token, a URL in a <code>) pushed its half of the row open and
 * took the other half past `.shell { overflow: hidden }` with it. */
.row-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--page-gap); }

/* THE STRIP IS ONE RULED BAND, and every tile in it is drawn the same way.
 *
 * It used to be drawn two ways. `.kpi:first-child { border-left: 0;
 * padding-left: 0 }` un-ruled the leading tile -- that is the missing line in
 * front of the dashboard's CLICKS tile, reported as "there isn't a line before
 * the clicks datacard" -- and `:first-child` cannot see a WRAP, so the first
 * tile of the second row kept the rule the first tile of the first row had
 * lost, and one strip showed both designs at once. Both exceptions are gone:
 * one border, one padding, every tile, every row.
 *
 * It is a GRID rather than a wrapping flex row, because a wrapping flex row
 * cannot make a wrapped strip coherent. Each flex LINE stretches its own items
 * over the full width, so Costs' fifteen tiles came out eight narrow ones on
 * the first row and seven wider ones on the second, columns aligned with
 * nothing; and each line was as tall as its own tallest tile, so the two rows
 * had different heights. `auto-fit` keeps the 140px basis the W7 width sweep
 * chose -- an EMPTY track collapses and the rest take its space, so six tiles
 * still fill the page exactly as they did, and the strip still reflows instead
 * of pushing past `.shell { overflow: hidden }` -- while every row lands on the
 * same columns. `grid-auto-rows: 1fr` sizes every row to the tallest, so the
 * band is one height throughout. `min(140px, 100%)` is the guard for a
 * container narrower than a single tile: a bare 140px minimum would overflow it.
 *
 * The rules touch, deliberately: `gap: 0` plus each tile's 9px of vertical
 * padding is what separates one row of tiles from the next, the way a table
 * separates two rows. */
.kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
  grid-auto-rows: 1fr;
  gap: 0;
}

.kpi {
  min-width: 0;
  padding: 9px 14px;
  border-left: 1px solid var(--color-neutral-900);
  border-top: 1px solid var(--color-neutral-900);
}

/* Every line of a tile folds -- the label, the figure and the note alike.
 * Only the label did, so a figure or a note that outgrew its tile wrapped onto
 * a second line, which changed that tile's height and, in a wrapped strip, its
 * whole row's. The whole of what a tile says stays readable in the tile's own
 * `title` (ui.js kpiStrip), which a screen with something better to say about a
 * tile -- Costs writes the metric's formula there -- overwrites wholesale. */
.kpi .micro, .kpi .kpi-figure, .kpi .kpi-note {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.th-ledger .kpi { border-color: rgba(255, 255, 255, 0.1); }
.kpi-figure { font-size: var(--kpi-size); font-weight: var(--kpi-weight); font-variant-numeric: tabular-nums; }
.kpi-note { font-size: 11px; color: var(--color-neutral-500); }

/* --- mini tables --------------------------------------------------------- */

.mini { display: flex; flex-direction: column; }

/* Tracks are minmax(0, …), never a bare fr. A bare 1fr track is min-content
 * wide at minimum, so a mono `source_profile_match` or a full campaign token
 * pushed its track open and the headers clipped mid-word instead of shortening.
 * The first column is the labelled one -- decision · reason, campaign token,
 * component name -- and gets twice the share. */
.mini-row {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: minmax(0, 2fr);
  grid-auto-columns: minmax(0, 1fr);
  align-items: center;
  gap: 9px;
  padding: 5px 0;
  border-bottom: 1px solid var(--line2);
  font-size: 12px;
}

/* Cells truncate with an ellipsis rather than overflowing their track. */
.mini-row > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-row .decision { max-width: 100%; min-width: 0; }
.mini-row .decision .pill { flex: none; }
.mini-row .decision-reason { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* An actions cell is buttons, not prose: ellipsising it would hide a control.
 * It wraps within its own track instead, which keeps the overflow off the page. */
.mini-row > .row-actions { overflow: visible; flex-wrap: wrap; justify-content: flex-end; }

.mini-row.clickable { cursor: pointer; }
.mini-row.clickable:hover { background: var(--row-hover); }

.mini-head {
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-500);
  border-bottom-color: var(--color-neutral-800);
}

/* A disabled account stays legible -- it is still an identity in the audit log,
   not a deleted one -- but reads as inactive at a glance. */
.row-disabled { opacity: 0.55; }
.row-disabled .mono { text-decoration: line-through; }

.kv { display: flex; align-items: baseline; gap: 8px; padding: 3px 0; border-bottom: 1px solid var(--line2); font-size: 12px; }
.kv-k { min-width: 150px; color: var(--color-neutral-500); }
.kv-v { min-width: 0; overflow-wrap: anywhere; }

.dl { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 4px 10px; margin: 0; font-size: 12px; }
.dl dt { color: var(--color-neutral-500); }
/* Same as .kv-v above it: a definition value is a served string and may be one
 * unbroken token, which has nowhere to break unless it is told it may break
 * anywhere. */
.dl dd { margin: 0; overflow-wrap: anywhere; }

.pill {
  display: inline-block;
  padding: 1px 6px;
  font-size: 11px;
  border: 1px solid currentColor;
  border-radius: var(--r2);
}

/* Ledger renders statuses as coloured text: no background, no border, no
 * padding. */
.th-ledger .pill { border: 0; padding: 0; }

.pill.tone-ok { color: var(--ok); }
.pill.tone-warn { color: var(--warn); }
.pill.tone-bad { color: var(--bad); }

/* Console brackets its statuses. */
.th-console .pill::before { content: '[ '; }
.th-console .pill::after { content: ' ]'; }

.decision { display: inline-flex; align-items: baseline; gap: 6px; }
.decision-reason { color: var(--color-neutral-500); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  font-size: 11.5px;
  background: var(--color-accent-200);
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--r2);
  /* A chip holds attacker-controlled text -- a campaign token, a sub_id value,
   * an account label, a 36-character click id. It never grows past the strip
   * that holds it, and what does not fit is cut rather than laid over the chip
   * beside it. There is no blanket ch cap here on purpose: `.chip-link` is a
   * sentence PLUS a value plus a close button, and capping the whole chip would
   * eat the sentence. The cap goes where the long text actually is, below. */
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

/* The parts of a multi-part chip fold individually, so the dot, the operator
 * and the close button keep their size and the VALUE gives up the room. */
.chip > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip > .chip-x { flex: none; }

/* A chip whose whole content is one text node has no element to fold: its text
 * is an ANONYMOUS flex item, and text-overflow does not reach one, so
 * overflow:hidden cut it mid-letter with no ellipsis. As a block container it
 * folds like everything else. Measured in Chrome, both ways round -- and a chip
 * WITH children must stay a flex row, which is why this is `:not(:has(*))`
 * rather than a display change for all of them. */
.chip:not(:has(*)) { display: inline-block; max-width: 28ch; }

.chip-op { color: var(--color-accent); }
.chip-value { font-family: var(--font-mono); }
/* The cap, on the part that carries the value. `.chip-link .chip-value` further
 * down keeps its tighter 22ch by source order. */
.chip .chip-value { max-width: 28ch; }
.chip-x { display: inline-flex; padding: 0; background: none; border: 0; color: inherit; cursor: pointer; font-size: 11px; }

.banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 11px;
  border-left: 2px solid currentColor;
  background: var(--flowbg);
  font-size: 12px;
}

.banner-text { display: flex; flex-direction: column; gap: 2px; color: var(--color-text); }
.banner.tone-ok { color: var(--ok); }
.banner.tone-warn { color: var(--warn); }
.banner.tone-muted { color: var(--color-neutral-500); }
.banner.tone-info { color: var(--color-accent); }
.banner .ctl { margin-left: auto; }

.source-label {
  padding: 1px 6px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--warn);
  border: 1px solid currentColor;
  border-radius: var(--r2);
}

.note { margin: 0; font-size: 11.5px; color: var(--color-neutral-500); }
.note ul, ul.note { margin: 4px 0 0; padding-left: 16px; }
.note li { margin: 2px 0; }
.hint { font-size: 11px; color: var(--color-neutral-600); }
.warn-inline { font-size: 11px; color: var(--warn); }

/* --- API-pending markers (v2, lane W2C) ----------------------------------
 *
 * The design puts this chip in the pane's view head. In this console the view
 * head belongs to the shell and a screen owns the strip below it, so the chip
 * lives in .screen-head instead -- same reading order, same wording ("ask N ·
 * what"), which is what keeps the chip and the backend-ask table searchably the
 * same item.
 *
 * It borrows the .source-label geometry deliberately: both are "this surface is
 * not what it looks like" markers and an operator should read them as one
 * family. */
.pend {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* Shrinkable, unlike .source-label and .ctl beside it: two chips plus a
   * button do not fit a 375px head, and .shell is overflow:hidden -- so a chip
   * that refused to shrink would push the last one off the page with nothing to
   * scroll to. The label ellipsises instead; the ask number is at the front. */
  min-width: 0;
  max-width: 220px;
  padding: 1px 6px;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--warn);
  border: 1px solid currentColor;
  border-radius: var(--r2);
}

.pend i { font-size: 10px; flex: none; }
.pend > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Ledger draws statuses as coloured text, no boxes -- the same rule .pill
 * follows -- and Console brackets them. The chip follows both so it does not
 * become the one framed thing on a ruled page. */
.th-ledger .pend { border: 0; padding: 0; }
.th-console .pend::before { content: '[ '; }
.th-console .pend::after { content: ' ]'; }

/* A column that is DESIGNED and has no data source yet. Every cell is an em
 * dash in this tone: not a number, not a blank, and visibly not the zero that
 * may be sitting in the column beside it. */
.pending-cell { color: var(--color-neutral-600); font-style: italic; }
.ag-theme-quartz .ag-cell.pending-cell { color: var(--color-neutral-600); font-style: italic; }

/* The design's statusbar: what is on screen on the left, the one sentence that
 * stops the screen being misread on the right. It wraps rather than clips --
 * the right-hand sentence is the half that carries the warning. */
.screen-foot {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 11px;
  color: var(--color-neutral-600);
}

.screen-foot .sp { flex: 1 1 auto; }

.empty { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 40px 20px; text-align: center; }
.empty-title { font-size: 13px; }
.empty-detail { font-size: 11.5px; color: var(--color-neutral-500); max-width: 520px; }

.loading { display: flex; align-items: center; gap: 8px; padding: 24px; font-size: 12px; color: var(--color-neutral-500); }
.loading.warming { color: var(--warn); }

.spinner {
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--color-neutral-800);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.error-panel { display: flex; flex-direction: column; gap: 4px; padding: 16px; border-left: 2px solid var(--bad); font-size: 12px; }

.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 60;
  padding: 8px 14px;
  font-size: 12px;
  background: var(--pop);
  border: 1px solid var(--color-accent);
  border-radius: var(--r2);
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.28);
}

/* --- chart --------------------------------------------------------------- */

.chart { display: flex; flex-direction: column; gap: 6px; }
.chart-svg { width: 100%; height: 236px; }
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-axis { fill: var(--chart-axis); font-size: 9px; font-family: var(--font-body); }
.chart-crosshair { stroke: var(--color-neutral-700); stroke-width: 1; stroke-dasharray: 3 3; }
.chart-dot { fill: var(--chart-clicks); }
.chart-legend, .chart-readout { display: flex; flex-wrap: wrap; gap: 12px; font-size: 11px; color: var(--color-neutral-500); }
.chart-legend-item, .chart-readout-item { display: inline-flex; align-items: center; gap: 5px; }
.chart-readout-bucket { color: var(--color-text); }
.chart-swatch { width: 8px; height: 2px; border-radius: 1px; }

/* --- grids --------------------------------------------------------------- */

.grid-wrap { display: flex; flex: 1; min-height: 380px; gap: 0; }
.grid-host { flex: 1; min-width: 0; height: 100%; min-height: 380px; }
.grid-summary { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--color-neutral-500); }
/* The facet strip wraps everywhere, not on the three screens that asked for it
 * one at a time. A strip of chips in a 450px pane is the same strip in a 375px
 * phone: `.shell { overflow: hidden }`, so a facet pushed past the right edge is
 * not scrolled to, it is GONE. Three screens had already been given their own
 * copy of this rule after the same defect was reported on each (Conv. types,
 * Postbacks out, Todo); the fourth pane to be made narrow would have been the
 * fourth report. */
.facets { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
.facet { text-transform: lowercase; }
.row-actions { display: inline-flex; gap: 4px; }
.group-row { font-size: 12px; }

/* The re-theming contract, one mapping: every --ag-* the console overrides is
 * fed by a --grid-* token. themes.css owns the --grid-* set -- names on :root,
 * per-theme values through each theme's own colour tokens -- so a theme
 * re-points the grid by declaring a variable, never by adding a rule here. */
.ag-theme-quartz.grid-host {
  --ag-background-color: var(--grid-bg);
  --ag-foreground-color: var(--grid-fg);
  --ag-header-background-color: var(--grid-header-bg);
  --ag-header-foreground-color: var(--grid-header-fg);
  --ag-border-color: var(--grid-border);
  --ag-secondary-border-color: var(--grid-cell-border);
  --ag-row-border-color: var(--grid-row-border);
  --ag-odd-row-background-color: transparent;
  --ag-row-hover-color: var(--grid-row-hover);
  --ag-selected-row-background-color: var(--grid-row-selected);
  --ag-control-panel-background-color: var(--panel);
  --ag-side-bar-panel-width: 206px;
  --ag-font-family: var(--font-body);
  --ag-font-size: var(--grid-font-size);
  --ag-cell-horizontal-padding: var(--grid-cell-padding-x);
  --ag-borders: none;
  --ag-header-column-separator-display: none;
  --ag-input-focus-border-color: var(--color-accent);
  --ag-checkbox-checked-color: var(--color-accent);
  --ag-range-selection-border-color: var(--color-accent);

  /* Quartz derives every floating surface from --ag-background-color with a
   * color-mix, and Ledger hands it `transparent` on purpose: the grid body sits
   * on the page, which is the whole point of that theme. The cost was invisible
   * until a menu opened -- color-mix(in srgb, transparent, #e9e9ed 3%) is a 97%
   * transparent panel, so the column menu and the filter dropdowns showed the
   * grid rows straight through a white wash. The body stays transparent; only
   * the surfaces that FLOAT get a fill of their own. */
  --ag-menu-background-color: var(--pop);
  --ag-panel-background-color: var(--pop);
  --ag-tooltip-background-color: var(--pop);
}

.ag-theme-quartz .ag-header {
  border-bottom: 1px solid var(--hair);
  font-size: var(--grid-header-font-size);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
}

.ag-theme-quartz .ag-cell.num, .ag-theme-quartz .ag-header-cell.num .ag-header-cell-label { justify-content: flex-end; }
.ag-theme-quartz .ag-cell.num { font-variant-numeric: tabular-nums; }
.ag-theme-quartz .ag-cell.mono { font-family: var(--font-mono); }

/* A grid cell cannot hold a pill or a button -- no cellRenderer of any form
 * draws under the vendored bundle -- so a decision reads as its own text,
 * coloured by the .tone-* class the column puts on the cell, and a row action
 * reads as a word that is visibly clickable. These are the two shapes that
 * replaced the renderers; the tone colours themselves are the shared .tone-*
 * rules near the top of this file, which win over the grid's own cell colour. */
.ag-theme-quartz .ag-cell.decision-cell .ag-cell-value { overflow: hidden; text-overflow: ellipsis; }
.ag-theme-quartz .ag-cell.cell-action {
  color: var(--color-accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.ag-theme-quartz .ag-cell.cell-action:hover { color: var(--color-accent-300); }
.ag-theme-quartz .ag-floating-filter { background: var(--fibg); }
.ag-theme-quartz .ag-floating-filter-input .ag-input-field-input { border-color: var(--filine); }
.ag-theme-quartz .ag-status-bar { border-top: 1px solid var(--color-neutral-900); height: 29px; min-height: 29px; font-size: 11px; }
.ag-theme-quartz .ag-side-buttons { width: 36px; }
.ag-theme-quartz .ag-row-pinned { background: var(--grid-totals-bg); font-weight: var(--fw-medium); }

.stripes .ag-theme-quartz { --ag-odd-row-background-color: var(--grid-row-stripe); }

/* --- detail panel -------------------------------------------------------- */

.detail {
  width: 312px;
  flex: none;
  overflow-y: auto;
  padding: 11px;
  background: var(--panel);
  border-left: 1px solid var(--color-neutral-900);
}

.detail-inner { display: flex; flex-direction: column; gap: 9px; }
.detail-head { display: flex; align-items: center; justify-content: space-between; }
.detail-head h2 { margin: 0; font-size: 13px; font-weight: 500; }
.detail-fields { display: flex; flex-direction: column; }
.detail-fields .kv-k { min-width: 120px; }
.params-box { padding: 8px; background: var(--flowbg); border: 1px solid var(--color-neutral-900); border-radius: var(--r2); }

/* --- the inspector rail -------------------------------------------------- */

/* The design's right rail (README §2.1): two modes, and a 26px collapsed strip.
 * Both widths are tokens -- --w-rail / --w-rail-collapsed -- so the rail is the
 * same geometry the design pack drew rather than a number chosen here.
 *
 * It is a SIBLING of .main, not a child of .page, because it outlives the
 * screen: that is the whole difference between this and the drawer it replaces
 * on the click log. A screen re-render repaints .page and leaves the rail, and
 * the selected row survives it. */
.rail {
  display: flex;
  flex-direction: column;
  width: var(--w-rail);
  min-height: 0;
  background: var(--panel);
  border-left: 1px solid var(--color-neutral-900);
}

.rail.collapsed { width: var(--w-rail-collapsed); }
/* Collapsed shows the toggle and nothing else: the mode tabs would be two
 * controls that each need the pane they cannot show. */
.rail.collapsed .rail-body, .rail.collapsed .rail-tab { display: none; }
.rail.collapsed .rail-head { padding: 0; justify-content: center; }
.rail.collapsed .rail-toggle i { transform: rotate(180deg); }

.rail-head {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
  height: 29px;
  padding: 0 6px;
  border-bottom: 1px solid var(--color-neutral-900);
}

.rail-gap { flex: 1; }
.rail-tab { min-width: 24px; padding: 0 5px; }
.rail-tab.on { color: var(--color-accent); border-color: var(--color-accent); }
.rail-toggle { min-width: 22px; padding: 0 3px; border-color: transparent; background: transparent; }

.rail-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 9px;
}

.rail-note { margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--color-neutral-500); }
.rail-note-block { display: flex; flex-direction: column; gap: 6px; }

/* A 212px column cannot hold a 150px label beside a 36-character click id, so
 * the rail stacks its key/value rows instead of shrinking the label to the
 * point of ambiguity. The pane's own layout is untouched everywhere else --
 * this is scoped to .rail. */
.rail .kv { flex-direction: column; align-items: stretch; gap: 0; padding: 4px 0; }
.rail .kv-k, .rail .detail-fields .kv-k { min-width: 0; font-size: 11px; }
.rail .detail-head h2 { font-size: 12px; }
.rail .detail-inner { gap: 8px; }

/* --- the multi-pane work area (v2fe lane W4) ------------------------------
 *
 * Present only when the Multi-pane work area preference is on: with the flag
 * off shell.js never builds `.panes`, `.main` holds the same single
 * `<main class="page">` it always held, and none of these rules match anything.
 *
 * The panes are a FLEX row rather than a grid, and the reason is the divider: a
 * drag writes one number, `flex-grow`, onto the pane it is resizing and its
 * neighbour, where a grid template would have to be rewritten as a whole string
 * on every pointermove.
 *
 * ⚠ THE STRIP SCROLLS AND A PANE HAS A FLOOR (lane P1, e2uipolish). The pane
 * ceiling is SEVEN, and seven panes on a 1440px laptop is 205px each -- which
 * is not a grid, it is a column of ellipses. So the trade is stated here rather
 * than left to the flexbox default: a pane may not go under 360px, and when the
 * open panes no longer fit, the WORK AREA scrolls sideways instead of crushing
 * every pane the operator already had open. `overflow-y` is pinned to hidden
 * because a row of full-height panes has nothing to scroll vertically and a
 * second scrollbar in the chrome is what ruling R2 removes.
 *
 * `min-width: 0` stays on the pane as well: it is what lets the GRID inside a
 * pane shrink to its share, and the 360px floor is a `min-width` on the same
 * property, so the pair is written as one declaration.
 *
 * The floor is desktop-only and its phone reset is two rules below. A bare
 * `.pane` rule applies at 375px too, and `.narrow .pane { flex: 1 1 100% }`
 * further down app.css does not reset a min-width -- so without the reset a
 * phone would get exactly the horizontal scrollbar R2 exists to remove. */
.panes {
  display: flex;
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
}

.pane {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-width: 360px;
  min-height: 0;
}

/* THE PHONE HAS ONE PANE, so it has no floor: visiblePanes() renders the
 * focused pane alone and it takes the whole width. This rule is the other half
 * of the 360px floor above and must move with it. */
.narrow .pane { min-width: 0; }

/* The focused pane's tab strip LIGHTENS (README §2.1). It is the only signal of
 * where a nav click will open, so it is a background change rather than a
 * border: a border would move the content by a pixel as focus travelled.
 *
 * THE STRIP NO LONGER SCROLLS (lane P1, e2uipolish, ruling R2). It used to
 * declare `overflow-x: auto`, so six tabs put a horizontal scrollbar through
 * the strip and carried the split and close buttons off the right-hand end with
 * them -- the operator's "sliders". The strip is now a row of FOUR things: a
 * clipped box holding the tabs, the overflow menu, split, close. Only the first
 * of them can run out of room, and paneshell.js folds the tabs that do not fit
 * into the menu.
 *
 * `overflow: visible` is stated rather than left to the default, because the
 * obvious `overflow: hidden` here is a TRAP -- the same one the block above
 * `.menu.menu-float` records. A clipping container clips an absolutely
 * positioned descendant whose containing block is inside it, and the overflow
 * button's menu is exactly that: `.menu-wrap > .menu` inside this strip. With
 * `hidden` the menu would open and be invisible, and the click handler would
 * work the whole time. The clipping that matters is one level in, on
 * `.pane-tabs-scroll`, which has no popup inside it -- and because that box
 * shrinks rather than overflowing, this row never overflows either, so
 * `visible` costs no scrollbar. */
.pane-tabs {
  display: flex;
  align-items: center;
  gap: 3px;
  flex: none;
  height: 27px;
  padding: 0 5px;
  background: var(--panel);
  border-bottom: 1px solid var(--color-neutral-900);
  overflow: visible;
}

/* The tabs, and nothing else. `flex: 1 1 0` + `min-width: 0` is what makes this
 * the box that gives, so the three controls beside it hold the strip's right
 * edge at every pane width. `overflow: hidden` and no scrollbar: a tab past the
 * edge is FOLDED into the overflow menu, not scrolled to. */
.pane-tabs-scroll {
  display: flex;
  align-items: center;
  gap: 3px;
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
}

.pane.focused .pane-tabs { background: var(--bar); }

/* `flex: 0 1 auto` with a floor and a ceiling: a tab takes the width its label
 * needs up to 220px, gives width back until 72px -- below which a tab is a ✕
 * beside two letters -- and past that the strip folds one away instead of
 * shrinking the rest into nothing. */
.pane-tab {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 1 auto;
  min-width: 72px;
  max-width: 220px;
  height: 21px;
  padding: 0 6px;
  border: 1px solid transparent;
  border-radius: var(--r2);
  background: transparent;
  color: var(--color-neutral-400);
  font-size: 11.5px;
  cursor: pointer;
}

.pane-tab.on { color: var(--color-accent); border-color: var(--color-accent); }
/* `min-width: 0` on the label as well: a flex item defaults to `min-width:
 * auto`, which refuses to go under its own text, and the ellipsis would then
 * never appear no matter how narrow the tab was allowed to become. */
.pane-tab-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pane-tab-x { flex: none; min-width: 14px; padding: 0 1px; border-color: transparent; background: transparent; font-size: 10px; }
.pane-more { flex: none; }
.pane-tabs-more { min-width: 20px; padding: 0 2px; gap: 1px; border-color: transparent; background: transparent; }
.pane-more-caret { font-size: 9px; line-height: 1; }
.pane-more-count { font-size: 10px; }
.pane-split, .pane-close { flex: none; min-width: 20px; padding: 0 3px; border-color: transparent; background: transparent; }

.pane-head {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: none;
  min-height: 27px;
  padding: 3px 6px;
  border-bottom: 1px solid var(--color-neutral-900);
}

.pane-link { min-width: 22px; padding: 0 3px; }
.pane-link.on { color: var(--color-accent); border-color: var(--color-accent); }
.pane-range { height: 21px; font-size: 11.5px; }

/* The divider is 5px of hit area over a 1px line. A 1px target is a target
 * nobody hits; the line stays 1px so the layout does not gain a visible gutter
 * per pane. */
.pane-divider {
  flex: none;
  width: 5px;
  margin: 0 -2px;
  background: transparent;
  border-left: 1px solid var(--color-neutral-900);
  cursor: col-resize;
  z-index: 1;
}

.pane-divider:hover { border-left-color: var(--color-accent); }

/* The drop zones (v2fe lane W5).
 *
 * These three nodes live on document.body, not in the work area, and are added
 * only while a drag is in flight. The pane host is RECONCILED to exactly its
 * panes and dividers, so an overlay parented there would be removed by the next
 * render -- and a drop is a render. On the body they are `fixed`, which is also
 * why panedrag.js works in viewport coordinates throughout: one space for the
 * hit test and the paint, with no origin arithmetic between them.
 *
 * --border-drop and --z-dropzone are the design pack's own tokens for this
 * surface. The zone sits UNDER the scrim's z-index (40) on purpose: a drag
 * cannot start over a dialog, so it must never paint over one either. */
.pane-dropzone,
.pane-dropcaret,
.pane-drag-ghost {
  position: fixed;
  z-index: var(--z-dropzone);
  pointer-events: none;
}

/* --color-accent-200 rather than the --accent-tint-* family: the tints are
 * declared in the nocturne block only, and this surface has to look right under
 * all four themes. --color-accent-200 is the translucent accent every theme
 * declares -- it is what a control's `.on` background and the palette's hit row
 * already use. */
/* --border-drop is the pack's own dropzone border and it is declared in the
 * .th-nocturne block only -- the composite borders are colour-bearing, so W1T's
 * ruling keeps them out of :root where th-day would inherit them. The fallback
 * is the same declaration written out, so the three themes that predate the
 * pack get the identical 1.5px dashed accent instead of no border at all. Its
 * only colour comes from --color-accent, which every theme declares. */
.pane-dropzone {
  border: var(--border-drop, 1.5px dashed var(--color-accent));
  border-radius: var(--r2);
  background: var(--color-accent-200);
}

/* The tab-strip zone is the insertion CARET's backdrop rather than a target in
 * its own right, so it is the quietest of the three: the caret says where, the
 * outline only says which strip. */
.pane-dropzone.on-tabstrip { background: transparent; }

.pane-dropcaret {
  background: var(--color-accent);
  border-radius: 1px;
}

/* The ghost carries the tab's label and nothing else -- it is a label under the
 * pointer, not a screenshot of the tab. The row gesture has no ghost of its own
 * here: the vendored grid renders `.ag-dnd-ghost`, and a second one following
 * the same pointer would read as two drags. */
.pane-drag-ghost {
  max-width: 220px;
  padding: 2px 7px;
  border: 1px solid var(--color-accent);
  border-radius: var(--r2);
  background: var(--pop);
  color: var(--color-text);
  font-size: 11.5px;
  line-height: 17px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0.95;
}

/* The nav's hover split icon. It is positioned rather than laid out so it
 * cannot change the nav item's height or push the label -- an affordance that
 * reflows the row it appears in reads as a rendering fault. */
.nav-item { position: relative; }

.nav-split {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  min-width: 18px;
  padding: 0 2px;
  border-color: transparent;
  background: transparent;
  opacity: 0;
  font-size: 12px;
}

.nav-item:hover .nav-split, .nav-split:focus-visible { opacity: 1; }

/* ...but "cannot push the label" is only half of it: the icon is drawn OVER the
 * label's last 22px, and a long label runs underneath it. In Russian the two
 * longest items -- «Исходящие постбеки», «Мёртвые сообщения» -- end within a
 * few pixels of where the icon starts. So the label folds, and the item
 * reserves the icon's corner. `:has(.nav-split)` because the button exists only
 * when the multi-pane work area is on: with the flag off the nav keeps every
 * pixel of its label, exactly as it shipped. */
.nav-item > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item:has(.nav-split) { padding-right: 27px; }

/* The rail's Layouts panel. 212px wide, so the row stacks. */
.layout-list { display: flex; flex-direction: column; gap: 7px; margin-top: 8px; }
.layout-save { display: flex; gap: 4px; align-items: center; }
.layout-save .layout-name { flex: 1; min-width: 0; height: 22px; font-size: 11.5px; }

.layout-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px 6px;
  border: 1px solid var(--color-neutral-900);
  border-radius: var(--r2);
}

.layout-apply {
  justify-content: flex-start;
  height: auto;
  padding: 0;
  border: 0;
  background: transparent;
  text-align: left;
}

.layout-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.layout-meta { display: flex; gap: 6px; }
.layout-acts { display: flex; gap: 4px; align-items: center; }

/* --- estate2-copy (W6/W7) ------------------------------------------------ */

/* The layouts rail's copy control. The rail is 212px wide and .layout-row is
   already a column, so the picker stacks under the row it belongs to rather
   than fighting the name for the same line; without the shorter input it is
   cramped enough to clip. Both classes ship from lane W7, which owns no CSS
   file (§6 gives app.css to nobody), so the rule lands here at CP1. */
.layout-copy { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; }
.layout-copy .input { height: 22px; font-size: 11.5px; }

/* The two selection counts in a screen head. Both are a bare span around a
   pill(), and .screen-head is a flex row, so both render correctly with no rule
   at all -- these only stop the pill stretching to the row's height and give it
   the same baseline as the button beside it. W7's todo count and W6's
   automation count, styled once because they are the same control. */
.todo-copy-count { display: inline-flex; align-items: center; }
.screen-head .automation-copy-count { flex: none; display: inline-flex; align-items: center; }

/* --- outbound attempts, conversion detail -------------------------------- */

/* One chain per group: (revision, destination_id). attempt_number restarts at 1
   inside each, so ungrouped rows show two chains both beginning at "#1" and a
   legitimate retry reads as a duplicate send. */
.attempt-group { display: flex; flex-direction: column; gap: 4px; }
.attempt-group + .attempt-group { margin-top: 9px; }
.attempt-group .params-box { display: flex; flex-direction: column; gap: 2px; }
.attempt-line > summary { cursor: pointer; }
/* The native marker sits outside the flex row and pushes the first cell out of
   line with its siblings; list-style removes it and the summary keeps the same
   .kv geometry as every other row in the pane. */
.attempt-line > summary::-webkit-details-marker { display: none; }
.attempt-line > summary { list-style: none; }
.attempt-line[open] > summary { color: var(--color-text); }
.attempt-line .kv-k { min-width: 42px; }
.attempt-line .kv-v { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.attempt-line .params-box { margin: 4px 0 6px 42px; }
/* A rendered_url is long and is the reason the pane exists; it wraps rather
   than truncating, because the interesting part is often the tail. */
.attempt-line .params-box .kv-v { overflow-wrap: anywhere; }

/* The Conversions screen's inbound-postback guide. A <details> for the same
   reason an attempt line is one: it is a reference an operator opens when a
   partner is being onboarded, not something they read past on every visit.
   Structure only -- every colour it shows belongs to the .kv, .flow-card and
   .params-box rules it reuses. */
.postback-guide > summary { cursor: pointer; }
.postback-guide > summary::-webkit-details-marker { display: none; }
.postback-guide > summary { list-style: none; }
.postback-guide > .detail-params { margin-top: 8px; gap: 8px; }
/* The endpoint and the example are long single-line values; they wrap rather
   than truncating, because the tail is the part that differs between two
   networks. */
.postback-guide .kv-v { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; overflow-wrap: anywhere; }

/* --- flows, campaign detail ---------------------------------------------- */

/* min-height, and it wraps. As a fixed 31px nowrap row, three Russian tab
 * labels -- «Типы конверсий · Исходящие постбеки · CAPI» measures ~420px -- did
 * not fit a 450px pane once its padding is taken off, and
 * `.shell { overflow: hidden }` means the third one is not scrolled to, it is
 * gone. One row is still exactly 31px. */
.tabs { display: flex; flex-wrap: wrap; gap: 2px; min-height: 31px; border-bottom: 1px solid var(--color-neutral-900); }
.tab { display: inline-flex; align-items: center; padding: 0 11px; font-size: 12px; white-space: nowrap; color: var(--color-neutral-400); border-bottom: 2px solid transparent; }
.tab.on { color: var(--color-text); border-bottom-color: var(--color-accent); }
.tab-body { display: flex; flex-direction: column; gap: var(--page-gap); flex: 1; min-height: 0; }

.url-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* flex-BASIS 240px with min-width:0, not min-width:240px. The strip already
 * wraps, so 240px is the width the code box asks for; as a hard minimum it was
 * a floor the box could not go under, and in a 450px pane -- three panes on a
 * 1440px screen -- the box plus its Copy button pushed past
 * `.shell { overflow: hidden }` and the end of the URL was simply not there. */
.url-bar code { flex: 1 1 240px; min-width: 0; padding: 6px 8px; background: var(--flowbg); border: 1px solid var(--color-neutral-900); border-radius: var(--r2); overflow-wrap: anywhere; }

/* Every `<code class="mono">` breaks inside a long unbroken token, wherever it
 * is. A URL, a token or an endpoint path has no space in it to break at, so
 * without this the code box does not wrap -- it sets its own min-content width
 * to the whole string and pushes the card, the pane or the page open around it.
 * campaigns.js:900, networks.js:602 and postbackguide.js:198 each build one. */
code.mono { overflow-wrap: anywhere; }

.flows { display: flex; flex-direction: column; gap: 8px; }

.flow-card { display: flex; flex-direction: column; gap: 6px; padding: 9px 11px; background: var(--flowbg); border: 1px solid var(--color-neutral-900); border-radius: var(--r2); }
.flow-default { border-style: dashed; }
.flow-head { display: flex; align-items: center; gap: 8px; }
.flow-index { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; font-size: 11px; color: var(--color-neutral-500); border: 1px solid var(--color-neutral-800); border-radius: var(--r2); }
/* The foot is a row now: a rotation split can carry a state pill beside it, and
 * on a 375px card the two wrap rather than pushing the card past the screen. */
.flow-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 11.5px; }
/* Secondary lines under the foot -- a destination's operator notes, the entries
 * of a rotation that is not the headline, and the drained/off consequence. They
 * wrap inside the card; a destination id is a single unbroken token and would
 * otherwise widen it. */
.flow-note { font-size: 11.5px; overflow-wrap: anywhere; }
.arrow { color: var(--color-neutral-600); }

/* --- forms and drawer ---------------------------------------------------- */

.drawer-scrim { position: fixed; inset: 0; z-index: 50; display: flex; justify-content: flex-end; background: rgba(0, 0, 0, 0.42); }

.drawer {
  display: flex;
  flex-direction: column;
  width: min(560px, 100%);
  height: 100%;
  background: var(--panel);
  border-left: 1px solid var(--color-neutral-800);
}

.drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; border-bottom: 1px solid var(--color-neutral-900); }
.drawer-head h2 { margin: 0; font-size: 14px; font-weight: 500; }
.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px; }
.drawer-foot { display: flex; gap: 8px; justify-content: flex-end; padding: 11px 14px; border-top: 1px solid var(--color-neutral-900); }
.confirm-text { margin: 0; font-size: 12.5px; }

/* --- the command palette ------------------------------------------------- */

/* z-index 55, between the drawer's 50 and the toast's 60. The palette closes
 * FIRST on Escape (shell.js ESCAPE_ORDER), so it has to sit above the drawer;
 * the toast stays above everything because it never takes input. */
.palette-scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 9vh;
  background: rgba(0, 0, 0, 0.42);
}

.palette {
  display: flex;
  flex-direction: column;
  width: min(var(--w-palette), calc(100% - 32px));
  max-height: 62vh;
  overflow: hidden;
  background: var(--pop);
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--r);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.34);
}

.palette-head {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: none;
  height: 38px;
  padding: 0 12px;
  border-bottom: 1px solid var(--color-neutral-900);
}

.palette-head i { color: var(--color-neutral-600); font-size: 15px; }

.palette-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: 0;
  background: transparent;
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 13px;
  outline: none;
}

.palette-list { flex: 1; min-height: 0; overflow-y: auto; padding: 5px; }
.palette-group { padding: 7px 8px 3px; font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600); }
.palette-note { margin: 0; padding: 10px 8px; font-size: 11.5px; line-height: 1.5; color: var(--color-neutral-500); }

.palette-row {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 26px;
  padding: 0 8px;
  border-radius: var(--r2);
  font-size: 12.5px;
  cursor: pointer;
}

.palette-row i { color: var(--color-neutral-500); font-size: 14px; }
.palette-row:hover { background: var(--row-hover); }
.palette-row.on { background: var(--color-accent-200); }
/* A campaign token is a single unbroken string and the only variable-width
 * thing in the row; it takes the slack and ellipsises rather than pushing the
 * hint out of the dialog. */
.palette-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.palette-hint { flex: none; font-size: 11px; color: var(--color-neutral-600); }

.palette-foot {
  display: flex;
  gap: 14px;
  align-items: center;
  flex: none;
  height: 26px;
  padding: 0 12px;
  border-top: 1px solid var(--color-neutral-900);
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--color-neutral-600);
}

.form { display: flex; flex-direction: column; gap: 11px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-inline { flex-direction: row; align-items: center; gap: 6px; }
.inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.row-list { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.mappings, .kv-rows, .rule-cards, .filter-rows { display: flex; flex-direction: column; gap: 6px; width: 100%; }
.mapping-row, .kv-row, .filter-row { display: flex; align-items: center; gap: 6px; }
.mapping-row .input, .kv-row .input { flex: 1; }

.rule-card { display: flex; flex-direction: column; gap: 6px; padding: 8px; background: var(--flowbg); border: 1px solid var(--color-neutral-900); border-radius: var(--r2); }
.rule-card.dragging { opacity: 0.5; }
.rule-head, .rule-condition { display: flex; align-items: center; gap: 6px; }
.rule-grip { cursor: grab; color: var(--color-neutral-600); }

/* rotation-truth lane B: a rule whose Send-to names a landing this document no
   longer declares. Publish rejects it; until then the drawer keeps showing the
   id rather than snapping the rule onto something that resolves.

   Both rules are written at 0,3,0 ON PURPOSE. A bare .tone-bad is 0,1,0 and
   loses to .input (which sets its own colour ~430 lines further down) and to
   .warn-inline (which sets amber ~215 lines down) -- so the obvious markup
   would have passed every source scan in this suite and painted nothing.
   Colour is not the whole signal either: the option reads "unknown landing" and
   the line beneath carries the rejection the server will give. */
.rule-card .input.is-dangling { color: var(--bad); border-color: var(--bad); }
.rule-card .warn-inline.tone-bad { color: var(--bad); }

.check-result { font-size: 11.5px; }
.sim-output { display: flex; flex-direction: column; gap: 6px; }

.issues { display: flex; flex-direction: column; gap: 4px; }
.issue { display: flex; gap: 10px; font-size: 12px; border-bottom: 1px solid var(--line2); padding: 3px 0; }
.issue-field { min-width: 240px; color: var(--warn); }

.appearance { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; }

/* --- report -------------------------------------------------------------- */

.report-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: var(--page-gap); flex: 1; min-height: 0; }
.report-builder { display: flex; flex-direction: column; gap: var(--page-gap); overflow-y: auto; }
.report-result { display: flex; flex-direction: column; min-height: 0; }
.metric-list { display: flex; flex-direction: column; gap: 4px; }

/* --- login --------------------------------------------------------------- */

.login-shell { display: block; }
.login { display: flex; align-items: center; justify-content: center; height: 100%; background: var(--color-bg); }
.login-card { display: flex; flex-direction: column; gap: 12px; width: 300px; padding: 22px; background: var(--color-surface); border: 1px solid var(--color-neutral-900); border-radius: var(--r); }
.th-ledger .login-card { border: 0; border-top: 1px solid var(--hair); }
.brand-login { padding: 0 0 6px; }
.login-error { font-size: 12px; color: var(--bad); }
.login-submit { justify-content: center; }

/* --- mobile / Mini App --------------------------------------------------- */

.mtabs { display: none; }

/* THE collapse rule, and it is a DESCENDANT selector for a reason.
 *
 * `narrow` is written onto <body> by state.js applyAppearance(); the shell
 * element is `#app.shell` INSIDE it. The rule here used to be
 * `.narrow.shell, .shell.narrow` -- two compound selectors that each require
 * both classes on the SAME element, which never happens. So the shell kept its
 * desktop `196px 1fr` template while `.narrow .sidebar { display: none }` took
 * the sidebar out of the layout, and `.main` auto-placed into the surviving
 * FIRST track: the whole application rendered in a 196px column with the 1fr
 * track empty. The descendant rules below still matched, which is why the page
 * looked half-mobile rather than obviously broken.
 *
 * Descendant, not a class mirrored onto #app, because every other rule in this
 * block is already `.narrow <descendant>` -- one mechanism, not two.
 * tests/narrow-shell.test.js guards the shape. See phase7-repairs/003. */
.narrow .shell { grid-template-columns: 1fr; }

.narrow .sidebar { display: none; }
/* The rail goes with the sidebar, and for the same arithmetic: 212px of a
 * 375px viewport is not an inspector beside the work, it IS the work. The phone
 * keeps the full-screen `.detail` sheet the card lists have always opened. */
.narrow .rail { display: none; }

/* A NARROW VIEWPORT COLLAPSES TO ONE PANE, and the collapse is done in the pane
 * shell rather than here: visiblePanes() returns only the focused pane, and
 * render() DESTROYS the grids of the panes that left the layout. Hiding them
 * with CSS instead would leave four grids alive in nodes with no width -- which
 * under the vendored bundle is the empty-cell failure this whole lane is built
 * around, and it would leave four ClickHouse reads refreshing behind a screen
 * nobody can see.
 *
 * So these two rules are the geometry only. The divider and the split verb go
 * with the second pane, because at 375px there is no second pane to drag against
 * or open into -- and the same 320px arithmetic that froze the tab bar at five
 * froze this at one. */
.narrow .panes { display: flex; }
.narrow .pane { flex: 1 1 100%; }
.narrow .pane-divider, .narrow .pane-split, .narrow .pane-close { display: none; }

/* Tall enough to hold 44px controls -- see the hit-target block below. */
.narrow .topbar { height: 54px; }
.narrow .topbar h1 { overflow: hidden; text-overflow: ellipsis; }
.narrow .controls .search { width: 120px; }
.narrow .page { padding: 10px 12px 14px; gap: 10px; padding-bottom: 76px; }
.narrow .row-2 { grid-template-columns: 1fr; }
.narrow .report-layout { grid-template-columns: 1fr; }
.narrow .dl { grid-template-columns: 1fr; }
.narrow .kv-k { min-width: 108px; }
.narrow .mini-row { font-size: 11.5px; }
.narrow .grid-wrap { min-height: 320px; }

/* The action button in a screen head drops to its own full-width row rather
 * than squeezing the title line. */
.narrow .screen-head { flex-wrap: wrap; }
.narrow .screen-head .ctl:last-child { flex: 1 0 100%; margin-left: 0; justify-content: center; }

/* 2x2 KPIs on mobile: Clicks · Visitors · Fallback % · Freshness (spec §5).
 *
 * Membership is a per-entry flag read by kpiStrip(), not a position. The rule
 * this replaces hid `:nth-child(n+5)` of a strip ordered CLICKS, VISITORS,
 * BOTS, ALLOWED, FALLBACKS, FRESHNESS -- so a phone showed Bots and Allowed and
 * hid the two the spec mandates, while the comment above it claimed otherwise.
 * `kpis-subset` is only present when a strip has actually been curated, so an
 * uncurated strip still shows everything rather than nothing. */
.narrow .kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
.narrow .kpis-subset .kpi:not(.kpi-mobile) { display: none; }
.narrow .kpi { border-left: 0; border-top: 1px solid var(--color-neutral-900); padding: 8px 10px; }
/* The right-hand column of the 2x2, marked from the ordinal WITHIN the subset.
 * nth-child cannot do this: it counts the tiles that display:none removed. */
.narrow .kpi-mobile-right { border-left: 1px solid var(--color-neutral-900); }

/* Tiles whose phone figure differs from their desktop one render both and let
 * the breakpoint choose, so crossing it needs no re-render. */
.kpi-narrow { display: none; }
.narrow .kpi-wide { display: none; }
.narrow .kpi-narrow { display: block; }

/* The config editors are desktop-only: they are multi-section forms with rule
 * builders and condition chips, and no phone design exists for them. The
 * affordances carry `desktop-only` and the explanation carries `narrow-note`,
 * both switched by CSS so a live resize is enough -- this replaces a comment
 * here that described a read-only mobile drawer nothing implemented. */
.narrow .desktop-only { display: none; }
.narrow-note { display: none; }
.narrow .narrow-note { display: block; }

.narrow .drawer { width: 100%; }
/* The click detail is a full-screen push sheet on a phone. The bottom padding
 * clears the fixed tab bar so the last field is reachable. */
.narrow .detail { position: fixed; inset: 0; z-index: 45; width: 100%; padding-bottom: 76px; }

/* Hit targets ≥44px. 28px is a desktop density; the same controls are tapped
 * with a thumb here. */
.narrow .ctl { min-height: 44px; }
.narrow .ctl-icon { min-width: 44px; justify-content: center; }
.narrow .menu-item { min-height: 44px; display: flex; align-items: center; }

/* --- mobile card lists --------------------------------------------------- */

/* Below the breakpoint the tab screens render cards instead of grids: a grid at
 * 375px is a tool rail plus a horizontally scrolling stub of a table. */
.card-list { display: flex; flex-direction: column; }

.list-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 44px;
  width: 100%;
  padding: 9px 2px;
  border: 0;
  border-bottom: 1px solid var(--line2);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.list-card:hover { background: var(--row-hover); }
.list-card-top { display: flex; align-items: baseline; gap: 8px; }
.list-card-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-card-metric { flex: none; font-variant-numeric: tabular-nums; }
.list-card-sub { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 11.5px; }
.list-card-sub > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The totals wrap rather than clip. Three labelled figures do not fit one
 * 274px line at 320px, and `.shell { overflow: hidden }` means an overflowing
 * line is not scrolled to -- it is simply gone. */
.list-total { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; padding: 9px 2px; border-top: 1px solid var(--color-neutral-800); font-size: 12px; }
.list-total-label { flex: 1 0 auto; }
.list-total-item { flex: none; font-variant-numeric: tabular-nums; }

.narrow .mtabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  height: 62px;
  padding-bottom: 6px;
  background: var(--bar);
  border-top: 1px solid var(--color-neutral-900);
}

.mtab {
  flex: 1;
  min-width: 53px;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  color: var(--color-neutral-500);
}

.mtab-icon i { font-size: 20px; }
.mtab .mtab-fill { display: none; }
.mtab.on { color: var(--color-accent); }
.mtab.on .ph { display: none; }
.mtab.on .mtab-fill { display: inline; }
.mtab-label { font-size: 9.5px; }

/* --- density ------------------------------------------------------------- */

.density-compact { font-size: 12px; }
.density-compact .ctl { height: 26px; }
.density-compact .card-body { padding: 7px 9px 9px; }
.density-compact .kpi { padding: 7px 11px; }

/* --- Simulator ------------------------------------------------------------
 * The pipeline diagram follows the CAPI dashboard's construction but uses this
 * console's tokens: --ok/--warn/--bad exist in both themes, --unknown does not,
 * so an unread tier borrows a neutral and is drawn dashed. Dashed-and-grey is
 * the whole point: a tier the harness could not read must never be mistaken for
 * one that failed.
 */
.sim-diagram-wrap { width: 100%; overflow-x: auto; }
.sim-diagram { width: 100%; min-width: 620px; height: auto; }

.sim-box { stroke-width: 1.5; cursor: pointer; }
.sim-green { fill: color-mix(in srgb, var(--ok) 16%, transparent); stroke: var(--ok); }
.sim-amber { fill: color-mix(in srgb, var(--warn) 16%, transparent); stroke: var(--warn); }
.sim-red { fill: color-mix(in srgb, var(--bad) 16%, transparent); stroke: var(--bad); }
.sim-unknown {
  fill: color-mix(in srgb, var(--color-neutral-600) 12%, transparent);
  stroke: var(--color-neutral-600);
  stroke-dasharray: 4 3;
}

.sim-node:focus-visible { outline: none; }
.sim-node:focus-visible .sim-box { stroke: var(--color-accent); stroke-width: 2.5; }

.sim-node-label { fill: var(--color-text); font-size: 13px; font-weight: 600; pointer-events: none; }
.sim-node-detail { fill: var(--color-neutral-400); font-size: 10.5px; pointer-events: none; }
.sim-edge { stroke: var(--color-neutral-600); stroke-width: 1.5; fill: none; marker-end: url(#sim-arrow); }
.sim-edge-label { fill: var(--color-neutral-500); font-size: 10px; }
#sim-arrow path { fill: var(--color-neutral-600); }

.sim-node-readout { margin-top: 10px; font-size: 0.85rem; min-height: 1.2em; }
.sim-node-readout .pill { margin-left: 6px; }

.sim-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.sim-num { width: 96px; }
.sim-late { min-height: 40px; }

@media (max-width: 720px) {
  .sim-controls { gap: 8px; }
  .sim-controls .field { flex: 1 1 40%; }
}

/* A grid placed inside a card has no definite height to inherit. `.grid-wrap`
 * takes its 380px from min-height, and a min-height does NOT resolve a child's
 * `height: 100%` -- so AG Grid's own wrapper resolved to zero and the grid
 * rendered a header and a "Rows: N" status bar over an empty body. Outside a
 * card the wrapper is a stretched flex item with a real height, which is why
 * every other screen was unaffected. An explicit height is definite and
 * resolves the whole chain down to .ag-body-viewport. */
.card .grid-wrap { height: 380px; }
.narrow .card .grid-wrap { height: 320px; }

/* --- v2fe lane W2A: the pane statusbar and the honest-gap treatment --------
 *
 * Three elements the v2 pack draws that this console had no class for. Every
 * value here is a token, so both themes carry them without a second block.
 *
 *   .statusbar    the pack's pane foot: one line under a table saying what the
 *                 table's own numbers mean (Trends, Create report).
 *   .cell-pending a grid column the design specifies and no endpoint feeds. It
 *   .col-pending  renders an em dash in every cell -- ruling 9: designed
 *                 treatment, explicit absence, never a fabricated number. The
 *                 grid rules are scoped inside .grid-host because the vendored
 *                 theme sets cell colour on its own selectors.
 *   .metric-pending  the same idea in the report builder's checkbox list.
 */

/* ⚠⚠ THE CLOSING BRACE BELOW WAS MISSING, AND ITS ABSENCE HAD NO FAILURE MODE.
 *
 * Found 2026-08-11 while adding the Quarantine rules at the end of this file:
 * they parsed, they were in document.styleSheets, and they did nothing. A CSS
 * block that is never closed does not error -- with native nesting, every rule
 * written after it becomes a DESCENDANT rule of the unclosed selector. So the
 * whole tail of this stylesheet, from here to the last line, was being applied
 * as `.statusbar .note-info`, `.statusbar .detail-headline`, `.statusbar
 * .cell-pending` and so on: live, valid, and matching nothing an operator ever
 * saw, because none of those elements is ever inside a .statusbar.
 *
 * Dead for as long as it has been here: .note-info, .note-rules,
 * .detail-headline, .detail-params, .screen-foot, .diff-row, .metric-pending,
 * .cell-pending and .pend's own siblings. Exactly the family of defect the dark
 * theme's own tripwire suite was written about -- "a CSS property that is
 * simply absent has no failure mode -- no error, no warning, no console
 * message, and that is precisely how this one shipped".
 */
.statusbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 12px;
  padding: 6px 2px 0;
}

/* --- v2 lane W2D: Status, Versions, Users, Todo, Simulator ---------------- */

/* The "designed, but not measured" marker. It sits in a card header or a screen
 * head and never inside a cell: what it states is true of a whole block, and
 * repeating it per row would read as a per-row failure rather than as a gap in
 * what this estate measures. */
.pend {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 1px 7px;
  font-size: 10.5px;
  color: var(--warn);
  border: 1px solid var(--warn);
  border-radius: var(--r2);
  white-space: nowrap;
}
.pend i { font-size: 12px; }
.th-ledger .pend { border: 0; padding: 0; }

/* A component cell carries the name and what the component IS, on two lines.
 * The mini-row default clips every child to one line, which is right for the
 * other five columns and wrong for this one. */
.mini-row > .comp { display: flex; flex-direction: column; white-space: normal; line-height: 1.35; }
.comp-detail { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The break-glass account is the one row on the Users screen with different
 * rules -- it cannot be edited, disabled or deleted -- so it reads as different
 * before it is read at all. */
.row-break-glass { background: var(--flowbg); }
.row-break-glass .mono { font-weight: 500; }

/* One line of the draft's field-path diff: kind, path, and the value change. */
.diff-row {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) minmax(0, 1fr);
  gap: 9px;
  align-items: baseline;
  padding: 3px 0;
  border-bottom: 1px solid var(--line2);
  font-size: 12px;
}
.diff-kind { font-size: 11px; white-space: nowrap; }
.diff-path, .diff-value { overflow-wrap: anywhere; }
.narrow .diff-row { grid-template-columns: 1fr; gap: 2px; }

/* The design's per-screen status bar: what this screen counted on the left, and
 * where its numbers came from on the right. */
.screen-foot {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line2);
  font-size: 11.5px;
  color: var(--color-neutral-500);
}

.statusbar .sp { flex: 1 1 0; min-width: 0; }

.ag-theme-quartz.grid-host .cell-pending,
.ag-theme-quartz.grid-host .col-pending {
  color: var(--color-neutral-600);
}

.metric-pending { color: var(--color-neutral-600); }
.metric-pending .pill { margin-left: auto; }

/* Run and CSV sit at the FOOT of the builder rail, as drawn. */
.report-builder .row-actions { margin-top: auto; padding-top: 4px; }

/* --- v2 logs re-skin (lane W2B) -------------------------------------------
 * Additive only: three classes the Click log and Conversions needed, and no
 * change to any existing rule. Every value is a token, so both themes follow
 * without a second block.
 */

/* The design's info strip: an accent glyph beside a wrapping sentence. The
 * glyph is flex:none so a long sentence wraps under itself rather than around
 * it, and it is aligned to the first line rather than centred. */
.note-info { display: flex; align-items: flex-start; gap: 6px; }
.note-info > i { flex: none; color: var(--color-accent); font-size: 13px; line-height: 1.45; }

/* The block of honest sentences beside a grid -- what the grid cannot do, said
 * where an operator would otherwise assume it could. One sentence per line. */
.note-rules { display: flex; flex-direction: column; gap: 3px; }

/* A GATED cell: a column the design draws and no endpoint can fill. It is
 * dimmer than an ordinary em dash on purpose, so an absent COLUMN reads
 * differently from an absent VALUE in the column beside it. */
.ag-theme-quartz .ag-cell.cell-pending { color: var(--color-neutral-600); }

/* The detail pane's headline identifier. A click id and a conversion key are
 * single unbroken tokens and would otherwise widen the 312px pane. */
.detail-headline { font-size: 12.5px; line-height: 1.4; overflow-wrap: anywhere; }

/* .detail-params carried NO rule at all until now -- it was a bare block whose
 * children happened to stack. It is used by these two panes and nowhere else
 * (grep), so giving it a column is additive. Children stretch, which is what
 * puts the reveal and open buttons across the pane the way the design rail
 * draws them. */
.detail-params { display: flex; flex-direction: column; gap: 6px; }

.screen-foot > :last-child { margin-left: auto; text-align: right; }
.narrow .screen-foot { flex-wrap: wrap; }
.narrow .screen-foot > :last-child { margin-left: 0; text-align: left; }

/* --- Quarantine ----------------------------------------------------------
 *
 * One card per error_class, and the geometry follows the reading order the
 * screen was built for: what the class is, how big it is, how many DISTINCT
 * identifiers it spans, then the sentence, then the values themselves.
 *
 * Every colour here is a token. The head borrows .kpi-figure for the count so
 * the number an operator scans for is the same size as a KPI's, because during
 * an incident this card IS the KPI. */
.quarantine-class-head { display: flex; align-items: baseline; gap: 8px; }
.quarantine-class-facts { display: flex; flex-wrap: wrap; gap: 2px 18px; }
.quarantine-class-facts .kv { gap: 6px; }
.quarantine-class-facts .kv-k { min-width: 0; }
.quarantine-values { display: flex; flex-direction: column; gap: 4px; }
/* The value chips WRAP. .facets does not by default -- the type facet it was
 * borrowed from is six short words -- and five 36-character subids ran straight
 * off the right edge of the card, taking the fifth offender with them. */
.quarantine-values .facets { flex-wrap: wrap; row-gap: 4px; }
.quarantine-class-actions { display: flex; gap: 6px; }

/* A repeated identifier is a BUTTON, because the whole point of naming it is
 * that the next click shows the rows behind it. overflow-wrap so a 36-character
 * UUID does not widen the card past the page. */
.quarantine-value { max-width: 100%; overflow-wrap: anywhere; text-align: left; }

/* The loud verdict line. It is the one sentence on the screen that changes an
 * operator's diagnosis, so it is the estate's warning colour and full size
 * rather than the muted 11.5px every other .note carries. */
.note-loud { color: var(--warn); font-size: 12.5px; line-height: 1.45; }

/* --- lane A: Conv. types, and the transition graph beside it ---------------
 *
 * The graph is a SECOND PANE INSIDE ONE SCREEN, not a pane of the pane shell,
 * so its split lives here rather than in the shell's own geometry. Grid left,
 * drawing right, stacked under 760px where a 40% rail would be 120px wide.
 *
 * ⚠ EVERY TOKEN BELOW IS DECLARED BY ALL FOUR THEMES, which is not true of the
 * two the design mock reaches for. `--color-accent-900` exists in .th-nocturne
 * and nowhere else: the other three themes would resolve it to nothing and the
 * node would paint with NO fill at all -- no error, no warning, and an edge
 * running visibly through every box. It is mapped to --color-accent-200, the
 * accent BACKGROUND token every theme declares (and which the dark themes
 * declare as a translucent tint, which is why .g-node-base exists below).
 * --color-neutral-800, the mock's other reach, does resolve in all four and is
 * kept as written.
 */
.ct-split { display: flex; flex: 1; min-height: 0; gap: 12px; align-items: stretch; }
.ct-split > .grid-wrap { min-width: 0; }
.ct-graph { flex: 0 0 clamp(260px, 34%, 480px); min-width: 0; overflow: auto; }
.ct-graph > .card { height: 100%; }
.narrow .ct-split { flex-direction: column; }
.narrow .ct-graph { flex: none; width: 100%; }

.g-graph { display: block; width: 100%; height: auto; }

/* The node's opaque ground. The tint that goes over it is translucent under the
 * dark themes, and an edge that skips a column passes through whatever node
 * stands between its ends. */
.g-node-base { fill: var(--color-bg); }

.g-edge { stroke: var(--color-neutral-600); fill: none; stroke-width: 1.2; }
.g-arrow { fill: var(--color-neutral-600); }
.g-node { fill: var(--color-accent-200); stroke: var(--color-accent); }
.g-node.term { fill: var(--color-surface); stroke: var(--color-neutral-800); stroke-dasharray: 3 3; }
.g-label { fill: var(--color-text); font-family: var(--font-mono); font-size: 11px; }
.g-label.term { fill: var(--color-neutral-500); }
.g-legend { fill: var(--color-neutral-600); font-size: var(--fs-xs); }

/* A node opens its editor, so it is focusable and says so when focused. The
 * ring is drawn on the node's own stroke rather than as an outline: an SVG
 * outline on a <g> boxes the whole group including the label's overflow. */
.g-node-hit { cursor: pointer; }
.g-node-hit:focus { outline: none; }
.g-node-hit:focus-visible .g-node,
.g-node-hit:focus-visible .g-node.term { stroke: var(--color-accent); stroke-width: 2; }
/* --- lane e2s-f3: the draft change count in the top bar -------------------
 *
 * OUTSIDE .controls, exactly like .lang-switch and for the same two reasons:
 * setControls() shows and hides every member of that strip per screen, and this
 * readout is true on all of them; and .controls is a scroll container in both
 * axes, which is the clip the range menu had to leave. flex-shrink: 0 keeps the
 * pill off the strip's scroll axis rather than letting the strip squash it.
 *
 * HIDDEN ON A NARROW LAYOUT, like the sidebar and the inspector rail. Config
 * editing is a desktop surface by contract -- every edit column carries
 * `hide: state.narrow` -- so a change count on a 320px viewport would be a
 * readout for a thing that width cannot reach, and it would take its width out
 * of the one control strip a phone operator has. shell.js skips the two
 * requests behind it there too; this rule is only the paint half. */
.draft-count { flex-shrink: 0; white-space: nowrap; }

/* Muted at zero, not gone at zero: "nothing is staged" is a fact worth
 * confirming, and a pill that disappeared would be indistinguishable from one
 * whose count never loaded. Transparent border rather than a second colour, so
 * the resting state reads as quiet without inventing a tone. */
.draft-count.draft-count-quiet { color: var(--color-neutral-500); border-color: transparent; }

.narrow .draft-count { display: none; }
/* --- Automation (lane C3) ------------------------------------------------
 *
 * Four blocks, and every colour is a token.
 *
 * The scope violation is the one cell on this grid that is neither safe nor
 * working -- the engine refuses the WHOLE PASS while it stands, so one bad rule
 * stops every other rule being evaluated. Colour alone would put it in the same
 * visual class as an ordinary bad value, so it also carries weight and a left
 * marker: the operator has to be unable to scan past it. */
.ag-theme-quartz .ag-cell.cell-scope-violation {
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--bad);
}

/* The pane's state pill sits on its own line under the rule id, so it reads as
 * the rule's status rather than as a suffix of its name. */
.automation-state { display: flex; align-items: center; gap: 6px; }

/* condition -> action -> reverse, stacked in the 312px pane. Stacked rather
 * than side by side on purpose: the condition is a sentence, and three
 * sentences in three columns of a narrow pane are three columns of one word. */
.automation-flow { display: flex; flex-direction: column; gap: 6px; }
.automation-flow-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 7px 8px;
  background: var(--flowbg);
  border: 1px solid var(--color-neutral-900);
  border-radius: var(--r2);
}
.automation-flow-text { font-size: 12.5px; line-height: 1.4; overflow-wrap: anywhere; }

/* One ledger line. The evidence is the half an operator copies, so it wraps
 * inside the pane rather than widening it -- the same rule .detail-headline
 * follows for a click id. */
.automation-ledger-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px 0;
  border-bottom: 1px solid var(--color-neutral-900);
}
.automation-ledger-head { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }
.automation-ledger-evidence { font-size: 11.5px; line-height: 1.35; overflow-wrap: anywhere; }
.automation-ledger-counters { display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 11px; }
/* --- lane D4: Dead letters ------------------------------------------------
 *
 * Four rules, and three of them exist because a shared class was almost right.
 *
 * The queue switch borrows .facet, which is `text-transform: lowercase` -- and
 * the labels on it are QUEUE NAMES, which an operator copies out of the console
 * and types at a broker. A lowercased identifier is a wrong identifier, and it
 * would look deliberate. So this strip opts out of the transform, and only this
 * strip; the type and table facets elsewhere are English words and stay as
 * they were.
 *
 * Every colour here is a token. */
.dlq-queues .facet { text-transform: none; }
.dlq-queues { flex-wrap: wrap; row-gap: 4px; }

/* The two operator verbs, drawn and disabled (ruling R3). They keep .ctl's own
 * `:disabled { opacity: .45; cursor: not-allowed }` -- there is deliberately no
 * bespoke disabled styling here, because a differently-disabled button reads as
 * a different KIND of unavailable. */
.dlq-actions { display: flex; gap: 6px; }

/* The refusal class, above the field list rather than inside it: it is the one
 * value in the pane that decides whether a retry could ever have worked. */
.dlq-reason { display: flex; gap: 6px; margin: 2px 0 6px; }
/* Postbacks out hosts two grids in plain block hosts (not stretched flex
 * items), so .grid-wrap's min-height cannot resolve AG Grid's height:100% --
 * the same chain the .card .grid-wrap rule above repairs. Explicit, definite. */
.postbacks-grid .grid-wrap { height: 380px; }
.narrow .postbacks-grid .grid-wrap { height: 320px; }

/* --- lane B: Postbacks out -----------------------------------------------
 *
 * STRUCTURE ONLY. Every colour on this screen comes from a token through a
 * class that already exists -- .tone-warn, .muted, .mono, .kpi-figure -- so
 * there is no new colour here to fall out of step with a theme.
 *
 * The two chip strips WRAP, and that is a fix rather than a preference. .facets
 * does not wrap by default: it was built for the six-word conversion type
 * facet. This screen puts the sender's NINE outcomes in one strip plus an
 * "all", and a tenth chip ran straight off the right edge of the page, taking
 * policy_denied and superseded -- the two an operator opens this screen for --
 * with it. */
.postbacks-facets, .postbacks-filters { flex-wrap: wrap; row-gap: 6px; }

/* The filter row mixes two selects and a free-text box. They are capped rather
 * than flexed: a destination id is long enough to push the search box off the
 * strip on its own. */
.postbacks-filters select.input { max-width: 220px; }
.postbacks-filters input.input { flex: 1 1 220px; min-width: 160px; max-width: 420px; }

/* The consent policy and the retry sweep sit side by side where there is room
 * and stack where there is not. minmax(0, 1fr) rather than 1fr: a policy line
 * carrying a long destination list is a single unbroken run, and a bare 1fr
 * track refuses to shrink below its content and widens the page instead. */
.postbacks-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--page-gap); }
.narrow .postbacks-cards { grid-template-columns: minmax(0, 1fr); }

/* One row per consent field. The field name is a schema word an operator
 * pastes into the policy document, so it keeps a fixed column of its own
 * rather than being centred on its value. */
.postbacks-policy .kv { gap: 8px; }
.postbacks-policy .kv-k { min-width: 168px; }
.postbacks-policy .kv-v { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; overflow-wrap: anywhere; }

/* The refusal reasons are a closed vocabulary from the policy package, so there
 * can be a dozen of them at once. They wrap like the value chips on
 * Quarantine, and for the same reason. */
.postbacks-reasons { flex-wrap: wrap; row-gap: 4px; }

/* --- lane R3: the Routing screen's live half ------------------------------
 *
 * STRUCTURE ONLY, like lane B's block above it. Every colour here comes from a
 * class that already exists -- .muted, .tone-warn, .pending-cell -- so there is
 * no new value to fall out of step with a theme, and the grid's two live
 * columns need no rule at all: .num, .tone-warn and .pending-cell are already
 * declared for .ag-cell.
 *
 * The default flow card gains a block of live rows under its configured split.
 * It is a two-column reading -- what the figure IS, then the figure -- with the
 * label column fixed rather than shrink-to-fit, so "cap · hour", "cap · day"
 * and "cap · total" line their numbers up instead of stepping in and out by a
 * character. */
.flow-live { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; font-size: 11.5px; }
.flow-live-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.flow-live-k { min-width: 132px; color: var(--color-neutral-500); }
/* A cap line is a run of digits an operator compares against the line above it,
 * so it gets the same tabular figures every number in this console has. */
.flow-live-row > :last-child { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* --- lane e2a-g2: the top bar's live-ness controls ------------------------
 * OUTSIDE .controls, beside .draft-count and .lang-switch, so neither the
 * strip's per-screen show/hide nor its overflow clip reaches them.
 * flex-shrink: 0 for the reason .lang-switch carries it. */
.live-wrap { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

/* The stamp is a READOUT, not a button: no border, no hover affordance, and
 * muted so it reads as a caption beside the control it sits next to. */
.fetched-stamp { color: var(--color-neutral-500); border-color: transparent; }

/* The poller's switch takes the shared .ctl.on accent -- the same rule the
 * theme picker and the rail tabs use -- so "which state am I in" reads
 * identically in all three places. */
.auto-refresh { min-width: 24px; }

/* Neither fits a 320px top bar, and a poller whose switch is off the screen is
 * a background cost an operator can neither find nor stop. shell.js stops the
 * timer with the control rather than leaving it running behind this rule. */
.narrow .live-wrap { display: none; }

/* The rail's reset action goes with the rail tabs when the rail is shut: 24px
 * of icon strip is a toggle, not a toolbar. */
.rail-reset { min-width: 22px; padding: 0 3px; border-color: transparent; background: transparent; }
.rail.collapsed .rail-reset { display: none; }
/* --- lane e2a-g3: the deep-link chip and the publish dialog's change list ---
 *
 * Two additions, both to surfaces that already exist.
 *
 * The facet strip never wrapped, and never had to: the decision chip holds one
 * of three short words. The deep-link chip holds a whole 36-character click id,
 * so the strip wraps rather than pushing its own ✕ off the edge of a 375px
 * layout, and the id truncates inside the chip -- the rail's headline carries it
 * in full, and the empty state names it in full when there is no row to open.
 *
 * The publish dialog's change list scrolls INSIDE the drawer body rather than
 * pushing Cancel and Publish below the fold. Forty diff rows are taller than the
 * drawer on any laptop, and a confirm whose buttons have to be scrolled to is a
 * confirm people press blind -- which is the failure this lane set out to fix.
 */
.facets { flex-wrap: wrap; row-gap: 6px; }
.chip-link .chip-value { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.confirm-body { display: flex; flex-direction: column; gap: 10px; }
.confirm-diff { display: flex; flex-direction: column; max-height: 46vh; overflow-y: auto; }
/* --- lane e2a-C4: the Trends conversions sparkline -------------------------
 *
 * Ask 9 puts confirmed conversions per day on Trends, and they do NOT go on the
 * traffic chart's axis: clicks run to five figures a day on this estate and
 * conversions to two, so a shared scale would flatten the conversion line onto
 * the baseline and an operator would read "no conversions" off a strip that has
 * them. It gets its own card, its own scale and 28px.
 *
 * The SVG stretches -- preserveAspectRatio="none" over a fixed 100x28 viewBox,
 * which is safe here only because the sparkline contains no text. The stroke
 * would stretch with it, so it is pinned with vector-effect: a 1.6px line on a
 * 900px card would otherwise be drawn at nine times its width horizontally and
 * at its own width vertically, which reads as a wedge rather than as a line.
 */
.spark { display: flex; flex-direction: column; gap: 6px; }
.spark-svg { width: 100%; height: 28px; display: block; }
.spark-line {
  fill: none;
  stroke: var(--chart-second);
  stroke-width: 1.6;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.spark-foot { display: flex; align-items: center; gap: 12px; font-size: 11px; color: var(--color-neutral-500); }
.spark-foot .sp { flex: 1 1 0; min-width: 0; }
/* --- end lane e2a-C4 ------------------------------------------------------ */
/* --- lane N3: Networks, the inbound side measured -------------------------
 *
 * STRUCTURE ONLY. Every colour here comes from a token through a class that
 * already exists -- .tone-ok, .tone-warn, .muted, .mono, .pill -- so there is
 * no new colour on this screen to fall out of step with a theme.
 *
 * The grid sits in a PLAIN BLOCK HOST with a definite height, not in a
 * stretched flex item. The screen carries an onboarding card, an inbound feed
 * and a status bar under the grid now, so .grid-wrap's `flex: 1` has nothing to
 * resolve against and AG Grid's height:100% renders the body 0px tall under a
 * live header -- the same chain the .card .grid-wrap and .postbacks-grid rules
 * repair, seen for real on Postbacks out in the 2026-08-21 box smoke. */
.networks-grid .grid-wrap { height: 420px; }
.narrow .networks-grid .grid-wrap { height: 340px; }

/* The two panes the design draws under the grid: the onboarding card takes the
 * room, the feed keeps a fixed 360px column. minmax(0, 1fr) rather than 1fr,
 * because the curl command is a single unbroken run and a bare 1fr track
 * refuses to shrink below its content and widens the whole page instead. */
.networks-panes { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: var(--page-gap); }
.narrow .networks-panes { grid-template-columns: minmax(0, 1fr); }

/* The onboarding rows are a fixed label column: "postback URL", "key id" and
 * "inbound auth" are the four things an operator reads down, and centring each
 * label on its own value makes them stop being a list. */
.networks-onboarding .kv-k { min-width: 132px; }
.networks-onboarding .kv-v { overflow-wrap: anywhere; }
.networks-curl { display: flex; flex-direction: column; gap: 5px; }

/* The feed is CAPPED IN HEIGHT and scrolls inside itself. Two hundred lines is
 * the server's own limit and a card that grew to hold them would push the
 * status bar off the bottom of a laptop screen -- and the status bar is where
 * the unclaimed-key count lives. */
.networks-feed .card-body { max-height: 420px; overflow-y: auto; }
.networks-feed-head-line { display: flex; align-items: center; justify-content: space-between; gap: 6px; }

/* One feed line. The identity wraps rather than widening the pane, the same
 * rule .detail-headline follows for a click id: a conversion key is 64
 * characters and the pane is 360px. */
.networks-feed-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px 0;
  border-bottom: 1px solid var(--color-neutral-900);
}
.networks-feed-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.networks-feed-id { min-width: 0; overflow-wrap: anywhere; }
.networks-feed-reason { font-size: 11.5px; line-height: 1.35; overflow-wrap: anywhere; }

/* The reveal. It is a control rather than a link -- an <a> with no href is not
 * focusable and a real href would be a navigation -- so it borrows .ctl and
 * only sheds the chrome that makes a button look like a verb bar. */
.networks-reveal { display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
.networks-reveal-link {
  align-self: flex-start;
  padding: 0 5px;
  font-size: 11px;
  background: none;
  border-color: transparent;
}
.networks-reveal-link:hover { border-color: var(--color-neutral-900); }
/* --- lane e2a-d2: Domains -------------------------------------------------
 *
 * STRUCTURE ONLY. Every colour on this screen comes from a token through a
 * class that already exists -- .tone-ok, .tone-warn, .muted, .mono, .note --
 * so there is nothing here to fall out of step with a theme.
 *
 * ⚠ THE GRID HEIGHT IS EXPLICIT, and it is the 0px-body trap rather than a
 * preference. The Domains grid sits in a plain block host with two cards
 * underneath it, so it is NOT a stretched flex item: .grid-wrap's min-height
 * cannot resolve AG Grid's own `height: 100%`, and the grid renders a header
 * and a "Rows: N" status bar over an empty body. The same chain the .card
 * .grid-wrap rule repairs, stated for this host. */
.domains-grid .grid-wrap { height: 380px; }
.narrow .domains-grid .grid-wrap { height: 320px; }

/* The certificate and edge cards sit side by side where there is room and
 * stack where there is not. minmax(0, 1fr) rather than 1fr: a SAN list is a
 * single unbroken run and a bare 1fr track refuses to shrink below its
 * content, widening the page instead of wrapping. */
.domains-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--page-gap); }
.narrow .domains-cards { grid-template-columns: minmax(0, 1fr); }

/* --- the add-domain pane --------------------------------------------------
 *
 * The design's second pane: a rail of four steps beside the body of the one
 * being read. The rail is BUTTONS, not decoration -- every step is reachable
 * by keyboard and by click, because three of the four are things to read
 * rather than things to complete, and a rail that only moved forwards would
 * hide the two an operator came back for. */
.wsplit { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: var(--page-gap); align-items: start; }
.narrow .wsplit { grid-template-columns: minmax(0, 1fr); }

.wrail { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.wstep {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px;
  text-align: left;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r2);
  color: var(--color-neutral-500);
  cursor: pointer;
  min-width: 0;
}
.wstep:hover { background: var(--row-hover); }
.wstep.on { background: var(--flowbg); border-color: var(--color-neutral-900); color: var(--color-text); }
.wstep .n {
  flex: none;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--color-neutral-800);
  font-family: var(--font-mono);
  font-size: 10.5px;
}
.wstep.on .n { border-color: var(--color-accent); color: var(--color-accent); }
.wstep-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wstep-text > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.wstep-text .muted { font-size: 11px; }

.wpane { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.wpane-title { font-size: 13px; color: var(--color-text); }
.wpane-actions { display: flex; gap: 6px; padding-top: 2px; }
/* --- lane e2a-s: the todo screen's four states ---------------------------
 *
 * .tone-acc is the fourth text tone. The three that existed -- ok, warn, bad --
 * are a verdict scale, and "in progress" is not a verdict: it is the one state
 * on this screen that means somebody is holding it right now, which is the
 * accent's job everywhere else in this console.
 *
 * The pill rules are separate from the cell rules because .pill sets its own
 * colour, so a bare .tone-acc beside it loses on specificity -- exactly as the
 * existing .pill.tone-ok / .pill.tone-warn pairs already do. */
.tone-acc { color: var(--color-accent); }
.pill.tone-acc { color: var(--color-accent); }
.pill.tone-muted { color: var(--color-neutral-500); }

/* The chip strip. It wraps rather than scrolls: five chips plus two counters
 * plus the New-todo button do not fit a 320px viewport on one line, and a
 * horizontally scrolling strip is the clipping shape this estate has already
 * been bitten by (.controls { overflow-x: auto } clipped the range picker for
 * the console's whole first year). */
.todo-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; row-gap: 6px; }
.todo-chips .chip { white-space: nowrap; }

/* The state + owner form inside the detail drawer. Two selects side by side
 * where there is room, stacked where there is not, with the Apply button on its
 * own line so it is never mistaken for part of the second select. */
.todo-state-form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  align-items: end;
  padding: 10px 0;
  border-top: 1px solid var(--line2);
  border-bottom: 1px solid var(--line2);
}
.todo-state-form .note, .todo-state-form .login-error, .todo-state-form > button { grid-column: 1 / -1; }
.todo-state-form > button { justify-self: start; }
.narrow .todo-state-form { grid-template-columns: minmax(0, 1fr); }

/* --- lane W1 (estate2-costs) ----------------------------------------------
 *
 * The Costs screen's own classes, and nothing else: the tiles are the
 * dashboard's .kpis, the cards are card(), the chart is .chart and the grid is
 * .grid-wrap, all unchanged (U6 -- no bespoke card CSS on this screen).
 *
 * Three things needed a rule of their own:
 *
 *   1. The head strip WRAPS. Account chips, four grain buttons, an app filter
 *      and two buttons do not fit a 540px pane on one line, and .screen-head is
 *      a nowrap flex row: without this the two buttons would be pushed past
 *      .shell { overflow: hidden } and simply not be there -- the same defect
 *      the KPI strip was given a 140px basis for. Nothing here clips: the smoke
 *      geometry audit fails an element that cannot be hit at its own rect.
 *
 *   2. The chart's marks. .chart-bar and .chart-line carry no colour -- each
 *      mark is filled or stroked from its own --chart-* token inline, so a
 *      theme restyles the chart by declaring a variable.
 *
 *   3. U7's fold. A campaign name longer than its column is clipped with an
 *      ellipsis and carries its full text in the cell tooltip; it never
 *      overlaps the column beside it, and nothing on this screen is positioned
 *      over text.
 */

.costs-strip { flex-wrap: wrap; row-gap: 8px; }
.costs-strip > * { flex: none; }
.costs-strip .costs-chips { min-width: 0; }

/* The two tab buttons sit at the right end of the strip, together. The shared
 * .screen-head .ctl:last-child rule pushes only the LAST control over, which
 * would leave "Manage cost accounts" stranded on the left; the pair moves as
 * one instead. */
.costs-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
/* --- lane c2-costsplit (2026-09-02) --- the padding is narrowed HERE and
 * nowhere else: `.costs-strip .costs-actions .ctl` is scoped to the two action
 * buttons of this one strip and reaches no other control in the console.
 *
 * The strip carries the account chips, four grain buttons, the app filter and
 * these two buttons on ONE line, and in Russian it was wrapping to a second.
 * That line costs ~40px, and on this screen those forty pixels come off the
 * BOTTOM: the grid's tool-panel side button falls past the fold of a 1440x900
 * display, where the browser smoke cannot click it and a human never finds it.
 * The four Russian labels were shortened for the same reason (web/i18n-ru.js);
 * this is the other half of the same fix. */
.costs-strip .costs-actions .ctl { margin-left: 0; padding-left: 9px; padding-right: 9px; }

/* An account chip is a BUTTON wearing .chip, so it needs the control resets
 * .ctl would have brought with it. .on mirrors the shared .ctl.on accent, which
 * is the same "this one is selected" the grain facets use one gap away. */
.costs-chip {
  min-height: 24px;
  font-family: inherit;
  color: var(--color-neutral-300);
  background: transparent;
  cursor: pointer;
}
.costs-chip.on { color: var(--color-accent); border-color: var(--color-accent); background: var(--color-accent-200); }
/* A google account is registry-only until a puller exists (§4.5). It is muted
 * and it says why on hover -- and it must not inherit .screen-head .muted,
 * which is the rule that lets a screen's ONE muted sentence grow to fill the
 * line. */
.costs-strip .costs-chip.muted { flex: none; overflow: visible; color: var(--color-neutral-500); }
.costs-dot { font-size: 9px; line-height: 1; }
.costs-app { max-width: 220px; }

/* A ratio computed over fewer than 20 conversions is drawn, and drawn quietly:
 * the figure keeps its place in the ladder and the tooltip says "Not enough
 * data". Greying it is the dataviz reading of the same caveat the registry
 * serves as n -- do not read a CPA off five conversions. */
.kpi.muted .kpi-figure { opacity: .55; }

/* .page is a flex COLUMN and every card on it shrinks by default. This is the
 * tallest screen in the console -- fifteen tiles, a three-band chart and a grid
 * -- and a shrunk GRID card is the worst of the two: the vendored grid draws
 * its group panel and its status bar around a body with no height, so the
 * header row and every cell simply are not there, with no error and no
 * warning. The page scrolls instead. */
.costs-card { flex: none; }
/* flex: none is the load-bearing half. .grid-wrap carries flex: 1, and inside a
 * card body -- which is itself a flex column -- a flex-basis of 0 wins over the
 * height property outright, so `.card .grid-wrap { height: 380px }` is silently
 * ignored and the grid gets whatever the flex algorithm leaves it.
 *
 * --- lane c2-costsplit (2026-09-02) --- the height is now a clamp and the grid
 * is DRAGGABLE. 380px was the floor a laptop needed and the ceiling a 27-inch
 * monitor was stuck with, on a screen that grew from 34 columns to 51: an
 * operator on a tall display was scrolling a ten-row viewport inside a page that
 * had six hundred pixels of empty space under it. 65vh tracks the display, the
 * 380px floor is unchanged for the laptop, 1100px stops the card from pushing
 * the totals row off a very tall screen, and `resize: vertical` hands the last
 * word to whoever is reading. flex: none stays: it is what lets a height be a
 * height at all here. */
.costs-grid-card .grid-wrap {
  flex: none;
  height: clamp(380px, 65vh, 1100px);
  /* hidden, not auto: the vendored grid runs its OWN virtual scroller and a
   * second scrollbar on the box around it fights it. overflow != visible is
   * only here because that is what makes resize: vertical legal at all. */
  overflow: hidden;
  resize: vertical;
}
/* A pane is not a page. 320px is the same figure .costs-accounts-grid uses one
 * card along, and vh means nothing useful inside a 540px column. */
.narrow .costs-grid-card .grid-wrap { height: 320px; resize: none; }

/* --- lane c2-costsui (2026-09-02): ONE lattice for the tiles ---------------
 *
 * THE TILES ARE ONE GRID, NOT FIVE STRIPS IN A WRAPPING RUN. The lane before
 * this one gave each group its own `.kpis` and let the groups pack across the
 * width as flex items: every row then sized its tiles to its OWN contents, so
 * the widths changed from row to row, the right edge was ragged, the last tile
 * of the Installs row was cut mid-word ("INSTALLS (META-REPC"), and the
 * two-tile Retargeting group sat alone looking like a rendering fault. The user
 * asked for the cards to look the same and the view to look united; one grid is
 * that, and nothing less than that is.
 *
 * A HEAD COLUMN PLUS FIVE EQUAL TILE COLUMNS. Every tile is exactly one cell of
 * the same track list, so every card is the same width and the right edge is
 * one straight line; every group is exactly one row, so the reading order is
 * unchanged and the heads stop floating. `minmax(0, 1fr)` rather than a bare
 * 1fr: a bare fr track is min-content wide at minimum, and one long label would
 * push the whole lattice past `.shell { overflow: hidden }`.
 *
 * ⚠⚠ THE 5 HERE AND `TILE_COLS` IN screens/costs.js ARE THE SAME FACT. The JS
 * builds filler cells up to five; this repeats five tracks. Change one without
 * the other and a short row's fillers wrap into the next row's head column.
 * tests/costs.test.js pins them to each other.
 *
 * It is SHORTER than the run it replaces (five rows of one tile-height, no
 * per-group captions above them), which is what keeps the grid card's own
 * controls -- the tool-panel side button especially -- inside the 1440x900
 * fold. That is a smoke gate, not a preference. */
.costs-tiles.kpis {
  grid-template-columns: 128px repeat(5, minmax(0, 1fr));
  gap: 0;
}

/* The head is a CELL of the lattice. It carries the same top and left rule as a
 * tile so the lines run unbroken from the card's left edge to its right, and it
 * borrows --grid-totals-bg -- the token the grid's own pinned totals row uses
 * one card below -- so the head column reads as the same family of furniture
 * rather than as a fifth colour on the screen. */
.costs-tiles .costs-tile-head {
  display: flex;
  /* --- unity lane U5 --- TWO LINES NOW: the group's name and its period. The
   * column is 128px and a period word is up to eleven characters, so they
   * stack rather than sharing a line; centring the pair keeps the pair on the
   * same optical axis the one-line head sat on. */
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
  overflow: hidden;
  padding: 8px 14px;
  border-left: 1px solid var(--color-neutral-900);
  border-top: 1px solid var(--color-neutral-900);
  background: var(--grid-totals-bg);
  color: var(--color-neutral-500);
  text-transform: uppercase;
  letter-spacing: .06em;
  line-height: 1.2;
}

/* Identical vertical rhythm on every cell, filler included: the label box
 * reserves two lines whether the label wrapped or not, the figure is one line
 * and the note is one line, so every card in the lattice is the same height and
 * the figures sit on one baseline across the row. */
.costs-tiles .kpi { padding: 8px 14px; }

/* THE LABEL FOLDS TO TWO LINES. "Install → payer (Meta basis)" and "Revenue,
 * re-engaged" do not fit one ~170px line, and .micro clips with an ellipsis:
 * the tiles would read "Install → payer (Meta…" and "Revenue, re-eng…", which
 * on a screen whose whole point is telling two similar metrics apart is the one
 * failure mode that matters. Two lines, clamped, with min-height holding the
 * figures on a common baseline. The FIGURE and the NOTE stay one line each --
 * they are short by construction and a wrapped figure would break the ladder
 * every column is read down. */
.costs-tiles .kpi .micro {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  white-space: normal;
  min-height: 2.4em;
}

/* The period word is the head's second line and is deliberately NOT in the
 * head's own uppercase: three periods run on this screen at once and the word
 * is read as prose about the row rather than as a second heading over it. */
.costs-tiles .costs-tile-head-name { overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.costs-tiles .costs-tile-period {
  font-size: 10px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-neutral-600);
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* A filler completes a row shorter than five so the ruled lines reach the right
 * edge instead of stopping in mid-air. It draws the lattice and says nothing:
 * no text, aria-hidden in the DOM, and no tile background of its own. */
.costs-tiles .kpi-empty { background: none; }

/* A pane is not a page. Two columns, the head spanning both as a band over its
 * own row, and the fillers gone -- a filler exists to straighten a right edge
 * that a two-column fold does not have. */
.narrow .costs-tiles.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.narrow .costs-tiles .costs-tile-head { grid-column: 1 / -1; border-left: 0; padding: 8px 10px; }
.narrow .costs-tiles .kpi-empty { display: none; }

/* The spend chart. Bars just under full opacity so an overlapping line reads
 * through them; lines at the console's own 1.6px. */
.chart-bar { opacity: .85; }
.chart-line { fill: none; stroke-width: 1.6; }
.chart-svg-bands { height: auto; }
.chart-band { font-weight: 500; }

/* U7. A folded cell is clipped, ellipsised and hoverable -- never wrapped over
 * its neighbour's row and never silently cut. */
.ag-theme-quartz .ag-cell.fold { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The Costs explain tab (web/costsexplain.js, lane W3). One block per metric:
 * the label and its clock badge on a line, then the formula, the sources and
 * one line per note code. No new colour and no new font -- .mono, .kv and .pill
 * already do that. W3 owns the module and W1 owns this file, so the rules land
 * here, verbatim from that lane's STATUS with ONE correction: it asked for a
 * --color-border token, which no theme in this console declares -- an
 * undeclared token draws NO border at all and warns nobody, which is the exact
 * defect tests/conversion-types.test.js:329 exists to catch (and does: the
 * token name is written without its var() here so the scan reads this as prose
 * rather than as a declaration). --line2 is the separator the rest of app.css
 * uses inside a card, and it resolves under all four themes. */
.explain-list { display: flex; flex-direction: column; gap: 10px; }
.explain-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 0; border-top: 1px solid var(--line2); }
.explain-row:first-child { border-top: 0; }
.explain-row-head { display: flex; align-items: center; gap: 8px; }

/* --- lane I1 (estate2-costs) ----------------------------------------------
 *
 * The ACCOUNTS panel's grid host (web/costs-accounts.js:392). Lane W2 owns that
 * module and lane W1 owns this file, so the rule was owed across the seam and
 * is landed here, from STATUS-lane-W2-costs.md hunk (c).
 *
 * It is the same chain the .costs-grid-card rule above repairs, one host along:
 * the panel mounts its grid as a plain div directly on .page -- a head above it,
 * two notes below -- so the div is a flex ITEM that shrinks, and .grid-wrap
 * inside it carries flex: 1, whose flex-basis of 0 beats the height property.
 * Without both declarations the vendored grid draws its group panel and its
 * status bar around a body with NO height: no header row, no cells, no error
 * and no warning. flex: none on the outer div is the half that is easy to miss.
 *
 * 320px on a narrow viewport, where the same 380px would push the two notes
 * under the panel off a phone screen entirely. */
.costs-accounts-grid { flex: none; }
.costs-accounts-grid .grid-wrap { flex: none; height: 380px; }
.narrow .costs-accounts-grid .grid-wrap { height: 320px; }

/* --- unity lane U5 (unity-20260922): the scope row and the dropdown kit -----
 *
 * ONE new region, and it is deliberately built out of parts this console
 * already owns. The trigger IS `.ctl` -- the same 28px, 12px, `var(--r2)`
 * control the grain facets one gap away are -- and the panel IS `.menu
 * menu-float`, the top bar's own floating surface. Nothing here declares a
 * colour, a radius or a font of its own; the whole block is layout plus the
 * accent tokens `.ctl.on` already uses.
 *
 * ⚠ That is a decision, not laziness. The spec for this control is the Control
 * Room's, and the artifact mocks it the way the CRM draws it: a 999px pill in
 * IBM Plex on a white card. Copied verbatim it would be the only round thing
 * on this screen and the only control that did not match the four beside it --
 * two consoles in one strip. The BEHAVIOUR is cloned; the vocabulary is this
 * console's.
 *
 * The panel is `position: fixed` (inherited from `.menu-float`) because `.page`
 * is `overflow-y: auto`, which computes `overflow-x: auto` as well and clips an
 * absolutely positioned child in both axes -- the defect that cost this console
 * a dead range button through 457 green unit tests. costs-networks.js writes
 * left/top from the trigger's rect on open. */

.costs-scope { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-width: 0; }
.costs-net { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.costs-net-label { flex: none; }
.costs-net-trigger { max-width: 24ch; }
.costs-net-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The count rides the trigger as a small accent numeral rather than as a
 * filled badge: a filled pill here would be the second accent shape on a strip
 * that already marks the selected grain with `.ctl.on`. */
.costs-net-count {
  flex: none;
  min-width: 15px;
  padding: 0 4px;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--color-accent);
  border: 1px solid var(--color-accent);
  border-radius: var(--r2);
}
.costs-net-caret { flex: none; font-size: 9px; color: var(--color-neutral-500); }

.costs-net-panel {
  min-width: 232px;
  max-width: min(320px, calc(100vw - 16px));
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.costs-net-search { height: 26px; font-size: 12px; }
/* The list scrolls and the footer does not: the sentence that says what is in
 * the list has to stay on screen while the list is being read. 232px is seven
 * rows, which is more networks than this estate will connect this year and few
 * enough that the panel never covers the tiles it is filtering. */
.costs-net-list { max-height: 232px; overflow-y: auto; display: flex; flex-direction: column; }

.costs-net-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--color-text);
  background: transparent;
  border: 0;
  border-radius: var(--r2);
  cursor: pointer;
}
.costs-net-opt:hover { background: var(--row-hover); }
.costs-net-opt.on { color: var(--color-accent); }
.costs-net-opt.muted { color: var(--color-neutral-500); }
.costs-net-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.costs-net-note { flex: none; color: var(--color-neutral-600); }

/* The tick box. It is a DRAWN box rather than an <input type="checkbox">,
 * because an interactive control inside a role="option" is an ARIA violation --
 * see the note at the head of costs-networks.js. The mark is two borders
 * rotated, which needs no glyph, no font and no image. */
.costs-net-box {
  flex: none;
  position: relative;
  width: 13px;
  height: 13px;
  border: 1px solid var(--color-neutral-700);
  border-radius: 2px;
}
.costs-net-box.on { background: var(--color-accent); border-color: var(--color-accent); }
.costs-net-box.on::after {
  content: '';
  position: absolute;
  left: 4px;
  top: 1px;
  width: 3px;
  height: 6px;
  border: solid var(--color-bg);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}

/* ⚠ THE PANEL'S PROSE IS NOT SHOUTED. `.micro` is uppercase and tracked out in
 * this console, which is right for a control LABEL and wrong for a sentence and
 * for a served value: the footer read "LISTED = A COST ACCOUNT IS CONNECTED"
 * and a partner tag read "WONKA" in a capture. Both are lower-cased back here,
 * where the words actually are. */
.costs-net-empty { padding: 6px 8px; }
.costs-net-empty,
.costs-net-note,
.costs-net-foot .micro { text-transform: none; letter-spacing: 0; }
.costs-net-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding-top: 4px;
  border-top: 1px solid var(--color-neutral-800);
}
.costs-net-foot .micro { min-width: 0; white-space: normal; line-height: 1.3; }
.costs-net-clear { flex: none; height: 22px; padding: 0 6px; font-size: 11px; }

/* The window preset row. `.facets` already lays out a run of `.ctl`s with the
 * right gap, so this only narrows the presets -- four of them plus Custom on a
 * strip that also carries Partner, Networks, four grain buttons and two tab
 * buttons. */
.costs-window { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.costs-window-preset { padding: 0 7px; }
.costs-window-custom { display: flex; align-items: flex-end; gap: 6px; flex: 1 0 100%; }
.costs-window-field { display: flex; flex-direction: column; gap: 2px; }
.costs-window-date { width: 132px; }

/* The opening sentence. It sits where the coverage note sits and reads as the
 * same kind of line, with the networks and the window at full text colour --
 * they are the two facts every figure below depends on. */
.costs-inforce-said { color: var(--color-text); font-size: 12px; }

/* A pane is not a page: the scope row takes the full width of a narrow shell so
 * its three controls stay on readable lines rather than each folding to one
 * word, and the panel widens to the gutter. */
.narrow .costs-scope { flex: 1 0 100%; gap: 8px; }
.narrow .costs-net { flex: 1 0 100%; }
.narrow .costs-net-trigger { flex: 1 1 auto; max-width: none; justify-content: space-between; }
.narrow .costs-net-panel { min-width: 0; width: calc(100vw - 16px); }
.narrow .costs-window { flex: 1 0 100%; }
.narrow .costs-window-date { width: 100%; }
.narrow .costs-window-field { flex: 1 1 40%; }

/* --- lane F0 (e2uipolish): form-tab ---------------------------------------
 *
 * The inline half of web/forms.js's editor surface. The SAME editor body and
 * the SAME action row the drawer has always shown, rendered into a host the
 * screen owns -- its pane, its tab -- instead of into a scrim on document.body.
 *
 * There is deliberately no width, no max-width and no padding here. A drawer is
 * a floating panel and has to size itself; a tab is the pane's whole body and
 * the pane has already sized it. What the block does own is the two properties
 * a form inside a flex pane gets wrong on its own: min-width: 0 on the column
 * and on the body, without which a long unbroken value in a field pushes the
 * pane wider than its share and takes the grid beside it with it.
 *
 * .form-tab-foot mirrors .drawer-foot (app.css:1131) on the two declarations
 * that carry meaning -- the 8px gap between the actions and the hairline above
 * them, from the same neutral token, so an editor does not change colour when
 * it changes surface. It differs on two, on purpose: the actions stay LEFT
 * rather than flex-end, because in a tab they sit under the form rather than at
 * the far edge of a floating panel, and they wrap rather than overflow, because
 * a narrow pane is a normal pane and Delete/Check/Cancel/Save is four of them.
 */
.form-tab { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.form-tab-title { margin: 0; font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.form-tab-body { min-width: 0; }
.form-tab-foot { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 8px; border-top: 1px solid var(--color-neutral-900); }

/* --- lane G2 (e2uipolish): detail-tab -------------------------------------
 *
 * The conversion detail, mounted as a TAB rather than as the 312px aside it
 * spent its life in. Every block inside it -- .detail-inner, .detail-head,
 * .detail-fields, .detail-params, .detail-headline, .params-box -- is the same
 * one the aside used, unchanged and still shared with the click log's rail;
 * only the container changed, so this block is only what the container has to
 * stop doing.
 *
 * There is deliberately NO width, no max-width, no border-left and no panel
 * background here. .detail owned all four because it was a floating column
 * sharing a row with a grid; a tab is the pane's whole body, the pane has
 * already sized and painted it, and repeating the border would draw a hairline
 * down the middle of nothing.
 *
 * What it DOES own is min-width: 0, and it is load-bearing rather than tidy: a
 * pane is a flex item, a conversion_key is 64 unbroken characters and a
 * rendered_url is longer, and without this the longest value in the detail sets
 * the pane's width and pushes every pane beside it off the strip. .kv-v already
 * carries overflow-wrap: anywhere (app.css:481), which is what lets the value
 * itself break once the column is allowed to be narrower than its content.
 */
.detail-tab { display: flex; flex-direction: column; min-width: 0; }
.detail-tab .detail-inner { min-width: 0; }
/* --- lane G3 (e2uipolish): the quarantine detail tab -----------------------
 *
 * A refused row used to open `aside.detail` -- a 312px panel wedged into the
 * grid's flex row, taking that width off the table for as long as it was open.
 * It is a TAB now, and a tab is the pane's whole body, so almost everything
 * .detail carried is wrong here: no fixed width, no left border, no panel
 * background painted over the page's own.
 *
 * What it does own is the three properties a long value needs in a pane that
 * can be a quarter of the window. min-width: 0 lets the column shrink below its
 * content -- without it a 1,800-byte raw_query on one unbroken line pushes the
 * pane wider than its share and takes the grid beside it with it. overflow-y
 * keeps the evidence scrolling INSIDE the tab rather than growing the page,
 * which is what the operator wants when a Send-test carries 127 pairs. And the
 * head's Back button sits on the row it shares with the title, which the
 * inherited .detail-head flex rule already does.
 *
 * max-width is deliberately absent for the same reason .form-tab has none: the
 * pane has already sized this, and a second opinion here would fight it. */
.detail-tab { min-width: 0; overflow-y: auto; padding-top: 2px; }
.detail-tab .detail-fields .kv-v { overflow-wrap: anywhere; }
.detail-tab .params-box .kv-v { overflow-wrap: anywhere; }
/* --- lane G1 (e2uipolish): the click as a tab -----------------------------
 *
 * #/clicks/<click_id> used to fill the shell's right rail: a 212px column with
 * its own width rule and its own font sizes (.rail .detail-inner, app.css:822-
 * 824). It is a TAB now, and a tab is the pane's whole body -- already sized,
 * already scrolled by the pane -- so this block sets no width and no padding,
 * exactly as the .form-tab block above it does not.
 *
 * What it does own is the two properties the pane cannot supply. min-width: 0
 * on the column and on the body, without which a click id, a referer or a
 * 200-character open parameter pushes the pane wider than its share and takes
 * the grid in the pane beside it with it. And the head row WRAPS: the Back
 * control and the widened-range sentence are a button plus a full line of
 * prose, and in a narrow pane they belong on two rows rather than in a
 * horizontal scroll the top-bar rule (§5, R2) spent this pack removing.
 *
 * .clicks-progress is the load-all counter, on the same row as its spinner.
 * It is a .micro line, so it inherits that size and colour and adds only the
 * arrangement -- one line that grows a number rather than a block that
 * re-lays-out under the operator once per page. */
.detail-tab { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.detail-tab-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
.detail-tab-head .muted { min-width: 0; overflow-wrap: anywhere; }
.detail-tab-body { min-width: 0; }
.clicks-progress { display: flex; align-items: center; gap: 8px; padding: 6px 0; }
/* --- lane G6 (e2uipolish): detail-tab -------------------------------------
 *
 * The read-only half of what .form-tab is to an editor. Automation, Jobs and
 * Dead letters used to render a row's detail into `aside.detail` -- a 312px
 * column bolted to the right of the grid, with its own width, its own border
 * and its own scrollbar. The same body is a TAB now, so all three of those
 * belong to the pane instead: a tab IS the pane's body and the pane has
 * already sized it.
 *
 * So this block sets NO width and NO background. What it owns is the column
 * the sections stack in, min-width: 0 (without it a long unbroken id -- a
 * message id, a rule name -- pushes the pane wider than its share and takes
 * the pane beside it with it), and the two places the pane's own typography
 * was too small for a full-width surface: the heading, which is a tab title
 * now rather than a panel label, and the kv key column, which had 120px in a
 * 312px pane and reads better with more when there is more.
 *
 * .detail-head keeps space-between from the pane rule (app.css:760), which is
 * what puts «Back to list» at the right edge of the heading row. */
.detail-tab { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.detail-tab .detail-inner { gap: 10px; min-width: 0; }
.detail-tab .detail-head { gap: 8px; }
.detail-tab .detail-head h2 { font-size: 15px; font-weight: 600; }
.detail-tab .detail-fields .kv-k { min-width: 160px; }
.narrow .detail-tab .detail-fields .kv-k { min-width: 120px; }

/* The todo tab is an EDITOR tab (.form-tab, lane F0) whose body carries the
 * read-only detail as well as the fields, so the two are given the same column
 * rhythm the pane had. Nothing here is a colour or a size that .todo-detail
 * did not already have; it is the stacking a drawer got from .drawer-body and
 * a tab has to be given. */
.form-tab-body > .todo-detail { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
/* --- lane G4 (e2uipolish): the entity editor tabs ---------------------------
 *
 * Landings, Offers, Traffic sources and Domains edit in a TAB now, and F0's
 * .form-tab block deliberately carries no width -- a tab is the pane's whole
 * body and the pane has already sized it (STATUS-lane-F0-uipolish.md, S8). On
 * a single-pane console that body is the whole work area, and a two-field form
 * stretched to 1800px is a row of inputs an operator has to track across the
 * screen to read one label. So the READING WIDTH is set here, in the screen's
 * own block, and it is the width the drawer these forms came out of already
 * used (.drawer, app.css:1122) so the same form is the same shape on both
 * surfaces.
 *
 * It is a max-width on the BODY and the FOOT rather than on .form-tab itself:
 * the title keeps the tab's full width, which is what lets a long hostname
 * ellipsis rather than wrap under a form that stops short of it.
 *
 * The detail lines above the fields (Domains reads nine of them) wrap instead
 * of pushing the pane wider -- the same min-width:0 rule F0 wrote for the
 * column, applied to the value cell that holds a hostname with no spaces in it.
 */
.form-tab-body, .form-tab-foot { max-width: min(560px, 100%); }
.form-tab-body .detail-lines { min-width: 0; }
.form-tab-body .detail-lines > * { min-width: 0; overflow-wrap: anywhere; }
/* --- lane G5 (e2uipolish) -------------------------------------------------
 *
 * ONE class, and it is the counterpart of lane F0's .form-tab: what a screen
 * wraps a READ-ONLY detail in when the inline pane, the rail or the drawer it
 * used to live in became a tab of its own.
 *
 * Affiliate networks (the onboarding card + the inbound feed) and Postbacks
 * out (one attempt: its ten schema fields, the send, and what happens next)
 * both draw one, so the two read the same way -- an operator moving between
 * them is looking at the same kind of thing and it must not change shape.
 *
 * Like .form-tab it carries NO width, no max-width and no padding: a tab is
 * the pane's whole body and the pane has already sized it. What it does own is
 * the two properties a stack of cards inside a flex pane gets wrong on its
 * own. `min-width: 0` because a long unbroken value -- a rendered URL, a 64
 * character conversion key -- otherwise pushes the pane wider than its share
 * and takes the grid in the pane beside it with it. `gap` because the cards
 * are siblings in a column and a card stack with no gap reads as one card with
 * lines in it.
 *
 * The screen-head inside it is the tab's own action row (Back to list, Edit).
 * It sits at the TOP rather than at the bottom, where a drawer's actions were:
 * a drawer is a floating panel an operator dismisses, and a tab is a place
 * they navigate out of, so the way out belongs where the eye starts.
 */
.detail-tab { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.detail-tab > .screen-head { margin: 0; }
.detail-tab .card { min-width: 0; }
