/* VibeDash design system.
 *
 * Palette and typeface are taken from Rackspace's own published assets, not
 * approximated: #C12134 primary red, #CF3530 accent, #1A1A23 ink, 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
   Neutrals carry a faint red bias so they sit with the accent
   rather than reading as generic grey.
   ============================================================ */
:root {
  color-scheme: light;
  --red: #C12134; --red-h: #A81B2C; --red-tint: #FBF0F1;
  --n0: #FFFFFF; --n1: #FAF8F8; --n2: #F3F0F0; --n3: #E4DFE0; --n4: #C9C2C4; --n5: #837D7F;
  --ok: #1B7F4E;   --ok-bg: #E8F4ED;
  --warn: #8A6100; --warn-bg: #FBF2DE;
  --bad: #B3261E;  --bad-bg: #FBEDEC;
  --info: #1F5FA9; --info-bg: #EAF1F9;
  --bar: #1A1A23; --bar-text: #E9E6E7; --bar-dim: #948E90; --bar-line: #35333C;
  --app-bg: #FAF8F8; --surface: #FFFFFF; --line: #E4DFE0; --line-sq: #F3F0F0;
  --text: #333132; --muted: #837D7F; --hover: #F7F4F4; --sel: #FBF0F1;
  --shadow: 0 8px 28px rgba(26,26,35,.14);

  --logo: url("/static/img/fanatiguy-reverse.png");
  --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;
    --red: #E04A5C; --red-h: #EE6273; --red-tint: #2C1B1F;
    --n0: #1C1B21; --n1: #141319; --n2: #24222A; --n3: #38343D; --n4: #4E4952; --n5: #9B9399;
    --ok: #5FC98D;   --ok-bg: #14271D;
    --warn: #D9A93A; --warn-bg: #2A2213;
    --bad: #F0736A;  --bad-bg: #2C1917;
    --info: #6BA6E8; --info-bg: #16202D;
    --bar: #0F0E13; --bar-line: #2A2830; --bar-dim: #8B858A;
    --app-bg: #141319; --surface: #1C1B21; --line: #33303A; --line-sq: #262430;
    --text: #E4E0E2; --muted: #9B9399; --hover: #24222A; --sel: #2C1B1F;
    --shadow: 0 8px 28px rgba(0,0,0,.5);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --red: #E04A5C; --red-h: #EE6273; --red-tint: #2C1B1F;
  --n0: #1C1B21; --n1: #141319; --n2: #24222A; --n3: #38343D; --n4: #4E4952; --n5: #9B9399;
  --ok: #5FC98D;   --ok-bg: #14271D;
  --warn: #D9A93A; --warn-bg: #2A2213;
  --bad: #F0736A;  --bad-bg: #2C1917;
  --info: #6BA6E8; --info-bg: #16202D;
  --bar: #0F0E13; --bar-line: #2A2830; --bar-dim: #8B858A;
  --app-bg: #141319; --surface: #1C1B21; --line: #33303A; --line-sq: #262430;
  --text: #E4E0E2; --muted: #9B9399; --hover: #24222A; --sel: #2C1B1F;
  --shadow: 0 8px 28px rgba(0,0,0,.5);
}
:root[data-theme="light"] {
  color-scheme: light;
  --red: #C12134; --red-h: #A81B2C; --red-tint: #FBF0F1;
  --n0: #FFFFFF; --n1: #FAF8F8; --n2: #F3F0F0; --n3: #E4DFE0; --n4: #C9C2C4; --n5: #837D7F;
  --ok: #1B7F4E;   --ok-bg: #E8F4ED;
  --warn: #8A6100; --warn-bg: #FBF2DE;
  --bad: #B3261E;  --bad-bg: #FBEDEC;
  --info: #1F5FA9; --info-bg: #EAF1F9;
  --bar: #1A1A23; --bar-line: #35333C; --bar-dim: #948E90;
  --app-bg: #FAF8F8; --surface: #FFFFFF; --line: #E4DFE0; --line-sq: #F3F0F0;
  --text: #333132; --muted: #837D7F; --hover: #F7F4F4; --sel: #FBF0F1;
  --shadow: 0 8px 28px rgba(26,26,35,.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(--red); text-decoration: none; }
.lnk:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--red); 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: 46px; flex: none;
  background: var(--bar); color: var(--bar-text);
  display: flex; align-items: center; gap: 4px;
  padding: 0 10px 0 14px;
  border-bottom: 1px solid var(--bar-line);
}
.brand { display: flex; align-items: center; gap: 9px; margin-right: 16px; }
.brand-mark { width: 21px; height: 21px; flex: none; background: var(--logo) center / contain no-repeat; }
.brand-name { font-size: 14px; font-weight: 700; letter-spacing: -.005em; }

