/* MantiDash design system.
 *
 * The palette is the Manti board: navy #0B1F3B, blue #1E4D7A, cream #F6EBD0, amber
 * #F6A623, orange #F05A28, red #C83B22. Chrome is #071A2C, the navy the board's art is
 * painted on. Typeface Fira Sans.
 *
 * Two rules keep this file from rotting:
 *   1. Style through the tokens. Never a literal color in a component rule, or the
 *      dark theme silently stops tracking.
 *   2. Size shared utility classes at the top, not inside one component's selector.
 *      An <svg> with no intrinsic size renders at the SVG default of 300x150 and
 *      destroys its container. See the .ic / .car / .x block below.
 */

/* ============================================================
   FONTS  (vendored, SIL OFL; never a CDN link)
   ============================================================ */
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/static/fonts/fira-sans-400.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("/static/fonts/fira-sans-500.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("/static/fonts/fira-sans-600.woff2") format("woff2"); }
@font-face { font-family: "Fira Sans"; font-style: normal; font-weight: 700;
  font-display: swap; src: url("/static/fonts/fira-sans-700.woff2") format("woff2"); }

/* ============================================================
   TOKENS
   Light neutrals carry a faint cream bias and dark ones are navy,
   so both sit with the brand rather than reading as generic grey.
   ============================================================ */
:root {
  color-scheme: light;
  --brand: #1E4D7A; --brand-h: #163C61; --brand-tint: #E4EEF7; --on-brand: #FFFFFF;
  --star: #F6A623; --ember: #F05A28;
  --n0: #FFFFFF; --n1: #FBF7EE; --n2: #F3EEE2; --n3: #E6DFCF; --n4: #C9C1B0; --n5: #5E6B7E;
  --ok: #1B7F4E;   --ok-bg: #E8F4ED;
  --warn: #8A6100; --warn-bg: #FBF2DE;
  --bad: #B3261E;  --bad-bg: #FBEDEC;
  --info: #0E6B73; --info-bg: #E6F4F5;
  --bar: #071A2C; --bar-text: #F6EBD0; --bar-dim: #8FA3BD; --bar-line: #1E3B5E;
  /* Brighter than --bar-text, for the one word a chrome surface lifts. Constant across
     themes, as --bar-text is: chrome is dark in both. */
  --bar-bright: #FFFFFF;
  /* The stripe: three lines under the top bar and along the sign-in aside. Red lives
     here and nowhere else; test_red_is_spent_only_on_the_stripe holds it. */
  --stripe-1: #F6A623; --stripe-2: #F05A28; --stripe-3: #C83B22;
  --stripe: linear-gradient(180deg, var(--stripe-1) 0 34%, var(--stripe-2) 34% 67%, var(--stripe-3) 67%);
  --app-bg: #FBF7EE; --surface: #FFFFFF; --line: #E6DFCF; --line-sq: #F0EBDF;
  --text: #0B1F3B; --muted: #5E6B7E; --hover: #F3EEE2; --sel: #E4EEF7;
  --shadow: 0 8px 28px rgba(11,31,59,.14);

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: "Fira Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --brand: #8DB8DC; --brand-h: #A9CBE8; --brand-tint: #1A3552; --on-brand: #0B1F3B;
    --star: #F6A623; --ember: #F28C5E;
    --n0: #122B4A; --n1: #0B1F3B; --n2: #1A3556; --n3: #1E3B5E; --n4: #35507A; --n5: #9FB0C4;
    --ok: #5FC98D;   --ok-bg: #14271D;
    --warn: #D9A93A; --warn-bg: #2A2213;
    --bad: #F0736A;  --bad-bg: #2C1917;
    --info: #5CC8D0; --info-bg: #10292B;
    --bar: #071A2C; --bar-line: #1E3B5E; --bar-dim: #8FA3BD;
    --app-bg: #0B1F3B; --surface: #122B4A; --line: #1E3B5E; --line-sq: #1A3556;
    --text: #EDE8DC; --muted: #9FB0C4; --hover: #1A3556; --sel: #1A3552;
    --shadow: 0 8px 28px rgba(0,0,0,.5);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --brand: #8DB8DC; --brand-h: #A9CBE8; --brand-tint: #1A3552; --on-brand: #0B1F3B;
  --star: #F6A623; --ember: #F28C5E;
  --n0: #122B4A; --n1: #0B1F3B; --n2: #1A3556; --n3: #1E3B5E; --n4: #35507A; --n5: #9FB0C4;
  --ok: #5FC98D;   --ok-bg: #14271D;
  --warn: #D9A93A; --warn-bg: #2A2213;
  --bad: #F0736A;  --bad-bg: #2C1917;
  --info: #5CC8D0; --info-bg: #10292B;
  --bar: #071A2C; --bar-line: #1E3B5E; --bar-dim: #8FA3BD;
  --app-bg: #0B1F3B; --surface: #122B4A; --line: #1E3B5E; --line-sq: #1A3556;
  --text: #EDE8DC; --muted: #9FB0C4; --hover: #1A3556; --sel: #1A3552;
  --shadow: 0 8px 28px rgba(0,0,0,.5);
}
:root[data-theme="light"] {
  color-scheme: light;
  --brand: #1E4D7A; --brand-h: #163C61; --brand-tint: #E4EEF7; --on-brand: #FFFFFF;
  --star: #F6A623; --ember: #F05A28;
  --n0: #FFFFFF; --n1: #FBF7EE; --n2: #F3EEE2; --n3: #E6DFCF; --n4: #C9C1B0; --n5: #5E6B7E;
  --ok: #1B7F4E;   --ok-bg: #E8F4ED;
  --warn: #8A6100; --warn-bg: #FBF2DE;
  --bad: #B3261E;  --bad-bg: #FBEDEC;
  --info: #0E6B73; --info-bg: #E6F4F5;
  --bar: #071A2C; --bar-line: #1E3B5E; --bar-dim: #8FA3BD;
  --app-bg: #FBF7EE; --surface: #FFFFFF; --line: #E6DFCF; --line-sq: #F0EBDF;
  --text: #0B1F3B; --muted: #5E6B7E; --hover: #F3EEE2; --sel: #E4EEF7;
  --shadow: 0 8px 28px rgba(11,31,59,.14);
}

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
  background: var(--app-bg);
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
a { color: inherit; text-decoration: none; }
/* A link that has to look like one, inside running text or a table cell. The bare rule
   above makes every anchor inherit, which is right for the rail and for a row that is
   itself clickable, and wrong for a sentence pointing somewhere else. Four templates
   were already asking for this class and getting plain text. */
.lnk { color: var(--brand); text-decoration: none; }
.lnk:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; border-radius: 2px; }
[x-cloak] { display: none !important; }
.icon-sprite { position: absolute; width: 0; height: 0; }

/* The `hidden` attribute is only a `display: none` in the UA stylesheet, so ANY author
   display rule outranks it. `.chip { display: inline-flex }` did exactly that, leaving
   the "Clear filters" chip on screen permanently with `hidden` faithfully set. Same
   family as x-show on an <option>: the attribute is set, the element is visible. */
[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }

/* Icon sizing floor. Do not remove: an <svg> with no width/height falls back to
   300x150 and tears its container apart. Contexts may override these. */
.ic  { width: 14px; height: 14px; flex: none; }
.car { width: 9px;  height: 9px;  flex: none; }
.x   { width: 10px; height: 10px; flex: none; }

/* An operator's sign-in notice can be a paragraph of legal text. Their line breaks
   are kept, and a long one scrolls rather than pushing the form off the page. */
.lede.notice { white-space: pre-line; max-height: 30vh; overflow-y: auto; }
.mono { font-family: var(--mono); font-size: 11.5px; }
.dim  { color: var(--muted); }
.sub  { color: var(--muted); font-size: 11.5px; }
.hint { font-size: 11.5px; color: var(--muted); margin-top: 4px; }

/* ============================================================
   TOP BAR
   ============================================================ */
.topbar {
  height: 52px; flex: none; position: relative;
  background: var(--bar); color: var(--bar-text);
  display: flex; align-items: center; gap: 4px;
  padding: 0 10px 6px 14px;
}
/* The stripe is the chrome's one signature: the three lines the wordmark sits on. */
.topbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 6px; background: var(--stripe); }
.brand { display: flex; align-items: center; gap: 9px; margin-right: 16px; }
/* The lockup carries the name, so no text sits beside it. */
.brand-mark { display: block; height: 34px; width: auto; flex: none; }

.scope { display: flex; align-items: center; gap: 2px; }
.scope-sep { color: var(--bar-line); font-size: 13px; }
.scope-btn {
  display: flex; align-items: baseline; gap: 6px;
  color: var(--bar-text); font-size: 12.5px; padding: 4px 8px; border-radius: 2px;
}
.scope-btn em {
  font-style: normal; color: var(--bar-dim); font-size: 10.5px;
  text-transform: uppercase; letter-spacing: .07em;
}
.scope-form { display: flex; align-items: baseline; margin: 0; }
.scope-select {
  background: none; border: 0; color: var(--bar-text); font: inherit; font-size: 12.5px;
  padding: 4px 6px; cursor: pointer; border-radius: 2px;
}
.scope-select:hover { background: rgba(255,255,255,.08); }
.scope-select option { color: var(--text); background: var(--surface); }

.top-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.top-user { font-size: 12.5px; color: var(--bar-dim); }
.top-link {
  background: none; border: 0; color: var(--bar-dim); font: inherit; font-size: 12.5px;
  padding: 5px 9px; cursor: pointer; border-radius: 2px;
}
.top-link:hover { background: rgba(255,255,255,.08); color: var(--bar-text); }

/* An icon button in the bar. The bell and the appearance control are the same button with
   a different glyph, so the chrome lives here and each of them adds only what makes it
   itself. `line-height: 0` because the glyph is the whole content and a text line box
   would pad it off centre; `inline-flex` rather than the default because this class lands
   on a <summary> as well as on a <button>, and a `display: list-item` box with no line
   height puts the glyph half a line above where the button puts it. */
.top-icon {
  position: relative; display: inline-flex; align-items: center;
  background: none; border: 0; color: var(--bar-dim);
  padding: 5px 7px; cursor: pointer; border-radius: 2px; line-height: 0;
}
.top-icon:hover { background: rgba(255,255,255,.08); color: var(--bar-text); }

/* What separates what you are looking at from whose account you are looking at it under.
   Without it the right side is one evenly spaced run of four unrelated things and the view
   switch reads as part of the account block. */
.top-rule { width: 1px; height: 18px; flex: none; margin: 0 3px; background: var(--bar-line); }

/* What's new. Rendered only when something is unread, so it is not permanent chrome:
   the bar on an ordinary day is the bar without it. The dot is a child element rather
   than a ::after, so the read state is the absence of the element -- one fewer state a
   template can get wrong -- and it is ringed in the bar colour so it reads as separate
   from the glyph at 16px. */
.top-bell i {
  position: absolute; top: 4px; right: 5px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--bar-bright); border: 1.5px solid var(--bar);
}

/* ============================================================
   THE OPERATIONS VIEW
   One strip under the top bar, said once for the whole view
   rather than a banner per page: the mode is what is being
   announced, and it does not change between pages.
   ============================================================ */
.ops-bar {
  flex: none; display: flex; align-items: center; gap: 9px;
  padding: 6px 18px; font-size: 11.5px; color: var(--warn);
  background: var(--warn-bg); border-bottom: 1px solid var(--line);
}
.ops-bar .ic { width: 13px; height: 13px; flex: none; }
.ops-bar b { font-weight: 600; }
/* The way out sits at the far end of the strip that says you are in. */
.ops-bar form { margin-left: auto; }

/* `.top-link` carries the dark bar's own colours, and this strip is a light ground in
   the light theme, so the control takes the strip's colour rather than the bar's. */
.ops-bar .top-link { color: inherit; font-size: 11.5px; text-decoration: underline; }
.ops-bar .top-link:hover { background: none; color: var(--text); }

/* The switch into the view, in the bar. An icon button has no lit state of its own;
   the two-ended switch beside it does, and this is the same control in icon form. */
.top-icon.on { background: rgba(255,255,255,.13); color: var(--bar-text); }

/* ============================================================
   SHELL
   ============================================================ */
.body-row { flex: 1; display: flex; min-height: 0; }

.rail {
  width: 198px; flex: none; background: var(--surface);
  border-right: 1px solid var(--line);
  /* No padding at the top. The find box is the first child and sticks to the top of the
     scrollport, so any padding above it is a strip the rail scrolls through *over* the
     sticky element -- items appearing above a box that is supposed to be covering them.
     The 10px lives on `.rail-find` instead, where it is inside the opaque box. */
  overflow-y: auto; padding: 0 0 20px;
}
.rail-group {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); padding: 13px 14px 5px;
}
.rail-item {
  display: flex; align-items: center; gap: 8px;
  border-left: 3px solid transparent;
  font-size: 12.5px; color: var(--text);
  padding: 6px 12px 6px 11px;
}
.rail-item:hover { background: var(--hover); }
.rail-item .ic { width: 15px; height: 15px; color: var(--muted); }
.rail-item.on { background: var(--sel); border-left-color: var(--brand); color: var(--brand); font-weight: 600; }
.rail-item.on .ic { color: var(--brand); }

/* The item you are on your way to. A rail click is a full page load, so between the click and
   the first paint of the next page there is nothing of ours on screen to put an indicator in.
   What is on screen is the page being left, and shell.js marks it from the click.

   Colour and nothing else. There was a spinner here too, revealed on a delay for a page that
   took longer than a glance -- and once every list panel fetched its own rows there was no
   such page left in the rail: the panels arrive at once and say the rest with their loading
   bars. It only ever appeared on the ones not yet converted, which made the rail look like it
   behaved differently depending on where you clicked.

   Colour is also what a reduced-motion user gets either way, which is the reason it was the
   primary signal rather than the spinner while both existed. */
.rail-item.pending { background: var(--sel); border-left-color: var(--brand); color: var(--brand); font-weight: 600; }
.rail-item.pending .ic { color: var(--brand); }
/* Two rows looking selected says nothing about which one you are going to, so while the
   rail is going the panel being left gives the selected look back. */
.rail.going .rail-item.on:not(.pending) {
  background: none; border-left-color: transparent; color: var(--text); font-weight: 400;
}
.rail.going .rail-item.on:not(.pending) .ic { color: var(--muted); }

/* A rail group whose heading is a wordmark rather than a label. The image is sized
   here, not left intrinsic: the mark ships at 2x for a retina rail, and an unsized
   <img> would render at 260px and shove the rail open. Same failure mode as an <svg>
   with no width, and the same fix. */
.rail-brand { padding: 6px 12px 8px; }
.rail-brand > img { display: block; width: 108px; height: auto; }

/* PANEL SEARCH -----------------------------------------------------------
   Type to narrow the rail, or Cmd/Ctrl+K for the same index as a palette. See
   static/find.js; the index is the rail itself. */

/* Sticky, so the box stays put while the results under it scroll. The rail is the
   scroll container, so `top: 0` is its own top edge and not the viewport's. Opaque,
   or items would show through as they pass beneath.

   The padding lives here and NOT on the `.search` inside it: `.search .ic` is absolutely
   positioned against `.search`, so any padding there pushes the field away from the icon
   while leaving the icon behind -- off-centre vertically and outside the box
   horizontally. */
.rail-find {
  position: sticky; top: 0; z-index: 1;
  padding: 10px 7px 8px; background: var(--surface);
}
/* The toolbar's field is a fixed 240px at 12.5px, which is a page-level control. In a
   198px rail it has to fill the width and carry less weight than the items below it, or
   the first thing in the nav is the heaviest thing in it.

   The three insets are not free numbers: they put this field's icon and its text on the
   rail's own column, 14px and 37px, so the glass sits directly above every item icon
   rather than three pixels off it. 7 (container) + 7 (icon) = 14; 7 + 30 (text) = 37,
   which is where a rail item's label starts after its 15px icon and 8px gap. */
.rail-find .search input {
  width: 100%; font-size: 12px; padding: 4px 8px 4px 30px;
}
.rail-find .search .ic { left: 7px; }

.rail-none { margin: 10px 14px; font-size: 11.5px; color: var(--muted); line-height: 1.5; }

/* Which alias matched, in the rail. `.rail-item` is a flex row with its own 8px gap, so
   the palette's left margin would be counted twice; and the rail is 198px wide, so a long
   alias has to be allowed to shrink rather than push the row open. */
.rail-item .find-alias { margin-left: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* A panel this deployment has that this region cannot serve. Only ever on screen when
   somebody has searched for it, and never a link -- there is nothing to open. */
.rail-absent { opacity: .55; cursor: default; }
.rail-absent:hover { background: none; }
.rail-why { margin-left: auto; padding-left: 6px; font-size: 10.5px; color: var(--muted); }

/* The palette ------------------------------------------------------------ */
/* Above the drawer rather than under it, unlike the ordinary scrim: a palette opened
   over an open drawer has to cover it. */
.find-scrim { z-index: 49; }
/* Narrower than .modal's 700px, and anchored high rather than centred: a list that grows
   downward from a fixed input is far easier to read than one that re-centres itself on
   every keystroke. */
.modal.find { width: 560px; top: 88px; transform: translateX(-50%); max-height: 74vh; }
.find-head {
  flex: none; display: flex; align-items: center; gap: 9px;
  padding: 11px 14px; border-bottom: 1px solid var(--line);
}
.find-head .ic { width: 14px; height: 14px; color: var(--muted); flex: none; }
/* Deliberately unstyled: the input *is* the head, and a bordered field inside a bordered
   head is a box in a box. */
.find-input {
  flex: 1; min-width: 0; font: inherit; font-size: 13.5px; color: var(--text);
  background: none; border: 0; outline: none; padding: 2px 0;
}
.find-input::placeholder { color: var(--muted); }
.find-body { flex: 1; min-height: 0; overflow-y: auto; padding: 5px 0; }
/* A heading inside the result list, in .rail-group's type so the palette and the rail
   read as the same index rather than two lists that happen to agree. */
.find-sec {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); padding: 9px 14px 4px;
}
.find-sec:first-child { padding-top: 3px; }
.find-foot {
  flex: none; display: flex; align-items: center; gap: 14px;
  padding: 8px 14px; border-top: 1px solid var(--line); background: var(--app-bg);
  font-size: 11px; color: var(--muted);
}
.find-foot-right { margin-left: auto; font-variant-numeric: tabular-nums; }

/* The group a result belongs to. The palette's one advantage over filtering the rail is
   that it can say where a match lives; without this it cannot. */
.find-group { margin-left: auto; padding-left: 8px; font-size: 11px; color: var(--muted); }
/* Which alias matched. Shown because a result whose name does not contain what was typed
   otherwise looks like a bug. */
.find-alias {
  margin-left: 8px; font-family: var(--mono); font-size: 10.5px; color: var(--brand);
  background: var(--sel); padding: 1px 5px; border-radius: 2px;
}
.find-row.find-absent .find-why { margin-left: auto; }

/* A key. There is no such token elsewhere in this stylesheet; the palette needs four. */
.kbd {
  font-family: var(--mono); font-size: 10px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 2px; padding: 0 4px;
  background: var(--surface);
}

/* Accelerator vendor mark: this app's own chip glyph plus the vendor name, in the
   muted ink. Deliberately not a vendor logo -- NVIDIA's and AMD's marks are licensed
   assets and adding their brand colours would put two more literals in a palette that
   is entirely tokens. */
.vendor { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; }
.vendor .ic { width: 12px; height: 12px; color: var(--muted); }
/* The model beside the vendor name. Quieter, because the vendor is what is filtered on
   and the model is the detail that tells two NVIDIA flavors apart. */
.vendor .model { color: var(--muted); }
.vendor.off { opacity: .45; }
.vendor + .vendor { margin-left: 8px; }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
.scroll { flex: 1; overflow: auto; }
.pane { padding: 16px 18px 32px; max-width: 1100px; }

/* A pane whose content is a list. The table runs the width of the page, flush with the
   toolbar, as the ordinary list panels draw it; the cap and the side padding apply only to
   the notices and notes around it. Guard: `test_a_paged_operations_list_uses_the_list_pane`. */
.pane-list { padding: 0 0 32px; max-width: none; }
.pane-list .table-wrap { border-left: 0; border-right: 0; }
.pane-list .note, .pane-list .hint, .pane-list .sec-title, .pane-list .ov-tiles,
.pane-list .caps, .pane-list .stub { margin-left: 18px; margin-right: 18px; }
.pane-list .note-lead, .pane-list .sec-title:first-child { margin-top: 12px; }

.page-head {
  flex: none; padding: 14px 18px 12px; background: var(--surface);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: flex-start; gap: 16px;
}
.page-title { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.page-desc { margin: 2px 0 0; font-size: 12px; color: var(--muted); }
.page-actions { margin-left: auto; display: flex; gap: 7px; align-items: center; }

/* Wraps, because a panel is allowed more filters than fit one line. Without this the
   overflow squashes the search input and pushes the count and refresh off the end, which
   is what six chips on Flavors did at a laptop width. */
.toolbar {
  flex: none; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; row-gap: 7px;
  padding: 8px 18px; background: var(--surface); border-bottom: 1px solid var(--line);
}
.search { position: relative; }
.search .ic {
  position: absolute; left: 8px; top: 50%; transform: translateY(-50%);
  width: 13px; height: 13px; color: var(--muted); pointer-events: none;
}
.search input {
  font: inherit; font-size: 12.5px; color: var(--text);
  background: var(--app-bg); border: 1px solid var(--line);
  padding: 5px 9px 5px 26px; width: 240px; border-radius: 2px;
}
.search input::placeholder { color: var(--muted); }
.tool-right { margin-left: auto; display: flex; align-items: center; gap: 7px; }
.result-count { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; padding: 4px 8px; border: 1px solid var(--line);
  background: var(--surface); color: var(--text); border-radius: 2px; cursor: pointer;
}
.chip:hover { border-color: var(--n4); }
.chip .car { color: var(--muted); }
.chip.applied { border-color: var(--brand); background: var(--sel); color: var(--brand); font-weight: 500; }
.chip.applied .x { color: var(--brand); }

/* ============================================================
   BUTTONS
   ============================================================ */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 12.5px; font-weight: 500;
  padding: 5px 11px; border-radius: 2px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--text);
  white-space: nowrap;
}
.btn:hover { background: var(--hover); border-color: var(--n4); }
.btn .ic { width: 13px; height: 13px; }
.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.btn-primary:hover { background: var(--brand-h); border-color: var(--brand-h); }
.btn-danger { color: var(--bad); }
.btn-danger:hover { background: var(--bad-bg); border-color: var(--bad); }
.btn-icon { padding: 5px 6px; }
.btn-block { width: 100%; justify-content: center; padding: 7px 11px; }
.btn:disabled { opacity: .45; cursor: not-allowed; }

/* ============================================================
   TABLES
   ============================================================ */
.table-wrap { overflow-x: auto; border: 1px solid var(--line); background: var(--surface); }
table.tbl { width: 100%; border-collapse: separate; border-spacing: 0; font-variant-numeric: tabular-nums; }
.tbl thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--n2); color: var(--muted);
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  text-align: left; padding: 7px 10px; white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
.tbl tbody td {
  padding: 0 10px; height: 35px;
  border-bottom: 1px solid var(--line-sq);
  white-space: nowrap; vertical-align: middle;
}

/* A sortable column header. Every column in a list table is one; static/table.js decides
   which, and a header with no name -- the checkbox column, an icon-only column -- is not.

   Everything here hangs off `aria-sort`, which table.js writes and which is therefore
   also the marker's state. One source, so what a screen reader announces and what is on
   screen cannot drift apart, and a header cannot advertise a sort on a page whose script
   has not run. It is a CSS ::after and not an element appended to the header, because
   markup built in JavaScript is what tests/test_separation.py fails on -- and because a
   chevron somebody wants to restyle should be one rule in here.

   THE SORT MARKER IS THIS CHEVRON. It is the app's caret glyph (#i-car) on the sprite's
   stroke weight, drawn through a mask so it takes currentColor and needs no second copy
   per theme. A panel does not draw its own: a table whose sort marker is a different
   shape from the next table's is the drift the design system exists to prevent. */
.tbl thead th[aria-sort] { cursor: pointer; user-select: none; }
.tbl thead th[aria-sort]:hover { color: var(--text); }
.tbl thead th[aria-sort]:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* Invisible at rest, and the space kept, so nothing shifts when it appears. Every column
   sorts, so "this one is sortable" is no longer information worth spending ink on -- a
   marker on all eight headers is eight chevrons of noise saying the same thing. What is
   worth saying is which column the table is sorted BY, and that is what stays lit. The
   pointer cursor and the header going to full contrast carry the affordance meanwhile. */
.tbl thead th[aria-sort]::after {
  content: ""; display: inline-block; width: 8px; height: 10px;
  margin-left: 5px; vertical-align: middle;
  background: currentColor; opacity: 0; transition: opacity .12s ease;
  -webkit-mask: var(--sort-mark) center / contain no-repeat;
  mask: var(--sort-mark) center / contain no-repeat;
  /* Both ways at rest: it says the column sorts, without claiming a direction it has not
     been asked for yet. Two chevrons need room to be two -- at 9x9 with a 1.7 stroke they
     met at the sides and the pair drew a diamond -- so the box is taller than it is wide
     and the rest mark is a shade lighter and narrower than the one that replaces it. */
  --sort-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.6 4.4L5 2l2.4 2.4'/%3E%3Cpath d='M2.6 7.6L5 10l2.4-2.4'/%3E%3C/svg%3E");
}
.tbl thead th[aria-sort="none"]:hover::after { opacity: .5; }
.tbl thead th[aria-sort="none"]:focus-visible::after { opacity: .5; }
/* The one column currently in effect, in the colour this app uses for exactly that, and
   down to one chevron: the question has an answer now, so the mark states it. */
.tbl thead th[aria-sort="ascending"], .tbl thead th[aria-sort="descending"] { color: var(--brand); }
.tbl thead th[aria-sort="ascending"]::after,
.tbl thead th[aria-sort="descending"]::after { opacity: 1; }
.tbl thead th[aria-sort="ascending"]::after {
  --sort-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.2 7.4L5 4.6l2.8 2.8'/%3E%3C/svg%3E");
}
.tbl thead th[aria-sort="descending"]::after {
  --sort-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 12' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.2 4.6L5 7.4l2.8-2.8'/%3E%3C/svg%3E");
}
/* The same table with form controls in it rather than data. A data row is 35px, which an
   input does not sit inside without being clipped, and a cell that can carry an error
   message underneath cannot be a fixed height at all. */
/* Fixed, so the columns are the widths the header asks for and the table is the width it
   was given. A table lays itself out to its contents otherwise, and a bare <select> is as
   wide as its widest option -- so one network with a long name pushed the table, its
   header bar and the rows under it straight out through the side of the card. */
.tbl-form { table-layout: fixed; }
.tbl-form tbody td { height: auto; padding: 5px 10px; white-space: normal; }
.tbl-form input, .tbl-form select { margin: 0; }
/* A control in a form table is not inside a .field, so it inherits none of the sizing a
   field gives one. Same rule, applied where the other half of the app's form controls
   live. */
.tbl-form input[type=text], .tbl-form select {
  font: inherit; font-size: 12.5px; width: 100%; max-width: 100%; padding: 6px 8px;
  border: 1px solid var(--line); background: var(--app-bg); color: var(--text);
  border-radius: 2px;
}
.tbl-form input:focus, .tbl-form select:focus { border-color: var(--brand); }
.tbl-form .err-msg { margin: 4px 0 0; }
/* The message was styled and the control it belongs to was not: `.field.err input` is
   scoped to `.field`, and a row in a form table is not one. Same for disabled. Both are
   the shared-component-styled-inside-one-container drift recorded in the design system;
   a repeater row needs to be able to look wrong and to look untouchable. */
