/* ============================================================================
LOCKED TOKENS + STRUCTURE
   ----------------------------------------------------------------------------
   packages/ui/CLAUDE.md splits the token set in two:
     brandable  color, font-families, radius   -> an operator may change these
     locked     type-scale, spacing, elevation, motion, and everything below
   "An operator who could change spacing could break the layout."

   Two prefixes, and the difference matters:
     --s8-*   brandable. Every direction defines the SAME names. This is the
              white-label surface, and boundary 4 (no per-operator builds) is
              exactly the claim that swapping these values is sufficient.
     --dir-*  the direction's own structural choices. NOT brandable. An
              operator rebrands within a direction; they cannot change it.

   That second prefix is a finding, not a convenience: three directions could
   not be expressed in --s8-* alone, which tells us the brandable set is
   narrower than a visual identity. Better to learn that here than during
   white-labeling.
   ========================================================================= */

/* Screen numbers in the comments below are HISTORICAL. Most were written against a numbering
   scheme retired in August and then moved again by the ANV-30 renumber, so a bare `14` in a
   comment names the screen that carried that number when the comment was written -- not the one
   that carries it now. The convention going forward is `NN-name`, and it is recorded in
   `.claude/rules/04-working-in-design.md`. A comment here is still worth reading for WHAT went
   wrong; go and look at the screen rather than trusting the number. */

:root {
  /* Tells the browser which way the page runs, so native controls, scrollbars, placeholders and
     form widgets follow the theme instead of the user-agent default. Without it a placeholder
     resolves to a UA grey that ignores the token palette entirely. Set per theme below. */
  color-scheme: light;

  /* type scale — locked */
  --s8-text-xs: 11px;
  --s8-text-sm: 12px;
  --s8-text-base: 13px;
  --s8-text-md: 14px;
  --s8-text-lg: 16px;
  --s8-text-xl: 20px;
  --s8-text-2xl: 26px;
  --s8-text-3xl: 34px;
  --s8-leading-tight: 1.2;
  --s8-leading-normal: 1.5;
  --s8-leading-relaxed: 1.65;
  /* 100 and 900 come from Praveen's review of #50: design.md's identity puts display and section
     headings at weight 100, with 900 for the hero exception, and the locked set defined neither.
     Additive on purpose -- nothing uses them until a direction asks, and whichever direction wins
     has them available. */
  --s8-weight-thin: 100;
  --s8-weight-normal: 400;
  --s8-weight-medium: 500;
  --s8-weight-semibold: 600;
  --s8-weight-black: 900;

  /* spacing — locked, 4px base */
  --s8-space-1: 4px;
  --s8-space-2: 8px;
  --s8-space-3: 12px;
  --s8-space-4: 16px;
  --s8-space-5: 20px;
  --s8-space-6: 24px;
  --s8-space-8: 32px;
  --s8-space-10: 40px;
  --s8-space-12: 48px;
  --s8-space-16: 64px;

  /* shell metrics — locked */
  --s8-topnav-h: 52px;
  --s8-sidebar-w: 216px;
  --s8-control-h: 32px;
  /* Every selectable option -- a state tab, a filter chip, a segment -- is this tall. They were
     28, 28 and 30 with paddings of 5px 12px, 0 12px and 6px 14px, two of them off the 4px grid,
     which is why three controls doing the same job read as three design systems. */
  --s8-option-h: 28px;
  --s8-container-main: 1180px;
  /* RF-K12, then RF-L1. The content column had EIGHT widths -- 1180 plus seven inline literals --
     so walking 02 -> 03 -> 04 it went 1180 -> 940 -> 1180. That was fixed with three named tiers,
     which removed the arbitrary numbers but not the symptom: three CENTRED widths are still three
     left edges, and the heading moved 262 -> 372 -> 422 depending on the page you were on.
     One page width now. What a page may be WIDE is settled above; how wide a given control should
     MEASURE is settled here, at the control. The two were conflated, and conflating them is what
     made a settings form move the page under the reader. */
  --s8-measure-field: 560px;
  --s8-text-doc: 30px;
  --s8-measure-form: 860px;
  /* The summary panel beside a form. 03 said 300, 13 said 320, 17 said 300 -- one component,
     three files, and the odd one out was invisible because no two of them are ever on screen
     together. */
  --s8-rail-w: 300px;
  /* Clearance for the fixed review bar. It is a MEASURED figure, not a guess: the bar is 55px with
     a one-line note and 81px with a two-line one, and it grows to 99 at 768 and 181 at 390 as the
     note wraps further. The old reserve was a flat 48px, so every screen with a bar hid content
     behind it -- 7px at 1440, and up to 133px on a phone, which is a whole card.
     A constant is only safe here because a gate re-measures it: `no content behind the review bar`
     fails at all four widths if a note gets longer than these figures allow. */
  --s8-preview-reserve: 96px;

  /* elevation — locked. The header above has listed this as a locked category since the file was
     written and it was never defined, so overlays had a border and nothing else. One value: there
     is exactly one elevated thing in this product, a surface floating over the page. */
  --s8-shadow-overlay: 0 10px 30px rgb(0 0 0 / 45%);

  /* motion — locked */
  --s8-duration-fast: 90ms;
  --s8-duration-normal: 160ms;
  --s8-ease: cubic-bezier(0.2, 0, 0.13, 1);
  /* Added 2026-09-27, sign-off granted by Hasnat with the rest of the foundation-completion
     motion work: a third duration for the choreographed entrances (modal/panel/drawer) this
     family is building, and a distinct exit curve so leaving reads faster than arriving, per the
     research (Part B, `.omc/plans/anv-30-foundation-completion.md`). `--s8-duration-slow` was
     already referenced with a `240ms` fallback on `.cvcard--settling` (never itself defined,
     the same dangling-token shape `--s8-duration-base` turned out to be) -- defined here at
     that exact fallback value, so nothing already using it changes. */
  --s8-duration-slow: 240ms;
  --s8-ease-exit: cubic-bezier(0.2, 0, 1, 0.9);
  /* Signed off 2026-09-27: a small overshoot, for the one node that becomes current. */
  --s8-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* Lets `height: 0` <-> `height: auto` interpolate smoothly (`.card--expand`, below) instead of
   snapping, without measuring `scrollHeight` in script. Global and inert everywhere else: it only
   changes behaviour for a property whose start or end value is itself a keyword, which nothing
   else on this page transitions to or from. */
:root {
  interpolate-size: allow-keywords;
}

[data-theme='dark'] {
  color-scheme: dark;
}
[data-theme='light'] {
  color-scheme: light;
}

/* ---------------------------------------------------------------- reset --- */
*,
*::before,
*::after {
  box-sizing: border-box;
}
body,
h1,
h2,
h3,
h4,
p,
figure,
ul,
ol,
dl,
dd {
  margin: 0;
}
ul,
ol {
  padding: 0;
  list-style: none;
}
button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}
::placeholder {
  color: var(--s8-color-text-muted);
  opacity: 1;
}
img,
svg {
  display: block;
  max-width: 100%;
}
a {
  color: inherit;
  text-decoration: none;
}
/* ...which is right for a link that IS a block -- a nav item, a card, a table row -- and wrong
   for a link inside a sentence. Measured: every inline link in prose on the September screens
   computed to the same color, weight and decoration as the text around it, which is a link
   nobody can see and therefore nobody clicks. The pre-existing screens do not have the fault
   because they do not use the pattern; they put actions in `.btn`. The pattern is worth having,
   so it needs a rule.
   Scoped to the containers that hold prose, so it cannot reach the block-level links above. */
.plain a,
.callout a,
.u__d a,
.fr2__d a,
.field__hint a,
.estate__b a,
.lonefoot a,
.mark__why a,
.statebar__note a,
.plan__s a,
.ghelp__link a {
  color: var(--s8-color-wayfind);
}
.plain a:hover,
.callout a:hover,
.u__d a:hover,
.fr2__d a:hover,
.field__hint a:hover,
.estate__b a:hover,
.lonefoot a:hover,
.mark__why a:hover,
.statebar__note a:hover,
.plan__s a:hover,
.ghelp__link a:hover {
  text-decoration: underline;
}

/* The shell is `min-height: 100%`, and a percentage height resolves against the parent -- so
   without this the grid never learns how tall the viewport is and the sidebar simply stops where
   the content stops. It lived in SIXTEEN screens' inline styles and was missing from ten, which is
   only visible on a screen whose content is short: 25 has no <style> block at all, so its sidebar
   ended halfway down the page. Found by looking at it. */
html,
body {
  height: 100%;
}

body {
  font-family: var(--s8-font-body);
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-normal);
  background: var(--s8-color-surface-sunken);
  color: var(--s8-color-text);
  -webkit-font-smoothing: antialiased;
}

:focus-visible {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
  border-radius: var(--s8-radius-sm);
}

.mono {
  font-family: var(--s8-font-mono);
  font-size: 0.94em;
  letter-spacing: -0.01em;
}
.muted {
  color: var(--s8-color-text-muted);
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  /* `clip` is deprecated; `clip-path: inset(50%)` is the modern spelling of the same trick and is
     what every current visually-hidden recipe uses. CodeRabbit, PR #50. */
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ============================================================ app shell === */
.app {
  display: grid;
  /* `minmax(0, ...)` and not `1fr`. `1fr` is `minmax(auto, 1fr)`, and an `auto` minimum floors at
     MIN-CONTENT -- so this track grew to fit the topnav's 383px of nowrap controls instead of
     letting them shrink, and the whole PAGE scrolled sideways at 390px on nine screens. The
     symptom looks like a topnav bug and the cause is one word in the track list. */
  grid-template-columns: var(--s8-sidebar-w) minmax(0, 1fr);
  grid-template-rows: var(--s8-topnav-h) 1fr;
  grid-template-areas: 'brand topnav' 'sidebar main';
  min-height: 100%;
  background: var(--s8-color-surface-sunken);
  /* Set color explicitly rather than inheriting from body. The shell has to establish its own
     color context: inherited text silently takes the host page's color, which looks correct in
     dark and wrong in light, so the bug hides in exactly one theme. Same failure class as the
     ANV-13 cascade bug in packages/bff-core/src/theme.ts, where white-labeling did not work at
     all while every test passed. */
  color: var(--s8-color-text);
  font-family: var(--s8-font-body);
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-normal);
  background-image: var(--dir-canvas-texture, none);
  background-size: var(--dir-canvas-texture-size, auto);
}

.brandbar {
  grid-area: brand;
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: 0 var(--s8-space-4);
  background: var(--dir-sidebar-bg, var(--s8-color-surface));
  border-right: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.brandbar__mark {
  width: 18px;
  height: 18px;
  flex: none;
  border-radius: var(--dir-mark-radius, var(--s8-radius-sm));
  background: var(--s8-color-accent);
}
.brandbar__name {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: var(--dir-brand-tracking, -0.02em);
  text-transform: var(--dir-brand-case, none);
}

/* ------------------------------------------------------------- top nav --- */
.topnav {
  grid-area: topnav;
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: 0 var(--s8-space-4);
  /* Below ~380px of pane the scope breadcrumb, the credit number, the ops chip and the avatar do
     not fit. Every one of them is a labeled control, so this scrolls rather than hiding one --
     the same trade visual-design.md already settled for Portal A's sidebar, where a horizontal
     strip beat a hamburger because "a hamburger hides the navigation on exactly the viewport
     where a user is least able to guess what is behind it". Inert when the content fits.
     THE KNOWN CONSEQUENCE HAPPENED. This comment used to end "a scroll container cannot have
     overflow-y: visible, so a real implementation must render the scope dropdowns in a portal
     (Radix does) or they will be clipped by this bar. Recorded rather than discovered later." The
     dropdowns arrived and were clipped, exactly as written -- they reported `display: block` and a
     real bounding box and painted nothing.
     So the scroll moved to the phone tier, where it is the only place it was ever needed:
     measured across all 26 product screens with clipping off, the bar overflows by 0px at 1440,
     1024 and 768, and by 59px at 390. Nothing above phone width was buying anything. */
  background: var(--dir-topnav-bg, var(--s8-color-surface));
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.scope {
  display: flex;
  align-items: center;
  gap: var(--s8-space-1);
  min-width: 0;
}
.scope__sep {
  color: var(--s8-color-border-strong);
  flex: none;
}
.scope__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-2);
  white-space: nowrap;
  background: transparent;
  border: var(--dir-border-w, 1px) solid transparent;
  border-radius: var(--s8-radius-sm);
  cursor: pointer;
  font-size: var(--s8-text-base);
  color: var(--s8-color-text);
  transition: background var(--s8-duration-fast) var(--s8-ease);
}
.scope__btn:hover {
  background: var(--s8-color-surface-sunken);
  border-color: var(--s8-color-border);
}
.scope__label {
  font-size: var(--s8-text-xs);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  font-weight: var(--s8-weight-semibold);
}
.scope__caret {
  color: var(--s8-color-text-muted);
  font-size: 9px;
}
.topnav__spacer {
  flex: 1;
}
.topnav__right {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
}

.omnibar {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  /* Rule 04 #9 — the top bar had five control heights (28/32/18/28/26): search and Help sat at
     --s8-option-h beside credit's --s8-control-h. One height for the row, --s8-control-h, same
     as every neighbour now carries. */
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-3);
  min-width: 190px;
  background: var(--s8-color-surface-sunken);
  border: var(--dir-border-w, 1px) solid var(--s8-color-control-border);
  border-radius: var(--s8-radius-sm);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  cursor: pointer;
}
.omnibar kbd {
  margin-left: auto;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  padding: 1px 5px;
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface);
}

/* the credit meter — the $25 grant is the whole onboarding, so it is chrome, not a page.
   Removed in ANV-30 on a misreading and RESTORED: `platform_design.md` puts "prepaid credits"
   in Phase 3 alongside "self-serve billing portal, volume discounts, marketplace integrations"
   — that row is customers BUYING credit, a commerce feature. It is not "a trial grant cannot
   exist": Phase 1 has manual invoice generation, so a manually granted trial with a manually
   computed balance is entirely possible, and whether new accounts get one is a GTM decision
   rather than a platform capability. PROPOSAL.md is explicit that the arithmetic is the design. */
.credit {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-3);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-raised);
  font-size: var(--s8-text-sm);
}
.credit__amount {
  font-family: var(--s8-font-mono);
  font-weight: var(--s8-weight-semibold);
}
.credit__track {
  width: 44px;
  height: 4px;
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-border);
  overflow: hidden;
}
/* 29 — a real fixture value ($21.05 of $25.00) needs a real gauge rather than a computed inline
   width, and `style=""` is banned in v1. `<progress value max>` carries the fraction as data
   instead: `appearance: none` clears the browser's own chrome, and the two vendor pseudo-elements
   below repaint it as the same flat fill `display:block` on a <span> used to give a <span>-based
   caller in an older screen. */
.credit__fill {
  display: block;
  height: 100%;
  width: 100%;
  appearance: none;
  border: 0;
  background: var(--s8-color-accent);
  border-radius: var(--s8-radius-sm);
}
.credit__fill::-webkit-progress-bar {
  background: transparent;
}
.credit__fill::-webkit-progress-value {
  background: var(--s8-color-accent);
  border-radius: var(--s8-radius-sm);
}
.credit__fill::-moz-progress-bar {
  background: var(--s8-color-accent);
  border-radius: var(--s8-radius-sm);
}

/* 29 D1 — the Billing group's Credit section reads at a size a person looks at once, not the
   compact chrome chip repeated in every corner (`.credit` above stays as-is, unowned by this
   family beyond reuse). Same `<progress value max>` reasoning as `.credit__fill`: the fraction is
   data, not a computed inline width, because `style=""` is banned in v1. */
.bal {
  display: flex;
  align-items: flex-end;
  gap: var(--s8-space-6);
  margin-bottom: var(--s8-space-4);
}
.bal__n {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-3xl);
  font-weight: var(--s8-weight-semibold);
  line-height: 1;
}
.bal__of {
  font-size: var(--s8-text-md);
  color: var(--s8-color-text-muted);
  padding-bottom: 3px;
}
.bal__track {
  display: block;
  width: 100%;
  height: 8px;
  appearance: none;
  border: 0;
  border-radius: var(--s8-radius-sm);
  overflow: hidden;
  background: var(--s8-color-border);
  margin-bottom: var(--s8-space-3);
}
.bal__track::-webkit-progress-bar {
  background: transparent;
}
.bal__track::-webkit-progress-value {
  background: var(--s8-color-accent);
  border-radius: var(--s8-radius-sm);
}
.bal__track::-moz-progress-bar {
  background: var(--s8-color-accent);
  border-radius: var(--s8-radius-sm);
}
.bal__l {
  display: flex;
  justify-content: space-between;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

.avatar {
  /* Rule 04 #9 — 32px, the top bar's one control height, not a sixth size of its own. */
  width: var(--s8-control-h);
  height: var(--s8-control-h);
  flex: none;
  border-radius: var(--dir-avatar-radius, 99px);
  background: var(--s8-color-border-strong);
  display: grid;
  place-items: center;
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  color: var(--s8-color-text);
}
/* The account menu's trigger is the avatar plus a caret, not the avatar alone -- a square with
   two letters in it gave no sign it opens anything. A wrapper around `.avatar`, not a modifier
   on it: `.avatar` alone stays a plain 32px square everywhere else it is used. */
.avatar-menu {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-1);
  cursor: pointer;
}
.avatar-menu__caret {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}

/* The component page has no page grid to size `.sidebar` from `.app`'s `grid-template-columns`
   below (--s8-sidebar-w, and 56px under the same max-width:1024px this file already collapses
   `.app` at) -- `.sidebar` sizes its own container query (`container: sidebar / inline-size` just
   below) against whatever width it actually has, so demoing it in a plain, unconstrained
   `.card__body` renders a wider Scale banner than production ever shows at every width, and
   disagrees with every real screen's own measurement of the same classes (verify.mjs's "matches
   the component page"). Kept in step with `.app`'s own breakpoint, not a new one. */
.compdemo--sidebar .sidebar {
  width: var(--s8-sidebar-w);
  /* `.scalebanner`'s `margin-top: auto` (below) only reads as "pinned to the floor" (26.2) when
     the flex column actually has floor to push to; a demo sized to its own content puts the banner
     right under the last nav item instead, indistinguishable from "next in the list". A real page's
     `.sidebar` runs the full column height (`.app`'s grid row), which this height stands in for. */
  height: 640px;
}
/* Rule 04 #23 — `.sidebar`'s own `padding-bottom: var(--s8-preview-reserve)` (96px) exists so a
   real page's fixed `.preview` statebar never covers the banner; this demo has no fixed
   statebar, so the reserve only pushed the pinned banner ~100px above the box's own visible
   bottom. A margin on `.scalebanner` itself, not `.sidebar`'s own padding -- `.sidebar`'s
   padding is one of the exact signatures `verify.mjs` compares against every real screen's own
   `.sidebar` (`measure: matches the component page`), and margin is not part of that signature.
   Position only; the meter's own fill is untouched. */
.compdemo--sidebar .scalebanner {
  margin-bottom: calc(var(--s8-space-4) - var(--s8-preview-reserve));
}
@media (max-width: 1024px) {
  .compdemo--sidebar .sidebar {
    width: 56px;
  }
  /* The viewport has already collapsed the rail; a toggle offering the same state again has
     nothing to do. */
  .sidebar__toggle {
    display: none;
  }
  /* Rule 04 #1 — belt and braces beside `@container sidebar (max-width: 80px)`: the viewport has
     already forced the rail width above, so the collapse chevron has nothing to reopen and stays
     hidden on the same condition, not only on the container's own (it was found floating loose
     at rail width on the component page). */
  .scalebanner__collapse {
    display: none;
  }
}
.compdemo--sidebar {
  position: relative;
}
/* Above the width the existing max-width:1024px query already force-collapses the demo sidebar at
   (two lines up) -- the manual toggle only ever offers a state the viewport is not already in,
   and staying out of that query means this rule can never win a specificity fight against the
   `display: none` it depends on, which is exactly the bug a first version of this had: a
   higher-specificity selector here silently un-hid the labels the query had already hidden,
   leaving real text overflowing a 56px rail. */
@media (min-width: 1025px) {
  .compdemo--sidebar .sidebar {
    transition: width var(--s8-duration-normal) var(--s8-ease);
  }
  /* Collapse and expand deliberately use different timing, not one rule shared both ways: collapse
     fades labels first (fast) then shrinks the column (normal, delayed by the fade); expand
     reverses the order, so the column grows first and labels fade back in only once there is room. */
  .compdemo--sidebar:has(#sidebar-collapse:checked) .sidebar {
    width: 56px;
    transition-delay: var(--s8-duration-fast);
  }
  .compdemo--sidebar .nav__item span:not(.nav__icon) {
    display: inline-block;
    max-width: 140px;
    overflow: hidden;
    white-space: nowrap;
    vertical-align: middle;
    transition:
      max-width var(--s8-duration-fast) var(--s8-ease) var(--s8-duration-normal),
      opacity var(--s8-duration-fast) var(--s8-ease) var(--s8-duration-normal);
  }
  .compdemo--sidebar:has(#sidebar-collapse:checked) .nav__item span:not(.nav__icon) {
    max-width: 0;
    opacity: 0;
    transition:
      max-width var(--s8-duration-fast) var(--s8-ease),
      opacity var(--s8-duration-fast) var(--s8-ease);
  }
  .compdemo--sidebar:has(#sidebar-collapse:checked) .nav__item {
    justify-content: center;
    padding: 0;
  }
}
/* An edge-mounted button, not a nav item -- the flex column's own footer spot is the scale
   banner's (`margin-top: auto`, `sidebar.html`). */
.sidebar__toggle {
  position: absolute;
  top: var(--s8-space-4);
  /* Mounted on the sidebar's OWN right edge, not the wider `.compdemo--sidebar` card body it sits
     in -- `right` alone positions against the nearest positioned ancestor's edge, which is the
     card, not the 216px column inside it. `left: sidebar width - half the button` tracks the
     sidebar's real edge instead, and moves with it when the width below changes. */
  left: calc(var(--s8-sidebar-w) - 14px);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--s8-radius-sm);
  border: 1px solid var(--s8-color-border-strong);
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text-muted);
  cursor: pointer;
  transition: left var(--s8-duration-normal) var(--s8-ease);
}
.sidebar__toggle:hover {
  color: var(--s8-color-text);
}
.sidebar__toggle .nav__icon {
  opacity: 1;
  transition: transform var(--s8-duration-normal) var(--s8-ease);
}
.compdemo--sidebar:has(#sidebar-collapse:checked) .sidebar__toggle .nav__icon {
  transform: rotate(180deg);
}
.compdemo--sidebar:has(#sidebar-collapse:checked) .sidebar__toggle {
  left: 42px;
  transition-delay: var(--s8-duration-fast);
}

/* cv-notice is `position: absolute` with no natural height of its own (comment on its own rule,
   above); a demo host needs one reserved, or the notice overlaps whatever the page puts after it.
   240px, not 180px: the tallest form (`--live`) sits at `top: calc(space-5 + 52px)` and stands
   ~161px on its own, so the box needs ~237px to actually contain it. 180px let it spill ~58px
   below the card on every width -- the "canvas notice sits outside its box" defect Hasnat
   reported, reproduced here and on the real canvas screens' own reservation below. */
.compdemo--notice {
  position: relative;
  height: 240px;
}

/* `.cvside` is `position: absolute; inset` against its nearest positioned ancestor (`.cvbody` on
   a real screen) -- the same reservation `.compdemo--sidebar` makes for `.sidebar`, and to the
   same height, so the panel demos at the size it actually runs at rather than a plain card's
   content height. */
.compdemo--panel {
  position: relative;
  height: 640px;
}

/* `.compdemo--*` reserves height two ways, on purpose, not by drift: `--notice`/`--panel`/`--staged`
   above set it on the wrapper the demoed piece is itself `position: absolute` against (the wrapper
   is what needs the reservation); `--sidebar`/`--canvas` below instead size a specific descendant
   (`.sidebar`, `.cvwrap`) that carries its own explicit width/height on a real screen, where the
   wrapper's own box isn't the thing being measured. Same goal, and the split tracks a real
   difference in which element the size actually belongs to. */
/* `.cvwrap`/`.cvbody` are `flex: 1; min-height: 0` against a real screen's full-height flex
   column (`.page--fill` down to `.states`/`.state`); a plain `.card__body` gives that chain no
   height to fill, so `.cvbody` collapses to nothing. `.cvtop`/`.cvctl`/`.cvcard` are then
   `position: absolute` against it, same reservation need as `.compdemo--panel` above. Sized for
   two cards and the controls cluster, not the full drawing. */
.compdemo--canvas .cvwrap {
  height: 420px;
}

/* Same reservation, for the drawer/staged-tray comparison below -- no cards, so its own name
   rather than reusing `.compdemo--canvas`. */
.compdemo--drawer .cvwrap {
  height: 420px;
}

/* `.cvstaged` is `position: absolute` (its own rule, below) against `.cvbody` on a real screen, exactly
   the reservation problem `.compdemo--notice` already solves for `.cvnotice` -- without it the
   collapsed strip renders outside a plain `.card`'s own (near-zero) height and is covered by the
   next card's opaque surface. Sized to the collapsed peek bar; the open/expanded rows are the
   click-through state `capture.mjs` reaches, not the resting render. */
.compdemo--staged {
  position: relative;
}
/* Doubled class so the inset beats `.card__body--flush` on the same element. */
.compdemo--staged.card__body {
  padding: var(--s8-space-6);
}
/* In flow here, so the open tray has room instead of spilling over the next section. */
.compdemo--staged > .cvstaged {
  position: relative;
  inset: auto;
}

/* `.stepper__pane` needs a bounded height to have anything to scroll within -- the same
   reservation problem every other `.compdemo--*` here solves, this time for real scroll rather
   than absolute positioning. */
.compdemo--stepper .stepper__pane {
  height: 220px;
}

/* `.stream` needs a bounded flex-column height to reserve, same reason as every other
   `.compdemo--*` above. */
.compdemo--stream .stream {
  height: 260px;
  margin: 0;
}

/* The log stream's own chrome -- real v0 (`23-build.html`/`32-logs.html`), no v1 CSS until now.
   `.stream`/`.stream__body`/`.lr`/`.spark`/`.srch` already existed and needed no change; this is
   what wraps them. */
.histaxis {
  display: flex;
  justify-content: space-between;
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  padding: 0 var(--s8-space-6);
  margin-bottom: var(--s8-space-2);
}
/* v0's volume bars were a `<span style="height: N%">` row -- `style=""` is banned in v1, so this
   is real SVG geometry instead (the same fix `.spark` already made for its own line chart), never
   a computed inline height. */
.hbrow {
  display: block;
  width: 100%;
  height: 40px;
  padding: 0 var(--s8-space-6);
}
.hb {
  fill: var(--s8-color-border-strong);
}
.logbar {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: 0 var(--s8-space-4);
  height: 44px;
  flex: none;
  border-bottom: 1px solid var(--s8-color-border);
}
.live {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex: none;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.jump {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s8-space-2);
  position: absolute;
  left: 50%;
  bottom: var(--s8-space-4);
  transform: translateX(-50%);
  padding: var(--s8-space-2) var(--s8-space-4);
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  font-size: var(--s8-text-sm);
  cursor: pointer;
  box-shadow: var(--s8-shadow-overlay);
  transition: border-color var(--s8-duration-fast) var(--s8-ease);
}
/* Hover elevation, colour only -- Geist's approach (border -> strong), the same pattern already
   established on `.opt` (base.css :4262); Neo is flat/square by design, so no shadow-lift or
   translateY (Material's approach, explicitly not this brand's). A real gap found auditing this
   unit's own new pieces: `.jump` had no hover state at all until now. */
.jump:hover {
  border-color: var(--s8-color-text-muted);
}
.jump__n {
  font-weight: var(--s8-weight-semibold);
}

/* ------------------------------------------------------------- sidebar --- */
.sidebar {
  grid-area: sidebar;
  /* 26 D2 — the Scale banner adapts to the real sidebar width, including the canvas icon rail. */
  container: sidebar / inline-size;
  padding: var(--s8-space-4) var(--s8-space-2);
  /* Phase-1 gate — the fixed review statebar at the foot of the viewport (`.preview`) covered the
     Scale banner at 1440×900. `.sidebar` is the one shell region that runs the full column height,
     so it is the one that needs the same reserve `.main` and `.lone` already carry. */
  padding-bottom: var(--s8-preview-reserve);
  /* `overflow-y: auto` alone computes `overflow-x` to `auto` too (the UA default pairing), not
     `visible` — so a future `.markchip`/`.cvpeek`-style hover explanation on a sidebar item would
     get clipped at this box's own edge rather than escaping it. Noted here rather than discovered
     later; nothing in this file currently opens sideways off a `.nav__item`. */
  overflow-y: auto;
  background: var(--dir-sidebar-bg, var(--s8-color-surface));
  border-right: var(--dir-border-w, 1px) solid var(--s8-color-border);
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-1);
}
.nav__group {
  margin: var(--s8-space-4) 0 var(--s8-space-1) var(--s8-space-3);
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
}
.nav__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-3);
  border-radius: var(--dir-nav-radius, var(--s8-radius-sm));
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-base);
  text-transform: var(--dir-nav-case, none);
  letter-spacing: var(--dir-nav-tracking, normal);
  cursor: pointer;
  transition:
    background var(--s8-duration-fast) var(--s8-ease),
    color var(--s8-duration-fast) var(--s8-ease);
}
.nav__item:hover {
  background: var(--s8-color-surface-sunken);
  color: var(--s8-color-text);
}
.nav__item[aria-current='page'] {
  background: var(--dir-nav-active-bg, var(--s8-color-surface-sunken));
  color: var(--dir-nav-active-fg, var(--s8-color-text));
  font-weight: var(--s8-weight-medium);
}
.nav__item[aria-current='page']::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: var(--dir-nav-bar-w, 0);
  height: 60%;
  background: var(--s8-color-wayfind);
  border-radius: 0 2px 2px 0;
}
.nav__item[aria-disabled='true'] {
  opacity: 0.42;
  cursor: not-allowed;
  /* RF-I2. `Deploy`, `Services` and `Logs` on 04 carried aria-disabled, a reason in `title`, this
     opacity and this cursor -- and a live `href`, so all three navigated on click. The ANV-30 gate
     asserts every ENABLED nav item is a real link, so a disabled item that is a real link passed
     it. 02 and 03 model the same lock correctly by omitting the href; this makes the styling carry
     the behavior so the two ways of writing it cannot disagree again. */
  pointer-events: none;
}
/* A nested nav entry -- the same list with a filter already on it. Inference and Data are not
   destinations of their own: forge §5.4 makes an application, a managed datastore and an inference
   service three typed specifications of ONE `Service`, so three top-level entries would tell a
   customer there are three kinds of thing. Indented under the entry they filter, and a size down,
   so the relationship is in the chrome rather than in a sentence somebody has to read. */
.nav__item--sub {
  padding-left: calc(var(--s8-space-3) + 14px + var(--s8-space-3));
  font-size: var(--s8-text-sm);
}

.nav__icon {
  /* 2026-09-24 visual correction — SVG stroke stays consistent across fonts and themes. */
  display: block;
  width: 16px;
  height: 16px;
  flex: none;
  opacity: 0.85;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.nav__lock {
  margin-left: auto;
  font-size: var(--s8-text-xs);
  opacity: 0.7;
}

/* The environment's one primary action, pinned above the nav. NOT a nav item, because it is not a
   destination you can be on -- nothing ever marks it `aria-current`, and the gate that asserts
   exactly one current nav item would need an exception carved for it if it were one.

   And deliberately NOT `.btn btn--primary` either, which is what it wore first. The happy-path
   walk clicks `a.btn:visible:has-text("Deploy")` and takes the FIRST match in the document -- so
   a Deploy button in the sidebar, above `.main`, swallowed the click on every screen in the flow
   and the walk landed back on the launcher. A pinned action that appears on 72 screens must not
   answer to a selector written for a page's own actions. It takes `.nav__item`'s box so its icon
   lines up with the column below it, and the accent from the palette so it still reads primary. */
.nav__cta {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-3);
  margin-bottom: var(--s8-space-2);
  border-radius: var(--dir-nav-radius, var(--s8-radius-sm));
  border: var(--dir-border-w, 1px) solid var(--s8-color-accent);
  background: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  text-transform: var(--dir-btn-case, none);
  letter-spacing: var(--dir-btn-tracking, normal);
  white-space: nowrap;
  cursor: pointer;
}

/* 26 D2 — the Scale banner at the foot of the sidebar, Base only. It is always there rather than
   dismissible for good, and it is collapsible to one line: the checkbox+label pair below is the
   set's existing script-free toggle mechanism (the same shape `.menu__toggle` already uses), and
   stands in for the browser-remembered collapse a real build persists. Prominence comes from the
   accent fill on `__meter`, never a glow — 12 forbids glow beyond `.skel`/`.cvbody`. */
.scalebanner {
  margin-top: auto;
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  padding: var(--s8-space-2);
  background: var(--s8-color-surface-raised);
}
/* 26 D2, corrected (Phase-1 architect gate) — the collapse control was inside `.scalebanner__body`,
   which the checked rule hides, so collapsing it also hid the only thing that could reopen it.
   `.scalebanner__h` (the heading and the collapse control) is now a sibling of `.scalebanner__body`
   in the markup, never hidden; only `__meter`/`__p` fold away. Known mockup limitation, the same
   class the `menu`/`aria-expanded` note above already records: the chevron's `aria-label` cannot
   flip between "Collapse" and "Expand" without script, so it stays one word and the rotate carries
   the rest of the meaning. */
.scalebanner__toggle:checked ~ .scalebanner__body {
  display: none;
}
.scalebanner__toggle:checked ~ .scalebanner__h .scalebanner__collapse {
  transform: rotate(180deg);
}
.scalebanner__toggle:checked ~ .scalebanner__h {
  margin-bottom: 0;
}
/* Scale banner, signed off 2026-09-27, removed with the glow at V1-F11 (`a7c7668`), restored here
   alone: C-19 amended by Hasnat 2026-09-29 to keep the meter's one-shot sheen on arrival while the
   glow modifier (`--glow`) stays removed. Named exception to C-19's tokens-only motion rule, same
   shape as F-11's 24 named exceptions -- the duration is the one already signed off, not re-derived
   from `--s8-duration-*`, which tops out at `--slow` (240ms), far short of a sheen meant to be seen
   crossing the whole meter once. */
.scalebanner__meter {
  height: 3px;
  border-radius: var(--s8-radius-sm);
  margin-bottom: var(--s8-space-2);
  background:
    linear-gradient(90deg, transparent, var(--s8-color-wayfind), transparent)
      no-repeat -100% 0 / 50% 100%,
    var(--s8-color-accent);
  animation: s8-sheen 1.2s var(--s8-ease) 0.3s 1 both;
}
@keyframes s8-sheen {
  to {
    background-position:
      200% 0,
      0 0;
  }
}
.scalebanner__h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--s8-space-1);
  font-size: var(--s8-text-sm);
  font-weight: var(--s8-weight-semibold);
}
.scalebanner__title {
  color: var(--s8-color-text);
  text-decoration: none;
}
.scalebanner__title:hover {
  color: var(--s8-color-wayfind);
}
.scalebanner__body {
  margin-top: 0;
}
.scalebanner__p {
  margin: 0;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}
.scalebanner__p a {
  color: var(--s8-color-wayfind);
}
.scalebanner__ask {
  display: none;
}
.scalebanner__collapse {
  flex: none;
  display: inline-block;
  cursor: pointer;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  transition: transform var(--s8-duration-fast) var(--s8-ease);
  /* Rule 04 #1 — at full width this sat close enough to `.sidebar`'s own right border to read as
     straddling it; a little breathing room, position only. */
  margin-right: var(--s8-space-1);
}
.scalebanner__collapse:hover {
  color: var(--s8-color-text);
}
/* 26 D2 — at 56px the sidebar is an icon rail. Keep the Scale invitation reachable without
   squeezing a full paragraph into a 38px card. This also covers the responsive rail at 768px. */
@container sidebar (max-width: 80px) {
  .scalebanner {
    padding: var(--s8-space-1);
  }
  .scalebanner__h {
    display: block;
    margin: 0;
    text-align: center;
  }
  .scalebanner__title {
    display: block;
    font-size: 10px;
    line-height: 1.2;
  }
  /* look-b 18 — the accent bar under the title, plus the bordered `.scalebanner` box, read as a
     tab or a progress indicator at rail width. `Ask` is plain link text, nothing under it. */
  .scalebanner__body,
  .scalebanner__collapse {
    display: none;
  }
  .scalebanner__ask {
    display: block;
    margin-top: var(--s8-space-1);
    font-size: 10px;
    color: var(--s8-color-wayfind);
  }
}

/* ---------------------------------------------------------------- main --- */
.main {
  grid-area: main;
  /* Applied always, so a screen with state variants and one without have the same page rhythm --
     a bar that changes the layout when it appears is a bar that makes two screens incomparable. */
  padding-bottom: var(--s8-preview-reserve);
  overflow: auto;
}
/* One page box on every screen. There used to be three tiers -- 1180 for lists, 960 for a form
   beside a panel, 860 for a settings list -- each centered with `margin: 0 auto`. Centring meant a
   narrower page did not just end sooner, it STARTED FURTHER RIGHT: walking 15 -> 16 -> 17 the
   heading moved 422 -> 262 -> 372. Seven different left edges across 26 screens, and the eye reads
   that as the whole product shifting under it.
   The width a page can be and the measure its content should have are two different questions.
   This answers the first; `--s8-measure-*` below answers the second, on the components that
   actually need it. A field is narrow because it is a field, not because of the page it is on. */
.page {
  max-width: var(--s8-container-main);
  /* More air above than beside. The page used to be an even 24px box, which put the heading 24px
     under a 52px bar of chrome -- the title read as attached to the top nav rather than as the
     start of the page. 40px above separates the two, and the pagehead's own bottom margin then
     reads as belonging to the heading rather than competing with the gap above it. */
  padding: var(--s8-space-10) var(--s8-space-6) var(--s8-space-6);
  margin: 0 auto;
}

/* A screen whose BODY scrolls inside the viewport instead of the page scrolling -- 07's build log
   and 11's log stream, where following a tail means the tail must stay on screen.
   Both screens built this privately, as `.buildhead` and `.lhead`, and both copied the page's
   `padding: 24px 24px 0` while copying neither its `max-width` nor its `margin: 0 auto`. So their
   headings sat at x=240 while every other screen's sat at 262 -- the "logs hugs the sidebar" that
   started this. A private copy of a shared component keeps the part you can see and drops the part
   you cannot. */
.main--fill {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.page--fill {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  width: 100%;
  padding-bottom: 0;
}
/* The fixed head of a fill page -- the title and controls that stay put while the body scrolls
   under them. 07 called it `.buildhead`, 11 called it `.lhead`, and both were the same single
   declaration. */
.fillhead {
  flex: none;
}

/* A page whose content is a single column -- a settings list, a form and the panels that belong
   with it. The PAGE is unchanged: same width, same left edge, same box as every other screen.
   This caps only how wide the content inside it runs.
   That distinction is the entire lesson of RF-L1. The class this replaces, `.page--form`, meant
   "make this page narrower", and a narrower CENTRED page starts further right -- so a decision
   about measure silently became a decision about position. Capping the content instead leaves the
   position alone.
   It applies to the children rather than the page because the blocks have to agree with EACH
   OTHER: capping just the settings list left 23 with a form ending at 1122 and a danger panel
   beside it ending at 1394, which reads as a broken column rather than a deliberate measure.
   It sits HERE, above the components, on purpose. It is a ceiling a component may tighten, never
   a floor that widens one. Declared after them it won on source order at equal specificity and
   stretched `.plain` from its 62ch prose measure out to 860 -- a page-level default silently
   overruling a deliberate local one, which is the same mistake as a page width standing in for a
   measure, just pointing the other way.
   `.preview` is excluded because it is chrome, not content -- a fixed full-width bar that the cap
   shortened to 860 on 13 and 17, which looked like the switcher had broken. The exclusion is
   wrapped in `:where()` so it contributes no specificity: written as a bare `:not(.preview)` it
   would raise this to 0,2,0 and re-break the ceiling rule the paragraph above exists for. */
/* `.cols` is the second exclusion, added 2026-09-14, and it is a MEASUREMENT rather than a taste.
   `.cols` is a two-region grid -- a working column plus a 300px rail -- and a form measure is a
   cap for ONE column of prose. Capping the pair squeezes both: measured at 1850, `17-create-
   environment` and `39-sizing-moment` ended their rail at 1327 against a page edge of 1623, so
   **296px of the page box sat empty to the right of the rail** while the form beside it was
   crushed to about 520. `38-model-card` and `51-template-detail` draw the identical `.cols` grid
   WITHOUT the cap and end flush at 1599 -- and `51` is the one Hasnat pointed at and said looked
   right, against `17` which "feels like it is on the left side leaving a lot of space".
   This plan already recorded the same arithmetic from the other direction: the parked `command`
   layer "fitted a 320px rail into that and left a 516px working column -- narrower than base",
   and the multi-region layers lift the cap for exactly this reason. */
.page--column > :not(:where(.preview, .cols)) {
  max-width: var(--s8-measure-form);
}
/* And the same exclusion one level down, because on a screen with state variants the chain is
   `.page--column > .states > .state > .cols` and the cap lands on `.states`, which then squeezes
   the grid inside it just as effectively. `39-sizing-moment` is that shape and stayed at 1327
   after the rule above fixed `17-create-environment`. Scoped with `:has()` so it lifts ONLY for a
   `.states` that actually contains a two-region grid -- every other state screen keeps its form
   measure. One level of `:has()`, never nested: this file records that `:has()` inside `:has()`
   is invalid and fails silently, taking the whole rule with it. */
.page--column > .states:has(.cols) {
  max-width: none;
}

/* The path to this screen, sitting above its heading.
   NOT in the top bar. The top bar is a SCOPE SWITCHER -- project and environment are the context
   you are working IN, not steps in a path. An earlier attempt at this turned that switcher into a
   breadcrumb by stripping its labels and appending the object, and it broke the one control on
   every screen that says where you are working. Hasnat: "they were for the project and environment
   and that was correct for what it was."
   The two live at different altitudes and neither repeats the other: the bar answers WHICH project
   and environment, this answers WHERE IN THIS ENVIRONMENT.
   The current screen is NOT in the trail -- it is the <h1> directly below, and repeating it would
   put the same word twice, eight pixels apart. */
.crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s8-space-2);
  margin: 0 0 var(--s8-space-2);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}
.crumbs a {
  color: var(--s8-color-text-muted);
  text-decoration: none;
}
.crumbs a:hover {
  /* A link, so wayfinding rather than the fill. */
  color: var(--s8-color-wayfind);
  text-decoration: underline;
}
.crumbs__sep {
  /* Was `--s8-color-border-strong` -- a border token, calibrated for a hairline's visibility, not
     for text; measured 1.75:1 as text against the surface (below the 4.5:1 floor), the first time
     `.crumbs` is actually rendered anywhere in v1. `--s8-color-text-muted` is what the trail's own
     links already use at this size and passes. */
  color: var(--s8-color-text-muted);
  flex: none;
}
/* The section's own icon, the same glyph the sidebar uses for it. The crumb and the nav item are
   the same place named twice, so they should look like it -- and it gives the trail an anchor the
   eye finds before it reads any words. */
.crumbs__icon {
  margin-right: var(--s8-space-1);
  opacity: 0.75;
}
/* The current screen. Present, because a trail that stops at the parent does not show you where
   you are -- it shows you where you came from, and the reader has to infer the last step. Not a
   link, because a link to here does nothing. */
.crumbs__here {
  color: var(--s8-color-text);
}

/* 03 D6, to-draw #3 (C) — the trail at organisation level: dimmed and clickable, the page's own
   name carried by the `<h1>` in `.pagehead` below rather than repeated here. NOT `.crumbs`: that
   trail says where you are INSIDE one environment; this is the only trail an organisation screen
   carries, and it says where you were before stepping off the canvas to reach it. Arrived by link
   with no history, it is just the organisation's name — decision 03 D6's second state. */
.orgtrail {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s8-space-2);
  margin-bottom: var(--s8-space-2);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}
.orgtrail a {
  color: var(--s8-color-text-muted);
  text-decoration: none;
}
.orgtrail a:hover {
  color: var(--s8-color-wayfind);
  text-decoration: underline;
}
/* 03 D6 — a reference tab may demonstrate the trail's other real state (arrived directly, no
   prior page) beside a screen's own "keeps where you were" trail. `.orgtrail--nohistory` sits as
   a sibling of the included trail, in the trail's own position, and this pair of rules ensures
   exactly one of the two ever renders: the alternative starts hidden and the default hides only
   once the alternative's own tab is checked. `#v-3` is the one screen using this today
   (63-people); the id is the reference tab's own, not a shared family like `o-1`/`o-2`. */
.orgtrail--nohistory {
  display: none;
}
#v-3:checked ~ .orgtrail--nohistory {
  display: flex;
}
#v-3:checked ~ .orgtrail:not(.orgtrail--nohistory) {
  display: none;
}

.pagehead {
  display: flex;
  align-items: flex-start;
  /* Wraps. A title plus two actions fits at 1440 and does not at 768, where the actions ran 69px
     past the pane -- and the phone tier only started stacking this at 720, so there was a window
     where it simply overflowed. Wrapping is the behavior at every width; the phone tier still
     does the rest. */
  flex-wrap: wrap;
  gap: var(--s8-space-4);
  margin-bottom: var(--s8-space-8);
}
.pagehead__title {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-2xl);
  /* Direction-controlled, defaulting to what every direction used before `kordin` existed, so the
     other three need no declaration and cannot shift. Same shape as `--dir-label-tracking` two
     hundred lines down, which has worked this way since the directions were written.
     `kordin` is why: DESIGN.md sets display and section headings at weight 100, uppercase, and
     calls the thin-uppercase headline "a core part of the identity". `--s8-weight-thin: 100` has
     sat in this file unused since it was written, with a comment saying it waits for a direction
     to ask for it. This is that direction asking. */
  font-weight: var(--dir-display-weight, var(--s8-weight-semibold));
  text-transform: var(--dir-display-transform, none);
  line-height: var(--s8-leading-tight);
  letter-spacing: var(--dir-display-tracking, -0.02em);
}
.pagehead__sub {
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-md);
  margin-top: var(--s8-space-1);
}
.pagehead__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  /* Wraps too, and for the same reason one level down: the head wrapping only moves the actions
     onto their own line, and two buttons on one line are still 337px wide in a 334px pane.
     `flex: none` is what stopped it -- `0 0 auto` sizes to max-content and never shrinks, so the
     wrap had nothing to wrap into. It has to be allowed to shrink before it can wrap. */
  flex-wrap: wrap;
  flex: 0 1 auto;
  min-width: 0;
}

/* ------------------------------------------------------------ elements --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s8-space-2);
  height: var(--s8-control-h);
  padding: 0 var(--s8-space-4);
  border-radius: var(--s8-radius-sm);
  /* `control-border` is one step from the raised card fill in dark, so a plain button read as
     bare text beside a ghost one; `border-strong` is the step that reads in both themes. */
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  cursor: pointer;
  white-space: nowrap;
  text-transform: var(--dir-btn-case, none);
  letter-spacing: var(--dir-btn-tracking, normal);
  transition:
    background var(--s8-duration-fast) var(--s8-ease),
    border-color var(--s8-duration-fast) var(--s8-ease);
}
/* Flow-advancing actions are anchors, because a <button> in a static mockup cannot navigate and a
   design nobody can walk cannot be reviewed. `.btn` is a class selector so the styling already
   applies -- these two properties are the only things an anchor brings that a button does not. */
a.btn {
  text-decoration: none;
  color: var(--s8-color-text);
}
a.btn--primary {
  color: var(--s8-color-accent-contrast);
}
/* And the same trap one modifier over, found 2026-09-13. `a.btn` above is 0,1,1 and
   `.btn--danger` is 0,1,0, so an ANCHOR carrying the danger modifier lost its colour to the
   ordinary text colour -- five of them, on `42-deployed-model`, `62-people`, `73-connections`
   and `72-api-keys`. Proved without planting anything: `62-people` and `73-connections` each
   draw a <button> and an <a> with the identical class list in one file, and they measured
   rgb(231,0,11) against rgb(238,238,238). Nothing could see it -- the class has a rule, the
   contrast is fine, and no colour literal is involved. */
a.btn--danger {
  color: var(--s8-color-danger);
}

.btn:hover {
  border-color: var(--s8-color-text-muted);
}
.btn--primary {
  background: var(--s8-color-accent);
  border-color: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
}
.btn--primary:hover {
  background: var(--s8-color-accent-hover);
  border-color: var(--s8-color-accent-hover);
}
.btn--ghost {
  background: transparent;
  border-color: transparent;
}
.btn--ghost:hover {
  background: var(--s8-color-surface-sunken);
  border-color: var(--s8-color-border);
}
/* Ruled by Hasnat 2026-09-26, from a real look at the component page: there was no button
   between `.btn--primary` (filled) and `.btn--ghost` (invisible until hover) -- so an action a
   reader needs to actually notice on sight (the canvas notice's Copy/Open/See why, Load more)
   had nowhere correct to go, and every one of them was reaching for `--ghost` and rendering as
   plain text. In dark theme this was not just understated: `--s8-color-control-border`
   (#1c1a1a) sits one step from `--s8-color-surface-raised` (#1d1b1b), the same fill both the
   button and its usual `.card` parent already use, so the plain `.btn`'s own border reads as
   nothing there. `--secondary` uses the sunken surface deliberately -- the one token already a
   real step away from raised in both themes -- so it reads as a control before any hover state. */
.btn--secondary {
  background: var(--s8-color-surface-sunken);
  border-color: var(--s8-color-border-strong);
}
.btn--secondary:hover {
  background: var(--s8-color-surface);
  /* Pulse is a fill, never a stroke (DESIGN.md:28) -- `--s8-color-wayfind` is the token that
     rule already exists for, not a new one. */
  border-color: var(--s8-color-wayfind);
}
/* Rule 04 #15 — in dark, `surface-sunken` IS `NULL` (#000000), a step BELOW its usual `.card`
   parent's `surface-raised` (#1d1b1b), the lightest of the three -- so the button read as a hole
   punched into the card rather than a raised control. Light keeps sunken (Shell, lighter than
   its own card there); dark gets the raised token instead, with a hover step lighter still
   (DESIGN.md: "Hover goes lighter on dark"). */
:root:not([data-theme='light']) .btn--secondary {
  background: var(--s8-color-surface-raised);
}
:root:not([data-theme='light']) .btn--secondary:hover {
  background: var(--s8-color-border);
}
/* Small buttons sit beside chips, tabs and segments, so they are the same height as those.
   26px put them 2px short of every neighbour and 6px short of the top bar's search box. */
.btn--sm {
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  font-size: var(--s8-text-sm);
}

.card {
  background: var(--s8-color-surface-raised);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  position: relative;
  /* Same reasoning as `.callout`: a bordered box needs air on the side you leave it by, or the
     next block sits on its border and the join reads as a rendering fault. Six screens had a
     section label or a table touching a card. */
  margin-bottom: var(--s8-space-6);
}
.card--corners::before,
.card--corners::after {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  border-color: var(--s8-color-wayfind);
  border-style: solid;
  opacity: var(--dir-corner-opacity, 0);
}
.card--corners::before {
  top: -1px;
  left: -1px;
  border-width: 1px 0 0 1px;
}
.card--corners::after {
  bottom: -1px;
  right: -1px;
  border-width: 0 1px 1px 0;
}
/* A `.card` that is itself a native `<details>` -- a modifier, not a new family (rule 2): every
   other `.card` property (background, border, radius, margin) still applies unchanged. */
.card--expand {
  padding: 0;
}
.card--expand > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s8-space-3);
  padding: var(--s8-space-4);
  cursor: pointer;
  list-style: none;
  font-weight: var(--s8-weight-medium);
}
.card--expand > summary::-webkit-details-marker {
  display: none;
}
.card--expand > summary::after {
  content: '';
  flex: none;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--s8-color-text-muted);
  border-bottom: 1.5px solid var(--s8-color-text-muted);
  transform: rotate(45deg);
  transition: transform var(--s8-duration-fast) var(--s8-ease);
}
.card--expand[open] > summary::after {
  transform: rotate(-135deg);
}
/* `::details-content` is the box holding everything after `<summary>` -- the only thing a native
   `<details>` lets CSS animate the native open/close of, since that box (not an author-written
   wrapper) is what the browser itself shows and hides. `content-visibility` transitions as a
   discrete property (`allow-discrete`): `hidden` only takes effect once `height` has finished
   animating TO 0, so the content stays visible (and its height interpolatable, `:root` above) for
   the whole collapse instead of disappearing a frame early. */
.card--expand::details-content {
  height: 0;
  overflow: hidden;
  padding: 0 var(--s8-space-4);
  content-visibility: hidden;
  transition:
    height var(--s8-duration-normal) var(--s8-ease),
    content-visibility var(--s8-duration-normal) var(--s8-ease) allow-discrete;
}
.card--expand[open]::details-content {
  height: auto;
  padding: 0 var(--s8-space-4) var(--s8-space-4);
  content-visibility: visible;
}
.card__head {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.card__title {
  font-size: var(--s8-text-sm);
  font-weight: var(--s8-weight-semibold);
  text-transform: var(--dir-cardtitle-case, none);
  letter-spacing: var(--dir-cardtitle-tracking, normal);
  color: var(--dir-cardtitle-color, var(--s8-color-text));
}
.card__actions {
  margin-left: auto;
  display: flex;
  gap: var(--s8-space-2);
}
.card__body {
  padding: var(--s8-space-4);
}
/* The card's padding is the inset; a child's own outer margin on top of it made the far edge
   20-24px deeper than the near one (`.field`, `.fields`, `.fbar`, `.upsell`). */
.card__body > :first-child {
  margin-top: 0;
}
.card__body > :last-child {
  margin-bottom: 0;
}
.card__body--flush {
  padding: 0;
}
/* A line of prose under flush content (the embedded log's "Open the full log") still needs the
   card's inset; flush it sat on the card's own border. */
.card__body--flush > .plain {
  max-width: none;
  margin: 0;
  padding: var(--s8-space-3) var(--s8-space-4);
  border-top: 1px solid var(--s8-color-border);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  height: 20px;
  padding: 0 var(--s8-space-2);
  border-radius: var(--dir-badge-radius, var(--s8-radius-sm));
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  background: var(--s8-color-surface-sunken);
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-medium);
  text-transform: var(--dir-badge-case, none);
  /* Not `var(--dir-label-tracking, ...)`: that token is for genuine all-caps text
     (`text-transform: uppercase` beside it, e.g. `.mark`, `th`) and Kordin Neo sets it wide
     (0.18em). A badge's case is direction-controlled and none is uppercase, so it stayed mixed
     case at 0.18em -- visibly loose. Fixed tight, matching this component's own case. */
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.dot {
  width: 6px;
  height: 6px;
  border-radius: 99px;
  flex: none;
  background: var(--s8-color-text-muted);
}
.dot--live {
  background: var(--s8-color-success);
}
.dot--warn {
  background: var(--s8-color-warning);
}
.dot--fail {
  background: var(--s8-color-danger);
}
/* Not `--s8-color-accent`: Pulse is a fill for the brand's own emphasis (CTAs, the canvas's
   active edges), and at 6px it reads as a small red dot next to `--fail`'s crimson -- an
   "Updating"/"Sent" state is neither a failure nor the brand's hero colour, it is neutral
   motion. Muted grey, still pulsing, says "in progress" without borrowing danger's colour role
   (DESIGN.md: "Danger ... is invented ... to read as a different colour from the accent", which
   only works if nothing else borrows the accent for a state it does not own). */
.dot--busy {
  background: var(--s8-color-text-muted);
  animation: breathe 1.8s ease-in-out infinite;
}
/* C-19: kept as a named exception — decorative motion is otherwise dropped, but this shows an in-progress state. */
@keyframes breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* Filter chip. Lives here rather than in a screen because more than one screen uses it —
   the first version of this was defined inside the services list and silently fell back to the
   browser's default button style on the logs screen. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  /* `border`, not `control-border`: an unpressed chip must still read as a control on a card. */
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--dir-chip-radius, 999px);
  background: transparent;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  font-family: inherit;
  cursor: pointer;
  text-transform: var(--dir-badge-case, none);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above; a filter chip's content is
     mixed case ("Needs attention", not "NEEDS ATTENTION"). */
  letter-spacing: 0.02em;
  white-space: nowrap;
  transition:
    color var(--s8-duration-fast) var(--s8-ease),
    border-color var(--s8-duration-fast) var(--s8-ease);
}
.chip:hover {
  color: var(--s8-color-text);
  border-color: var(--s8-color-border-strong);
}
.chip[aria-pressed='true'] {
  color: var(--s8-color-text);
  border-color: var(--s8-color-border-strong);
  background: var(--s8-color-surface-raised);
}
.chip__n {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  opacity: 0.7;
}

.tabs {
  display: flex;
  gap: var(--s8-space-1);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
  margin-bottom: var(--s8-space-5);
}
/* Wrapping belongs to the one row that cannot fit -- the environment's, which carries twelve
   destinations once the Scale tabs are granted -- and it was written unscoped, on `.tabs`, under
   a comment claiming "rows of six or fewer -- every other tabbed screen in the set -- are
   unaffected". Both halves of that were wrong: the set has 7- and 8-item rows, and they were
   affected. Measured across all 26 tabbed screens at 1440 / 1024 / 768, wrap against nowrap:

     - 1440: the twelve-item row fits exactly, 1132px of tabs in a 1132px pane. Wrapping changes
       nothing on any screen at this width, so it buys nothing here.
     - 1024: nowrap crushes `Direct access` 111px -> 68px and `Worker pools` 108px -> 70px, two
       lines each, AND still overflows by 4px. The row genuinely needs to wrap.
     - 768: nowrap overflows by 260px and pans `.main` sideways by 236px on ten screens -- the
       exact fault the pane-panning gate exists for. It needs it here most of all.
     - 768, and this is what the unscoped rule cost: the EIGHT-item rows on `25-overview` and
       `26-services-list` fit without it (664px in 664px, no panning) and it made both 24px
       taller for nothing.

   Threshold is nine because nothing in the set sits near it: every environment row is exactly
   twelve children, and every other row in all 26 tabbed screens is eight or fewer. Below 720 the
   collected phone tier wraps every row, which is right there and is a separate rule. */
.tabs:has(> :nth-child(9)) {
  flex-wrap: wrap;
}
.tab {
  position: relative;
  padding: var(--s8-space-2) var(--s8-space-3) var(--s8-space-3);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-base);
  cursor: pointer;
  text-transform: var(--dir-tab-case, none);
  letter-spacing: var(--dir-tab-tracking, normal);
}
.tab:hover {
  color: var(--s8-color-text);
}
.tab[aria-selected='true'] {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
.tab[aria-selected='true']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--s8-color-wayfind);
}

/* ---- product tabs, radio-driven ------------------------------------------------------------
   The shared counterpart to the `v-*` state family, and here for the same reason: a screen adding
   tabs writes MARKUP ONLY -- six radios, six labels, six panes -- and no CSS at all. `27` owned a
   private copy of this, which is how its active-tab marker came to be a near-white fill under
   `kordin` while every other selected thing in the product was blue: the four shared rules were
   repointed at `--s8-color-wayfind` and the copy inside the screen was not.

   Ids are FIXED -- `t-1` .. `t-6` -- because the selectors live here and cannot be generated. They
   are numbered rather than named for the same reason `v-2`..`v-8` are: service detail's third tab
   is Metrics and the model detail's third tab is Metrics too, but the fourth is Deployments on one
   and Playground on the other. The meaning belongs on the label, where a reader sees it.

   Both nesting shapes are covered -- panes wrapped in `.panes`, and panes as direct siblings of
   the radios -- because the state family shipped nine screens with no active marker by supporting
   only one shape, and the nine that broke were the nine that followed the convention. */
.tabs label {
  position: relative;
  padding: var(--s8-space-2) var(--s8-space-3) var(--s8-space-3);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-base);
  cursor: pointer;
  text-transform: var(--dir-tab-case, none);
  letter-spacing: var(--dir-tab-tracking, normal);
}
.tabs label:hover {
  color: var(--s8-color-text);
}
.tabs a {
  padding: var(--s8-space-2) var(--s8-space-3) var(--s8-space-3);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-base);
  text-transform: var(--dir-tab-case, none);
  letter-spacing: var(--dir-tab-tracking, normal);
}
.tabs a:hover {
  color: var(--s8-color-text);
}
.pane {
  display: none;
}
#t-1:checked ~ .tabs label[for='t-1'],
#t-2:checked ~ .tabs label[for='t-2'],
#t-3:checked ~ .tabs label[for='t-3'],
#t-4:checked ~ .tabs label[for='t-4'],
#t-5:checked ~ .tabs label[for='t-5'],
#t-7:checked ~ .tabs label[for='t-7'],
#t-8:checked ~ .tabs label[for='t-8'],
#t-9:checked ~ .tabs label[for='t-9'],
#t-6:checked ~ .tabs label[for='t-6'] {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
#t-1:checked ~ .tabs label[for='t-1']::after,
#t-2:checked ~ .tabs label[for='t-2']::after,
#t-3:checked ~ .tabs label[for='t-3']::after,
#t-4:checked ~ .tabs label[for='t-4']::after,
#t-5:checked ~ .tabs label[for='t-5']::after,
#t-7:checked ~ .tabs label[for='t-7']::after,
#t-8:checked ~ .tabs label[for='t-8']::after,
#t-9:checked ~ .tabs label[for='t-9']::after,
#t-6:checked ~ .tabs label[for='t-6']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  /* Wayfinding, not a fill -- the same rule `.tab[aria-selected]::after` gets above. */
  background: var(--s8-color-wayfind);
}
/* Descendant-tolerant forms, added with the environment batch for the same reason the state
   family needed them: `o-*` sits OUTSIDE `t-*`, so an option's `.tabs` and `.panes` are one
   level below the radios and `~ .tabs` cannot reach them. Ids are one-per-document, so the
   radios stay at page level and both options read the same three. Measured before adding: no
   screen in the set nests one `.panes` inside another. */
#t-1:checked ~ * .tabs label[for='t-1'],
#t-2:checked ~ * .tabs label[for='t-2'],
#t-3:checked ~ * .tabs label[for='t-3'],
#t-4:checked ~ * .tabs label[for='t-4'],
#t-5:checked ~ * .tabs label[for='t-5'],
#t-7:checked ~ * .tabs label[for='t-7'],
#t-8:checked ~ * .tabs label[for='t-8'],
#t-9:checked ~ * .tabs label[for='t-9'],
#t-6:checked ~ * .tabs label[for='t-6'] {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
#t-1:checked ~ * .tabs label[for='t-1']::after,
#t-2:checked ~ * .tabs label[for='t-2']::after,
#t-3:checked ~ * .tabs label[for='t-3']::after,
#t-4:checked ~ * .tabs label[for='t-4']::after,
#t-5:checked ~ * .tabs label[for='t-5']::after,
#t-7:checked ~ * .tabs label[for='t-7']::after,
#t-8:checked ~ * .tabs label[for='t-8']::after,
#t-9:checked ~ * .tabs label[for='t-9']::after,
#t-6:checked ~ * .tabs label[for='t-6']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--s8-color-wayfind);
}
/* THE PART OF A SCREEN THAT SURVIVES A BANNER BUT NOT A PAGE REPLACEMENT.
   `27-service-detail` draws seven states, and they split in two: `Live`, `Crash loop`, `Not
   answering` and `Source disconnected` KEEP the page -- Rule 2, a failure is a banner where the
   person already is -- while `First build`, `Removing it` and `Service is gone` replace it.
   Before this, the tab row and all six panes were copied into each of the four that keep the
   page: measured at 3,500 lines of BYTE-IDENTICAL markup, and every edit to a pane had to land
   four times with an assertion that it had. The panes do not vary with the service's health, so
   they are drawn once and shown for exactly the states that keep the page.
   Ids are fixed here for the same reason every other `:checked` rule in this file is: a screen
   may not write one. BOTH nesting shapes, because `27` wraps its states in an `o-*` option and a
   screen without options does not -- the uncle problem this file already records twice, once for
   the state family and once for `.board`'s inspector, and the same fix. Written the direct form
   first so the descendant form cannot be the only one anybody tests. */
.paneset {
  display: none;
}
#v-ok:checked ~ .paneset,
#v-2:checked ~ .paneset,
#v-3:checked ~ .paneset,
#v-4:checked ~ .paneset,
#v-ok:checked ~ * .paneset,
#v-2:checked ~ * .paneset,
#v-3:checked ~ * .paneset,
#v-4:checked ~ * .paneset {
  display: block;
}

#t-1:checked ~ * .panes .pane--1,
#t-2:checked ~ * .panes .pane--2,
#t-3:checked ~ * .panes .pane--3,
#t-4:checked ~ * .panes .pane--4,
#t-5:checked ~ * .panes .pane--5,
#t-7:checked ~ * .panes .pane--7,
#t-8:checked ~ * .panes .pane--8,
#t-9:checked ~ * .panes .pane--9,
#t-6:checked ~ * .panes .pane--6 {
  display: block;
}
#t-1:checked ~ .panes .pane--1,
#t-1:checked ~ .pane--1,
#t-2:checked ~ .panes .pane--2,
#t-2:checked ~ .pane--2,
#t-3:checked ~ .panes .pane--3,
#t-3:checked ~ .pane--3,
#t-4:checked ~ .panes .pane--4,
#t-4:checked ~ .pane--4,
#t-5:checked ~ .panes .pane--5,
#t-5:checked ~ .pane--5,
#t-6:checked ~ .panes .pane--6,
#t-7:checked ~ .panes .pane--7,
#t-7:checked ~ .pane--7,
#t-8:checked ~ .panes .pane--8,
#t-8:checked ~ .pane--8,
#t-9:checked ~ .panes .pane--9,
#t-9:checked ~ .pane--9,
#t-6:checked ~ .pane--6 {
  display: block;
}

/* Focus-visible audit (Part B): a keyboard user tabbing to any of these `sr-only` radios lands
   the browser's own focus ring on a 1px, visually-clipped box -- invisible, not merely subtle.
   `.menu__toggle` (base.css :2698, :5869) already forwards its own ring to a visible sibling; the
   shared `t-*` tab-radio family did not. Same `~ *` uncle-problem form the `:checked` rules above
   already use, since the label is a level down inside `.tabs`/`.tabs a`, never a direct sibling. */
#t-1:focus-visible ~ * label[for='t-1'],
#t-2:focus-visible ~ * label[for='t-2'],
#t-3:focus-visible ~ * label[for='t-3'],
#t-4:focus-visible ~ * label[for='t-4'],
#t-5:focus-visible ~ * label[for='t-5'],
#t-6:focus-visible ~ * label[for='t-6'],
#t-7:focus-visible ~ * label[for='t-7'],
#t-8:focus-visible ~ * label[for='t-8'],
#t-9:focus-visible ~ * label[for='t-9'] {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}

/* t-9 added 2026-09-13 for `27-service-detail`'s `Datastores` tab. All eight slots were already
   taken -- the plain service uses Builds at t-5 and the model service uses Playground there, and
   both rows run to eight. Extended in all SIX families at once, because the two nesting shapes and
   the label/marker/pane split is exactly how nine screens once shipped with no active tab.
   The row costs nothing: measured by appending a ninth label before writing any of this, 1440,
   1024 and 768 stay ONE row at 41px and 390 stays THREE at 128px -- +0px, zero overflow. */

/* The scrolling stream itself -- the box a log lives in on a fill page. `31-logs` had it as
   `.stream` and `22-build` had the same thing as `.logwrap` / `.logbody`, where the body was
   BYTE-IDENTICAL and the wrapper differed by one declaration. Two names, one component, and
   neither duplicate gate could see it: they compare identical RULES, and a component under a
   different name is never identical to itself. Third time in this repo, after the filter box
   under three names and the state mechanism spelled two ways.

   `position: relative` stays for everyone. Only `33` positions a jump-to-now button inside the
   stream today, and a containing block costs nothing to the screen that does not. */
.stream {
  flex: 1;
  min-height: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  margin: 0 var(--s8-space-6) var(--s8-space-6);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
  background: var(--s8-color-surface-raised);
}
.stream__body {
  flex: 1;
  overflow: auto;
  padding: var(--s8-space-2) 0;
}

/* ---- the log row, and the controls around it ------------------------------------------------
   Shared because logs exist at TWO scopes and it is one component: the sidebar's Logs screen is
   environment-wide and interleaves every service, and a service's Logs tab is the same thing with
   the filter locked to one service. The only difference is the filter.

   These lived in `31-logs.html`'s own <style> while their PHONE rules lived down in the tier
   below -- a component split across two files, where the half that made a stacked row readable was
   shared and the half that made it a row at all was not. `27` could not have used it without
   copying it, and a copy is a second component that merely looks alike. */
.sel {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  height: 32px;
  padding: 0 var(--s8-space-3);
  background: var(--s8-color-surface);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text);
  cursor: pointer;
}
.lr {
  display: flex;
  gap: var(--s8-space-3);
  padding: 0 var(--s8-space-4);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  line-height: 1.75;
}
.lr:hover {
  background: var(--s8-color-surface-sunken);
}
.lr__t {
  color: var(--s8-color-text-muted);
  flex: none;
  opacity: 0.65;
}
.lr__s {
  flex: none;
  width: 78px;
  color: var(--s8-color-wayfind);
  opacity: 0.85;
}
.lr__m {
  /* NOT `pre-wrap`, which it was in `31-logs`. Every file here is Prettier-formatted, so a message
     long enough to wrap in the SOURCE gets a newline and twenty-six spaces of indentation -- and
     `pre-wrap` renders them. Two lines on the service Logs tab came out with their continuation
     pushed halfway across the card. Shortening the strings is not a fix: the formatter re-wraps
     them on the next `format:write`. A real log viewer wants `pre-wrap` for stack traces; this set
     draws no payload that carries meaningful leading whitespace, so it buys nothing and costs a
     rendering bug the formatter keeps reintroducing. A gate watches for the general case. */
  white-space: normal;
  word-break: break-word;
}
.lr--err {
  background: color-mix(in oklab, var(--s8-color-danger) 11%, transparent);
}
.lr--err .lr__m {
  color: var(--s8-color-danger);
}
.lr--warn .lr__m {
  color: var(--s8-color-warning);
}
/* A build log has two line kinds an application log does not: a section header, and the line that
   says it worked. They arrived with `22-build`'s private copy of this component and are promoted
   rather than dropped -- they are the reason that screen's log reads as a narrative. A build line
   carries no source column, which is an absence in the markup rather than a modifier. */
.lr--sec .lr__m {
  color: var(--s8-color-wayfind);
}
.lr--ok .lr__m {
  color: var(--s8-color-success);
}
.foot {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-2) var(--s8-space-4);
  border-top: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  flex: none;
}
.rate {
  font-family: var(--s8-font-mono);
}

/* ---- a code sample, with its tab bar --------------------------------------------------------
   `28-live-url` shows how to call a model you host yourself; `75` shows how to call one we host.
   Same component, and it was private to the first until the second existed.

   The body is a `<pre>` on purpose, recorded in `45` when it was a `<div>` of inline spans: the
   newlines have to survive, and `<pre>` is also what stops Prettier reflowing lines the sample
   depends on. It is the one element the source-indentation gate excludes, for that reason. */
.code {
  background: var(--s8-color-surface-sunken);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
}
.code__bar {
  display: flex;
  align-items: center;
  gap: var(--s8-space-1);
  padding: var(--s8-space-2) var(--s8-space-3);
  border-bottom: 1px solid var(--s8-color-border);
}
.code__tab {
  padding: 3px 10px;
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.code__tab[aria-selected='true'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
}
.code__copy {
  margin-left: auto;
}
/* A <pre>, and `white-space: pre-wrap`. It was a <div> of inline spans with newlines in the
   source and no white-space rule, so the browser collapsed all of it: two shell comments and
   a four-line curl command rendered as one run-on paragraph. It had never once looked like
   code. <pre> also stops Prettier reflowing the lines this now depends on. */
.code__body {
  white-space: pre-wrap;
  margin: 0;
  padding: var(--s8-space-4);
  overflow-x: auto;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  line-height: 1.75;
}
.code__body .c {
  color: var(--s8-color-text-muted);
}
.code__body .s {
  color: var(--s8-color-success);
}

/* The second line in a table cell -- what a row means, under what it says. Private to
   `26-services-list` until `84` needed it, and its PHONE rule was already shared: base.css has
   carried `.main .tbl--cards .why` in the tier below for as long as the component existed. */
.why {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: 2px;
}
.why--bad {
  color: var(--s8-color-danger);
}

/* The name cell in a list of things you can open -- services on `26`, data services on `02`.
   Text color rather than the wayfinding one: the column beside it already carries accent, and two
   colored columns in a six-column table reads as noise rather than as navigation. The underline
   on hover is what says it is a link. */
.svc {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
}
.svc__i {
  width: 18px;
  text-align: center;
  color: var(--s8-color-text-muted);
  flex: none;
}
.svc__n {
  font-weight: var(--s8-weight-medium);
  white-space: nowrap;
  color: var(--s8-color-text);
  text-decoration: none;
}
.svc__n:hover {
  text-decoration: underline;
}

/* The connection string: what a managed data service actually gives you. One string several
   services read, so the two actions on it are copy-once and rotate-because-it-leaked.

   `flex-wrap` and `min-width: 0` are load-bearing, not tidiness. At 390 the string sat between a
   fixed-width key and two buttons and was squeezed to NINE pixels -- the narrow-text gate caught
   it, and nothing overflowed, because the box did exactly what a flex item is told to do. */
.conn {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface-sunken);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  overflow-wrap: anywhere;
  margin-bottom: var(--s8-space-4);
}
.conn__k {
  color: var(--s8-color-wayfind);
  flex: none;
}
.conn > .muted {
  flex: 1 1 260px;
  min-width: 0;
}
.conn__a {
  margin-left: auto;
  flex: none;
  display: flex;
  gap: var(--s8-space-2);
}

/* ---- a variable row ---------------------------------------------------------------------------
   The service's own variables (`27`), the environment's shared ones (`05`), and the inline add
   during a first deploy (`21`). It was private to `27` -- correctly, at the time: exactly one
   screen had variables. Three do now, and the inline flow on `21` is the reason decision 12 exists,
   so a first deploy can set a variable without being sent somewhere else to do it. */
/* Arrived with `17-variables`, which this screen absorbs. Local because after the
   absorption exactly one screen has variables -- if the model detail or a data service
   grows them, this moves to base.css rather than being copied. */
.vars {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
  margin-bottom: var(--s8-space-5);
}
.var {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: 1px solid var(--s8-color-border);
}
.var:last-child {
  border-bottom: 0;
}
.var--edited {
  background: var(--s8-color-surface-raised);
  box-shadow: inset 3px 0 0 var(--s8-color-accent);
}
.var__k {
  width: 220px;
  flex: none;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  overflow-wrap: anywhere;
}
.var__v {
  flex: 1;
  min-width: 0;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  overflow-wrap: anywhere;
}
.var__a {
  flex: none;
  display: flex;
  gap: var(--s8-space-2);
}
.var__note {
  font-family: var(--s8-font-body);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-wayfind);
}

/* ---- the playground: a request and its answer ------------------------------------------------
   `75` calls a model we run; `47` calls one running on the tenant's own card. Same component, and
   the difference between them is one line of copy about what the call costs -- which is the whole
   distinction between the two arrangements, and worth showing in identical furniture so a reader
   compares the sentence rather than the layout.

   Its PHONE rule was already here (`.main .pg` collapses the pair at 390) while these sat in a
   screen -- half-shared, the same split the log row and the paste box had. */
/* A request and its answer, side by side at width and stacked below it. The point of the
   screen is that you can see the call before you write any code, so the code sample is not
   an afterthought panel -- it is half the screen. */
.pg {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s8-space-4);
  margin-bottom: var(--s8-space-5);
}
.pg__h {
  font-size: var(--s8-text-xs);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-2);
}
.out {
  min-height: 180px;
  padding: var(--s8-space-3);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-raised);
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-relaxed);
  color: var(--s8-color-text);
}
.meter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-4);
  padding: var(--s8-space-3) var(--s8-space-4);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.meter b {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}

/* ---- the catalog card ---------------------------------------------------------------------
   One card, three catalogs: the models you run on your own hardware (`41`), managed inference
   (`74`) and managed data (`02`). It was private to `41` until the second one needed it, which is
   the moment a component has to move -- a copy is a second component that merely looks alike, and
   a fix to one reaches neither of the others.

   `.grid` and `.t` were fine as private names in one screen. In a shared sheet beside `.grid2`,
   `.chip` and `.badge` they are collisions waiting to happen, so they arrive as `.cards` and
   `.tag`. */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  gap: var(--s8-space-3);
}
.m {
  display: flex;
  flex-direction: column;
  text-align: left;
  cursor: pointer;
  padding: var(--s8-space-4);
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  transition: border-color var(--s8-duration-fast) var(--s8-ease);
}
.m:hover {
  border-color: var(--s8-color-border-strong);
}
.m__top {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-2);
  margin-bottom: var(--s8-space-1);
}
.m__name {
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-semibold);
}
.m__by {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.m__d {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-normal);
  margin-bottom: var(--s8-space-4);
}
.m__foot {
  margin-top: auto;
  display: flex;
  align-items: flex-end;
  gap: var(--s8-space-3);
}
/* Hardware and price shown on the card, not held back until a detail page. The whole product
     claim is that we choose the hardware and say so - saying so late is a different promise. */
.m__hw {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text);
  white-space: nowrap;
}
.m__hwl {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  display: block;
  margin-bottom: 2px;
}
.m__price {
  margin-left: auto;
  text-align: right;
}
.m__rate {
  display: block;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  white-space: nowrap;
}
.m__run {
  display: block;
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  margin-top: 2px;
}
.m__tags {
  display: flex;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--s8-space-3);
}
.tag {
  font-size: var(--s8-text-xs);
  padding: 2px 7px;
  border-radius: var(--dir-badge-radius, 999px);
  border: 1px solid var(--s8-color-border);
  color: var(--s8-color-text-muted);
  text-transform: var(--dir-badge-case, none);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above. */
  letter-spacing: 0.02em;
}
.tag--fit {
  border-color: var(--s8-color-success);
  color: var(--s8-color-success);
}
/* A license is a gate, not decoration: some of these models are not redistributable, and
   the tenant has to see which before the click rather than after. Muted because it is a
   fact to read, not a state to act on. */
.tag--lic {
  color: var(--s8-color-text-muted);
  font-family: var(--s8-font-mono);
}
.m__free {
  color: var(--s8-color-success);
}

/* Wide tables scroll inside their own container so the page never does. */
.tblwrap {
  overflow-x: auto;
}
/* `.tblwrap` alone has no bounded height, so it is never the scroll parent `position: sticky`
   needs on `thead th` -- a header only sticks against a container that itself scrolls. This
   modifier bounds the height and takes over the vertical scroll so the header can pin to it,
   for a list long enough that a reader loses the column names before reaching the bottom. */
.tblwrap--scroll {
  max-height: 240px;
  overflow-y: auto;
}
.tblwrap--scroll thead th {
  position: sticky;
  top: 0;
  background: var(--s8-color-surface-raised);
  z-index: 1;
}
table.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--s8-text-base);
}
.tbl .nowrap,
.tbl .mono {
  white-space: nowrap;
}
.tbl th {
  text-align: left;
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  padding: var(--s8-space-2) var(--s8-space-4);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
  /* A column label is a short word, not prose -- between the phone-card tier (720px) and a wide
     grid it was wrapping into two lines, which also narrowed the column above the text that
     still had to fit in it. */
  white-space: nowrap;
}
.tbl td {
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.tbl tr:last-child td {
  border-bottom: 0;
}
.tbl tbody tr:hover {
  background: var(--s8-color-surface-sunken);
}
/* A money or count column, right-aligned so digits line up -- no v1 table has shown one until the
   billing accordion's history section. Named rather than reached for `.mono` alone, which is used
   for plenty of non-tabular, non-numeric monospaced text too. */
.tbl th.num,
.tbl td.num {
  text-align: right;
}
/* A row that opens a panel names itself with a label, so it takes the pointer like a link. */
.tbl label[for] {
  cursor: pointer;
}

/* A ruled table: one hairline between cells, and digits that line up column to column. The
   vertical rule is scoped ABOVE the phone card tier on purpose -- `.tbl--cards` stacks every
   cell, and a vertical rule inside a stacked card is a line down the middle of nothing.
   `tabular-nums` is safe at every width and stays outside the query. */
.tbl--rule {
  font-variant-numeric: tabular-nums;
}
@media (min-width: 721px) {
  .tbl--rule th + th,
  .tbl--rule td + td {
    border-left: var(--dir-border-w, 1px) solid var(--s8-color-border);
  }
}

/* Every track below is `minmax(0, …)`, never a bare `1fr`. `.app` two hundred lines up already
   carries the reason -- `1fr` is `minmax(auto, 1fr)` and an `auto` minimum floors at MIN-CONTENT --
   and this is the same bug one level down, found on 14: `.grid2`'s single phone-tier column
   resolved to 330px inside a 286px container, because its min-content was an unbreakable 35-character
   host name. The symptom is a screen that pans sideways; the cause is one word in a track list. */
.kv {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  gap: var(--s8-space-2) var(--s8-space-4);
  font-size: var(--s8-text-base);
}
.kv dt {
  color: var(--s8-color-text-muted);
}
.kv dd {
  color: var(--s8-color-text);
}

.metric {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-3);
}
.metric__label {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  width: 74px;
  flex: none;
}
.metric__value {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
}
.metric__spark {
  margin-left: auto;
}
.spark {
  display: block;
  height: 22px;
  width: 108px;
  overflow: visible;
}
.spark path {
  fill: none;
  stroke: var(--s8-color-accent);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}
.spark .spark__area {
  fill: var(--s8-color-accent);
  opacity: var(--dir-spark-fill, 0.1);
  stroke: none;
}
/* A chart rather than a sparkline. `.spark` is 108x22 because it sits inline beside a number;
   the Metrics pane draws the same shape at reading size, and the service pane, the model pane and
   the environment console all want it. A width here would fight the grid, so it fills its card. */
.spark--tall {
  height: 80px;
  width: 100%;
}
/* The second series in a two-line chart -- p50 under p99. Dimmed rather than recolored, because a
   second hue would read as a second meaning. */
.spark .spark--faint {
  opacity: 0.45;
}

/* A block of text pasted or typed rather than a single-line field: a .env file, an IP allowlist.
   Its phone rule was already down in the tier below while this half sat in `17-variables`'s own
   <style> -- the same split the log row had. */
.paste {
  width: 100%;
  min-height: 120px;
  padding: var(--s8-space-3);
  background: var(--s8-color-surface);
  color: var(--s8-color-text);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  line-height: var(--s8-leading-relaxed);
  resize: vertical;
}

.url {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: var(--s8-space-2) var(--s8-space-3);
  background: var(--s8-color-surface-sunken);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-base);
}
.url a {
  color: var(--s8-color-wayfind);
}

.log {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  line-height: 1.65;
}
.log__row {
  display: flex;
  gap: var(--s8-space-3);
  padding: 1px var(--s8-space-4);
}
.log__row:hover {
  background: var(--s8-color-surface-sunken);
}
.log__ts {
  color: var(--s8-color-text-muted);
  flex: none;
}
.log__msg {
  white-space: pre-wrap;
  /* `word-break: break-word` is deprecated. This pair is its replacement, and it is the same
     property that let a 42-character host wrap on 14. CodeRabbit, PR #50. */
  overflow-wrap: anywhere;
  word-break: normal;
}
.log__msg--err {
  color: var(--s8-color-danger);
}
.log__msg--ok {
  color: var(--s8-color-success);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
}
.row {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
}
.grid2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s8-space-4);
}
.grid3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s8-space-4);
}

/* Below 1024 the sidebar becomes an icon rail. Not a hamburger: the operator console already
   made that call, and a rail keeps the destinations visible instead of hiding them behind a
   gesture. The wordmark and the search affordance go with it, or they clip. */
/* ---- the preview-state switcher ------------------------------------------------------------
   Six screens each declared an identical `.statebar` in their own <style>, differing only in
   margin. It belongs here: a screen adding a state variant should not have to re-derive the
   control, and `20` proved the cost of that -- it used the class, had no copy of the rule, and
   the labels rendered as bare centered text.

   Per-screen margin overrides still win on source order, which is correct: the margin is layout
   and the rest is the control. */
/* One row however many states a screen has: labels never break, and a long bar scrolls. */
.statebar {
  display: inline-flex;
  max-width: calc(100vw - var(--s8-space-16) - var(--s8-space-8));
  overflow-x: auto;
  gap: var(--s8-space-1);
  padding: var(--s8-space-1);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
}
/* The component page's demo captions sit in `.page`'s centred, padded column, not `.preview`'s
   edge-to-edge canvas the vw-based max-width above assumes -- it overflowed by exactly the
   sidebar-plus-padding that formula subtracts (58px, measured at 1440). `.page` already caps its
   own width, so the bar only needs to fit what is left of it. */
.page > .statebar {
  max-width: 100%;
}
.statebar label {
  flex: none;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above; fixture state names are
     mixed case ("Degraded", not "DEGRADED"). */
  letter-spacing: 0.02em;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.statebar label:hover {
  color: var(--s8-color-text);
}
/* 87 carries nine states, the most in the set, and at 768 they need 757px inside a 736px space --
   the bar's right edge lands 5px past a viewport nothing can scroll. Wrapping was tried first and
   was worse: it took the bar from 90px to 131px tall, and the screens' clearance is sized for one
   row, so content on 93 ended up UNDERNEATH the bar. Measured both, so this tightens the label
   instead. Nine labels x 8px saved is 72px, which fits every state on one row and leaves the bar's
   height alone.

   Declared AFTER `.statebar label`, not before it: equal specificity means source order decides,
   and the first version of this sat above the base rule and was silently overridden. */
@media (max-width: 820px) {
  .statebar label {
    padding: 0 var(--s8-space-1);
  }
}
/* The shared ids, so a screen adopting the convention gets the active marker for free.
   Two things were wrong with the first version of this, and both were invisible because the
   result of a selector that matches nothing is a screen that looks merely plain.
   1. `~ .statebar` needs the radio to be a SIBLING of the bar. Every screen that adopted the
      convention nests the bar inside `.preview`, so the radio is an uncle and nothing matched --
      nine screens with no active tab, while the seven using private ids worked.
   2. Only `v-ok` and `v-fail` were listed. `v-load` and `v-empty` are equally fixed ids, so two
      of the four tabs could never mark even where the selector did match.
   Both nesting shapes are covered below, wrapper or no wrapper. */
#v-ok:checked ~ .statebar label[for='v-ok'],
#v-ok:checked ~ * .statebar label[for='v-ok'],
#v-fail:checked ~ .statebar label[for='v-fail'],
#v-fail:checked ~ * .statebar label[for='v-fail'],
#v-load:checked ~ .statebar label[for='v-load'],
#v-load:checked ~ * .statebar label[for='v-load'],
#v-empty:checked ~ .statebar label[for='v-empty'],
#v-empty:checked ~ * .statebar label[for='v-empty'],
#v-2:checked ~ .statebar label[for='v-2'],
#v-2:checked ~ * .statebar label[for='v-2'],
#v-3:checked ~ .statebar label[for='v-3'],
#v-3:checked ~ * .statebar label[for='v-3'],
#v-4:checked ~ .statebar label[for='v-4'],
#v-4:checked ~ * .statebar label[for='v-4'],
#v-5:checked ~ .statebar label[for='v-5'],
#v-5:checked ~ * .statebar label[for='v-5'],
#v-6:checked ~ .statebar label[for='v-6'],
#v-6:checked ~ * .statebar label[for='v-6'],
#v-7:checked ~ .statebar label[for='v-7'],
#v-7:checked ~ * .statebar label[for='v-7'],
#v-8:checked ~ .statebar label[for='v-8'],
#v-8:checked ~ * .statebar label[for='v-8'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  box-shadow: inset 0 0 0 1px var(--s8-color-border);
}
.statebar__note {
  margin-left: var(--s8-space-3);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  /* RF-A4's remainder. On 12 of the 14 screens with a switcher the note sits beside the tabs and
     clears the page heading by 9px or more. On 18, 22 and 24 it is long enough to wrap onto its
     own line, and then it touched the <h1>: 0px, 0px and 3px of clearance. This belongs on the
     note rather than on `.preview` because 24 does not use `.preview` at all -- it has private
     variant ids -- so a fix in the wrapper would miss the worst of the three.
     The bottom margin this used to carry is GONE. It existed to keep the note off the page
     heading while the switcher was in the content flow; the switcher is a fixed footer bar now, so
     the note cannot collide with anything -- and inside a flex row that margin only made the bar
     taller on some screens than others. A fix outliving its cause is still a bug. */
  display: inline-block;
}

/* ---- the design-option switcher, `o-*` -----------------------------------------------------
   Rule 10. Three radio families now share a screen and they mean three different things:

     t-*   which VIEW of the object        -- product navigation, `.tabs`
     v-*   what the SYSTEM is doing        -- a product fact, `.statebar`
     o-*   a design option we are proposing -- review chrome, this bar

   The reason this is a third family rather than more `v-*` ids: put an option in `v-*` and a
   reviewer cannot tell **Failed** from **Option B**. That confusion is what the 2026-09-10
   review suffered, so the separation is the point of the component, not a tidiness preference.

   It reads differently from `.statebar` on purpose -- dashed edge, a leading word, no filled
   surface -- because it does NOT ship. A reviewer must never mistake it for a control the
   product has. It sits inside `.preview` beside the state bar, so the radios are uncles rather
   than siblings and both nesting shapes are covered below, exactly as `.statebar` learned to.

   The pane class is `.optview`, not `.opt`: `.opt` / `.opts` is the choice-card component on 20,
   17, 02 and a dozen others, and reusing the name would have hidden every card on those screens
   behind a radio that is not on the page. Ids are FIXED, `o-1` .. `o-4`, one family per screen. */
.optbar {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-1);
  padding: var(--s8-space-1);
  border: 1px dashed var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  background: transparent;
}
.optbar__lead {
  padding: 0 var(--s8-space-2);
  font-size: var(--s8-text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--s8-color-text-muted);
}
.optbar label {
  display: inline-flex;
  align-items: center;
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above. */
  letter-spacing: 0.02em;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.optbar label:hover {
  color: var(--s8-color-text);
}
#o-1:checked ~ .optbar label[for='o-1'],
#o-1:checked ~ * .optbar label[for='o-1'],
#o-2:checked ~ .optbar label[for='o-2'],
#o-2:checked ~ * .optbar label[for='o-2'],
#o-3:checked ~ .optbar label[for='o-3'],
#o-3:checked ~ * .optbar label[for='o-3'],
#o-4:checked ~ .optbar label[for='o-4'],
#o-4:checked ~ * .optbar label[for='o-4'] {
  color: var(--s8-color-text);
  box-shadow: inset 0 0 0 1px var(--s8-color-border-strong);
}
.optbar__note {
  margin-left: var(--s8-space-3);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  display: inline-block;
}
.optview {
  display: none;
}
#o-1:checked ~ .optset .optview--1,
#o-1:checked ~ .optview--1,
#o-2:checked ~ .optset .optview--2,
#o-2:checked ~ .optview--2,
#o-3:checked ~ .optset .optview--3,
#o-3:checked ~ .optview--3,
#o-4:checked ~ .optset .optview--4,
#o-4:checked ~ .optview--4 {
  display: block;
}

/* `o-*` sits OUTSIDE `v-*`, not inside it: pick an option, then walk its states. That puts each
   option's `.states` block one level down from the `v-*` radios, where `~ .states` cannot reach
   it -- the same uncle problem that left nine screens with no active state marker, and the same
   fix. Measured before adding: no screen in the set nests one `.states` inside another, so the
   descendant form can only match more, never something it should not. */
#v-ok:checked ~ * .states .state--ok,
#v-fail:checked ~ * .states .state--fail,
#v-2:checked ~ * .states .state--2,
#v-3:checked ~ * .states .state--3,
#v-4:checked ~ * .states .state--4,
#v-5:checked ~ * .states .state--5,
#v-6:checked ~ * .states .state--6,
#v-7:checked ~ * .states .state--7,
#v-8:checked ~ * .states .state--8,
#v-9:checked ~ * .states .state--9,
#v-10:checked ~ * .states .state--10,
#v-11:checked ~ * .states .state--11,
#v-12:checked ~ * .states .state--12,
#v-13:checked ~ * .states .state--13,
#v-14:checked ~ * .states .state--14,
#v-15:checked ~ * .states .state--15,
#v-16:checked ~ * .states .state--16,
#v-17:checked ~ * .states .state--17,
#v-18:checked ~ * .states .state--18,
#v-19:checked ~ * .states .state--19,
#v-20:checked ~ * .states .state--20,
#v-21:checked ~ * .states .state--21,
#v-22:checked ~ * .states .state--22,
#v-23:checked ~ * .states .state--23,
#v-24:checked ~ * .states .state--24,
#v-25:checked ~ * .states .state--25,
#v-load:checked ~ * .states .state--load,
#v-empty:checked ~ * .states .state--empty {
  display: block;
}

/* Focus-visible audit (Part B), the same shared `v-*` vocabulary this file already reasons about
   above: every one of these ids is a `sr-only` review-toggle radio, and a keyboard user tabbing to
   one lands the native focus ring on a 1px, visually-clipped box. Forwarded to whichever real
   label targets it, `~ *` for the same uncle-problem reason as every `:checked` rule here. */
#v-ok:focus-visible ~ * label[for='v-ok'],
#v-fail:focus-visible ~ * label[for='v-fail'],
#v-2:focus-visible ~ * label[for='v-2'],
#v-3:focus-visible ~ * label[for='v-3'],
#v-4:focus-visible ~ * label[for='v-4'],
#v-5:focus-visible ~ * label[for='v-5'],
#v-6:focus-visible ~ * label[for='v-6'],
#v-7:focus-visible ~ * label[for='v-7'],
#v-8:focus-visible ~ * label[for='v-8'],
#v-9:focus-visible ~ * label[for='v-9'],
#v-10:focus-visible ~ * label[for='v-10'],
#v-11:focus-visible ~ * label[for='v-11'],
#v-12:focus-visible ~ * label[for='v-12'],
#v-13:focus-visible ~ * label[for='v-13'],
#v-14:focus-visible ~ * label[for='v-14'],
#v-15:focus-visible ~ * label[for='v-15'],
#v-16:focus-visible ~ * label[for='v-16'],
#v-17:focus-visible ~ * label[for='v-17'],
#v-18:focus-visible ~ * label[for='v-18'],
#v-19:focus-visible ~ * label[for='v-19'],
#v-20:focus-visible ~ * label[for='v-20'],
#v-21:focus-visible ~ * label[for='v-21'],
#v-22:focus-visible ~ * label[for='v-22'],
#v-23:focus-visible ~ * label[for='v-23'],
#v-24:focus-visible ~ * label[for='v-24'],
#v-25:focus-visible ~ * label[for='v-25'],
#v-load:focus-visible ~ * label[for='v-load'],
#v-empty:focus-visible ~ * label[for='v-empty'] {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}

/* RF-A2. `.btn--danger` was used on 21 and 23 and defined nowhere, so `Remove api` -- the only
   irreversible action in the product, behind a typed confirmation -- rendered as an ordinary
   button. Outlined rather than filled on purpose: a solid red button sitting where the primary
   action usually sits is how people delete things by muscle memory. It fills on hover, once the
   pointer is already on it and the intent is no longer accidental. */
.btn--danger {
  border-color: var(--s8-color-danger);
  color: var(--s8-color-danger);
  background: transparent;
}
.btn--danger:hover {
  background: var(--s8-color-danger);
  border-color: var(--s8-color-danger);
  color: var(--s8-color-surface);
}
/* Rule 04 #14 — a row of buttons with no wrapper fell back to inline whitespace (~4px at body
   size), not a real gap; `.modal__acts`/`.estate__acts` already use `--s8-space-2`/`-3` for
   this, so a plain button row gets the lighter of the two rather than a third number. */
.btnrow {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
}

/* RF-A1, ranked the worst defect in the set. Defined only in 13 and used by four screens, so on
   21, 23 and 27 a segmented control rendered as run-together prose: `OnOff`, and
   `PublicNeeds a credential`. Nothing said it was a control, and nothing said which half was on. */
.seg {
  display: inline-flex;
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  overflow: hidden;
}
.seg__o {
  display: inline-flex;
  align-items: center;
  appearance: none;
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  background: transparent;
  border: 0;
  border-right: var(--dir-border-w, 1px) solid var(--s8-color-border);
  font-family: inherit;
  font-size: var(--s8-text-sm);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above. */
  letter-spacing: 0.02em;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.seg__o:last-child {
  border-right: 0;
}
.seg__o--on,
.seg input:checked + .seg__o {
  background: var(--s8-color-surface);
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
  box-shadow: inset 0 -2px var(--s8-color-wayfind);
}

/* ------------------------------------------------------------- callout --- */
/* The set's only banner, and it was defined on 04 alone. RF-K counted 16 banner-ish elements
   across the screens and ONE shared definition -- this one, unshared. The specs ask for several
   more (a credit warning, a dunning notice, a downgrade block), so it is the component most
   likely to be needed next. `--warn` and `--fail` carry the two semantic borders. */
.callout {
  /* Space on BOTH sides. It had a top margin only, so anything following one sat directly on its
     bottom border -- on seven screens a section label was touching it, which reads as a rendering
     fault rather than as a boundary. A bordered box needs air on the side you leave it by too. */
  margin-top: var(--s8-space-6);
  margin-bottom: var(--s8-space-6);
  padding: var(--s8-space-4);
  border-radius: var(--s8-radius-md);
  border: 1px solid var(--s8-color-border);
  background: var(--s8-color-surface-raised);
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-relaxed);
  color: var(--s8-color-text-muted);
}
.callout b {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-semibold);
}
.callout--warn {
  border-color: var(--s8-color-warning);
}
.callout--fail {
  border-color: var(--s8-color-danger);
}
.callout__actions {
  display: flex;
  gap: var(--s8-space-2);
  margin-top: var(--s8-space-4);
  flex-wrap: wrap;
}

/* Leads the content it introduces rather than following it. */
.callout--lead {
  margin-top: 0;
  margin-bottom: var(--s8-space-6);
}

/* ---- annotation marker -------------------------------------------------------------------
   The destination frame draws things the backend cannot do yet, so a reader has to be able to see
   WHICH things without opening 01-architecture. Dashed rather than solid on purpose: a `.badge` is
   product data, a `.mark` is a note from us about the drawing, and the two must not look alike.
   Two uses today -- `proposed` on an unbacked feature, and `illustrative` on a figure that is not
   a price (`22` RF-B6, re-ruled on #53). */
.mark {
  display: inline-flex;
  align-items: center;
  height: 18px;
  padding: 0 var(--s8-space-2);
  border: 1px dashed var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-medium);
  color: var(--s8-color-text-muted);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  white-space: nowrap;
}
/* A reference chip that opens its own detail (`Compare roles`) is not a status -- it read as a
   DEFERRED/proposed badge because it borrowed the whole status look (dashed, uppercase,
   tracked). Solid border, sentence case, no tracking, body size, so the two vocabularies stay
   apart. */
.mark--ref {
  height: auto;
  padding: var(--s8-space-1) var(--s8-space-2);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-normal);
  color: var(--s8-color-text);
  text-transform: none;
  letter-spacing: normal;
}
/* Rule 04 #9 — the top bar's bell: a 32px square icon button, the row's one control height, with
   its DEFERRED status as a small attached badge rather than inline text carrying the whole
   button ("🔔 DEFERRED" was 18px tall beside 28-32px neighbours and read as a label, not a
   control). The status is real (19 D4), so it stays a `.mark`, corner-mounted. */
.mark--icon {
  position: relative;
  height: var(--s8-control-h);
  width: var(--s8-control-h);
  padding: 0;
  border: var(--dir-border-w, 1px) solid var(--s8-color-control-border);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-md);
  text-transform: none;
  letter-spacing: normal;
  justify-content: center;
}
.mark--icon__badge {
  position: absolute;
  top: -6px;
  right: -6px;
  height: 14px;
  padding: 0 4px;
  border: 1px dashed var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface);
  font-size: 9px;
  font-weight: var(--s8-weight-medium);
  color: var(--s8-color-text-muted);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  white-space: nowrap;
}
/* Its own case and tracking, never the label's. On 14 this sits inside `.sub`, which uppercases,
   so the same sentence read as normal prose on 10 and as SHOUTED CAPS on 14 -- an explanatory note
   inheriting the styling of the thing it is explaining. */
.mark__why {
  margin-left: var(--s8-space-2);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  text-transform: none;
  letter-spacing: normal;
  font-weight: var(--s8-weight-normal);
}
/* NO `::after` separator here, and the attempt is worth recording. Body text follows a
   `.mark__why` on seven marks across six screens and the two run together into one sentence:
   "…a shape rather than a price list You can grow an environment later". A
   `.mark__why:not(:last-child)::after { content: ' —' }` looked like the one-line fix and does
   NOTHING: the text that follows is a TEXT NODE, not an element, so the span still matches
   `:last-child` and the rule never fires. **CSS cannot tell "followed by prose" from "ends the
   block"**, so this is punctuation and it belongs in the copy. The seven were fixed there. */

/* 05 D1, corrected (Phase-1 architect gate) — 05 D1 rejected a `.mark__why` sentence sitting open
   beside its chip; it has to open on press, focus or hover, the same as `.cvpeek` (:6050 in this
   file). An OPT-IN wrapper rather than a redefinition of `.mark`/`.mark__why` themselves — both
   families are shared with screens this run does not own, and this run's brief is Edit-tool
   changes to its OWN families only while another agent edits this file concurrently. `.markchip`
   wraps a `.mark` that is a real `<button>` (focusable and clickable with no script) plus the
   `.mark__why` that used to sit open beside it.
   `display: none`, not `.cvpeek`'s `opacity: 0`. `.cvpeek` gets away with opacity because its
   containing block, `.cvbody`, clips overflow; a `.markchip` can sit anywhere on a plain page with
   `overflow: visible`, and a hidden-but-present 320px box still counts toward the document's
   scrollable area wherever its trigger happens to sit near an edge — reproduced on 63-people at
   every width, 161–165px of sideways scroll from mark__why boxes nobody could see. Losing the fade
   is the right trade against a page that scrolls sideways for a box that is never shown. */
.markchip {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}
.markchip > .mark {
  appearance: none;
  background: transparent;
  font-family: inherit;
  cursor: pointer;
}
.markchip .mark__why {
  display: none;
  position: absolute;
  left: 0;
  top: calc(100% + var(--s8-space-2));
  z-index: 20;
  width: 320px;
  max-width: 60vw;
  margin-left: 0;
  padding: var(--s8-space-3);
  background: var(--s8-color-surface-raised);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
}
.markchip:hover .mark__why,
.markchip:focus-within .mark__why,
.mark__why--on {
  display: block;
}
/* Same reasoning as `.cvpeek--left`: a chip close enough to the right edge (the topbar's bell)
   would open a 320px box straight off the viewport. Anchored from the right instead. */
.markchip--left .mark__why {
  left: auto;
  right: 0;
}

/* A generic tooltip -- distinct from `.markchip` above, whose box is a long explanation anchored
   to a `.mark` badge specifically. This is a short label anchored to any focusable trigger,
   `role="tooltip"` content shown on hover or keyboard focus, hidden the same way `.markchip`
   already learned to: `display: none`, not `opacity: 0`, so a hidden box never counts toward the
   page's scrollable area (`.markchip`'s own comment records the 161-165px sideways-scroll bug that
   shape caused). Replaces a leaking `title` attribute (Part A #6, `V1-01e`) with something a
   keyboard user can actually reach. */
.tooltip {
  position: relative;
  display: inline-flex;
}
.tooltip__t {
  display: none;
  position: absolute;
  left: 50%;
  bottom: calc(100% + var(--s8-space-2));
  transform: translateX(-50%);
  z-index: 20;
  padding: var(--s8-space-1) var(--s8-space-2);
  background: var(--s8-color-surface-raised);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  box-shadow: var(--s8-shadow-overlay);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text);
  white-space: nowrap;
}
.tooltip:hover .tooltip__t,
.tooltip:focus-within .tooltip__t {
  display: block;
}

/* ========================================================= wiring === */
/* 213 buttons and not one of them did anything, because `design/` is deliberately script-free --
   "no build step, no server, every screen opens from file://". Settled 2026-09-02 (`22` §G): the
   controls behave, and no script is added. Both mechanisms below are the one already proven by the
   state variants -- a hidden form control plus a sibling selector.
   The switcher sits at z-index 9999 and STAYS ON TOP on purpose: review chrome should remain
   reachable while a modal is open. Elevation is three signals (12 D4): a raised background, a
   strong border, and --s8-shadow-overlay — a rim on dark, a drop shadow on light, on the four
   temporary surfaces only (.menu__panel, .modal__box, .cvpop, .cvnotebar).
   TWO LIMITATIONS, recorded rather than papered over (`01-architecture` carries them too):
     - `aria-expanded` cannot be toggled without script, so a CSS-only menu is announced less well
       than a real one.
     - A `:target` modal cannot trap focus.
   Both are mockup limitations, not product decisions. */

/* ---- menus: a focusable sr-only checkbox, a label for the visible affordance ---------------
   The checkbox stays in the accessibility tree and keeps its tab stop -- `.sr-only` hides it
   visually without `display: none` -- so Space opens the menu from the keyboard. `display: none`
   here would have made the whole pattern mouse-only, which is the trap the nav-item finding was
   about one element type over: an affordance that looks operable and takes no tab stop. */
.menu {
  position: relative;
  display: inline-flex;
}
/* Absolute, so the panel tracks its trigger exactly. Fixed positioning was tried first, to escape
   the top bar's clip, and it cannot work here: the drift was -83px on the environment menu at 1440
   and +164px on the project menu at 1024, because the sidebar collapses to a 56px rail below 1024
   and the offset is therefore not a constant. Moving the clip was the fix; this is just a panel.
   FOUND BY LOOKING. The computed style said `display: block` with a real bounding box. The
   screenshot said the menu was not there. */
.menu__panel {
  display: none;
  position: absolute;
  top: calc(100% + var(--s8-space-2));
  left: 0;
  z-index: 60;
  min-width: 210px;
  padding: var(--s8-space-1);
  background: var(--s8-color-surface-raised);
  /* 12 D4 — border-strong, like the other two floating surfaces */
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
}
.menu__panel--right {
  left: auto;
  right: 0;
}
/* A full-viewport scrim so a click anywhere closes the menu, which is what people expect and what
   `:target` gets for free but a checkbox does not. Behind the panel, above everything else. */
.menu__scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 50;
  cursor: default;
}
.menu__toggle:checked ~ .menu__panel,
.menu__toggle:checked ~ .menu__scrim {
  display: block;
}
/* The focus ring has to be drawn on the LABEL, because the thing that actually has focus is the
   1px checkbox nobody can see. */
.menu__toggle:focus-visible ~ .menu__btn {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}
/* Normalised across <a>, <button> and <span>, because a menu holds all three: links, an action
   with nowhere to go yet, and the current selection. Making "Sign out" a real <button> immediately
   gave it a different width from the links above it -- a button shrink-wraps where a span filled --
   so the element type has to stop mattering here. */
.menu__item {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: var(--s8-space-3);
  border: 0;
  background: transparent;
  font: inherit;
  text-align: left;
  padding: var(--s8-space-2) var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-base);
  color: var(--s8-color-text);
  cursor: pointer;
}
.menu__item:hover {
  background: var(--s8-color-surface-sunken);
}
.menu__item--on {
  color: var(--s8-color-wayfind);
}
.menu__note {
  display: block;
  padding: var(--s8-space-2) var(--s8-space-3);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.menu__sep {
  display: block;
  height: 1px;
  margin: var(--s8-space-1) 0;
  background: var(--s8-color-border);
}

/* ---- modals: `:target`, so the back button closes them, which is correct ------------------- */
.modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 70;
  align-items: center;
  justify-content: center;
  padding: var(--s8-space-6);
}
.modal:target {
  display: flex;
}
/* An anchor, not a div: clicking the backdrop must close, and that needs a link to the no-op
   fragment. `#closed` matches no id, so nothing is targeted and the browser does not scroll. */
.modal__scrim {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 62%);
}
/* A modal is a CARD with a scrim, and it has to look like one: same surface, same radius, the same
   direction-aware border width, and the same head / body / actions rhythm. It was a flat box with a
   single 24px pad, so the heading sat at 24px, the input at 24px and the list items at 36px --
   three left edges in one dialog. */
.modal__box {
  position: relative;
  width: 100%;
  max-width: 460px;
  /* UI-PANEL — the add modal's five panes can run taller than the viewport; without a cap the
     box grew past it with nothing to scroll. Head and acts stay their natural size as flex
     siblings; only `.modal__body` (below) takes the overflow. */
  max-height: calc(100vh - var(--s8-space-12));
  display: flex;
  flex-direction: column;
  background: var(--s8-color-surface-raised);
  /* 12 D4 — border-strong, like the other two floating surfaces */
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
}
/* Rule 04 #22 — dark's own `--s8-color-border-strong` (#474342) against `--s8-color-surface-
   raised` (#1d1b1b) and a shadow that is itself a black wash read as no separation from the
   dimmed page behind it. Colour only, not width -- `--s8-color-text-muted` (#938d8c) is the
   next step up the same dark ramp; light already reads. */
:root:not([data-theme='light']) .modal__box {
  border-color: var(--s8-color-text-muted);
}
.modal__head {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
  flex: none;
}
.modal__head > :last-child:not(:first-child) {
  margin-left: auto;
}
.modal__body {
  padding: var(--s8-space-4);
  flex: 1;
  overflow-y: auto;
  min-height: 0;
}
.modal__body > .field:last-child {
  margin-bottom: 0;
}
/* A list body insets by space-1 so its items' own space-3 lands on the same 16px edge as the head.
   Aligning the container instead of the items is what keeps hover full-width. */
.modal__body--list {
  padding: var(--s8-space-1);
  border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.modal__box--wide {
  max-width: 620px;
}
/* Rule 04 #21 — was `--s8-text-sm` (12px), a size BELOW `.modal__b`'s own 13px body text: a
   modal's title read as smaller than its own body copy. Every modal inherits this one rule. */
.modal__h {
  font-size: var(--s8-text-lg);
  font-weight: var(--s8-weight-semibold);
  text-transform: var(--dir-cardtitle-case, none);
  letter-spacing: var(--dir-cardtitle-tracking, normal);
  color: var(--dir-cardtitle-color, var(--s8-color-text));
}
.modal__b {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}
.modal__acts {
  display: flex;
  justify-content: flex-end;
  gap: var(--s8-space-2);
  padding: var(--s8-space-3) var(--s8-space-4);
  border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
  flex-wrap: wrap;
  flex: none;
}
/* `Copy` cannot work without script and is NOT faked. It carries this class so the gate can tell a
   deliberately inert control from one nobody wired. */
.inert {
  cursor: default;
}

/* =============================================== borrowed components === */
/* RF-A3 to RF-A6 and RF-K2: seven components whose only definition sat in one screen's inline
   style while other screens used them. Each is shared at the SMALLEST scope that repairs the
   borrower -- a screen-specific extra (a grid placement, a margin, a border) stays on the screen
   that needs it, so moving a definition here never restyles the screen it came from.
   Deliberately NOT shared: .after, .after__h, .reveal and .steps. Those four names each hold two
   unrelated components (RF-K10) -- .reveal is an inline "Show" link on 10 and a bordered warning
   panel on 22 -- so sharing them would break a screen rather than align it. */

/* 73 D1 -- the "copy this now" secret row inside the just-created key state. Was a per-screen
   <style> on v0's 73 (forbidden in v1); named .secret rather than .reveal on purpose -- RF-K10
   above already spends that name on two unrelated things, and a third would break a screen
   instead of aligning one. Sits inside .callout--warn, which already carries the heading and
   the "you will not see it again" sentence, so this rule only shapes the mono value + Copy row. */
.secret {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  margin-top: var(--s8-space-3);
  background: var(--s8-color-surface-sunken);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  overflow-wrap: anywhere;
}
.secret__v {
  flex: 1;
  min-width: 0;
}

/* The state switcher is REVIEW CHROME and it was living in the page flow -- sat above every
   screen's <h1>, pushed the heading down, landed at a different x on every screen because it
   inherited whichever content column that page used, and looked like product UI because it is
   styled like a tab group. Hasnat's words: "this should be a top bar or a footer bar, not like
   this".
   So it is a footer bar: fixed, full width, identical on all sixteen screens, and out of the
   content flow entirely -- the same status the direction switcher already has. `.main` gains
   bottom clearance so nothing hides under it, and the bar leaves room on the right for the
   switcher, which stays on top at z 9999.
   The radios stay exactly where they are in the DOM, so every `#id:checked ~ ...` selector is
   untouched: only the painting moves. */
.preview {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  flex-wrap: wrap;
  row-gap: var(--s8-space-2);
  min-height: calc(var(--s8-option-h) + var(--s8-space-6));
  padding: var(--s8-space-2) var(--s8-space-16) var(--s8-space-2) var(--s8-space-4);
  background: var(--s8-color-surface-raised);
  border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
}

/* A section heading. Four screens declared this identically apart from margin; three more used it
   with no declaration at all and got plain small text. The visual identity is shared, the spacing
   is not -- 12 and 17 keep their own margins, and 17 keeps its rule. */
.sec {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  margin: 0 0 var(--s8-space-3);
  padding-bottom: var(--s8-space-2);
  border-bottom: 1px solid var(--s8-color-border);
}

/* Prose in a panel. `--dim` is the secondary line under it. */
.plain {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
  margin-bottom: var(--s8-space-3);
  max-width: 62ch;
}
.plain--dim {
  font-size: var(--s8-text-sm);
  /* Zero is right for the LAST thing in a block, which is what this modifier was written for --
     a closing note under a card. It is wrong the moment anything follows, and something follows
     36 of the 51 visible instances: measured 2026-09-14, **31 of them were touching the next
     block at 0px**, across 26 screens. A `proposed` or `illustrative` mark sat welded to the
     heading it qualifies on `08`, `12`, `17`, `33`, `36`, `41`, `54`, `55`, `67`, `78` and more.
     Nothing could see it: no overflow, no clipping, every class resolved, and the bordered-box
     gate only watches boxes with four borders. `.plain--dim` has none. */
  margin-bottom: 0;
}
.plain--dim:not(:last-child) {
  margin-bottom: var(--s8-space-3);
}

/* A labeled row: a fixed mono term on the left, a definition on the right. RF-A3 -- on 19 these
   rendered as one run-together paragraph with no separation between items. */
.st {
  display: flex;
  gap: var(--s8-space-4);
  padding: var(--s8-space-4);
  border-bottom: 1px solid var(--s8-color-border);
  align-items: flex-start;
}
.st__w {
  width: 120px;
  flex: none;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
/* The body takes the slack so the row's action lands on a common right edge. Without it the
   middle column sized to its own text: on 19 the button after a two-line description sat 264px
   right of the button after a one-line one, in the same list, and the eye reads that as two
   different lists. */
.st__b {
  flex: 1;
  min-width: 0;
}
.st .btn {
  margin-left: auto;
  flex: none;
}
/* Where a step sits on a ladder. Nine `.st__w` spans across 53, 54 and 55 carried
   `style="color: var(--s8-color-danger)"` or `--text-muted` inline, which is a component's own
   state spelled as a one-off nine times -- and there was no way to restyle "the step you are on"
   without editing three screens. `--now` and `--todo` say it once. `.st__w` is already muted, so
   `--todo` exists to be explicit at the call site rather than to change anything: a ladder where
   two of three steps say nothing and the third says `--now` reads as an omission. */
.st--now > .st__w {
  color: var(--s8-color-danger);
}
.st--todo > .st__w {
  color: var(--s8-color-text-muted);
}
/* `display: block` on both, because 19 spells these as <div> and 15 as <span>. The rules read
   identically -- `.st__d` even carries a `margin-top` that only makes sense stacked -- but on 15
   the two ran together on one line as a single run-on sentence, and on 19 they stacked. Same
   component, same CSS, two renderings, decided by an element name in the markup. */
.st__t {
  display: block;
  font-size: var(--s8-text-base);
}
.st__d {
  display: block;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: 3px;
  line-height: var(--s8-leading-relaxed);
}

/* The row of controls under a table. */
.pager {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-4) 0 0;
}
/* Promoted 2026-09-12: `26`'s record and `25`'s live list both draw it, and the move happens
   when the second consumer appears. Next / Previous only -- the contract paginates by opaque
   cursor and returns no total, so a page count cannot be rendered because the number does not
   exist; nor is there an `orderBy`, which is why no column header in the set is clickable. */
.pager__note {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-left: auto;
}

/* The accent glyph beside an option in a chooser. */
.opt__icon {
  font-size: 17px;
  color: var(--s8-color-wayfind);
  flex: none;
  line-height: 1.3;
  width: 20px;
  text-align: center;
}

/* An explanatory paragraph inside a framed row. */
.fr__note {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
  max-width: 56ch;
  margin-bottom: var(--s8-space-3);
}

/* ====================================================== form controls === */
/* RF-K2. These five were defined three times over -- once each in 03, 06 and 13 -- and used by
   six screens. The three that had no copy (21, 23, 27) rendered ten inputs as native browser
   controls: grey, bevelled, square, 2px padding, in a dark product. The class was ALSO named in
   base.css already, inside `@media (forced-colors: active)`, which is why every scan for
   "classes with no rule" reported those screens clean.
   The three copies agreed on `.input` exactly and disagreed twice: `.field` margin (space-6 on
   two screens, space-5 on one) and `.field__hint` line-height (set on two, omitted on one). The
   majority and the fuller declaration win, which is why this is a move rather than a decision. */
/* A settings list: a bordered stack of rows, each a fixed label column and a control beside it.
   21, 23 and 27 each carried a BYTE-IDENTICAL private copy of these eight rules -- 96 lines of
   the same component in three inline styles, which is the arrangement where a fix to one of them
   silently does not reach the other two.
   The `max-width` is the part none of the three had. They did not need it while they sat on an
   860px page, because the PAGE was doing the capping; the moment every page became one width the
   same input went from 560px to 880px. That is the lesson in one component: a page width was
   standing in for a measure, and it only looked like it worked. */
.fields {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
  margin-bottom: var(--s8-space-6);
}
.fr2 {
  display: flex;
  align-items: flex-start;
  gap: var(--s8-space-5);
  padding: var(--s8-space-4) var(--s8-space-5);
  border-bottom: 1px solid var(--s8-color-border);
}
.fr2:last-child {
  border-bottom: 0;
}
.fr2__l {
  width: 190px;
  flex: none;
}
.fr2__n {
  font-size: var(--s8-text-base);
}
.fr2__d {
  margin-top: 2px;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}
.fr2__c {
  flex: 1;
  min-width: 0;
  /* Same ceiling as `.field`. Without it the identical `.input` was 560px on 06 and 608px on 21,
     23 and 27 -- one component, two widths, decided by which container it happened to land in. */
  max-width: var(--s8-measure-field);
}
/* The note under a control in a settings row. It sat flush against the control above it, which
   made a caption look like part of the input. */
.fr2__ro {
  /* BLOCK, for the reason `.field__hint` two hundred lines up now carries: this rule sets a
     `margin-top`, and a vertical margin does nothing on an inline box. Eight of the set's 34
     `.fr2__ro`s were written as a <span> -- `27-service-detail` x4, `33-environment-settings` x2,
     `49-data-detail`, `52-deploy-from-template` -- against 26 written as a <div>.
     Proved by forcing `display: block` on the live spans and watching them MOVE: 5px on `27` and
     `52`, 5 and 11px on `33`. A static read could only say the margin was "probably" lost. */
  display: block;
  margin-top: var(--s8-space-2);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

/* A field is narrow because it is a field. It used to be narrow because it sat on an 860px page,
   which meant the same input was 812px on one screen and 1132px on another -- and a 1132px text
   box for a project name is not a form, it is a horizon. */
.field {
  /* Block, so a wrapping `<label class="field">` keeps its margin and measure. */
  display: block;
  margin-bottom: var(--s8-space-6);
  max-width: var(--s8-measure-field);
}
.field__label {
  display: block;
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-2);
}
.field__hint {
  /* BLOCK, explicitly. It carries a `margin-top`, and a vertical margin does nothing on an
     inline element -- so the three hints written as a <span> rather than a <div> sat flush
     against the input's border with no gap at all. 75 of the set's 78 hints are divs; the
     three that were not are `82-delete-project` and `62-people`, and only `82`'s was ever
     reported, because the gate reads a screen's DEFAULT render and `62`'s two live inside a
     state that is hidden until a tab is clicked. The markup is fixed too; this line is what
     stops the next <span> losing its margin silently. */
  display: block;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-2);
  line-height: var(--s8-leading-normal);
}
/* Danger as border/text colour only, never a fill (neo.css) -- a filled input would read as a
   state the field itself is in, not an error about what's typed into it. */
.field--err .input {
  border-color: var(--s8-color-danger);
}
/* Same box model as `.field__hint` (block, so its own margin-top isn't dropped on an inline
   element -- that exact bug, above); it replaces the hint rather than sitting beside it. */
.field__err {
  display: block;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-danger);
  margin-top: var(--s8-space-2);
  line-height: var(--s8-leading-normal);
}
.input,
.selectbox {
  width: 100%;
  height: 38px;
  padding: 0 var(--s8-space-3);
  background: var(--s8-color-surface);
  color: var(--s8-color-text);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-md);
  font-family: inherit;
}
.selectbox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
}
/* The second of the two sizes RF-K3 settles on: 38px for a form field a user fills in, 32px for a
   filter or a search box beside a list. Six sizes existed; two is the decision. */
.input--sm {
  width: 150px;
  height: 32px;
}
.input::placeholder {
  color: var(--s8-color-text-muted);
}

/* =================================================== loading and empty === */
/* F6's other two states. The audit found ZERO loading states across seventeen screens and one
   empty state, and the error component in the next block was the third of three -- so this
   completes the set F6 asks for as "shared components, built once and used everywhere".

   A skeleton must match the SHAPE of what it replaces, which is why there is a row form and a
   card form rather than one spinner. `visual-design.md`: "Loading skeletons must match the shape
   of the content they replace -- table rows for tables, card shape for cards. Never a full-page
   spinner." */
.skel {
  background: linear-gradient(
    90deg,
    var(--s8-color-surface-sunken) 0%,
    var(--s8-color-border) 50%,
    var(--s8-color-surface-sunken) 100%
  );
  background-size: 200% 100%;
  border-radius: var(--s8-radius-xs, 3px);
  animation: skel 1.4s ease-in-out infinite;
}
@keyframes skel {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
/* The one thing a shimmer must not do is ignore a reduced-motion preference: it is decoration on
   a wait, and a wait is already the moment a vestibular trigger is least welcome. */
@media (prefers-reduced-motion: reduce) {
  .skel {
    animation: none;
    background: var(--s8-color-surface-sunken);
  }
}
.skel--line {
  height: var(--spacing-skeleton-line, 12px);
}
.skel-row {
  display: flex;
  align-items: center;
  gap: var(--s8-space-4);
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: 1px solid var(--s8-color-border);
}
.skel-row:last-child {
  border-bottom: 0;
}

/* The empty state. Not an error and not a failure -- so it carries the primary action rather than
   a retry, which is the whole difference. */
.empty2 {
  max-width: var(--s8-container-state, 560px);
  margin: var(--s8-space-10) auto;
  padding: var(--s8-space-6);
  border: 1px dashed var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  text-align: center;
}
.empty2__h {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-xl);
  font-weight: var(--s8-weight-semibold);
  margin: 0 0 var(--s8-space-3);
}
.empty2__b {
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
  margin-bottom: var(--s8-space-5);
}

/* ===================================================== state variants === */
/* F6 asks for waiting, empty and error as SHARED components, "built once and used everywhere".
   The audit found one empty state and two failure states across seventeen screens, and both
   failures were on WAIT screens -- so every screen that loads a list or a fetch could not say it
   had failed. This is that component.

   The mechanism is the one 07 already used: a pair of sr-only radios and a sibling selector, so a
   reviewer can switch states with no script. Ids are FIXED (`v-ok` / `v-fail`) because the
   selector below is shared -- a screen inventing its own id gets no toggle and `verify.mjs`'s
   failure-state check is what says so. */
/* ONE family, and every selector for it lives here.

   There used to be two. Nine screens used `.vstates` / `.vstate` with the shared `v-*` ids and
   needed no CSS of their own; six used `.states` / `.state` with private ids (`st-*`, `b-*`, `l-*`,
   `f0`-`f8`, `e-*`, `s-*`) and each carried eight to twenty lines restating the display and marker
   rules. Same mechanism, two names, and the second one made every new multi-state screen cost a
   block of copied CSS -- which is how the marker rule came to be wrong on nine screens at once
   (RF-K1) and right on the seven that had ignored the convention.

   The ids are FIXED because the selectors are shared. Four are named for the canonical states --
   `ok`, `fail`, `load`, `empty` -- and `2` through `8` are neutral slots for any further state,
   because not every extra state is a failure: 18 has three normal ways of watching a log stream,
   and 06 has four outcomes of which two are errors with DIFFERENT REMEDIES. The extras are
   numbered rather than named because the meaning belongs on the tab, where a reader sees it; the
   id is only a handle. */
.state {
  display: none;
}
#v-ok:checked ~ .states .state--ok,
#v-fail:checked ~ .states .state--fail,
#v-2:checked ~ .states .state--2,
#v-3:checked ~ .states .state--3,
#v-4:checked ~ .states .state--4,
#v-5:checked ~ .states .state--5,
#v-6:checked ~ .states .state--6,
#v-7:checked ~ .states .state--7,
#v-8:checked ~ .states .state--8,
#v-9:checked ~ .states .state--9,
#v-10:checked ~ .states .state--10,
#v-11:checked ~ .states .state--11,
#v-12:checked ~ .states .state--12,
#v-13:checked ~ .states .state--13,
#v-14:checked ~ .states .state--14,
#v-15:checked ~ .states .state--15,
#v-16:checked ~ .states .state--16,
#v-17:checked ~ .states .state--17,
#v-18:checked ~ .states .state--18,
#v-19:checked ~ .states .state--19,
#v-20:checked ~ .states .state--20,
#v-21:checked ~ .states .state--21,
#v-22:checked ~ .states .state--22,
#v-23:checked ~ .states .state--23,
#v-24:checked ~ .states .state--24,
#v-25:checked ~ .states .state--25,
#v-load:checked ~ .states .state--load,
#v-empty:checked ~ .states .state--empty {
  display: block;
}

/* On a fill page the visible state IS the column that fills, so it is a flex container rather than
   a block. Its own `.page--fill` rule cannot say so: the `:checked` selector above carries an id
   and beats it 1,2,0 to 0,1,0 -- which is why 10 and 18 each restated every display selector they
   had, purely to change one word. Stated once, at a specificity that actually wins. */
#v-ok:checked ~ .states .state--ok.page--fill,
#v-fail:checked ~ .states .state--fail.page--fill,
#v-load:checked ~ .states .state--load.page--fill,
#v-empty:checked ~ .states .state--empty.page--fill,
#v-2:checked ~ .states .state--2.page--fill,
#v-3:checked ~ .states .state--3.page--fill,
#v-4:checked ~ .states .state--4.page--fill,
#v-5:checked ~ .states .state--5.page--fill,
#v-6:checked ~ .states .state--6.page--fill,
#v-7:checked ~ .states .state--7.page--fill,
#v-8:checked ~ .states .state--8.page--fill,
#v-9:checked ~ .states .state--9.page--fill,
#v-10:checked ~ .states .state--10.page--fill,
#v-11:checked ~ .states .state--11.page--fill,
#v-12:checked ~ .states .state--12.page--fill,
#v-13:checked ~ .states .state--13.page--fill,
#v-14:checked ~ .states .state--14.page--fill,
#v-15:checked ~ .states .state--15.page--fill,
#v-16:checked ~ .states .state--16.page--fill,
#v-17:checked ~ .states .state--17.page--fill,
#v-18:checked ~ .states .state--18.page--fill,
#v-19:checked ~ .states .state--19.page--fill,
#v-20:checked ~ .states .state--20.page--fill,
#v-21:checked ~ .states .state--21.page--fill,
#v-22:checked ~ .states .state--22.page--fill,
#v-23:checked ~ .states .state--23.page--fill,
#v-24:checked ~ .states .state--24.page--fill,
#v-25:checked ~ .states .state--25.page--fill {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* The error state itself, modelled on 04's capacity failure -- the best-executed UX in the
   proposal and the one the audit singled out: it names the fault, says what it did NOT cost, and
   offers a retry AND an alternative. A wall of red with "something went wrong" is none of those. */
/* Capped at 560 and LEFT-ALIGNED, not centered. Centring made a failure state float into the
   middle of the pane while the loaded state's content began at the page's left edge, so switching
   between the two state tabs slid the whole screen sideways -- the same fault as the old page
   tiers, one level down. The cap is a measure; the position is the page's. */
.estate {
  max-width: var(--s8-container-state, 560px);
  margin: var(--s8-space-10) 0;
  border: 1px solid var(--s8-color-border);
  border-left: 3px solid var(--s8-color-warning);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-6);
}
.estate--fail {
  border-left-color: var(--s8-color-danger);
}
/* An empty state is not a warning: nothing is wrong, there is just nothing yet. */
.estate--empty {
  border-left-color: var(--s8-color-border-strong);
}
/* An <h1>, not a styled div: when the error IS the page, its title is the page's heading. The
   page's own <h1> sits inside the loaded variant, so without this a failure state gave a
   heading-navigation user nothing to land on -- the same defect the implementation register
   records against its NotFound screen. */
.estate__h {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-xl);
  font-weight: var(--dir-display-weight, var(--s8-weight-semibold));
  text-transform: var(--dir-display-transform, none);
  letter-spacing: var(--dir-display-tracking, normal);
  margin: 0 0 var(--s8-space-3);
}
/* Kordin Neo uppercases display text (`--dir-display-transform`), and `text-transform` inherits
   -- a typed filter query quoted in a "No X match" heading turned into PAYMENT/STAGE, reading as
   a different word than what was typed. `.mono` around the query, not the whole heading. */
.estate__h .mono {
  text-transform: none;
}
/* When the error IS the page -- 25 is nothing but a not-found -- its title is the page's title
   and has to measure like one. `.estate__h` is 20px because it normally sits inside a page that
   already carries a 26px heading above it; used as a page's only heading it rendered the 404's
   title SMALLER than every other screen's, which is the one screen where a reader most needs to
   know where they are. */
/* The whole page is the error, so it starts where every other page starts. */
.estate--page {
  margin-top: 0;
}
.estate--page .estate__h {
  font-size: var(--s8-text-2xl);
  letter-spacing: -0.02em;
}

.estate__b {
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}
/* The reset zeroes `p`, so two paragraphs in an error body ran together as one block of prose --
   25's 404 explained itself in what looked like a single run-on paragraph. */
.estate__b p + p {
  margin-top: var(--s8-space-3);
}
.estate__acts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-3);
  margin-top: var(--s8-space-5);
}
/* Only when something follows it. Every use before 2026-09-08 was the last thing in its block, so
   the row never needed a bottom margin -- and then a screen put a section label under one and the
   buttons touched it. Scoped, so nothing that was already correct moves. */
.estate__acts:not(:last-child) {
  margin-bottom: var(--s8-space-6);
}
/* The kind and a request id, in mono and muted. `data-patterns.md` gives six kinds and a distinct
   rendering for each; naming the kind on screen is what lets a reader tell "temporarily
   unavailable" from "you may not do this", which are the same red box otherwise. */
.estate__kind {
  margin-top: var(--s8-space-4);
  padding-top: var(--s8-space-3);
  border-top: 1px solid var(--s8-color-border);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}

/* --------------------------------------------------------------------- unit --- */
/* The suffix after a figure: `240` then `/min`, `$0.19` then `/hour`, `44 GB` then `of 80`. It
   existed on five screens inside FOUR different parents -- `.metric__value`, `.size__price`,
   `.rail__big` and `.m__rate` -- spelled every time as
   `class="muted" style="font-size: var(--s8-text-xs)"`, twenty times.

   That is a component with no name, which is the shape this file's `promoted` section further
   down exists for: the duplicate gates cannot see it, because there is only ever one copy of each
   inline attribute. Its own size and colour, so it works wherever the figure it follows does. */
.unit {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-normal);
  color: var(--s8-color-text-muted);
}

/* A shell-less screen's heading has no `.pagehead` above it to provide the gap, so all ten of
   them set the same `margin-bottom` inline -- fifty times, unanimously, which is a component
   default written out by hand rather than a decision. Scoped to `.lone` because on a shell screen
   `.pagehead` already owns that space, and giving the heading its own margin would double it. */
.lone .pagehead__title {
  margin-bottom: var(--s8-space-3);
}

/* ------------------------------------------------------------------- reason --- */
/* The one shared rendering for "why this failed". `forge/contracts/reasons.yaml` freezes 46 codes
   across 17 namespaces, and each one carries an AUTHORED customer message with the next action
   written into it. Before this the set had 40 screens with a failure surface and independent copy
   on every one of them, which 11 §17 forbids twice over: the portal, the CLI and the API must
   render the SAME reason vocabulary, and a client "cannot infer retryability, cancellation or
   responsibility from phase/message text" -- those are FIELDS, so the affordance has to come from
   the field and not from how the sentence reads.

   Four parts, and each one is a fact the screens were missing:

     `__code`  the frozen identifier. Mono and accent, because it is code -- the same reason
               `.u__l` is. It is what a customer quotes and what a search finds.
     `__who`   the `owner` field: who acts next. 11 §1 decision 4 -- "every non-success state says
               who can act next" -- and it is the sentence a failure should end on.
     `__msg`   the authored message. Prose, so body font.
     `__ref`   the support correlation ID of 11 §19, which was absent from all 78 screens. One
               opaque string a customer can paste anywhere, mapping internally to the operation,
               attempt, request, trace, controller, fence, adapter task, provider correlation,
               audit event and release version.

   Sunken rather than bordered-red: the box around it is already carrying the alarm, and a second
   red border inside a `.callout--fail` reads as two problems. Margin on BOTH sides for the reason
   the callout comment gives -- a bordered box that touches the block below it reads as breakage. */
/* THE SAME TREATMENT WHEREVER A CODE APPEARS. Ruled by Hasnat 2026-09-14, looking at the folded
   project page: *"there are still a few places where we are showing the code but in the same way
   as it was before."*

   He is right, and the cause is that pass 4's sweep only reached a `.reason` attached to a banner.
   Measured: of the 53 left, **32 sit inside an `.estate`** (a full-page error, which is a box, so
   this was a box inside a box), **18 sit directly in a state with no banner at all**, and 2 in a
   `.lone__card`. So the set had a code rendered two ways — a hairline footer inside `.fbar`, and a
   bordered, filled box everywhere else.

   Rather than convert 53 blocks and invent a lead sentence for the 18 that have no banner, the
   COMPONENT changes: same hairline, no border, no fill. One rule, and every one of the set's 75
   reason codes now reads the same whether it is inside a banner, inside a full-page error, or
   standing on its own. */
.reason {
  margin-top: var(--s8-space-5);
  margin-bottom: var(--s8-space-5);
  padding-top: var(--s8-space-3);
  border-top: 1px solid var(--s8-color-border);
}
.reason__top {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-3);
  flex-wrap: wrap;
  margin-bottom: var(--s8-space-2);
}
.reason__code {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-wayfind);
}
/* Muted and small: it labels the code rather than competing with it. `owner` is one of four
   closed values, so this span never holds anything a reader has to parse. */
.reason__who {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  text-transform: var(--dir-badge-case, none);
  /* Fixed tight, not `--dir-label-tracking` -- see `.badge`, above. */
  letter-spacing: 0.02em;
}
.reason__msg {
  margin: 0;
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-relaxed);
  color: var(--s8-color-text);
}
/* `overflow-wrap` because a correlation ID has no break opportunity and this box is 860px on a
   `page--column` screen and 286px on a phone. `.secret` on 67 already pays for this lesson. */
.reason__ref {
  margin-top: var(--s8-space-3);
  padding-top: var(--s8-space-3);
  border-top: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  overflow-wrap: anywhere;
}

/* ---- the failure banner, `.fbar` -----------------------------------------------------------
   PICKED BY HASNAT 2026-09-14 from three arrangements drawn on `00-patterns.html`, and it
   REPLACES the pair directly above it: `.callout--fail` for what happened, then a separate
   `.reason` below it for the code, the owner and the support reference.

   The fault it fixes is the split, not the styling. Measured across the set: `callout--fail`
   appears 22 times on 17 screens and 19 of those -- on 15 screens -- are immediately followed by
   a `.reason`. Two outlined blocks for one event, and the second is the one people stop reading,
   which matters because the code in it is the string a customer is meant to quote at support.

   So: no outline at all. A danger rule down the left carries the severity, the text runs at the
   container's own measure instead of being inset twice, and the code, the owner and the
   reference sit on a hairline footer INSIDE the same block. Lightest of the three, and it stops
   a banner looking like a modal that failed to open.

   `.reason` stays, and is not deprecated: 52 of the set's 72 `.reason` blocks sit inside
   full-page errors and `.estate` blocks rather than behind a banner, where there is no second
   outline to fold away. This replaces the PAIR, not the component. */
.fbar {
  display: flex;
  gap: var(--s8-space-4);
  border-left: 3px solid var(--s8-color-danger);
  padding: var(--s8-space-1) 0 var(--s8-space-1) var(--s8-space-5);
  margin: var(--s8-space-6) 0;
}
.fbar__m {
  flex: none;
  color: var(--s8-color-danger);
  font-size: var(--s8-text-md);
  line-height: 1.4;
}
/* `min-width: 0` because a flex item's default `min-width: auto` refuses to shrink below its
   longest unbreakable run -- and this block routinely holds a frozen reason code and a
   correlation ID, neither of which has a break opportunity. Same lesson `.u__d` and `.fr2__c`
   already carry. */
.fbar__b {
  flex: 1;
  min-width: 0;
  line-height: var(--s8-leading-relaxed);
}
.fbar__t {
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-semibold);
  margin-bottom: var(--s8-space-2);
}
.fbar__a {
  display: flex;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
  margin-top: var(--s8-space-4);
}
/* The hairline that used to be a second border. Everything a reader might quote lives here --
   the code, who acts next, the reference and the frozen message -- so it is one line to find
   rather than a block to skip. `overflow-wrap` for the same reason `.reason__ref` carries it. */
/* The same banner at warning weight. Ruled by Hasnat 2026-09-14, immediately after the failure
   sweep: *"the yellow banner should also be like the other banners, just a yellow line on the
   side."* He is right, and the reason is the one the set already states — two treatments for one
   kind of thing is the fault one level up. A failure and a warning differ in SEVERITY, which is a
   colour, and not in composition.
   47 instances across 29 screens, against 18 failures: the warning is the commoner banner of the
   two, so leaving it boxed would have made the exception look like the rule. */
.fbar--warn {
  border-left-color: var(--s8-color-warning);
}
.fbar--warn .fbar__m {
  color: var(--s8-color-warning);
}
.fbar__f {
  margin-top: var(--s8-space-4);
  padding-top: var(--s8-space-3);
  border-top: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  display: flex;
  gap: var(--s8-space-3);
  flex-wrap: wrap;
  /* Centre, not baseline: the footer carries a Copy button, and baseline hung it below the text. */
  align-items: center;
  overflow-wrap: anywhere;
}

/* New family, named decision (rule 2): decision 05.4 (`docs/design/v1/decisions/05-explanatory-
   prose.md`, "Decision 4 — the three claims") needs one quiet line for whichever of "Nothing
   happened" / "Nothing was charged" / "Nothing was disturbed" is true. No v0 family could do it:
   v0 spread the same three claims across fourteen free wordings inside `plain`, `note` and `muted`
   containers with no shared role -- decision 05 measured "10,743 words ... sit in plain, note,
   muted and callout containers with no role" and named this "a component nobody built" (05:124-129).
   Quiet and secondary to the failure message above it, so no state colour: `--s8-color-text-dim`
   for the mark keeps it below `.reason__who`'s own muted weight, never `--s8-color-success`, which
   would read as a state on a claim that is not one. */
.reassure {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-2);
  margin: var(--s8-space-2) 0 0;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.reassure__m {
  flex: none;
  color: var(--s8-color-text-dim);
}

/* New family, named decision (rule 2): ruling 26.3 (`docs/design/v1/decisions/26-scale.md:64`)
   needs a one-time, dismissible message-plus-action bar ("You can add Kubernetes to this
   environment. [ Set it up ]") in each environment below Scale's first one. Nothing existing
   fits: `.strip` is a KPI stats row (label/value/description columns), not a message; `.fbar`
   only ever carries a state colour (danger or, since 2026-09-14, warning) on its left border, and
   an upsell is neither a failure nor a warning -- giving it one would be exactly the "state colour
   on a non-state thing" fault the checklist already names. `.upsell` borrows `.fbar`'s flat,
   left-accented layout but keeps the accent neutral. */
.upsell {
  display: flex;
  align-items: center;
  gap: var(--s8-space-4);
  border: 1px solid var(--s8-color-border);
  border-left: 3px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  padding: var(--s8-space-4) var(--s8-space-5);
  margin-bottom: var(--s8-space-5);
  background: var(--s8-color-surface);
}
.upsell__b {
  flex: 1;
  min-width: 0;
  line-height: var(--s8-leading-relaxed);
}

/* Scroll-linked wayfinding stepper -- genuinely new-to-v1 (Part 3/4 of the foundation-completion
   plan), not a v0 port: Hasnat's own description is "five sections joined by circles and a
   connecting line... as the pane scrolls, a fill on the line tracks scroll position, and each
   circle glows once the scroll reaches its section." `stepper.js` drives it with
   IntersectionObserver against `.stepper__pane`'s own sections, not `animation-timeline: scroll()`
   (real, but not yet broadly supported, and this needs to work everywhere). `data-reached` is a
   Pulse FILL (brand-legal, DESIGN.md:28) -- `data-active` (the section actually in view right now)
   adds the existing focus-ring token as a box-shadow, borrowed for a second, non-focus purpose
   rather than inventing a new "glow" token. Horizontal is the real, separate need Hasnat named,
   with its exact use case still open (`.omc/plans/anv-30-foundation-completion.md`, "Deferred, not
   blocking") -- built here as the same mechanism, `--h`, not scroll-linked (a wizard's steps
   advance by page, not by scrolling), its own completion tick drawn with a real
   `stroke-dashoffset` self-draw, default `--s8-ease` (the researched elastic-overshoot curve is a
   sign-off item, not built). */
.stepper {
  display: flex;
  gap: var(--s8-space-6);
}
.stepper__rail {
  position: relative;
  flex: none;
  width: 168px;
  list-style: none;
  padding: 0 0 0 var(--s8-space-5);
  margin: 0;
}
.stepper__rail::before {
  content: '';
  position: absolute;
  /* 11px centres the 2px line on the 18px number (which spans 3-21px of the rail). */
  left: 11px;
  top: 9px;
  bottom: 9px;
  width: 2px;
  background: var(--s8-color-border);
}
.stepper__rail::after {
  content: '';
  position: absolute;
  left: 11px;
  top: 9px;
  bottom: 9px;
  width: 2px;
  background: var(--s8-color-accent);
  transform: scaleY(var(--stepper-p, 0));
  transform-origin: top;
  transition: transform var(--s8-duration-normal) var(--s8-ease);
}
.stepper__i + .stepper__i {
  margin-top: var(--s8-space-1);
}
.stepper__i a {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: var(--s8-space-2) 0;
  color: var(--s8-color-text-muted);
  text-decoration: none;
  font-size: var(--s8-text-sm);
}
.stepper__n {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: none;
  margin-left: calc(-1 * var(--s8-space-5) + 3px);
  /* Above the rail's absolutely positioned line, which otherwise drew through every digit. */
  position: relative;
  z-index: 1;
  border-radius: var(--s8-radius-sm);
  border: 1px solid var(--s8-color-border-strong);
  background: var(--s8-color-surface);
  color: var(--s8-color-text-muted);
  font-size: 10px;
}
.stepper__i[data-reached] .stepper__n {
  background: var(--s8-color-accent);
  border-color: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
}
.stepper__i[data-active] a {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
.stepper__i[data-active] .stepper__n {
  /* An offset outline, so the ring reads as a ring and not as a thicker fill. */
  outline: 1px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}
.stepper__pane {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
}
.stepper__sec {
  padding-bottom: var(--s8-space-6);
  color: var(--s8-color-text-muted);
}
.stepper__sec h3 {
  color: var(--s8-color-text);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-2);
}

/* Horizontal -- page-to-page, not scroll-linked. */
.stepper--h {
  display: flex;
  align-items: center;
  gap: 0;
}
.stepper--h .stepper__i {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
}
.stepper--h .stepper__i:not(:last-child)::after {
  content: '';
  width: 32px;
  height: 2px;
  flex: none;
  margin: 0 var(--s8-space-2);
  background: var(--s8-color-border);
}
/* A connector is travelled only once the step it leads to is reached, not when the one it leaves is. */
.stepper--h .stepper__i[data-reached]:has(+ .stepper__i[data-reached])::after {
  background: var(--s8-color-accent);
}
.stepper--h .stepper__n {
  /* The vertical rail's hanging offset; horizontally it pushed the first step off the card. */
  margin-left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: var(--s8-radius-sm);
  border: 1px solid var(--s8-color-border-strong);
  font-size: var(--s8-text-xs);
}
.stepper--h .stepper__i[data-reached] .stepper__n {
  background: var(--s8-color-accent);
  border-color: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
}
.stepper--h .stepper__i[data-active] {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
.stepper--h .stepper__i[data-active] .stepper__n {
  outline: 1px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}
/* A completed step's tick is drawn already, not mid-draw at rest -- every other "done" marker on
   this page (`.stp--done`, `.wzt__s--done`) shows its checkmark complete, never playing an entry
   animation on cold load. `.stepper__tick--draw` is the hook for the one moment that IS live (a
   step actually completing while the person watches); added by script when that happens, not by
   default. */
.stepper__tick {
  stroke-dasharray: 20;
  stroke-dashoffset: 0;
}
.stepper__tick--draw {
  stroke-dashoffset: 20;
  animation: stepper-draw var(--s8-duration-normal) var(--s8-ease) forwards;
}
@keyframes stepper-draw {
  to {
    stroke-dashoffset: 0;
  }
}
/* ================================================================ promoted ===
   Everything below was living in two or more screens' inline <style> blocks. A rule in a screen's
   own <style> BEATS base.css -- later in the cascade, same specificity -- so a component defined
   in five screens is five components that merely look alike, and a fix to base reaches none of
   them. `.fields` was the worked example: byte-identical in 21, 23 and 27, and none of the three
   had the measure it turned out to need.
   30 selectors were duplicated when this was measured. A gate now fails the build if a selector
   is defined identically in two screens, because prose asking people not to do it did not work.
   ------------------------------------------------------------------------------------------- */

/* A page that is a form or a list with a summary panel beside it -- 03, 13, 17. */
.cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--s8-rail-w);
  gap: var(--s8-space-10);
  align-items: start;
}
.rail {
  position: sticky;
  top: var(--s8-space-6);
}
/* A label/value row inside a rail, as a <dl>. */
.rw {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: var(--s8-space-2) 0;
  font-size: var(--s8-text-base);
}
.rw dt {
  color: var(--s8-color-text-muted);
}
.rw dd {
  font-family: var(--s8-font-mono);
  text-align: right;
}
.rule {
  height: 1px;
  background: var(--s8-color-border);
  margin: var(--s8-space-3) 0;
}
.railnote {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-3);
  line-height: var(--s8-leading-normal);
}
/* What the money buys and how long it lasts -- the sentence under a price. */
.runway {
  margin-top: var(--s8-space-4);
  padding: var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  border: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-sm);
  line-height: var(--s8-leading-relaxed);
  color: var(--s8-color-text-muted);
}
.runway b {
  color: var(--s8-color-text);
}
/* `.wrap` REMOVED 2026-09-14. It was "a narrow reading column inside a page -- 04, 06, 16", and
   by the time it was measured none of those three used it: the restructure had moved them to
   `.lone` or to the shared page box. Its only remaining users were `18-provisioning`,
   `20-deploy-launcher` and `21-connect-configure`, where a third width -- 720 against the set's
   860 and 1180 -- made the deploy flow walk 884 → 744 → 744 → 884. All three take `page--column`
   now and the flow is one width.
   Kept as a note rather than deleted silently: a component whose stated consumers have all moved
   on is exactly what the dead-rule gate is for, and this one was invisible to it because that
   gate reads what a SCREEN declares, never what `base.css` does. */

/* The state-variant mechanism. Only the hiding is shared: the `:checked` selectors naming each
   screen's own radio ids stay with the screen, because the ids are the screen's. */
.state {
  display: none;
}
/* Scoped to a fill page. `.states` is on 33 screens, and only 10 and 18 scroll their body inside
   the viewport -- the other 31 are ordinary pages that were being handed a flex column and a
   `flex: 1` they never asked for. A generic name over fill-specific rules is how a container
   starts meaning two things.
   Scoped to `.main--fill`, not `.page--fill`: `.states` is the PARENT of the `.page--fill` state,
   not a descendant of it, so the first spelling of this rule matched NOTHING and both fill screens
   silently lost the middle link of their flex chain -- `main` was 948px tall and the stream inside
   it stopped at 632. Every gate passed: the state rendered a body, the heading was visible, the
   tabs revealed their panes, nothing overflowed. A fill page that does not fill looks like a short
   page, and only measuring `main` against the state finds it. */
.main--fill .states {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* WAYFINDING vs FILL. Four rules read `--s8-color-wayfind` rather than `--s8-color-accent`: the
   selected nav item, the active tab, a link, and the selected menu item. In forge, workshop and
   blueprint the two tokens hold the same value, so this is invisible. `kordin` is why it exists --
   its DESIGN.md says "primary is white, not blue" and reserves electric blue for "eyebrows, focus,
   and hover", so a filled button and a link cannot share one token there.
   Everything else stays on the accent deliberately: a button fill, a brand mark, a status dot, a
   sparkline and a step marker are not navigation, and under a monochrome brand they are meant to go
   monochrome with it. `--s8-color-focus-ring` already existed separately and needs no change. */

/* A row of controls above a list or a stream -- 11, 12. */
.ctl {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--s8-space-4);
}
/* A filter box in a control row. It existed three times under three names -- `.srch` on 09 and 12,
   `.logsearch` on 07, `.ctl__search` on 11 -- rendering at 340px, 280px and 420px, with two
   different backgrounds. Only ONE of those differences meant anything: 11 searches log lines with
   `key=value`, so its text is mono. That is the modifier below; the rest was drift. */
.srch {
  flex: 1;
  min-width: 200px;
  max-width: 340px;
  height: var(--s8-option-h);
  padding: 0 var(--s8-space-3);
  background: var(--s8-color-surface);
  color: var(--s8-color-text);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
}

/* The URL under a service's name in a list. NOT `.addr`, and not `.mono`: both are `nowrap`, and
   a 48-character host in a 38% track measures 336px against 310px of column at 1024 -- which is
   the clipped-out-of-reach gate, at a width no phone tier can rescue. Promoted out of
   `25-overview` when the filtered-services list appeared on two more screens. */
.svcurl {
  font-family: var(--s8-font-mono);
  overflow-wrap: anywhere;
}

/* A query, not a name: monospaced, and given more room because a filter expression is longer than
   a service name. */
.srch--query {
  max-width: 420px;
  min-width: 240px;
  font-family: var(--s8-font-mono);
}

/* The row a filter box sits in. It was `26-services-list`'s private block until a second
   consumer appeared, which is the move this repository already prescribes -- and the second
   consumer is the environment root, which is where that list now lives. `.ctl` above is the
   near neighbour and is deliberately NOT the same thing: `.ctl` is the control row above a
   STREAM and carries its own bottom margin rhythm for a fill page. */
.filters {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  margin-bottom: var(--s8-space-4);
  flex-wrap: wrap;
}
/* What the filter is narrowing, said out loud. A cursor-paged list has no total, so a filter
   here narrows the page you already have and never the query -- and a control that quietly
   implies otherwise is the same class of claim as a count that is really a page size. */
/* A select in a filter row sits at the filter height beside `.srch`, not the 100%-wide form size. */
.filters .selectbox {
  width: auto;
  flex: none;
  gap: var(--s8-space-3);
  height: var(--s8-option-h);
  font-size: var(--s8-text-sm);
}
.filters__note {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-dim);
}

/* A multi-select filter: real checkboxes, because a kind filter is genuinely multi-select and
   a segmented control cannot say "apps AND models". `.chip[aria-pressed]` looks similar and is
   not the same component -- a chip's state lives in an attribute, so nothing a customer clicks
   changes it, and this set has no script. A checkbox toggles by itself.
   `accent-color` is what keeps the box inside the palette: without it the browser paints its
   own blue, which is exactly the "is this colour in the palette?" question the unstyled-control
   gate asks. Promoted with the component rather than left on the one screen that had it. */
.fchk {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-3);
  flex-wrap: wrap;
}
.fchk__o {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text);
  cursor: pointer;
}
.fchk__o input,
.fchk > input {
  accent-color: var(--s8-color-accent);
  margin: 0;
}
.fchk__n {
  color: var(--s8-color-text-dim);
  font-variant-numeric: tabular-nums;
}

/* 16 D1, to-draw #4 — an Access column reads as one value, `Developer · acme-web`, and several
   grants on one row are built both ways. `.grants` is a native `<details>`: the summary IS the
   Access value, so a row with one grant and a row with several read identically until opened,
   and no script is needed for "expand in place". `.grants__sum--modal` is the alternative's
   trigger — same reading, `href="#…"` into an ordinary `.modal` instead. The review toggle
   (`data-rv`) shows exactly one of the two per `tokens/review.css`. */
.grants {
  display: inline-block;
}
.grants__sum {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  cursor: pointer;
  list-style: none;
}
.grants__sum::-webkit-details-marker {
  display: none;
}
.grants__sum--modal {
  color: var(--s8-color-text);
  text-decoration: none;
}
.grants__n {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  padding: 0 var(--s8-space-2);
}
.grants__list {
  margin-top: var(--s8-space-2);
  padding-left: var(--s8-space-4);
  border-left: 1px solid var(--s8-color-border);
}
.grants__row {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  padding: 2px 0;
}

/* A table whose columns say how the list is ordered. The set has no script, so the caret is a
   STATEMENT about the current order rather than a control that reorders -- the same honesty the
   pager keeps when it says "the first eight" instead of drawing a total it was never sent.
   `aria-sort` is the source of truth and the caret is generated from it, so a header cannot
   show one order while announcing another. */
.tbl--sort thead th {
  cursor: pointer;
}
.tbl--sort thead th[aria-sort] {
  color: var(--s8-color-text);
}
/* Drawn, not a glyph: `↓`/`↑` are in neither brand face and fell through to a system font, the
   same gap `.acc__h`'s chevron was already fixed for (DESIGN.md's font table). Same technique --
   a bordered corner, rotated -- read as a direction instead of an open/close state. */
.tbl--sort thead th[aria-sort]::after {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: var(--s8-space-1);
  border-right: 1.5px solid var(--s8-color-wayfind);
  border-bottom: 1.5px solid var(--s8-color-wayfind);
  transform: rotate(45deg) translateY(-3px);
  vertical-align: middle;
}
.tbl--sort thead th[aria-sort='ascending']::after {
  transform: rotate(-135deg) translateY(3px);
}

/* The stages of a piece of work, as chips. 10 uses them live during a build; 12 uses them to show
   which steps an image source SKIPS, which is branch point 4 of the flow map and the whole reason
   the image arm exists. A component used by two screens is a shared component. */
.stprow {
  display: flex;
  gap: var(--s8-space-1);
  margin: var(--s8-space-4) 0;
  flex-wrap: wrap;
}
.stp {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: 4px 10px;
  border-radius: var(--s8-radius-sm);
  border: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.stp--done {
  color: var(--s8-color-text);
}
.stp--done .stp__m {
  color: var(--s8-color-success);
}
.stp--now {
  color: var(--s8-color-text);
  border-color: var(--s8-color-wayfind);
}
.stp--now .stp__m {
  color: var(--s8-color-wayfind);
  animation: breathe 1.8s ease-in-out infinite;
}
.stp--fail {
  color: var(--s8-color-text);
  border-color: var(--s8-color-danger);
}
.stp--fail .stp__m {
  color: var(--s8-color-danger);
}
/* A stage this source does not have. Struck through rather than hidden: the point is that the
   path is SHORTER, and a step you cannot see is not visibly absent. */
.stp--skip {
  opacity: 0.55;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
.stp__m {
  font-size: var(--s8-text-sm);
}

/* Work happening now: a progress line and the steps under it -- 04, 24. */
.prog__meta {
  display: flex;
  justify-content: space-between;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-6);
}
.task {
  display: flex;
  align-items: flex-start;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) 0;
  border-bottom: 1px solid var(--s8-color-border);
}
.task:last-child {
  border-bottom: 0;
}
.task__mark {
  width: 18px;
  flex: none;
  text-align: center;
  font-size: var(--s8-text-sm);
  line-height: 1.5;
  color: var(--s8-color-text-muted);
}
.task__mark--done {
  color: var(--s8-color-success);
}
.task__mark--now {
  color: var(--s8-color-wayfind);
  animation: breathe 1.8s ease-in-out infinite;
}
.task__body {
  flex: 1;
}
.task__name {
  font-size: var(--s8-text-base);
}
.task__t {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  flex: none;
}
.task__d {
  /* A block, because the `margin-top` below only means something on one -- and it is a <span>, so
     without this the description sits on the same line as the name with whatever whitespace
     happened to be in the source as its only separator. That was "Direct access turned onby Sam
     Okonkwo" on one screen and a 4px gap on another, from one rule that read as correct. */
  display: block;
  margin-top: 2px;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

/* A capability row: what it is on the left, what it means on the right -- 17, 26. */
.u {
  display: flex;
  gap: var(--s8-space-4);
  padding: var(--s8-space-4) 0;
  border-bottom: 1px solid var(--s8-color-border);
  align-items: flex-start;
}
/* The label in a `.u` row: a short mono identifier, on the left. It lived in 55's <style> as
   `.u__l` and in 54's as `.u__n`, at 150px/13px and 170px/14px -- one component under two names
   with two arbitrary widths, which is the shape a gate cannot see because there is only ever one
   copy of each. Its PHONE rule was already here while its base rule was not, which is the
   half-shared split this file's own note describes. 170 and the larger size, because a frozen
   identifier like `customer_kubernetes_access` has to fit in it. */
.u__l {
  width: 170px;
  flex: none;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-base);
  color: var(--s8-color-wayfind);
  /* THE COMMENT ABOVE WAS WRONG about 170 being enough: `customer_kubernetes_access` measures
     about 218px at this size and has been running 23px past its own column on 76-direct-access
     since that screen was written, into the 16px gap and then into the description beside it.
     `kubernetes.developer` on 67-roles does the same.

     This is the THIRD fixed label column in this file to meet an identifier longer than itself --
     `.kv` at 132px met `customer_kubernetes_access`, `.st__w` at 120px met
     `GROWTH_RESTRICTED` -- so the answer is not a fourth guess at a number. A mono identifier
     wraps here instead. Mid-token wrapping is not pretty; colliding with the sentence next to it
     is worse, and the column has to stay 170 because two label widths on one page is the fault
     one level up. */
  overflow-wrap: anywhere;
}
.u:last-child {
  border-bottom: 0;
}
/* A row's own action, on a common right edge -- the same rule `.st .btn` carries a hundred lines
   down, for the same reason its comment gives. It has to be a SIBLING of `.u__d` rather than
   inside it: `.u__d` is `flex: 1`, so a button in its text flow lands wherever the sentence ends,
   and two rows with descriptions of different lengths then put their buttons in different places.
   The two controls that were inside `.u__d` were the only two in the set. */
.u > .btn {
  margin-left: auto;
  flex: none;
  align-self: flex-start;
}
/* A `.u` row whose label is a PHRASE rather than an identifier. `.u__l` is mono and accent
   because it names code -- `kubectl`, `kubernetes.owner`, `standard-4-16` -- and setting an
   English sentence in it renders prose as a code token: "What you get" and "Making something
   smaller" arrived looking like variable names on six screens. The row is the same row; the label
   is a different KIND of thing, which is what a modifier is for. Same 170px width on purpose --
   two label widths in one page is the fault one level up. */
.u__l--text {
  font-family: var(--s8-font-body);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  color: var(--s8-color-text);
}
.u__d {
  flex: 1;
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}

/* A change waiting to be applied, above the form that made it -- 21, 23. */
.staged {
  display: flex;
  align-items: center;
  gap: var(--s8-space-4);
  padding: var(--s8-space-4) var(--s8-space-5);
  /* 22 D1 — Pulse belongs on the action fill, not around the whole strip. */
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface-raised);
  margin-bottom: var(--s8-space-6);
}
.staged__t {
  flex: 1;
  font-size: var(--s8-text-sm);
}

/* The irreversible action, fenced off at the bottom of a settings page -- 21, 23. */
.danger {
  border: 1px solid var(--s8-color-danger);
  border-radius: var(--s8-radius-md);
  padding: var(--s8-space-5);
}
.danger__h {
  font-weight: var(--s8-weight-semibold);
  margin-bottom: var(--s8-space-2);
}
.danger__d {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
  margin-bottom: var(--s8-space-4);
}
.danger__f {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  flex-wrap: wrap;
}
.danger__f .input--sm {
  width: 220px;
}

/* A selectable card: an icon, a title and a description, in a grid of them -- 05's deploy
   launcher. 13 carried a second, compact copy of this whole family and USED NONE OF IT: seven
   rules matching nothing in their own screen. It read as a design disagreement about how big an
   option card should be, and there was no disagreement -- just dead CSS that a reader has to
   reconcile before discovering it never rendered. The tag rules went the same way, dead in both
   screens that declared them. */
.opts {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s8-space-3);
}
.opt {
  display: flex;
  gap: var(--s8-space-4);
  text-align: left;
  align-items: flex-start;
  padding: var(--s8-space-4);
  cursor: pointer;
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  transition: border-color var(--s8-duration-fast) var(--s8-ease);
}
.opt:hover {
  border-color: var(--s8-color-border-strong);
}
.opt__t {
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-1);
}
.opt__d {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-normal);
}
.opt--soon {
  opacity: 0.5;
  cursor: not-allowed;
}
.opt--soon .opt__icon {
  color: var(--s8-color-text-muted);
}

/* ============================================== screens with no shell === */
/* Sign-in, sign-up, verification, reset, the invitation, the session screens. They carry no
   sidebar and no top bar, and `66-accept-invitation` argues why on itself: you are being asked to
   join an org you are not in yet, so showing its navigation would be a lie.

   That makes these the ONE place in the set where a genuine layout choice exists -- every other
   screen inherits its layout from the shell -- which is why three treatments are drawn and the
   team picks one. They live here rather than in a screen because ten screens use them, which is
   the second-consumer rule: `.lone` was 47's private block until the sign-in set needed it. */
.lone {
  min-height: 100%;
  background: var(--s8-color-surface-sunken);
  /* The same reserve `.main` carries. 47 had none and passed the review-bar gate only because its
     content is short; a sign-in screen with four providers and a password path is not short. */
  padding: var(--s8-space-10) var(--s8-space-6) var(--s8-preview-reserve);
}

/* One treatment's ground. `pane` rather than `stage` because `stage` is a banned noun, and a class
   name teaches a word whether or not the scanner is exempted from seeing it -- the argument that
   renamed a fixture service from `worker` to `mailer`. */
.lone__pane {
  display: grid;
  place-items: center;
}
.lone__box {
  width: 100%;
  max-width: 520px;
}
.lone__brand {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  margin-bottom: var(--s8-space-8);
}
.lone__mark {
  width: 22px;
  height: 22px;
  border-radius: var(--s8-radius-xs, 3px);
  background: var(--s8-color-accent);
  flex: none;
}
.lone__name {
  font-family: var(--s8-font-display);
  font-weight: var(--s8-weight-semibold);
  font-size: var(--s8-text-md);
}
/* Treatment A. A bordered card on a neutral field -- familiar and safe, and it reads like every
   other developer platform, which is both the strength and the criticism. */
.lone__card {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-8);
}

/* Treatment B. The left half signs you in and the right half is the product. The only one of the
   three that sells while it authenticates -- it costs half the screen, and it needs content that
   stays true, which is why the panel shows the URL moment and a model answering rather than a
   marketing sentence that would go stale without anybody editing it. */
.lone__pane--split {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s8-space-10);
  place-items: center stretch;
  max-width: 1120px;
  margin: 0 auto;
}
.lone__pane--split .lone__box {
  max-width: 460px;
}
.lone__aside {
  border-left: 1px solid var(--s8-color-border);
  padding-left: var(--s8-space-10);
}
.lone__asideh {
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-3);
}
/* 14 D1 — the right half of the split carries the operator's logo and nothing else, replacing the
   removed product panel (the counts, "four minutes", the URL). Centred rather than top-aligned:
   with one thing in the column there is nothing left for it to sit above. The logo itself is
   `.lone__brand` unchanged (mark hidden, wordmark painted by `dir-kordin-neo.css`) — this modifier
   only gives it a room to sit in the middle of. */
.lone__aside--brand {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 320px;
}
.lone__aside--brand .lone__brand {
  margin-bottom: 0;
}

/* Treatment C. No card at all: the wordmark does the work a border does in A, and the page is
   left-aligned rather than centered. The most distinctive and the least conventional, and it lives
   or dies on the typeface -- which is why it compounds with the open kordin / kordin-v2 question
   rather than being decidable on its own. */
.lone__pane--bleed {
  display: block;
  max-width: 760px;
  margin: 0 auto;
}
.lone__pane--bleed .lone__box {
  max-width: none;
}
.lone__pane--bleed .lone__brand {
  gap: var(--s8-space-4);
  margin-bottom: var(--s8-space-10);
}
.lone__pane--bleed .lone__mark {
  width: 34px;
  height: 34px;
  border-radius: var(--s8-radius-sm);
}
.lone__pane--bleed .lone__name {
  font-size: var(--s8-text-doc);
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* And the heading steps up with it. In A the card's border says where the screen starts; here
   nothing does, so the type has to. */
.lone__pane--bleed .pagehead__title {
  font-size: var(--s8-text-xl);
}
.lone__pane--bleed .plain {
  max-width: 62ch;
}

/* A provider chooser is a stack of full-width options, not the two-up grid `.opts` gives a pair of
   alternatives to choose between. The row itself is `.opt` unchanged -- an icon, a title and a
   line saying what it does -- so this is one modifier rather than a second component. */
.opts--stack {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s8-space-2);
}
/* And C sets them across, which is that treatment's argument: a row of doors rather than a list
   of rules. `auto-fit` collapses tracks it does not need to 0px and still reports them, so the
   empty-column gate counts non-zero tracks -- four providers always fill four. */
.opts--row {
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--s8-space-2);
}

/* The line between the two routes. Not `.rule`: that is a hairline inside a rail and carries
   nothing, and this one has to hold a word, which means the word needs a ground to sit on. */
.orsep {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  margin: var(--s8-space-6) 0;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.orsep::before,
.orsep::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--s8-color-border);
}

/* The row under a form: the primary action, and the way out of the screen beside it. */
.loneact {
  display: flex;
  align-items: center;
  gap: var(--s8-space-4);
  flex-wrap: wrap;
  margin-top: var(--s8-space-6);
}
.lonefoot {
  margin-top: var(--s8-space-6);
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
}

/* An ASCII diagram: columns of characters where the alignment carries the meaning. `01` had it
   privately until `78-runtime` drew the accepted runtime lifecycle, which is the same object.
   `<pre>` rather than a styled div, and it is the one element the preserved-whitespace gate
   carves out, because preserving the whitespace is the entire technique. */
pre.art {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  line-height: 1.55;
  color: var(--s8-color-text-muted);
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  padding: var(--s8-space-4) var(--s8-space-5);
  overflow-x: auto;
  margin-bottom: var(--s8-space-5);
}
pre.art b {
  color: var(--s8-color-wayfind);
  font-weight: var(--s8-weight-normal);
}
pre.art i {
  color: var(--s8-color-text);
  font-style: normal;
}

/* The review pages' own heading. 01 said 30px/-0.02em/12px and index said 32px/-0.025em/14px --
   near-identical values that no one chose differently on purpose. Their h2s genuinely DO differ in
   role (01's is a section title, index's is a group label), so those stay local: a shared class
   for two things that are not the same thing is the mistake this sweep is undoing. */
.doc h1 {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-doc);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: -0.02em;
  margin-bottom: var(--s8-space-3);
}

/* Review-page chrome. 01 and index are not product screens, but they duplicated these between
   themselves just as readily. */
.kicker {
  font-size: var(--s8-text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: var(--s8-weight-semibold);
  color: var(--s8-color-text-muted);
}
.docnote {
  border-left: 2px solid var(--s8-color-wayfind);
  padding: 2px 0 2px 16px;
  margin: 20px 0;
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-relaxed);
  color: var(--s8-color-text-muted);
  max-width: 76ch;
}
.docnote strong {
  color: var(--s8-color-text);
}

@media (max-width: 1024px) {
  /* The note wraps to three lines by 768: measured 99px. */
  :root {
    /* 112 -> 128 on 2026-09-12. The environment page's four state tabs and the project page's
       four design options both wrap the bar to three rows at 768, measured at 127px against a
       112px reserve on both. The bar grew, the build failed, the number moved -- which is the
       whole argument for keeping a hand-written constant behind a gate that re-derives it at
       every width, and the second time this file records doing it. */
    --s8-preview-reserve: 128px;
  }

  .app {
    grid-template-columns: 56px minmax(0, 1fr);
  }
  .nav__item span:not(.nav__icon),
  .nav__cta span:not(.nav__icon) {
    display: none;
  }
  .nav__item,
  .nav__cta {
    justify-content: center;
    padding: 0;
  }
  .nav__group {
    display: none;
  }
  .brandbar {
    justify-content: center;
    padding: 0;
  }
  .brandbar__name {
    display: none;
  }
  /* Search kept visible down to 768 (Hasnat, 2026-09-29, amending decision 31: hiding it only at
     1024 and not at the narrower 768 made no layout sense — if it fits at 768 it fits at 1024).
     `.omnibar` no longer hides at this breakpoint; the narrow-width exception this used to need
     in tools/v1_capture.mjs is removed alongside. */
  .topnav {
    padding: 0 var(--s8-space-3);
    gap: var(--s8-space-1);
  }
  .scope__label {
    display: none;
  }

  /* The split collapses at exactly the width where the shell drops its nav labels. Below it the
     two halves are two narrow columns and the half that sells stops selling, which is worse than
     not having it: the treatment's whole argument is that the proof is beside the form. */
  .lone__pane--split {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s8-space-8);
  }
  .lone__pane--split .lone__box {
    max-width: none;
  }
  .lone__aside {
    border-left: 0;
    border-top: 1px solid var(--s8-color-border);
    padding-left: 0;
    padding-top: var(--s8-space-8);
  }

  /* Layout 2's three-in-a-line drops to two here. Three plan cards in a 968px pane are 305px
     each and every feature line wraps to two; two at 468 read normally. Layout 1 is already
     two-up and needs nothing until the phone tier. */
  .main .plans--l2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Narrower still, the credit meter keeps its number and drops its bar before anything
   load-bearing is dropped: the number is the point. */
@media (max-width: 720px) {
  /* THE CANVAS HAS NO PHONE FORM. A 232px card in a 334px pane is a drawing you pan, and the
     narrow gates refuse that deliberately -- the 390px fix that "passed every overflow measure"
     did so by shrinking the payoff to a 73px window. So the drawing is replaced by the list it is
     an alternative to, and the screen says which. */
  .main .cvwrap {
    display: none;
  }
  .main .cvphone {
    display: block;
    padding: var(--s8-space-5) var(--s8-space-4);
  }

  /* At 390 the tabs wrap too, and 24's bar measures 181px. Raised to 200 on 2026-09-08: the
     sign-in family added screens with six and seven state tabs, which wrap to one more row than
     anything in the set had before -- measured 199px on 59, 60 and 62. The alternative was
     cryptic tab labels, and a tab a reviewer cannot read is worse than 8px of padding. This is
     the constant doing the job the gate makes safe: the bar grew, the build failed, the number
     moved. */
  :root {
    /* 200 -> 216 on 2026-09-12, same measurement one tier down: 209px of bar at 390.
       216 -> 248 on 2026-09-13, third time, same reason. `27-service-detail` measured 241px at
       390: it is the most-optioned screen in the set -- seven state tabs wrapping to three rows,
       plus two drawings wrapping to two, plus a note. Everything else is at 213 or below.
       The cheaper fixes were taken FIRST and are why this is only 32px: the option labels lost
       the words the `Drawn as` lead already supplies, two state labels were shortened, and the
       note was cut to a sentence -- together 127 -> 81 at 1440, 145 -> 101 at 1024 and 145 -> 127
       at 768, which cleared three of the four widths without touching any constant.
       Raising the 1440 constant instead would have put 32px of dead padding on all 87 screens to
       serve one screen; here the cost is confined to the tier where the bar actually grew. */
    --s8-preview-reserve: 248px;
  }

  /* A log stream is the one thing that should reach both edges of a phone. Giving 07 and 11 the
     shared page box was right -- it put their headings on the same left edge as every other
     screen -- but the page's 24px gutter each side took the log MESSAGE column down to 105px at
     390. The gutter belongs on the head, which is prose; the body is a stream and wants the pane.
     Caught by the 390px narrow-text gate, which is exactly the fault it was written for: nothing
     overflowed, no text was clipped, the column had simply become unreadable. */
  .page--fill {
    padding-left: 0;
    padding-right: 0;
  }
  .page--fill > .fillhead {
    padding-left: var(--s8-space-6);
    padding-right: var(--s8-space-6);
  }

  .credit__track {
    display: none;
  }
  /* The scope must not SHRINK -- it must scroll with the rest of the bar. Left shrinkable it cut
     `production` to `producti` mid-word with no ellipsis, which is the one outcome worse than
     either alternative: the reader cannot tell a truncated environment name from a real one.
     `flex: none` pushes the overflow out to `.topnav`, whose `overflow-x: auto` is there for it. */
  .scope {
    flex: none;
  }
  .pagehead {
    flex-wrap: wrap;
  }
  .grid2,
  .grid3 {
    grid-template-columns: minmax(0, 1fr);
  }

  /* A card head is a flex row of title, badges, marks and actions. Restoring the metrics card put
     five things in one, and at 334px of pane the actions landed 140px outside the page while the
     annotation was squeezed into a 51px column -- both faults the 390px gates were written for.
     Wrapping is the fix; the annotation takes its own line because a reason nobody can read is
     not a reason. */
  .main .card__head {
    flex-wrap: wrap;
    row-gap: var(--s8-space-2);
  }
  .main .mark__why {
    flex-basis: 100%;
    margin-left: 0;
  }

  /* The top bar's horizontal scroll lives HERE and only here -- 59px of overflow at 390px on 04,
     and 0px at 768 and above. Above phone width it was clipping the scope dropdowns and buying
     nothing. See the note on `.topnav`. */
  .main .topnav,
  .topnav {
    overflow-x: auto;
  }

  /* A scroll container clips, so an absolute panel inside the bar is invisible at this width. Fixed
     escapes the clip, and a full-width sheet under the bar is the better phone pattern anyway --
     a 210px panel anchored to a trigger that may be scrolled out of view is not usable. */
  .main .menu__panel,
  .menu__panel {
    position: fixed;
    top: var(--s8-topnav-h);
    left: var(--s8-space-3);
    right: var(--s8-space-3);
    min-width: 0;
  }

  /* A host name is ONE unbreakable token. `.url` is `inline-flex`, so it is sized by its content
     and cannot wrap -- and once the middle segment became the environment name (settled
     2026-09-03) the model's host reached 35 characters, wider than the 286px content box, so it
     stuck out past the page padding on 14, the screen whose entire point is that address.
     WRAP it, do not scroll it: an earlier fix here shrank the URL to a 73px window on a 361px
     string, and the gate "no scroller shows under 50% of its content" exists because of that.
     Wrapping keeps the whole address readable, which is the thing the screen is for. */
  .main .url {
    display: flex;
    min-width: 0;
  }
  .main .url a {
    overflow-wrap: anywhere;
  }

  /* ---- the phone tier proper ------------------------------------------------------------
     B20 said there was no phone tier, and the four rules above are why it looked like there
     was one. Measured at 390px: 8 of 16 screens kept a DESKTOP layout inside the content pane
     -- `main` holding 357-692px of content in a 334-383px pane -- and let the user pan it
     sideways with nothing on screen saying so.

     Two causes, and neither is per-screen even though the class names are:

     1. A two-column page grid with a FIXED second track: `1fr 300px` on 03, `1fr 320px` on 13
        and 17, `1fr 226px` on 05. The rail simply sat outside the pane.
     2. A centered hero. `place-items: center` sizes the item to its own MAX-CONTENT, so `20`'s
        622px of prose set the track width instead of wrapping.

     The class names below are declared in per-screen <style> blocks and are collected HERE on
     purpose: a phone tier spread across nine files is one nobody can review in a diff. What
     keeps this list complete is not a convention -- `design/verify.mjs`'s pane-panning check
     fails on any screen whose layout is missing from it. */
  /* A mono identifier has no break opportunity, and this set is identifier-heavy: the frozen
     meter names are up to 49 characters (`managed_capacity.accelerator_device.allocated_unit_seconds`
     measured 420px in a 334px pane), and `kubernetes.operator` inside a `.u` row's description
     put 67-roles 17px past the pane on its own.

     Wrapping mid-token is not pretty. Panning sideways with nothing on screen saying so is
     worse, and it is the fault this whole tier was written for. `.u__l` already carries the same
     rule at every width for the same reason -- its own comment records that a fixed label column
     meeting a longer identifier is now the THIRD instance in this file. */
  .mono {
    overflow-wrap: anywhere;
  }
  /* `.tbl .mono` sets `white-space: nowrap`, which is right at a desktop width -- a mono value in
     a table column should not wrap mid-token -- and beats `overflow-wrap` outright. At 390 the
     table has already stacked into cards, so there is no column left to keep intact, and the
     frozen meter names are 49 characters. Named at the same specificity so it lands. */
  .tbl .nowrap,
  .tbl .mono {
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .main .cols,
  .main .opts,
  /* `.lone` too: a screen with no shell has no `.main` for the prefix to bite on, and 61 offers
     two routes in an `.opts` that stayed two-up at 390. The prefix is here to beat a screen's own
     later rule, and a screen that declares none still needs the collapse. */
  .lone .opts,
  .main .sum,
  /* `74`'s two-arrangement contrast and `75`'s request/answer pair. `.arr` is
     `minmax(320px, 1fr)`, which is wider than the 334px pane at 390 and overflowed it by
     10px -- the pane-panning check is what keeps this list complete, and it found both. */
  .main .arr,
  .main .pg {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Higher specificity than the screens' own `.main`, which is later in source order. Pin the
     track to the pane so a `max-width` hero cannot set it, and stretch the item into it.
     `justify-items` only -- NOT `place-items` -- because 07's `.main` is a flex column and
     `align-items: start` there would break the log body's `flex: 1`. */
  .app .main {
    grid-template-columns: minmax(0, 1fr);
    justify-items: stretch;
  }
  /* Flex rows default to nowrap, which is why a 46px button ended up 133px past the pane. */
  /* The preview-state group and its note are siblings on one line, and the group is
     `inline-flex` with no wrap -- so at 390px the note's FIRST WORD stayed on the group's line
     and the rest wrapped below, reading as a truncated sentence above the page title. Review
     chrome, but it renders in the page flow, so it is a layout fault like any other. */
  .statebar__note,
  .optbar__note {
    display: block;
  }
  .opt,
  .fr,
  .keyrow,
  .logbar,
  .tabs,
  .statebar,
  .optbar,
  .cr,
  .empty__actions,
  .paste,
  .repo {
    flex-wrap: wrap;
  }
  /* ---- prose must not be squeezed into a column ----------------------------------------
     Constraining the pane MOVED this fault rather than removing it on two screens: 11's log
     message dropped to 63px and 09's deploy note to 56px, because the row kept its side-by-side
     shape and the prose column absorbed every pixel the pane lost. (They read wider before only
     because the page itself panned.) 17 was already 100px across 12 lines and 15 110px across 6.

     All five are one shape: a flex row whose LAST child is a sentence. At phone width the
     sentence takes its own line. Under ~150px a 40-character string gets fewer than ten
     characters per line, which is why `design/verify.mjs` now fails below that -- the overflow
     checks pass happily while the words become unreadable one column over. */
  .lr,
  .st,
  .u,
  .modes,
  .key,
  .gh,
  .repo2,
  .fr2,
  .danger__f,
  .staged,
  .env2,
  .proj__h,
  .row,
  .secret,
  .kc,
  .tbl--cards td {
    flex-wrap: wrap;
  }
  /* Stacking the log row made every entry two lines with no gap between entries, so a message
     read as ambiguously belonging to the timestamp above OR below it. The indent and the gap
     restore the grouping the columns used to carry -- a stacked row still has to say where one
     record ends. */
  .lr {
    margin-bottom: var(--s8-space-2);
  }
  .lr__m {
    padding-left: var(--s8-space-4);
  }
  /* All prefixed with `.main`. Every one of these is declared in a per-screen <style> that comes
     LATER in source order -- `.u__d { flex: 1 }`, `.fr2__l { width: 190px; flex: none }` -- so an
     unprefixed rule here loses. Specificity, not order, is what beats them, and getting this wrong
     is silent: the row wraps and the prose still ends up in an 86px column. */
  .main .lr__m,
  .main .st__d,
  .main .u__l,
  .main .u__d,
  .main .modes__note,
  .main .gh__b,
  .main .repo2__m,
  .main .tbl--cards .why,
  /* Scoped to .key: `.muted` is a shared color utility used on hundreds of elements, and
     giving it a flex-basis globally would restyle every one of them. */
  .main .key .muted {
    flex: 1 1 100%;
    width: auto;
  }
  /* Prefixed, because the screens declaring these rows also declare a fixed label width and
     `flex: 1` on the control, later in source order. Specificity, not order, is what beats them. */
  .main .env2__n,
  .main .env2__m,
  .main .secret__v,
  .main .kc__t,
  .main .st__b,
  .main .fr2__l,
  .main .fr2__c {
    flex: 1 1 100%;
    width: auto;
    min-width: 0;
  }
  /* Once the row has stacked there is no slack left to push an action across, and `margin-left:
     auto` on a full-width item just indents it. */
  .main .st .btn {
    margin-left: 0;
  }

  /* ---- tables at phone width -----------------------------------------------------------
     The services table is 838px across six columns, so 34% of it fits a 284px pane and the URL
     column -- the product's payoff -- truncated to TWO CHARACTERS. `overflow-x: auto` on the
     wrapper made that reachable and did not make it readable.

     Hiding columns cannot reach it: Service + State + URL alone is 515px. So the row becomes a
     card and each cell announces its own column through `data-label`, which is the reason a card
     list beats dropping columns -- no value loses its header.

     OPT-IN, via `tbl--cards` on the table and `tblwrap--cards` on its wrapper. A three-column
     table is more readable as a grid and should keep one; the other four tables in the set are
     above the readability floor today and are deliberately left alone. `design/verify.mjs`'s
     scroller check is what says when one drops below it. */
  .tblwrap--cards {
    overflow-x: visible;
  }
  .tbl--cards thead {
    display: none;
  }
  .tbl--cards,
  .tbl--cards tbody,
  .tbl--cards tr,
  .tbl--cards td {
    display: block;
    width: auto;
  }
  .tbl--cards tr {
    padding: var(--s8-space-3) var(--s8-space-4);
    border-bottom: 1px solid var(--s8-color-border);
  }
  .tbl--cards tr:last-child {
    border-bottom: 0;
  }
  .tbl--cards td {
    display: flex;
    align-items: baseline;
    gap: var(--s8-space-3);
    padding: var(--s8-space-1) 0;
    border: 0;
    /* A numeric column is right-aligned in a grid because the digits line up with the column
       above. In a card there IS no column above, so the alignment loses its reason and only
       pushes the value away from its label. */
    text-align: left;
  }
  .tbl--cards td::before {
    content: attr(data-label);
    flex: 0 0 34%;
    color: var(--s8-color-text-muted);
    font-size: var(--s8-text-xs);
    font-weight: var(--s8-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  /* The actions cell has no header, so it gets no label and sits flush. */
  .tbl--cards td[data-label='']::before {
    content: none;
  }
  /* A URL cannot stay `nowrap` in a cell that is 66% of 286px -- it overflows by exactly the
     amount the string exceeds the cell, which is how this table still panned by 7px after the
     cards landed. Wrapping mid-URL is ordinary. The alternative is the two-character truncation
     this whole block exists to remove. Specificity, not source order, is what beats the screen's
     own `.addr`. */
  .tbl--cards .addr {
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* The three environment sizes. Three tracks at 96/106/101px plus gaps do not fit 334px, and
     the consequence was the sharpest single item in the render pass: `Large` was off-screen, so
     a form the user MUST complete offered two of its three options. One column, all three
     readable. */
  .main .sizes {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The shell-less screens. The card's 32px inset each side takes a 334px pane down to 270 for a
     provider row whose own label measures more than that, and treatment C's row of four doors is
     four 160px tracks in a 334px pane. Both are the fixed-track fault the panning gate exists
     for: `overflow: visible` means the content leaves its box without `main` scrolling. */
  .lone {
    padding-left: var(--s8-space-4);
    padding-right: var(--s8-space-4);
  }
  .lone__card {
    padding: var(--s8-space-5);
  }
  .opts--row {
    grid-template-columns: minmax(0, 1fr);
  }

  /* `.kv`'s first column is a fixed 132px, which leaves 138 at 390 -- and a frozen identifier is
     not a phrase that wraps. `customer_kubernetes_access` measured 188px in it. Breaking a
     snake_case identifier mid-word is what a reader expects; a fixed track that stops fitting is
     invisible to every overflow measure, because `overflow: visible` lets the content leave the
     box without `main` scrolling. */
  /* `.kv`'s first column is a fixed 132px, which leaves 138 at 390 -- and neither a frozen
     identifier nor a badge is a phrase that wraps: `customer_kubernetes_access` measured 188px in
     it and a "Granted, not enabled" badge 151px. The label column earns its width here instead of
     being given it, which is the same fix three grid rows needed in the last pass. Breaking a
     snake_case identifier mid-word is what a reader expects; a fixed track that stops fitting is
     invisible to every overflow measure, because `overflow: visible` lets content leave its box
     without `main` scrolling. */
  .main .kv,
  .lone .kv {
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  }
  /* Any mono run inside a row that has to share its line. A 40-character commit hash and a
     26-character frozen identifier have no break opportunity at all, so they leave their box
     rather than wrap -- and `overflow: visible` means they do it without `main` scrolling, which
     is invisible to every overflow measure taken at the page level. */
  .main .kv .mono,
  .lone .kv .mono,
  .main .task__d .mono,
  .main .u__d .mono,
  .main .fr2__d .mono {
    overflow-wrap: anywhere;
  }

  /* Both plan layouts become one column. A plan card is a price, a paragraph and four feature
     lines; at 390 two of them are 155px each, which is the fixed-track fault one component over.
     Unscoped, unlike the rules above: the component page shows `.plans--l1` in a bare `.page`,
     with neither `.main` nor `.lone` as an ancestor, and needs the same collapse. */
  .plans--l1,
  .plans--l2 {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Windows High Contrast and Chrome's forced-colors replace author colors wholesale. Without
   these, transparent-filled controls lose their boundary and read as blank white boxes - which
   is exactly how the filter chips were reported. Use the system palette on purpose. */
@media (forced-colors: active) {
  .chip,
  .btn,
  .omnibar,
  .credit,
  .badge,
  .input,
  .srch {
    border-color: ButtonBorder;
    background: ButtonFace;
    color: ButtonText;
    forced-color-adjust: none;
  }
  .chip[aria-pressed='true'],
  .btn--primary,
  .nav__item[aria-current='page'] {
    background: Highlight;
    color: HighlightText;
    border-color: Highlight;
  }
  .dot {
    forced-color-adjust: none;
    background: CanvasText;
  }
  .dot--live,
  .dot--busy,
  .dot--warn,
  .dot--fail {
    background: Highlight;
  }
  :focus-visible {
    outline: 2px solid Highlight;
  }
}

/* ---- the relationship canvas ---------------------------------------------------------------
   Promoted out of `14-project-detail`'s own <style> on 2026-09-11, because a second screen needs
   it: `15-project-network` draws the project's attachments as a canvas under Rule 8, for the same
   reason `14` draws its environments as one -- an attachment is a RELATIONSHIP, and a list throws
   the relationship away.

   Promoted rather than copied. A component under two names in two screens is the fault this file
   already carries three recorded instances of: the filter box under three names, the state
   mechanism spelled two ways, and `.stream` / `.logwrap`. The duplicate gates cannot see any of
   them, because they compare identical rules and a component under a different name is never
   identical to itself. */
.canvas {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface-sunken);
  padding: var(--s8-space-8);
  display: flex;
  align-items: center;
  gap: var(--s8-space-6);
  flex-wrap: wrap;
}
.cnode {
  flex: 1 1 240px;
  min-width: 0;
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-4);
}
.cnode__n {
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-1);
}
.cnode__m {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
a.cnode:hover {
  border-color: var(--s8-color-border-strong);
}
.cflow {
  flex: none;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-lg);
}
.cflow__l {
  display: block;
  font-size: var(--s8-text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* ---- the numbered "what happens next" list ------------------------------------------------
   Promoted out of `16-empty-project`'s own <style> on 2026-09-11. `16` merged into the project
   page's empty state under the 2026-09-10 inventory, and the onboarding steps went with it --
   so the component is now on two screens and the markup on `14` was pointing at a rule that
   lives inside a different file. That is the exact fault this file's first convention names:
   eight screens once shipped partly unstyled with every check passing, because the markup was
   right and nothing painted it.

   The two fixed widths are the component's own measure, not spacing, and are kept as measured
   so the promoted rule renders identically to the one it replaces. */
.steps {
  border-top: 1px solid var(--s8-color-border);
  padding-top: var(--s8-space-5);
}
.steps__h {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-4);
}
.step {
  display: flex;
  gap: var(--s8-space-4);
  padding: var(--s8-space-3) 0;
  align-items: baseline;
}
.step__n {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-wayfind);
  width: 16px;
  flex: none;
}
.step__t {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text);
  width: 190px;
  flex: none;
}
.step__d {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
}
/* The phone tier for this component sits HERE rather than in the collected tier four hundred
   lines above, for the reason `.strip`'s own block records further down: a media query adds no
   specificity, so `.step__t` inside `@media (max-width: 720px)` up there scores 0,1,0 and loses
   to the `.step__t { width: 190px }` six lines up, which is later in source order.

   It was skipped when the component gained its second consumer, and the promotion is exactly
   when it should have been written. Measured at 390 before this existed: the row has 286px, of
   which the number and the title take 190 + 16 + two 16px gaps, so `.step__d` rendered at 48px,
   51px and 57px -- four and five lines of single stacked words -- and the first `.step`
   overflowed its own box by 9px. Same shape as `.u` and `.fr2` (fixed label, prose beside it)
   and the same fix they take at the same width: the prose gets its own line, indented to the
   title's edge so the number still says where a step begins. */
@media (max-width: 720px) {
  .step {
    flex-wrap: wrap;
  }
  .step__t {
    width: auto;
    flex: 1 1 auto;
  }
  .step__d {
    flex: 1 1 100%;
    /* `.step__n`'s 16px plus the row gap, so the description starts under the title. */
    padding-left: calc(16px + var(--s8-space-4));
  }
}

/* ---- the step bar, `.wzt` -------------------------------------------------------------------
   PICKED BY HASNAT 2026-09-14 from three arrangements drawn on `00-patterns.html`. It is the
   first step indicator in this set, and that is a measurement rather than an impression: the
   only two files containing the string `step N of M` are `18-provisioning` and
   `48-data-provisioning`, and both are a RUNNING JOB reporting its own progress, not a person
   being walked through a form. `.stp` above is the build pip row and `.step` below it is the
   what-to-do-next checklist. Neither says "you are on the second of two".

   Why the bar and not the rail that was drawn beside it: the rail costs 212px of width
   permanently, which a form at `--s8-measure-form` can afford and a table-led step page cannot.
   The bar keeps the full width and reads as one page rather than a page inside a frame.

   The move goes in `.wzfoot`, at the bottom, where the eye already is when a form is finished --
   which is the one thing the rail could not do.

   `::before` on the adjacent sibling draws the connector, so the FIRST step has none without a
   modifier and the markup carries no positional class. `.wzt__s--done` / `--now` are the only
   two states a step page has: there is no "later" styling, because an unreached step is simply
   not marked. */
.wzt {
  display: flex;
  gap: var(--s8-space-2);
  align-items: center;
  flex-wrap: wrap;
  padding-bottom: var(--s8-space-4);
  border-bottom: 1px solid var(--s8-color-border);
  margin-bottom: var(--s8-space-6);
}
.wzt__s {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
}
.wzt__s + .wzt__s::before {
  content: '';
  width: 28px;
  height: 1px;
  background: var(--s8-color-border);
  margin-right: var(--s8-space-2);
}
.wzt__n {
  width: 20px;
  height: 20px;
  border-radius: var(--s8-radius-sm);
  border: 1px solid var(--s8-color-border);
  display: grid;
  place-items: center;
  font-size: 10px;
  flex: none;
}
.wzt__s--done .wzt__n {
  border-color: var(--s8-color-success);
  color: var(--s8-color-success);
}
.wzt__s--now {
  color: var(--s8-color-text);
}
.wzt__s--now .wzt__n {
  border-color: var(--s8-color-accent);
  background: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
}
/* The footer that carries the move. `margin-left: auto` on the spacer rather than
   `justify-content: space-between`, because a step page with only a forward action must still
   put it on the right rather than centring it by having nothing to push against. */
.wzfoot {
  display: flex;
  gap: var(--s8-space-2);
  align-items: center;
  flex-wrap: wrap;
  border-top: 1px solid var(--s8-color-border);
  padding-top: var(--s8-space-5);
  margin-top: var(--s8-space-7);
}
.wzfoot__sp {
  margin-left: auto;
}

/* ---- the tier switch, `data-tier` ----------------------------------------------------------
   Rule 6's fourth switch, added 2026-09-11 with the environment batch, because that batch is the
   first to need it: `81-restricted-scale`'s outcome is "a state of 25 with data-tier=base", and
   the three Scale tabs have to be absent below Scale rather than drawn and broken.

   Three tiers, one set of screens: `free`, `base`, `scale`. `scale` is the authored default, so
   a document with NO attribute renders everything -- which is why this could land on twelve
   files without touching the other 75. An absent attribute is inert.

   Two classes, and the difference is the whole of Rule 6:

     .tier-scale  a surface the tier grants. Below Scale it is ABSENT, not broken.
     .tier-base   the below-Scale counterpart -- the short reason, the upgrade path. Absent at
                  Scale.

   What must never appear is a screen explaining the tier system to the user. `.tier-why` is
   five words next to a control, not a paragraph: "Scale only" beside a disabled button, never
   "your plan does not include". The marking of what is and is not in the October cut lives in
   `01-architecture.html`, and Rule 4 keeps it off the product. */
[data-tier='base'] .tier-scale,
[data-tier='free'] .tier-scale {
  display: none;
}
.tier-base {
  display: none;
}
[data-tier='base'] .tier-base,
[data-tier='free'] .tier-base {
  display: block;
}
/* A nav item is a flex row, so the tier switch's generic `display: block` above would strip the
   icon/label/lock alignment off the one nav item that is drawn twice. The `Scale` entry is that
   item: the Decided table requires it PRESENT on every tier and LOCKED below Scale, rather than
   hidden -- `CLAUDE.md` boundary 5, "a portal that only hides a feature is bypassed by anyone
   holding an API key". So the live link carries `.tier-scale` and the locked span carries
   `.tier-base`, and exactly one of them is ever in the tree. The locked one is a `<span>` with
   `aria-disabled`, which `.nav__item[aria-disabled='true']` above already makes un-actuatable --
   the gate that a locked nav item cannot be clicked through asserts the computed property, not
   the absence of an href. */
[data-tier='base'] .nav__item.tier-base,
[data-tier='free'] .nav__item.tier-base {
  display: flex;
}

.tier-why {
  margin-left: var(--s8-space-2);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

/* ---- the option switcher on a fill page -----------------------------------------------------
   `31-logs` and `22-build` scroll their body inside the viewport, and that works by a chain of
   flex children: `.main--fill` -> `.states` -> `.state.page--fill` -> `.fillhead` + the stream.
   Putting an `o-*` option between the main and the states inserts two new links into that chain,
   so both have to carry the flex through or the stream stops scrolling and the page grows
   instead. Three rules, all scoped to `.main--fill`, so no other screen sees them.

   The second rule exists because the generic show rule above sets `display: block`, which is
   right everywhere except here. The third is the descendant form of the `.page--fill` state
   rule, needed for the same reason the `.states` family needed one: with an option in between,
   the radio is an uncle rather than a sibling. */
.main--fill .optset {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.main--fill #o-1:checked ~ .optset .optview--1,
.main--fill #o-2:checked ~ .optset .optview--2,
.main--fill #o-3:checked ~ .optset .optview--3,
.main--fill #o-4:checked ~ .optset .optview--4 {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
#v-ok:checked ~ * .states .state--ok.page--fill,
#v-fail:checked ~ * .states .state--fail.page--fill,
#v-load:checked ~ * .states .state--load.page--fill,
#v-empty:checked ~ * .states .state--empty.page--fill,
#v-2:checked ~ * .states .state--2.page--fill,
#v-3:checked ~ * .states .state--3.page--fill,
#v-4:checked ~ * .states .state--4.page--fill,
#v-5:checked ~ * .states .state--5.page--fill,
#v-6:checked ~ * .states .state--6.page--fill,
#v-7:checked ~ * .states .state--7.page--fill,
#v-8:checked ~ * .states .state--8.page--fill,
#v-9:checked ~ * .states .state--9.page--fill,
#v-10:checked ~ * .states .state--10.page--fill,
#v-11:checked ~ * .states .state--11.page--fill,
#v-12:checked ~ * .states .state--12.page--fill,
#v-13:checked ~ * .states .state--13.page--fill,
#v-14:checked ~ * .states .state--14.page--fill,
#v-15:checked ~ * .states .state--15.page--fill,
#v-16:checked ~ * .states .state--16.page--fill,
#v-17:checked ~ * .states .state--17.page--fill,
#v-18:checked ~ * .states .state--18.page--fill,
#v-19:checked ~ * .states .state--19.page--fill,
#v-20:checked ~ * .states .state--20.page--fill,
#v-21:checked ~ * .states .state--21.page--fill,
#v-22:checked ~ * .states .state--22.page--fill,
#v-23:checked ~ * .states .state--23.page--fill,
#v-24:checked ~ * .states .state--24.page--fill,
#v-25:checked ~ * .states .state--25.page--fill {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* ---- the environment summary strip ---------------------------------------------------------
   Promoted 2026-09-11. It was declared twice -- in `25-overview` and in `00-directions` -- and
   the environment batch took the count to four users, because `75` and `81` render the
   environment page and the strip is part of it. A component under two private copies is the
   duplicate this file already records three instances of; a fourth user made it unarguable. */
/* RE-CUT 2026-09-12, batch 5a. A gapped flex row of four cells read as four cards that had lost
   their borders; the reference this set is benchmarked against draws a KPI row as a 1px hairline
   grid, not as gapped cards. So the gap goes to zero, each cell carries its own padding, and the
   rule between two cells is one hairline. The MARKUP does not change, which is the point: `75`
   and `81` are `.strip` consumers this batch does not own and they inherit the new cut without
   being edited. The radius stays on the box -- under `kordin-neo` that token is 0 anyway. */
.strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 0;
  /* NO top margin, and that is deliberate. A `states -> strip = 0px` reading on
     `75-scale-environment` and `81-restricted-scale` looked like the `.callout` fault one more
     time, and a `margin-top` was added for it on 2026-09-14 and then REVERTED: `.states` on both
     screens is **zero-height** at that point, so the measurement was the distance between an
     invisible box and this one, and the margin collapsed straight through without moving
     anything. A rule that changes nothing anywhere is dead weight.
     The lesson is about the probe, not the CSS: **an adjacency measurement against a zero-height
     element is meaningless**, and a gap sweep has to discard those the way it already discards
     `position: absolute`. */
  margin-bottom: var(--s8-space-5);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
}
.strip__g {
  display: flex;
  flex-direction: column;
  /* was a raw 2px, which is off the 4px grid the brief's checklist requires */
  gap: var(--s8-space-1);
  min-width: 0;
  padding: var(--s8-space-4) var(--s8-space-5);
}
.strip__g + .strip__g {
  border-left: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.strip__l {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
}
.strip__v {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-lg);
  font-weight: var(--s8-weight-medium);
  line-height: 1.2;
}
.strip__u {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  font-weight: var(--s8-weight-normal);
}
.strip__d {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

/* The phone tier for this component sits HERE rather than in the collected tier eight hundred
   lines above, and it has to. A media query adds no specificity, so `.strip` inside
   `@media (max-width: 720px)` at line ~3440 and `.strip` at line ~4050 both score 0,1,0 and the
   LATER one wins at every width -- which is how the two-column override measured as having done
   nothing at all: six cells still at 47px each in a 334px pane, panning by 7px. `.strip` was
   promoted to the end of this file, after the tier, so its responsive half has to follow it.

   Two columns, and the hairlines change axis with them: an odd-numbered cell starts a row and so
   takes no left rule, and everything from the third cell on takes a top one instead. */
@media (max-width: 720px) {
  .strip {
    grid-auto-flow: row;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .strip__g {
    padding: var(--s8-space-3) var(--s8-space-4);
  }
  .strip__g:nth-child(odd) {
    border-left: 0;
  }
  .strip__g:nth-child(n + 3) {
    border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
  }
}

/* And one column below 480. Two columns in a 334px pane leaves 110px of content, which put a
   50-character roll-up ("3 Ready · 1 Degraded · 1 Updating · 1 Provisioning") in a column no
   phone can read -- the narrow-measure gate, not an overflow, which is exactly the fault that
   gate exists for. `+ .strip__g` has to carry BOTH halves: the 720 tier sets its left rule at
   0,2,0 and a bare `.strip__g { border-left: 0 }` at 0,1,0 would lose to it. */
@media (max-width: 480px) {
  .strip {
    grid-template-columns: minmax(0, 1fr);
  }
  .strip__g + .strip__g {
    border-left: 0;
    border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
  }

  /* `.var` had NO phone tier, and `.var__k` is a fixed 220px. In a 334px pane that leaves the
     value column at **0px** -- measured, not estimated: every `.var__v` on `21-connect-configure`
     reported 0 wide at 390, including the one holding "shared by production · already available".
     Nothing saw it. The narrow-measure gate wants 40+ characters under 150px and that string is
     38; the overflow measures pass because a 0px flex item does not overflow anything; and the
     clipped-text gate reads text nodes, while two of the three here are input placeholders.
     Found by looking at the render, which is the fourth time this file records that sentence.
     Stacked rather than narrowed: a key and its value are one thing read top to bottom on a
     phone, and the action follows them. */
  .var {
    flex-wrap: wrap;
    gap: var(--s8-space-2);
  }
  .var__k,
  .var__v {
    width: 100%;
    flex: 1 1 100%;
    min-width: 0;
  }
}

/* ---- a preserved before-record --------------------------------------------------------------
   Rule 7 keeps a replaced version in the same file as an `o-*` option. The first attempt copied
   the old markup verbatim, which was wrong twice over: the record brought its own `id`s, so a
   document ended up with two `v-ok` radios and a `for=` that resolved to whichever came first;
   and its content sat inside `.state` / `.pane`, which are `display: none` until a radio the
   record no longer owns is checked -- so half of what "nothing is deleted" preserved could not
   be seen at all.

   A record is not interactive. It is a flat, labelled stack of everything the screen used to
   show, with no ids and no radios, so it can never collide with the live option and can never
   hide from a reviewer. The label comes from the state or tab the block used to be. */
.recset {
  display: block;
}
.rec {
  display: block;
  border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
  padding-top: var(--s8-space-4);
  margin-top: var(--s8-space-6);
}
.rec::before {
  content: attr(data-rec);
  display: block;
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-3);
}

/* ============================================================================
   THE CANVAS  (87-93, the canvas POC)
   ----------------------------------------------------------------------------
   Shared from the start rather than after the fact: six screens draw the same
   graph, and a component copied into six `<style>` blocks is six components
   that merely look alike. `canvas.js` enhances this markup; none of it depends
   on the script, and with JS off the drawing is complete.

   FULL BLEED, and the two gates that shape it. A canvas state is
   `state state--N page--fill` -- `page--fill` WITHOUT `page`. The fill gate
   fails a `main--fill` screen that has no visible `.page--fill`, and the
   left-edge gate skips a screen that has no visible `.page` at all. Taking
   `page--fill` alone satisfies the first and steps out of the second, which is
   what lets the drawing reach the edges of its pane while every other screen
   keeps its 238px page box.
   ========================================================================= */

/* A column, not a positioning context for everything at once. The bar is a real row that takes
   its own height, and `.cvbody` is the drawing. Making both of them absolute against `.cvwrap`
   is what put the side panel on top of the bar and hid its controls. */
.cvwrap {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.cvbody {
  position: relative;
  flex: 1;
  min-height: 0;
  background-color: var(--s8-color-surface-sunken);
  background-image: radial-gradient(var(--s8-color-border) 1px, transparent 1px);
  background-size: 16px 16px;
  overflow: hidden;
}
.cvwires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Muted text rather than a border colour. A line IS the content on this screen, and at border
   strength on the sunken ground it read as a rendering artefact rather than as the relationship
   it stands for. */
.cvw__line {
  fill: none;
  stroke: var(--s8-color-text-muted);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}
/* A 1.5px line is not a pointer target. The hit path is the same curve at fourteen, invisible,
   and it is what `:hover` actually lands on. */
.cvw__hit {
  fill: none;
  stroke: transparent;
  stroke-width: 14;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}
.cvw:hover .cvw__line {
  stroke: var(--s8-color-wayfind);
  stroke-width: 2.5;
}
/* Staged, not yet applied. The same dashed treatment `.var--edited` uses for a variable that has
   been changed and not applied, carried onto the drawing. */
.cvw--new .cvw__line {
  /* Neo: thin wayfinding uses the lighter Pulse role. */
  stroke: var(--s8-color-wayfind);
  stroke-dasharray: 5 4;
}

/* THE LABEL IS NOT ALWAYS ON. Six services reading four variables is twenty-four labels, and the
   drawing stops being readable well before that -- so a wire carries its name only while it is
   hovered, and clicking it opens the variable in the side panel where there is room for who reads
   it and what rotating it costs. `:has()` is what makes that possible without script; it is a
   reveal, so it is deliberately absent from every screenshot. */
.cvlab {
  position: absolute;
  transform: translate(-50%, -50%);
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text);
  background: var(--s8-color-overlay);
  border: 1px solid var(--s8-color-border-strong);
  padding: 2px 7px;
  border-radius: var(--s8-radius-sm);
  white-space: nowrap;
  opacity: 0;
  z-index: 4;
  pointer-events: none;
  transition: opacity var(--s8-duration-fast) var(--s8-ease);
}
.cvwrap:has(.cvw--db:hover) .cvlab--db,
.cvwrap:has(.cvw--redis:hover) .cvlab--redis,
.cvlab--on {
  opacity: 1;
}

/* A quick-glance pop-out for a wire's variable, rather than the full `.cvside` rail for a
   three-line answer. It is placed in clear space on the drawing and NOT on its wire: a 220px box
   on the label at 33% covers the card column at 48% at every width, so the two highlighted
   `.cvlab` chips are what tie it to its subject. That is weaker than a tail and is the open part
   of this component. Same surface/shadow/radius as
   `.menu__panel` -- it is the same "floating over the drawing" component, just positioned by
   the canvas's own coordinate system instead of by its trigger's own box. Consequential content
   (a service's full detail, a removal) still belongs in `.cvside`, which is why this is a second
   component rather than a smaller variant of that one -- see `.claude/rules/04-working-in-design.md`
   on a modifier needing a real difference: "quick lookup" vs. "deliberate reading" is one. */
.cvpop {
  position: absolute;
  z-index: 6;
  /* Capped against the box the same way `.cvside` is, and for the same reason: a fixed px width
     on a percentage offset drifts into the cards at whatever width nobody measured. Caught at
     768, where a flat 220px ran 16px into `api` and `checkout`. */
  width: min(220px, 30%);
  background: var(--s8-color-surface-raised);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
  padding: var(--s8-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
}
/* The header is the grip. Dragging from anywhere would fight selecting the text inside it, and a
   box you cannot move is a rail that happens to be smaller -- which is the thing the pop-out exists
   to be an alternative to. `canvas.js` enhances this; with the script off the box is still complete
   and still in a sensible place, it just does not move. */
.cvpop__h {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  cursor: grab;
  touch-action: none;
}
.cvpop__h:active {
  cursor: grabbing;
}
/* A service's full detail is more than a three-line answer, so the pop-out carrying one is wider
   than the pop-out carrying a variable. Still well under the 260px rail it is the alternative to. */
.cvpop--wide {
  width: min(300px, 32%);
  max-height: 78%;
  overflow-y: auto;
}
.cvpop__sec {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-2);
}
.cvpop__n {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  font-weight: var(--s8-weight-medium);
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}
.cvpop__row {
  font-size: var(--s8-text-xs);
  line-height: 1.5;
}
.cvpop__row b {
  font-weight: var(--s8-weight-medium);
}
.cvpop__link {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-wayfind);
}

/* ---- a label the user writes on the drawing --------------------------------
   Not a component the product owns the content of. It is the person's own note about their own
   arrangement -- "checkout path", "everything Priya owns" -- so that a drawing of fourteen things
   reads as three ideas. The drawing stays the source of truth about what depends on what; this
   only says what the person was thinking when they arranged it.

   Deliberately NOT a container. A box drawn around cards would claim a relationship the platform
   does not model and cannot enforce -- CANVAS.md 6.2's whole warning is that the drawing shows
   less than people believe it shows, and a frame around four services looks exactly like a thing
   that groups them. Text sitting above them claims nothing.

   Two sizes, because the point is a hierarchy of ideas: one for the heading over a whole region
   and one for a note beside a couple of cards. */
.cvnote {
  position: absolute;
  z-index: 3;
  transform: translate(-50%, -50%);
  /* Hugs its text. Without this the box is the full 320px whatever the label says, so selecting a
     two-word label draws a wide empty rectangle around mostly nothing. */
  width: max-content;
  max-width: 320px;
  padding: 2px 6px;
  border: var(--dir-border-w, 1px) solid transparent;
  border-radius: var(--s8-radius-sm);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  line-height: var(--s8-leading-normal);
  cursor: grab;
  touch-action: none;
  /* `pre-wrap` is not a choice here and declaring anything else is a lie: Blink FORCES it on a
     `contenteditable="plaintext-only"` element during style adjustment, so an author rule saying
     `normal` is computed away -- measured, the rule was ignored and the element still reported
     `pre-wrap`. That matters because every file here is Prettier-formatted: a label long enough to
     wrap in the source gets a newline and eighteen spaces, and `pre-wrap` renders them -- "The
     checkout path" came out pushed halfway across its own panel. So the markup holds the formatter
     off with `<!-- prettier-ignore -->` instead, which is also what keeps a hard break meaning what
     the author typed. Shortening the text is not a fix; the formatter re-wraps it on the next
     write. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.cvnote:hover {
  border-color: var(--s8-color-border);
}
.cvnote:focus {
  outline: none;
  border-color: var(--s8-color-wayfind);
  color: var(--s8-color-text);
  cursor: text;
}
.cvnote:active {
  cursor: grabbing;
}
/* The heading over a region. Big enough to read as a heading at a glance and still quieter than a
   card, which is the thing that matters -- a label that outshouts the services under it has the
   hierarchy backwards. */
.cvnote--lg {
  font-size: var(--s8-text-2xl, 22px);
  letter-spacing: var(--dir-label-tracking, 0.01em);
  text-transform: uppercase;
  color: var(--s8-color-text-muted);
  opacity: 0.85;
}

/* Three sizes and two orientations, because a label over a column and a label down the side of one
   are the same idea at different angles. `writing-mode` rather than a `rotate()`: a rotated box
   still lays out as a wide one, so it would overlap what sits beside it, and the caret in a
   rotated `contenteditable` lands in the wrong place. */
.cvnote--sm {
  font-size: var(--s8-text-sm);
  text-transform: none;
  letter-spacing: normal;
  opacity: 1;
}
.cvnote--vert {
  writing-mode: vertical-rl;
  max-width: none;
  max-height: 320px;
}
/* The tools for the selected label. It follows the label rather than sitting in a corner, because
   at this size the label is small enough that a toolbar anywhere else is a hunt. */
.cvnotebar {
  position: absolute;
  z-index: 11;
  display: none;
  align-items: center;
  gap: 2px;
  padding: 3px;
  transform: translate(-50%, -100%);
  background: var(--s8-color-overlay);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  box-shadow: var(--s8-shadow-overlay);
  white-space: nowrap;
}
.cvnotebar--on {
  display: flex;
}
.cvnotebar button {
  min-width: 24px;
  height: 22px;
  padding: 0 var(--s8-space-2);
  border: 0;
  border-radius: var(--s8-radius-sm);
  background: transparent;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-xs);
  font-family: inherit;
  cursor: pointer;
  line-height: 1;
}
.cvnotebar button:hover {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
}
.cvnotebar button[aria-pressed='true'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-wayfind);
}
.cvnotebar__sep {
  width: 1px;
  align-self: stretch;
  margin: 0 2px;
  background: var(--s8-color-border);
}

/* ---- a thing on the drawing ------------------------------------------------
   A label rather than a button, because selecting is a radio and everything
   below the canvas enhancement is script-free. */
.cvcard {
  position: absolute;
  z-index: 2;
  transform: translate(-50%, -50%);
  width: 232px;
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-3);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-2);
}
.cvcard:hover {
  border-color: var(--s8-color-text-muted);
}
/* Measured against the real drawing (design:review's own overlap check, run at the suite's three
   widths -- 1440/1100/768), not assumed: a fixed 232px card against percentage-based positions
   means the SAME layout collides once the canvas itself narrows, because the card's own pixel
   footprint stays put while the room between columns shrinks. At 1100 (1044px of drawing) the
   tightest adjacent-column gap measured 188px; at 768 (712px) it measured 128px. Two tiers, not
   one: 900px was already accepted for the narrow tier, widened here down from 196px (till narrow
   enough to clear 128px) and extended up to cover 1100 too, which had no reduction at all before
   and still collided at full width. */
@media (max-width: 1200px) {
  .cvcard {
    width: 175px;
  }
}
@media (max-width: 900px) {
  .cvcard {
    width: 150px;
  }
}
/* Dragging a card selected its text, so a drag across the drawing left every card highlighted.
   A card is a thing you move, not a paragraph you quote. */
.cvcard,
.cvground {
  -webkit-user-select: none;
  user-select: none;
}
/* The card is a div with a stretched label over it rather than a label wrapping everything,
   because it now carries TWO controls: open, and remove. A label cannot nest a label. */
.cvcard__hit {
  position: absolute;
  inset: 0;
  cursor: pointer;
  border-radius: var(--s8-radius-md);
  /* Some cards use a <button> while others use a <label>. Native button paint
     covered the card body with a solid rectangle in the captured canvas. */
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
}
/* One gesture on the card, not two. It used to be a bare `×`, which made the destructive action
   the single most prominent thing on every card; a `⋯` demotes removal to one row among seven and
   gives the other six somewhere to live. Built from the shared `.menu` — hidden checkbox, label,
   scrim, panel — so it opens with no script at all, takes a keyboard stop, and works on touch.
   `canvas.js` adds right-click as an accelerator onto the very same checkbox. */
.cvmenu {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: 3;
}
/* The shared scrim is `position: fixed; inset: 0`, which covers the viewport everywhere else in
   the set. It cannot here: `.cvcard` carries `transform: translate(-50%, -50%)`, and a transform
   makes the element the CONTAINING BLOCK for fixed descendants — so the scrim was fixed to the
   card and covered only the card, which is why the menu closed on clicking itself and nothing
   else.

   It is pinned to the card on purpose now, and `canvas.js`'s document-level handler is what
   makes a click anywhere dismiss the menu. The intermediate spelling — absolute with
   `inset: -3000px`, to cover the drawing — was a 6000px invisible element extending 2475px past
   the right of the page, and the clipped-out-of-reach gate says so. That gate had never seen it:
   it reaches a state by CLICKING its tab, and the document handler was closing the menu on that
   very click, so the one state with an open menu was measured with the menu shut. Fixing the
   screenshot is what made the gate able to see this. */
.cvmenu .menu__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.cvmenu .menu__panel {
  z-index: 2;
}
/* A card with its menu open has to rise above its neighbours. Every card sits at the same
   z-index, so without this the panel opens UNDERNEATH whichever card comes later in the markup —
   which is what happened: four of the seven rows were hidden behind the card below. */
.cvcard:has(.cvmenu .menu__toggle:checked) {
  z-index: 8;
}
/* The hover pop-out shows on `:hover`/`:focus-within` (`.cvpeek`, below), and the checkbox inside
   an open menu keeps `:focus-within` true on the card — so the pop-out sat over the open menu it
   has nothing to do with. Menu open wins; the pop-out waits its turn. */
.cvcard:has(.cvmenu .menu__toggle:checked) .cvpeek {
  opacity: 0;
  pointer-events: none;
}
/* ONE small icon affordance, four places that need it: the card's `⋯`, the panel's `✕`, the
   pop-out's `✕`, and (UI-MODAL) every modal's own `✕` — added here rather than as a fourth
   near-identical block, the same reuse this comment already argues for. Written as a selector
   list rather than separate blocks -- `04-working-in-design.md` records the same box under three
   names (`.srch` / `.logsearch` / `.ctl__search`), and its order-to-reach-for puts a modifier
   ahead of a new rule. Only the genuine differences are restated below: the card's is
   hover-revealed because a `⋯` on every card at rest is nine pieces of chrome on the drawing, and
   the pop-out's sits on a raised surface, so its hover has to go the other way to show at all. */
.cvcard__x,
.cvside__x,
.cvpop__x,
.modal__x {
  width: 22px;
  height: 22px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  padding: 0;
  border-radius: var(--s8-radius-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
  line-height: 1;
  font: inherit;
}
/* C-18, ruled by Hasnat 2026-09-29: right-click only, no visible ⋯ affordance -- the card's own
   trigger is inert, same treatment `.cvground .menu__btn` already had for the ground's menu below.
   It was hover/focus/checked-revealed before; a hover reveal is still a visible affordance, just
   a later one, so opening the menu (`:checked`) no longer needs to keep it shown either. The
   checkbox stays a real, focusable, Space-toggled control -- only its own label is hidden. */
.cvmenu .cvcard__x {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
/* DESIGN.md: "Hover goes lighter on dark and darker on light." The shared `.menu__item:hover`
   (surface-sunken) is darker than this panel's own surface-raised in both themes, which reads as
   the row sinking rather than lighting — mixing a slice of the theme's own text colour into the
   panel surface lightens on a dark panel and darkens on a light one without a second token. Scoped
   to the canvas card menu; the shared menu keeps its own rule. */
.cvmenu .menu__item:hover {
  background: color-mix(
    in oklab,
    var(--s8-color-text) 8%,
    var(--s8-color-surface-raised)
  );
}
/* Remove is the one destructive row on the card's menu (services.manage, `actions.yaml:50`) and
   read the same as every other action until you were already on it. Danger's own token, straight
   against this panel's `surface-raised`, measures 4.36:1 in dark -- under WCAG's 4.5 for this
   size text (checked, not assumed). A 12% white mix (still `--s8-color-danger` at its core, the
   same derivation `--s8-color-accent-hover` already uses for a context the flat token cannot
   clear) passes both themes without a second named colour. */
.cvmenu .menu__item--danger {
  color: color-mix(in oklab, var(--s8-color-danger) 88%, white);
}
.cvmenu .menu__item--danger:hover {
  background: color-mix(in oklab, var(--s8-color-danger) 12%, transparent);
}
.cvpop__x {
  width: 20px;
  height: 20px;
}
.cvcard__x:hover,
.cvside__x:hover {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
}
.cvpop__x:hover,
.modal__x:hover {
  background: var(--s8-color-surface);
  color: var(--s8-color-text);
}
/* Right-clicking the ground rather than a card. Where people look for "add something", and the
   natural home for the layout action the drawing does not have yet. */
.cvground {
  position: absolute;
  left: 50%;
  top: 46%;
  z-index: 3;
}
.cvground .menu__btn {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.cvcard__t {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
}
/* The logo slot. A real brand mark drops straight in here; what ships today is a mark for the
   KIND of source, because an invented brand mark reads exactly as authoritative as a real one and
   this set has already paid for that lesson once.
   Masked rather than inlined: twenty cards across five states is twenty copies of the same path
   in the markup, and the shape is presentation. `currentColor` keeps it on the token scale. */
.cvlogo {
  width: 22px;
  height: 22px;
  flex: none;
  display: block;
  background: currentColor;
  color: var(--s8-color-text-muted);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 22px 22px;
  mask-size: 22px 22px;
}
.cvlogo--git {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='6' cy='6.5' r='2.6'/%3E%3Ccircle cx='6' cy='17.5' r='2.6'/%3E%3Ccircle cx='18' cy='12' r='2.6'/%3E%3Cpath d='M6 9.1v5.8'/%3E%3Cpath d='M8.6 6.5h4.6a2.2 2.2 0 0 1 2.2 2.2v1'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='6' cy='6.5' r='2.6'/%3E%3Ccircle cx='6' cy='17.5' r='2.6'/%3E%3Ccircle cx='18' cy='12' r='2.6'/%3E%3Cpath d='M6 9.1v5.8'/%3E%3Cpath d='M8.6 6.5h4.6a2.2 2.2 0 0 1 2.2 2.2v1'/%3E%3C/svg%3E");
}
.cvlogo--image {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 20 7.7v8.6L12 20.6 4 16.3V7.7Z'/%3E%3Cpath d='M4 7.7 12 12l8-4.3M12 12v8.6'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.4 20 7.7v8.6L12 20.6 4 16.3V7.7Z'/%3E%3Cpath d='M4 7.7 12 12l8-4.3M12 12v8.6'/%3E%3C/svg%3E");
}
.cvlogo--data {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cellipse cx='12' cy='6.4' rx='7' ry='3.2'/%3E%3Cpath d='M5 6.4v11.2c0 1.8 3.1 3.2 7 3.2s7-1.4 7-3.2V6.4'/%3E%3Cpath d='M5 12c0 1.8 3.1 3.2 7 3.2s7-1.4 7-3.2'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cellipse cx='12' cy='6.4' rx='7' ry='3.2'/%3E%3Cpath d='M5 6.4v11.2c0 1.8 3.1 3.2 7 3.2s7-1.4 7-3.2V6.4'/%3E%3Cpath d='M5 12c0 1.8 3.1 3.2 7 3.2s7-1.4 7-3.2'/%3E%3C/svg%3E");
}
.cvlogo--model {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 3.2c0 4 1.7 5.8 5.7 5.8-4 0-5.7 1.8-5.7 5.8 0-4-1.7-5.8-5.7-5.8 4 0 5.7-1.8 5.7-5.8Z'/%3E%3Cpath d='M17.4 14c0 2 .9 2.9 2.9 2.9-2 0-2.9.9-2.9 2.9 0-2-.9-2.9-2.9-2.9 2 0 2.9-.9 2.9-2.9Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 3.2c0 4 1.7 5.8 5.7 5.8-4 0-5.7 1.8-5.7 5.8 0-4-1.7-5.8-5.7-5.8 4 0 5.7-1.8 5.7-5.8Z'/%3E%3Cpath d='M17.4 14c0 2 .9 2.9 2.9 2.9-2 0-2.9.9-2.9 2.9 0-2-.9-2.9-2.9-2.9 2 0 2.9-.9 2.9-2.9Z'/%3E%3C/svg%3E");
}
/* A name breaking mid-word ("ch/ec/ko/ut") at the narrow card tiers was worse than truncating it —
   nowrap + ellipsis reads as "the rest is cut off", `overflow-wrap: anywhere` read as broken text. */
.cvcard__n {
  font-weight: var(--s8-weight-medium);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
.cvcard__st {
  flex: none;
}
/* Where the thing came from -- a repository, an image, a catalogue model, a kind of database.
   This is the line that tells you what a service IS, and the drawing was thinner without it. */
/* ---- the map, which is the canvas one level out ---------------------------
   Same ground, same cards, same floating bar. That is the argument, not a saving: a map drawn in a
   different visual language would read as a different product, when what it shows is the same
   estate at a wider zoom. The only new parts are the level path and the note that says what a
   level MEANS.

   A LEVEL IS A LEGAL BOUNDARY, NOT A DISTANCE. Two metros inside EU-27 are one answer to "where
   may this data be"; two metros either side of that line are two answers. That is why the top
   level is jurisdictions rather than continents, and it is the whole reason the hierarchy exists.
   ------------------------------------------------------------------------- */
/* THE MAP IS AN ACTUAL MAP. Equirectangular, drawn in degrees -- a path's coordinates ARE
   longitude+180 and 90-latitude, so a pin's position is its real position rather than a number
   somebody nudged until it looked right. Each level crops the SAME drawing with a different
   `viewBox`, which is what makes zooming a level a real geographic zoom and not a second picture.

   `preserveAspectRatio="none"` on purpose: slice CROPS, and a cropped map no longer agrees with a
   pin placed by percentage -- the pins drifted off their coastlines, measured. Stretching the
   projection to the pane keeps a pin exactly where its coordinates say, which is the half that has
   to be true; equirectangular is already a distortion and this is one more.

   THE COASTLINES ARE REAL. Natural Earth 1:110m admin-0, which is PUBLIC DOMAIN -- its terms say
   no permission is needed and it may be modified and redistributed, so there is no licence
   question to carry. Projected into degrees by the same two lines as everything else, decimated to
   0.12 degrees and rounded to one, which is about 11km: far finer than any zoom this screen offers
   and about 125KB of path data. The first version of this was drawn by hand at twenty points a
   continent -- it read as the world at world zoom and as angular blobs the moment anyone zoomed
   into Europe, which is the level the screen exists for.
*/
/* The map's own surface. It takes the pointer for panning, so it sits under the floating bar and
   the note but over the ground. */
/* On the map the caption sits bottom-left, where the zoom cluster lives on the drawing, so the
   cluster moves to the other corner rather than the two sharing a spot. */
/* Two classes on purpose: `.cvctl` is declared later in this file, so a single-class modifier
   loses to it on source order and the bar keeps BOTH offsets -- which stretches it across the
   whole pane rather than moving it. */
.cvctl.cvctl--r {
  left: auto;
  right: var(--s8-space-4);
}
.cvmapwrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}
.cvmapwrap--grabbing {
  cursor: grabbing;
}
.cvmap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.cvmap__g {
  fill: none;
  stroke: var(--s8-color-border);
  stroke-width: 0.3;
  vector-effect: non-scaling-stroke;
  opacity: 0.5;
}
.cvmap__l {
  fill: var(--s8-color-surface);
  stroke: var(--s8-color-border-strong);
  stroke-width: 0.8;
  vector-effect: non-scaling-stroke;
}
/* A pin is a dot at the place and a label beside it, NOT a card. A 232px card on a map covers the
   geography it is pointing at, which is the one thing a map is for. */
/* THE DOT is on the coordinate, not the pin's midpoint. Centring the whole pin puts the dot
   wherever the label's width happens to push it -- measured, that put the UK's dot 80px EAST of
   the EU's, which is the one thing a map may not get wrong. So the offset is half a dot, and the
   label hangs off it. */
.cvpin {
  position: absolute;
  z-index: 4;
  transform: translate(-4.5px, -50%);
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  cursor: pointer;
}
/* Label on the other side of the dot. Two places 11 degrees apart are two pixels apart at world
   zoom, so at some point a label has to go the other way -- the dot stays on the place either way,
   which is the part that has to be true. */
.cvpin--l {
  flex-direction: row-reverse;
  transform: translate(calc(-100% + 4.5px), -50%);
}
/* `hidden` has to WIN over the class. `.cvpin` sets `display: flex`, and an author rule beats the
   UA's `[hidden] { display: none }` every time -- so pins the script had hidden went on rendering,
   and both the jurisdictions and the metros drew at once. Found by looking at the screen; the probe
   was reading the `hidden` property, which was perfectly true and said nothing about what was on
   the screen. */
.cvpin[hidden] {
  display: none;
}
.cvpin__d {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50%;
  background: var(--s8-color-accent);
  box-shadow: 0 0 0 3px var(--s8-color-overlay);
}
.cvpin__b {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--s8-space-2) var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-overlay);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  white-space: nowrap;
}
.cvpin:hover .cvpin__b {
  border-color: var(--s8-color-wayfind);
}
.cvpin__n {
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-medium);
  color: var(--s8-color-text);
}
.cvpin__m {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}

.cvlevel {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  font-size: var(--s8-text-sm);
}
.cvlevel__k {
  color: var(--s8-color-text-muted);
  opacity: 0.7;
}
.cvlevel__o {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.cvlevel__o:hover {
  color: var(--s8-color-text);
}
.cvlevel__o--on {
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
  cursor: default;
}
/* The level you are on opens its own things (to-draw #1), so as a control it takes the pointer. */
label.cvlevel__o--on {
  cursor: pointer;
}
label.cvlevel__o--on:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cvlevel__sep {
  color: var(--s8-color-text-muted);
  opacity: 0.6;
}
/* Pinned to the foot of the drawing rather than floated over it: it is the caption for the level,
   and a caption that moves when you pan is a caption you lose. */
.cvmapnote {
  z-index: 4;
  position: absolute;
  left: var(--s8-space-6);
  right: var(--s8-space-6);
  bottom: var(--s8-space-5);
  max-width: 62ch;
  padding: var(--s8-space-3) var(--s8-space-4);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-overlay);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  line-height: var(--s8-leading-normal);
}

/* DEDICATED HARDWARE, made scannable. `1×H100` was sitting in the same muted meta line as
   `512 MB`, so the one scarce thing on the drawing read like any other number -- on a drawing whose
   whole job is "what is here and what does it cost", the GPU is the fact you look for first.

   Deliberately NO colour. `base.css`'s two channels are already spoken for: health owns the border
   and the badge, staged owns the left stripe. A third colour on a card would collide with both, so
   this is a mono token on the existing surface -- weight and typeface, not hue.

   WHOLE CARDS ONLY. `40-sizing-moment` states the rule -- the card is dedicated to this model,
   there is no co-tenant on it -- and fractional or utilization display is in vocabulary.json's
   `removed` list annotated STAYS REMOVED. So this names the allocation and never a percentage of
   one. */
.cvhw {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  letter-spacing: 0.04em;
  padding: 1px 5px;
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  color: var(--s8-color-text);
  white-space: nowrap;
}
.cvcard__src {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-2);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  border-top: 1px solid var(--s8-color-border);
  padding-top: var(--s8-space-2);
  min-width: 0;
}
.cvcard__srck {
  font-family: var(--s8-font-mono);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  flex: none;
}
.cvcard__srcv {
  font-family: var(--s8-font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.cvcard__m {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.cvcard--on {
  border-color: var(--s8-color-wayfind);
  box-shadow: 0 0 0 1px var(--s8-color-wayfind);
}
/* TWO CHANNELS, TWO MEANINGS -- and they must not share one.
   A card can be crash-looping AND have an uncommitted change, so health and staged cannot both
   own the border: one would silently overwrite the other and the drawing would lie about it.
   HEALTH owns the border and the badge. STAGED owns the left inset. Nothing else touches either. */
/* DESIGN.md: "Pulse is a fill, never a stroke." This inset line IS a stroke, and it read as an
   error edge rather than a staged marker -- the wayfind token (a lighter Pulse) is what carries a
   thin marker like this one. */
.cvcard--edited {
  box-shadow: inset 3px 0 0 var(--s8-color-wayfind);
}
/* Health, as a ladder. Live is SILENT -- no badge, no colour, nothing. Twenty-seven cards all
   saying `Live` is not a signal, it is texture, and it raises the threshold for spotting the one
   card that matters. The good news is a property of the environment and gets said once, in the
   bar; the bad news belongs to one thing and lives on that thing. */
.cvcard--warn {
  border-color: var(--s8-color-warning);
}
.cvcard--fail {
  border-color: var(--s8-color-danger);
}
/* Stopped is deliberate, not wrong. Colouring it red teaches people to ignore red. */
.cvcard--idle {
  opacity: 0.6;
}
/* Unknown must never be mistakable for fine -- that is the whole risk silence creates. */
.cvcard--unknown {
  border-style: dashed;
}
.cvcard__why {
  font-size: var(--s8-text-sm);
}
.cvcard--warn .cvcard__why {
  color: var(--s8-color-warning);
}
.cvcard--fail .cvcard__why {
  color: var(--s8-color-danger);
}
/* The state chip (`Degraded`) carried the same plain `.badge` as `Updating`, so the one state
   that IS a warning read no differently from one that is not — the border already turns warning
   colour (above); the word naming it did not. */
.cvcard--warn .cvcard__st {
  color: var(--s8-color-warning);
}
/* The corner holds the most useful thing about a card: normally its url, exceptionally its
   problem. Dropping the badge from healthy cards frees the slot rather than emptying it. */
.cvcard__url {
  flex: none;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  position: relative;
  z-index: 1;
}
.cvcard__url:hover {
  color: var(--s8-color-wayfind);
}

/* The affirmation. Said once, here, because silence on a card can only mean healthy if something
   else states that the platform actually looked. */
.cvsum {
  display: flex;
  align-items: center;
  gap: var(--s8-space-4);
  flex-wrap: wrap;
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-1);
}
.cvsum__x {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
}
.cvsum__n {
  color: var(--s8-color-text);
  font-variant-numeric: tabular-nums;
}
/* Toggled by canvas.js only when the digit actually changed (never on first render), same
   opt-in-class shape as `.stepper__tick--draw` -- a class the script adds and removes, not a
   permanent rule, so the animation replays each time instead of running once and going inert. */
.cvsum__n--bump {
  animation: cvsum-bump var(--s8-duration-normal) var(--s8-ease);
}
@keyframes cvsum-bump {
  from {
    opacity: 0.3;
  }
}
/* Opaque, not transparent. A ghost card still has to occlude the wire that ends inside it — drawn
   see-through, the dashed line ran straight across its own label. */
.cvcard--ghost {
  border-style: dashed;
  /* 23 D1 — a staged card's outline is a thin marker, not a Pulse fill. */
  border-color: var(--s8-color-wayfind);
  background: var(--s8-color-surface-sunken);
}
/* Found on the drawer state (v-9, "Nothing running is touched"): forced uppercase on already
   sentence-case authored text, at the wide tracking `--dir-label-tracking` is for genuine all-caps
   only (Hasnat 2026-09-29: wide spacing only for all-caps text). Reads as written; the Pulse-derived
   colour stays. */
.cvcard__f {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-wayfind);
}

/* ---- the controls that make a change --------------------------------------
   Drawn, and deliberately inert: every one is `data-wire="pending"`, which in
   this set means "should be wired later" rather than "cannot be". They are here
   so the LAYOUT of the panel can be judged; the "after" picture each one
   produces is a state tab, hand-authored, because the counts on these screens
   are written rather than counted.

   Not a native checkbox. The set has none visible anywhere -- its on/off idiom
   is `.seg` -- and a native control would also have to survive the
   unstyled-control gate in five directions. A button carries the affordance,
   takes a keyboard stop, and says what it is. */
.cvpick {
  display: flex;
  align-items: flex-start;
  gap: var(--s8-space-3);
  width: 100%;
  text-align: left;
  padding: var(--s8-space-2) var(--s8-space-2);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--s8-radius-sm);
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.cvpick:hover {
  background: var(--s8-color-surface-raised);
}
.cvpick__box {
  width: 18px;
  height: 18px;
  flex: none;
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--s8-text-xs);
  color: transparent;
  margin-top: 2px;
}
.cvpick--on .cvpick__box {
  background: var(--s8-color-accent);
  border-color: var(--s8-color-accent);
  color: var(--s8-color-accent-contrast);
}
.cvpick__b {
  min-width: 0;
}
.cvpick__k {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  display: block;
}
.cvpick__d {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  display: block;
}
/* Changed and not applied -- the same accent inset a variable row already uses, so a staged
   tick and a staged variable read identically. */
.cvpick--staged {
  border-color: var(--s8-color-wayfind);
  box-shadow: inset 3px 0 0 var(--s8-color-accent);
}
.cvpick__f {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-wayfind);
  display: block;
}

/* A number you step rather than type: instances is 1..8, and a text field invites 400. */
.cvstep {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--s8-color-control-border);
  border-radius: var(--s8-radius-sm);
  overflow: hidden;
}
.cvstep__b {
  width: 30px;
  height: var(--s8-option-h);
  background: none;
  border: 0;
  color: var(--s8-color-text-muted);
  cursor: pointer;
  font: inherit;
}
.cvstep__b:hover {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
}
.cvstep__v {
  min-width: 34px;
  text-align: center;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-base);
  border-left: 1px solid var(--s8-color-control-border);
  border-right: 1px solid var(--s8-color-control-border);
  line-height: var(--s8-option-h);
}

/* ---- chrome that floats ON the drawing rather than above it ----------------
   The environment name, the Canvas/List switch and the deploy action used to
   sit in a page header, which cost the drawing 120px of height and read as two
   separate surfaces. They are one surface now. `canvas.js` keeps every one of
   these out of the panning stage. */
.cvbar {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s8-space-4);
  flex-wrap: wrap;
  /* The DRAWING is full-bleed on purpose; the HEADING is not. `.page` is capped at
     `--s8-container-main` and centred, so on a wide pane its left edge sits further right than a
     flush 24px — and a canvas bar padded flush put `production` at 240px while all 67 other product
     headings start at 262. The left-edge gate measures exactly that and said so.

     This reproduces `.page`'s centring without a wrapper element: the percentage resolves against
     `.cvwrap`, which is the pane, so the inset is the same one `margin: 0 auto` produces. `max()`
     keeps it at a plain 24px once the pane is narrower than the container, which is where `.page`
     stops centring too. */
  padding: var(--s8-space-5)
    max(
      var(--s8-space-6),
      calc((100% - var(--s8-container-main)) / 2 + var(--s8-space-6))
    );
  background: var(--s8-color-surface);
  border-bottom: 1px solid var(--s8-color-border);
}
.cvbar__l {
  min-width: 0;
}
.cvbar__r {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex: none;
}
.cvbar h1 {
  font-size: var(--s8-text-2xl);
  font-weight: var(--dir-display-weight, var(--s8-weight-semibold));
  text-transform: var(--dir-display-transform, none);
  letter-spacing: var(--dir-display-tracking, 0);
  margin: 0;
}
.cvbar__sub {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  margin-top: 2px;
}
/* Bottom LEFT, deliberately: a gate fails anything interactive sitting behind the direction
   switcher, which is bottom right. Railway puts its canvas controls here too. */
.cvctl {
  z-index: 5;
  position: absolute;
  left: var(--s8-space-4);
  bottom: var(--s8-space-4);
  display: flex;
  align-items: center;
  gap: var(--s8-space-1);
  background: var(--s8-color-surface);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  padding: var(--s8-space-1);
}
/* Six controls in a row read as one undifferentiated strip. A divider between
   `◍` / zoom / Tidy up / List groups them by what they do, the same vertical rule
   `.cvnotebar__sep` already uses for its own button cluster. */
.cvctl__sep {
  width: 1px;
  align-self: stretch;
  margin: 0 2px;
  background: var(--s8-color-border);
}
/* `−`/`＋` are the one pair in this bar with no label, just a glyph, so they get a square 28px
   icon button (the size `.sidebar__toggle`/`.cvnotice__x` already use) instead of `.btn--sm`'s own
   12px side padding, which reads as an oversized pill around one character. 6px, not 0: the
   measure gate wants every `.btn` to keep at least 6px of its own padding. */
.cvctl__zoom {
  width: 28px;
  height: 28px;
  padding: 0 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ---- the side panel -------------------------------------------------------
   Opens on the canvas rather than navigating away, because the whole argument
   for a drawing is that you keep looking at it while you change one thing. */
/* ---- the template card, promoted ------------------------------------------
   Written in `51-template-gallery`'s own <style> and now needed by the canvas add flow too.
   `04-working-in-design.md`: if two screens need the same component it goes in base.css --
   copying it would make two components that merely look alike, and the duplicate-selector gate
   would have caught it as one. Moved rather than copied; 51 no longer declares it.
   ------------------------------------------------------------------------- */
.tg {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--s8-space-4);
}
.tcard {
  /* A column, so the chip row can be pushed to the bottom. The cards are already equal
           height -- the grid stretches them -- but their chips sat wherever the description ended,
           so a row of four read as four different cards rather than one row. */
  display: flex;
  flex-direction: column;
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-5);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--s8-duration-fast) var(--s8-ease);
}
.tcard:hover {
  border-color: var(--s8-color-border-strong);
}
.tcard__n {
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-1);
}
.tcard__d {
  font-size: var(--s8-text-base);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-normal);
  margin-bottom: var(--s8-space-4);
}
.tcard__m {
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-1);
}

/* ---- canvas mode: the shell gets out of the way ---------------------------
   The drawing is the screen here, so the nav drops to the 56px icon rail at EVERY width rather
   than only below 1024 -- same rail the shell already collapses to, not a second mechanism. The
   label it drops comes back on hover as a floating chip, so a rail does not cost you the
   destination's name. `.app--canvas` is the opt-in: every other screen keeps the full nav. */
.app--canvas {
  grid-template-columns: 56px minmax(0, 1fr);
}
.app--canvas .nav__item span:not(.nav__icon),
.app--canvas .nav__cta span:not(.nav__icon) {
  position: absolute;
  left: calc(100% + var(--s8-space-2));
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  padding: 3px 9px;
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-overlay);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border-strong);
  color: var(--s8-color-text);
  font-size: var(--s8-text-sm);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--s8-duration-fast) var(--s8-ease);
}
.app--canvas .nav__item:hover span:not(.nav__icon),
.app--canvas .nav__item:focus-visible span:not(.nav__icon),
.app--canvas .nav__cta:hover span:not(.nav__icon) {
  opacity: 1;
}
.app--canvas .nav__item,
.app--canvas .nav__cta {
  position: relative;
  justify-content: center;
  padding: 0;
}
.app--canvas .nav__group,
.app--canvas .brandbar__name {
  display: none;
}
.app--canvas .brandbar {
  justify-content: center;
  padding: 0;
}

/* A PERCENTAGE, not a px count, and that is the whole fix. Every card on a panel-open screen is
   hand-placed at a percentage assuming the drawing keeps the left 66% of `.cvbody`; the fixed
   372px this used to be was only ~30% of `.cvbody` at the one width anyone measured it at (1440),
   so everywhere else it drifted out of step with them. Measured across 85 panel states at
   768/1024/1280/1366/1440: 151 card-vs-panel overlaps before, 0 after. A percentage resolves against the
   same box the cards' percentages do, so it cannot drift again at a width nobody checked.

   30% rather than the 34% the cards reserve, because a card is a FIXED 232px on a percentage
   coordinate: at 768 the rightmost one is centred at 52.14% and still reached 3px past a 32%
   panel. 30% clears it at every width the canvas is drawn at -- it has no form below 720, where
   `.cvphone` takes over.

   `min()` caps it at 260px, so above ~870px of `.cvbody` the panel stops widening with the
   viewport. Down from 372px: the width itself is the "significantly reduce, make more minimal"
   that was asked for, rather than a paint job over the same footprint. */
/* 01 D2 (amended), D3, D6 — the 420px narrow rail is gone; the panel has
   exactly two widths, half (the default) and full. See the widen control
   and the `:has()` rule this feeds, appended under "v1, ANV-30" below. */
.cvside {
  z-index: 5;
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: var(--cv-split, 50%);
  background: var(--s8-color-surface);
  border-left: 1px solid var(--s8-color-border-strong);
  padding: var(--s8-space-5);
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
}

/* SELECTION IS A CANVAS-WIDE FACT, not a card-level one. A ring on the open card is easy to lose
   on a drawing this dense, so the rest of the drawing steps back instead: one class on `.cvbody`
   dims every card that is not the open one, rather than a modifier on each of the others. Wires go
   with them, or a dimmed card stays tied to the drawing by a line at full strength. */
/* Only while tidying. A permanent transition on `left`/`top` would make dragging lag the pointer,
   which is the one thing a canvas cannot do. */
/* The pan-and-zoom layer canvas.js wraps the drawing in; it fills the wrapper exactly, so every
   authored percentage resolves against the same box. */
.cvstage {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
}
.cvcard--settling {
  transition:
    left var(--s8-duration-slow) var(--s8-ease),
    top var(--s8-duration-slow) var(--s8-ease);
}
.cvbody--sel .cvcard:not(.cvcard--on) {
  opacity: 0.32;
}
.cvbody--sel .cvw__line {
  opacity: 0.35;
}
.cvcard--on {
  z-index: 7;
}

/* Tabs INSIDE the drawer. The panel carries everything a service has, which is more than one
   column of prose can hold without becoming a scroll -- so the settings sit in their own tab
   rather than below the fold of the overview. Radio-driven like every other state in this set,
   with its own group name so it cannot collide with the screen's `v-*` radios. */
.cvtabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-1);
  border-bottom: var(--dir-border-w, 1px) solid var(--s8-color-border);
  margin: 0 calc(-1 * var(--s8-space-5));
  padding: 0 var(--s8-space-5);
}
.cvtabs label {
  padding: var(--s8-space-2) var(--s8-space-3);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.cvtabs label:hover {
  color: var(--s8-color-text);
}
.cvpane {
  display: none;
  flex-direction: column;
  gap: var(--s8-space-4);
}
/* The pane's gap already spaces a heading from what follows; its own margin and a first row's
   top padding stacked to 44px of dead air under every rule. */
.cvpane > .sec {
  margin-bottom: 0;
}
.cvside .cvpane > .sec + .fr2 {
  padding-top: 0;
}
/* Consecutive rows share one hairline rhythm (their own padding), not padding plus the pane gap. */
.cvside .cvpane > .fr2 + .fr2 {
  margin-top: calc(-1 * var(--s8-space-4));
}
/* A value and its badge are two things; inline whitespace in mono left them touching. */
.mono + .mark {
  margin-left: var(--s8-space-2);
}
#sv-a:checked ~ .cvside__b .cvpane--a,
#sv-b:checked ~ .cvside__b .cvpane--b,
#sv-c:checked ~ .cvside__b .cvpane--c,
#sv-d:checked ~ .cvside__b .cvpane--d,
#sv-e:checked ~ .cvside__b .cvpane--e {
  display: flex;
}
#sv-a:checked ~ .cvtabs label[for='sv-a'],
#sv-b:checked ~ .cvtabs label[for='sv-b'],
#sv-c:checked ~ .cvtabs label[for='sv-c'],
#sv-d:checked ~ .cvtabs label[for='sv-d'],
#sv-e:checked ~ .cvtabs label[for='sv-e'] {
  color: var(--s8-color-text);
  /* Neo: selected-tab underline is a thin wayfinding marker. */
  border-bottom-color: var(--s8-color-wayfind);
}
/* The shared settings row, stacked. `.fr2__l` is a 190px label column beside its control, which is
   right on a full settings page and wrong in a 416px rail -- 190 of 416 leaves the control 200px
   and the description wrapping inside it. Same modifier-with-a-real-difference call as
   `.cvside__acts .btn`, and the same reason `.cvfield` exists instead of `.u` here. */
.cvside .fr2 {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s8-space-2);
  padding: var(--s8-space-4) 0;
}
.cvside .fr2__l {
  width: auto;
}
.cvside .fr2:last-child {
  border-bottom: 0;
}
/* A URL is the payoff of the whole product, so it gets room to be read and copied rather than
   being a value in a row. Mono, wrapping anywhere, because a host does not break on spaces.
   F-08/VX-6, `journey-checklist.md` URL-01 — the always-column stack put Copy on its own line
   under the value at every width, including full, where the block is widest and the buttons had
   the most room to sit beside it instead: "a very wide bordered block whose Copy button drops
   beneath the value ... leaving most of the block empty." Value and actions now share a row and
   wrap only when the width actually forces it, the same compact grouping `.cvnotice__item`
   already uses for a card's ready URL. */
.cvurl {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s8-space-2) var(--s8-space-3);
  padding: var(--s8-space-3);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
}
.cvurl__v {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  overflow-wrap: anywhere;
  flex: 1 1 240px;
  min-width: 0;
}
.cvstep {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
}
.cvstep .input {
  width: 56px;
  text-align: center;
}
.cvurl__acts {
  display: flex;
  flex: none;
  gap: var(--s8-space-2);
}

.cvside__b {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
  flex: 1;
  min-height: 0;
  /* UI-PANEL — was on `.cvside` itself, which scrolled the header and tabs away with the
     content. Header, kind line and tabs are the sibling flex items above this one and stay put;
     only the open pane scrolls. */
  overflow-y: auto;
}

/* A PEEK, not a panel. The pop-out follows the pointer's interest: hovering a card shows it,
   moving onto the pop-out itself keeps it (so its link is reachable), and leaving both takes it
   away. Clicking is the other gesture entirely and always opens the drawer -- this never replaces
   that, which is why it carries a few lines and a way in rather than the service's controls.
   `--on` forces it visible, because a hover cannot be photographed and every screenshot in this
   set is a photograph. */
.cvpeek {
  position: absolute;
  left: calc(100% + var(--s8-space-2));
  top: 0;
  /* A FIXED width here, unlike `.cvpop` on the drawing. Its containing block is the card, not
     `.cvbody`, so `.cvpop`'s `min(220px, 30%)` would resolve 30% against a 232px card and hand
     this a 70px column -- measured, and every line in it overflowed. */
  width: 220px;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--s8-duration-fast) var(--s8-ease);
}
/* A CHILD of the card, not a sibling. It then tracks the card when the card is dragged, with no
   second coordinate to keep in step -- and hovering the pop-out is still hovering the card, so
   "stays while the pointer is on it" needs no rule of its own. */
/* Flipped to the card's left, for a card close enough to the right edge that the pop-out would
   be cut off by `.cvbody`'s `overflow: hidden` -- measured at 52px for the two in the rightmost
   column, which is 52px of an unreachable pop-out. Authored in the markup so it is right with the
   script off, and `canvas.js` re-decides it whenever a card moves. */
.cvpeek--left {
  left: auto;
  right: calc(100% + var(--s8-space-2));
}
.cvcard:hover .cvpeek,
.cvcard:focus-within .cvpeek,
.cvpeek--on {
  opacity: 1;
  pointer-events: auto;
}
.cvcard:hover {
  z-index: 9;
}
/* Says whose details these are. A pop-out floating beside four cards has to name its subject or
   it is a fact about nothing in particular. */
.cvpop__of {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}

.cvside__h {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
}
.cvside__n {
  font-size: var(--s8-text-xl);
  font-weight: var(--s8-weight-medium);
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The panel's own dismiss, now that `.cvbar__r` keeps its normal Canvas/List toggle instead of
   shrinking to a bare Close label -- there is no longer room for one there, and the panel closing
   itself is the more common pattern anyway (a card's menu, a dropdown). Box and hover come from
   the shared icon rule above. */
/* NOT `.u`. That row is a 170px label beside its explanation, which is right on a narrow rail and
   wrong in a wider one -- 170 of 260 left the prose in an 90px column, which is the narrow-text
   fault the gates exist to catch. Stacked instead. */
/* The container itself carried no rule of its own -- `__k`/`__d` supplied their own type and
   `.cvside__b`'s flex `gap` supplied the spacing between fields, so it read fine and still had no
   loaded rule to its own name until the panel actually rendered on the component page. */
.cvfield {
  display: flex;
  flex-direction: column;
}
.cvfield + .cvfield {
  margin-top: var(--s8-space-4);
}
/* A section heading after a field needs its own air; the shared `.sec` has none to spare because
   on a full page it always follows a block that brought its own bottom margin. */
.cvfield + .sec,
.cvside p + .sec,
.cvside .cvpick + .sec {
  margin-top: var(--s8-space-6);
}
/* A variable row, sized for the rail. `.vars`/`.var` is a full-page grid and its value column
   collapses here -- `8000` came out stacked one digit per line. */
.cvvar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s8-space-3);
  padding: var(--s8-space-2) 0;
  border-bottom: 1px solid var(--s8-color-border);
}
.cvvar__k {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  min-width: 0;
  overflow-wrap: anywhere;
}
.cvvar__v {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-left: auto;
  white-space: nowrap;
}
.cvfield__k {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-sm);
  /* Neo: glyphs and text use wayfinding; Pulse remains on filled actions. */
  color: var(--s8-color-wayfind);
}
.cvfield__k--text {
  font-family: var(--s8-font-body);
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
.cvfield__d {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-1);
}
/* NOT stuck to the panel floor, and that was tried. Pinning the row kept the primary action in
   view at 260px -- where `Apply — add one, restart one, grow one` wraps to two lines and falls
   below the fold -- but an opaque bar over a scrolling panel covers whatever is under it, and on
   `88` what it covered was the `ILLUSTRATIVE` mark and its "no rate here is a published price"
   note, at every width including 1440. A `mark__why` is the honesty device this set uses to say
   a number is not real; hiding one by default to keep a button visible is the wrong trade. The
   action scrolls to, like the rest of the panel. */
.cvside__acts {
  display: flex;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: var(--s8-space-4);
}
/* The row already wrapped; what did not was a single label inside it. "Redraw Next against what
   is there" is 33 characters, and `.btn`'s own `white-space: nowrap` ran it 51px past the rail at
   768 -- the same "a label over about 29 characters does not fit" fault `03-gates.md` records
   four times, one tier narrower. The wrap is the modifier's real difference: a button on a
   full-width page has room this one does not. `height` has to give with it, or the second line is
   clipped by the fixed control height instead of by the width, which is the same defect wearing a
   different property. */
.cvside__acts .btn {
  white-space: normal;
  height: auto;
  min-height: var(--s8-control-h);
  padding-block: var(--s8-space-2);
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
}

/* The centred blocks the empty and failed states put on the drawing. */
.cvmid {
  z-index: 3;
  position: absolute;
  left: var(--s8-space-8);
  top: var(--s8-space-8);
  right: var(--s8-space-8);
}

/* The staged banner, floated over the drawing under the bar rather than pushing it down. */
/* THE CANVAS IS THE PAGE. The heading row above the drawing is gone -- the environment's name is
   already in the top bar's own switcher two lines up, and the line explaining what a wire means was
   a caption on a drawing that now says it by being hovered. What is left of that row is the
   summary and the two actions, and they belong ON the drawing rather than in a strip above it:
   a canvas with a header is a page with a canvas in it.

   Floated rather than laid out, for the same reason `.cvctl` and `.cvstaged` are: the drawing is
   full-bleed and anything that reserves a row out of it makes the drawing smaller at every width. */
.cvtop {
  z-index: 5;
  position: absolute;
  top: var(--s8-space-5);
  left: var(--s8-space-6);
  right: var(--s8-space-6);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s8-space-4);
  flex-wrap: wrap;
  pointer-events: none;
}
/* Only the controls take the pointer. The bar spans the drawing, so a transparent strip across it
   would eat every press meant for a card underneath. */
.cvtop > * {
  pointer-events: auto;
}
.cvtop__l,
.cvtop__r {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
}
/* An unconditional rule for the state hook itself -- it previously existed only inside the
   media query below, so the class had no rule a browser matches outside that width, which is
   exactly what `verify.mjs`'s per-viewport CSS check measures at 1440. `inline-flex` restates what
   `.cvsum__x` already renders; nothing changes on screen. */
.cvsum__x--ready {
  display: inline-flex;
}
/* Restore the useful healthy/attention comparison beside the trail. At the
   compact rail width the attention count remains; the healthy count yields
   to the action rather than wrapping over the staged strip. */
@media (max-width: 900px) {
  .cvsum__x--ready {
    display: none;
  }
}
/* The SUMMARY carries its own ground, because a row of small text read straight off a dotted grid
   is a row you have to squint at. The actions do not: a button already is a surface, and a box
   drawn around one is a panel containing a button. */
/* The right half takes the same ground when it carries a SUMMARY rather than a button -- on the
   map both ends are text, and text read straight off a dotted grid is text you squint at. */
.cvtop__l,
.cvtop__r--sum {
  padding: var(--s8-space-2) var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-overlay);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
/* Below the controls, not under them. */
.cvstaged {
  z-index: 5;
  position: absolute;
  top: calc(var(--s8-space-5) + 52px);
  left: var(--s8-space-6);
  right: var(--s8-space-6);
  margin: 0;
}

/* ---- deploying, from the canvas -------------------------------------------
   A picker rather than a page: the kinds of thing you can add down the left,
   and what that kind needs on the right. It exists because "Deploy something"
   used to leave the canvas, and leaving the canvas to add to the canvas is the
   flow this whole screen is arguing against. */
/* The picker needs two columns, and the shared wide modal is sized for one. Widened here rather
   than in `.modal--wide`, which eight other screens rely on at its current width. */
.modal--canvas .modal__box {
  max-width: 780px;
  width: 100%;
}
.cvdep {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  min-height: 380px;
}
.cvdep__l {
  border-right: 1px solid var(--s8-color-border);
  padding: var(--s8-space-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cvdep__o {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: var(--s8-space-2) var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
  font-size: var(--s8-text-base);
}
/* The picker switches now. It was five labels with one `--on` and a single right-hand panel, so
   four of the five sources could be chosen and nothing happened -- the same "looks operable, is
   not" fault the button gate exists for, wearing a `<span>`. Radio-driven like every other choice
   in this set, under its own `dep` group. */
#dep-a:checked ~ .cvdep .cvdep__p--a,
#dep-b:checked ~ .cvdep .cvdep__p--b,
#dep-c:checked ~ .cvdep .cvdep__p--c,
#dep-d:checked ~ .cvdep .cvdep__p--d,
#dep-e:checked ~ .cvdep .cvdep__p--e {
  display: flex;
}
#dep-a:checked ~ .cvdep label[for='dep-a'],
#dep-b:checked ~ .cvdep label[for='dep-b'],
#dep-c:checked ~ .cvdep label[for='dep-c'],
#dep-d:checked ~ .cvdep label[for='dep-d'],
#dep-e:checked ~ .cvdep label[for='dep-e'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  border-color: var(--s8-color-wayfind);
}
/* The account the repositories come from, stated once above the picker rather than left implied.
   A dot the same green a live service uses, because "connected" is the same fact about a
   different thing -- and if it is ever not connected, the row is where that shows. */
.cvconn {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  padding: var(--s8-space-3);
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  font-size: var(--s8-text-sm);
}
.cvconn__n {
  font-weight: var(--s8-weight-medium);
}
.cvconn__x {
  margin-left: auto;
}
/* A dropdown that fills a field, rather than the 210px the menu panel defaults to. The trigger is
   `.selectbox` so it reads as the same control the rest of the set uses to choose one of a list;
   only the panel under it is new here. */
.cvsel {
  display: block;
  position: relative;
}
.cvsel .menu__panel {
  min-width: 100%;
  max-height: 232px;
  overflow-y: auto;
}
.cvsel__d {
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
}
.menu__item .cvsel__d {
  display: block;
}
/* Only when the image is not public. Revealed by the same segmented control the canonical
   `25-deploy-image` uses for it, so the two screens do not disagree about how this works. */
.cvcred {
  display: none;
  flex-direction: column;
  gap: var(--s8-space-4);
  padding-top: var(--s8-space-2);
}
#img-private:checked ~ .cvcred {
  display: flex;
}
#img-public:checked ~ .cvseg label[for='img-public'],
#img-private:checked ~ .cvseg label[for='img-private'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
}
.cvseg {
  display: inline-flex;
  /* A long toggle wraps inside a half-width panel rather than pushing the page sideways. */
  flex-wrap: wrap;
  max-width: 100%;
  padding: 2px;
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  width: fit-content;
}
.cvseg label {
  padding: var(--s8-space-1) var(--s8-space-3);
  border-radius: var(--s8-radius-sm);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
/* The box cannot grow past the window. With a template grid in it the canvas modal measured 864px
   and at a 720px-tall window its own head -- the title and Close -- sat 72px ABOVE the top of the
   screen, on a centred box with nothing to scroll. So the box is capped and the SOURCE PANEL is
   what scrolls, which keeps the head and the source list still while the fields move. Scoped to
   this modal: the other 71 in the set are short and are not this shape. */
.modal--canvas .modal__box {
  max-height: 88vh;
  display: flex;
  flex-direction: column;
}
.modal--canvas .cvdep {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.modal--canvas .cvdep__r {
  min-height: 0;
  overflow-y: auto;
}
.cvdep__p {
  display: none;
  flex-direction: column;
  gap: var(--s8-space-4);
}
/* `.field` carries a 24px bottom margin for a page, where it is the only thing separating one
   field from the next. In here the column already has a gap, so every field was paying twice --
   40px between controls, and a five-field source that needed scrolling to reach its own button.
   The gap is the one that stays, because it is the one this column controls. */
.cvdep__p .field,
.cvside .field {
  margin-bottom: 0;
}
.cvdep__p .field__label,
.cvside .field__label {
  /* A 4px label-to-control gap made the canvas modal fields look attached. */
  margin-bottom: var(--s8-space-2);
}
/* A LIST, not a grid of cards. The cards were the gallery's own component and right there, where
   choosing a template IS the page; in a modal it is one field of five and four cards pushed the
   button that submits the form off the bottom. A row per template shows more of them in less
   height and scrolls for the rest, which is what a picker does. */
.cvtpl {
  display: flex;
  flex-direction: column;
  max-height: 216px;
  overflow-y: auto;
  border: var(--dir-border-w, 1px) solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface);
}
.cvtpl__o {
  display: flex;
  align-items: baseline;
  gap: var(--s8-space-3);
  padding: var(--s8-space-2) var(--s8-space-3);
  border-left: 2px solid transparent;
  cursor: pointer;
}
.cvtpl__o + .cvtpl__o {
  border-top: var(--dir-border-w, 1px) solid var(--s8-color-border);
}
.cvtpl__o:hover {
  background: var(--s8-color-surface-raised);
}
/* The chosen one is marked on its edge rather than by a tick in the row: the row is already a
   flex line of three things and a fourth column of mostly-empty box is how a picker starts to
   look like a table. */
.cvtpl__o--on {
  background: var(--s8-color-surface-raised);
  border-left-color: var(--s8-color-wayfind);
}
.cvtpl__n {
  flex: none;
  font-weight: var(--s8-weight-medium);
}
.cvtpl__d {
  flex: 1;
  min-width: 0;
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cvtpl__m {
  flex: none;
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}
.cvdep__o:hover {
  background: var(--s8-color-surface-raised);
}
.cvdep__o--on {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  font-weight: var(--s8-weight-medium);
}
.cvdep__r {
  padding: var(--s8-space-5) var(--s8-space-6);
}

/* ---- flow C: the same graph in two states ---------------------------------
   A thing being removed is not a thing that failed, so it is not the danger
   colour by default -- it is struck through and dimmed, and the wire that dies
   with it goes dotted. Red is reserved for something that went wrong. */
.cvcard--gone {
  opacity: 0.5;
  border-style: dashed;
}
.cvcard--gone .cvcard__n {
  text-decoration: line-through;
}
.cvw--gone .cvw__line {
  stroke-dasharray: 2 4;
  opacity: 0.6;
}
/* Drawn over the top of the current graph rather than beside it, so the diff is one picture. */
.cvcard--faint {
  opacity: 0.4;
}

/* ---- flow B: the change set, counted ---------------------------------------
   Three numbers before any detail. A list of eleven rows does not answer "how
   big is this", and that is the first question anyone asks of a batch they are
   about to apply to production. */
.cvtiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--s8-color-border);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
  margin-bottom: var(--s8-space-6);
}
.cvtile {
  background: var(--s8-color-surface);
  padding: var(--s8-space-4) var(--s8-space-5);
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-1);
}
.cvtile__n {
  /* The largest numeral in the set, and the only one big enough to carry an accent face.
     A direction may name one; the fallback is `inherit`, so the five that do not are
     unchanged. */
  font-family: var(--dir-font-numeral, inherit);
  font-size: var(--s8-text-3xl);
  font-weight: var(--dir-display-weight, var(--s8-weight-semibold));
  line-height: var(--s8-leading-tight);
  font-variant-numeric: tabular-nums;
}
.cvtile__t {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}
.cvtile--none .cvtile__n {
  color: var(--s8-color-text-muted);
}
/* The before/after pair in a change row. Mono both sides, because what changed is a value. */
.cvwas {
  color: var(--s8-color-text-muted);
  text-decoration: line-through;
}
.cvnow {
  color: var(--s8-color-text);
}

/* ---- the phone answer ------------------------------------------------------
   There is not one. A 232px card in a 334px pane is a drawing you pan, and the
   gates refuse that on purpose -- "nothing overflows" is satisfiable by making
   content vanish. So below the phone tier the canvas is REPLACED by the list it
   is an alternative to, and the screen says so rather than shipping a desktop
   layout with no affordance. */
.cvphone {
  display: none;
}
.cvrow {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) 0;
  border-bottom: 1px solid var(--s8-color-border);
}
.cvrow__b {
  min-width: 0;
  flex: 1;
  /* Name over its state line; as inline spans they ran together (`checkoutDegraded`). */
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cvrow__n {
  font-weight: var(--s8-weight-medium);
}
.cvrow__m {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
}

/* ============================================================ v1, ANV-30 ===
   Additions for the four reference screens — the canvas (87), the panel, the
   add modal (both in 87) and the map (94). Every block below is tagged with
   the decision it carries out. Families in cv*, panel/modal--canvas and
   cvmap are this build's to change; shell/table/sign-in families are not
   touched here. */

/* 01 D2 (amended), D3, D6 — the panel has exactly two widths, half and full;
   the 420px narrow rail v0 drew is gone. A widen/narrow control sits in the
   header, in the same place on every tab, and is a plain radio pair scoped
   inside `.cvside` so `:has()` can size the ancestor from a descendant with
   no JS. Half is the default: cards repack into the space beside it (the
   repacked positions are authored on the card, same mechanism as any other
   layout). Full covers the drawing and nothing repacks. */
.cvside:has(.cvside__pw-full:checked) {
  width: 100%;
}
.cvside__w {
  display: flex;
  align-items: center;
  gap: var(--s8-space-1);
  margin-left: auto;
}
.cvside__w label {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: var(--s8-radius-sm);
  color: var(--s8-color-text-muted);
  cursor: pointer;
  font-size: var(--s8-text-sm);
}
.cvside__w label:hover {
  color: var(--s8-color-text);
  background: var(--s8-color-surface-raised);
}
.cvside:has(.cvside__pw-half:checked) .cvside__w label[for$='pw-half'],
.cvside:has(.cvside__pw-full:checked) .cvside__w label[for$='pw-full'] {
  color: var(--s8-color-text);
  background: var(--s8-color-surface-raised);
}

/* 09 D7, 01 D5, 06 D4, 01-authorities — a sixth tab (Datastores) and a
   seventh (Domains, DEFERRED) beside v0's five. Same radio mechanism,
   extended from `sv-e` to `sv-g`. */
#sv-f:checked ~ .cvside__b .cvpane--f,
#sv-g:checked ~ .cvside__b .cvpane--g {
  display: flex;
}
#sv-f:checked ~ .cvtabs label[for='sv-f'],
#sv-g:checked ~ .cvtabs label[for='sv-g'] {
  color: var(--s8-color-text);
  border-bottom-color: var(--s8-color-wayfind);
}

/* 01 D5 — one panel component, one instance per object; a second,
   independent tab-radio group for a second card's panel on the same page
   (`panel-head-api`, `asv-*`). "No duplicate
   id" (verify.mjs) rules out reusing `sv-*`'s own ids, so this is the same
   mechanism under its own name rather than a second `.cvside` family. The
   widen control needs no equivalent: its CSS matches on the shared
   `.cvside__pw-half`/`.cvside__pw-full` classes and an id ending `pw-half`/
   `pw-full`, so `apw-half`/`apw-full` already work. */
#asv-a:checked ~ .cvside__b .cvpane--a,
#asv-b:checked ~ .cvside__b .cvpane--b,
#asv-c:checked ~ .cvside__b .cvpane--c,
#asv-d:checked ~ .cvside__b .cvpane--d,
#asv-e:checked ~ .cvside__b .cvpane--e,
#asv-f:checked ~ .cvside__b .cvpane--f,
#asv-g:checked ~ .cvside__b .cvpane--g {
  display: flex;
}
#asv-a:checked ~ .cvtabs label[for='asv-a'],
#asv-b:checked ~ .cvtabs label[for='asv-b'],
#asv-c:checked ~ .cvtabs label[for='asv-c'],
#asv-d:checked ~ .cvtabs label[for='asv-d'],
#asv-e:checked ~ .cvtabs label[for='asv-e'],
#asv-f:checked ~ .cvtabs label[for='asv-f'],
#asv-g:checked ~ .cvtabs label[for='asv-g'] {
  color: var(--s8-color-text);
  border-bottom-color: var(--s8-color-wayfind);
}
/* `panel-head-api`'s own tab group, `bsv-*` (its markup, not checkout's). */
#bsv-a:checked ~ .cvside__b .cvpane--a,
#bsv-b:checked ~ .cvside__b .cvpane--b,
#bsv-c:checked ~ .cvside__b .cvpane--c,
#bsv-d:checked ~ .cvside__b .cvpane--d,
#bsv-e:checked ~ .cvside__b .cvpane--e,
#bsv-f:checked ~ .cvside__b .cvpane--f,
#bsv-g:checked ~ .cvside__b .cvpane--g {
  display: flex;
}
#bsv-a:checked ~ .cvtabs label[for='bsv-a'],
#bsv-b:checked ~ .cvtabs label[for='bsv-b'],
#bsv-c:checked ~ .cvtabs label[for='bsv-c'],
#bsv-d:checked ~ .cvtabs label[for='bsv-d'],
#bsv-e:checked ~ .cvtabs label[for='bsv-e'],
#bsv-f:checked ~ .cvtabs label[for='bsv-f'],
#bsv-g:checked ~ .cvtabs label[for='bsv-g'] {
  color: var(--s8-color-text);
  border-bottom-color: var(--s8-color-wayfind);
}

/* Every panel head names its tab radios `<x>sv-<pane>`, so one rule drives them all. */
input[id$='sv-a']:checked ~ .cvside__b .cvpane--a,
input[id$='sv-b']:checked ~ .cvside__b .cvpane--b,
input[id$='sv-c']:checked ~ .cvside__b .cvpane--c,
input[id$='sv-d']:checked ~ .cvside__b .cvpane--d,
input[id$='sv-e']:checked ~ .cvside__b .cvpane--e,
input[id$='sv-f']:checked ~ .cvside__b .cvpane--f,
input[id$='sv-g']:checked ~ .cvside__b .cvpane--g,
input[id$='sv-h']:checked ~ .cvside__b .cvpane--h {
  display: flex;
}
input[id$='sv-a']:checked ~ .cvtabs label[for$='sv-a'],
input[id$='sv-b']:checked ~ .cvtabs label[for$='sv-b'],
input[id$='sv-c']:checked ~ .cvtabs label[for$='sv-c'],
input[id$='sv-d']:checked ~ .cvtabs label[for$='sv-d'],
input[id$='sv-e']:checked ~ .cvtabs label[for$='sv-e'],
input[id$='sv-f']:checked ~ .cvtabs label[for$='sv-f'],
input[id$='sv-g']:checked ~ .cvtabs label[for$='sv-g'],
input[id$='sv-h']:checked ~ .cvtabs label[for$='sv-h'] {
  color: var(--s8-color-text);
  border-bottom-color: var(--s8-color-wayfind);
}
/* 26 D4 — the Kubernetes section is a tab set inside an accordion, so its panes are the radios'
   own siblings rather than a `.cvside__b`'s children. */
input[id$='sv-a']:checked ~ .cvpane--a,
input[id$='sv-b']:checked ~ .cvpane--b,
input[id$='sv-c']:checked ~ .cvpane--c {
  display: flex;
}
.k8rt,
.k8s__c,
.k8rp--wait,
.k8da--rev,
.cvstaged.k8s__strip {
  display: none;
}
#rt-absent:checked ~ .cvpane .k8rt--absent,
#rt-staged:checked ~ .cvpane .k8rt--staged,
#rt-prov:checked ~ .cvpane .k8rt--prov,
#rt-ready:checked ~ .cvpane .k8rt--ready,
#rt-nopools:checked ~ .cvpane .k8rt--nopools,
#rt-upd:checked ~ .cvpane .k8rt--upd,
#rt-del:checked ~ .cvpane .k8rt--del,
#rt-blocked:checked ~ .cvpane .k8rt--blocked,
#rt-drift:checked ~ .cvpane .k8rt--drift {
  display: block;
}
#rt-drift:checked ~ .k8s__c--drift,
#rp-wait:checked ~ .cvpane .fbar.k8rp--wait,
#da-rev:checked ~ .cvpane .k8da--rev {
  display: flex;
}
#rp-wait:checked ~ .cvpane span.k8rp--wait {
  display: inline;
}
#rp-wait:checked ~ .cvpane .k8rp--ok,
#da-rev:checked ~ .cvpane .k8da--ok {
  display: none;
}
/* 26 D8 — setting it up is staged: the strip appears while the section shows it staged. */
.cvbody:has(#rt-staged:checked) .cvstaged.k8s__strip {
  display: flex;
}
/* 26 D6 — drift counts in the trail, beside the environment's name. */
.k8s__driftn {
  display: none;
}
.cvbody:has(#rt-drift:checked) .k8s__driftn {
  display: inline-flex;
}
/* The pool's running count and the access blocks are state hooks; neither box grows. */
.k8rp,
.k8da {
  min-width: 0;
}
/* 82 — Scale ended: the condition shows, growth controls go, the access line says why. */
.k8lapsed {
  display: none;
}
#ks-lapsed:checked ~ .k8s__c--lapsed {
  display: flex;
}
#ks-lapsed:checked ~ .cvpane .k8lapsed {
  display: block;
}
#ks-lapsed:checked ~ .cvpane .k8grow {
  display: none;
}
/* 26 D1-D2 — on 87's Scale states the banner goes (Base only) and the trail carries the mark;
   once Scale has ended both return to Base. */
.k8s__scale {
  display: none;
}
.app:has(#v-24:checked, #v-25:checked):not(:has(#ks-lapsed:checked)) .k8s__scale {
  display: inline-flex;
}
.app:has(#v-24:checked, #v-25:checked):not(:has(#ks-lapsed:checked)) .scalebanner {
  display: none;
}
/* 10 D4 — the progress block's review states, and the model's (41, 15 D4), one at a time. */
.cvsp,
.cvms {
  display: none;
}
#sp-build:checked ~ .cvside__b .cvsp--build,
#sp-sched:checked ~ .cvside__b .cvsp--sched,
#sp-wait:checked ~ .cvside__b .cvsp--wait,
#sp-conf:checked ~ .cvside__b .cvsp--conf,
#sp-verify:checked ~ .cvside__b .cvsp--verify,
#sp-cancel:checked ~ .cvside__b .cvsp--cancel,
#ms-ready:checked ~ .cvside__b .cvms--ready,
#ms-prov:checked ~ .cvside__b .cvms--prov,
#ms-blocked:checked ~ .cvside__b .cvms--blocked {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
}
/* 05.1, VX-4, SC-5 — the Kubernetes and model review rows: a second `.statebar`, shown only
   while their owning canvas state (v-24, v-16) is active. Their own sub-state radios (rt-/rp-/
   da-/ks-/ms-) sit deep inside the panel, not beside this row, so `.app` is the nearest ancestor
   with both -- the same reach `.app:has(#v-24...)` above already needed for `.k8s__scale`. The
   component page has no `v-24`/`v-16`, so its demo caption reuses plain `.statebar` instead
   (always shown); `:is(.app, .page)` lets the same highlight rule reach both.

   `.statebar--notice` follows the same shape, added when the canvas notice's own Live/Did not go
   live/Partly applied switch (`#notice-*`) was found `position: fixed` in `.cvnoticewrap`,
   floating loose over whatever the page happened to have at that fixed coordinate -- the actual
   "canvas notice outside its box" defect on 87 and 00 both, not the `.cvnotice` card itself. Its
   radios stay in `cv-notice.html`, right before the `.cvnotice--*` divs the `~` combinator below
   still needs; only the visible switch moved into the review statebar it always belonged in
   (F-01: reviewer chrome stays in the reviewer's own place, never floating over product chrome).
   87 gates it to v-12/v-13 (the only states that include `cv-notice`); 00's demo has no v-N to
   gate on, so it uses a plain, always-shown `.statebar` beside that section instead, same as the
   Kubernetes/model rows above. */
.statebar--k8s,
.statebar--model,
.statebar--notice,
.statebar--invite {
  display: none;
  margin-top: var(--s8-space-1);
}
.app:has(#v-24:checked) .statebar--k8s,
.app:has(#v-16:checked) .statebar--model,
.app:has(#v-12:checked, #v-13:checked) .statebar--notice {
  display: inline-flex;
}
/* F-01, 05.1 — modal-invite's Sending/Sent review switch, out of the product modal (it sat
   inside `.modal__box`, flush to the border) and into this statebar, shown only while the
   modal itself is open. The radios stay in `modal-invite.html`, siblings of `.invform`/
   `.invsent`; only the visible switch moved. */
:is(.app, .page):has(#invite:target) .statebar--invite {
  display: inline-flex;
}
:is(.app, .page):has(#notice-live:checked) .statebar label[for='notice-live'],
:is(.app, .page):has(#notice-fail:checked) .statebar label[for='notice-fail'],
:is(.app, .page):has(#notice-partial:checked) .statebar label[for='notice-partial'],
:is(.app, .page):has(#ks-on:checked) .statebar label[for='ks-on'],
:is(.app, .page):has(#ks-lapsed:checked) .statebar label[for='ks-lapsed'],
:is(.app, .page):has(#rt-absent:checked) .statebar label[for='rt-absent'],
:is(.app, .page):has(#rt-staged:checked) .statebar label[for='rt-staged'],
:is(.app, .page):has(#rt-prov:checked) .statebar label[for='rt-prov'],
:is(.app, .page):has(#rt-ready:checked) .statebar label[for='rt-ready'],
:is(.app, .page):has(#rt-nopools:checked) .statebar label[for='rt-nopools'],
:is(.app, .page):has(#rt-upd:checked) .statebar label[for='rt-upd'],
:is(.app, .page):has(#rt-del:checked) .statebar label[for='rt-del'],
:is(.app, .page):has(#rt-blocked:checked) .statebar label[for='rt-blocked'],
:is(.app, .page):has(#rt-drift:checked) .statebar label[for='rt-drift'],
:is(.app, .page):has(#rp-ok:checked) .statebar label[for='rp-ok'],
:is(.app, .page):has(#rp-wait:checked) .statebar label[for='rp-wait'],
:is(.app, .page):has(#da-ok:checked) .statebar label[for='da-ok'],
:is(.app, .page):has(#da-rev:checked) .statebar label[for='da-rev'],
:is(.app, .page):has(#ms-ready:checked) .statebar label[for='ms-ready'],
:is(.app, .page):has(#ms-prov:checked) .statebar label[for='ms-prov'],
:is(.app, .page):has(#ms-blocked:checked) .statebar label[for='ms-blocked'],
:is(.app, .page):has(#inv-form:checked) .statebar label[for='inv-form'],
:is(.app, .page):has(#inv-sent:checked) .statebar label[for='inv-sent'] {
  background: var(--s8-color-surface-raised);
  color: var(--s8-color-text);
  box-shadow: inset 0 0 0 1px var(--s8-color-border);
}

/* 09 D7 — the panel names the kind in one line under the header; one <span>,
   as measured (topic 09 decision 7). */
.cvside__kind {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-top: calc(-1 * var(--s8-space-2));
}

/* 27 D1–D4 — the canvas notice. Floated above the drawing like `.cvstaged`
   and `.cvtop`, and stacks above the strip when both show (27 D1). One
   family carries the live form and the failure form; `--fail` only changes
   the rule colour, per the failure-vs-warning pattern `.fbar--warn` already
   set. */
.cvnotice {
  z-index: 6;
  position: absolute;
  /* Below the trail row, like the strip: over it, the notice hid where you are. */
  top: calc(var(--s8-space-5) + 52px);
  left: var(--s8-space-6);
  width: min(760px, calc(100% - 2 * var(--s8-space-6)));
  display: flex;
  gap: var(--s8-space-3);
  align-items: flex-start;
  padding: var(--s8-space-3) var(--s8-space-4);
  border-radius: var(--s8-radius-md);
  border-left: 3px solid var(--s8-color-success, var(--s8-color-accent));
  background: var(--s8-color-surface-raised);
  border-top: 1px solid var(--s8-color-border-strong);
  border-right: 1px solid var(--s8-color-border-strong);
  border-bottom: 1px solid var(--s8-color-border-strong);
  /* Ruled by Hasnat 2026-09-26: this sits on top of the canvas by design (D1) but read as flat
     -- no separation from what is behind it. `--s8-shadow-overlay` already exists for exactly
     this (the review segment control above uses it, base.css :6981); reused, not invented. */
  box-shadow: var(--s8-shadow-overlay);
}
.cvnotice--fail {
  border-left-color: var(--s8-color-danger);
}
/* A partial outcome is not a full failure -- two of three landed -- so it reads as a warning, not
   the same danger stripe as a total miss. After `.cvnotice--fail` on purpose: `.cvnotice--partial`
   also carries `--fail` (below) for its layout/icon rules, and this is the one property warning
   overrides. */
.cvnotice--partial {
  border-left-color: var(--s8-color-warning);
}
/* 27 D2–D4 — three reachable forms, one visible at a time; a review-only
   local radio, the same idiom as the add modal's connection gate. */
/* Invisible to layout and to `.cvnotice`'s own absolute positioning; exists
   only so canvas.js can pin the whole group (see canvas.js's own comment on
   its `pinned` selector) without splitting the radios from what they
   drive. */
.cvnoticewrap {
  display: contents;
}
/* The three notice forms are a review control, and their switch now lives in the review
   statebar (`.statebar--notice`, above) rather than floating `position: fixed` over the canvas --
   that float was the real "outside its box" defect, on 87 as well as 00's demo. Only the radios
   stay here, in front of the `.cvnotice--*` divs the `~` combinator below still needs. */
.cvnotice--live,
.cvnotice--fail,
.cvnotice--partial,
.cvnotice--rolledback {
  display: none;
}
/* 28 D3 — the rolled-back notice, driven by `panel-head-checkout`'s own
   `rollback-*` radio (unique per instance, so this reaches only the
   `.cvside__b` it is inside). */
#rollback-done:checked ~ .cvside__b .cvnotice--rolledback {
  display: flex;
}
#notice-live:checked ~ .cvnotice--live,
#notice-fail:checked ~ .cvnotice--fail:not(.cvnotice--partial),
#notice-partial:checked ~ .cvnotice--partial {
  display: flex;
}
.cvnotice__b {
  flex: 1;
  min-width: 0;
  font-size: var(--s8-text-sm);
  line-height: var(--s8-leading-relaxed);
}
.cvnotice__t {
  font-weight: var(--s8-weight-semibold);
  margin-bottom: var(--s8-space-1);
}
/* The fail/partial variants used to sit flush against the notice's own left padding while the
   live variant led with a 24px tick plus its gap -- about 36px of unexplained extra inset,
   found by the visual review. One state icon each, same box as `.cvnotice__ok`, keeps every
   variant's body at the same inset instead of removing the tick's. */
.cvnotice__ico {
  flex: none;
  width: 24px;
  height: 24px;
  margin-top: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--s8-text-md);
  line-height: 1;
}
.cvnotice__ico--fail {
  color: var(--s8-color-danger);
}
.cvnotice__b .reason__msg {
  margin-top: 0;
  font-size: var(--s8-text-sm);
}
/* "See why" used to sit inline, butted against the sentence before it (partial) or hanging off
   its end (fail). Its own row, at the notice's left edge -- not indented under the icon, `.cvnotice__b`
   already is. */
.cvnotice__act {
  margin-top: var(--s8-space-2);
}
.cvnotice__row {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
  margin-top: var(--s8-space-1);
  color: var(--s8-color-text-muted);
}
.cvnotice__x {
  flex: none;
  width: 28px;
  height: 28px;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.cvnotice__x:hover {
  background: var(--s8-color-surface-sunken);
  color: var(--s8-color-text);
}
.cvnotice__list {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-1);
  margin-top: var(--s8-space-2);
}
.cvnotice__item {
  display: flex;
  align-items: center;
  gap: var(--s8-space-2);
  flex-wrap: wrap;
}
.cvnotice__item > b:first-child {
  min-width: 64px;
}
/* The partly-applied list's name/detail rows (`api` / "Key updated…", `checkout` / "Now 2
   instances.") only approximated a column with a min-width on the name -- real names run longer
   than 64px (`checkout`), so the detail column drifted per row instead of lining up. A real grid,
   scoped to this one variant: the leading failure line and its "See why" stay their own full-width
   rows (`grid-column: 1 / -1`), the two-part rows become the two columns. */
.cvnotice--partial .cvnotice__list {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--s8-space-3);
  row-gap: var(--s8-space-1);
}
.cvnotice--partial .cvnotice__item {
  display: contents;
}
.cvnotice--partial .cvnotice__item > b:only-child,
.cvnotice--partial .cvnotice__act {
  grid-column: 1 / -1;
}
.cvnotice__url {
  color: var(--s8-color-text);
  overflow-wrap: anywhere;
  /* F-08 -- 12px clear of the actions beside it; `.cvnotice__item`'s own gap (8px) reads as
     touching once a mono URL sits next to two buttons, found by the visual review. */
  margin-right: var(--s8-space-1);
}
.cvnotice__actions {
  display: inline-flex;
  gap: var(--s8-space-2);
}
.cvnotice__meta {
  color: var(--s8-color-text-muted);
  padding-bottom: var(--s8-space-2);
  border-bottom: 1px solid var(--s8-color-border);
}

/* 22 D1–D3, 23 D1–D3 — the strip opens in place into rows. `.cvstagerow` is
   `.staged`'s open content: a typed-consequence chip (23), the service, the
   what/old→now, and a per-row ×. `.cvwas`/`.cvnow` already exist (90); this
   is the row shell around them. */
.cvstaged {
  display: flex;
  flex-direction: column;
  /* `.staged`'s `center` shrank the rows to a centred column once the tray opened. */
  align-items: stretch;
  gap: 0;
  /* 22 D1 — the inner bar owns spacing; .staged padding doubled its height. */
  padding: 0;
  /* It floats over the drawing, like `.cvnotice`. */
  box-shadow: var(--s8-shadow-overlay);
}
.cvstaged__bar {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  flex-wrap: wrap;
}
.cvstaged__t {
  flex: 1;
  font-size: var(--s8-text-sm);
}
.cvstagerows {
  display: none;
  flex-direction: column;
  border-top: 1px solid var(--s8-color-border);
}
.cvstaged:has(.cvstaged__open:checked) .cvstagerows {
  display: flex;
}
.cvstaged:has(.cvstaged__open:checked) .cvstaged__peek {
  display: none;
}
.cvstaged__close {
  display: none;
}
.cvstaged:has(.cvstaged__open:checked) .cvstaged__close {
  display: inline-flex;
  margin-left: auto;
}
.cvstagerow {
  display: flex;
  align-items: flex-start;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-5);
  border-bottom: 1px solid var(--s8-color-border);
}
.cvstagerow:last-of-type {
  border-bottom: 0;
}
.cvstagerow__b {
  flex: 1;
  min-width: 0;
  font-size: var(--s8-text-sm);
}
.cvstagerow__label {
  display: block;
  font-size: var(--s8-text-xs);
  /* 23 D1 — consequence label is text, not a Pulse fill. */
  color: var(--s8-color-wayfind);
  text-transform: uppercase;
  letter-spacing: var(--dir-label-tracking, 0.07em);
  margin-top: var(--s8-space-1);
}
.cvstagerow__reason {
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-1);
}
.cvstagerow__x {
  flex: none;
  color: var(--s8-color-text-muted);
  cursor: pointer;
}
.cvstagerow__x:hover {
  color: var(--s8-color-text);
}
.cvstagerow--stale {
  border-left: 3px solid var(--s8-color-warning);
}
.cvstagerow__age {
  display: block;
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  margin-top: var(--s8-space-1);
}
/* 22 D1 — "with the panel open the strip spans the drawing's half." The
   panel is `.cvside`'s own 50%; the strip stops at the same line. */
.cvstaged--half {
  right: 50%;
}

/* 09 D5 — the list's three reachable forms: rows, nothing yet, nothing
   matched. A local review-only radio, same idiom as the connection gate. */
.cvlist {
  display: none;
}
#list-rows:checked ~ .cvlist--rows,
#list-empty:checked ~ .cvlist--empty,
#list-none:checked ~ .cvlist--none {
  display: block;
}
/* 01 D9 — the list at half width beside the panel. */
.cvmid--half {
  right: 50%;
}
/* 22 D2 — the × removes its own row, not the whole strip: a checkbox
   immediately before each row, adjacent-sibling hidden when checked. No
   JS. */
.cvstagerows input[type='checkbox']:checked + .cvstagerow {
  display: none;
}
/* And clears the matching card's staged mark on the drawing — `.cvbody` is
   the ancestor both the strip and the cards share. */
.cvbody:has(#stagerow-search-x:checked) [data-cv='search'] {
  display: none;
}
.cvbody:has(#stagerow-api-x:checked) [data-cv='api'] {
  box-shadow: none;
}
.cvbody:has(#stagerow-api-x:checked) [data-cv='api'] .cvcard__f {
  display: none;
}
.cvbody:has(#stagerow-web-x:checked) [data-cv='web'] {
  box-shadow: none;
}
.cvbody:has(#stagerow-web-x:checked) [data-cv='web'] .cvcard__f {
  display: none;
}
.cvbody:has(#stagerow-checkout-x:checked) [data-cv='checkout'] .cvcard__f {
  display: none;
}

/* 10 D2, D4, D5 — the panel's progress block, five reachable busy states
   with no card colour of their own; a local radio, unique to `v-load`. Card
   goes `--warn` only for Waiting for capacity, once elapsed passes the
   estimate — the one state among the five forge gives a reason code for. */
.cvbusy {
  display: none;
  flex-direction: column;
  gap: var(--s8-space-4);
}
#busy-scheduling:checked ~ .cvbusy--scheduling,
#busy-waiting:checked ~ .cvbusy--waiting,
#busy-configuring:checked ~ .cvbusy--configuring,
#busy-verifying:checked ~ .cvbusy--verifying,
#busy-cancelling:checked ~ .cvbusy--cancelling {
  display: flex;
}
.cvbody:has(#busy-waiting:checked) [data-cv='search'] {
  border-color: var(--s8-color-warning);
}
/* 22 D3 — the row expands in place for the full was/now; a native
   `<details>` reusing the row shell, so the × and the summary share one
   line. */
details.cvstagerow {
  display: block;
  padding: 0;
}
/* Block, and inset past the 22px logo column the sibling rows carry: as a flex row its label,
   age and was/now ran together on one line. */
details.cvstagerow > .cvstagerow__b {
  display: block;
  padding: var(--s8-space-3) var(--s8-space-10) var(--s8-space-3)
    calc(var(--s8-space-5) + 22px + var(--s8-space-3));
  cursor: pointer;
  list-style: none;
}
details.cvstagerow > .cvstagerow__b::-webkit-details-marker {
  display: none;
}
details.cvstagerow .cvstagerow__x {
  position: absolute;
  right: var(--s8-space-5);
  margin-top: var(--s8-space-3);
}
details.cvstagerow {
  position: relative;
}
.cvstagerow__full {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-2);
  padding: 0 var(--s8-space-5) var(--s8-space-4)
    calc(var(--s8-space-5) + 22px + var(--s8-space-3));
}
.cvstagefoot {
  display: flex;
  align-items: flex-start;
  gap: var(--s8-space-3);
  padding: var(--s8-space-4) var(--s8-space-5);
  border-top: 1px solid var(--s8-color-border);
}
.cvstagefoot .field {
  flex: 1;
}
/* Primary last and at the far edge, as `.wzfoot` does; centred on the 38px input, not on the
   input plus the DEFERRED mark under it. */
.cvstagefoot > .btn {
  margin-top: calc((38px - var(--s8-option-h)) / 2);
}
.cvstagefoot > .field + .btn {
  margin-left: auto;
}

/* 09 D3 — no page numbers, no total: a list scrolls and loads more. One line
   under the table, same measure as `.filters`. */
.tblfoot {
  display: flex;
  justify-content: center;
  padding: var(--s8-space-4) 0;
}

/* 02, topic 03's accordion — grouped sections inside a panel tab, and (topic
   09 D6, to-draw #4/#5) a row that expands in place. Native `<details>`, so
   it needs no radio group and no JS. */
.acc {
  border-bottom: 1px solid var(--s8-color-border);
}
.acc:first-child {
  border-top: 1px solid var(--s8-color-border);
}
/* A card already frames the group, so its first and last hairlines would double the card's edge. */
.card__body > .acc:first-child {
  border-top: 0;
}
.card__body > .acc:last-child {
  border-bottom: 0;
}
/* Stacked sections share hairlines; the rail's own 16px flex gap floated each one apart. */
.cvside__b:has(> .acc) {
  gap: 0;
}
.acc__h {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  min-height: 44px;
  padding: var(--s8-space-3) 0;
  cursor: pointer;
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  color: var(--s8-color-text);
  list-style: none;
  user-select: none;
}
.acc__h::-webkit-details-marker {
  display: none;
}
/* Drawn, not a glyph: `▾` is in neither brand face and fell through to a system font. */
.acc__h::after {
  content: '';
  flex: none;
  width: 7px;
  height: 7px;
  margin: -3px var(--s8-space-1) 0 auto;
  border-right: 1.5px solid var(--s8-color-text-muted);
  border-bottom: 1.5px solid var(--s8-color-text-muted);
  transform: rotate(45deg);
  transition:
    transform var(--s8-duration-normal) var(--s8-ease),
    border-color var(--s8-duration-fast) var(--s8-ease);
}
.acc__h:hover::after {
  border-color: var(--s8-color-text);
}
.acc[open] > .acc__h::after {
  margin-top: 3px;
  transform: rotate(-135deg);
}
.acc__h:has(.acc__meta)::after {
  margin-left: 0;
}
/* What a closed section holds, so a column of seven names can be read without opening each. */
.acc__meta {
  margin-left: auto;
  font-size: var(--s8-text-sm);
  font-weight: var(--s8-weight-normal);
  color: var(--s8-color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* Danger is text or border only, never a fill (neo.css). */
.acc--danger > .acc__h {
  color: var(--s8-color-danger);
}
.acc__b {
  padding-bottom: var(--s8-space-5);
}
.acc__b > :first-child {
  margin-top: 0;
}
.acc__b > :last-child {
  margin-bottom: 0;
}

/* 26 D1, 29 — the plans page's two cards, ported from v0's inline `<style>` on `56-plans`
   (v0 drew four; forge settles only two real states, Base and Scale — "the plan is shown, never
   bought"). This sheet already carried `.plans--l1`/`.plans--l2` responsive collapse rules with no
   base to attach to (the four-card layout's own breakpoints, dormant); `.plans--l1` is the column
   count v0 used for two-up, so it is kept as the modifier those rules expect rather than folded
   into `.plans` itself. Only `--l1` is used by a v1 screen today. */
.plans {
  display: grid;
  gap: var(--s8-space-4);
  margin-bottom: var(--s8-space-8);
}
.plans--l1 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* v0's card that spans on purpose: the one you are on above, the quote below. */
.plan--wide {
  grid-column: 1 / -1;
}
.plan {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
  padding: var(--s8-space-6);
  display: flex;
  flex-direction: column;
}
.plan--on {
  /* Pulse is a fill, never a stroke (DESIGN.md:28) -- `--s8-color-wayfind` is the token that
     rule already exists for, not a new one. */
  border-color: var(--s8-color-wayfind);
}
/* Rule 04 #20 — plain inline flow let the badge wrap onto its own line with no gap of its own
   (whatever whitespace the source happened to have); a real row with a real gap, so a wrap
   is a deliberate line break, not crowding. */
.plan__n {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s8-space-2);
  font-size: var(--s8-text-md);
  font-weight: var(--s8-weight-medium);
  margin-bottom: var(--s8-space-1);
}
.plan__p {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-2xl);
  font-weight: var(--s8-weight-semibold);
  line-height: 1.1;
  margin-bottom: var(--s8-space-1);
}
/* A quote or a posture ("Ask about Scale") is not a number, so it does not get the number's
   typeface — mono set on prose reads as a value that failed to load. */
.plan__p--text {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-xl);
  letter-spacing: -0.01em;
}
.plan__s {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-5);
  line-height: var(--s8-leading-relaxed);
}
.plan__l {
  list-style: none;
  margin: 0 0 var(--s8-space-5);
  padding: 0;
  font-size: var(--s8-text-base);
  line-height: var(--s8-leading-relaxed);
}
.plan__l li {
  padding-left: var(--s8-space-5);
  position: relative;
  margin-bottom: var(--s8-space-2);
}
.plan__l li::before {
  content: '·';
  position: absolute;
  left: var(--s8-space-2);
  color: var(--s8-color-text-muted);
}
.plan__a {
  margin-top: auto;
}

/* 09 D6 — a deep link to the model kind of the one add modal (06 D5): `#add-model`, a no-op
   sibling anchor; canvas.js selects the model pane on arrival. The sidebar's Models door is
   `#models`, the same pane opened in place. */
#add-model:target ~ .modal#new-service,
#add-model:target ~ * .modal#new-service {
  display: flex;
}

/* to-draw #1 — the alternative shape (C, a drawer from the bottom), built (Part 2,
   "build both, then look"), beside variant B (`panel-env.html`'s own `.cvside`) for Hasnat to
   decide between after seeing both -- not a replacement. `.cvdrawer` wraps the same
   `panel-env` include variant B already uses, so both are demonstrably the same content in two
   shapes; the wrapper does the real positioning (rises from the bottom, its own elevation), and
   `.cvdrawer .cvside` resets the included panel's own absolute positioning to plain flow inside
   it, so its nested content rules (`.cvside .fr2`, `.cvside .field`, …) still apply unchanged. */
.cvdrawer {
  z-index: 5;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 45%;
  background: var(--s8-color-surface);
  border-top: 1px solid var(--s8-color-border-strong);
  box-shadow: var(--s8-shadow-overlay);
  overflow-y: auto;
}
.cvdrawer .cvside {
  position: static;
  width: auto;
  height: auto;
  border-left: 0;
}
/* The drawer takes the bottom edge, so the zoom cluster rides on its top rather than under it. */
.cvbody:has(> .cvdrawer) > .cvctl {
  bottom: calc(45% + var(--s8-space-4));
}
/* With a panel open the panel carries the detail, so the bar keeps the trail and the add button. */
.cvbody:has(> .cvside) > .cvtop .cvsum__x,
.cvbody:has(> .cvside) > .cvtop .cvtop__l > .cvlevel__sep {
  display: none;
}
/* With the panel open the top bar keeps to the drawing's half, as the strip does (22 D1). */
.cvbody:has(> .cvside) > .cvtop {
  right: calc(var(--cv-split, 50%) + var(--s8-space-6));
}
/* Opened wide, the panel starts below the strip: the staged changes stay reachable over it. */
.cvbody:has(> .cvstaged) > .cvside:has(.cvside__pw-full:checked) {
  padding-top: calc(var(--s8-space-5) + 52px + 54px + var(--s8-space-4));
}

/* 08 D4 — the coverage table, drawn once at the map level. Floated like
   `.cvmapnote` and the other chrome over the drawing (`.cvctl`, `.cvstaged`),
   in the corner `.cvmapnote` does not use, so the two never overlap. */
.cvcoverage {
  z-index: 4;
  position: absolute;
  top: calc(var(--s8-space-5) + 52px);
  right: var(--s8-space-6);
  max-width: 360px;
}
.cvcoverage .tbl td:first-child {
  color: var(--s8-color-text-muted);
  white-space: nowrap;
  padding-right: var(--s8-space-4);
}

/* 06 D2, D4 — the add modal's connection gate and refusal-at-button, each a
   reachable state via a local radio (`conn`), the same idiom the image pane
   already uses for public/private. Not shipped chrome: `.cvseg` here is
   product-idiomatic (it is the segmented control the set already has), but
   in a built product only one of the three is ever true at once — this
   toggle exists so a static set can show all three. */
.cvgate,
.cvconnected,
.cvaddfull {
  display: none;
}
#conn-ok:checked ~ .cvconnected {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
}
#conn-none:checked ~ .cvgate {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
}
#conn-full:checked ~ .cvconnected {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
}
#conn-full:checked ~ .cvconnected .cvaddok {
  display: none;
}
#conn-full:checked ~ .cvconnected .cvaddfull {
  display: flex;
}

/* The refusal shown at the button is a block of text, not a row of actions. */
#conn-full:checked ~ .cvconnected .reason.cvaddfull {
  display: block;
}
/* A review toggle at the head of a modal body keeps its distance from the first field. */
.modal__body > .cvseg {
  margin-bottom: var(--s8-space-4);
}
/* 20 D1 — Get help: the help link is drawn only when forge sent one, and the contact sits apart
   as a label and its value. The with/without-documentation review switch that used to gate
   `.ghelp__link` moved to the component page as a second static example (MO-3); the product
   modals always show the with-documentation state now. */
.ghelp__link {
  margin: var(--s8-space-3) 0 0;
}
.ghelp__who {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-2) var(--s8-space-3);
  align-items: baseline;
  margin-top: var(--s8-space-4);
  padding-top: var(--s8-space-3);
  border-top: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-sm);
}
/* Rule 04 #11 — without `.ghelp__link` above it (the no-documentation state, drawn only as a
   static example on the component page, MO-3), this row is the modal body's only content and
   the rule sits over nothing. A separator only separates.
   A modifier, not `:first-child`: `verify.mjs`'s "matches the component page" check compares a
   real screen's `.ghelp__who` against this exact class combination's signature (padding, border)
   on this page -- every real modal keeps `.ghelp__link` above it, so `:first-child` would never
   fire there, but the SHARED "ghelp__who" key would still carry this page's changed signature
   and read as drift on every real screen. A modifier isolates the one instance that needs it. */
.ghelp__who--lead {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.ghelp__k {
  color: var(--s8-color-text-muted);
  /* A two/three-word label ("Take it to"), not prose -- it wrapped word-by-word in the
     narrower modals ("Take it / to   Support"), reading as a broken sentence rather than a
     label beside its value. */
  white-space: nowrap;
}
/* The support row's value: muted like its key, with its own gap before the DEFERRED chip. */
.ghelp__v {
  display: inline-flex;
  align-items: center;
  gap: var(--s8-space-2);
  color: var(--s8-color-text-muted);
}
/* 09 D6 — the Models door's review states (v0 36-38), one shown at a time. */
.cvmp {
  display: none;
}
#mp-ok:checked ~ .cvmp--ok,
#mp-own:checked ~ .cvmp--own,
#mp-refused:checked ~ .cvmp--refused,
#mp-down:checked ~ .cvmp--down,
#mp-none:checked ~ .cvmp--none {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
}

/* 29 D2 — the Add-a-card modal's two states, adding and declined, the same reachable-local-radio
   idiom as `#conn-*` above (`card`, not `conn` — a modal that already carries `conn` elsewhere on
   the same page must not collide with this one's radio name). */
.cvaddcard,
.cvaddcarddeclined {
  display: none;
}
#card-ok:checked ~ .cvaddcard {
  display: block;
}
#card-declined:checked ~ .cvaddcarddeclined {
  display: block;
}

/* C-05 — the Invite modal's explicit result state, the same reachable-local-radio idiom as
   `#card-*` above (`inv`, not `card`/`conn` — a modal that already carries one of those
   elsewhere on the same page must not collide with this one's radio name). */
.invform,
.invsent {
  display: none;
}
#inv-form:checked ~ .invform {
  display: block;
}
#inv-sent:checked ~ .invsent {
  display: block;
}

/* ============================================================== motion ===
   Part B's motion pieces (.omc/plans/anv-30-foundation-completion.md), all on the existing
   duration and easing tokens. Entrances arrive at *slow*, exits leave at *fast* on the exit curve,
   and the global reduced-motion rule near the top collapses every one of them to an instant. */

/* Modal: the scrim fades first, then the box rises 40ms later. An animation rather than a
   transition, because `:target` flips `display` and an animation replays on every appearance. */
.modal:target .modal__scrim {
  animation: s8-fade var(--s8-duration-normal) var(--s8-ease) both;
}
.modal:target .modal__box {
  animation: s8-rise var(--s8-duration-slow) var(--s8-ease) 40ms both;
}
/* The panel slides from the right and the drawer rises from the bottom, on the same tokens, so
   the panel-versus-drawer comparison stays fair. */
.cvside {
  animation: s8-from-right var(--s8-duration-slow) var(--s8-ease) both;
}
.cvdrawer {
  animation: s8-from-below var(--s8-duration-slow) var(--s8-ease) both;
}
.cvdrawer .cvside {
  animation: none;
}
@keyframes s8-fade {
  from {
    opacity: 0;
  }
}
@keyframes s8-rise {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
}
@keyframes s8-from-right {
  from {
    opacity: 0;
    transform: translateX(24px);
  }
}
@keyframes s8-from-below {
  from {
    transform: translateY(100%);
  }
}

/* Page to page: a cross-fade, with the sidebar held still across it. Where the browser has no
   view transitions the switch is instant, which is today's behavior. */
@view-transition {
  navigation: auto;
}
.sidebar {
  view-transition-name: sidebar;
}
::view-transition-old(root) {
  animation: s8-fade var(--s8-duration-fast) var(--s8-ease-exit) reverse both;
}
::view-transition-new(root) {
  animation: s8-fade var(--s8-duration-normal) var(--s8-ease) both;
}

/* A card being dragged lifts above the drawing; the dot ground behind it moves at half speed
   (canvas.js), which together read as depth. */
.cvcard--lift {
  z-index: 3;
  box-shadow: var(--s8-shadow-overlay);
  transition: box-shadow var(--s8-duration-fast) var(--s8-ease);
}

/* The panel's resizable edge: an 8px hit area drawing a 4px line only while hovered, focused or
   held, so the edge looks like the plain border it is until someone reaches for it. */
.cvside__grip {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -4px;
  width: 8px;
  z-index: 1;
  cursor: col-resize;
  touch-action: none;
}
.cvside__grip::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 2px;
  width: 4px;
  background: transparent;
  transition: background var(--s8-duration-fast) var(--s8-ease);
}
.cvside__grip:hover::after,
.cvside__grip:focus-visible::after,
.cvside__grip--on::after {
  background: var(--s8-color-accent);
}
.cvside__grip:focus-visible {
  outline: none;
}

/* Stepper, signed off 2026-09-27: the current node settles in with a small overshoot and carries
   a soft ring in the accent. */
.stepper__n {
  transition:
    transform var(--s8-duration-slow) var(--s8-ease-spring),
    box-shadow var(--s8-duration-slow) var(--s8-ease);
}
.stepper__i[data-active] .stepper__n,
.stepper--h .stepper__i[data-active] .stepper__n {
  transform: scale(1.12);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--s8-color-accent) 24%, transparent);
}

/* 27 — the moment it works. The notice rises in, the tick draws itself, one halo ring leaves it,
   and a short burst of the brand's colors falls away. All of it plays once per appearance. */
.cvnotice--live {
  animation: s8-rise var(--s8-duration-slow) var(--s8-ease) both;
}
.cvnotice__ok {
  position: relative;
  flex: none;
  width: 24px;
  height: 24px;
  margin-top: 1px;
}
.cvnotice__tick {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: var(--s8-color-success);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.cvnotice__tick circle {
  stroke-dasharray: 63;
  stroke-dashoffset: 63;
  animation: s8-draw 400ms var(--s8-ease) 120ms forwards;
}
.cvnotice__tick path {
  stroke-dasharray: 16;
  stroke-dashoffset: 16;
  animation: s8-draw var(--s8-duration-slow) var(--s8-ease) 480ms forwards;
}
@keyframes s8-draw {
  to {
    stroke-dashoffset: 0;
  }
}
.cvnotice__ok::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--s8-color-success);
  opacity: 0;
  animation: s8-halo 700ms var(--s8-ease-exit) 560ms both;
}
@keyframes s8-halo {
  from {
    opacity: 0.8;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(2.4);
  }
}
/* `.cvnotice__burst` and its `s8-burst` keyframes were removed here (C-19 — no decorative motion
   unless it explains a state change; the burst explained nothing the tick above it did not
   already). `cv-notice.html`'s own comment records the removal. */

/* The command palette. The active option carries the same border the hover does, so the keyboard
   and the pointer land on one look. */
.palette {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
  max-width: var(--s8-measure-field);
}
.palette__list {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-3);
}
.palette__q {
  width: 100%;
}
.palette__g {
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-1);
}
.palette__g[hidden],
.palette__list .opt[hidden] {
  display: none;
}
.palette__gh {
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  color: var(--s8-color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0 var(--s8-space-1);
}
.palette__list .opt {
  padding: var(--s8-space-2) var(--s8-space-3);
  align-items: center;
  color: var(--s8-color-text);
  text-decoration: none;
}
.palette__list .opt__d {
  display: block;
}
.opt--active {
  border-color: var(--s8-color-border-strong);
  background: var(--s8-color-surface-sunken);
}
.palette__none {
  margin: 0;
  padding: var(--s8-space-4);
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
}
.palette__keys {
  display: flex;
  align-items: center;
  gap: var(--s8-space-1);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
}
.palette__keys kbd {
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  padding: 1px 5px;
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface);
}

/* Sign-in: email and password never show together (v0 02-sign-in), so they slide past each other
   in one place instead of stacking. */
.compdemo--auth {
  display: grid;
  max-width: 460px;
  overflow: hidden;
}
.compdemo--auth .lone__card {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  gap: var(--s8-space-4);
}
.lone__card--pass,
#auth-pass:checked ~ .lone__card--email {
  visibility: hidden;
}
#auth-pass:checked ~ .lone__card--pass {
  visibility: visible;
  animation: s8-step-in var(--s8-duration-normal) var(--s8-ease) both;
}
#auth-email:checked ~ .lone__card--email {
  animation: s8-step-back var(--s8-duration-normal) var(--s8-ease) both;
}
@keyframes s8-step-in {
  from {
    opacity: 0;
    transform: translateX(16px);
  }
}
@keyframes s8-step-back {
  from {
    opacity: 0;
    transform: translateX(-16px);
  }
}
.lone__back {
  color: var(--s8-color-wayfind);
  cursor: pointer;
}
.lone__back:hover {
  text-decoration: underline;
}

/* Action feedback: a Copy that has just copied reads as done, in the success color. */
.btn--done {
  color: var(--s8-color-success);
}

/* The toast. Bottom-right of the viewport, above the page and below the review bar. */
.toasts {
  position: fixed;
  right: var(--s8-space-6);
  bottom: calc(var(--s8-preview-reserve) + var(--s8-space-2));
  z-index: 80;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s8-space-2);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  max-width: 360px;
  padding: var(--s8-space-3) var(--s8-space-4);
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border-strong);
  border-left: 3px solid var(--s8-color-success);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
  font-size: var(--s8-text-sm);
  animation: s8-rise var(--s8-duration-slow) var(--s8-ease) both;
}
.toast--out {
  animation: s8-fade var(--s8-duration-fast) var(--s8-ease-exit) reverse both;
}
.toast--still {
  margin-top: var(--s8-space-4);
  width: max-content;
  animation: none;
}
.toast__t {
  color: var(--s8-color-text);
}

/* Onboarding tip. The dim is the target's own spread shadow, so the target stays the one lit
   thing without a cut-out mask. */
.compdemo--coach {
  position: relative;
  overflow: hidden;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s8-space-8);
}
.coach {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s8-space-3);
}
.coach__tip {
  display: none;
  align-items: center;
  gap: var(--s8-space-3);
  padding: var(--s8-space-3) var(--s8-space-4);
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-md);
  box-shadow: var(--s8-shadow-overlay);
  font-size: var(--s8-text-sm);
}
.coach__target {
  position: relative;
  z-index: 2;
  transition: box-shadow var(--s8-duration-slow) var(--s8-ease);
}
.coach__toggle:checked ~ .coach .coach__target {
  box-shadow: 0 0 0 9999px color-mix(in srgb, var(--s8-color-overlay) 70%, transparent);
}
.coach__toggle:checked ~ .coach .coach__tip {
  display: flex;
  position: relative;
  z-index: 3;
  animation: s8-rise var(--s8-duration-slow) var(--s8-ease) 120ms both;
}
/* The checkbox is the thing with focus, so its ring is drawn on the visible label beside it. */
.coach__toggle {
  appearance: none;
}
.coach__toggle:focus-visible + .btn {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}

/* 29.1 — v0 55's usage chart and key, from its inline <style>. v0 set each bar's height inline;
   here the bars are SVG geometry, the same fix `.spark` made, so no style="" is needed. */
.days {
  display: block;
  width: 100%;
  max-width: 480px;
  height: 90px;
  margin-bottom: var(--s8-space-2);
}
.day__b {
  fill: var(--s8-color-accent);
  opacity: 0.85;
}
.day__b--model {
  fill: var(--s8-color-warning);
}
.days__l {
  display: flex;
  justify-content: space-between;
  max-width: 480px;
  font-family: var(--s8-font-mono);
  font-size: var(--s8-text-xs);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-2);
}
.key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s8-space-4);
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  margin-bottom: var(--s8-space-8);
}
.key i {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  margin-right: 6px;
}
.key__a {
  background: var(--s8-color-accent);
}
.key__w {
  background: var(--s8-color-warning);
}
/* 29.4 — v0 55's credit ladder, from its inline <style>. */
.ladder {
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  overflow: hidden;
  margin-top: var(--s8-space-6);
}
.ladder__h {
  padding: var(--s8-space-3) var(--s8-space-4);
  border-bottom: 1px solid var(--s8-color-border);
  background: var(--s8-color-surface-raised);
  font-size: var(--s8-text-sm);
  font-weight: var(--s8-weight-semibold);
}
.ladder .st:last-child {
  border-bottom: 0;
}

/* 16.4 — v0 73's key-shown-once panel, from its inline <style>. */
.reveal {
  border: 1px solid var(--s8-color-warning);
  border-radius: var(--s8-radius-md);
  padding: var(--s8-space-5);
  margin-bottom: var(--s8-space-6);
}
.reveal__h {
  font-weight: var(--s8-weight-semibold);
  margin-bottom: var(--s8-space-2);
}
.reveal__d {
  font-size: var(--s8-text-sm);
  color: var(--s8-color-text-muted);
  line-height: var(--s8-leading-relaxed);
  margin-bottom: var(--s8-space-4);
}

.u__d--ctl .selectbox {
  width: auto;
}

/* 16.2 — an option that is a choice (the access picker): the chosen one carries the same wayfind
   border as the plan you are on, and keyboard focus lands on the option, not its hidden radio. */
/* Inside a <label> the title and description have to be spans; they still stack as they do
   everywhere else, where they are divs. */
.opt__t,
.opt__d {
  display: block;
}
.opt:has(input:checked) {
  border-color: var(--s8-color-wayfind);
}
.opt:has(input:focus-visible) {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}

/* 19.2 — the on/off switch its Notifications rows need. No v0 family is one: `.seg` picks between
   named options and a bare checkbox renders as the browser's. Neutral rather than Pulse, so a page
   of eight switches does not become a page of eight accents; square, like the rest of Neo. */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
  cursor: pointer;
}
.switch__i {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.switch__t {
  position: relative;
  width: 32px;
  height: 18px;
  border: 1px solid var(--s8-color-border-strong);
  border-radius: var(--s8-radius-sm);
  background: var(--s8-color-surface-sunken);
  transition:
    background var(--s8-duration-fast) var(--s8-ease),
    border-color var(--s8-duration-fast) var(--s8-ease);
}
.switch__t::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 2px;
  background: var(--s8-color-text-muted);
  transition:
    transform var(--s8-duration-normal) var(--s8-ease),
    background var(--s8-duration-fast) var(--s8-ease);
}
.switch__i:checked + .switch__t {
  background: var(--s8-color-text);
  border-color: var(--s8-color-text);
}
.switch__i:checked + .switch__t::after {
  transform: translateX(14px);
  background: var(--s8-color-surface);
}
.switch__i:focus-visible + .switch__t {
  outline: 2px solid var(--s8-color-focus-ring);
  outline-offset: 2px;
}
.switch__i:disabled + .switch__t {
  opacity: 0.55;
  cursor: not-allowed;
}
/* A row whose value is controls: the place and the switch sit on one line, at the row's end. */
.u__d--ctl {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s8-space-3);
}

/* 02.1 — a promotion between environments reads a step stronger than a variable wire. */
.cvw--promote .cvw__line {
  stroke: var(--s8-color-text);
}

/* The standalone design launcher keeps v0's descriptive destination rows, without its
   screen numbers or internal project notes. */
.launcher {
  min-height: 100vh;
  background: var(--s8-color-surface-sunken);
}
.launcher__shell {
  width: min(1180px, calc(100% - 48px));
  margin: 0 auto;
  padding: 64px 0 40px;
}
.launcher__hero {
  display: grid;
  grid-template-columns: 180px minmax(0, 720px);
  gap: var(--s8-space-10);
  align-items: start;
  padding: 0 0 56px;
  border-bottom: 1px solid var(--s8-color-border);
}
.launcher__brand {
  display: flex;
  align-items: center;
  gap: var(--s8-space-3);
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-lg);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: -0.02em;
}
.launcher__brand-mark {
  width: 28px;
  height: 28px;
  background: center / contain no-repeat url('logo/kordin_icon_resolved_color.png');
}
.launcher__intro h1 {
  max-width: 680px;
  margin: var(--s8-space-3) 0 var(--s8-space-5);
  font-family: var(--s8-font-display);
  font-size: clamp(40px, 5vw, 68px);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: -0.055em;
  line-height: 0.98;
}
.launcher__eyebrow {
  margin: 0;
  color: var(--s8-color-accent);
  font-size: var(--s8-text-xs);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
.launcher__lede {
  max-width: 650px;
  margin: 0;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-md);
  line-height: 1.7;
}
.launcher__featured,
.launcher__group {
  padding-top: 48px;
}
.launcher__section-head {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: var(--s8-space-8);
  margin-bottom: var(--s8-space-5);
}
.launcher__section-head h2 {
  margin: var(--s8-space-2) 0 0;
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-xl);
  font-weight: var(--s8-weight-semibold);
  letter-spacing: -0.025em;
}
.launcher__section-head > p {
  max-width: 390px;
  margin: 0;
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  line-height: 1.55;
  text-align: right;
}
.launcher__feature-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s8-space-4);
}
.launcher__feature {
  position: relative;
  display: grid;
  grid-template-columns: 136px minmax(0, 1fr) auto;
  gap: var(--s8-space-5);
  align-items: center;
  min-height: 164px;
  padding: var(--s8-space-5);
  overflow: hidden;
  color: var(--s8-color-text);
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-lg);
  box-shadow: var(--s8-shadow-overlay);
  transition:
    border-color var(--s8-duration-fast) var(--s8-ease),
    transform var(--s8-duration-fast) var(--s8-ease);
}
.launcher__feature:hover {
  border-color: var(--s8-color-accent);
  transform: translateY(-2px);
}
.launcher__feature-art {
  position: relative;
  display: block;
  height: 112px;
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-md);
  background: var(--s8-color-surface);
}
.launcher__feature-art span {
  position: absolute;
  display: block;
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: 4px;
}
.launcher__feature-art--canvas span:nth-child(1) { inset: 16px 56px 58px 14px; }
.launcher__feature-art--canvas span:nth-child(2) { inset: 56px 16px 16px 50px; }
.launcher__feature-art--canvas span:nth-child(3) {
  width: 42px;
  height: 1px;
  top: 53px;
  left: 46px;
  background: var(--s8-color-accent);
  border: 0;
  transform: rotate(28deg);
}
.launcher__feature-art--map span {
  width: 28px;
  height: 28px;
  border-radius: 50%;
}
.launcher__feature-art--map span:nth-child(1) { top: 18px; left: 20px; }
.launcher__feature-art--map span:nth-child(2) { top: 54px; right: 18px; }
.launcher__feature-art--map span:nth-child(3) {
  width: 68px;
  height: 1px;
  top: 53px;
  left: 40px;
  background: var(--s8-color-accent);
  border: 0;
  transform: rotate(22deg);
}
.launcher__feature-copy {
  display: grid;
  gap: var(--s8-space-2);
}
.launcher__feature-label {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-lg);
  font-weight: var(--s8-weight-semibold);
  line-height: 1.2;
}
.launcher__feature-desc {
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  line-height: 1.55;
}
.launcher__arrow,
.launcher__item i {
  color: var(--s8-color-text-muted);
  font-style: normal;
  transition: color var(--s8-duration-fast) var(--s8-ease);
}
.launcher__feature:hover .launcher__arrow,
.launcher__item:hover i {
  color: var(--s8-color-accent);
}
.launcher__groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--s8-space-8);
}
.launcher__group--wide {
  grid-column: 1 / -1;
}
.launcher__list {
  overflow: hidden;
  background: var(--s8-color-surface-raised);
  border: 1px solid var(--s8-color-border);
  border-radius: var(--s8-radius-lg);
  box-shadow: var(--s8-shadow-overlay);
}
.launcher__list--two {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.launcher__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s8-space-4);
  min-height: 76px;
  padding: var(--s8-space-4) var(--s8-space-5);
  color: var(--s8-color-text);
  border-bottom: 1px solid var(--s8-color-border);
  transition: background var(--s8-duration-fast) var(--s8-ease);
}
.launcher__list--two .launcher__item:first-child {
  border-right: 1px solid var(--s8-color-border);
}
.launcher__item:last-child,
.launcher__list--two .launcher__item {
  border-bottom: 0;
}
.launcher__item:hover {
  background: var(--s8-color-surface);
}
.launcher__item span {
  display: grid;
  gap: 4px;
}
.launcher__item b {
  font-family: var(--s8-font-display);
  font-size: var(--s8-text-base);
  font-weight: var(--s8-weight-semibold);
}
.launcher__item small {
  color: var(--s8-color-text-muted);
  font-size: var(--s8-text-sm);
  line-height: 1.45;
}
.launcher__footer {
  display: flex;
  justify-content: space-between;
  margin-top: 64px;
  padding-top: var(--s8-space-5);
  color: var(--s8-color-text-muted);
  border-top: 1px solid var(--s8-color-border);
  font-size: var(--s8-text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
@media (max-width: 900px) {
  .launcher__hero {
    grid-template-columns: 1fr;
    gap: var(--s8-space-8);
  }
  .launcher__feature-grid,
  .launcher__groups {
    grid-template-columns: 1fr;
  }
  .launcher__group--wide {
    grid-column: auto;
  }
}
@media (max-width: 640px) {
  .launcher__shell {
    width: min(100% - 32px, 1180px);
    padding-top: 32px;
  }
  .launcher__hero {
    padding-bottom: 40px;
  }
  .launcher__intro h1 {
    font-size: 40px;
  }
  .launcher__section-head {
    align-items: start;
    flex-direction: column;
    gap: var(--s8-space-3);
  }
  .launcher__section-head > p {
    text-align: left;
  }
  .launcher__feature {
    grid-template-columns: 96px minmax(0, 1fr);
  }
  .launcher__feature-art {
    height: 88px;
  }
  .launcher__arrow {
    display: none;
  }
  .launcher__list--two {
    grid-template-columns: 1fr;
  }
  .launcher__list--two .launcher__item:first-child {
    border-right: 0;
    border-bottom: 1px solid var(--s8-color-border);
  }
}