.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); }

/* ============================================================
   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(--red); color: var(--red); font-weight: 600; }
.rail-item.on .ic { color: var(--red); }

/* 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(--red); color: var(--red); font-weight: 600; }
.rail-item.pending .ic { color: var(--red); }
/* 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(--red);
  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; }

.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(--red); background: var(--sel); color: var(--red); font-weight: 500; }
.chip.applied .x { color: var(--red); }

/* ============================================================
   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(--red); border-color: var(--red); color: #fff; }
.btn-primary:hover { background: var(--red-h); border-color: var(--red-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. `data-sort` on the <th> names the row attribute to sort on,
   so a panel opts a column in with one attribute and the key is readable in the template
   -- the same shape the filter convention already has. static/table.js does the rest.

   The marker is drawn from `aria-sort` rather than from a class of its own, so the state
   a screen reader announces and the state that is drawn cannot drift apart. 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 triangle somebody wants to
   restyle should be one rule in here. */
.tbl thead th[data-sort] { cursor: pointer; user-select: none; }
.tbl thead th[data-sort]:hover { color: var(--text); }
.tbl thead th[data-sort]:focus-visible { outline: 2px solid var(--red); outline-offset: -2px; }
/* Faint at rest, so the column reads as sortable before anyone hovers it, and pointing
   the way the first click will sort rather than at some neutral third shape. Rest, hover,
   active is three steps, which is what makes the header read as a control. */
.tbl thead th[data-sort]::after {
  content: ""; display: inline-block; width: 0; height: 0;
  margin-left: 5px; vertical-align: middle; opacity: .25;
  border-left: 3px solid transparent; border-right: 3px solid transparent;
  border-bottom: 4px solid currentColor;
}
.tbl thead th[data-sort]:hover::after { opacity: .6; }
/* The one column currently in effect, in the colour this app uses for exactly that. */
.tbl thead th[aria-sort="ascending"], .tbl thead th[aria-sort="descending"] { color: var(--red); }
.tbl thead th[aria-sort="ascending"]::after { opacity: 1; }
.tbl thead th[aria-sort="descending"]::after {
  opacity: 1; border-bottom: 0; border-top: 4px solid currentColor;
}
/* 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(--red); }
.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; }
.name-cell { font-weight: 500; color: var(--text); }

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

/* ============================================================
   STATUS
   ============================================================ */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; letter-spacing: .03em;
  padding: 2px 7px 2px 6px; border-radius: 2px; text-transform: uppercase;
}
.pill i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }
/* 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; }
.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(--red-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(--red-tint); color: var(--red); }

/* 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(--red); 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(--red); }
.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. Same shape as `.pill i`, so "nothing has
   happened here" reads the same everywhere. */
.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(--red); 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(--red);
  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
   ============================================================ */
.sec-title {
  font-size: 10px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 24px 0 8px; padding-bottom: 5px; border-bottom: 1px solid var(--line);
}
.pane > .sec-title:first-child { margin-top: 0; }
.dl { display: grid; grid-template-columns: 160px 1fr; gap: 0; margin: 0 0 4px; }
.dl dt { color: var(--muted); font-size: 12px; padding: 5px 0; }
.dl dd { margin: 0; padding: 5px 0; font-size: 12.5px; word-break: break-word; }
/* 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(--red); }
.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(--red); }

/* 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 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 `--red`: 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(--red); 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(--red); }
.pick > input[type=radio] { 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; }
.pick-id { font-size: 11px; color: var(--muted); overflow-wrap: anywhere; }
/* 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
   ============================================================ */
.login { flex: 1; display: flex; min-height: 0; background: var(--surface); }
.login-aside {
  width: 40%; max-width: 460px; flex: none; background: var(--bar); color: #E9E6E7;
  padding: 34px;
  /* Bottom-aligned, as it was when the mark sat above it and pushed it down. */
  display: flex; flex-direction: column; justify-content: flex-end;
}

/* 21px, not the 30px this once was: nothing else in the app goes above 19px, and Fira
   Sans narrows visibly at display sizes. The tracking is opened slightly rather than
   tightened, which is what made the larger setting read as condensed. */