.tbl-form tr.err input, .tbl-form tr.err select { border-color: var(--bad); }
.tbl-form input:disabled, .tbl-form select:disabled {
  background: var(--n1); color: var(--muted); cursor: not-allowed;
}
.tbl tbody tr:hover td { background: var(--hover); }
.tbl tbody tr.picked td { background: var(--sel); }
.col-check { width: 32px; padding-left: 14px !important; }
.col-act { width: 40px; text-align: right; }
/* An action column holding a labelled button rather than an icon. 40px is the room a kebab
   needs; "Join" needs its word. */
.col-join { width: 76px; text-align: right; }
/* A column holding a bar: wide enough that the bar reads, and the same on every row. */
.col-bar { width: 200px; }
.name-cell { font-weight: 500; color: var(--text); }

/* A role beside its glyph. The icon is inline and has no gap of its own, and the pair
   must not wrap apart into a glyph on one line and a word on the next. */
.role-cell { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }

/* A name that goes somewhere: a container to open, a folder to walk into.

   Distinct from the row's own click, which opens a drawer over what you are reading. The
   chevron is the whole of the distinction, deliberately: it stays the colour and weight of
   every other name in every other table, so a list of these does not read as a list of
   links. Red here was two signals for one fact, and on the containers list -- where every
   row has one -- it turned the name column into a wall of colour.

   Underlined on hover of the row rather than of the name itself. The whole row lights up
   under the pointer already, and a name that only answers when the pointer is exactly on
   it reads as the one dead thing in a live row. */
.name-into { display: inline-flex; align-items: center; gap: 4px; }
.name-into .ic { width: 11px; height: 11px; opacity: .75; }
.tbl tbody tr:hover .name-into { text-decoration: underline; text-underline-offset: 2px; }
/* A name cell whose sub-line is a sentence rather than an id.

   Table cells are nowrap everywhere else and must stay that way, so this opts one
   column out: bounded width, the sentence wrapped to at most two lines, and the
   vertical room two lines need that a 35px single-line row does not.

   Two things it has already got wrong. Without the width bound, a 180-character
   summary makes the column 1200px wide and pushes every other column off the page.
   And the selector must carry `.tbl tbody td`, not be a bare `.cell-2line`: the shared
   cell rule above is specificity (0,1,2) and a bare class is (0,1,0), so a plain
   `.cell-2line { padding }` loses to it whatever the order, and the padding silently
   never applies. Same trap as `.field label` outranking `.pick`. */
.tbl tbody td.cell-2line {
  white-space: normal; max-width: 430px; padding-top: 11px; padding-bottom: 11px;
}
.cell-2line .dd-sub {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden; white-space: normal;
  margin-top: 3px; line-height: 1.5;
}
/* A row that opens its detail drawer. The whole row is the target, so the pointer has
   to say so -- except over the checkbox, which selects rather than opens. */
.tbl tbody tr[hx-get] { cursor: pointer; }
.tbl tbody tr[hx-get] .col-check { cursor: default; }
.tbl tbody tr[hx-get]:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* The OS hint. A guess from image metadata, so it is a quiet mark beside the name
   rather than a column that looks authoritative. */
.os-hint { color: var(--muted); vertical-align: -3px; margin-right: 5px; }
input[type=checkbox], input[type=radio] { accent-color: var(--brand); width: 13px; height: 13px; cursor: pointer; margin: 0; }

.kebab { background: none; border: 0; padding: 3px; cursor: pointer; color: var(--muted); border-radius: 2px; line-height: 0; }
.kebab:hover { background: var(--n2); color: var(--text); }

/* A firewall rule's order number. Firewall rules are evaluated in order and security
   group rules are not, so this column is the difference between the two made visible. Dim
   and monospace: it is an index, not data, and must not compete with the action. */
.tbl th.rule-n, .tbl td.rule-n {
  width: 34px; font-family: var(--mono); font-size: 11px;
  color: var(--muted); text-align: right; padding-right: 10px;
}
/* Right-aligned controls in a rule row. */
.tbl td.rule-acts { text-align: right; white-space: nowrap; }

/* The reply-path offer in the add-rule form. A bordered block rather than a bare
   checkbox, because it creates a second rule and the form should look like it is asking a
   second question. */
.mirror-box { border: 1px solid var(--line); background: var(--n2); padding: 11px 12px; }
.mirror-box .hint { margin-top: 6px; }
.mirror-prev {
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line); font-size: 11.5px;
}
.mirror-prev p { margin: 3px 0 0; font-size: 11.5px; color: var(--text); }

/* ============================================================
   STATUS
   ============================================================ */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; letter-spacing: .03em;
  padding: 2px 7px; border-radius: 2px; text-transform: uppercase;
}
/* No dot. A coloured chip already carries the state, and a dot in front of the word said
   the same thing a second time in a shape that means nothing on its own. The left padding
   is symmetric again now that nothing hangs off that edge -- the 6px was there to sit the
   dot closer to the border than the text. The busy spinner stays: it is not decoration,
   it is the one mark here that says something the word does not. */
/* The transition riding alongside a status: lighter, and lowercase because that is how
   the API spells it -- an operator grepping for block_device_mapping should see it. */
.pill .task { font-weight: 500; letter-spacing: 0; text-transform: lowercase; opacity: .75; }

/* A pill that is the control rather than a label beside one. Instances' "Never joined" is
   the cell that raises the question, so it is the cell that opens the answer -- and a
   separate button in that column would be a second thing to look at saying the same word.

   `font: inherit` is not enough on its own: a <button> resets the family and the size, and
   `.pill` above sets neither, so without this the pill renders in the UA's button font. */
.pill-act {
  font: inherit; font-size: 11px; font-weight: 600; letter-spacing: .03em;
  border: 0; cursor: pointer;
}
.pill-act:hover { filter: brightness(.94); }
.pill-act:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.pill .task::before { content: "\00b7"; margin-right: 4px; }
.pill.ok   { background: var(--ok-bg);   color: var(--ok); }
.pill.warn { background: var(--warn-bg); color: var(--warn); }
.pill.bad  { background: var(--bad-bg);  color: var(--bad); }
.pill.info { background: var(--info-bg); color: var(--info); }
.pill.idle { background: var(--n2);      color: var(--muted); }
/* The networks one interface also reaches by tag. Indented under the interface that
   carries them, because that relationship is the entire point: they are not interfaces
   of their own and listing them as siblings would say they were. */
.lanes { margin-top: 6px; border-left: 2px solid var(--brand-tint); padding-left: 9px; }
.lane { display: flex; align-items: center; gap: 6px; font-size: 11.5px; padding: 1px 0; }
.lane .arr { color: var(--n4); }
.lane-tag { margin-left: auto; color: var(--muted); font-size: 11px; }
.tag-trunk { background: var(--brand-tint); color: var(--brand); }

/* Adding a VLAN to a trunk that already exists -- the one change to a trunk that does not
   need the instance rebuilt, so it sits in the drawer rather than behind a button. */
.vlan-add {
  margin-top: 18px; padding: 12px 13px 13px;
  border: 1px solid var(--line); border-left: 3px solid var(--brand); background: var(--app-bg);
}
.vlan-add-head { margin: 0 0 11px; font-size: 13px; font-weight: 600; }
.vlan-add-row { display: grid; grid-template-columns: 92px 1fr; gap: 11px; }
.vlan-add-row .field { margin-bottom: 0; }
.vlan-add-foot { display: flex; justify-content: flex-end; margin-top: 11px; }

/* ============================================================
   NIC — one interface, in a list where the order is the point
   ============================================================
   An access interface is the row and nothing else, so the ordinary launch is one line
   per network. Switching it to a trunk grows a body on the same row rather than opening
   anything: the two are one object in two states, not two controls.

   The red left border is the app's existing "this one is active" marker, used here to
   bind a trunk's several networks together as a single interface — which is the whole
   idea being expressed. */
.nic { border: 1px solid var(--line); background: var(--surface); margin-bottom: 6px; }
.nic-trunk { border-left: 3px solid var(--brand); }
.nic-head { display: flex; align-items: center; gap: 8px; padding: 7px 8px; }
/* A floor rather than a width: 42px lines the short names up, and a longer one -- "outside"
   -- overran a fixed box and sat against the text beside it instead of pushing it along. */
.nic-name { flex: none; min-width: 42px; font-size: 12px; }
/* Wide enough for "Existing trunk". It was 92px, sized for Access and Trunk, and a
   truncated mode is the one control on the row that has to be readable. */
.nic-mode { flex: none; width: 124px; margin: 0; }
/* What an interface is pointed at, where it is something that already exists rather than
   something to be built: the name, and under it the facts that make it recognisable. Takes
   the place of the network select, because the port has a network and it is not a choice. */
.nic-chosen { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.nic-chosen b { font-size: 12.5px; font-weight: 600; }
.nic-chosen .sub { font-size: 11px; color: var(--muted); }
/* The muted twin of .nic-trunk's red bar: this interface is bound to something that
   outlives the launch, which is worth marking and is not worth shouting about. */
.nic-held { border-left: 3px solid var(--n4); }
.nic-net { flex: 1; min-width: 0; margin: 0; }
.nic-body { padding: 2px 12px 12px; }
.nic-body .field { margin-bottom: 12px; }
.nic-body .hint { max-width: 68ch; }
.mk-nic-head {
  display: flex; align-items: center; justify-content: space-between; margin: 0 0 4px;
}
.mk-nic-head label { font-size: 12px; font-weight: 500; margin: 0; }

/* ============================================================
   RUN — a declared sequence of work and how far through it got
   ============================================================
   For an action that is several calls in a forced order: a launch, a trunk build. The
   bar answers "did it finish" from across the room; the list answers "what happened"
   and, when it did not finish, where it stopped.

   A step that never ran is drawn in the same quiet neutral as one deliberately skipped,
   because neither is a fault -- only the step that actually failed is coloured. */
.md-narrow { width: 460px; }
.run-bar {
  height: 4px; border-radius: 2px; background: var(--n2); overflow: hidden;
  margin-bottom: 14px;
}
.run-bar > span { display: block; height: 100%; background: var(--n4); transition: width .2s ease; }
.run-bar.ok > span { background: var(--ok); }
.run-bar.bad > span { background: var(--bad); }

.run { list-style: none; margin: 0; padding: 0; }
.run-step { display: flex; gap: 9px; align-items: flex-start; padding: 5px 0; }
.run-mark { flex: none; width: 16px; height: 16px; display: flex; align-items: center;
            justify-content: center; color: var(--n4); }
.run-mark .ic { width: 13px; height: 13px; }
/* The dot for a step that has not run: a step with no icon yet still needs to occupy its
   column, or the list jogs sideways as each one resolves. Unrelated to a status chip,
   which carries no dot. */
.run-mark i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.run-ok .run-mark { color: var(--ok); }
.run-bad .run-mark { color: var(--bad); }
.run-text { font-size: 12.5px; min-width: 0; }
.run-text .sub { display: block; font-size: 11.5px; color: var(--muted); }
.run-bad .run-text { color: var(--bad); }
.run-idle .run-text { color: var(--muted); }

.spin { animation: sp 1.1s linear infinite; transform-origin: 50% 50%; }
@keyframes sp { to { transform: rotate(360deg); } }

.alert { padding: 9px 12px; font-size: 12.5px; border-left: 3px solid var(--brand); background: var(--sel); margin-bottom: 16px; }
.alert-bad { border-left-color: var(--bad); background: var(--bad-bg); color: var(--text); }
/* Disruptive but reversible -- stopping or rebooting -- as distinct from destructive. */
.alert-warn { border-left-color: var(--warn); background: var(--warn-bg); color: var(--text); }
.alert .hint { display: block; margin-top: 4px; }
/* A credential shown once and never again. Set large and in mono, because it is read
   character by character by anyone who cannot paste -- into a console that is often a
   picture of a terminal rather than a terminal. Selectable as well as copyable, for the
   same reason. */
.pw-value { margin: 6px 0 2px; font-size: 15px; letter-spacing: .02em; }
.pw-value .mono { user-select: all; }

.tag {
  display: inline-block; font-size: 11px; padding: 1px 6px;
  background: var(--n2); color: var(--text); border-radius: 2px;
  margin: 0 4px 4px 0; font-family: var(--mono);
}
/* "This one is yours." Prose rather than mono, because it is a sentence about the
   reader and not a value copied from an API. */
.tag-you {
  background: var(--sel); color: var(--brand);
  font-family: var(--sans); font-weight: 600;
}
/* A flag that narrows what can be asked of the thing, as distinct from one that merely
   describes it. */
.tag-warn { background: var(--warn-bg); color: var(--warn); }

/* ============================================================
   DEFINITION LISTS / DETAIL
   ============================================================ */
/* Flex so `.sec-spacer` can do its job. It sets margin-left:auto, which does nothing to an
   inline element -- so every heading with trailing controls was sitting them against its
   own text: Diagram / List / Attach jammed up beside the word "Network". */
.sec-title {
  display: flex; align-items: center; gap: 8px;
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 20px 0 7px; padding-bottom: 5px; border-bottom: 1px solid var(--line);
}
/* Unscoped: a drawer's sections are not inside a `.pane`, and they need this too. */
.sec-title:first-child { margin-top: 0; }
/* A step's own sentence, under its heading. The negative top margin closes the gap the
   heading's rule already leaves, which is why this was two identical inline styles on two
   launch forms before the section loop became one macro. */
.sec-desc { margin: -6px 0 10px; }
/* A fixed track stays fixed, so a term wider than it does not push it out -- it simply
   overflows and paints across whatever is in the next column. Custom property names are
   exactly that case: `owner_specified.openstack.sha256` has no space to wrap at, and
   underscores and dots are not break opportunities, so it ran straight over its own value.
   `overflow-wrap: anywhere` gives it somewhere to break; the column gap keeps a wrapped
   term off the value even so; `minmax` lets the column give way in a narrow drawer instead
   of overflowing again. */
.dl {
  display: grid; grid-template-columns: minmax(0, 138px) 1fr;
  column-gap: 12px; row-gap: 0; margin: 0 0 4px;
}
.dl dt {
  color: var(--muted); font-size: 12px; padding: 5px 0;
  overflow-wrap: anywhere; min-width: 0;
}
.dl dd { margin: 0; padding: 5px 0; font-size: 12.5px; word-break: break-word; min-width: 0; }
/* Custom properties, where the terms are namespaced and none of them fit 138px:
   `owner_specified.openstack.sha256` is half as wide again. Sizing the column to the
   longest term in this particular list keeps every name on one line, which the fixed
   column cannot do for any of them.

   Capped at 55%, because the names here are the user's and nothing bounds them -- past the
   cap the term wraps (see `overflow-wrap` above) rather than squeezing the values it is
   there to label. Only this list: elsewhere the fixed column is what keeps one drawer's
   sections aligned with each other, and the terms are ours and known to fit.

   `fit-content()` and not `minmax(0, min(max-content, 55%))`: intrinsic keywords are not
   valid inside `min()`, so that spelling is dropped whole and the column silently stays
   138px. This is the primitive that means what it says -- size to the content, up to a
   cap. */
.dl-props { grid-template-columns: fit-content(55%) 1fr; }
/* A secondary line under a definition value, aligned to it rather than trailing the
   first line. */
.dd-sub { display: block; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
.copy { display: inline-flex; align-items: center; gap: 5px; }
.copy button { background: none; border: 0; padding: 1px; cursor: pointer; color: var(--n4); line-height: 0; }
.copy button:hover { color: var(--brand); }
.copy .ic { width: 12px; height: 12px; }

/* ============================================================
   FORMS
   ============================================================ */
.field { margin-bottom: 14px; }
/* Direct child, NOT a descendant. `.field label` is specificity (0,1,1) and beats
   `.pick` at (0,1,0), so it silently forced `display: block` onto every picker row
   nested inside a field -- killing the grid and dropping the radio onto its own line.
   A field's caption is its own child; labels further down belong to components. */
.field > label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; }
.field input[type=text], .field input[type=password], .field input[type=date],
.field input[type=time], .field select, .field textarea {
  font: inherit; font-size: 12.5px; width: 100%; padding: 6px 8px;
  border: 1px solid var(--line); background: var(--app-bg); color: var(--text); border-radius: 2px;
}
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--brand); }

/* A number and the unit it is in, entered as one control.

   Every rate on the QoS panel is stored in kilobits or kilopackets per second while people
   think in megabits and gigabits. A bare input labelled kbps makes the reader do the
   conversion, and two controls side by side read as two questions. One bordered pair, the
   select flush against the input, reads as a single answer.

   The select is not sized as a field: `width: 100%` above would make it fill the row and
   push the number out. */
.qty { display: flex; max-width: 260px; }
.qty input[type=text] { flex: 1; min-width: 0; border-radius: 2px 0 0 2px; }
.field .qty select {
  flex: none; width: auto; border-radius: 0 2px 2px 0; margin-left: -1px;
  background: var(--n1); color: var(--muted);
}
/* Focus has to sit above its neighbour, or the red border is drawn under the -1px overlap
   on one side and the control looks half focused. */
.qty input:focus, .qty select:focus { position: relative; z-index: 1; }

/* Nothing styled a disabled form control at all, so one rendered at full strength and read
   as an ordinary control that simply refused to open. No opacity here on purpose: the
   field it sits in carries `.field-off` for that, and two opacities multiply into
   something too faint to read. */
.field input:disabled, .field select:disabled, .field textarea:disabled {
  cursor: not-allowed;
  background: var(--n1);
  color: var(--muted);
}
/* A value the form is stating rather than asking for -- an address that came from the
   deployment's list of authorities, say. Inset like a disabled control so it does not read as
   a box somebody forgot to fill in, but the text stays at full strength: it is a value being
   shown, not one that has been ruled out, and it is often the thing on screen worth reading
   most carefully. The caret is left alone -- selecting and copying it is the point. */
.field input:read-only {
  background: var(--n1);
  border-style: dashed;
}

/* A repeated block of fields that belongs together and can be added to -- one group of
   machines on the fleet form, where a page carries several and each is a small form of its
   own. A `fieldset` rather than a div because that is what it is, and the legend is the
   only place a browser will put a label *on* a border.

   The default fieldset border is drawn in a colour no theme here uses and a width nothing
   else has, so all three are restated. Everything inside it is already `.field`, which is
   why this rule is six lines rather than a component. */
fieldset.fleet-group {
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--surface);
  padding: 12px 14px 2px;
  margin: 0 0 12px;
}
/* The line above the mesh node list: coverage per region, and how many machines never
   joined. A row in the table rather than a band above it, because it is a summary OF these
   rows and a band would scroll away from them. Quieter than a row of data, since it is not
   one -- nothing here is a node. */
.tbl tbody tr.mesh-summary td {
  background: var(--n1);
  padding: 8px 12px;
  height: auto;
  white-space: normal;
}
.tbl tbody tr.mesh-summary:hover td { background: var(--n1); }

/* One group's count, on the fleet drawer. A row rather than a stacked field, because it
   is a number beside the thing it counts and there are as many of these as there are
   groups. */
.fleet-scale {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line-sq);
  font-size: 12.5px;
}
.fleet-scale:last-of-type { border-bottom: 0; }
.fleet-scale > span:first-of-type { flex: 1; min-width: 0; }
.fleet-scale input[type="number"] {
  font: inherit;
  width: 4.5rem;
  padding: 4px 6px;
  border: 1px solid var(--line);
  background: var(--app-bg);
  color: var(--text);
  border-radius: 2px;
}

/* One connection on the fleet form: three fields that read as one sentence, so they sit
   on a line rather than stacking the way a form's fields otherwise do. Collapses on a
   narrow screen because a sentence that has to be scrolled sideways is not one. */
.fleet-conn {
  display: grid;
  grid-template-columns: 1fr 1fr 7rem;
  gap: 10px;
  align-items: start;
}
@media (max-width: 640px) { .fleet-conn { grid-template-columns: 1fr; } }
.fleet-conn .field { margin-bottom: 10px; }

/* Adding a front door, inside the fleet drawer. Set apart from the sections above it
   because it is a form in the middle of a page of facts, and a run of fields with no
   boundary reads as more of the same rather than as something to fill in. */
.fleet-door {
  padding: 12px;
  margin-top: 10px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--surface);
}
.fleet-door .field:last-of-type { margin-bottom: 0; }

/* One of the two ways to connect a mesh controller. Two forms on one page rather than one
   with fields a script hides, so each keeps its own required set -- which means they need a
   boundary, or they read as one long form whose submit button appears twice. */
.mesh-connect {
  max-width: 560px;
  padding: 14px 16px 16px;
  margin-bottom: 14px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--surface);
}
.mesh-connect .dr-sub { margin-top: 0; }

/* Controllers this project already built, offered instead of a form.

   Deliberately a row and not a `.picker`: the picker's frame is search and paging, which
   exist because an image list can be thousands long. This list is the controllers one
   project has built, so it is one or it is two, and a search box over two rows is furniture.
   What it borrows is the shape of `.opt-row`, so a thing you choose looks the same here as
   everywhere else you choose one. */
.mesh-found { max-width: 560px; margin-bottom: 18px; }
.mesh-found-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--surface);
}
.mesh-found-row + .mesh-found-row { margin-top: 6px; }
/* Dimmed, not hidden. A controller still building is the answer to "where has mine gone",
   and a list that omitted it would say nothing was found. */
.mesh-found-row.off { opacity: .55; }
.mesh-found-main { flex: 1; min-width: 0; }
.mesh-found-main > b { display: block; font-size: 12.5px; }
.mesh-found-main .sub { display: block; font-size: 11px; color: var(--muted); }
.mesh-found-row form { margin: 0; }

/* Coverage, above the table rather than inside it.

   It was a `<tr>` spanning every column, which put the one summary the panel exists to
   produce inside the thing it summarises -- so it scrolled away, and a table sorted by any
   column had a header row sitting in the middle of it. */
.mesh-health {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
}
/* Inside an accordion section, where both values were written inline on every section that
   had one -- so a new section got them by being copied from an old one. */
.gsec-body > .mesh-health { padding: 0 16px; }
.gsec-body .table-wrap { border: 0; }
.mesh-cover {
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 2px;
  font-size: 11px;
  color: var(--muted);
  background: var(--surface);
}
.mesh-cover > b { color: var(--text); font-weight: 600; }
/* A region where some machines are not on the mesh. Marked and not coloured red: partial
   coverage is the ordinary state of a mesh being built, not a fault. */
.mesh-cover.warn { border-color: var(--warn, var(--line)); }

/* The A record being wrong, on a controller's row. An `.alert-bad` under a stop mark, not
   the grey 11px hint this was: the hint sat under the row it condemned and read as trivia,
   and connecting past it builds a mesh whose agents dial something else entirely. */
.mesh-dns {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 6px 0 0;
  line-height: 1.45;
}
.mesh-dns .ic { width: 18px; height: 18px; flex: none; color: var(--bad); }

/* The mesh a fleet will join, where a text field used to invite a name that did nothing. */
.fleet-mesh { margin: 0 0 2px; display: flex; align-items: baseline; gap: 8px; }
.fleet-mesh .mono { font-size: 11px; color: var(--muted); }

/* Which mesh this is, beside the button that forgets it. */
.mesh-which { display: inline-flex; align-items: baseline; gap: 8px; margin-right: 10px; }
.mesh-which .sub { font-size: 11px; color: var(--muted); }

/* A heading below `.dr-sec`: the name of one card inside a section, not a section of its
   own. Quieter than `.dr-sec` on purpose -- two headings at the same weight would make a
   drawer read as a flat list of equals, which is the thing a card is drawn to avoid. */
.dr-sub {
  margin: 0 0 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--muted);
}

/* What a form is about to do, listed before it does it. Marked rather than bulleted: each
   line is a thing that will happen, and a disc in the margin reads as prose while this
   has to read as a manifest. */
.dr-will { margin: 0; padding: 0; list-style: none; font-size: 12px; color: var(--muted); }
.dr-will > li { position: relative; padding: 3px 0 3px 16px; }
.dr-will > li::before {
  content: ""; position: absolute; left: 4px; top: 10px;
  width: 4px; height: 4px; border-radius: 50%; background: var(--n4);
}

fieldset.fleet-group > legend {
  padding: 0 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--muted);
}
/* A field another answer has made impossible. Dimmed whole -- label, control and reason
   together -- so it reads as "not applicable" rather than as one greyed-out box whose
   label still looks like a question. */
.field-off { opacity: .5; }

/* The answer, where the control would have been. For a field another choice has already
   settled: a router on a flavor with no external interface takes no gateway, so a menu
   for one is an invitation to a mistake rather than a choice.
   Distinct from `.field-off`, which dims a control that is still there. This replaces it,
   and states why -- a disabled menu says "not now" where this says "not ever, and here
   is the reason". The left marker is `--n4` rather than `--brand`: it is settled, not
   wrong, and the emphasis marker belongs to things that need attention. */
.stated {
  display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
  border: 1px solid var(--line); border-left: 3px solid var(--n4);
  background: var(--n1); padding: 7px 10px; border-radius: 2px; font-size: 12.5px;
}
.stated .why { color: var(--muted); font-size: 11.5px; }

.seg { display: flex; border: 1px solid var(--line); border-radius: 2px; margin-bottom: 16px; overflow: hidden; }
.seg button { flex: 1; background: var(--surface); border: 0; font: inherit; font-size: 12px; padding: 6px 4px; cursor: pointer; color: var(--muted); }
.seg button + button { border-left: 1px solid var(--line); }
.seg button.on { background: var(--sel); color: var(--brand); font-weight: 600; }

.pick-list { border: 1px solid var(--line); max-height: 320px; overflow-y: auto; }

/* Three columns: the radio, the text, the region markers.
   The radio lives alone in its own column and is centred against the whole text
   block, so it reads as belonging to the row rather than to the first line of it.
   The text column is minmax(0, 1fr) so a uuid wraps inside its own column instead
   of pushing the markers off the row, and every line in it starts at the same
   left edge. */
.pick {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 12px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line-sq);
  cursor: pointer;
  text-align: left;
}
.pick:last-child { border-bottom: 0; }
.pick:hover { background: var(--hover); }
.pick.on, .pick:has(> input:checked) { background: var(--sel); box-shadow: inset 3px 0 0 var(--brand); }
.pick > input[type=radio] { align-self: center; margin: 0; }
/* The same row with a checkbox, for choosing a set rather than one of a list -- the
   security groups on a port. Without this the box lands with the browser's own margins
   and sits a pixel high against the text block beside it, because the radio was the only
   control the row had a rule for. */
.pick > input[type=checkbox] { align-self: center; margin: 0; }
/* A row present but not selectable -- an image the source forbids exporting (OSPC: owned
   by another project), or a volume attached to nothing. The control is disabled and the
   row dims; the reason shows in the right column. `.opt-row` (the server-side picker) had
   these and `.pick` did not, so a disabled row never actually dimmed. */
.pick.off { opacity: .55; cursor: not-allowed; }
.pick.off:hover { background: var(--surface); }
.pick .why { font-size: 11px; color: var(--warn); }

.pick-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  text-align: left;
}
.pick-name {
  font-size: 12.5px; font-weight: 600; color: var(--text);
  overflow-wrap: anywhere;
}
.pick-desc { font-size: 12px; color: var(--text); overflow-wrap: anywhere; }
/* The small muted third line on a picker row. Mostly prose -- a tunnel's summary, a
   flavor's qualifier, "3 inbound, 2 outbound" -- which is why it may break anywhere: those
   are sentences and a long one has to fit. */
.pick-id { font-size: 11px; color: var(--muted); overflow-wrap: anywhere; }
/* An identifier on that line is the exception, and `.mono` is what marks one: a project's
   uuid is a single token, not a sentence, and breaking it across two lines makes it
   unreadable at a glance and unselectable by double-click -- which is the one thing anybody
   wants to do with it. It is 36 characters at 11.5px and the row is wider than that, so
   this asks for no more room than it already had. */
.pick-id.mono { overflow-wrap: normal; white-space: nowrap; }
/* A name line carrying metadata beside it rather than under it: the Neutron name a router
   flavor is really called, and the role it is restricted to. Everywhere else `.pick-name`
   is one string, so it has no gap to give them and they render welded together
   ("Cisco ASAcisco-asa"). Flex here so the pieces are spaced and wrap as a group, and the
   `.tag` margin is dropped because the gap is now doing that job. */
.pick-name.meta { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.pick-name.meta .tag { margin: 0; }

/* ============================================================
   LOGIN / SCOPE
   Three grounds, one per load (web/auth/common.py signin_look): .login-sun is the
   hero band over the form, .login-badge and .login-creature are an aside beside it.
   ============================================================ */
.login { flex: 1; display: flex; min-height: 0; background: var(--surface); }
.login-aside {
  width: 44%; max-width: 640px; flex: none; position: relative; overflow: hidden;
  background: var(--bar); color: var(--bar-text);
  padding: 28px 34px 46px;
  /* In dark both sides are navy and without a rule the two panes flatten into one. */
  border-right: 1px solid var(--bar-line);
  display: flex; flex-direction: column; justify-content: flex-end;
}
.login-aside > * { position: relative; }
.login-badge-art { position: absolute; left: 50%; top: 90px; transform: translateX(-50%); width: 340px; max-width: calc(100% - 68px); height: auto; }
.login-creature .login-aside { background-image: url("/static/img/manti-hero-right.jpg"); background-size: cover; background-position: left center; }
/* Fades the crop into the ground so the motto stays legible over the plates. */
.login-creature .login-aside::before { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 45%, var(--bar) 85%); }
.login-stripe { position: absolute; left: 0; right: 0; bottom: 0; height: 6px; background: var(--stripe); }

.login-sun { flex-direction: column; background: var(--bar); color: var(--bar-text); overflow-y: auto; }
/* Capped so the card below fits a 900px viewport; the wordmark sits at the crop's foot. */
.login-hero { display: block; width: 100%; height: auto; max-height: 44vh; object-fit: cover; object-position: left bottom; flex: none; }
.login-sun .login-stripe { position: static; flex: none; }
/* The card overlaps the hero's foot beside the copy. The copy column is 42% so the card
   clears the wordmark, whose right edge sits at 40% of the crop at every width. */
.login-sun .login-main { flex: 1; align-items: stretch; justify-content: flex-start; gap: 64px; padding: 32px 40px; overflow: visible; }
.login-copy { display: flex; flex-direction: column; flex: 0 0 42%; }
.login-sun .login-card {
  width: 400px; flex: none; align-self: flex-start; margin-top: -150px;
  padding: 28px; border-radius: 10px; background: var(--surface); color: var(--text);
  border: 1px solid var(--line); box-shadow: var(--shadow);
}

/* 24px: the one setting above the 19px console ceiling besides Essentials. Fira Sans
   narrows at display sizes, so the tracking opens rather than tightens. */
.login-motto {
  margin: 0; font-size: 24px; font-weight: 400; line-height: 1.3; letter-spacing: .005em;
  color: var(--bar-text); max-width: 24ch; text-wrap: balance;
}
.login-motto b { font-weight: 600; color: var(--ember); }
.login-meta { margin: 10px 0 0; font-size: 12.5px; letter-spacing: .02em; color: var(--bar-dim); }
.login-meta b { font-weight: 500; color: var(--bar-text); }
.login-place { margin: 32px 0 0; font-size: 11.5px; letter-spacing: .18em; color: var(--bar-dim); font-family: var(--mono); }
.login-sun .login-place { margin-top: auto; padding-top: 32px; }

.login-main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; overflow-y: auto; }
.login-card { width: 330px; }
.login-card-wide { width: 430px; }
.login-title { margin: 0 0 3px; font-size: 19px; font-weight: 600; letter-spacing: -.01em; }
.lede { margin: 0 0 20px; font-size: 12.5px; color: var(--muted); }
.sso { display: flex; flex-direction: column; gap: 7px; }
.sso-btn { justify-content: center; padding: 7px 11px; }
.rule { display: flex; align-items: center; gap: 10px; margin: 16px 0; color: var(--muted); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
.rule::before, .rule::after { content: ""; flex: 1; height: 1px; background: var(--line); }

/* An optional sign-up link under the sign-in button. Quiet, centred, its own line.
   Muted rather than red: the primary button is 14px above it, and two reds that close
   together say the choice between them does not matter. Same shape as .jb-link. */
.signup-link { margin: 14px 0 0; text-align: center; font-size: 12.5px; }
.signup-link a { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; }
.signup-link a:hover { color: var(--text); }

@media (max-width: 780px) {
  .login-aside { display: none; }
  .login-sun .login-main { flex-direction: column; padding: 24px; }
  .login-sun .login-card { margin-top: 0; width: 100%; }
}

/* ============================================================
   EMPTY / STUB
   ============================================================ */
.stub { padding: 32px 24px; max-width: 60ch; }
.stub-title { margin: 0 0 6px; font-size: 15px; font-weight: 600; }
.stub p { margin: 0; color: var(--muted); font-size: 12.5px; }
/* A second sentence that is a separate thought, not a continuation. */
.stub p + p { margin-top: 10px; }

/* ============================================================
   DRAWER, DETAIL, AND MODAL
   Ported from the approved mockup. Detail opens as a right-hand
   drawer over the list rather than a route change, so the list
   context is never lost.
   ============================================================ */
/* Drawer ----------------------------------------------------- */
/* Fixed, not absolute: the mockup lived inside a fixed-size demo frame; here the
   drawer overlays the viewport. */
.scrim { position: fixed; inset: 0; background: rgba(26,26,35,.32); z-index: 40; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: 520px; max-width: 82%;
  background: var(--surface); border-left: 1px solid var(--line);
  box-shadow: var(--shadow); z-index: 41;
  display: flex; flex-direction: column;
}
.dr-head { flex: none; padding: 13px 16px 0; border-bottom: 1px solid var(--line); }
/* A head with nothing to act on. `.dr-acts` normally supplies the bottom padding, so a
   drawer that is only something to read needs it back or the rule sits on the title. */
.dr-head-plain { padding-bottom: 13px; }
.dr-top { display: flex; align-items: flex-start; gap: 10px; }
.dr-name { font-size: 15px; font-weight: 600; margin: 0; }
/* `.copy` makes this a flex line where it carries a copy button, so the margin has to
   survive that -- a bare `display: inline-flex` from `.copy` would otherwise drop it. */
.dr-id { font-family: var(--mono); font-size: 11px; color: var(--muted); margin: 3px 0 0; }
.dr-id.copy { display: flex; }
.dr-close { margin-left: auto; background: none; border: 0; cursor: pointer; color: var(--muted); padding: 3px; line-height: 0; border-radius: 2px; }
.dr-close:hover { background: var(--n2); color: var(--text); }
.dr-close .ic { width: 16px; height: 16px; }
.dr-acts { display: flex; gap: 6px; padding: 11px 0 12px; flex-wrap: wrap; }
/* Where a button is deliberately absent, the reason goes where the button would have
   been. A control that always fails is worse than no control, but no control and no
   explanation is worse than both. */
.dr-acts .act-note { flex-basis: 100%; margin: 2px 0 0; }
/* What this project is to the thing in the drawer, and the flags that change what it may
   ask of it -- under the name, because they govern every button below. */
.dr-flags { display: flex; flex-wrap: wrap; margin-top: 6px; }
.tabs { display: flex; gap: 2px; }
.tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  font: inherit; font-size: 12.5px; color: var(--muted);
  padding: 6px 10px; cursor: pointer; margin-bottom: -1px;
}
.tab:hover { color: var(--text); }
.tab.on { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
/* The same strip on a page rather than in a drawer, for a resource read as a place of its
   own -- a cluster, which holds pools, what it built in this project, and how to reach it.
   `.tab` carries `margin-bottom: -1px` expecting a 1px rule underneath to sit on, and in a
   drawer that rule belongs to `.dr-head`. On a page nothing provides it, so the marker
   under the active tab floats against nothing and the strip sits hard against the left
   edge while every band above and below it is inset. */
.page-tabs { padding: 0 18px; border-bottom: 1px solid var(--line); }
/* A drawer holding a form has the form between .drawer and .dr-body, which breaks the
   flex chain: the body stops being a flex child, never gets a bounded height, and a
   long Advanced section runs off the bottom of the page with no way to scroll to the
   buttons. The form has to be the column. */
.drawer > form { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.dr-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 16px 22px; }

/* Pinned under the scrolling body, so the submit button is always reachable. */
.dr-foot {
  flex: none; display: flex; gap: 8px; align-items: center;
  padding: 11px 16px; border-top: 1px solid var(--line); background: var(--app-bg);
}
.dr-foot .right { margin-left: auto; display: flex; gap: 8px; }
/* Beside a disabled submit rather than under it, saying why it is disabled. Same class as
   the drawer's absent-control note and the same rule behind both: a control that cannot
   work is removed or held shut, never left live, and never without the reason. */
.dr-foot .act-note { margin: 0; font-size: 11.5px; color: var(--muted); }

/* The preflight list: what a change would do, split into what the service will refuse and
   what it will carry out without objecting. Those are different risks -- one costs a
   click, the other takes a subnet off the network -- so the kind is named in the margin
   and not left to colour alone.

   A list rather than a table: each finding is a sentence with a variable amount of
   evidence under it, and a table would either truncate the evidence or leave most cells
   empty. */
.pf { list-style: none; margin: 0 0 18px; padding: 0; }
.pf > li {
  display: grid; grid-template-columns: 92px 1fr; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--line);
  font-size: 12.5px; line-height: 1.55;
}
.pf > li:last-child { border-bottom: 1px solid var(--line); }
.pf-mark {
  font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  padding-top: 2px;
}
.pf-stop .pf-mark { color: var(--bad); }
.pf-warn .pf-mark { color: var(--warn); }
/* The remedy, under the finding. Deliberately not a button: every one of these is an
   action on somebody else's resource, and a control here would imply this screen owns it. */
.pf-fix { display: block; margin-top: 5px; font-size: 11.5px; color: var(--muted); }
/* Evidence inside a finding -- supporting detail rather than the subject, so tighter than
   a section table and without its row hover. */
.pf-tbl { margin-top: 8px; }
.pf-tbl .tbl { font-size: 11.5px; }
.pf-tbl .tbl tbody tr:hover td { background: none; }
/* The id under a row's name, or under a drawer's title. The control waits for the line's
   hover rather than announcing itself: beside a value nobody reads character by character,
   a permanently dark icon is the loudest thing in the row. Faded, never `display: none` --
   a hidden control is unreachable from a keyboard, and a touch screen has no hover to
   give, so it stays in the layout and in the tab order at all times. */
.copy-quiet button { opacity: .35; transition: opacity .12s; }
.copy-quiet:hover button, .copy-quiet button:focus-visible { opacity: 1; }
/* On its own line under the name it belongs to, rather than trailing off the end of it.
   `.copy` is an inline-flex, so this is a flex that is *block* level -- and the pair is
   written as one two-class selector so it cannot depend on which rule comes last. */
.copy.copy-line { display: flex; }
/* Said on the control that was pressed: at this size the only question is whether the
   click registered, and an answer elsewhere on the screen is a second thing to find.
   A word rather than a colour change alone, which says nothing to anyone who cannot see
   the difference between two small grey-green icons. */
.copy button.copied { opacity: 1; color: var(--ok); }
/* A labelled copy button says it in its own label instead, so this carries only colour. */
.btn.copied { color: var(--ok); border-color: var(--ok); }
.copy button.copied::after {
  content: "copied"; margin-left: 4px; font-family: var(--sans); font-size: 10px;
  letter-spacing: .02em; vertical-align: 2px; line-height: 1;
}
.meter { height: 4px; background: var(--n2); border-radius: 2px; overflow: hidden; width: 100%; max-width: 200px; margin-top: 5px; }
.meter > i { display: block; height: 100%; background: var(--brand); }

/* Event log -------------------------------------------------- */
.log { display: flex; flex-direction: column; }
.log-row { display: grid; grid-template-columns: 14px 1fr auto; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line-sq); align-items: start; }
/* The offset belongs to the grid row, not to the mark: a shared dot carrying a layout
   nudge can only be used in the one place it was written for. */
.log-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--n4); }
.log-row .log-dot { margin-top: 5px; }
.log-dot.ok { background: var(--ok); } .log-dot.bad { background: var(--bad); } .log-dot.warn { background: var(--warn); }
/* Live. Slow and shallow: a boot log is read for minutes at a time, and anything sharper
   than this becomes a thing to look away from. */
.log-dot.live { background: var(--ok); animation: dot-live 2.4s ease-in-out infinite; }
@keyframes dot-live { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .log-dot.live { animation: none; } }
.log-what { font-size: 12.5px; }
/* A second line under the entry's title. `.hint` is inline where it follows a field,
   which is right there and wrong here. */
.log-what .hint { display: block; margin-top: 2px; }
.log-when { font-size: 11.5px; color: var(--muted); white-space: nowrap; }

/* Modal ------------------------------------------------------ */
.modal {
  position: fixed; z-index: 50; top: 50%; left: 50%; transform: translate(-50%,-50%);
  width: 700px; max-width: 92%; max-height: 88%;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow);
  display: flex; flex-direction: column;
}
.md-head { flex: none; display: flex; align-items: center; padding: 13px 16px; border-bottom: 1px solid var(--line); }
.md-head h3 { margin: 0; font-size: 15px; font-weight: 600; }
/* The note beside a modal's title, and the controls that trail it. */
.md-head .dr-id { margin-left: 10px; }
.md-head-acts { margin-left: auto; display: flex; gap: 8px; align-items: center; }
/* The same break the drawer has, for the same reason: a modal whose body and footer are
   wrapped in a form loses the flex chain, so the body never gets a bounded height and the
   footer is pushed off the bottom of the viewport. Which is worse here than it sounds --
   the footer is where the submit button lives, and on the import and convert dialogs it
   is also where the acknowledgement that enables it lives. */
.modal > form { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.md-body { flex: 1; overflow-y: auto; display: flex; min-height: 0; }
.md-steps { width: 176px; flex: none; border-right: 1px solid var(--line); padding: 12px 0; background: var(--app-bg); }
.md-step { display: flex; gap: 9px; align-items: baseline; padding: 7px 14px; font-size: 12.5px; color: var(--muted); }
.md-step b { font-family: var(--mono); font-size: 11px; font-weight: 600; }
.md-step.on { color: var(--brand); font-weight: 600; background: var(--sel); box-shadow: inset 3px 0 0 var(--brand); }
.md-step.done { color: var(--text); }
.md-pane { flex: 1; padding: 16px 18px; min-width: 0; }
.md-foot { flex: none; display: flex; gap: 8px; align-items: center; padding: 11px 16px; border-top: 1px solid var(--line); background: var(--app-bg); }
.md-foot .right { margin-left: auto; display: flex; gap: 8px; }
/* What the console log's footer says it is doing. It shortens rather than pushing the
   controls off the end of a narrow modal. */
.md-foot .sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opt-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 2px; margin-bottom: -1px; cursor: pointer; background: var(--surface); }
.opt-row:hover { background: var(--hover); }
.opt-row.on { border-color: var(--brand); background: var(--sel); position: relative; z-index: 1; }
.opt-main { flex: 1; min-width: 0; }
.opt-main b { font-weight: 500; font-size: 12.5px; }


/* ============================================================
   CAPACITY METERS
   One number against one limit is a meter, not a chart: a bar
   against a track is read correctly at a glance where a pie of
   two slices is not. Grouped by the service that owns the
   quota, because that is who refuses the request.
   ============================================================ */
/* What an action is about to do, itemised, inside the dialog that will do it. One row
   per thing, so a selection that collapses into fewer jobs than it has rows can show
   that rather than assert it. */
.rev { border: 1px solid var(--line); }
.rev-row {
  display: flex; gap: 12px; padding: 10px 12px;
  border-bottom: 1px solid var(--line-sq);
}
.rev-row:last-child { border-bottom: 0; }
.rev-row > .ic { color: var(--muted); flex: none; margin-top: 1px; }

/* An acknowledgement in a dialog's footer, beside the button it gates. It sits there
   rather than at the foot of the scrolling body because a disabled primary button whose
   reason is below the fold reads as broken. */
.ack { display: flex; align-items: center; gap: 8px; font-size: 12.5px; cursor: pointer; }
.ack input { margin: 0; }

/* A job's progress: the same track-and-fill, wherever a percent is shown against a
   state. Three migration templates were each drawing it with inline styles, which is
   three chances for them to disagree about how tall a progress bar is. */
.mtr {
  width: 110px; height: 5px; flex: none;
  background: var(--n3); border-radius: 3px; overflow: hidden;
}
.mtr > i { display: block; height: 100%; background: var(--brand); }
.mtr.ok > i { background: var(--ok); }
/* The other half of .mtr.ok, and it was missing: without it a failed job draws its bar in
   the brand accent next to a --bad pill saying failed, which is two colours for one state. */
.mtr.bad > i { background: var(--bad); }
/* Inside a table cell. 110px is sized for a detail pane and too wide for a column that also
   carries a percentage, so the row gets a narrower variant rather than a second component. */
.mtr.mtr-row { width: 66px; display: inline-block; vertical-align: middle; }
/* The number beside the bar. The bar answers "how far", this answers "how far exactly", and
   tabular-nums stops it jittering as it counts. */
.pct {
  margin-left: 7px; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* A Size cell during an upload: the bar and its number are one unit, and a percentage
   wrapped under a bar reads as two separate facts. */
td.up-cell { white-space: nowrap; }

/* ============================================================
   DROP ZONE
   The one new component the image upload needed. A dashed border because the region is a
   target rather than a surface: every solid 1px --line box in this app is something you
   read, and this is something you drop onto.
   ============================================================ */
.dropzone {
  border: 1px dashed var(--n4); border-radius: 2px; background: var(--n1);
  padding: 26px 20px; text-align: center; cursor: pointer;
  transition: border-color .12s, background .12s;
  /* Matches .field's own bottom margin so the zone sits in the form's rhythm rather than
     butting against the label under it. */
  margin-bottom: 14px;
}
.dropzone:hover { border-color: var(--n5); background: var(--n2); }
/* Dragging over it. Red, because for the length of the drag this is the one thing on the
   page that will act -- the same claim .btn-primary makes. */
.dropzone.over { border-color: var(--brand); background: var(--brand-tint); }
.dropzone:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.dropzone .dz-ic { width: 22px; height: 22px; color: var(--n4); }
.dropzone.over .dz-ic { color: var(--brand); }
.dz-lead { margin: 7px 0 2px; font-size: 12.5px; font-weight: 600; }
.dz-sub { margin: 0; font-size: 11.5px; color: var(--muted); }
.dz-link { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
.dz-formats { margin: 9px 0 0; font-family: var(--mono); font-size: 11px; color: var(--n4); }

/* With a file chosen the zone stops being a target first and becomes a statement of what is
   about to be uploaded: left-aligned, tighter, no longer inviting. It is still a drop
   target, which the last line says quietly rather than in the middle in bold. */
.dropzone:has(> [data-dz-file]:not([hidden])) { padding: 10px 12px; text-align: left; }
.dz-file { display: flex; align-items: center; gap: 10px; }
.dz-file .dz-ic, .dz-file .btn { flex: none; }
.dz-meta { min-width: 0; flex: 1; }
.dz-name {
  margin: 0; font-family: var(--mono); font-size: 11.5px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dz-swap {
  margin: 8px 0 0; padding-top: 7px; border-top: 1px solid var(--line);
  font-size: 11px; color: var(--muted);
}

/* Where a detected format came from. Quiet, because it answers a question nobody asked out
   loud -- but the difference between a header and a filename is exactly what somebody wants
   to know before overriding it. */
.det-inline {
  display: inline-block; font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ok); font-weight: 600; white-space: nowrap;
}
/* From the name rather than the bytes: a claim, not a fact, so it is not green. */
.det-inline.det-name { color: var(--muted); }
/* The separator between the file size and the mark. Drawn here rather than written into the
   mark's text because .det-inline is inline-block, which collapses a leading space -- the
   size and the mark rendered welded together as "40.0 KiB· QCOW2 FROM HEADER". */
.dz-sub .det-inline::before { content: "·"; margin: 0 5px; color: var(--n4); }

/* A batch rather than one file. The image zone switches to its tight left-aligned form on
   `:has(> [data-dz-file])`; this is the same switch with a second hook, because a queue is a
   list and the list is what the class lands on. */
.dropzone.dz-filled { padding: 10px 12px; text-align: left; }

/* Ten files must not push the Upload button off the bottom of the drawer, so the list
   scrolls and the footer stays where it is. */
.dz-list { max-height: 236px; overflow-y: auto; }
.dz-list .dz-file { padding: 6px 0; border-top: 1px solid var(--line); }
.dz-list .dz-file:first-child { border-top: 0; padding-top: 0; }
/* Both fixed: .dz-meta already takes the slack, and a bar that resizes with the filename
   beside it makes five rows look like five different scales. */
.dz-list .mtr, .dz-list .pct { flex: none; }
.dz-list .pct { min-width: 34px; text-align: right; }

/* A name already taken in this folder. Uploading replaces it and the store does not ask, so
   the row says so first -- warn rather than bad, because it is legal and often meant. */
.dz-clash .dz-name { color: var(--warn); }
.det-inline.det-clash { color: var(--warn); }

/* A file the batch cannot take. Struck through and dimmed rather than dropped from the list:
   a file that vanishes on drop reads as the page having missed it, and the count somebody
   dragged in has to reconcile with the count about to be sent. */
.dz-file.dz-out { opacity: .62; }
.dz-out .dz-name { text-decoration: line-through; text-decoration-color: var(--n4); }
.det-inline.det-out { color: var(--bad); }

/* A row belonging to the one above it -- a container holding another's large-object
   segments. Stepped in and quieted so the pair reads as one thing, while staying a row with
   its own count, its own size and its own link.

   The step is on the name rather than the cell's padding, because the name is not in the
   first cell: a selection column sits to its left, and `td:first-child` would have indented
   the checkbox and left the name exactly where it was. */
.tbl tbody tr.row-child .name-cell { margin-left: 16px; color: var(--muted); font-weight: 400; }
/* A note in place of rows the table decided not to fetch. Not a row about a resource, so it
   does not take the hover a resource row does. */
.tbl tbody tr.row-note:hover td { background: none; }
.tbl tbody tr.row-note td { height: auto; padding: 10px 12px; white-space: normal; }

.caps { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        gap: 14px 22px; margin-bottom: 6px; }
.cap-group { border: 1px solid var(--line); background: var(--surface); padding: 10px 12px 12px; }
.cap-group > h4 {
  margin: 0 0 9px; font-size: 10px; font-weight: 700; letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted);
  display: flex; gap: 7px; align-items: baseline;
}
.cap-group > h4 > span {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0; text-transform: none;
  color: var(--n4);
}
.cap { display: grid; grid-template-columns: 92px 1fr auto; gap: 10px; align-items: center;
       padding: 3px 0; font-size: 11.5px; }
.cap-label { color: var(--muted); }
.cap-track { position: relative; height: 6px; background: var(--n3); border-radius: 3px;
             overflow: hidden; }
.cap-track > i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--n5);
                 border-radius: 3px; }
/* Semantic, not the accent: red here would compete with "the panel you are on". */
.cap.warn .cap-track > i { background: var(--warn); }
.cap.bad  .cap-track > i { background: var(--bad); }
.cap-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
.cap-value em { font-style: normal; color: var(--muted); margin-left: 5px; }
.cap.bad .cap-value { color: var(--bad); font-weight: 600; }
/* Unlimited: a dashed track, because a solid bar at any width would be a claim. */
.cap-none { background: repeating-linear-gradient(90deg, var(--n3) 0 3px, transparent 3px 6px); }
.cap-out { opacity: .8; }
.cap-out .hint { margin: 0; display: flex; align-items: center; gap: 8px; }

/* Loading. A flat track, never a shimmer: the global prefers-reduced-motion reset kills
   animation, so anything that relies on movement to read as "loading" reads as "broken"
   for those users. The spinner beside the heading carries it, and it is the same one the
   status pills already spin. */
.cap-skel { background: var(--n2); }
.cap-skel-text { display: block; width: 74px; height: 8px; background: var(--n2); border-radius: 2px; }
.sec-loading {
  display: inline-flex; align-items: center; gap: 6px; margin-left: 10px;
  font-size: 10px; font-weight: 400; letter-spacing: 0; text-transform: none;
  color: var(--muted);
}
.sec-loading .ic { width: 11px; height: 11px; }
.sec-note {
  margin-left: 8px; font-size: 10px; font-weight: 400; letter-spacing: 0;
  text-transform: none; color: var(--muted);
}

/* At the ceiling, what the ceiling stops. Inside the row's own grid and spanning it, so it
   reads as part of that row rather than as a note under the block. Only ever rendered for
   a meter in the `bad` state -- see overview/_capacity.html and quota.Row.at_limit. */
.cap-blocked { grid-column: 1 / -1; margin-top: -2px; font-size: 11.5px; line-height: 1.5;
  color: var(--bad); }

/* How the number above was reached: the physical total, the overcommit and what is held
   back. Deliberately not `.cap-blocked`'s colour -- that line is a consequence at the
   ceiling, this one is arithmetic, and a reader scanning for red must not find both. */
.cap-sub { grid-column: 2 / -1; margin-top: -1px; font-size: 11px; line-height: 1.5;
  color: var(--muted); font-variant-numeric: tabular-nums; }

/* ============================================================
   THE "NOW" BAND
   What is unsettled in this project, above the capacity it is
   using. Exception-driven: a tile with nothing wrong is
   monochrome, so the one that is coloured is the whole message
   and can be found without reading. A band that lights up on a
   good day is a band people stop looking at.
   ============================================================ */
/* auto-fit, not auto-fill: three tiles take the row rather than three of five 200px
   columns with the rest empty, which is what crowded Where it lands. */
.ov-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 10px; margin: 0 0 4px; }

/* Deliberately the same box as `.cap-group`: 1px --line on --surface, same padding, same
   heading. The band above capacity and the band below it are one piece of furniture holding
   two kinds of fact, and a reader learns the shape once.

   Restated rather than shared with `.cap-group` because that component is rendered by six
   panels and this ships without editing any of them. If the two ever want to move together
   they should share a `.tile` rule; until then the duplication is three properties. */
.ov-tile { display: flex; flex-direction: column; gap: 8px;
           border: 1px solid var(--line); background: var(--surface); padding: 10px 12px 11px; }

/* The kind, and the total. `.cap-group > h4`'s type exactly. The total is the smallest
   thing on the tile because it is context: the news is the headline under it. */
.ov-tile > h4 { margin: 0; display: flex; align-items: baseline; gap: 7px;
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); }
.ov-tile > h4 > a:hover { color: var(--brand); }
.ov-tile > h4 > .ov-n { margin-left: auto; font-size: 11.5px; font-weight: 400;
  letter-spacing: 0; font-variant-numeric: tabular-nums; color: var(--muted); }

/* What narrows the kind, where the name alone would overclaim: Stacks counts only the ones
   no catalogue built. Mono and lower case so it reads as a qualifier rather than as a
   second name. */
.ov-tile > h4 > .ov-qual { font-family: var(--mono); font-size: 10px; letter-spacing: 0;
  text-transform: none; color: var(--n4); }

/* The headline: a phrase, never a number on its own. Semantic colour, not the accent --
   the red here is the same red that means "the panel you are on" everywhere else, and this
   is the one place it is allowed to mean something is wrong. */
.ov-tile > .ov-head { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em;
  line-height: 1.25; }
.ov-tile > .ov-head > a:hover { text-decoration: underline; }
.ov-tile.warn > .ov-head { color: var(--warn); }
.ov-tile.bad  > .ov-head { color: var(--bad); }

/* The population, by state. `.cap-track`'s geometry exactly -- 6px on --n3 -- because it
   is the same idea one row up: a proportion, read at a glance, against a track. */