.login-quote {
  font-size: 21px;
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: .005em;
  max-width: 22ch;
  text-wrap: balance;
  margin: 0;
}
.login-quote b { color: #fff; font-weight: 600; }
.login-meta {
  margin-top: 22px;
  font-size: 11.5px;
  color: var(--bar-dim);
  font-family: var(--mono);
}
/* The diffusion field takes the aside's empty upper region. flex:1 above the
   bottom-aligned text is what keeps the tagline and service list exactly where they
   already sat. Drawn by login-art.js; absent without JavaScript, which costs nothing
   but the ornament. */
.login-art { flex: 1; min-height: 0; margin: -10px -10px 22px; }
.login-art > canvas { display: block; width: 100%; height: 100%; }
/* The service the field is currently resolving into. Colour only -- no motion and no
   layout shift, so it cannot jitter the line it sits in. */
.login-meta span { transition: color .5s ease; }
.login-meta span.lit { color: var(--bar-text); }

.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. */
.signup-link { margin: 14px 0 0; text-align: center; font-size: 12.5px; }
.signup-link a { color: var(--red); text-decoration: underline; }

@media (max-width: 780px) {
  .login-aside { display: none; }
}

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

/* ============================================================
   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); }
.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(--red); border-bottom-color: var(--red); font-weight: 600; }
/* 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; }
/* 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 has been 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);
}
.sec-title:first-child { margin-top: 0; }
.dl { display: grid; grid-template-columns: 138px 1fr; gap: 0; }
.dl dt { color: var(--muted); font-size: 12px; padding: 5px 0; }
.dl dd { margin: 0; padding: 5px 0; font-size: 12.5px; word-break: break-word; }
/* 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(--red); }
.copy .ic { width: 12px; height: 12px; }
/* 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); }
.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(--red); }
.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); }

/* 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; }
.log-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 5px; background: var(--n4); }
.log-dot.ok { background: var(--ok); } .log-dot.bad { background: var(--bad); } .log-dot.warn { background: var(--warn); }
.log-what { font-size: 12.5px; }
.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 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(--red); font-weight: 600; background: var(--sel); box-shadow: inset 3px 0 0 var(--red); }
.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; }
.hint { font-size: 11.5px; color: var(--muted); margin-top: 4px; }
.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(--red); 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(--red); }
.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(--red); background: var(--red-tint); }
.dropzone:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.dropzone .dz-ic { width: 22px; height: 22px; color: var(--n4); }
.dropzone.over .dz-ic { color: var(--red); }
.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(--red); 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);
}

/* ============================================================
   MULTI-KEYSTONE REGION MARKERS
   Which independent Keystones carry a given project. Nothing syncs
   them, so absence is real information and reads as such.
   ============================================================ */
.region-marks { display: flex; gap: 3px; flex: none; align-self: center; }
.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(--red); border-color: var(--red); 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; }
.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); }
/* 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(--red); }

/* 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(--red); background: var(--red-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(--red); 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(--red); }
.crumb.on { color: var(--text); font-weight: 600; }
.crumb-sep { color: var(--n4); }

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

/* ============================================================
   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(--red); 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;
}

/* 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(--red); }
.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(--red); 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; }

/* 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(--red);
  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(--red); color: var(--red);
  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; }

/* The escape hatch: what is deliberately not here, and the command that does it. */
.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;
}
.cli-note > code {
  display: block; margin-top: 6px; font-family: var(--mono); font-size: 11px;
  color: var(--text); background: var(--n2); padding: 5px 7px; border-radius: 2px;
  overflow-x: auto; white-space: pre;
}

/* ============================================================
   PRE-FLIGHT
   The mark carries the state; colour is never doing it alone.
   ============================================================ */
.pf { display: flex; flex-direction: column; }
.pf-row { display: flex; gap: 11px; padding: 11px 12px; border: 1px solid var(--line); margin-bottom: -1px; }
.pf-row > div { min-width: 0; }
.pf-row b { font-weight: 600; font-size: 12.5px; }
.pf-mark {
  flex: none; width: 17px; height: 17px; border-radius: 50%; display: grid;
  place-items: center; font-size: 10px; font-weight: 700; margin-top: 1px;
}
.pf-row.ok   .pf-mark { background: var(--ok-bg);   color: var(--ok); }
.pf-row.warn .pf-mark { background: var(--warn-bg); color: var(--warn); }
.pf-row.bad  .pf-mark { background: var(--bad-bg);  color: var(--bad); }
.pf-row.bad {
  background: var(--bad-bg); border-color: var(--bad);
  position: relative; z-index: 1;
}
.pf-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(--red); 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; }