.ov-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--n3); }
.ov-bar > i { display: block; min-width: 3px; background: var(--n5); }
.ov-bar > i.warn { background: var(--warn); }
.ov-bar > i.bad  { background: var(--bad); }
.ov-bar > i.idle { background: var(--n3); }

/* Every non-zero state, worst first. Each one that can be filtered for is a link, so the
   tile ends somewhere rather than only informing. */
.ov-states { margin: 0; display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 11.5px;
  color: var(--muted); }
.ov-states > * { display: inline-flex; align-items: baseline; gap: 4px; }
.ov-states b { font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text); }
.ov-states > a:hover { text-decoration: underline; }
.ov-states > .warn, .ov-states > .warn b { color: var(--warn); }
.ov-states > .bad,  .ov-states > .bad  b { color: var(--bad); }
.ov-states > .idle b { color: var(--muted); font-weight: 400; }

/* Loading. A flat skeleton at the shape of the answer, and never a shimmer: the global
   prefers-reduced-motion reset kills animation, so anything relying on movement to read as
   "loading" reads as "broken" for those users. The spinner beside the heading carries it. */
.ov-skel > .ov-head { color: var(--n4); }
.ov-skel-text { display: inline-block; height: 8px; background: var(--n2); border-radius: 2px; }
.ov-skel-fill { background: var(--n2) !important; }

/* A tile whose service did not answer.
   Dashed, and never coloured. `quota.worst()` already made this call for the meters below:
   an unread service is not a failing one, and colouring it teaches people to discount the
   colour. The dash says the box has no answer in it, and the heading withholds "nothing
   needs attention" for as long as one of these is on screen. */
.ov-dead { border-style: dashed; }
.ov-dead > .ov-head { color: var(--muted); font-size: 12.5px; font-weight: 400; }

/* ============================================================
   THE FOLDS
   The two blocks at the foot of Overview are reference, not
   news, and what a reader wants from either on an ordinary
   visit is one line. The summary carries that line, so the
   common case is answered without opening.
   ============================================================ */
.ov-fold { margin-top: 20px; border-top: 1px solid var(--line); }
/* Two in a row are one stack of two rows, not two sections 20px apart. */
.ov-fold + .ov-fold { margin-top: 0; }
.ov-fold:last-of-type { border-bottom: 1px solid var(--line); }
.ov-fold > summary { display: flex; align-items: center; gap: 8px; padding: 9px 0;
  font-size: 12.5px; color: var(--muted); cursor: pointer; list-style: none; }
.ov-fold > summary::-webkit-details-marker { display: none; }
.ov-fold > summary:hover { color: var(--text); }
.ov-fold > summary > b { font-size: 10px; font-weight: 700; letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted); }
.ov-fold > summary:hover > b { color: var(--text); }
/* `#i-car` points down, which is the open state. Closed turns it a quarter to the right. */
.ov-fold-car { width: 11px; height: 11px; color: var(--n4); transform: rotate(-90deg);
  transition: transform .15s ease; }
.ov-fold[open] > summary > .ov-fold-car { transform: none; }
/* The right-hand slot: the one thing in the block that is not what you expect. Absent when
   there is nothing to say, which is most regions and most sessions. */
.ov-fold-flag { margin-left: auto; font-size: 11px; font-weight: 600; letter-spacing: .03em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 2px;
  background: var(--n2); color: var(--muted); }
/* A region id is not a word. Uppercasing `dfw-3` prints something the catalog does not
   contain, on the one line whose whole job is to name what it does contain. */
.ov-fold-flag .mono { text-transform: none; }
/* The other slot: a fact that is only news at one end of its range. Token expiry is this
   session's one fact with a clock on it, so it is stated plainly and turns loud on its own
   rather than growing a second element. */
.ov-fold-note { margin-left: auto; font-size: 11.5px; color: var(--muted); }
.ov-fold-flag + .ov-fold-note { margin-left: 10px; }
.ov-fold-note.soon { color: var(--warn); font-weight: 600; }
.ov-fold-body { padding: 4px 0 14px; }
@media (prefers-reduced-motion: reduce) { .ov-fold-car { transition: none; } }

/* ============================================================
   MULTI-KEYSTONE REGION MARKERS
   Which independent Keystones carry a given project. Nothing syncs
   them, so absence is real information and reads as such.
   ============================================================ */
/* The markers wrap onto a second or third line rather than holding one, because the row
   beside them carries a project's uuid and that must not break. Four regions at 10px is
   about seven characters each: on a narrow modal that is the difference between an id on
   one line and an id sliding underneath these. They are a legend, so two short lines of
   them costs nothing; an identifier broken across two lines cannot be read at a glance or
   selected with a double-click, which is the only thing anybody does with it.

   `justify-content: flex-end` keeps them against the right edge as they wrap, so the
   column does not appear to drift left when a row has fewer of them. */
.region-marks {
  display: flex; gap: 3px; flex: none; align-self: center;
  flex-wrap: wrap; justify-content: flex-end; max-width: 8.5rem;
}
.region-mark {
  font-family: var(--mono); font-size: 10px; letter-spacing: .04em;
  padding: 1px 5px; border-radius: 2px;
  background: var(--n2); color: var(--n4);
  border: 1px solid transparent;
}
.region-mark.has { background: var(--sel); color: var(--brand); border-color: var(--brand); font-weight: 600; }
.scope-select option[disabled] { color: var(--muted); }

/* ============================================================
   SCOPE SWITCHER
   ============================================================ */
.scope-btn-action {
  background: none; border: 0; font: inherit; cursor: pointer;
  border-radius: 2px;
}
.scope-btn-action:hover { background: rgba(255,255,255,.08); }
.scope-btn-action .car { color: var(--bar-dim); }

/* The chooser is a form, not a scrolling report: narrower than a detail modal, and
   its body should not fight the picker's own scroll region. */
.modal-narrow { width: 480px; }
.md-body-plain { display: block; }

/* ============================================================
   BULK ACTIONS, FILTERS, AND NOTICES
   ============================================================ */

/* A native <details> menu: keyboard accessible and open/close without JS. */
.menu { position: relative; }
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary.is-disabled { opacity: .5; cursor: not-allowed; }
.count { font-variant-numeric: tabular-nums; color: var(--muted); font-weight: 400; }
/* Beside a tab label the number needs air the summary's own gap already gives it. */
.tab .count { margin-left: 5px; }
/* A failure count beside one. Both the news and the way to the rows it counts, which is
   the only shape in which "how many failed" earns a line of its own. */
.tab .count.bad { color: var(--bad); }
.menu-list {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
  min-width: 168px; padding: 4px;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow);
}
.menu-item {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; font: inherit; font-size: 12.5px; color: var(--text);
  padding: 6px 9px; cursor: pointer; border-radius: 2px;
}
.menu-item:hover { background: var(--hover); }
.menu-item-danger { color: var(--bad); }
.menu-item-danger:hover { background: var(--bad-bg); }
/* A menu that names what it is a menu of. Only the appearance menu needs this so far: its
   three words sit under a glyph rather than under a worded summary, so without it the
   first thing you read is an option and not a subject. */
.menu-head {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); padding: 5px 9px 6px;
}
/* The item you are already on, marked the way `.seg` marks its own selected half rather
   than with a tick: one fewer glyph in the sprite, and the same signal the rest of the
   product uses for "this is the one". */
.menu-item.on { color: var(--brand); font-weight: 600; }
.menu-item.on .ic { color: var(--brand); }
/* An item carrying an icon. The rule above is `display: block`, which leaves an inline
   glyph sitting on the text's baseline rather than its centre line -- close enough to
   look like a mistake rather than a choice. Opt-in, so every text-only item in every
   other panel keeps the box it has always had. */
.menu-item-icon { display: flex; align-items: center; gap: 8px; }
.menu-item .ic { width: 13px; height: 13px; color: var(--muted); flex: none; }
.menu-item-danger .ic { color: var(--bad); }

/* Filter selects share the chip look so the toolbar reads as one control strip. */
select.chip {
  font: inherit; font-size: 12px; color: var(--text); cursor: pointer;
  padding: 4px 8px; border: 1px solid var(--line); background: var(--surface);
  border-radius: 2px;
}
select.chip:hover { border-color: var(--n4); }

/* Overflow marker for a cell that must stay one line. The count is the indicator;
   the title carries the rest on hover. */
.more {
  display: inline-block; margin-left: 5px; padding: 0 5px;
  font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--muted); background: var(--n2); border-radius: 2px; cursor: help;
}
.more:hover, .more:focus-visible { background: var(--sel); color: var(--brand); }

/* Which lane of an interface an address arrives on: a VLAN id, or `native` for the
   untagged wire. Mono and tabular, because a column of tags is read down as numbers.
   No vertical margin: this sits in a table cell that must stay exactly one line high. */
.lane-pill {
  display: inline-block; margin-right: 5px; padding: 0 5px;
  font-family: var(--mono); font-size: 10.5px; font-variant-numeric: tabular-nums;
  color: var(--brand); background: var(--brand-tint); border-radius: 2px;
}
/* The map from a fixed address to the floating one that reaches it. Quiet: the arrow is
   punctuation between two values, not a third value. */
.map-to { color: var(--n4); margin: 0 3px; }

/* Long values truncate rather than wrapping the row onto a second line. */
.cell-clip { max-width: 210px; overflow: hidden; text-overflow: ellipsis; }

/* A row for something present but not yet usable -- an imported image that still needs
   converting. Dimmed rather than hidden: it is really there, it just cannot be launched
   yet, and the row's title says so. The status cell still carries full-strength colour,
   so "why" stays readable. */
.tbl tbody tr.row-muted td { opacity: .55; }
.tbl tbody tr.row-muted td:has(.pill) { opacity: 1; }
.tbl tbody tr.row-muted:hover td { opacity: 1; }

.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 60;
  max-width: 70ch; padding: 9px 14px;
  background: var(--bar); color: var(--bar-text); font-size: 12.5px;
  border-left: 3px solid var(--brand); box-shadow: var(--shadow);
}

/* ============================================================
   PAGINATION
   Bottom-right, below the table. Client-side over rows already
   fetched, so paging costs nothing.
   ============================================================ */
.table-foot {
  flex: none;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 8px 18px;
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.pager { display: flex; align-items: center; gap: 8px; }
.pager-size { font-size: 11.5px; color: var(--muted); }
.pager-label {
  font-size: 12px; color: var(--text); font-variant-numeric: tabular-nums;
  min-width: 11ch; text-align: center;
}
.btn-sm { padding: 3px 9px; font-size: 12px; }

/* A destructive primary: red fill rather than red text, because it is the default
   action of a dialog whose whole purpose is destruction. */
.btn-danger-solid { background: var(--bad); border-color: var(--bad); color: #fff; }
.btn-danger-solid:hover { background: var(--bad); border-color: var(--bad); filter: brightness(1.1); }

.plain-list { margin: 10px 0 0; padding-left: 18px; font-size: 12.5px; }
.plain-list li { padding: 1px 0; overflow-wrap: anywhere; }

/* Swift breadcrumb: a prefix path, not a directory tree. */
.crumbs { display: flex; align-items: center; gap: 6px; font-size: 12.5px; }
.crumb { color: var(--muted); }
.crumb:hover { color: var(--brand); }
.crumb.on { color: var(--text); font-weight: 600; }
.crumb-sep { color: var(--n4); }
/* Over the title in a page head, where a burrow-in says where it came from. */
.page-head .crumbs { margin-bottom: 4px; }

/* The source-vs-target project hazard, ported from milkshake-server's own portal
   (portal/partials/source_hazard_styles.html) so the same condition reads the same in
   both products. Shown while a connected OSPC source may not belong to the project
   being imported into.

   Above the modal layer on purpose -- the condition outlives whatever dialog is open --
   and pointer-events:none so it never intercepts a click.

   The rgba values are milkshake's literal ones rather than tokens, for two reasons: a
   token resolves to a pale tint in dark mode and loses the alarm, and this has to read
   as hazard while sitting next to a brand accent that is already red. */
.hazard {
  position: fixed; inset: 0; z-index: 70; pointer-events: none;
  box-shadow: inset 0 0 40px 4px rgba(210,31,31,.5),
              inset 0 0 120px 30px rgba(210,31,31,.3),
              inset 0 0 220px 60px rgba(210,31,31,.14);
}
.hazard-banner {
  position: fixed; top: 0; left: 50%; transform: translateX(-50%); z-index: 71;
  padding: 5px 14px; border: 1px solid #d21f1f; border-top: none;
  border-radius: 0 0 8px 8px; background: #fbeaea; color: #8f1414;
  font-size: 11px; font-weight: 600; letter-spacing: .02em; white-space: nowrap;
  box-shadow: 0 2px 8px rgba(140,20,20,.25);
}

/* The idle warning. Bottom-centre, above everything, and never covering the primary
   action of whatever is open behind it. */
.idle-warn {
  position: fixed; z-index: 60; left: 50%; bottom: 22px; transform: translateX(-50%);
  display: flex; align-items: center; gap: 16px;
  background: var(--surface); border: 1px solid var(--warn);
  border-left: 3px solid var(--warn); box-shadow: var(--shadow);
  padding: 11px 14px; border-radius: 2px; max-width: 92vw;
}
.idle-acts { display: flex; gap: 7px; flex: none; }

/* A console needs room; a terminal at half size is not usable. */
.console-modal { width: 1100px; height: 78vh; }
.console-frame { width: 100%; height: 100%; border: 0; display: block; background: #000; }
/* The console log, in the same overlay the console uses. Dark like the terminal beside it
   rather than like the rest of the app: this is the machine's own output, and it is read
   in the same sitting as the console it sits next to.

   `pre-wrap` and not `pre`: a kernel line is longer than any window, and a reader chasing
   a boot failure should not be scrolling sideways to finish a sentence. `break-spaces`
   keeps a run of them from pushing the wrap point off the edge. */
.console-log {
  margin: 0; width: 100%; height: 100%; overflow: auto; box-sizing: border-box;
  padding: 14px 16px; background: #000; color: #D6D3D1;
  font-family: var(--mono); font-size: 12px; line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-spaces;
}

/* ============================================================
   PROGRESS
   htmx adds .htmx-request to whatever carries hx-indicator for
   the life of a request. Anything that goes to the server and
   is not instant needs one, or a slow step reads as a dead
   button and gets clicked twice.
   ============================================================ */
.busy-bar {
  position: absolute; left: 0; right: 0; top: 0; height: 2px;
  overflow: hidden; opacity: 0; transition: opacity .12s;
  pointer-events: none;
}
.busy-bar::after {
  content: ""; position: absolute; inset: 0; width: 40%;
  background: var(--brand); animation: busy-slide 1s ease-in-out infinite;
}
/* Delayed in, immediate out. A request that answers in 80ms was showing a red bar sweep
   across and vanish, which is a flash rather than progress -- and in a form where every
   answer is a round trip it happened on every click. The delay is on the way IN only, so a
   fast response shows nothing at all and a slow one still says it is working. */
.htmx-request .busy-bar, .busy-bar.htmx-request { opacity: 1; transition-delay: .35s; }
@keyframes busy-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}

/* The button reflects its own press for the life of its request: it dims and a spinner
   joins its label, so a click that goes to the server always reads as received. htmx
   carries .htmx-request on whatever triggered the request -- the button itself for a
   direct hx-get/hx-post (and the confirm dialog's button, which panel.js marks the same
   way). For a form submit htmx marks the *form*, so the primary action button is targeted
   through it -- and only that one, so a Back or other secondary submit alongside it does
   not also spin. The label stays put and the spinner is in the current text colour, so it
   reads on a red primary button and a plain one alike. */
.btn.htmx-request,
form.htmx-request button[type="submit"].btn-primary { opacity: .85; pointer-events: none; }
.btn.htmx-request::after,
form.htmx-request button[type="submit"].btn-primary::after {
  content: ""; display: inline-block; width: 11px; height: 11px; margin-left: 8px;
  vertical-align: -1px; border: 2px solid currentColor; border-top-color: transparent;
  border-radius: 50%; animation: busy-spin .6s linear infinite;
}

/* An icon-only button has no label for that spinner to join, so it simply arrives beside
   the glyph -- two marks in a control that is one, and on Refresh a still arrow next to a
   spinning disc reads as the button having produced a second button. The spinner stands in
   for the icon instead: `visibility`, not `display`, so the button keeps its width and
   nothing on the toolbar shifts while a request is in flight, and the disc is laid over
   the space the glyph left rather than added after it. */
.btn-icon.htmx-request { position: relative; }
.btn-icon.htmx-request > .ic { visibility: hidden; }
.btn-icon.htmx-request::after { position: absolute; inset: 0; margin: auto; }

/* Skeleton shimmer for a drawer whose detail is still being fetched. shell.js drops the
   drawer frame in the instant a row is clicked, filled with these muted bars, so the
   pane is there at once and the real content swaps in when the cloud answers -- rather
   than a second of nothing. A band sweeps along each bar; the two ends are theme tokens,
   so it reads in light and dark, and reduced-motion (global rule) freezes it to a plain
   bar that still says "loading". */
.skel-line {
  height: 12px; border-radius: 3px; margin: 0 0 14px;
  background: linear-gradient(90deg, var(--n3) 25%, var(--n2) 37%, var(--n3) 63%);
  background-size: 400% 100%;
  animation: skel-sweep 1.4s ease infinite;
}
@keyframes skel-sweep { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }

/* The same bars in a table, for a panel that fetches its own rows (list_page.html's
   rows_loading). The stacking margin is what the drawer's column of bars needs and what a
   cell must not have: a table row sets its own height, and 14px underneath each bar pushes
   every skeleton row taller than the real row that replaces it, so the table visibly
   collapses at the swap. */
.tbl .skel-line { margin: 0; }
/* Not hover-highlighted and not clickable: these rows are furniture, and a pointer cursor on
   one invites a click that opens nothing. */
.tbl tr.row-skel:hover { background: none; }
.tbl tr.row-failed td { text-align: center; height: 60px; }

/* A small copy button shared by the deployment drawer's outputs and usage code, reading
   the text to copy from the DOM so quotes and newlines survive. */
.copy-btn { background: none; border: 0; padding: 2px; margin-left: 6px; cursor: pointer;
  color: var(--n4); line-height: 0; vertical-align: middle; flex: none; }
.copy-btn:hover { color: var(--brand); }
.copy-btn .ic { width: 12px; height: 12px; }

/* An output value: the value (a link if it is a URL, else plain mono) and a copy button,
   with the description underneath. */
.out-val { display: block; }
.out-val > .mono, .out-val > a { word-break: break-all; }
.out-val > a { color: var(--brand); text-decoration: none; }
.out-val > a:hover { text-decoration: underline; }

/* A migration job's raw log, behind a disclosure so it is there on demand rather than
   filling the drawer. Smaller than usage-code: log lines are dense and monospaced, and a
   12px block of them dominated the pane. */
.joblog {
  margin: 8px 0 0; padding: 8px 10px; background: var(--n2);
  border: 1px solid var(--line); border-radius: 4px; overflow: auto; max-height: 260px;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; color: var(--text);
}

/* "How to use": prose steps and copyable command blocks, from a blueprint's usage. */
.usage p { margin: 0 0 8px; font-size: 12.5px; }
.usage-code { position: relative; margin: 0 0 12px; }
.usage-code pre { margin: 0; padding: 9px 32px 9px 11px; background: var(--n2);
  border: 1px solid var(--line); border-radius: 4px; overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.5; white-space: pre; }
.usage-code .copy-btn { position: absolute; top: 6px; right: 6px; margin: 0; }

/* The same steps under a form field, saying how to produce the answer it wants. One
   heading per platform, because the route differs by where you are standing. Sized below
   `.sec-title` deliberately: this sits inside a disclosure inside a field, and a heading
   at section weight in there reads as a new section rather than a label on a group. */
.howto-platform {
  margin: 12px 0 6px; font-size: 11px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
}
.howto-platform:first-child { margin-top: 0; }
/* Tighter than a deployment drawer's, which has the width of the drawer to itself. */
.howto .usage p { font-size: 12px; }
.howto .usage-code { margin-bottom: 8px; }

/* An inline spinner for a control whose own region is being replaced. */
.busy-dot {
  width: 11px; height: 11px; flex: none;
  border: 2px solid var(--n4); border-top-color: var(--brand);
  border-radius: 50%; animation: busy-spin .7s linear infinite;
  /* Always in the layout, never shifting what is beside it. It used to be display:none
     until a request started, so every fetch nudged the row it sat in. */
  display: inline-block; opacity: 0; animation-play-state: paused;
  transition: opacity .12s linear;
}
.htmx-request .busy-dot, .busy-dot.htmx-request {
  opacity: 1; animation-play-state: running;
  /* The delay is the point. A picker's answer is often cached and back in single-digit
     milliseconds, and a spinner shown for that long is a flash, not feedback -- worse
     than nothing, because it draws the eye to something already finished. Dropping the
     class cancels the pending transition, so a fast answer never reaches opacity 1 and
     only a fetch slow enough to be worth reporting is ever drawn. */
  transition-delay: .35s;
}
@keyframes busy-spin { to { transform: rotate(360deg); } }

/* The region being replaced dims rather than blanking, so the page does not jump. The
   interface row is the same case: choosing its network changes the address it offers, and
   the address field going quiet for the length of that round trip is what says so. The
   head stays lit -- the select somebody just used, and the spinner beside it. */
/* Delayed for the same reason as the dot above: dimming the rows and undimming them a few
   milliseconds later is a flicker, and switching modes back and forth made it strobe. */
.htmx-request.pick-body, .htmx-request .pick-body {
  opacity: .45; transition: opacity .1s linear .35s;
}
.nic.htmx-request .nic-body { opacity: .45; transition: opacity .1s; }

/* ============================================================
   PICKERS  (search + page on the server, select on the client)
   ============================================================ */
.picker { border: 1px solid var(--line); border-radius: 2px; }
.pick-head {
  display: flex; align-items: center; gap: 8px; padding: 7px 9px;
  border-bottom: 1px solid var(--line); background: var(--app-bg);
}
.pick-head .search { flex: 1; }
.pick-head input[type=text] {
  font: inherit; font-size: 12.5px; width: 100%; padding: 5px 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--text);
  border-radius: 2px;
}
.pick-body { max-height: 236px; overflow-y: auto; }
.pick-body .opt-row { border-left: 0; border-right: 0; }
.pick-foot {
  display: flex; align-items: center; gap: 8px; padding: 6px 9px;
  border-top: 1px solid var(--line); background: var(--app-bg);
  font-size: 11.5px; color: var(--muted);
}
.pick-foot .right { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.opt-row.off { opacity: .55; cursor: not-allowed; }

/* A selection the drawer is stating rather than offering: arrived from the table with
   rows already ticked. Built on .plain-list, which the bulk confirmation dialog uses for
   the same job, so the two places that say "here is what you picked" say it alike. */
.sel-list { margin-bottom: 14px; }
.sel-list > li { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; }
/* The name takes the room and wraps inside itself. `min-width: 0` is what lets it: a flex
   item's default minimum is its content, so without this a long unbroken volume name
   refuses to shrink and shoves the status tag out of the drawer. */
.sel-list .sel-main { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.sel-list > li > .tag, .sel-list > li > .why { flex: none; margin: 0; }
.sel-list .why { font-size: 11px; color: var(--warn); }

/* What a batch is about to create, listed before it is sent. A quiet inset block rather
   than an alert: nothing is wrong, this is the form answering "and what will that be
   called" while there is still time to change the answer. */
.preview {
  border: 1px solid var(--line); border-radius: 2px; background: var(--n1);
  padding: 9px 11px; margin-bottom: 14px;
}
.preview-head { font-size: 11.5px; font-weight: 600; margin-bottom: 5px; }
.preview ul { margin: 0; padding-left: 16px; }
.preview li { font-size: 12px; margin-bottom: 2px; }

/* A picker grouped by the thing that holds its rows: instances, and the volumes attached
   to each. The group is the unit somebody aims at, so it reads as one block with a
   hairline between groups rather than the uniform -1px row stack the flat list uses. */
.pick-group { border-bottom: 1px solid var(--line); }
.pick-group:last-child { border-bottom: 0; }
.opt-head b { font-weight: 600; }
/* The head of a group nothing can be selected by -- the volumes attached to no instance.
   Padded to where the checkbox would have put the text, so the headings line up. */
.opt-head-plain { padding-left: 32px; cursor: default; }
/* A volume under its instance. Indented by the checkbox plus its gap, so the child names
   start under the parent's name rather than under its box. */
.opt-child { padding-left: 32px; background: var(--n1); }
.opt-child b { font-weight: 400; }
.opt-row .why { font-size: 11px; color: var(--warn); flex: none; }
.opt-row .tag, .opt-row .reco { flex: none; }
.reco { font-size: 11px; color: var(--ok); font-weight: 600; }

/* Chips: what is selected, still visible when the filter has hidden the row. */
.chips { display: flex; flex-wrap: wrap; gap: 5px; padding: 7px 9px; border-bottom: 1px solid var(--line); }
.chip-sel {
  display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px;
  background: var(--sel); border: 1px solid var(--brand); color: var(--brand);
  border-radius: 2px; padding: 2px 5px 2px 8px; font-weight: 500;
}
.chip-sel button {
  background: none; border: 0; color: inherit; cursor: pointer;
  padding: 0 1px; line-height: 0;
}
.chips-none { font-size: 11.5px; color: var(--muted); padding: 1px 0; }

/* ============================================================
   FORMS, continued
   ============================================================ */
.check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; cursor: pointer; }
/* Stacked checkboxes. `.check` carries no margin of its own -- it is used alone as
   often as in a group -- so the group supplies the rhythm. */
.checks { margin-bottom: 14px; }
.checks .check + .check { margin-top: 7px; }
.field.err input, .field.err select, .field.err textarea { border-color: var(--bad); }
.err-msg { font-size: 11.5px; color: var(--bad); margin: 4px 0 0; }

/* Advanced: a curated set, closed by default. */
/* Room on both sides of the rule. Closed, this is a single line between a divider above
   and whatever the pane ends with below -- usually a footer border -- and it was sitting
   squeezed against both. */
.adv { border-top: 1px solid var(--line); margin-top: 18px; padding: 15px 0 4px; }
.adv > summary {
  cursor: pointer; font-size: 12px; font-weight: 600; color: var(--text);
  list-style: none; display: flex; align-items: center; gap: 7px;
  padding: 3px 0;
}
.adv > summary::-webkit-details-marker { display: none; }
.adv > summary::before { content: "\25B8"; color: var(--muted); font-size: 10px; }
.adv[open] > summary::before { content: "\25BE"; }
.adv > summary span { font-weight: 400; color: var(--muted); }
.adv-body { padding-top: 13px; }

/* A quiet aside beside a form: what the app does not do here, and who does. Dashed
   rather than solid so it does not read as an alert -- nothing is wrong. The name is
   older than the use: it held the CLI escape hatches, which are gone. */
.cli-note {
  margin-top: 14px; padding: 9px 11px; border: 1px dashed var(--line);
  background: var(--app-bg); font-size: 11.5px; color: var(--muted); line-height: 1.55;
}

/* ============================================================
   STACKED ROWS
   One bordered row, stacking into a list by pulling the next up over its own
   border. No opinion about what is in it -- that is the caller's.
   ============================================================ */
/* This was `.pf-row`, and Migration was already using it bare, with no `.pf` around it
   and no mark in it, because a row is what it wanted. That is what a primitive is for, so
   it is named as one now rather than left looking like part of a preflight component. */
.stack-row {
  display: flex; gap: 11px; padding: 11px 12px;
  border: 1px solid var(--line); margin-bottom: -1px;
}
.stack-row > div { min-width: 0; }
.stack-row b { font-weight: 600; font-size: 12.5px; }

/* ============================================================
   READINESS CHECKS
   The mark carries the state; colour is never doing it alone.
   ============================================================ */
/* "Can this run, and what is missing" -- the launch preflight and a node's hardware
   capabilities. Not `.pf`, which is the *worded* findings list further up this file:
   that one says "Refused" in the margin and offers its remedy as a sentence, because its
   fixes act on somebody else's resource. This one badges a glyph and offers a button.
   Two deliberate answers, which is precisely why they cannot share `.pf-mark` -- they
   both did, and neither rule overrode the other, so a worded mark was being laid out
   inside this one's 17px circle. */
.chk { display: flex; flex-direction: column; }
.chk-mark {
  flex: none; width: 17px; height: 17px; border-radius: 50%; display: grid;
  place-items: center; font-size: 10px; font-weight: 700; margin-top: 1px;
}
.chk-row.ok   .chk-mark { background: var(--ok-bg);   color: var(--ok); }
.chk-row.warn .chk-mark { background: var(--warn-bg); color: var(--warn); }
.chk-row.bad  .chk-mark { background: var(--bad-bg);  color: var(--bad); }
.chk-row.bad {
  background: var(--bad-bg); border-color: var(--bad);
  position: relative; z-index: 1;
}
.chk-fix { margin-top: 6px; }

/* Quota, shown as headroom rather than a number to interpret. */
.quota { margin-top: 16px; border-top: 1px solid var(--line-sq); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.quota-row { display: grid; grid-template-columns: 62px 1fr auto; gap: 10px; align-items: center; font-size: 11.5px; color: var(--muted); }
.quota .bar { position: relative; height: 6px; background: var(--n3); border-radius: 3px; overflow: hidden; }
.quota .bar i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--n5); }
.quota .bar u { position: absolute; top: 0; bottom: 0; background: var(--brand); opacity: .55; }

/* An advisory attached to the choice that caused it. */
.note { margin: 8px 0 0; font-size: 12px; line-height: 1.55; padding: 9px 11px; }
.note b { font-weight: 600; }
/* The same notice opening a pane rather than following the thing it is about. Its own
   margin is below it; this one's is above what comes next. */
.note-lead { margin: 0 0 12px; }

/* How many of the shape somebody just described fit here. A count of that thing has to
   read as a quantity rather than as an id, and zero is an answer, not an absence. */
.fits { font-variant-numeric: tabular-nums; font-weight: 600; }
.fits-0 { color: var(--muted); font-weight: 400; }

/* ============================================================
   OPERATOR VIEWS
   Admin is a different mode and has to look like one. Amber
   rather than red: red already means "the panel you are on",
   and a second red would compete with it.
   ============================================================ */
.admin-banner {
  display: flex; align-items: center; gap: 9px; padding: 8px 14px; margin-bottom: 12px;
  background: var(--warn-bg); border: 1px solid var(--warn); border-radius: 2px;
  font-size: 12px; color: var(--text);
}
.admin-banner .ic { color: var(--warn); }
.admin-banner b { font-weight: 600; }

.rail-admin {
  display: inline-flex; align-items: center; font-size: 9px; font-weight: 700;
  letter-spacing: .09em; color: var(--warn); border: 1px solid var(--warn);
  border-radius: 2px; padding: 0 4px; margin-left: 7px; line-height: 1.5;
}

/* Capacity. Full is marked, because "why did that launch fail" is usually this. */
.cap-bar { position: relative; height: 6px; background: var(--n3); border-radius: 3px; overflow: hidden; }
.cap-bar i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--n5); }
.cap-bar.over i { background: var(--bad); }
.cap-row {
  display: grid; grid-template-columns: 60px 1fr auto; gap: 10px;
  align-items: center; font-size: 11.5px; color: var(--muted); margin-bottom: 8px;
}
.cap-row .mono { color: var(--text); }

/* ============================================================
   INSTANCE NETWORK DIAGRAM
   Shapes come from compute/topology.py; this only colours them.
   Every value goes through a token: the diagram tracks the dark
   theme for free, and an <svg> has no theme of its own to fall
   back on when one does not.
   ============================================================ */
.sec-title .sec-spacer { margin-left: auto; }

/* The quiet action a section heading carries: Edit, Cancel, Add. Red text, no chrome,
   so a heading can offer something without competing with the primary action in the
   drawer's own button row. Sized here rather than inside one component, or it renders
   unstyled the first time somebody uses it somewhere else. */
.sec-act {
  font: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none;
  color: var(--brand); background: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px; padding: 0;
}
.sec-act:hover { text-decoration: underline; }
.sec-act .ic { width: 12px; height: 12px; }

/* One section is in edit at a time.
   A drawer is 520px wide: two open forms in it means two Save buttons and no way to
   tell which belongs to which. The section being edited carries `.on`, and every OTHER
   section's action goes quiet -- CSS, not JavaScript, because the state is already in
   the markup and nothing has to be kept in step with it. The editing section's own
   Cancel stays live, which is also how the user gets back out. */
.dr-body:has(.dr-sec.on) .dr-sec:not(.on) .sec-act {
  color: var(--n4); pointer-events: none; text-decoration: none;
}

/* The region a section becomes while it is being edited. The 3px left rule in --brand is
   the app's existing marker for an active or emphasised block (.rail-item.on, .alert,
   .toast), borrowed rather than reinvented. No tinted ground: a form is not a notice. */
.sec-edit { border-left: 3px solid var(--brand); padding: 13px 0 2px 14px; margin: 2px 0 8px; }
.sec-edit .field:last-of-type { margin-bottom: 10px; }
.sec-foot { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
.sec-foot .right { margin-left: auto; display: flex; gap: 8px; }

/* A qualifier on a section heading: the date the section is as of, or the API field it
   comes from. Reads as an aside beside the heading rather than as part of it, which is
   what keeps a dated section from needing a line of its own underneath. */
.sec-title .sec-note {
  font-weight: 400; letter-spacing: 0; text-transform: none; opacity: .85;
}

/* Which of Diagram / List is showing. A toggle with no pressed state is a pair of
   buttons that look like they do nothing. */
.btn[aria-pressed="true"] {
  background: var(--sel); border-color: var(--brand); color: var(--brand); font-weight: 500;
}

.topo { position: relative; }
.topo > svg { display: block; width: 100%; height: auto; }

/* Node fills and strokes. */
.n-fill-surface { fill: var(--surface); }
.n-fill-soft    { fill: var(--n1); }
.n-fill-info    { fill: var(--info-bg); }
.n-fill-red     { fill: var(--brand-tint); }
.n-fill-warn    { fill: var(--warn-bg); }
.n-stroke       { stroke: var(--line); stroke-width: 1; }
.n-stroke-info  { stroke: var(--info); stroke-width: 1; stroke-opacity: .45; }
.n-stroke-red   { stroke: var(--brand); stroke-width: 1; stroke-opacity: .5; }
.n-stroke-warn  { stroke: var(--warn); stroke-width: 1; stroke-opacity: .55; }
.plate          { fill: var(--surface); }
/* Divides one NIC box into its lanes. Lighter than any node's own edge: it separates the
   networks arriving on one interface without suggesting they are separate interfaces. */
.n-div          { fill: none; stroke: var(--line); stroke-width: 1; stroke-opacity: .8; }

/* Sprite icons reused inside the diagram. */
.ic-muted { color: var(--muted); }
.ic-info  { color: var(--info); }
.ic-red   { color: var(--brand); }

/* Type inside the diagram. Sized here rather than per-shape, so a chip that gains a
   line does not need a new class. */
.topo .t-name  { font: 500 12px var(--sans); fill: var(--text); }
.topo .t-meta  { font: 400 10.5px var(--mono); fill: var(--muted); }
.topo .t-label { font: 700 8.5px var(--sans); fill: var(--muted); letter-spacing: .11em; }
.topo .t-wire  { font: 400 9.5px var(--mono); fill: var(--muted); }
.topo .t-nic   { font: 500 10.5px var(--sans); fill: var(--text); }
.topo .t-mac   { font: 400 10px var(--mono); fill: var(--text); }
/* The guest's name for one lane of a trunk. Mono, because eth1.101 is a string somebody
   has to type into the machine, and the tag in it is the only exact thing in the name. */
.topo .t-lane  { font: 500 10.5px var(--mono); fill: var(--text); }
.topo .t-sub   { font: 400 9.5px var(--sans); fill: var(--muted); }
.topo .t-oui   { fill: var(--n4); }
.topo .t-warn  { fill: var(--warn); }
.topo .t-red   { fill: var(--brand); }
.topo .t-info  { fill: var(--info); }

/* Wires. The dashed variants are the ones that mean something is missing. */
.wire { fill: none; stroke: var(--info); stroke-width: 1.4; stroke-opacity: .5;
        transition: stroke-opacity .12s ease, stroke-width .12s ease; }
.wire.dead  { stroke: var(--n4);  stroke-dasharray: 4 3; stroke-opacity: .9; }
/* The mesh rail, and coverage drawn inside a diagram. Green because a mesh either carries
   a region or does not, which is the ok/absent axis and not the info/fault one the wires
   above use. See panels/mesh/diagram.py. */
.wire.mesh { stroke: var(--ok); stroke-dasharray: 6 4; stroke-opacity: .75; }
.n-fill-ok   { fill: var(--ok); fill-opacity: .8; }
.n-fill-line { fill: var(--n3); }
.wire.down  { stroke: var(--warn); stroke-dasharray: 4 3; stroke-opacity: .9; }
.wire.float { stroke: var(--brand);  stroke-dasharray: 3 3; stroke-opacity: .5; }

.st { stroke-width: 0; }
.st-ok { fill: var(--ok); } .st-warn { fill: var(--warn); }
.st-bad { fill: var(--bad); } .st-idle { fill: var(--n4); }

/* Trace: hovering any part of one path fades every other. */
.branch { transition: opacity .12s ease; }
.topo svg.tracing .branch { opacity: .18; }
.topo svg.tracing .branch.on { opacity: 1; }
.topo svg.tracing .wire.on { stroke-width: 2; stroke-opacity: 1; }
.hit { fill: transparent; cursor: pointer; }
.hit:focus { outline: none; }
.hit:focus-visible { fill: none; stroke: var(--brand); stroke-width: 1.6; }

/* The hover panel: every address the diagram deliberately leaves out. */
.topo .tip {
  position: absolute; z-index: 5; pointer-events: none; opacity: 0; width: 236px;
  transform: translate(-50%, -100%); transition: opacity .1s ease;
  background: var(--bar); color: var(--bar-text); border-radius: 3px;
  padding: 8px 10px; box-shadow: var(--shadow);
}
.topo .tip.show { opacity: 1; }
.topo .tip.below { transform: translate(-50%, 0); }
.topo .tip-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.topo .tip-head b { font-size: 12px; font-weight: 600; }
.topo .tip-head em {
  margin-left: auto; font-style: normal; font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--bar-dim);
}
.topo .tip > dl { display: grid; grid-template-columns: 50px 1fr; gap: 3px 8px; margin: 0; }
.topo .tip dt {
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--bar-dim); padding-top: 1px;
}
.topo .tip dd { margin: 0; font-family: var(--mono); font-size: 10.5px; word-break: break-all; }
.topo .tip .oui { color: var(--bar-dim); }
.topo .tip .none { color: var(--bar-dim); font-family: var(--sans); font-size: 10px; }
.topo .tip .warnish {
  font-family: var(--sans); font-size: 9.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--warn);
}
.topo .tip .maps { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--bar-line); }
.topo .tip .maps-label {
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--bar-dim); margin-bottom: 4px;
}
.topo .tip .map {
  display: grid; grid-template-columns: 1fr 12px 1fr; gap: 6px; align-items: baseline;
  font-family: var(--mono); font-size: 10.5px; margin-bottom: 2px;
}
.topo .tip .map .arr { color: var(--bar-dim); text-align: center; }
.topo .tip .map .fip { color: var(--brand-h); }

.legend {
  display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 10px;
  font-size: 11px; color: var(--muted);
}
.legend > span { display: inline-flex; align-items: center; gap: 5px; }
.legend > span > i { display: block; width: 16px; height: 0; border-top: 1.6px solid var(--info); opacity: .6; }
.legend i.float { border-top-style: dashed; border-color: var(--brand); }
.legend i.down  { border-top-style: dashed; border-color: var(--warn); }
.legend i.dead  { border-top-style: dashed; border-color: var(--n4); }
.legend i.mesh  { border-top-style: dashed; border-color: var(--ok); }
.legend .lg-gw { font-size: 8.5px; font-weight: 700; letter-spacing: .11em; color: var(--info); }
.legend .lg-gw.bad { color: var(--warn); }
.legend .lg-lane { font: 500 10px var(--mono); color: var(--text); }

/* Two gateways is the one thing here worth a sentence. One is ordinary and silent. */
.advisory {
  margin-top: 12px; padding: 9px 11px; border-radius: 2px; font-size: 12px;
  background: var(--warn-bg); color: var(--warn);
}

/* The list view. One row per fixed address, each carrying its own float. */
.dl-ports dt .dt-sub { display: block; font-size: 11.5px; color: var(--muted); }
.dl-ports .ipmap { display: flex; align-items: baseline; gap: 7px; font-size: 12.5px; }
.dl-ports .ipmap .arr { color: var(--n4); }
.dl-ports .ipmap .fip { color: var(--brand); }

/* ============================================================
   PORT WIRE
   What holds one port, drawn: the network it sits on, the port
   itself, and whatever claimed it. "What is this port for" is a
   sentence about three things, and a definition list makes the
   reader assemble it from six rows themselves.

   Stacked and not side by side, because it lives at the top of
   the 520px drawer and three cells across that leave nothing for
   a name. A trunk VLAN gets a fourth cell -- it reaches a machine
   it is not attached to -- which stacking costs height rather
   than forcing something out.

   Plain HTML rather than the SVG `.topo` the instance drawer
   draws: that is generated by topology.py to lay out a whole
   machine's ports, and this is one port and three boxes, where
   generating an SVG would be arithmetic to draw a straight line.
   ============================================================ */
.pw { display: flex; flex-direction: column; margin: 0 0 18px; }
.pw-node { border: 1px solid var(--line); background: var(--surface); padding: 8px 10px; }
/* The subject of the drawer, marked with the 3px left rule every emphasised block uses. */
.pw-node.self { border-left: 3px solid var(--brand); }
/* Nothing is holding it. Dashed, so an empty cell reads as deliberately empty rather
   than as a cell that failed to render. */
.pw-node.none { border-style: dashed; background: none; }
.pw-k {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 4px;
}
.pw-v { font-size: 12.5px; font-weight: 500; color: var(--text); overflow-wrap: anywhere; }
.pw-node.none .pw-v { font-weight: 400; color: var(--muted); }
.pw-s { font-size: 11.5px; color: var(--muted); margin-top: 2px; overflow-wrap: anywhere; }
/* The thread between two cells, indented so it reads as a connector rather than as a
   border belonging to either box. */
.pw-link {
  height: 15px; margin-left: 19px; border-left: 1px solid var(--line); position: relative;
}
/* A wire carrying nothing: attached but unbound, or nothing attached at all. Same
   vocabulary `.wire.dead` uses in the instance diagram. */
.pw-link.off { border-left-style: dashed; }
.pw-link em {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  font-style: normal; font-size: 10px; color: var(--muted); white-space: nowrap;
}

/* ============================================================
   JITTERBUG
   The simple view. Larger than the dashboard it sits inside, but only by a step:
   body text at 16px and up, every target at least 44px tall. Big enough for an
   unsteady hand and a tired eye without the page reading as a toy.

   Styled through the same tokens as everything else, so the dark theme tracks and
   this is recognisably the same product, only louder.
   ============================================================ */
.jb-page { max-width: 860px; margin: 0 auto; padding: 40px 24px 64px; }

.jb-eyebrow { font-size: 19px; font-weight: 600; color: var(--muted); margin: 0 0 18px; }
/* One per kind of thing, so a dozen items read as two short lists. */
.jb-group { font-size: 12px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
            color: var(--muted); margin: 26px 0 10px; }
.jb-group:first-of-type { margin-top: 0; }
.jb-q { font-size: clamp(22px, 2.6vw, 28px); line-height: 1.2; letter-spacing: -.018em;
        font-weight: 700; margin: 0 0 8px; text-wrap: balance; }
.jb-sub { font-size: 16px; color: var(--muted); margin: 0; max-width: 56ch; line-height: 1.5; }

/* The running total leads: the most useful number on the page, and one the full
   dashboard never shows. Not boxed -- a card here would compete with the cards under
   it instead of heading them. */
.jb-total { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap;
            padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--line); }
.jb-total .n { font-size: clamp(42px, 6.5vw, 58px); font-weight: 700; letter-spacing: -.03em;
               line-height: 1; font-variant-numeric: tabular-nums; }
.jb-total .n small { font-size: 20px; font-weight: 400; color: var(--muted); letter-spacing: 0; }
.jb-total .cap { font-size: 16.5px; color: var(--muted); line-height: 1.45; max-width: 34ch; }
/* The one control that starts a wizard sits opposite the heading, not under the last
   row where someone with twenty-five things would have to scroll to reach it. */
.jb-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }
.jb-head > .btn-jb { margin-left: auto; }
.jb-head > .jb-eyebrow { margin: 0; }
.jb-total .cap > b { display: block; font-size: 18px; color: var(--text); font-weight: 600; }

/* One line per thing. Names at a fixed left edge, so twenty-five of them form a
   column the eye can run down -- which is the whole reason this is a list and not a
   grid of cards. */
.jb-row { display: flex; align-items: center; gap: 12px; padding: 11px 12px; }
.jb-row + .jb-row { border-top: 1px solid var(--line); }
.jb-row:hover { background: var(--hover); }
.jb-row .nm { font-size: 15.5px; font-weight: 600; letter-spacing: -.005em; min-width: 0;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jb-row .meta { font-size: 13.5px; color: var(--muted); margin-left: auto; display: flex;
                gap: 14px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
/* Twenty-five rows times one button each is twenty-five buttons shouting at once.
   Revealed on hover, and always present where there is no hover to reveal them. */
.jb-row .go { opacity: 0; }
.jb-row:hover .go, .jb-row:focus-within .go { opacity: 1; }
@media (hover: none) { .jb-row .go { opacity: 1; } }

/* One button per kind of thing: big, always visible, each carrying its own count, so
   "how many machines do I have" is answered without clicking anything. */
.jb-kinds { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px;
            border-bottom: 1px solid var(--line); padding-bottom: 14px; }
.jb-kind { font: inherit; font-size: 15px; font-weight: 600; padding: 10px 16px;
           min-height: 44px; border: 1px solid var(--line); border-radius: 9px;
           background: var(--surface); color: var(--text); cursor: pointer;
           display: inline-flex; align-items: center; gap: 8px; }
.jb-kind:hover { background: var(--hover); }
.jb-kind.on { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.jb-kind .n { font-size: 13px; font-weight: 700; color: var(--muted); background: var(--hover);
              border-radius: 999px; padding: 1px 8px; font-variant-numeric: tabular-nums; }
.jb-kind.on .n { background: rgba(255,255,255,.22); color: var(--on-brand); }
.jb-kind .pip { width: 7px; height: 7px; border-radius: 50%; background: var(--bad); }

.jb-finder { display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
             margin-bottom: 6px; }
.jb-finder input { font: inherit; font-size: 15px; padding: 10px 14px; min-height: 42px;
                   flex: 1; min-width: 200px; border: 1px solid var(--line); border-radius: 8px;
                   background: var(--hover); color: var(--text); }
.jb-finder input:focus { outline: 2px solid var(--brand); outline-offset: 1px;
                         background: var(--surface); }
.jb-chip { font: inherit; font-size: 13.5px; font-weight: 600; padding: 7px 13px;
           min-height: 40px; border: 1px solid var(--line); border-radius: 999px;
           background: var(--surface); color: var(--muted); cursor: pointer; }
.jb-chip.on { background: var(--sel); border-color: var(--brand); color: var(--brand); }
.jb-chip .n { font-variant-numeric: tabular-nums; }
.jb-none { padding: 22px 12px; color: var(--muted); font-size: 15px; }

.jb-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--n4); }
.jb-dot-ok { background: var(--ok); }
.jb-dot-warn { background: var(--warn); }
.jb-dot-bad { background: var(--bad); }

/* Buttons, sized for an unsteady hand rather than for density. */
.btn-jb { display: inline-block; font: inherit; font-size: 16px; font-weight: 600;
          padding: 12px 20px; border-radius: 8px; border: 1px solid var(--line);
          background: var(--surface); color: var(--text); min-height: 44px;
          text-decoration: none; cursor: pointer; }
.btn-jb:hover { background: var(--hover); }
.btn-jb-go { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.btn-jb-go:hover { background: var(--brand-h); border-color: var(--brand-h); }
.btn-jb-sm { font-size: 14.5px; padding: 9px 15px; min-height: 40px; border-radius: 7px; }

.jb-foot { display: flex; align-items: center; gap: 14px; margin-top: 32px; padding-top: 22px;
           border-top: 1px solid var(--line); flex-wrap: wrap;
           font-size: 15.5px; color: var(--muted); }
.jb-link { font: inherit; font-size: 15.5px; background: none; border: 0; padding: 0;
           color: var(--muted); text-decoration: underline; text-underline-offset: 3px;
           cursor: pointer; }
.jb-link:hover { color: var(--text); }

/* The first-visit chooser. */
.jb-modal { max-width: 660px; }
.jb-modal-body { padding: 28px 28px; }
.jb-choices { display: grid; gap: 10px; margin-top: 20px; }
.jb-choice { display: flex; flex-direction: column; gap: 3px; text-align: left; width: 100%;
             font: inherit; padding: 15px 18px; border: 1px solid var(--line); border-radius: 9px;
             background: var(--surface); color: var(--text); cursor: pointer; }
.jb-choice:hover { border-color: var(--brand); background: var(--sel); }
.jb-choice .t { display: flex; align-items: center; gap: 9px;
               font-size: 17px; font-weight: 600; line-height: 1.3; }
.jb-choice .d { font-size: 14.5px; color: var(--muted); line-height: 1.45; }

/* The switch between views, in the top bar of both. Two words, always visible: a
   mode you cannot see you are in is worse than no mode at all.

   A recessed track with the active half raised out of it, rather than a pill filled with
   the primary-action colour. Three things follow from the bar it sits in. The radius is
   2px like every other control up there. The two halves differ in fill, weight AND colour
   rather than in fill alone, so the inactive one stops competing. And there is no brand fill,
   which leaves the bell's unread dot as the only bright mark in the bar -- it is the
   one element here whose whole job is to be noticed.

   The white alphas are literals, against rule 1 at the top of this file, and deliberately:
   `--bar` is dark in BOTH themes, which makes this the one region of the stylesheet where
   a white tint is not a theme-dependent value. The bar's existing hovers already take the
   same exception. */
.view-switch { display: inline-flex; align-items: center; gap: 8px; margin-right: 14px; }
.view-switch .seg-mini { display: inline-flex; gap: 2px; padding: 2px; border-radius: 3px;
                         background: rgba(255,255,255,.06); }
.view-switch button { font: inherit; font-size: 12px; font-weight: 500; padding: 4px 10px;
                      background: transparent; border: 0; border-radius: 2px;
                      color: var(--bar-dim); cursor: pointer; }
.view-switch button:hover { background: rgba(255,255,255,.05); color: var(--bar-text); }
.view-switch button.on { background: rgba(255,255,255,.13); color: var(--bar-text); font-weight: 600; }

/* The simple view's forms are one page rather than one question per screen, so their
   height follows their content -- and a region with forty flavors grew the modal to the
   88% cap while a region with four left it half that. A form that changes size as it is
   answered is unsettling, and at the cap it loses the margin that says it is a dialog.
   Capped lower, and in absolute terms too, so a large screen keeps its margins. */
.jb-modal.va-app { max-height: min(86%, 860px); }

/* The simple wizard: one question per screen, inside the shared drawer. */
.jb-modal-foot { display: flex; align-items: center; gap: 12px; flex: none;
                 padding: 18px 34px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.jb-dots { display: flex; align-items: center; gap: 10px; margin-bottom: 22px; }
.jb-dots > i { width: 10px; height: 10px; border-radius: 50%; background: var(--line);
                display: block; }
.jb-dots i.on { background: var(--brand); transform: scale(1.3); }
.jb-dots i.done { background: var(--brand); opacity: .4; }
.jb-dots b { font-size: 15px; font-weight: 500; color: var(--muted); margin-left: 8px; }
.jb-choice.on { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand);
                background: var(--sel); }
.jb-price { font-weight: 600; color: var(--brand); }
.jb-sentence { font-size: 18px; line-height: 1.6; letter-spacing: -.006em; max-width: 40ch; }
.jb-sentence mark { background: var(--sel); color: var(--text); font-weight: 600;
                    padding: 2px 6px; border-radius: 5px; }
.jb-cost { display: flex; align-items: baseline; gap: 10px; margin-top: 24px;
           padding: 18px 20px; background: var(--hover); border-radius: 9px; flex-wrap: wrap; }
.jb-cost .n { font-size: 30px; font-weight: 700; letter-spacing: -.02em;
              font-variant-numeric: tabular-nums; }
.jb-cost .u { font-size: 15.5px; color: var(--muted); max-width: 40ch; }

/* Step one: a family tab, then every image in it. Smaller targets than the rest of
   Jitterbug because these are a list to scan, not a decision to weigh -- but still
   comfortably above the 44px a fingertip needs. */
.jb-seg { margin-bottom: 16px; }
.jb-count { font-size: 12px; color: var(--muted); margin-left: 5px; }
.jb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px;
           max-height: 46vh; overflow-y: auto; }
.jb-pick { font: inherit; font-size: 15px; text-align: left; padding: 11px 14px; min-height: 44px;
           border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
           color: var(--text); cursor: pointer; }
.jb-pick:hover { border-color: var(--n4); background: var(--hover); }
.jb-pick.on { border-color: var(--brand); background: var(--sel); font-weight: 600; }

/* One machine, in plain words. */
.jb-detail-head { display: flex; gap: 14px; align-items: center; margin-bottom: 22px; }
.jb-facts { display: grid; grid-template-columns: minmax(120px, max-content) 1fr;
            gap: 14px 20px; margin: 0; font-size: 16.5px; }
.jb-facts > dt { color: var(--muted); font-size: 15px; }
.jb-facts > dd { margin: 0; overflow-wrap: anywhere; }
/* A caveat under a command: quieter than the command, still legible. */
.jb-aside { display: block; font-size: 13.5px; color: var(--muted); margin-top: 5px; }

/* ============================================================
   THE SIMPLE VIEW'S PRODUCT SKIN
   Everything here is prefixed --va- / .va- and lives under
   .va-app, which only Jitterbug renders.

   What is borrowed from an outside console is its STRUCTURE --
   the pane, the numbered step, the fieldset legend, the 2px
   selected border with a filled corner tick, the 8px card. Not
   its palette. A second accent makes the two views of one product
   look like two products, and the palette is the Manti board
   rather than an approximation of anybody's.

   So every token below DERIVES from the app's. That is not
   tidiness -- it is what makes the dark theme work without a
   second block, because app.css already redefines the tokens
   these point at under both `prefers-color-scheme` and
   `[data-theme]`. A literal here would silently stop tracking.
   ============================================================ */
.va-app {
  --va-accent: var(--brand); --va-accent-h: var(--brand-h); --va-accent-soft: var(--sel);
  --va-page: var(--app-bg); --va-card: var(--surface); --va-sunk: var(--n1);
  --va-line: var(--line); --va-line-soft: var(--line-sq);
  --va-ink: var(--text); --va-head: var(--text); --va-muted: var(--muted);
  --va-green: var(--ok); --va-amber: var(--warn); --va-red: var(--bad);
  --va-hover: var(--hover);
  /* The one measurement that is genuinely this view's own. The app's chrome is 2px, which
     is right for a dense table and wrong for a page of cards somebody reads at arm's
     length. */
  --va-r: 8px;
  font-family: var(--sans);
  color: var(--va-ink);
  background: var(--va-page);
}

/* The section a modal belongs to, named in type above its heading. */
.va-eyebrow { display: block; margin: 0 0 8px; font-size: 11px; font-weight: 600;
              letter-spacing: .08em; text-transform: uppercase; color: var(--va-muted); }

/* The page body inside the skin. */
.va-body { padding: 24px 22px 30px; max-width: 1180px; margin: 0 auto; }
.va-h1 { margin: 0 0 3px; font-size: 22px; font-weight: 700; letter-spacing: -.015em; }
.va-lede { margin: 0; font-size: 13.5px; color: var(--va-muted); line-height: 1.5; max-width: 62ch; }
.va-row { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.va-row > .va-right { margin-left: auto; }

/* The standing capacity strip. The accelerator count is the first thing on the page,
   because it is what the customer bought. */
.va-stats { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 24px; }
.va-stat { flex: 1 1 190px; background: var(--va-card); border: 1px solid var(--va-line);
           border-radius: var(--va-r); padding: 13px 15px; }
.va-stat > .k { font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
                color: var(--va-muted); margin-bottom: 6px; }
.va-stat > .v { font-size: 26px; font-weight: 700; letter-spacing: -.025em; line-height: 1.1; }
.va-stat > .v small { font-size: 13px; font-weight: 400; color: var(--va-muted); letter-spacing: 0; }
.va-stat > .s { font-size: 12px; color: var(--va-muted); margin-top: 4px; }
.va-stat.on { border-color: var(--va-accent); background: var(--va-accent-soft); }

/* A network, drawn as the box its machines sit inside. Containment shown rather than
   described is the only honest answer to "are these connected". */
.va-pool { border: 1px solid var(--va-line); border-radius: var(--va-r);
           background: var(--va-card); margin: 0 0 12px; padding: 0; }
/* padding: 0 14px on the legend, not 0 8px: a legend punches a hole in the border it sits
   on, and that padding IS the gap between the last pill and where the border resumes. At
   8px the line reads as an arrow leaving the last chip. */
.va-pool > legend { margin-left: 14px; padding: 0 14px; display: flex; align-items: center;
                    gap: 9px; flex-wrap: wrap; font-size: 12.5px; font-weight: 600;
                    color: var(--va-head); }
.va-pool > legend .cidr { font-family: var(--mono); font-size: 11px; color: var(--va-muted);
                          font-weight: 400; }
.va-pool-in { padding: 4px 15px 14px; }
.va-pool.sealed { background: var(--va-sunk); }

/* A shut group is a BAR, not an emptied fieldset. A <legend> cuts a hole in the top border
   and the box hangs below it, so a fieldset with its contents hidden renders as a notch
   with a border stub trailing off to the right. One row of information, one row of box. */
.va-pool.shut { background: var(--va-sunk); padding: 0; }
.va-pool-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; width: 100%;
               font: inherit; font-size: 12.5px; font-weight: 600; color: var(--va-head);
               background: none; border: 0; border-radius: var(--va-r); padding: 12px 15px;
               cursor: pointer; text-align: left; }
.va-pool-bar:hover { background: var(--va-hover); }
.va-pool-bar .chev { color: var(--va-muted); font-size: 10px; flex: none; }
.va-pool-bar .cidr { font-family: var(--mono); font-size: 11px; color: var(--va-muted);
                     font-weight: 400; }
/* The rollup goes to the far end, so every shut bar carries its counts in the same column
   however long the network's name is. */
.va-pool-bar > .va-rollup { margin-left: auto; }
.va-rollup { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* One machine. */
.va-vm { display: flex; align-items: center; gap: 12px; padding: 11px 2px; }
.va-vm + .va-vm { border-top: 1px solid var(--va-line-soft); }
.va-vm .nm { font-size: 14px; font-weight: 600; letter-spacing: -.005em; min-width: 0; }
/* Direct child: `.va-vm .nm small` would reach a <small> in any component nested here and
   force display on it, which is the shape tests/test_stylesheet.py exists to catch. */
.va-vm .nm > small { display: block; font-size: 11.5px; font-weight: 400;
                     color: var(--va-muted); margin-top: 1px; }
.va-vm > .va-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.va-vm .addr { font-family: var(--mono); font-size: 11.5px; color: var(--va-muted); }
.va-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--va-line); flex: none; }
.va-dot-ok { background: var(--va-green); }
.va-dot-warn { background: var(--va-amber); }
.va-dot-bad { background: var(--va-red); }