/* ============================================================
   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(--red); 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 --red 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(--red); 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(--red); color: var(--red); 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(--red-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(--red); 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(--red); }

/* 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(--red); }
.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; }
.wire.down  { stroke: var(--warn); stroke-dasharray: 4 3; stroke-opacity: .9; }
.wire.float { stroke: var(--red);  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(--red); 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(--red-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(--red); }
.legend i.down  { border-top-style: dashed; border-color: var(--warn); }
.legend i.dead  { border-top-style: dashed; border-color: var(--n4); }
.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(--red); }

/* ============================================================
   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(--red); border-color: var(--red); color: #fff; }
.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: #fff; }
.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(--red); 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(--red); color: var(--red); }
.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(--red); border-color: var(--red); color: #fff; }
.btn-jb-go:hover { background: var(--red-h); border-color: var(--red-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(--red); background: var(--sel); }
.jb-choice .t { 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. */
.view-switch { display: inline-flex; align-items: center; gap: 8px; }
.view-switch .seg-mini { display: inline-flex; border: 1px solid var(--bar-line); border-radius: 7px;
                         overflow: hidden; }
.view-switch button { font: inherit; font-size: 12.5px; font-weight: 600; padding: 5px 11px;
                      background: transparent; border: 0; color: var(--bar-dim); cursor: pointer; }
.view-switch button:hover { color: var(--bar-text); }
.view-switch button.on { background: var(--red); color: #fff; }

/* 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(--red); transform: scale(1.3); }
.jb-dots i.done { background: var(--red); opacity: .4; }
.jb-dots b { font-size: 15px; font-weight: 500; color: var(--muted); margin-left: 8px; }
.jb-choice.on { border-color: var(--red); box-shadow: inset 0 0 0 1px var(--red);
                background: var(--sel); }
.jb-price { font-weight: 600; color: var(--red); }
.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(--red); 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 the Rackspace Spot 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. Spot's #005AC8 was carried here for one
   pass and taken back out: a second accent makes the two views of
   one product look like two products, and the branding rules are
   explicit that the palette is Rackspace's own 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(--red); --va-accent-h: var(--red-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 sub-brand, naming the view rather than the app.
 *
 * Where a section eyebrow would go, and deliberately not in the top bar: beside the
 * Fanatiguy mark it read as a second product logo instead of as the name of what this
 * view is. Sized well under the wordmark it sits below, so the page still opens on
 * "Your machines". The two placements never co-occur -- an empty account has no heading
 * to sit above -- so the mark is never on screen twice. */
.va-eyebrow { display: block; height: 15px; width: auto; margin: 0 0 8px; }
.va-hero > .va-eyebrow { height: 32px; margin-bottom: 18px; }

/* 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. The mark rather than the word: the pane it belongs to is titled
   with the same mark, so the two read as the same thing without a second logo on the row.
   The image is sized here and not in the markup so a row cannot be knocked out of line by
   whatever dimensions the file happens to have. */
.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 > img { height: 11px; width: auto; display: block; opacity: .85; }

/* 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: #fff; }
.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, .va-back: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: #fff; }
.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
 * is Spot's 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: #fff; 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: #fff; 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 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: #fff; }
.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 > img { height: 14px; width: auto; flex: none; margin-top: 2px; }
.va-owned-note > button { margin-left: auto; flex: none; }

/* The four pillars, as four panes.
   Two columns at width, one when there is not room. Deliberately not auto-fit: these are
   four named things rather than a variable number of cards, and a lone pane stretched
   across a wide row reads as a mistake. */
.va-quad { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 900px) { .va-quad { grid-template-columns: minmax(0, 1fr); } }

.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; }
/* The AI pane is titled with the wordmark instead of the word. Three panes named in type
   and one named by its mark says the AI here is VibeAI without a second logo anywhere on
   the screen, and it is the only pane that is a product rather than a resource. */
/* 20px, not 15. The wordmark's box includes ascenders and a sparkle, so matching its
   HEIGHT to the 14px headings beside it made the letters visibly smaller than they are.
   Sized so the letterforms read at the same weight as the words in the other three. */
.va-pane > header > .mark { height: 20px; width: auto; display: block; }
.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 four. One link, always in the same place: a section
   you can reach and not leave is the trap the view switch already exists to avoid. */
.va-back { display: inline-flex; align-items: center; gap: 7px; margin-bottom: 14px;
           font: inherit; font-size: 12.5px; font-weight: 600; color: var(--va-accent);
           background: none; border: 0; padding: 0; cursor: pointer; }

/* 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(--red); 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(--red); 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(--red); 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(--red); }
.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(--red); 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(--red); }
.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; }