/* The accelerator as a mark rather than a word in a list, and the neutral pill beside it
   for facts that are not accelerators -- so an accelerator is the only blue thing in a row.
   Vendor names are set in type with this app's own chip glyph: NVIDIA's and AMD's marks are
   licensed assets and their brand colours would be literals in a palette of tokens. */
.va-gpu, .va-tag { display: inline-flex; align-items: center; gap: 5px; flex: none;
                   font-size: 11.5px; font-weight: 600; color: var(--va-accent);
                   background: var(--va-accent-soft); border: 1px solid transparent;
                   padding: 3px 9px; border-radius: 13px; white-space: nowrap; }
.va-tag { color: var(--va-muted); background: transparent; border-color: var(--va-line);
          font-weight: 400; }
/* `.q` and not `.x`: app.css sizes a global `.x` to 10x10 as an icon utility, and a
   quantity span wearing that class collapses to a 10px block. */
.va-gpu .q { font-variant-numeric: tabular-nums; }

/* A machine that sits on more than one network. It is genuinely in both boxes, so it is
   drawn in both -- and this tag is what stops the second sighting reading as a second
   machine. Quieter than the accelerator, and dashed, because it is a fact about topology
   rather than about what was bought. */
.va-also { display: inline-flex; align-items: center; gap: 5px; flex: none; font-size: 11px;
           font-weight: 500; color: var(--va-head); background: var(--va-sunk);
           border: 1px dashed var(--va-line); padding: 2px 8px; border-radius: 12px;
           white-space: nowrap; }

/* A machine a workload made. It belongs in the machine list -- leaving it out hid
   something somebody pays for from the place they go to count machines -- and this is what
   says where it came from, with the AI tile's glyph. */
.va-owned { display: inline-flex; align-items: center; gap: 6px; flex: none; font-size: 11px;
            font-weight: 500; color: var(--va-muted); background: var(--va-sunk);
            border: 1px solid var(--va-line); padding: 2px 8px; border-radius: 12px;
            white-space: nowrap; }
.va-owned > .ic { width: 11px; height: 11px; flex: none; }

/* What a network is for, said once at the foot of the group it applies to. */
.va-pool-note { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px;
                padding-top: 11px; border-top: 1px solid var(--va-line-soft);
                font-size: 12px; color: var(--va-muted); }
.va-pool-note > .va-right { margin-left: auto; }
/* A network with nothing in it is a real state -- "a network first" produces exactly one --
   and an empty box with no explanation reads as broken. */
.va-pool-empty { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
                 padding: 14px 2px 4px; font-size: 12.5px; color: var(--va-muted); }
.va-pool-empty > .va-right { margin-left: auto; }

/* Controls. */
.va-btn { font: inherit; font-size: 13px; font-weight: 600; line-height: 1; padding: 10px 16px;
          min-height: 38px; border-radius: 5px; cursor: pointer; border: 1px solid var(--va-line);
          background: var(--va-card); color: var(--va-ink); display: inline-flex;
          align-items: center; gap: 7px; }
.va-btn:hover { border-color: var(--va-muted); }
.va-btn-go { background: var(--va-accent); border-color: var(--va-accent); color: var(--on-brand); }
.va-btn-go:hover { background: var(--va-accent-h); border-color: var(--va-accent-h); }
.va-btn-sm { font-size: 12px; padding: 7px 12px; min-height: 32px; }
.va-btn[disabled] { background: var(--va-line); border-color: var(--va-line);
                    color: var(--va-muted); cursor: default; }
.va-link { font: inherit; font-size: 12.5px; font-weight: 600; background: none; border: 0;
           padding: 0; color: var(--va-accent); cursor: pointer; text-decoration: none; }
a.va-link:hover { text-decoration: underline; }

/* A segmented pill: a filled lozenge riding inside a rounded track. */
.va-seg { display: inline-flex; align-items: center; gap: 2px; padding: 4px;
          border: 1px solid var(--va-line); border-radius: 22px; background: var(--va-card); }
.va-seg > button { font: inherit; font-size: 13px; font-weight: 600; padding: 6px 15px;
                   min-height: 30px; border: 0; border-radius: 18px; background: none;
                   color: var(--va-head); cursor: pointer; }
.va-seg > button.on { background: var(--va-accent); color: var(--on-brand); }
.va-seg > button:hover:not(.on) { color: var(--va-ink); }

/* The finder, above the groups, once there are enough rows to need it. */
.va-find { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; margin: 0 0 16px; }
.va-find > input { font: inherit; font-size: 13px; padding: 9px 13px; min-height: 38px;
                   min-width: 260px; border: 1px solid var(--va-line); border-radius: 6px;
                   background: var(--va-card); color: var(--va-ink); }
.va-chip { font: inherit; font-size: 12px; font-weight: 600; padding: 7px 12px; min-height: 32px;
           border: 1px solid var(--va-line); border-radius: 16px; background: var(--va-card);
           color: var(--va-head); cursor: pointer; }
.va-chip.on { border-color: var(--va-accent); background: var(--va-accent-soft); color: var(--va-accent); }
.va-more { display: flex; align-items: center; gap: 8px; width: 100%; font: inherit;
           font-size: 12.5px; font-weight: 600; color: var(--va-accent); background: none;
           border: 0; padding: 10px 2px; cursor: pointer; }
.va-none { padding: 20px 2px; color: var(--va-muted); font-size: 13px; }

/* A numbered step, and the cards under it. */
.va-step { display: flex; align-items: center; gap: 11px; flex-wrap: wrap; margin: 26px 0 14px; }
.va-step > i { width: 24px; height: 24px; border-radius: 50%; flex: none; display: grid;
               place-items: center; border: 1px solid var(--va-line); font-style: normal;
               font-size: 12px; font-weight: 600; color: var(--va-head); }
.va-step > h2 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
.va-step > .sub { font-size: 12.5px; color: var(--va-muted); }

/* auto-FILL, not auto-fit. auto-fit collapses the empty tracks and lets the survivors
   stretch, so a group with one flavor in it drew a single card the full width of the row
   -- which reads as a banner rather than as one of a set, and makes two groups of
   different sizes look like two different components. auto-fill keeps the tracks, so a
   lone card is card-sized and the odd one out lines up with the rest. */
.va-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 12px; }
.va-picks.os { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }
/* A vendor's name over its own row of cards. Quiet: it labels a group rather than asking
   a question, and the numbered step above it is the thing being asked. */
.va-sub {
  margin: 14px 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--va-muted);
}
/* The one card in a step that makes something rather than choosing something. Dashed, so
   it reads as an action before it is read at all. */
.va-add { border-style: dashed; }
.va-add:hover { border-style: dashed; border-color: var(--va-accent); }
.va-add .t { display: flex; align-items: center; gap: 7px; color: var(--va-accent); }
.va-pick { position: relative; text-align: left; font: inherit; cursor: pointer;
           border: 1px solid var(--va-line); border-radius: var(--va-r); background: var(--va-card);
           padding: 13px 15px; display: flex; flex-direction: column; gap: 5px; color: inherit; }
.va-pick:hover { border-color: var(--va-muted); }
/* Selected is 2px against 1px, with the padding reduced to match, so the box does not move
   when it is chosen. */
.va-pick.on { border: 2px solid var(--va-accent); background: var(--va-accent-soft); padding: 12px 14px; }
.va-pick .t { font-size: 14px; font-weight: 700; letter-spacing: -.005em; }
.va-pick .d { font-size: 12px; color: var(--va-muted); line-height: 1.45; }
.va-pick .p { font-size: 12px; font-weight: 600; margin-top: 2px; }
.va-pick .why { font-size: 11.5px; color: var(--va-muted); font-style: italic; }
.va-pick .code { display: inline-block; vertical-align: 1px; margin-left: 5px;
                 font-family: var(--mono); font-size: 10.5px; font-weight: 400;
                 color: var(--va-muted); border: 1px solid var(--va-line);
                 border-radius: 3px; padding: 0 5px; }
.va-pick[disabled] { opacity: .55; cursor: default; background: var(--va-sunk); }
/* The tick sits INSIDE the card it marks.
 *
 * It used to overlap the corner at -9px with a ring punched out of the card colour, which
 * a borrowed shape and does not survive the move to this palette: a filled accent circle
 * half outside an accent-bordered card reads as a flag stuck on rather than as a state,
 * and on a card spanning its whole row it ends up marooned at the far right. Inside and
 * smaller, it reinforces the border and the tint instead of competing with them. */
.va-tick { position: absolute; top: 9px; right: 10px; width: 17px; height: 17px;
           border-radius: 50%; background: var(--va-accent); color: var(--on-brand); display: grid;
           place-items: center; font-size: 10px; line-height: 1; }
/* Room for it, so a long title does not run underneath. */
.va-pick > .t { padding-right: 24px; }
.va-flag { position: absolute; top: -9px; left: 13px; padding: 2px 8px; border-radius: 3px;
           background: var(--va-accent); color: var(--on-brand); font-size: 9.5px; font-weight: 700;
           letter-spacing: .09em; text-transform: uppercase; }
/* A count is one or two characters, so it gets a button sized for one or two characters. */
.va-counts { display: flex; gap: 10px; flex-wrap: wrap; }
.va-counts > .va-pick { min-width: 58px; padding: 12px 14px; align-items: center;
                        justify-content: center; }
.va-counts > .va-pick.on { padding: 11px 13px; }
.va-counts > .va-pick .t { font-size: 15px; padding-right: 0; }
/* No tick on a compact button. On a card it reinforces the border and the tint; on a
   button the width of one word it lands on top of that word, and the border and tint are
   already unmistakable at that size. */
.va-counts > .va-pick > .va-tick { display: none; }
/* One card per operating system, the version a menu inside it. */
.va-ver { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
.va-ver > label { font-size: 12px; color: var(--va-muted); }
/* max-width, because a <select> sizes itself to its widest option -- one long image name
   would otherwise widen its card and, through the grid, the whole row. */
.va-ver > select { font: inherit; font-size: 12.5px; padding: 5px 8px; border-radius: 5px;
                   border: 1px solid var(--va-line); background: var(--va-card);
                   color: var(--va-ink); max-width: 100%; min-width: 0; }
/* A number typed in rather than chosen from a list. Wide enough for four digits and the
   spinner Chrome puts beside them: sized like the select it stands in for, a number input
   collapses to about two characters and reads as broken. */
.va-ver > input[type="number"] { font: inherit; font-size: 12.5px; padding: 5px 8px;
                                 border-radius: 5px; border: 1px solid var(--va-line);
                                 background: var(--va-card); color: var(--va-ink);
                                 width: 6.5em; min-width: 0; }

/* The running total, under the form. */
.va-bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 26px;
          padding: 14px 16px; border-radius: var(--va-r); border: 1px solid var(--va-line);
          background: var(--va-card); }
.va-bar .sum { font-size: 13px; color: var(--va-muted); }
.va-bar .sum b { color: var(--va-ink); font-weight: 600; }
.va-bar .cost { font-size: 20px; font-weight: 700; letter-spacing: -.02em; }
.va-bar .cost small { font-size: 12px; font-weight: 400; color: var(--va-muted); letter-spacing: 0; }
.va-bar > .va-right { margin-left: auto; display: flex; align-items: baseline; gap: 9px; }
.va-hint { display: flex; gap: 9px; padding: 11px 13px; border-radius: 6px;
           background: var(--va-accent-soft); font-size: 12.5px; line-height: 1.5;
           color: var(--va-head); margin-top: 16px; }

/* A blueprint's own field. The control vocabulary is the generated one -- HOT declares the
   type, the label, the description and the constraints -- so this styles what already
   exists rather than describing anything new.

   Written for the mockup and then not shipped with the rest of the skin, which is why
   every label, control and hint in the blueprint form ran together on one line: an
   unstyled div is a block full of inline children. */
.va-field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 18px; max-width: 560px; }
.va-field > label { font-size: 13px; font-weight: 600; }
.va-field > .hint { font-size: 12px; color: var(--va-muted); line-height: 1.45; }
.va-field > label > .hint { font-weight: 400; }
.va-field > select, .va-field > input, .va-field > textarea {
  font: inherit; font-size: 13.5px; padding: 9px 12px; min-height: 40px;
  border: 1px solid var(--va-line); border-radius: 6px;
  background: var(--va-card); color: var(--va-ink); max-width: 100%;
}
.va-field > textarea { font-family: var(--mono); font-size: 12px; line-height: 1.6; }
.va-field > .unit { display: flex; align-items: center; gap: 9px; }
.va-field > .unit > input {
  flex: 0 1 130px; min-width: 0; font: inherit; font-size: 13.5px; padding: 9px 12px;
  min-height: 40px; border: 1px solid var(--va-line); border-radius: 6px;
  background: var(--va-card); color: var(--va-ink);
}
.va-field > .unit > span { font-size: 12.5px; color: var(--va-muted); }
/* A field with one correct answer, shown rather than asked. Same box as the control it
   replaces, so it reads as the answer to that question and sits in the form's rhythm; no
   fill and muted type, so nobody tries to type in it.

   Every token falls back, because this is used in both views now: the Essentials launch form
   and the main catalogue's, which have different palettes and different geometry. Inside
   `.va-app` it is that view's line, muted and radius; outside it is the dashboard's own, and
   the radius lands on 2px where the design system says everything does. Without the
   fallbacks the custom properties resolve to nothing outside `.va-app` and the border simply
   vanishes -- the failure AGENTS.md describes for a class styled under one component only. */
.va-derived { font-size: 13.5px; padding: 9px 12px; min-height: 40px; display: flex;
              align-items: center; border: 1px dashed var(--va-line, var(--line));
              border-radius: var(--va-r, 2px);
              color: var(--va-muted, var(--muted)); max-width: 100%; }

/* A caveat under a command or a fact: quieter than the thing it qualifies. */
.va-aside { display: block; font-size: 12px; color: var(--va-muted); margin-top: 5px;
            line-height: 1.5; }

/* A question that only exists once an earlier answer makes it relevant. */
.va-reveal { margin-top: 14px; padding-left: 13px; border-left: 2px solid var(--va-accent); }
.va-reveal > .lbl { font-size: 12.5px; font-weight: 600; color: var(--va-head); margin-bottom: 9px; }
/* Which doors are open, asked as doors. The security group is the answer, not the question. */
.va-ports { display: flex; gap: 10px; flex-wrap: wrap; }
.va-port { display: flex; align-items: center; gap: 9px; cursor: pointer;
           border: 1px solid var(--va-line); border-radius: 6px; padding: 10px 13px;
           background: var(--va-card); font-size: 13px; }
.va-port.on { border-color: var(--va-accent); background: var(--va-accent-soft); }
.va-port .box { width: 16px; height: 16px; border-radius: 3px; flex: none; display: grid;
                place-items: center; border: 1px solid var(--va-line); background: var(--va-card);
                font-size: 10px; color: transparent; }
.va-port.on .box { background: var(--va-accent); border-color: var(--va-accent); color: var(--on-brand); }
.va-port .pt { font-weight: 600; }
.va-port .pn { color: var(--va-muted); font-family: var(--mono); font-size: 11.5px; }

/* The opt-in "also let me SSH from where I am now" box, under the doors. A checkbox and
   its explanation, so the address and the caveat sit beside the tick rather than under it. */
.va-check { font-size: 13px; color: var(--va-ink); line-height: 1.45; }
.va-check input { margin-top: 2px; flex: none; }
.va-check:has(input:disabled) { cursor: not-allowed; }

/* One row per machine on the hand-off, because a batch is built one at a time and each has
   its own password (and, public, its own address). */
.va-made-table { font-size: 12.5px; }
.va-made-table th { font-weight: 600; color: var(--va-muted); border-bottom: 1px solid var(--va-line); }
.va-made-table td { border-bottom: 1px solid var(--va-line); vertical-align: top; }

/* The empty account. */
.va-hero { padding: 30px 2px 8px; max-width: 62ch; }
.va-hero > h1 { margin: 0 0 10px; font-size: 30px; font-weight: 700; letter-spacing: -.026em;
                line-height: 1.15; }
.va-hero > p { margin: 0; font-size: 15px; color: var(--va-muted); line-height: 1.55; }
.va-doors { display: grid; gap: 12px; margin-top: 24px; max-width: 560px; }
.va-door { position: relative; display: flex; align-items: center; gap: 14px; text-align: left;
           font: inherit; color: inherit; cursor: pointer; padding: 15px 17px;
           border: 1px solid var(--va-line); border-radius: var(--va-r); background: var(--va-card); }
.va-door.on { border: 2px solid var(--va-accent); padding: 14px 16px; }
.va-door:hover { border-color: var(--va-muted); }
/* `.bx` and not `.ic`: `.va-door .ic` would also match the <svg class="ic"> nested inside
   and blow the glyph up to the size of its own tile. */
.va-door .bx { width: 38px; height: 38px; border-radius: 8px; flex: none; display: grid;
               place-items: center; background: var(--va-accent-soft); color: var(--va-accent); }
.va-door .bx .ic { width: 17px; height: 17px; }
.va-door .t { display: block; font-size: 14.5px; font-weight: 700; }
.va-door .d { display: block; font-size: 12.5px; color: var(--va-muted); line-height: 1.45;
              margin-top: 3px; }
.va-door .go { margin-left: auto; color: var(--va-muted); font-size: 16px; }

/* A credential shown exactly once. The private key is generated locally and never stored,
   and Compute returns the password once, so neither can go in a notice that fades. */
.va-once { border: 1px solid var(--va-amber); border-left: 3px solid var(--va-amber);
           border-radius: var(--va-r); background: var(--va-card); padding: 14px 16px;
           margin-top: 14px; }
.va-once > .hd { display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
                 font-size: 13.5px; font-weight: 700; }
.va-once > .hd > .va-right { margin-left: auto; }
.va-once .val { display: flex; align-items: center; gap: 10px; margin: 10px 0 0;
                font-family: var(--mono); font-size: 12px; word-break: break-all;
                background: var(--va-sunk); border: 1px solid var(--va-line-soft);
                border-radius: 5px; padding: 9px 11px; }
.va-once .val > .k { flex: 1; min-width: 0; color: var(--va-ink); }
.va-once .why { display: block; margin-top: 9px; font-size: 12px; color: var(--va-muted);
                line-height: 1.5; }
.va-once .why b { color: var(--va-ink); }
/* white-space: pre is load-bearing -- a <code> block is not a <pre>, so two commands written
   on two lines render as one run and read as a single nonsense command. */
.va-cmd { display: block; margin-top: 14px; font-family: var(--mono); font-size: 12px;
          white-space: pre; line-height: 1.7; background: var(--va-sunk);
          border: 1px solid var(--va-line-soft); border-radius: 5px; padding: 10px 12px;
          color: var(--va-ink); overflow-x: auto; }
/* Inside a pane row it is the row, so it does not need the gap that separates it from
   prose above it elsewhere. */
.va-pane-row > .va-cmd { margin-top: 0; width: 100%; }

/* A machine that came with a deployment, said on the card somebody has open when they go
   looking for the way to remove it. Loud enough not to be missed, and not an error: it is
   a fact about what this machine belongs to, not a thing that went wrong. */
.va-owned-note { display: flex; align-items: flex-start; gap: 12px; margin-top: 18px;
                 padding: 13px 15px; border: 1px solid var(--va-line);
                 border-radius: var(--va-r); background: var(--va-sunk);
                 font-size: 12.5px; line-height: 1.5; }
.va-owned-note > .ic { width: 14px; height: 14px; flex: none; margin-top: 2px; }
.va-owned-note > button { margin-left: auto; flex: none; }

/* The landing as a launcher: one tile per panel, the way Launchpad draws apps.
   Auto-fill, not a fixed four: the grid is what the page was asked to render, so a sixth
   tile is a sixth tile and nothing here changes. This replaced .va-quad, which was
   deliberately two-by-two because four was a fixed number -- and stopped being one the
   moment a person could add DNS to the page. */
.va-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 16px; margin-top: 26px; }

/* A tile looks like a card, lifts on hover, and carries three things: what the panel is,
   how much of it there is, and whether any of it needs attention. Rows do not belong on
   it -- that is what the panel is for. */
.va-tile { position: relative; display: flex; flex-direction: column; gap: 14px;
  min-height: 196px; padding: 22px 20px 18px; color: inherit; text-decoration: none;
  background: var(--va-card); border: 1px solid var(--va-line); border-radius: 14px;
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; }
.va-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow);
  border-color: var(--va-muted); }
.va-tile:active { transform: translateY(0); }

/* The title is the link and the link covers the card, so the ring belongs to the tile
   rather than to the word inside it. An anchor cannot contain the arrows -- interactive
   content inside a link is invalid and unreachable by keyboard -- which is why the card
   is a plain element with one stretched link in it rather than one big <a>. */
.va-tile > h2 > a, .va-tile > h2 > button { font: inherit; color: inherit; background: none;
  border: 0; padding: 0; text-align: left; text-decoration: none; cursor: pointer; }
.va-tile > h2 > a::after, .va-tile > h2 > button::after { content: ""; position: absolute;
  inset: 0; border-radius: inherit; }
/* `:has(:focus-visible)` and not `:focus-within`: the ring is a focus ring, and the
   browser's own rule for when one is worth drawing is what :focus-visible answers.
   :focus-within cannot ask -- it fires on the focus a mouse click leaves behind, so
   clicking a tile ringed it in the accent and, through the rule further down, opened the
   arrows on it. That reads as rearranging, which is not what a click asked for. */
.va-tile:has(:focus-visible) { outline: 2px solid var(--va-accent); outline-offset: 2px; }

/* The glyph well. A rounded square like an app icon, on the sunk tone so it reads as an
   icon and not as a second card. */
.va-tile > .glyph { width: 54px; height: 54px; border-radius: 15px; display: grid;
  place-items: center; background: var(--va-sunk); border: 1px solid var(--va-line-soft); }
.va-tile > .glyph > .ic { width: 26px; height: 26px; color: var(--va-accent); }
.va-tile > h2 { margin: 0; font-size: 15.5px; font-weight: 700; letter-spacing: -.01em; }
.va-tile > h2 > small { display: block; margin-top: 3px; font-size: 12.5px; font-weight: 400;
  color: var(--va-muted); line-height: 1.45; }
.va-tile > .st { margin-top: auto; display: flex; align-items: center; gap: 7px;
  font-size: 12px; color: var(--va-muted); }
.va-tile > .st > .va-right { margin-left: auto; }
.va-tile > .st > .va-right > .ic { width: 14px; height: 14px; color: var(--va-muted); }

/* A panel whose service this region does not offer keeps its tile, dimmed, and says so.
   Dropping it would leave people wondering where the AI went. */
.va-tile.off { opacity: .6; }
.va-tile.off:hover { transform: none; box-shadow: none; border-color: var(--va-line); }

/* The bar over the grid: one button, and the hint that appears while rearranging. */
.va-tiles-bar { display: flex; align-items: center; gap: 12px; margin-top: 22px;
  min-height: 32px; }
.va-tiles-bar > .va-right { margin-left: auto; }
.va-tiles-hint { font-size: 12.5px; color: var(--va-muted); }
.va-tiles-bar + .va-tiles { margin-top: 10px; }

/* The one tile that is not a panel. Dashed, on the page tone rather than the card tone, so
   it reads as a place for a tile rather than as a tile. Always last. */
.va-tile-add { font: inherit; text-align: left; cursor: pointer; background: transparent;
  border-style: dashed; color: var(--va-muted); }
.va-tile-add > .glyph { background: transparent; border-style: dashed; }
.va-tile-add > .glyph > .ic { color: var(--va-muted); }
.va-tile-add > h2 { color: var(--va-ink); }
.va-tile-add:hover { background: var(--va-card); color: var(--va-ink); }
.va-tile-add:hover > .glyph > .ic { color: var(--va-accent); }

/* Rearranging.
   The tools sit in the tile's top-right, so a tile keeps its shape whether or not they are
   showing, and above the stretched link so they are clickable at all.
   Three ways to reach them, and each is somebody's only way. The .arrange mode is the
   mouse's, revealed by a button. Keyboard focus is the keyboard's, and it is also the
   no-script path -- these are plain forms, so tabbing into a tile has to be enough on its
   own. Hover reveals the cross alone, so a tile can be taken off without entering a mode;
   the four essentials never get one.

   Keyboard focus, and specifically not :focus-within, which is also what a mouse click
   leaves behind: clicking a tile opened its arrows as if Rearrange had been pressed. */
.va-tile > .tools { position: absolute; top: 10px; right: 10px; z-index: 1;
  display: flex; gap: 4px; margin: 0; }
.va-tile > .tools > button { display: none; width: 26px; height: 26px; place-items: center;
  padding: 0; border: 1px solid var(--va-line); border-radius: 7px; background: var(--va-card);
  color: var(--va-muted); cursor: pointer; }
.va-tiles.arrange .va-tile > .tools > button,
.va-tile:has(:focus-visible) > .tools > button { display: grid; }
.va-tile:hover > .tools > .rm { display: grid; }
.va-tile > .tools > button:hover { color: var(--va-ink); border-color: var(--va-muted); }
.va-tile > .tools > button[disabled] { color: var(--va-line); cursor: default; }
.va-tile > .tools > button[disabled]:hover { color: var(--va-line);
  border-color: var(--va-line); }
.va-tile > .tools > button > .ic { width: 12px; height: 12px; }
.va-tile > .tools > .mv[value="left"] > .ic { transform: rotate(180deg); }
.va-tile > .tools > .rm:hover { color: var(--va-red); border-color: var(--va-red); }
.va-tiles.arrange .va-tile:hover { transform: none; box-shadow: none; }

/* Dragging. The tile being carried fades rather than moves: the grid is not rearranged
   until the server answers, so the only thing shown is where it would land. The marker is
   a shadow on the edge it would go in on -- a line between two cards rather than a gap
   opening, which would move every tile after it and make the target a moving one. */
.va-tile[draggable="true"] { cursor: grab; }
.va-tile.dragging { opacity: .4; cursor: grabbing; }
.va-tile.dragging:hover { transform: none; box-shadow: none; }
.va-tile.drop-before { box-shadow: -3px 0 0 0 var(--va-accent); }
.va-tile.drop-after { box-shadow: 3px 0 0 0 var(--va-accent); }

/* Quota: a pill in the page's top-right, and a modal behind it.
   The dot is uncoloured until a limit is near or reached, so on most days the pill is
   just a word. A row of gauges that are always fine teaches people to stop reading
   gauges, which is the failure this shape exists to avoid. */
.va-quota-pill { display: inline-flex; align-items: center; gap: 7px; font: inherit;
  font-size: 12px; font-weight: 600; padding: 6px 11px 6px 9px; color: var(--va-muted);
  background: var(--va-card); border: 1px solid var(--va-line); border-radius: 999px;
  cursor: pointer; }
.va-quota-pill:hover { color: var(--va-ink); border-color: var(--va-muted); }
.va-quota-pill > .ic { width: 14px; height: 14px; }
.va-quota-pill > .va-dot { width: 7px; height: 7px; }
/* The dot with no tone class is the common case, and it is not drawn at all. A grey dot
   is still a dot, and reads as a state rather than as the absence of one. */
.va-quota-pill > .va-dot:not([class*="va-dot-"]) { display: none; }

.va-qlist { display: flex; flex-direction: column; margin-top: 20px; }
.va-q { display: grid; grid-template-columns: 150px 1fr 130px; align-items: center;
  gap: 14px; padding: 11px 0; font-size: 13px; }
.va-q + .va-q { border-top: 1px solid var(--va-line-soft); }
.va-q > .k { font-weight: 600; }
.va-q > .bar { height: 6px; border-radius: 3px; background: var(--va-line-soft); overflow: hidden; }
.va-q > .bar > i { display: block; height: 100%; background: var(--va-muted); border-radius: 3px; }
.va-q > .v { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.va-q > .v > small { font-weight: 400; color: var(--va-muted); }
/* Spanning both columns under the row it belongs to, so the sentence sits with its
   number rather than in a column that would squeeze every other row to fit it. */
.va-q > .s { grid-column: 1 / -1; margin-top: -4px; font-size: 12px; color: var(--va-muted); }
/* No limit, so no track: an empty track is read as nought percent. */
.va-q.free > .bar { background: none; }
.va-q.warn > .bar > i { background: var(--va-amber); }
.va-q.warn > .v, .va-q.warn > .s { color: var(--va-amber); }
.va-q.bad > .bar > i { background: var(--va-red); }
.va-q.bad > .v, .va-q.bad > .s { color: var(--va-red); }
.va-q-unread { margin-top: 16px; }

/* The chooser's rows carry a glyph before the title, which .jb-choice did not need. */
.jb-tiles .jb-modal-body { flex: 1; min-height: 0; overflow: auto; }
.jb-tiles .jb-modal-foot > .va-right { margin-left: auto; }
.jb-choice .t > .ic { width: 18px; height: 18px; color: var(--va-accent); flex: none; }
.jb-choice .t > .va-tag { margin-left: 4px; font-size: 11px; }
.jb-choice[disabled] { opacity: .55; cursor: default; }
.jb-choice[disabled]:hover { border-color: var(--line); background: var(--surface); }

/* The global reduced-motion reset kills the transition; the transform it was easing has to
   go with it, or the lift lands in one frame instead of not happening. */
@media (prefers-reduced-motion: reduce) {
  .va-tile:hover { transform: none; }
}

.va-pane { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--va-line);
           border-radius: var(--va-r); background: var(--va-card); }
.va-pane > header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
                    padding: 13px 15px; border-bottom: 1px solid var(--va-line-soft); }
.va-pane > header > h2 { margin: 0; font-size: 14px; font-weight: 700; letter-spacing: -.01em; }
.va-pane > header > .n { font-size: 12px; color: var(--va-muted);
                         font-variant-numeric: tabular-nums; }
.va-pane > header > .va-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* The body carries rows, not a number. A pane showing only a count would hide three
   machines behind a click, which is worse than the list it replaced. */
.va-pane-in { padding: 4px 15px 6px; flex: 1; }
.va-pane-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; font-size: 13px; }
.va-pane-row + .va-pane-row { border-top: 1px solid var(--va-line-soft); }
.va-pane-row > .nm { font-weight: 600; letter-spacing: -.005em; min-width: 0; }
.va-pane-row > .nm > small { display: block; font-size: 11.5px; font-weight: 400;
                             color: var(--va-muted); margin-top: 1px; }
/* A value that is somewhere you can go. Muted like the rest of the line until it is
   hovered, so a row of outputs does not become a row of accent-coloured text -- but
   underlined always, because that is what says it is a link at all. */
.va-pane-row > .nm > small a { color: inherit; text-decoration: underline;
                               text-underline-offset: 2px; }
.va-pane-row > .nm > small a:hover { color: var(--va-accent); }
.va-pane-row > .va-right { margin-left: auto; display: flex; align-items: center; gap: 9px; }
.va-pane-row .qty { font-size: 11.5px; color: var(--va-muted);
                    font-variant-numeric: tabular-nums; white-space: nowrap; }
.va-pane-foot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
                padding: 10px 15px 13px; border-top: 1px solid var(--va-line-soft);
                font-size: 12px; color: var(--va-muted); }
.va-pane-foot > .va-right { margin-left: auto; }
/* A pane with nothing in it yet. Every one of the four can be empty on day one, and an
   empty box with no words is the shape people read as broken. */
.va-pane-none { padding: 16px 0 14px; font-size: 12.5px; color: var(--va-muted); line-height: 1.5; }

/* The product's own sentence over the four, rather than a new one. */
.va-lede-big { margin: 0 0 3px; font-size: 24px; font-weight: 700; letter-spacing: -.02em; }
.va-lede-big b { color: var(--va-accent); }

/* The way back from a section to the landing. A crumb rather than one arrow-and-word: the
   landing is "Essentials" and a section sits one level under it, so this can grow a third
   level -- a machine's own page -- without a second pattern being invented for it. A
   section you can reach and not leave is the trap the view switch already exists to
   avoid, so the link is always in the same place. */
.va-crumbs { display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
             font-size: 12.5px; }
.va-crumbs > a { display: inline-flex; align-items: center; gap: 6px; font-weight: 600;
                 color: var(--va-muted); text-decoration: none; }
.va-crumbs > a:hover { color: var(--va-accent); text-decoration: underline; }
.va-crumbs > a > .ic { width: 13px; height: 13px; }
.va-crumbs > a > .ic.back { transform: rotate(180deg); }
.va-crumbs > .sep { color: var(--va-line); font-size: 14px; }
.va-crumbs > b { font-weight: 600; color: var(--va-ink); }

/* The way back to the full dashboard. */
.va-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 26px;
           padding-top: 16px; border-top: 1px solid var(--va-line); font-size: 12px;
           color: var(--va-muted); }
.va-foot > .va-right { margin-left: auto; }

/* ============================================================
   LOAD-BALANCER CASCADE TREE
   A load balancer is a graph -- one VIP, its frontends (listeners)
   and its backends (pools of members a monitor watches) -- so the
   drawer shows it as that graph, front to back, with the wiring
   between them made explicit rather than split across tabs. The one
   component the rest of the app does not already have; built from the
   same tokens, the 2px radius, 1px --line rules, the inset-3px active
   marker, and the status pills. Members reuse the picker (.pick-*).
   ============================================================ */
.lbtree { margin: 2px 0 0; }
.lb-band {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 18px 0 8px; display: flex; align-items: center; gap: 8px;
}
.lb-band:first-child { margin-top: 4px; }
.lb-band .n { font-family: var(--mono); font-size: 10px; letter-spacing: 0; color: var(--n4); font-weight: 400; }
.lb-band .sec-spacer { margin-left: auto; }

/* The VIP header -- the front door. */
.lb-vip {
  display: flex; align-items: center; gap: 12px; padding: 11px 12px;
  border: 1px solid var(--line); border-radius: 2px; background: var(--n1);
}
.lb-vip .vic { width: 20px; height: 20px; color: var(--brand); flex: none; }
.lb-vip .addr { font-family: var(--mono); font-size: 13.5px; font-weight: 600; color: var(--text); }
.lb-vip .roll { margin-left: auto; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }

/* A node -- a listener or a pool. */
.lb-card { border: 1px solid var(--line); border-radius: 2px; background: var(--surface); margin-bottom: -1px; }
.lb-node { display: grid; grid-template-columns: 20px 1fr auto; gap: 12px; align-items: center; padding: 9px 12px; }
.lb-node .nic { width: 17px; height: 17px; color: var(--muted); flex: none; justify-self: center; }
.lb-node.hot { box-shadow: inset 3px 0 0 var(--brand); background: var(--sel); }
.lb-main { min-width: 0; }
.lb-line1 { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.lb-title { font-size: 13px; font-weight: 600; color: var(--text); }
.lb-proto { font-family: var(--mono); font-size: 11.5px; color: var(--text); background: var(--n2); border-radius: 2px; padding: 1px 6px; }
.lb-meta { font-size: 11.5px; color: var(--muted); margin-top: 3px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.lb-meta .mono { font-size: 11px; }
.lb-right { display: flex; align-items: center; gap: 9px; justify-self: end; }
.lb-count { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lb-count b { color: var(--text); font-weight: 600; }
.lb-count.warn b { color: var(--warn); } .lb-count.bad b { color: var(--bad); }

/* The wire from a listener to the pool it feeds -- what a tabbed view hides. */
.lb-wire { display: flex; align-items: center; gap: 7px; padding: 7px 12px 8px 44px; border-top: 1px solid var(--line-sq); font-size: 12px; flex-wrap: wrap; }
.lb-wire .arr { color: var(--n4); }
.lb-wire .rule { color: var(--muted); }
.lb-wire .rule b { color: var(--text); font-weight: 600; font-family: var(--mono); font-size: 11.5px; }
.lb-ref { font: inherit; font-size: 12px; font-weight: 600; color: var(--brand); background: var(--sel); border: 1px solid transparent; border-radius: 2px; padding: 1px 8px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; }
.lb-ref:hover, .lb-ref.lit { border-color: var(--brand); }
.lb-ref .ic { width: 11px; height: 11px; }
.lb-tls { font-size: 11px; color: var(--info); display: inline-flex; align-items: center; gap: 4px; background: none; border: 0; font-family: inherit; cursor: pointer; padding: 0; }
.lb-tls:hover { text-decoration: underline; } .lb-tls .ic { width: 11px; height: 11px; }

/* Members and the monitor sit inside their pool card. Members reuse the picker frame. */
.lb-kids { border-top: 1px solid var(--line-sq); padding: 3px 0; background: var(--n1); }
.lb-mem { display: grid; grid-template-columns: 20px 1fr auto; gap: 12px; align-items: center; padding: 5px 12px; font-size: 12px; }
.lb-mem .mdot { width: 8px; height: 8px; border-radius: 50%; justify-self: center; background: var(--n4); }
.lb-mem .mdot.ok { background: var(--ok); } .lb-mem .mdot.bad { background: var(--bad); } .lb-mem .mdot.warn { background: var(--warn); }
.lb-mem .maddr { font-family: var(--mono); font-size: 11.5px; color: var(--text); }
.lb-mem .mname { color: var(--muted); margin-left: 8px; font-family: var(--sans); font-size: 11.5px; }
.lb-mem .mright { justify-self: end; display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; }
.lb-mon { display: grid; grid-template-columns: 20px 1fr auto; gap: 12px; align-items: center; padding: 7px 12px; font-size: 12px; border-top: 1px solid var(--line-sq); margin-top: 2px; }
.lb-mon .hic { width: 15px; height: 15px; justify-self: center; color: var(--ok); }
.lb-mon.none .hic { color: var(--n4); }
.lb-mon .mono { font-size: 11px; }
.lb-nomon { color: var(--warn); font-weight: 600; }

/* ============================================================
   SPREAD RING
   How a set of values divides, for a part-to-whole with a
   handful of parts. Shared: firmware across a fleet is the
   first caller, volumes by type and addresses in use are the
   obvious next ones.

   The slices are STATES, not identities, so the baseline is a
   neutral rather than a hue -- colouring a majority green
   claims it is the correct value, which the data behind these
   rings does not support. The two slices that carry meaning
   were stepped apart with a palette validator, not by eye: the
   neutral and the warn tone separate well past the readability
   floor for normal vision and under simulated colour blindness
   in both themes. The neutral sits under 3:1 against the
   surface, which obliges relief, so every slice is labelled
   with its value and count in the key beside it.
   ============================================================ */
.rings { display: flex; gap: 26px; flex-wrap: wrap; }
.ring { display: flex; align-items: center; gap: 12px; margin: 0; }
.ring > svg { width: 76px; height: 76px; flex: none; }
.ring .seg-base { stroke: var(--n4); }
.ring .seg-off { stroke: var(--warn); }
.ring .hatch-bg { fill: var(--n2); }
.ring .hatch-line { stroke: var(--n4); stroke-width: 2.5; }
.ring-name { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .11em; color: var(--muted); margin-bottom: 4px; }
.key { list-style: none; margin: 0; padding: 0; }
.key > li { display: flex; align-items: center; gap: 7px; font-size: 11.5px; padding: 1px 0; }
.key > li > b { font-weight: 400; color: var(--text); }
.key > li > em { font-style: normal; color: var(--muted); font-variant-numeric: tabular-nums; }
.sw { width: 9px; height: 9px; flex: none; border-radius: 2px; }
.sw-base { background: var(--n4); }
.sw-off { background: var(--warn); }
/* An absence, drawn as an absence: hatched rather than a third weak grey, which would
   read as a third quantity. */
.sw-none { background: repeating-linear-gradient(45deg, var(--n2) 0 2px, var(--n4) 2px 3.5px); }

/* Where a section's facts came from, under its heading. Anywhere a view mixes a record
   kept current with a cached snapshot, an undated fact reads as live. */
.src { margin: -4px 0 8px; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; }

/* ============================================================
   RUNBOOK CARD
   A named procedure, its steps, and what running it costs --
   stated above the button rather than in a confirm dialog
   people learn to click through.
   ============================================================ */
.rb { border: 1px solid var(--line); background: var(--surface); margin-bottom: 10px; }
.rb-destructive { border-left: 3px solid var(--bad); }
.rb-head { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px 8px; }
.rb-head > div { flex: 1; min-width: 0; }
.rb-name { font-size: 12.5px; font-weight: 600; }
.rb-head .sub { font-size: 11.5px; color: var(--muted); }
.rb-steps { margin: 0; padding: 0 12px 4px 30px; }
.rb-steps li { padding: 3px 0; font-size: 12.5px; }
.rb-steps .sub { display: block; font-size: 11.5px; color: var(--muted); }
.rb-foot { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-top: 1px solid var(--line-sq); background: var(--app-bg); }
.rb-foot .hint { flex: 1; margin: 0; }

/* A firmware version is data, not status: monospace like every other identifier, with
   the one that disagrees marked in the warn tone rather than given a pill, which would
   read as a fault rather than as a difference. */
.fw { font-family: var(--mono); font-size: 12px; }
.fw-off { color: var(--warn); font-weight: 600; }
.fw-none { color: var(--muted); font-style: italic; }

/* Two views over one set of nodes. The list is one flat table; the fleet is one table
   per group, because a group's columns are what that kind of machine reports and a
   column another group needs is not a gap here. The wrapper carries the mode.

   It also has to keep the flex chain intact -- .main gives its scrolling child the
   height, and a plain div between them would collapse it. */
.views { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.views[data-view="list"] .node-fleet { display: none; }
.views[data-view="fleet"] .node-list { display: none; }
.seg-view { margin-bottom: 0; width: 176px; }
.fleet-band { padding: 12px 16px 14px; }
.fleet-band .hint { max-width: 78ch; }
.search-scope { flex: none; padding: 6px 16px 0; margin: 0; }
.group-by { display: flex; align-items: center; gap: 6px; font-size: 11.5px;
  color: var(--muted); white-space: nowrap; }
.group-by select { margin: 0; }

/* One group of like machines. Collapsed, these lines are the whole fleet summary --
   sorted worst first, one per group -- so they have to scan as columns rather than as
   ragged sentences, which is what the min-widths are for. Only one is ever open: the
   shared table.js works on a single table, and a second open group would be a second
   table asking to be it. */
.gsec { border-bottom: 1px solid var(--line); }
.gsec-head {
  display: flex; align-items: center; gap: 10px; width: 100%; cursor: pointer;
  font: inherit; text-align: left; padding: 10px 16px; background: transparent;
  border: 0; color: var(--text);
}
.gsec-head:hover { background: var(--hover); }
.gsec-head:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.gsec-head > .car { width: 12px; height: 12px; color: var(--muted); flex: none;
  transition: transform .12s ease; }
.gsec.open > .gsec-head > .car { transform: rotate(180deg); }
.gsec-body { padding-bottom: 10px; }
.gc-name { font-size: 12px; font-weight: 600; min-width: 11em; }
.gc-sub { font-size: 11px; color: var(--muted); min-width: 8em; }
.gc-n { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums;
  min-width: 5.5em; }
.gc-state { font-size: 11px; padding: 1px 6px; border-radius: 2px; white-space: nowrap; }
.gc-off { background: var(--warn-bg); color: var(--warn); font-weight: 600; }
.gc-ok { background: var(--ok-bg); color: var(--ok); }
.gc-quiet { background: var(--n2); color: var(--muted); }
.gnote { padding: 12px 16px 0; margin: 0; max-width: 78ch; }
@media (prefers-reduced-motion: reduce) { .gsec-head > .car { transition: none; } }

/* ============================================================
   BIOS SETTINGS
   A form, not a table, and it filters rather than pages: a real
   registry runs to a couple of hundred entries, and the ten-row
   pager everything else uses would page an edit away from the
   person who made it.
   ============================================================ */
.bios-tools { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.bios-tools > .search { flex: 1; }
.bios { border: 1px solid var(--line); max-height: 320px; overflow-y: auto; }
.bios-row {
  display: grid; grid-template-columns: 1fr 170px; gap: 12px; align-items: center;
  padding: 7px 12px; border-bottom: 1px solid var(--line-sq); background: var(--surface);
}
.bios-row:last-child { border-bottom: 0; }
/* Marked the moment a control moves, so an edit made among two hundred rows is findable
   again after scrolling past it. The server still decides what actually differs. */
.bios-row.changed { background: var(--sel); box-shadow: inset 3px 0 0 var(--brand); }
.bios-row.locked { background: var(--n1); }
.bios-name > .mono { font-size: 12.5px; }
.bios-name > .sub { display: block; font-size: 11px; color: var(--muted); }
.bios-row > select, .bios-row > input { width: 100%; }
.bios-fixed { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
.bios-fixed > .ic { width: 12px; height: 12px; }
.bios-foot { display: flex; align-items: center; gap: 12px; margin-top: 10px; }
.bios-foot > .hint { flex: 1; margin: 0; }

/* ============================================================
   APPLICATION CREDENTIALS
   An expiry somebody picks, and the rules a credential may use.
   ============================================================ */

/* The custom expiry row: a date, a time, and the zone they are in. A bare date input is
   a block and stretches to the full width of the field, which puts a 90px value in a
   488px box and pushes the clock onto the line below. Both hold a value of known length
   and neither gains anything from the slack. */
.when { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.when input[type=date] { width: 148px; }
.when input[type=time] { width: 104px; }

/* A row of chips that is not the toolbar -- the access-rule presets, and anywhere else a
   set of quick actions sits above a form. `.toolbar` supplies its own gap, so chips
   outside one have no spacing rule at all. */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }

/* A path inside the rules repeater. `.tbl-form input[type=text]` sets the sans face for
   every field in every repeater, which is right for a property name and wrong for
   `/v2.1/servers/*/action`: the wildcards and the segment boundaries are what is being
   read, and proportional type is where a stray space hides. */
.tbl-form input.mono { font-family: var(--mono); font-size: 11.5px; }

/* The Expires cell: a pill with the absolute timestamp under it. `.tbl tbody td` fixes
   the row height and forbids wrapping, so the second line needs both released -- and the
   selector has to be `tbody td` to outrank app.css's own (0,1,2) rule above. */
.tbl tbody td.exp-cell {
  height: auto; white-space: nowrap; padding-top: 6px; padding-bottom: 6px;
}
.exp-cell .dd-sub { font-variant-numeric: tabular-nums; }

/* ============================================================
   VPN
   Two ends and the wire between them, plus the three small
   rules the panel needs and the sheet did not have.
   ============================================================ */

/* A connection has no single object and therefore no single id, so its drawer cannot open
   with the usual name and monospace uuid. It opens with the picture instead, and the
   picture answers the only question anybody arrives with: what is joined to what.

   Side by side rather than stacked, which is the opposite of `.pw` in the Ports drawer.
   Ports stacks because its three cells are a chain in one place; two ends are a symmetry,
   and drawing them one above the other would say that one contains the other. 1fr auto 1fr
   keeps them the same width whatever their names, because neither end is the important
   one. */
.span { display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; margin: 0 0 18px; }
.span-end { border: 1px solid var(--line); background: var(--surface); padding: 8px 10px; min-width: 0; }
/* The end you are looking from. Switch region and the mark moves to the other cell. */
.span-end.self { border-left: 3px solid var(--brand); }
.span-k {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 4px;
}
.span-v { font-size: 12.5px; font-weight: 600; color: var(--text); margin: 0; }
.span-s { font-size: 11.5px; color: var(--muted); margin: 2px 0 0; overflow-wrap: anywhere; }
/* The wire: a rule through the middle of the gutter with the state written on it, the same
   trick `.pw-link em` plays vertically. Dashed when the tunnel is not up, so a broken link
   is legible in a screenshot with no colour. */
.span-wire { position: relative; width: 34px; }
.span-wire::before { content: ""; position: absolute; top: 50%; left: 0; right: 0; border-top: 1px solid var(--n4); }
.span-wire.off::before { border-top-style: dashed; }
.span-wire em {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: var(--app-bg); padding: 0 3px;
  font-style: normal; font-size: 10px; color: var(--muted); white-space: nowrap;
}

/* A check that passed with something worth saying. `.run` colours exactly two outcomes,
   done and failed, because a launch either ran a step or stopped at it. A preflight has a
   third: nothing is wrong, and you should still read this line. Without it an advisory has
   to be drawn as a failure, which makes the button that stays live look like a bug. */
.run-warn .run-mark { color: var(--warn); }

/* Two fields on one line. `networking/_rule_form.html` has put a `.row2` around its port
   pair since before this panel existed and nothing has ever defined it, which
   docs/design-system.md records under Known drift. This clears that entry and is also what
   the VPN wizard's two columns need, so the rule earns itself twice. */
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* A heading inside a `.pick-list`, naming the group of rows under it. It heads a
   `.pick-group`, which is where the name comes from -- and not `.pick-head`, which the
   picker macro has owned since long before this existed and uses for the search bar at
   the top of a list. Two different things: one is a search field, this is a label over a
   set of rows. Sharing the name meant both rule sets applied to both, in five panels.

   Deliberately not the uppercase micro-label the rest of the app uses for section labels:
   this one carries a router's name, which is an identifier somebody will compare against a
   CLI, and uppercasing an identifier makes it a different string on screen from the one in
   the API. It also has to hold a `.sec-act`, and `text-transform` inherits into one. */
.pick-group-head {
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 12px; background: var(--n2); border-bottom: 1px solid var(--line);
  font-size: 11.5px; font-weight: 600; color: var(--text);
}
.pick-group-head .sub { font-weight: 400; }
.pick-group-head .sec-act { margin-left: auto; }

/* The checkbox variant of a pick row is styled once, with the picker itself -- see
   `.pick > input[type=checkbox]` up in the PICKER section. It was re-solved here, which
   is the shape `test_stylesheet.py` now refuses. */

/* The "all of this router" tick on a picker group head, ahead of the name so a column of
   them reads as one control down the left edge.

   `align-self` because the head is `align-items: baseline`, which is right for the name
   and the `.sub` beside it and wrong for a checkbox: a box has no text baseline, so the
   browser aligns its bottom edge and it sits low against the name. Centred against the
   line instead, which is the same fix `.pick > input[type=checkbox]` needed one level
   down. */
.pick-group-head > input[type=checkbox] { align-self: center; margin: 0; }

/* An action that reaches a DNS provider stays dim until the acknowledgement beside it is
   ticked. `:has()` on the form, because the box is in the body and the button is in the
   footer.

   Dimmed and still there, not hidden: a button that appears out of nowhere is a button
   somebody has to find, whereas one sitting there greyed says an action exists and something
   is owed first. `pointer-events` rather than the `disabled` attribute, because the state
   belongs to the checkbox and nothing should have to keep an attribute in step with it. */
form.ack-gate [type="submit"] {
  opacity: .45;
  pointer-events: none;
  cursor: not-allowed;
}
form.ack-gate:has(input[name="acknowledged"]:checked) [type="submit"] {
  opacity: 1;
  pointer-events: auto;
  cursor: pointer;
}

/* A table whose rows carry a second line under the name. `.tbl` rows are one line and 35px,
   which is right for a dense list of single values and cramped the moment a cell holds a
   `.sub` as well -- the text ends up against the top and the left edge.

   `padding` on `tbody td` and not on `td`: `.tbl tbody td` is specificity (0,1,2) and a
   (0,1,1) selector loses to it however late it appears. */
.tbl-roomy thead th { padding: 9px 14px; }
.tbl-roomy tbody td { height: auto; padding: 12px 14px; vertical-align: top; }
.tbl-roomy tbody td .sub { margin-top: 3px; }

/* The consistency block: what a snapshot of a volume group will actually be, stated as two
   independent facts rather than one blended claim. Whether the volumes were captured
   together is decided by the storage behind them; whether the guest had stopped writing is
   decided by the guest. Neither implies the other, so a single "consistent / not
   consistent" line has to be wrong about one of them.

   Deliberately not an `.alert`. An alert is a notice about something unusual, and this is
   the ordinary answer to the question the person came here to ask -- it appears on every
   group whether the news is good or not. It is also toneless on purpose, with no --ok or
   --warn: "one after another" is not a fault, it is what that storage does. Notice styling
   on a permanent fixture is how a page teaches people to skip it. */
.cons { border: 1px solid var(--line); background: var(--n1); margin-bottom: 16px; }
.cons-row { display: grid; grid-template-columns: 124px 1fr; gap: 10px; padding: 9px 12px; }
.cons-row + .cons-row { border-top: 1px solid var(--line); }
.cons-k { font-size: 11.5px; color: var(--muted); padding-top: 1px; }
.cons-v { font-size: 12.5px; }
.cons-v b { font-weight: 600; }
.cons-v .sub { display: block; margin-top: 3px; line-height: 1.5; }

/* The foot carries what the two rows cannot: the operator's note for this storage, or the
   offer of the route that does freeze the guest. Tinted rather than ruled, so it reads as
   part of the block and not as a second component stuck underneath it. The offer is a
   `.btn-sm` inside the block rather than a second action in the drawer footer, because two
   primary buttons side by side say the choice does not matter. */
.cons-foot {
  display: flex; flex-direction: column; align-items: flex-start; gap: 9px;
  padding: 9px 12px; border-top: 1px solid var(--line); background: var(--n2);
  font-size: 11.5px; color: var(--muted); line-height: 1.55;
}
.cons-foot b { color: var(--text); }

/* ============================================================
   The detail pane. Detail docked under the list at full content
   width, in flow -- the third pane of the shell. The drawer
   remains for panels not yet converted; #drawer-host stays the
   overlay host for modals, wizards and the confirm dialog.
   ============================================================ */
/* The host is a flex child of .main, under the table's .scroll and the pager. The
   height here is the default; the grip writes an inline height over it, and collapsing
   stores that inline height aside rather than fighting this rule. */
.detail {
  flex: none; position: relative; height: 320px; max-height: 60vh;
  display: flex; flex-direction: column;
  background: var(--surface); border-top: 1px solid var(--line);
}
/* A fragment swapped into the host participates in the host's own column layout. */
.detail-view { display: contents; }
.detail-grip { position: absolute; top: -3px; left: 0; right: 0; height: 7px; cursor: row-resize; z-index: 3; }
.detail-grip::after {
  content: ""; position: absolute; top: 2px; left: 50%; margin-left: -18px;
  width: 36px; height: 3px; background: var(--n4); border-radius: 2px;
}

.dt-head { flex: none; display: flex; align-items: flex-start; gap: 14px; padding: 10px 18px 0; }
.dt-acts { margin-left: auto; padding: 0; flex-wrap: nowrap; }
.dt-head .dr-close { margin-left: 0; }
/* Notes that belong to the whole resource -- the ERROR hint, the resize-waiting
   alert -- between the head and the essentials, never inside a tab. */
.dt-notes { flex: none; padding: 8px 18px 0; }
.dt-notes .alert, .dt-notes .hint { margin-bottom: 0; }
.dt-notes .dr-acts { padding: 8px 0 0; }

/* Essentials: the facts the resource is opened for, one band above the tabs,
   one line each. Which facts is the panel's decision, in its template. */
.dt-ess {
  flex: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: 0 26px;
  padding: 4px 18px 8px;
}
.dt-ess .dl { grid-template-columns: 82px 1fr; margin: 0; }
.dt-ess .dl dt, .dt-ess .dl dd { padding: 3px 0; font-size: 11.5px; border-bottom: 0; }

.dt-tabs { flex: none; padding: 0 18px; border-bottom: 1px solid var(--line); }
.dt-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 18px 18px; }
/* Tab bodies at pane width: columns of the same .dl, not one stack. */
.dt-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 4px 40px; align-items: start; }
.dt-cols .dl { grid-template-columns: 110px 1fr; margin: 0; }
/* The topology diagram scales to its container; the full pane width would blow it up. */
.dt-body .topo { max-width: 640px; }
/* One section in edit at a time, as in the drawer. Same rule, the pane's body. */
.dt-body:has(.dr-sec.on) .dr-sec:not(.on) .sec-act {
  color: var(--n4); pointer-events: none; text-decoration: none;
}

/* Collapsed: the head compacts into a 38px command bar for the open row -- the
   reading folds away, the verbs stay. JS moves the dragged inline height aside
   before adding .min, so this height wins without fighting an inline style. */
.detail.min { height: 38px; }
.detail.min .dt-ess, .detail.min .dt-tabs, .detail.min .dt-body, .detail.min .dt-notes { display: none; }
.detail.min .dt-head { align-items: center; padding: 0 18px; flex: 1; min-height: 0; }
.detail.min .dt-head .dr-id { display: none; }
.detail.min .dt-head .dr-name { font-size: 13px; }
/* The chevron points down while the pane is open, up while it is collapsed. */
.dt-head .dr-close .ic { transition: transform .12s ease; }
.detail.min .dt-head .dr-close .ic { transform: rotate(180deg); }

/* The empty pane: a quiet bar, and the table has the height back. */
.dt-empty { flex: 1; display: flex; align-items: center; padding: 0 18px; font-size: 12px; color: var(--muted); }
.detail:has(> .dt-empty), .detail:has(> .detail-view > .dt-empty) { height: 38px; }

/* The open row: the one whose detail fills the pane. The same mark the rail uses --
   red owns exactly one row at a time. Ticked rows keep only their checkbox tint. */
.tbl tbody tr.open td { background: var(--sel); }
.tbl tbody tr.open td:first-child { box-shadow: inset 3px 0 0 var(--brand); }

/* The pane's loading shimmer, cloned from #tpl-detail-skeleton. No scrim: the pane
   is part of the page, and dimming the page it is part of would say otherwise. */
.detail .dt-skel { flex: 1; min-height: 0; padding: 12px 18px; }

/* The name is the way into detail -- a link, not a whole-row trap. The row's other
   cells are plain selectable text: copying an address off a row must never open
   anything. The colour arrives on hover; at rest the name is a name. */
.tbl .name-link { color: var(--text); text-decoration: none; cursor: pointer; }
.tbl .name-link:hover, .tbl .name-link:focus-visible { color: var(--brand); text-decoration: underline; }

/* Essentials in the drawer head (Azure): the facts a resource is opened for, two
   columns above the tabs, one line each. Which facts is the panel's decision, in
   its template. */
.dr-ess { display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; padding: 2px 0 10px; }
.dr-ess .dl { grid-template-columns: 64px 1fr; margin: 0; }
.dr-ess .dl dt, .dr-ess .dl dd { padding: 3px 0; font-size: 11.5px; border-bottom: 0; }

/* ============================================================
   SOLUTION GUIDE
   A blueprint's guide.md as a page: the one thing in the app
   that is read top to bottom rather than scanned, so it gets
   the pane's width and a running list of its sections.
   ============================================================ */
.guide { display: grid; grid-template-columns: minmax(0, 1fr) 200px; gap: 56px; align-items: start; }
/* No reading measure: the pane already caps at 1100px, and the prose fills what is left
   beside the section list. Narrow the window and the list drops below it. */
.guide-body { min-width: 0; font-size: 13px; line-height: 1.6; }
.guide-body h2 { margin: 30px 0 8px; font-size: 15px; font-weight: 600; scroll-margin-top: 16px; }
.guide-body h2:first-child { margin-top: 0; }
.guide-body p { margin: 0 0 10px; }
.guide-body ul { margin: 0 0 10px; padding-left: 18px; }
.guide-body li { margin: 0 0 4px; }
.guide-body li::marker { color: var(--muted); }
.guide-body b { font-weight: 600; }
.guide-body code {
  font-family: var(--mono); font-size: 11.5px; background: var(--n2);
  padding: 0 3px; border-radius: 2px;
}
/* Plain-text diagrams and commands. Mono at 12px like .usage-code, and it scrolls
   sideways itself rather than widening the column: a diagram is as wide as it is. */
.guide-body pre {
  margin: 0 0 12px; padding: 9px 11px; background: var(--n2); border: 1px solid var(--line);
  border-radius: 2px; overflow-x: auto; font-family: var(--mono); font-size: 12px; line-height: 1.5;
}
.guide-side { position: sticky; top: 16px; font-size: 12px; padding-left: 20px; border-left: 1px solid var(--line); }
.guide-side .sec-title { margin-top: 0; }
.guide-side .sec-title + .sec-title, .guide-side ol + .sec-title, .guide-side .dl + .sec-title, .guide-side div + .sec-title { margin-top: 20px; }
.guide-side ol { list-style: none; margin: 0; padding: 0; }
.guide-side li { margin: 0; }
.guide-side li a {
  display: block; padding: 4px 0 4px 10px; color: var(--muted); text-decoration: none;
  border-left: 2px solid var(--line);
}
.guide-side li a:hover { color: var(--text); border-left-color: var(--brand); }
.guide-side .dl { grid-template-columns: 74px 1fr; }
.guide-side .dl dt, .guide-side .dl dd { font-size: 12px; padding: 3px 0; }
@media (max-width: 860px) {
  .guide { grid-template-columns: 1fr; }
  .guide-side { position: static; padding-left: 0; border-left: 0; }
}

/* ============================================================
   RUN STRIP
   One square per run of a schedule, oldest on the left, in the
   pills' own four colours so an outcome means the same thing
   here as it does in the table beside it.

   The one component the Schedules panel adds. It earns its place
   because a row of green with a red one on the end answers "is
   this working" without opening anything, which is the question
   that panel exists to answer.
   ============================================================ */
.runbar { display: flex; align-items: flex-end; gap: 2px; }
/* `>`, not descent: `display` set on a bare element by descent outranks any component
   class it lands on, and `i` is a common enough element to land on one. */
.runbar > i {
  width: 7px; height: 14px; border-radius: 1px; background: var(--n3);
  display: block; flex: none;
}
.runbar > i.ok   { background: var(--ok); }
.runbar > i.bad  { background: var(--bad); }
.runbar > i.warn { background: var(--warn); }
.runbar > i.info { background: var(--info); }
.runbar > i.idle { background: var(--n3); }
/* In a table cell the strip is the whole cell: it clips rather than pushing the columns
   after it around on a narrow window. */
.tbl .runbar { overflow: hidden; }
/* Taller in a drawer, where it is the thing being read rather than a glance. */
.dr-body .runbar { margin: 0 0 4px; }
.dr-body .runbar > i { width: 9px; height: 26px; }

/* ============================================================
   CATALOGUE GRID
   A catalogue is browsed, not operated. Thirteen solutions fit
   in a table; thirty do not, and a table row has nowhere to put
   the one fact that decides between two of them -- what the
   thing builds.

   This is the documented exception to the 2px radius and to
   "no shadow unless it overlays"; see the Geometry section of
   docs/design-system.md, which names this component and only
   this one. The exception buys the card a physical affordance
   -- it lifts, so it reads as a thing you pick up -- on the one
   surface in the app whose job is choosing rather than working.

   Nothing else in the dashboard may cite it.
   ============================================================ */
/* The gutter is wider than a card's own padding, so cards read as separate objects rather
   than as cells of one grid. Three across at the content width. */
.cat-grid {
  display: grid; gap: 20px; padding: 14px 18px 24px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  align-content: start;
}

/* What an operator said to lead with. The same `.cat-card` in a wider track, so it is the
   same card at a larger size rather than a second component: no carousel, no gradient, no
   photograph. Three across on a wide page and fewer as it narrows, by the same auto-fill
   the grid uses, so the row never becomes a horizontal scroll. */
.cat-feat {
  display: grid; gap: 12px; padding: 14px 18px 0;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
/* Only the type grows. The glyph, the strip and the meta row stay where the eye already
   knows them from the grid below, which is what keeps the two reading as one set. */
.cat-feat .cat-t { font-size: 15px; }
.cat-feat .cat-d { font-size: 12.5px; }

/* The category strip over the grid. `.tab` sits under a table's header and shares that
   header's rule; this sits over a grid with no rule to share, so it carries its own line
   and wraps rather than scrolling -- six categories at a narrow width is two rows, and a
   strip that scrolled sideways would hide the ones somebody has not thought to look for. */
.cat-rail {
  display: flex; flex-wrap: wrap; gap: 2px;
  padding: 0 18px; margin-top: 4px; border-bottom: 1px solid var(--line);
}
.cat-tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  font: inherit; font-size: 12.5px; color: var(--muted);
  padding: 7px 10px; cursor: pointer; margin-bottom: -1px;
}
.cat-tab:hover:not(:disabled) { color: var(--text); }
.cat-tab.on { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
/* A category nothing currently matches. Left in place at half strength rather than removed:
   one that vanished while somebody typed would read as the catalogue having lost it. */
.cat-tab:disabled { opacity: .45; cursor: default; }
/* Tabular so the strip does not shuffle sideways as the counts change under a search. */
.cat-tab > i {
  font-style: normal; font-size: 11px; font-variant-numeric: tabular-nums;
  color: var(--muted); margin-left: 4px;
}
.cat-tab.on > i { color: inherit; }

/* An anchor: middle-click, copy-link and open-in-new-tab all work, and the keyboard
   reaches it without a tabindex. `display: flex` on an <a> is why the children are
   spans. */
.cat-card {
  position: relative; display: flex; flex-direction: column; gap: 10px;
  padding: 20px 20px 18px; color: inherit; text-decoration: none;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.cat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--n4); }
.cat-card:active { transform: translateY(0); }
.cat-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The glyph ground is the selected-row tint rather than the red itself: a card is not the
   one thing on the page being pointed at, and thirty red squares would spend the pointer
   colour on decoration. See the Branding note on red as a pointer. */
.cat-glyph {
  width: 36px; height: 36px; border-radius: 10px; flex: none;
  display: grid; place-items: center; background: var(--sel); color: var(--brand);
}
.cat-glyph .ic { width: 18px; height: 18px; }

.cat-t { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
/* `flex: 1` so every card in a row ends its meta and strip at the same height whatever
   the summary's length. A grid of cards that disagree about where their footers sit reads
   as broken before it reads as anything else. */
.cat-d { font-size: 12px; color: var(--muted); line-height: 1.5; flex: 1; }

/* Nobody has launched this and watched it work. Brand red rather than `--bad`: the semantic
   tokens carry a resource's status, and this is a fact about the blueprint.

   Tinted ground and no border, the same treatment `.pill` and `.va-gpu` use, because the
   note above `.cat-glyph` applies here too and more so -- every card carries this until an
   operator clears it, and thirty outlined red boxes would be a pattern rather than a
   warning. Small, coloured, and out of the way is what makes it still read as one. */
.cat-beta {
  font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--brand); background: var(--brand-tint); border: 1px solid transparent;
  border-radius: 2px; padding: 2px 6px; white-space: nowrap;
}
/* On a card it sits over the top-right corner rather than in `.cat-meta`, which is the row
   the grid filters on and where a fourth kind of chip would blur into the workload and the
   vendors. `.cat-card` is already `position: relative`. Aligned with the glyph's top edge. */
.cat-card > .cat-beta { position: absolute; top: 20px; right: 20px; }

.cat-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cat-chip {
  font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--line); border-radius: 2px; padding: 2px 6px;
}

/* What a launch would create, from catalogs/builds.py over preflight.demand(). Ruled off
   rather than boxed: it is a footnote to the card, not a second card inside it. */
.cat-builds { display: flex; gap: 10px; padding-top: 9px; border-top: 1px solid var(--line); }
.cat-builds > i {
  font-style: normal; display: inline-flex; align-items: center; gap: 3px;
  font-size: 11px; font-variant-numeric: tabular-nums; color: var(--muted);
}
.cat-builds .ic { width: 13px; height: 13px; }

/* The empty catalogue. A `.stub` is the panel-wide answer for an empty list, and it wants
   a table cell to span; this sits in the grid instead, spanning every column so it reads
   as the page's answer rather than as a first card. */
.cat-none {
  grid-column: 1 / -1; margin: 0; padding: 24px 0; max-width: 60ch;
  font-size: 12.5px; color: var(--muted); line-height: 1.6;
}

/* The same list while it is being built. A grid rather than a flex row so the states line
   up in a column: seven rows whose second column starts wherever the noun happened to end
   is a list nobody can scan for the one that failed. */
.cat-steps { list-style: none; margin: 14px 0 0; padding: 0; }
.cat-steps > li {
  display: grid; grid-template-columns: 14px minmax(0, 1fr) auto; gap: 4px 8px;
  align-items: center; padding: 5px 0; font-size: 12.5px;
  border-bottom: 1px solid var(--line-sq);
}
.cat-steps > li > .ic { width: 14px; height: 14px; color: var(--muted); }
.cat-steps > li > .st { font-size: 11.5px; color: var(--muted); }
/* The reason, under the row it belongs to and across both text columns. Only a failed row
   has one, which is what makes it findable in a list of seven. */
.cat-steps > li > .why {
  grid-column: 2 / -1; font-size: 11.5px; line-height: 1.45; color: var(--bad);
}
.cat-steps > li.done > .ic { color: var(--ok); }
.cat-steps > li.failed > .ic, .cat-steps > li.failed > .st { color: var(--bad); }
.cat-steps > li.running > .ic, .cat-steps > li.running > .st { color: var(--info); }
.cat-steps > li.waiting { opacity: .6; }

/* What a blueprint creates, in a drawer or a guide's aside. The card's `.cat-builds` strip
   is the same data with no room for the noun; this is the version with room. */
.cat-creates { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
/* A direct child, never `.cat-creates li`: that is (0,1,1) on a bare element and reaches
   into anything nested, which `tests/test_stylesheet.py` refuses for the reason the CSS
   rules in AGENTS.md give -- `.field label` forced display:block onto every picker row. */
.cat-creates > li { display: flex; align-items: center; gap: 7px; font-size: 12.5px; }
.cat-creates > li > .ic { width: 14px; height: 14px; color: var(--muted); flex: none; }


/* What a launch will make. Two places, one component: beside the questions where there is
   room for a column, and under them where there is not.

   Beside is the better of the two and is why the wide drawer exists: every row is derived
   from an answer, so a plan that moves as the form is filled in is being read DURING the
   decision. At the foot it is read after it, which is still worth having and is all a
   phone-width screen can offer.

   The breakpoint is the width at which two columns each stay readable. Below it `.wiz` is
   an ordinary block and the plan falls back under the questions, exactly where it was. */
.drawer-wide { width: 520px; }
.wiz-side { margin-top: 4px; }
@media (min-width: 1100px) {
  .drawer-wide { width: 880px; }
  .wiz { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 26px; align-items: start; }
  /* Its own scroll, so a long form does not push the plan off the bottom, and a long plan
     does not stretch the questions. `min-height: 0` because a grid item's default
     `min-height: auto` refuses to shrink and the overflow never engages. */
  .wiz-ask { min-width: 0; }
  .wiz-side {
    position: sticky; top: 0; margin-top: 0;
    max-height: calc(100vh - 150px); overflow-y: auto; min-height: 0;
  }
  /* Beside the questions it is the column, so it needs no rule to separate it from them
     and no top margin to push it clear. */
  .wiz-side > .wiz-plan-block { margin-top: 0; padding-top: 0; border-top: 0; }
}

/* Under the questions. Ruled off rather than boxed: it is the last thing read before the
   button, not a panel competing with the fields above it. */
.wiz-plan-block { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--line); }
.wiz-cost { margin: 12px 0 0; font-size: 13px; }
.wiz-cost b { font-size: 15px; letter-spacing: -.01em; }
.wiz-cost span { color: var(--muted); font-size: 12px; }
.wiz-pre {
  list-style: none; margin: 10px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.wiz-pre > li {
  display: flex; gap: 7px; align-items: flex-start;
  font-size: 11.5px; line-height: 1.45; color: var(--muted);
}
.wiz-pre > li > .ic { width: 13px; height: 13px; margin-top: 1px; flex: none; }
.wiz-pre > li.ok > .ic { color: var(--ok); }
.wiz-pre > li.warn > .ic { color: var(--warn); }

/* ============================================================
   MESH: THE CARD, AND ONE MESH'S PAGE
   Level one is the catalogue card with a mesh in it. Level two
   is a square card on the console's line holding two tables.
   ============================================================ */
/* The category chip carrying a state. Coloured only for the two that mean something is
   wrong; every other state is as quiet as any other chip. */
.cat-chip.warn { color: var(--warn); border-color: var(--warn); }
.cat-chip.bad { color: var(--bad); border-color: var(--bad); }
/* The strip's warning count: the one number on the card that is a fault. */
.cat-builds > i.warn { color: var(--warn); font-weight: 600; }
/* The card that is not a mesh. Dashed on the page ground so it reads as a place for one. */
.cat-card-add { border-style: dashed; background: transparent; }
.cat-card-add > .cat-glyph {
  background: transparent; border: 1px dashed var(--line); color: var(--muted);
}
.cat-card-add:hover > .cat-glyph { color: var(--brand); }

/* One mesh, operated rather than browsed, so it keeps the console's square corners. Its
   head and both tables sit 20px in rather than running to the edge. */
.mesh-card { border: 1px solid var(--line); background: var(--surface); margin: 14px 0 0; }
.mc-head {
  display: flex; align-items: center; gap: 12px; padding: 12px 16px;
  border-bottom: 1px solid var(--line-sq);
}
/* The page head names the mesh, so the card's head carries only the facts. */
.mc-head-quiet { padding: 14px 20px; border-bottom: 0; }
.mc-facts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px; font-size: 11.5px;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.mc-facts b { color: var(--text); font-weight: 600; }

/* The regions stop growing at eight rows and scroll inside the box. `.tbl thead th` is
   already sticky, so the header stays put while they do. */
.mc-rtable {
  max-height: 302px; overflow-y: auto; margin: 0 20px; border: 1px solid var(--line-sq);
}
.mc-diagram { margin: 0 20px 14px; }
.mc-diagram > svg { display: block; width: 100%; height: auto; }
.mc-diagram > .legend { margin-top: 6px; }
.mc-mtable { padding: 0 20px 10px; }
.mc-mtable > .table-wrap { border-color: var(--line-sq); }
.mc-grid-head { display: flex; align-items: baseline; gap: 12px; padding: 18px 0 8px; }
.mc-grid-head > .hint { margin-left: auto; }
.mc-rh {
  margin: 0; display: flex; align-items: baseline; gap: 8px; font-size: 10px;
  font-weight: 700; letter-spacing: .11em; text-transform: uppercase; color: var(--muted);
}
.mc-rh > span {
  font-weight: 400; letter-spacing: 0; text-transform: none; font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* A region row is a filter: clicking it sets the toolbar's region chip. */
.tbl-regions tbody tr { cursor: pointer; }
.tbl-regions th.n, .tbl-regions td.n {
  text-align: right; font-variant-numeric: tabular-nums; width: 9em;
}
.tbl-regions td.n { color: var(--muted); }
.tbl-regions td.n.warn { color: var(--warn); font-weight: 600; }
.tbl-regions td.n.quiet { color: var(--muted); font-weight: 600; }
.tbl-regions .rg-name { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
.tbl-regions .rg-name > .ic { width: 12px; height: 12px; color: var(--brand); }
.rg-on { display: inline-flex; align-items: center; gap: 10px; }
.rg-on > .ov-bar { width: 110px; }
.rg-on > span { font-variant-numeric: tabular-nums; min-width: 5.5em; }
.rg-on.warn > span { color: var(--warn); font-weight: 600; }
/* The chosen region: the mark a picked row gets everywhere else, plus the rule. */
.tbl-regions tbody tr.on td { background: var(--sel); }
.tbl-regions tbody tr.on td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
/* A machine that never joined: no address, no agent, one action. */
.tbl tbody tr.never td { color: var(--muted); }
.tbl tbody tr.never td.name-cell { color: var(--text); }

/* ============================================================
   SIGN-UP AND THE STARTER ZONE
   The hosted sign-up pages sit in the sign-in card; the Starter
   zone is an ordinary panel page; the badge sits in the scope bar.
   ============================================================ */

.signup-steps {
  display: flex; gap: 14px; margin: 0 0 16px; padding: 0; list-style: none;
  font-size: 12px; color: var(--muted);
}
.signup-steps > li { display: flex; align-items: center; gap: 6px; }
.signup-steps > li.on { color: var(--text); font-weight: 600; }
.signup-steps > li.done { color: var(--ok); }
.signup-step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%; border: 1px solid currentColor;
  font-size: 11px; font-variant-numeric: tabular-nums;
}
.signup-lede { margin: 0 0 14px; font-size: 12.5px; color: var(--muted); }
.signup-go { margin-top: 6px; }
.signup-rates { margin-bottom: 14px; }
.signup-rates .n { text-align: right; }
.signup-cap { margin: 0 0 8px; padding-left: 18px; font-size: 12.5px; }
.signup-cap > li { margin: 2px 0; }
.signup-progress { margin: 0 0 14px; padding: 0; list-style: none; }
.signup-progress > li {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid var(--line-sq); font-size: 12.5px;
}

.scope-starter {
  margin-left: 10px; padding: 1px 8px; border: 1px solid var(--star); border-radius: 999px;
  color: var(--star); font-size: 11px; font-weight: 600; text-decoration: none; white-space: nowrap;
}
.scope-starter:hover { background: var(--bar-line); }

.st-steps { margin: 0 0 18px; padding: 0; list-style: none; max-width: 560px; }
.st-steps > li {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--line-sq); font-size: 12.5px;
}
.st-month { font-size: 12.5px; margin: 0; }

/* ---- AI: the Erebine pages ----------------------------------------------------------
   Models, Agents and Workspace share a card row, a two-column connect and workspace layout,
   and the notes block that draws what the workspace tools answer. Tokens only. */
.er-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  padding: 12px 14px; display: flex; flex-direction: column; gap: 6px;
}
.er-card > h4 { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.er-card > p { margin: 0; font-size: 12.5px; line-height: 1.45; }
.er-big { font-size: 19px; font-weight: 600; letter-spacing: -0.01em; }
.er-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 18px; }
.er-connect, .er-workspace { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 18px; align-items: start; }
.er-form { max-width: 520px; display: flex; flex-direction: column; gap: 18px; }
.er-form > form { display: flex; flex-direction: column; gap: 12px; }
.er-aside, .er-notes { display: flex; flex-direction: column; gap: 12px; }
.er-facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; margin: 0; font-size: 12.5px; }
.er-facts > dt { color: var(--muted); }
.er-facts > dd { margin: 0; }
.er-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.er-approval { border-left: 3px solid var(--bad); margin-bottom: 10px; }
.er-approval > h4 { text-transform: none; letter-spacing: 0; font-size: 13px; color: var(--text); }
.er-approval-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.er-search { display: flex; gap: 6px; }
.er-serve > form { display: flex; flex-direction: column; gap: 12px; }
.er-serve-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.er-cell-act { text-align: right; white-space: nowrap; }
.er-search > input {
  font: inherit; font-size: 12.5px; padding: 6px 8px; min-width: 220px;
  border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--text);
}
.er-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.notes { font-size: 12.5px; line-height: 1.5; }
.notes > h3, .notes > h4, .notes > h5, .notes > h6 { margin: 10px 0 4px; font-size: 12.5px; font-weight: 600; }
.notes > p { margin: 0 0 8px; }
.notes ul { margin: 0 0 8px; padding-left: 18px; }
.notes ul ul { margin: 2px 0 0; }
.notes code { font-family: var(--mono); font-size: 11.5px; }
@media (max-width: 900px) {
  .er-cards, .er-connect, .er-workspace { grid-template-columns: minmax(0, 1fr); }
}
