/*
 * Torwarden components — SPEC §8.1.
 *
 * Every colour in here is a semantic alias from theme.css. No hex, no
 * primitive, no exceptions: that is what makes the theme one file to change,
 * and there is a test that enforces it.
 *
 * The shape of the interface is a control plane, not a document: a fixed rail,
 * a fixed topbar, and one scrolling region between them. Density is the point —
 * an operator looking at ten hosts should not have to scroll to find out
 * whether anything is broken.
 */

*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute has to win.
   
   It is `display: none` in the user-agent stylesheet, which any component rule
   setting `display: flex` or `display: grid` outranks — so a drawer marked
   hidden rendered open, and four panel inspectors stacked on top of each other.
   The attribute is the only way this app has to say "not now" without a class,
   and it has to mean it. */
[hidden] { display: none !important; }

html { height: 100%; }

body {
  margin: 0;
  height: 100%;
  background: var(--canvas);
  color: var(--fg);
  font-family: var(--sans);
  font-size: var(--t-3);
  line-height: 1.5;
  /* On the body so it is the default rather than something each new component
     has to remember — and one of them would not. §8.5: metric values must not
     shift width as they update. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }

.mono { font-family: var(--mono); font-size: .94em; }
.muted { color: var(--fg-muted); }
.text-ok { color: var(--status-ok-fg); }
.text-warn { color: var(--status-warn-fg); }
.text-crit { color: var(--status-crit-fg); }

/* A tracked-out micro-label. This is the workhorse of the whole design: it is
   what lets a dense panel say what a number is without spending a line of body
   text on it. */
.section-label, .tile-label, .vedit-label, .field-label, .action-label, .tl-label,
.widget-title, .vpanel-title, .chart-title, .band-label, .k {
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

/* ============================================================ the shell === */

/* Rail, topbar and content are a grid rather than fixed positioning, so the
   content column cannot slide under the rail at any width. */
body {
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  grid-template-rows: 100%;
  transition: grid-template-columns var(--dur-2) var(--ease);
}
:root[data-rail="collapsed"] { --rail-w: 64px; }

.sidebar {
  grid-row: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface);
  border-right: var(--edge-w) solid var(--edge);
  overflow: hidden;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  height: 52px;
  flex: none;
  padding: 0 var(--s-4);
  font-size: var(--t-4);
  font-weight: 650;
  letter-spacing: -.02em;
  color: var(--fg-strong);
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}

/* The brand mark. This used to be a green dot captioned "the instance is up",
   which was ornament wearing a signal's clothes — it was green unconditionally,
   and if you can see the page at all then the instance is up. The tower says
   something the dot could not, and it is a far better icon in the collapsed
   rail, where it is the only thing left.

   currentColor, so it inverts with the theme rather than being two files. */
/* The wordmark beside the mark, in a span rather than loose in the div.
   Collapsing the rail hides `.sidebar-brand > :not(.brand-mark)`, and a child
   combinator matches elements — a bare text node is not one. So the name was
   never actually hidden: it overflowed the 56px rail and shoved the mark out
   of sight, which is the state it had been in since the rail was built. */
.brand-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }


.brand-mark {
  width: 22px; height: 22px;
  flex: none;
  color: var(--fg-strong);
}

@keyframes pulse-ring {
  0%, 100% { box-shadow: 0 0 0 3px var(--status-ok-glow); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}

.sidebar-nav {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s-3) var(--s-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Nav groups (SPEC §14.1).
   Fourteen flat items is a list you read; four labelled groups is a structure
   you scan. The labels are the only thing separating "where do I look at
   things" from "where do I change things", which is the distinction people
   actually navigate by. */
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-group + .nav-group { margin-top: var(--s-4); }
.nav-group-label {
  padding: 0 var(--s-3) var(--s-2);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-muted);
  opacity: .75;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 7px var(--s-3);
  border-radius: var(--r-2);
  color: var(--fg-muted);
  font-size: var(--t-3);
  font-weight: 500;
  white-space: nowrap;
  position: relative;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
/* A translucent white wash rather than a grey: it reads the same on the
   sidebar, in a popover and over a card, where a fixed grey only reads right
   on one of them. */
.nav-item:hover { background: rgba(255, 255, 255, .06); color: var(--fg-strong); text-decoration: none; }
.nav-item:active { background: rgba(255, 255, 255, .09); }
.nav-item.active {
  background: var(--accent-soft);
  color: var(--accent-text);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
}

/* A rail marker rather than a filled block: the active row should read as
   "you are here", not as a button that is stuck down.
   
   view-transition-name is what makes it slide between routes instead of
   snapping. This is a server-rendered application — every navigation is a new
   document — so a JavaScript sliding indicator would have nothing to animate
   between. Cross-document view transitions do: the browser matches the two
   elements by name across the navigation and tweens them itself, with no
   script at all. Browsers without it simply do not animate, which is the
   correct degradation. */
.nav-item.active::before {
  content: "";
  position: absolute;
  left: -8px; top: 50%;
  width: 3px; height: 18px;
  transform: translateY(-50%);
  border-radius: 0 2px 2px 0;
  background: var(--accent);
  view-transition-name: nav-active;
}
.nav-icon { width: 16px; height: 16px; flex: none; }

.nav-badge {
  margin-left: auto;
  min-width: 18px;
  padding: 1px 5px;
  border-radius: var(--r-pill);
  background: var(--status-crit-bg);
  color: var(--status-crit-fg);
  border: 1px solid var(--status-crit-line);
  font-size: var(--t-1);
  font-weight: 650;
  text-align: center;
}

/* The viewer row in the sidebar footer. */
.viewer { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); }
.viewer-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; }
.viewer-name {
  font-size: var(--t-2); font-weight: 600; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.viewer-role { font-size: 10px; line-height: 1.3; }
.viewer-actions { display: flex; gap: var(--s-2); margin-bottom: var(--s-2); }
.viewer-actions form { margin: 0; }
.avatar {
  flex: none;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: var(--edge-w) solid var(--edge);
  background: var(--surface-3);
  color: var(--fg-strong);
  font-family: var(--display);
  font-size: 12px; font-weight: 900; line-height: 1;
  /* The glyph is optically low in a circle at this size unless it is nudged. */
  text-indent: .02em;
  user-select: none;
}

.sidebar-footer {
  flex: none;
  padding: var(--s-3);
  border-top: 1px solid var(--hairline);
  font-size: var(--t-1);
  color: var(--fg-dim);
  white-space: nowrap;
}

/* Collapsed rail: icons only. Labels are hidden rather than removed so the
   accessible name survives. */
:root[data-rail="collapsed"] .sidebar-brand { justify-content: center; padding: 0; }
:root[data-rail="collapsed"] .nav-item { justify-content: center; padding: 7px 0; }
:root[data-rail="collapsed"] .nav-item.active::before { left: 0; }
:root[data-rail="collapsed"] .sidebar-brand > :not(.brand-mark),
:root[data-rail="collapsed"] .nav-item > :not(.nav-icon):not(.nav-badge),
:root[data-rail="collapsed"] .sidebar-footer,
:root[data-rail="collapsed"] .nav-label { display: none; }
/* Collapsed rail: a five-letter label truncated to nothing is worse than no
   label, so the grouping is carried by a rule instead. The heading stays in the
   accessible tree — a screen reader still hears the structure a sighted user
   now sees as a line. */
:root[data-rail="collapsed"] .nav-group-label {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
:root[data-rail="collapsed"] .nav-group + .nav-group {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--hairline);
}

:root[data-rail="collapsed"] .nav-badge {
  position: absolute; top: 2px; right: 6px; margin: 0;
  min-width: 0; padding: 0 3px; font-size: 9px;
}

.main {
  grid-row: 1;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

/* ---------------------------------------------------------------- topbar */
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  height: 52px;
  padding: 0 var(--s-5);
  background: color-mix(in srgb, var(--canvas) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: var(--edge-w) solid var(--edge);
}

.rail-toggle {
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  flex: none;
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-1);
  background: transparent;
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.rail-toggle:hover { color: var(--fg); border-color: var(--hairline-strong); }

.topbar-title {
  font-size: var(--t-3);
  font-weight: 600;
  color: var(--fg-strong);
  letter-spacing: -.01em;
  white-space: nowrap;
}

/* The command palette trigger. Sized like a search field because that is what
   people will try to click, and labelled with its shortcut because that is
   what makes them stop clicking it. */
.cmdk-trigger {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: min(340px, 34vw);
  margin-left: auto;
  padding: 5px var(--s-3);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-2);
  background: var(--surface);
  color: var(--fg-dim);
  font: inherit;
  font-size: var(--t-2);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.cmdk-trigger:hover { background: var(--surface-2); }
.rail-toggle:active, .cmdk-trigger:active, .live-pill:active,
.filter-chip:active, .range-bar .range-btn:active {
  transform: var(--press-sm); box-shadow: none;
}
.cmdk-trigger .kbd { margin-left: auto; }

.kbd {
  font-family: var(--mono);
  font-size: 10px;
  padding: 1px 5px;
  border: 1px solid var(--edge);
  border-radius: var(--r-1);
  color: var(--fg-dim);
  background: var(--surface-2);
}

/* Live tick: the instance is talking to you right now. */
.live-dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--status-ok-fill);
  flex: none;
}
.live-pill {
  display: flex; align-items: center; gap: 6px;
  flex: none;
  padding: 3px var(--s-2);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-pill);
  font-size: var(--t-1);
  color: var(--fg-dim);
  white-space: nowrap;
}
.live-pill[data-live="on"] .live-dot { animation: pulse-ring 2s var(--ease) infinite; }
.live-pill[data-live="off"] .live-dot { background: var(--status-unknown-fill); }

.page {
  flex: 1;
  padding: var(--s-5);
  max-width: 1680px;
  width: 100%;
}

/* ================================================== command palette ====== */
.cmdk-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(0, 0, 0, .48);
  backdrop-filter: blur(6px);
  display: grid;
  place-items: start center;
  padding-top: 12vh;
}
.cmdk-backdrop[hidden] { display: none; }

.cmdk-box {
  width: min(560px, 92vw);
  /* Vibrancy, at an opacity that is still readable. A genuinely translucent
     panel is one whose text sits on whatever happens to be behind it, which on
     a dashboard is a chart. 88% dark fill plus a heavy blur reads as glass and
     stays legible over anything. */
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  animation: cmdk-in var(--dur-2) var(--ease-out);
}
@keyframes cmdk-in {
  from { opacity: 0; transform: translateY(-8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.cmdk-input {
  width: 100%;
  padding: var(--s-4);
  border: 0;
  border-bottom: 1px solid var(--hairline);
  background: transparent;
  color: var(--fg-strong);
  font: inherit;
  font-size: var(--t-4);
  outline: none;
}
.cmdk-list { max-height: 44vh; overflow-y: auto; padding: var(--s-2); margin: 0; list-style: none; }
.cmdk-item {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-2);
  cursor: pointer;
  color: var(--fg);
  font-size: var(--t-3);
}
.cmdk-item[aria-selected="true"] { background: var(--accent-soft); color: var(--accent-text); }
.cmdk-kind {
  margin-left: auto;
  font-size: var(--t-1);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.cmdk-empty { padding: var(--s-5); text-align: center; color: var(--fg-dim); font-size: var(--t-2); }

/* ================================================== page furniture ======= */
.page-header, .page-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
}
/* A header action sits at the far end, away from the title it is not part of. */
.page-header > .btn:last-child, .page-head > .btn:last-child { margin-left: auto; }
/* Heavy and set in caps. The display face is the other half of this language:
   hard edges around light type reads as a wireframe, not as print. */
.page-title {
  font-family: var(--display);
  font-size: var(--t-6); font-weight: 900; letter-spacing: -.02em;
  text-transform: uppercase; color: var(--fg-strong);
}
.page-sub, .sub, .hint { color: var(--fg-muted); font-size: var(--t-2); }
.page-actions { margin-left: auto; display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }
.page-back { font-size: var(--t-2); color: var(--fg-muted); }
.section-title {
  font-family: var(--display);
  font-size: var(--t-4); font-weight: 900; letter-spacing: .01em;
  text-transform: uppercase; color: var(--fg-strong);
  margin: var(--s-5) 0 var(--s-3);
}
.section { margin-bottom: var(--s-5); }
.meta-row { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); color: var(--fg-muted); font-size: var(--t-2); }
.empty, .widget-empty {
  padding: var(--s-6) var(--s-5);
  text-align: center;
  color: var(--fg-muted);
  font-size: var(--t-2);
}
/* A mark above the sentence, drawn rather than illustrated: three bars and a
   flat line, which is what an empty chart of anything looks like. It is there
   so an empty panel reads as a considered state rather than as a panel that
   failed to render — the same distinction the skeleton draws, one step later. */
.empty::before, .widget-empty::before {
  content: "";
  display: block;
  width: 44px;
  height: 30px;
  margin: 0 auto var(--s-3);
  opacity: .5;
  background:
    linear-gradient(var(--fg-dim), var(--fg-dim)) 0 100% / 8px 12px no-repeat,
    linear-gradient(var(--fg-dim), var(--fg-dim)) 18px 100% / 8px 22px no-repeat,
    linear-gradient(var(--fg-dim), var(--fg-dim)) 36px 100% / 8px 7px no-repeat,
    linear-gradient(var(--hairline-strong), var(--hairline-strong)) 0 100% / 100% 1px no-repeat;
}

/* ================================================== cards and widgets ==== */
.card, .widget, .chart-card, .vpanel, .vdash-card, .alert-panel, .exp-panel,
.install-box, .preview-pane, .vedit-panel, .auth-card {
  /* A cut sheet laid on the page, not a region of it (§24).
     
     Three declarations do the whole job and they have to agree: an edge darker
     than the fill, a solid un-blurred offset behind it, and a radius soft
     enough that the offset is the only hard thing about the card. Blur any of
     it and the language collapses back into ordinary material elevation. */
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-hard);
}
.card, .widget, .chart-card, .vpanel, .alert-panel { padding: var(--pad-card); }

/* Hover elevation. Only on things that go somewhere — a card that lifts and
   then does nothing when clicked is a lie about affordance. */
.vdash-card, a.card, .widget-grid a.card {
  transition: transform var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
}
.vdash-card:hover, a.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-2);
  border-color: var(--accent-line);
  text-decoration: none;
}

.widget-head, .vpanel-head, .chart-header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--hairline);
}
/* A heading inside a widget that already has one — "Busiest CPU" under "Under
   load". Lighter than the widget's own head, or the panel reads as two panels. */
.widget-head-sub { margin-bottom: var(--s-2); padding-bottom: var(--s-1); border-bottom: none; }
.widget-link { margin-left: auto; font-size: var(--t-1); color: var(--fg-dim); white-space: nowrap; }
.widget-link:hover { color: var(--accent-text); }

/* The asymmetric grid. Twelve columns, and panels claim spans by importance
   rather than all being the same size — what is broken is big, what is fine
   is small. */
.widget-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--s-4);
  align-items: start;
}
.widget { grid-column: span 6; min-width: 0; }
.widget-wide { grid-column: span 12; }
.widget-lead { grid-column: span 8; }
.widget-side { grid-column: span 4; }
@media (max-width: 1100px) {
  .widget, .widget-lead, .widget-side, .widget-wide { grid-column: span 12; }
}

/* ================================================== the fleet ribbon ===== */
/* The signature element. One cell per host, worst first — the whole estate as
   a single line you can read without counting. It is the most characteristic
   object in Torwarden's world and it earns the top of the page. */
.fleet-strip {
  display: flex;
  gap: 3px;
  margin-bottom: var(--s-4);
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--r-3);
  overflow: hidden;
}
.fleet-seg {
  flex: 1 1 0;
  min-width: 6px;
  height: 34px;
  border-radius: var(--r-1);
  background: var(--status-unknown-fill);
  position: relative;
  transition: transform var(--dur-1) var(--ease), filter var(--dur-1) var(--ease);
}
.fleet-seg:hover { transform: scaleY(1.14); filter: brightness(1.25); }
/* Muted fills, not the full-saturation status colours.
   
   The fleet strip is one segment per host across the top of the page, and at
   full saturation it became a band of glowing blocks that dominated a page
   whose actual subject is the alert list below it — the "harsh solid block"
   the brief is written against, in a different shape. Mixing toward the
   surface keeps the hue, which is the entire signal at that size, and lets the
   hostname on top stay legible without a plate behind it. Critical stays
   strongest, because that is the one that should catch an eye crossing the
   page. */
.fleet-seg.chip-ok       { background: color-mix(in srgb, var(--status-ok-fill) 38%, var(--surface-2)); }
.fleet-seg.chip-warning  { background: color-mix(in srgb, var(--status-warn-fill) 38%, var(--surface-2)); }
.fleet-seg.chip-critical {
  background: color-mix(in srgb, var(--status-crit-fill) 52%, var(--surface-2));
  box-shadow: 0 0 12px var(--status-crit-glow);
}
.fleet-seg.chip-offline  { background: color-mix(in srgb, var(--status-unknown-fill) 45%, var(--surface-2)); }
.fleet-seg.chip-maint    { background: color-mix(in srgb, var(--status-maint-fill) 38%, var(--surface-2)); }
/* The label needs to hold against every one of those, so it is white with a
   dark halo rather than a colour chosen per state. */
.fleet-seg .fleet-name { color: var(--fg-strong); text-shadow: 0 1px 2px rgba(0, 0, 0, .6); }

/* ================================================== the status band ====== */
.status-band {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
/* Capped rather than stretched: a healthy fleet has two or three of these and
   three cards spread across 1400px is a lot of ceremony for two numbers. */
.band-chip { flex: 1 1 180px; max-width: 300px; }
.band-chip {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--edge);
  border-left: 3px solid var(--status-unknown-fill);
  border-radius: var(--r-2);
  color: var(--fg);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.band-chip:hover { background: var(--surface-2); border-color: var(--hairline-strong); text-decoration: none; }
/* The count is the hero — §8.1. Big, tight, tabular, and the label is the
   caption underneath rather than a competing line of body text. */
.band-count {
  font-size: var(--t-6);
  font-weight: 650;
  letter-spacing: -.03em;
  line-height: 1.05;
  color: var(--fg-strong);
}
/* A band chip shares its state class with the little inline badges, and
   inherited their fill and their all-round border — so a summary tile became a
   solid block of colour, which is the thing a status summary must not be.
   Reset first, then colour only the rail and the number. */
.band-chip.chip-ok, .band-chip.chip-warning, .band-chip.chip-critical,
.band-chip.chip-offline, .band-chip.chip-maint {
  background: var(--surface-2);
  border-color: var(--hairline);
}
.band-chip.chip-ok       { border-left-color: var(--status-ok-fill); }
.band-chip.chip-warning  { border-left-color: var(--status-warn-fill); }
.band-chip.chip-critical { border-left-color: var(--status-crit-fill); }
.band-chip.chip-offline  { border-left-color: var(--status-unknown-fill); }
.band-chip.chip-maint    { border-left-color: var(--status-maint-fill); }
.band-chip:hover { background: var(--surface-3); }
.band-chip.chip-ok .band-count       { color: var(--status-ok-fg); }
.band-chip.chip-warning .band-count  { color: var(--status-warn-fg); }
.band-chip.chip-critical .band-count { color: var(--status-crit-fg); }
.band-chip.chip-maint .band-count    { color: var(--status-maint-fg); }
.band-delta { font-size: var(--t-1); color: var(--fg-dim); }
.band-delta.worse  { color: var(--status-crit-fg); }
.band-delta.better { color: var(--status-ok-fg); }

/* ================================================== tiles ================ */
/* auto-fill, not auto-fit, and a capped track. With auto-fit and 1fr, a page
   with two tiles gave each one half the window: two 700px cards holding the
   number 1 and the number 4. A summary tile is a glance, and a glance does not
   need seven hundred pixels. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 190px));
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.tile {
  padding: var(--s-4);
  background: var(--surface);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-hard-sm);
  min-width: 0;
}
.tile-value {
  display: block;
  font-size: var(--t-5);
  font-weight: 650;
  letter-spacing: -.02em;
  color: var(--fg-strong);
  line-height: 1.15;
}
.tile-label { display: block; margin-top: 2px; }
.tile-sub { display: block; font-size: var(--t-1); color: var(--fg-dim); margin-top: 2px; }

/* A number that just changed. The flash is the whole point of the live layer:
   it tells you the interface is reading, not that it once read. */
@keyframes tick-flash {
  from { color: var(--accent-text); }
  to   { color: inherit; }
}
.ticked { animation: tick-flash 900ms var(--ease); }

/* ================================================== status marks ========= */
/* §8.5: every status indicator conveys state without colour. Each of these
   carries a distinguishing shape as well as a hue, so the set survives
   greyscale and every form of colour blindness. */
.dot, .status-dot {
  display: inline-block;
  width: 8px; height: 8px;
  flex: none;
  border-radius: 50%;
  background: var(--status-unknown-fill);
  vertical-align: middle;
}
/* Scoped to the mark itself, and "a span" is not tight enough. statusClass
   returns these names for anything that carries a state: a <tr> in the host
   list, and the <span> fill inside a dashboard row's progress bar. Unscoped
   they painted every warning row as a full-width triangle; scoped to span they
   turned every warning bar into a 10px triangle. Only an element that *is* a
   status mark gets a shape. */
.status-dot.status-ok, .dot.status-ok { background: var(--status-ok-fill); border-radius: 50%; box-shadow: 0 0 0 2px var(--status-ok-glow); }
.status-dot.status-warning, .dot.status-warning {
  background: var(--status-warn-fill);
  border-radius: 0;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  width: 10px; height: 9px;
}
.status-dot.status-critical, .dot.status-critical { background: var(--status-crit-fill); border-radius: 1px; box-shadow: 0 0 0 2px var(--status-crit-glow); }
.status-dot.status-offline, .dot.status-offline { background: transparent; border: 1.5px solid var(--status-unknown-fill); border-radius: 50%; }
.status-dot.status-maint, .dot.status-maint { background: var(--status-maint-fill); border-radius: 1px; transform: rotate(45deg); }

/* A row's status is a tint on its leading edge, which is legible at a glance
   without making the text on top of it unreadable. */
tr.status-ok       td:first-child { box-shadow: inset 4px 0 0 var(--status-ok-fill); }
tr.status-warning  td:first-child { box-shadow: inset 4px 0 0 var(--status-warn-fill); }
tr.status-critical td:first-child { box-shadow: inset 4px 0 0 var(--status-crit-fill); }
tr.status-offline  td:first-child { box-shadow: inset 4px 0 0 var(--status-unknown-fill); }
/* A tint, not a wash. --status-crit-bg is the fill for a pill a few
   characters wide; stretched across a 1600px row it becomes a red band that
   makes the row harder to read rather than easier, and the left rail already
   says which row it is. Half strength, and the hover still moves. */
tr.status-critical { background: color-mix(in srgb, var(--status-crit-bg) 45%, transparent); }
tr.status-critical:hover { background: color-mix(in srgb, var(--status-crit-bg) 70%, transparent); }
.host-row td { vertical-align: middle; }

.dot.chip-ok       { background: var(--status-ok-fill); border-radius: 50%; }
.dot.chip-warning  { background: var(--status-warn-fill); clip-path: polygon(50% 0, 100% 100%, 0 100%); border-radius: 0; }
.dot.chip-critical { background: var(--status-crit-fill); border-radius: 1px; }
.dot.chip-offline  { background: transparent; border: 1.5px solid var(--status-unknown-fill); }
.dot.chip-maint    { background: var(--status-maint-fill); border-radius: 1px; transform: rotate(45deg); }

/* ================================================== chips ================ */
/* A chunky pill with a hard edge and solid fill, not a tinted lozenge (§24).
   
   Solid means the ink on it has to be dark, so every state carries its own
   --status-*-ink and a test measures the pair. A tint would have been the safer
   choice and the wrong one here: at 14% alpha a pill is a suggestion, and this
   language does not suggest. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 10px;
  border-radius: var(--r-pill);
  border: var(--edge-w) solid var(--edge);
  font-size: var(--t-1);
  font-weight: 700;
  letter-spacing: .03em;
  white-space: nowrap;
  vertical-align: middle;
  background: var(--status-unknown-fill);
  color: var(--status-unknown-ink);
}
.chip-sm { font-size: 10px; padding: 1px 8px; border-width: 1px; }
/* The shape marker inside a chip, so a chip is legible without its colour. */
.chip::before {
  content: "";
  width: 6px; height: 6px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}
.chip-ok       { background: var(--status-ok-fill);   color: var(--status-ok-ink); }
.chip-warning  { background: var(--status-warn-fill); color: var(--status-warn-ink); }
.chip-warning::before { border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.chip-critical { background: var(--status-crit-fill); color: var(--status-crit-ink); }
.chip-critical::before { border-radius: 1px; }
/* Offline stays hollow. Every other state is a filled block, so the one that
   means "we have no reading" is the one with nothing in it — which is the same
   argument the hatched tile makes on the fleet map. */
.chip-offline  { background: none; color: var(--status-unknown-fg); border-color: var(--status-unknown-fg); }
.chip-offline::before { background: transparent; border: 1.5px solid currentColor; }
.chip-maint    { background: var(--status-maint-fill); color: var(--status-maint-ink); }
.chip-maint::before { border-radius: 1px; transform: rotate(45deg); }
.chip-resolved { background: var(--status-unknown-bg); color: var(--fg-muted); border-color: var(--hairline-strong); }

.metric-tag, .tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px var(--s-2);
  border-radius: var(--r-1);
  background: var(--surface-3);
  border: 1px solid var(--edge);
  color: var(--fg-muted);
  font-family: var(--mono);
  font-size: var(--t-1);
  white-space: nowrap;
}
/* A metric tag now holds a phrase ("Filesystem usage"), not a key. Monospace is
   for things you might retype; it makes prose look like a fragment of config.
   A tag still holding a key — a host tag — keeps the mono above. */
.metric-tag { font-family: inherit; }
.tag-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.tag-add-btn {
  padding: 1px var(--s-2);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--r-1);
  background: transparent;
  color: var(--fg-dim);
  font: inherit;
  font-size: var(--t-1);
  cursor: pointer;
}
.tag-add-btn:hover { color: var(--accent-text); border-color: var(--accent-line); }

/* ================================================== buttons ============== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px var(--s-3);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-hard-sm);
  background: var(--surface-2);
  color: var(--fg);
  font: inherit;
  font-size: var(--t-2);
  font-weight: 550;
  line-height: 1.6;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease);
}
.btn:hover { background: var(--surface-3); text-decoration: none; }
/* The press moves the control exactly onto its own shadow, and the shadow goes
   to nothing. Not a nudge — the offset and the travel are the same token, so a
   button that is offset 3px travels 3px and lands flush. It is the one
   interaction this language has, and getting it a pixel wrong is what makes a
   neo-brutalist interface feel like a costume. */
.btn:active { transform: var(--press-sm); box-shadow: none; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }
/* Smaller and denser than a marketing button. A page action on a control
   plane is something an operator uses twenty times a day, not a call to
   action — at 15px in a 40px pill it dominates the page it sits above and
   makes everything else look like an afterthought. */
.btn-primary {
  background: var(--accent);
  color: var(--accent-fg);
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .01em;
  padding: 6px var(--s-3);
}
.btn-primary:hover { background: var(--accent-strong); border-color: transparent; }
.btn-danger { background: var(--status-crit-fill); border-color: var(--status-crit-fill); color: var(--fg-inverse); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--fg-muted); }
.btn-ghost:hover { background: var(--surface-3); color: var(--fg); }
.btn-sm { padding: 3px var(--s-2); font-size: var(--t-1); }
.btn-xs { padding: 1px 6px; font-size: var(--t-1); }

/* ================================================== forms ================ */
/* ── form controls ─────────────────────────────────────────────────────────
   Every control the browser would otherwise draw itself. A native select or
   file input on a dark page is a white rectangle with a system-blue button in
   it, and one of those in a form is enough to make the whole page look
   unfinished — so nothing here inherits a user-agent appearance.
   
   The type selector is deliberately long rather than a bare `input`: a bare
   selector also catches checkboxes and radios, which need their own treatment
   and look wrong given this one. */
/* input:not([type]) is in this list deliberately.
   
   A bare <input> is a text input — the HTML default — but it does not match
   input[type=text], so it fell through every rule below and got Chromium's own
   dark-mode field: a light grey box on a black form. It is the easiest control
   in the language to write and was the only one this stylesheet did not cover.
   The exclusions are the types that are buttons or widgets rather than fields
   and are styled on their own terms further down. */
input:not([type]),
input[type=text], input[type=password], input[type=number], input[type=search],
input[type=email], input[type=url], input[type=tel], input[type=date],
input[type=time], input[type=datetime-local], input[type=month], input[type=week],
select, textarea, .filter-input {
  appearance: none;
  -webkit-appearance: none;
  padding: 7px var(--s-3);
  border: var(--edge-w) solid var(--control-border);
  border-radius: var(--r-2);
  background: var(--control-bg);
  color: var(--fg);
  font: inherit;
  font-size: var(--t-2);
  line-height: 1.4;
  transition: border-color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--fg-dim); }
input:hover:not(:focus), select:hover:not(:focus), textarea:hover:not(:focus) {
  border-color: var(--border-strong);
}

/* A soft ring, not a hard outline: two shadows so the halo sits off the edge
   of the control rather than against it. */
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  background: var(--surface-2);
}

/* The select's own arrow, since appearance:none removed the browser's. An
   inline SVG data URI rather than a pseudo-element, because a select cannot
   have one. */
select {
  padding-right: var(--s-6);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%237b8194' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-3) center;
}
/* The dropdown itself is drawn by the OS and cannot be styled; this is the one
   hint the platform accepts about which palette to render it in. */
select option { background: var(--surface-2); color: var(--fg); }

/* A file input is two controls in one trench coat — a button and a label — and
   only the button half is addressable. */
input[type=file] {
  width: 100%;
  padding: var(--s-2);
  border: 1px dashed var(--control-border);
  border-radius: var(--r-2);
  background: var(--control-bg);
  color: var(--fg-muted);
  font-size: var(--t-2);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
input[type=file]:hover { border-color: var(--accent-line); background: var(--surface-2); }
input[type=file]::file-selector-button {
  margin-right: var(--s-3);
  padding: 5px var(--s-3);
  border: 1px solid var(--control-border);
  border-radius: var(--r-1);
  background: var(--surface-3);
  color: var(--fg);
  font: inherit;
  font-size: var(--t-2);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}
input[type=file]::file-selector-button:hover { background: var(--border); }

/* Checkboxes and radios, drawn rather than tinted. accent-color gets the fill
   right and leaves the box the platform's, which on Linux is a grey square
   with a black tick. */
input[type=checkbox], input[type=radio] {
  appearance: none;
  -webkit-appearance: none;
  width: 15px; height: 15px;
  flex: none;
  margin: 0;
  border: 1px solid var(--border-strong);
  background: var(--control-bg);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
input[type=checkbox] { border-radius: 4px; }
input[type=radio] { border-radius: 50%; }
input[type=checkbox]:hover, input[type=radio]:hover { border-color: var(--accent-line); }
input[type=checkbox]:checked, input[type=radio]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
input[type=checkbox]:checked {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 11 11' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 5.6 4.3 8 9 3'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
}
input[type=radio]:checked {
  /* An inset ring rather than a dot element, which a radio cannot have. */
  box-shadow: inset 0 0 0 3px var(--control-bg);
}

/* The range slider, which is four separate pseudo-elements across engines. */
input[type=range] {
  appearance: none;
  -webkit-appearance: none;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--border);
}
input[type=range]::-webkit-slider-thumb {
  appearance: none;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface-2);
  cursor: pointer;
}
input[type=range]::-moz-range-thumb {
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface-2);
  cursor: pointer;
}

/* The picker glyphs a date or time input draws are the browser's, in the
   browser's palette — a light calendar icon on a dark field, which is the
   single most obvious tell that a form was not finished. There is no way to
   replace them, so they are inverted to match. */
input[type=date]::-webkit-calendar-picker-indicator,
input[type=time]::-webkit-calendar-picker-indicator,
input[type=datetime-local]::-webkit-calendar-picker-indicator,
input[type=month]::-webkit-calendar-picker-indicator,
input[type=week]::-webkit-calendar-picker-indicator {
  filter: invert(1) opacity(.55);
  cursor: pointer;
}
input[type=date]::-webkit-calendar-picker-indicator:hover,
input[type=datetime-local]::-webkit-calendar-picker-indicator:hover {
  filter: invert(1) opacity(.9);
}
/* And the fields inside a date input, which inherit their own colours. */
::-webkit-datetime-edit { color: var(--fg); }
::-webkit-datetime-edit-fields-wrapper { padding: 0; }
::-webkit-datetime-edit-text { color: var(--fg-dim); padding: 0 1px; }
input[type=search]::-webkit-search-cancel-button { filter: invert(1) opacity(.45); cursor: pointer; }

/* Mono in a textarea because what goes in one here is PEM, JSON or a template
   — all of which are read by alignment. */
textarea { font-family: var(--mono); line-height: 1.5; }
label { font-size: var(--t-2); color: var(--fg-muted); }

.field, .form-group { display: flex; flex-direction: column; gap: 4px; margin-bottom: var(--s-3); }
.row-actions, .copy-row, .detail-actions, .route-row {
  display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap;
}

/* A grid, not a wrapping flex row.
   
   As flex, each field was as tall as its own help text, so a row of three
   fields whose captions ran to one, three and two lines put the next row's
   labels at three different heights — and every settings page had a ragged
   second row that read as a rendering fault rather than as a layout. A grid
   gives every cell in a row the same track, and the subgrid below aligns the
   label, the control and the caption across all of them. */
.field-row, .form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--s-3) var(--s-4);
  align-items: start;
  margin-bottom: var(--s-3);
}
.field-row > .field, .form-row > .form-group,
.field-row > .field-wide, .form-row > .form-group--wide {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  gap: 4px;
  margin-bottom: 0;
  /* A grid item's min-width is auto, which means min-content, and a <select>'s
     min-content is the width of its longest <option>. The metric picker holds
     sixty of them, so the track refused to shrink and the card overflowed by
     45px — visibly, on the guard row of the rule editor. This is the fix for
     every form row in the product, not just that one. */
  min-width: 0;
  min-width: 0;
}
/* subgrid needs the parent to have rows to inherit. Browsers without it fall
   back to the block flow above, which is the layout as it was — ragged, but
   not broken. */
@supports (grid-template-rows: subgrid) {
  .field-row, .form-row { grid-template-rows: auto auto auto; }
}
.form-group--wide, .field-wide { grid-column: span 2; }

/* Two columns, not "as many as fit".
   
   auto-fit is right for a summary of tiles and wrong for a form: on a wide
   monitor the settings page laid its fields out five across, which put a label
   and the box it names two hundred pixels apart and turned a form into a
   spreadsheet. A form is read down, so it gets a fixed pair of columns and a
   width that stops. */
.field-row-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .field-row-2 { grid-template-columns: minmax(0, 1fr); } }
@supports (grid-template-rows: subgrid) {
  .field-row-2 { grid-template-rows: auto auto auto; }
}
/* Controls take the width of the field, never set it. Without this a long
   option or a long placeholder decides how wide a column is. */
.field > input, .field > select, .field > textarea,
.form-group > input, .form-group > select, .form-group > textarea,
.form-group--wide > input, .form-group--wide > select, .form-group--wide > textarea {
  width: 100%;
  min-width: 0;
  max-width: 100%;
}

.field-help { font-size: var(--t-1); line-height: 1.45; color: var(--fg-muted); }

/* The settings column. One number, in one place, rather than a max-width
   written inline on each of nine cards — which is how they came to disagree. */
.settings-stack { max-width: 860px; }
.settings-form { max-width: 860px; }
.setting-block { display: flex; flex-direction: column; gap: var(--s-3); }
.setting-block > .section-title { margin: 0; }
.setting-blurb { margin: 0; font-size: var(--t-1); line-height: 1.5; color: var(--fg-muted); max-width: 68ch; }
.setting-note { margin: var(--s-3) 0 0; font-size: var(--t-2); }
.users-stack { max-width: 920px; }
.role-name { width: 100px; }

/* A form that lives inside a table row: a select and a button on one line,
   sized to their content rather than to the column. */
.row-form { display: flex; align-items: center; gap: var(--s-1); margin: 0; }
.row-form-cell { padding-top: 0; }
.select-inline, .input-inline {
  width: auto;
  padding: 3px 24px 3px 6px;
  font-size: var(--t-1);
}
.input-inline { max-width: 240px; padding-right: 6px; }

/* Autofill.
   
   Chromium paints its own background on an autofilled field and will not let a
   `background` declaration override it — the username and display-name boxes on
   this page came back light tan on a black form. The inset shadow is the
   documented way through: it covers the field's own painting area, so the
   colour that shows is one from the token layer rather than one from the
   browser's light-theme assumptions. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
select:-webkit-autofill,
textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--fg);
  -webkit-box-shadow: 0 0 0 1000px var(--control-bg) inset;
  box-shadow: 0 0 0 1000px var(--control-bg) inset;
  caret-color: var(--fg);
  transition: background-color 100000s ease-in-out 0s;
}
input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--control-bg) inset, 0 0 0 3px var(--accent-soft);
  box-shadow: 0 0 0 1000px var(--control-bg) inset, 0 0 0 3px var(--accent-soft);
}
.field-label { margin-bottom: 2px; }
.filter-bar {
  display: flex;
  gap: var(--s-2);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
  padding: var(--s-3);
  background: var(--surface);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-2);
}
.filter-input { min-width: 200px; }

.action-radio { display: inline-flex; align-items: center; gap: 5px; font-size: var(--t-2); }
/* ============================================ alert actions ============== */
/* Acknowledge, snooze and close are three different decisions with three
   different consequences. They used to sit in one flex row alongside the
   <details> summary: three forms of three different lengths, ragged, with the
   word "Actions" floating vertically centred beside them and the whole strip
   overlapping the next alert in the list.

   In flow rather than an absolute popover, deliberately. This component also
   renders inside a <td>, and .tbl-wrap is overflow-x: auto — a popover would be
   clipped to the cell. A panel that grows the row is less elegant than one that
   floats, and far better than one that is half invisible. */
details.row-actions { display: block; }
details.row-actions > summary { display: inline-flex; width: auto; cursor: pointer; }

.action-pop {
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-2);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  width: min(420px, 100%);
  margin-top: var(--s-3);
}

.action-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin: 0;
  padding: var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--edge);
  border-radius: var(--r-2);
}
.action-card .action-label { margin: 0; }
.action-card input, .action-card select { width: 100%; }
/* The radios are a choice about what happens next, not a form field, so they
   read as sentences rather than as labelled inputs. */
.action-card .action-radio {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  font-size: var(--t-2);
  line-height: 1.45;
  color: var(--fg-muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.action-card .action-radio input { width: auto; margin-top: 2px; flex: none; }
.action-card-hint { margin: 0; font-size: var(--t-1); color: var(--fg-muted); line-height: 1.45; }
.action-card-foot { display: flex; justify-content: flex-end; gap: var(--s-2); }
/* Each card's button is the primary action of that card, so none of them is a
   ghost. A transparent, borderless "Snooze" beside a filled "Acknowledge" and
   an outlined "Close" reads as a caption rather than a control. */
.action-card-foot .btn-ghost { border-color: var(--border); color: var(--fg); }
.view-toggle, .theme-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-2);
}
.view-toggle a, .view-toggle button, .theme-toggle button, .range-btn {
  padding: 2px var(--s-2);
  border: 0;
  border-radius: var(--r-1);
  background: transparent;
  color: var(--fg-muted);
  font: inherit;
  font-size: var(--t-1);
  cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.view-toggle a:hover, .theme-toggle button:hover, .range-btn:hover { color: var(--fg); text-decoration: none; }
.view-toggle a.active, .theme-toggle button[aria-pressed="true"], .range-btn.active {
  background: var(--surface); color: var(--accent-text); box-shadow: var(--shadow-1);
}
.range-sep { color: var(--fg-dim); padding: 0 2px; }
.range-custom { display: flex; gap: var(--s-2); align-items: center; }

/* ================================================== tables =============== */
/* A table is a panel, not loose rows on the page background.
   
   Bare full-width rows read as a debug dump: nothing tells the eye where the
   data starts and stops, and on a wide screen the last column drifts off
   towards a horizon of empty dark. The card gives it edges; the header rail
   gives it a top. */
.tbl-wrap {
  overflow-x: auto;
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-hard);
}
/* Nested inside a card already — a card in a card is a border nobody needs. */
.card > .tbl-wrap, .widget > .tbl-wrap, .vpanel > .tbl-wrap, .alert-panel > .tbl-wrap {
  background: none;
  border: none;
  border-radius: 0;
  /* And no second offset. Two hard shadows nested inside each other is the one
     way this language reads as a mistake rather than as a style. */
  box-shadow: none;
}
.tbl-wrap > .tbl thead th {
  position: sticky;
  top: 0;
  background: var(--surface-3);
  border-bottom: 1px solid var(--hairline);
  z-index: 1;
}
.tbl { width: 100%; border-collapse: collapse; font-size: var(--t-2); }
.tbl th {
  text-align: left;
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-dim);
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 1;
}
.tbl td {
  padding: 7px var(--s-3);
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
}
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody tr { transition: background var(--dur-1) var(--ease); }
.tbl tbody tr:hover { background: var(--surface-2); }
/* Numbers align on the decimal, which is the only reason to use tabular
   figures in the first place. */
.tbl td.mono, .tbl td.n, .n { text-align: right; font-variant-numeric: tabular-nums; }
.th-sort { cursor: pointer; color: var(--fg-dim); }
.th-sort:hover { color: var(--fg); }
/* A hostname is machine-facing text — an identifier you match character by
   character against a terminal, not a label you read. It gets the mono face and
   enough weight to be the thing the eye lands on first in a row of eleven
   columns. */
.host-link {
  font-family: var(--mono);
  font-size: var(--t-2);
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--fg-strong);
}
.host-link:hover { color: var(--accent-text); }

/* And it takes the row's state. The row already carries a status class for its
   left edge; colouring the name from the same class means the two cannot
   disagree, and it puts the state on the one word you were going to read
   anyway rather than only on a chip two columns away. */
.tbl tbody tr.status-critical .host-link,
.tbl tbody tr.is-crit .host-link { color: var(--status-crit-fg); }
.tbl tbody tr.status-warning .host-link,
.tbl tbody tr.is-warn .host-link { color: var(--status-warn-fg); }
.tbl tbody tr.status-offline .host-link { color: var(--status-unknown-fg); }
.vol-hl { background: var(--accent-soft); }

/* A table inside a card scrolls itself rather than pushing the card wide —
   the bug that cut the Memory column off the right edge of the host list. */
.widget > .tbl, .vpanel > .tbl { display: block; overflow-x: auto; }
.widget > .tbl thead, .widget > .tbl tbody,
.vpanel > .tbl thead, .vpanel > .tbl tbody { display: table; width: 100%; }

/* ================================================== progress ============= */
.prog-wrap, .prog {
  position: relative;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  overflow: hidden;
  min-width: 48px;
}
.prog-fill, .prog > span {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur-2) var(--ease);
}
.prog-fill.chip-ok, .prog-fill.prog-ok           { background: var(--status-ok-fill); }
.prog-fill.chip-warning, .prog-fill.prog-warning { background: var(--status-warn-fill); }
.prog-fill.chip-critical, .prog-fill.prog-critical { background: var(--status-crit-fill); }

/* A value next to its bar, which is the pairing §8.1 asks for: the bar gives
   the shape at a glance and the number gives the precision. */
.bar-cell { display: flex; align-items: center; gap: var(--s-2); }
.bar-cell .prog-wrap { flex: 1; }
.bar-cell .bar-val { width: 52px; text-align: right; font-family: var(--mono); font-size: var(--t-2); }

/* ================================================== banners ============== */
/* A banner is a card that is telling you something, so it takes the same edge
   and offset. Without them it was the one full-width element on the page still
   drawn in the old language. */
.banner, .notice, .stale-banner, .rule-error, .bulk-bar {
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--fg-dim);
  border-radius: var(--r-2);
  background: var(--surface);
  font-size: var(--t-2);
  margin-bottom: var(--s-4);
}
.banner-error, .notice-error, .rule-error {
  border-left-color: var(--status-crit-fill);
  background: var(--status-crit-bg);
  color: var(--status-crit-fg);
}
.banner-ok, .notice-ok, .notice-success {
  border-left-color: var(--status-ok-fill);
  background: var(--status-ok-bg);
  color: var(--status-ok-fg);
}
.stale-banner { border-left-color: var(--status-warn-fill); background: var(--status-warn-bg); color: var(--status-warn-fg); }
/* The error case was an inline style carrying two hand-written reds, so it kept
   the palette from two designs ago while the banner beside it moved on. */
.stale-banner.is-error { border-left-color: var(--status-crit-fill); background: var(--status-crit-bg); color: var(--status-crit-fg); }
.bulk-spacer { flex: 1; }
.bulk-reason { min-width: 200px; }
.bulk-minutes { width: 80px; }
.bulk-bar { border-left-color: var(--accent); background: var(--accent-soft); position: sticky; bottom: var(--s-4); z-index: 20; }
.install-box, .preview-pane { padding: var(--s-4); font-family: var(--mono); font-size: var(--t-2); overflow-x: auto; }
.notice { margin-bottom: var(--s-3); }

/* ================================================== charts =============== */
.chart-card { display: flex; flex-direction: column; gap: var(--s-2); }
.chart-header { margin-bottom: 0; }
.chart-value {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--t-4);
  font-weight: 600;
  color: var(--fg-strong);
}
.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: var(--s-4);
}
/* A sparkline is a shape, not a chart: no axes, no labels, just the last hour
   of a series sitting behind its own number. */
.spark { display: block; width: 100%; height: 30px; overflow: visible; }
.spark path { fill: none; stroke: var(--accent-text); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
.spark .spark-area { fill: var(--accent-soft); stroke: none; }
.spark.ok path   { stroke: var(--status-ok-fill); }
.spark.warn path { stroke: var(--status-warn-fill); }
.spark.crit path { stroke: var(--status-crit-fill); }

.u-legend { font-size: var(--t-1) !important; color: var(--fg-muted) !important; }
.u-title { color: var(--fg-strong) !important; }

/* ================================================== timeline ============= */
.timeline { display: flex; flex-direction: column; gap: 0; }
.tl-time, .tl-label, .tl-detail { font-size: var(--t-2); }
.timeline > * {
  display: grid;
  grid-template-columns: 84px 120px 1fr;
  gap: var(--s-3);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--hairline);
}
.timeline > *:last-child { border-bottom: 0; }
.tl-time { font-family: var(--mono); color: var(--fg-dim); font-size: var(--t-1); }
.tl-detail { color: var(--fg-muted); }

/* ================================================== detail pages ========= */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s-3) var(--s-5);
  margin-bottom: var(--s-4);
}
.k { display: block; }
/* One alert on one line: what it is on the left, how long it has been that way
   on the right, and the action that changes it at the end.
   
   It was a *column* flex, which stacked those three and — because a column
   flex stretches its children by default — gave the Actions button the full
   width of the panel. A 1276px button for a word.
   
   The resolved list uses the same row, so "resolved · mem.used_pct · lasted
   59m" reads as one line with a rule under it rather than as three stacked
   fragments. */
.alert-mini {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 9px 0;
  border-bottom: 1px solid var(--hairline);
  min-width: 0;
}
.alert-mini:last-child { border-bottom: 0; }
.alert-mini > a { flex: 1 1 auto; min-width: 0; }
.alert-mini > span:first-child { flex: 1 1 auto; min-width: 0; }
/* The trailing metadata sits against the actions, not against the text. */
.alert-mini > .muted { margin-left: auto; flex: none; white-space: nowrap; }
.alert-mini .act-open { flex: none; }
.rule-summary { font-size: var(--t-2); color: var(--fg-muted); }
.exp-hdr {
  display: flex; align-items: center; gap: var(--s-2);
  cursor: pointer; padding: var(--s-2) 0; font-size: var(--t-2); font-weight: 550;
}
.exp-chevron { transition: transform var(--dur-1) var(--ease); color: var(--fg-dim); }
.exp-hdr[aria-expanded="true"] .exp-chevron { transform: rotate(90deg); }
.exp-panel { padding: var(--s-3); margin-bottom: var(--s-3); }

/* ================================================== dashboards =========== */
.vdash-grid, .editor-grid, .vedit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--s-4);
}
.vdash-card { padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-2); }
.vdash-card-title { font-size: var(--t-4); font-weight: 600; color: var(--fg-strong); }
.vdash-card-desc { font-size: var(--t-2); color: var(--fg-muted); flex: 1; }
.vdash-card-meta { font-size: var(--t-1); color: var(--fg-dim); }
.vpanel { min-width: 0; }
.vedit-panel { padding: var(--s-3); }
.vedit-form { display: flex; flex-direction: column; gap: var(--s-2); }
.vedit-controls { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }
.vedit-row { display: flex; gap: var(--s-2); align-items: center; }
.vedit-add { display: flex; gap: var(--s-2); align-items: center; margin-bottom: var(--s-3); }
.editor-grid { align-items: start; }

/* ================================================== endpoints =========== */
/* Utility colours for a value that is itself the status, where a chip would be
   too much furniture — a tile's number, or an expiry date in a table cell. */
.text-ok   { color: var(--status-ok-fg); }
.text-crit { color: var(--status-crit-fg); }

/* Right-aligned figures line up on the decimal point rather than drifting with
   the glyph widths. */
.num { text-align: right; font-variant-numeric: tabular-nums; }
th.num, td.num { text-align: right; }

.tile-unit { font-size: var(--t-2); color: var(--fg-muted); margin-left: 2px; font-weight: 400; }

/* The URL under an endpoint's name: present, because it is what is actually
   being probed, but quiet, because the name is what people scan for. */
.ep-url {
  font-size: var(--t-1);
  color: var(--fg-dim);
  margin-top: 2px;
  max-width: 42ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-checks { display: flex; flex-wrap: wrap; gap: 4px; }
/* The reason it is down, in the row. Wraps rather than truncating: "connection
   refused" and "certificate expired 3 days ago" are different problems and the
   distinction is the whole value of the column. */
.ep-error {
  font-size: var(--t-1);
  color: var(--fg-muted);
  margin-top: 3px;
  max-width: 46ch;
  line-height: 1.4;
}

/* ================================================== rule editor ========== */
/* The form used .editor-grid — repeat(auto-fill, minmax(260px, 1fr)) — which
   builds as many columns as will fit and then puts the form's two children in
   the first two of them. Measured at 1600px wide: four 318px columns, 684px
   used, 636px of empty page to the right, and every field wrapping onto its own
   line inside a well too narrow to hold two. auto-fill is for a variable number
   of equal things; this is two fixed things of different weights, so it says so.
   The form takes what is left, the preview takes a column wide enough to read a
   paragraph in. */
.rule-editor {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: var(--s-5);
  align-items: start;
}
.rule-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
  align-items: start;
}
.rule-card { padding: var(--s-4); margin: 0; }
.rule-card--wide { grid-column: 1 / -1; }
.rule-card .section-label { margin-bottom: var(--s-3); }

/* Fields share a row instead of each claiming a line. The basis is what makes
   a threshold pair sit side by side in a half-width card and still wrap on a
   phone, without a media query per card. */
.rule-card .form-group { flex: 1 1 150px; min-width: 0; margin-bottom: 0; }
.rule-card .form-row { align-items: flex-start; margin-bottom: var(--s-3); }
.rule-card .form-row:last-child { margin-bottom: 0; }
.rule-card .form-group--wide { flex: 2 1 240px; }

.rule-hint { font-size: var(--t-1); color: var(--fg-muted); margin: var(--s-3) 0 0; }
/* A hint used to be the last thing in a card, so it only needed a gap above
   it. In a card long enough to explain itself twice it sits between two
   groups of fields, and without this the sentence touches the next label. */
.rule-card .rule-hint:not(:last-child) { margin-bottom: var(--s-3); }
.rule-toggles {
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4);
  margin-top: var(--s-3);
}
.rule-actions {
  grid-column: 1 / -1;
  display: flex; gap: var(--s-3); align-items: center;
}

/* The preview follows a form taller than the screen. It is the thing you are
   checking each edit against, so scrolling away from it defeats the point. */
.preview-pane.is-sticky { position: sticky; top: var(--s-4); max-height: calc(100vh - var(--s-8)); overflow-y: auto; }

@media (max-width: 1180px) {
  .rule-editor { grid-template-columns: minmax(0, 1fr); }
  .preview-pane.is-sticky { position: static; max-height: none; }
}
@media (max-width: 820px) {
  .rule-cards { grid-template-columns: minmax(0, 1fr); }
}

/* ================================================== auth ================= */
/* The mark above the sign-in heading. This is the one screen where the logo has
   room to be itself, and the first thing anybody sees of the product. */
.auth-mark {
  width: 44px;
  height: 44px;
  display: block;
  margin: 0 0 var(--s-3);
  color: var(--fg-strong);
}

.auth-card {
  width: min(400px, 92vw);
  margin: 12vh auto;
  padding: var(--s-6);
  box-shadow: var(--shadow-2);
}
body.bare { grid-template-columns: 1fr; background: var(--canvas); }

/* ================================================== motion ============== */
/* Content arrives in one short, staggered pass rather than all at once. The
   delay is capped so a long page does not animate for a second and a half. */
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* `backwards`, not `both` — and the difference is structural, not cosmetic.
   
   `both` retains the animation's final `transform: none`, which computes as an
   identity matrix and stays applied forever. Any transform makes an element a
   containing block for `position: fixed` descendants, so every direct child of
   .page was silently redefining where "the viewport" is: a drawer inside the
   alerts section was 154px tall in a 1000px window, because that is how tall
   the section is.
   
   `backwards` applies the from-state before the animation starts and lets the
   element return to its own styles afterwards. The `to` keyframe is already
   the natural state, so nothing moves when it ends. */
.page > * { animation: rise var(--dur-2) var(--ease) backwards; }
/* Except the overlays, and this is not cosmetic.
   
   `fill-mode: both` leaves the animation's final `transform: none` applied, and
   an applied transform — even an identity one — makes the element a containing
   block for every `position: fixed` descendant. So a drawer rendered anywhere
   inside the page was being positioned and sized against its section rather
   than the viewport: measured at 1312px tall in a 1000px window, with the
   bottom of it simply off the screen.
   
   Overlays should not arrive with the page anyway; they arrive when you open
   them. */
.page > .drawer, .page > .drawer-scrim,
.page .drawer, .page .drawer-scrim { animation: none; }
.page > :nth-child(1) { animation-delay: 0ms; }
.page > :nth-child(2) { animation-delay: 30ms; }
.page > :nth-child(3) { animation-delay: 60ms; }
.page > :nth-child(n+4) { animation-delay: 90ms; }

/* ================================================== responsive ========== */
@media (max-width: 860px) {
  /* One full-width column, not a zero-width one.
     `grid-template-columns: 0` defines a single track of width 0. The sidebar
     is display:none here, which removes it from the grid entirely, so .main
     becomes the first grid item, lands in that 0px track, and the entire
     application renders blank at every viewport below 860px. It looked correct
     in the source — a collapsed rail is 0 wide — and nothing on any page was
     broken enough to notice, because there were no pages at all. */
  body { grid-template-columns: minmax(0, 1fr); }

  /* The rail becomes a drawer instead of disappearing.
     It used to be display:none with nothing in its place, which left the
     application with no navigation at all below 860px: the toggle still
     flipped data-rail, no rule listened, and the only ways to reach another
     page were the command palette or typing the URL. */
  .sidebar {
    position: fixed;
    top: 0; bottom: 0; left: 0;
    z-index: 60;
    width: min(82vw, 300px);
    transform: translateX(-100%);
    transition: transform 160ms ease;
    overflow-y: auto;
  }
  :root[data-nav="open"] .sidebar { transform: none; }
  /* Tapping anywhere off the drawer closes it; shell.js watches for that. */
  :root[data-nav="open"] body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 55;
    background: var(--scrim);
  }
  .page { padding: var(--s-4); }
  .cmdk-trigger { width: auto; }
  .cmdk-trigger .cmdk-placeholder { display: none; }
}

/* One template says class="table" where the rest say "tbl". Aliased rather
   than renamed: the markup is the source of truth for what a component is
   called, and 13a's mistake was deciding otherwise. */
.table { width: 100%; border-collapse: collapse; font-size: var(--t-2); }
.table th {
  text-align: left; font-size: var(--t-1); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-dim);
  padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--hairline);
}
.table td { padding: 7px var(--s-3); border-bottom: 1px solid var(--hairline); }
.table tbody tr:last-child td { border-bottom: 0; }

/* Empty states, kept reachable but not given a headline. */
.band-quiet {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-4);
  margin: calc(var(--s-3) * -1) 0 var(--s-4);
  padding-left: 2px;
}
.band-zero { font-size: var(--t-1); color: var(--fg-dim); letter-spacing: .04em; text-transform: uppercase; }
.band-zero:hover { color: var(--fg-muted); text-decoration: none; }

/* A ten-host ribbon stretched edge to edge reads as a bar chart of nothing, so
   segments are capped and the name sits inside. Past ~32 hosts the name cannot
   fit at any size and the ribbon goes back to being pure colour, which is what
   makes it work at five hundred. */
.fleet-seg { max-width: 120px; display: flex; align-items: flex-end; padding: 3px 5px; overflow: hidden; }
.fleet-name {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--fg-inverse);
  opacity: .82;
  white-space: nowrap;
}
.fleet-strip { justify-content: flex-start; }
.fleet-dense .fleet-seg { max-width: none; padding: 0; }
.fleet-dense .fleet-name { display: none; }

/* Licensing seat meter and the alert filter chips — both were unstyled, and
   both were only found once the class check stopped being fooled by a quote
   inside a template expression. */
.prog-bar {
  display: block;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur-2) var(--ease);
}
.prog-bar.prog-ok   { background: var(--status-ok-fill); }
.prog-bar.prog-warn { background: var(--status-warn-fill); }
.prog-bar.prog-crit { background: var(--status-crit-fill); }

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px var(--s-3);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--fg-muted);
  font-size: var(--t-2);
  white-space: nowrap;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.filter-chip:hover { color: var(--fg); border-color: var(--hairline-strong); text-decoration: none; }
.filter-chip.active { background: var(--accent-soft); color: var(--accent-text); border-color: var(--accent-line); }

.tl-row {
  display: grid;
  grid-template-columns: 84px 120px 1fr;
  gap: var(--s-3);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--hairline);
  align-items: baseline;
}
.tl-row:last-child { border-bottom: 0; }

/* Sparkline cells. A canvas has an intrinsic 300px width, so inside a flex row
   it overran its cell and printed the value on top of the line — and the
   volume column ran into the load column beside it. The canvas flexes, the
   number does not. */
.host-row .spark { flex: 1 1 auto; width: auto; min-width: 36px; height: 26px; }
.host-row canvas.spark + .mono { flex: none; width: 50px; text-align: right; }
.host-row td { white-space: nowrap; }
/* The host list is the widest table in the product and the one the heavier
   chips and pills cost the most. These three give back what they took: the OS
   string was already being ellipsised and did not need 150px to be ellipsised
   in, the row actions do not need to be full-size buttons, and a status
   qualifier is not a second status. */
.os-cell { max-width: 108px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.host-row td:last-child .btn { font-size: 10px; padding: 3px 8px; }
.status-cell { flex-wrap: wrap; row-gap: 3px; }

/* Destructive actions are available, not loud. Ten rows of solid red Delete
   buttons make the one row that is actually critical harder to find. */
.btn-danger {
  background: transparent;
  border-color: var(--status-crit-line);
  color: var(--status-crit-fg);
}
.btn-danger:hover { background: var(--status-crit-bg); border-color: var(--status-crit-fg); }

/* ================================================== chart internals ====== */
/* These classes are built by chart.js rather than written in a template, so
   the template-class check cannot see them — and they were unstyled, which is
   why every legend read "CPU usage33.2%min 0.0% · max 39.9%" with the label,
   the value and the statistics run together. */
.vchart { position: relative; width: 100%; }

.vchart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid var(--hairline);
}
/* Two fixed lines: identity and reading on the first, the range statistics on
   the second. The height is a property of the structure rather than of how
   much text happens to be in it, so entering and leaving the plot cannot move
   anything — and nothing has to be clipped to achieve that. */
.vchart-legend-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "swatch label value" "sub sub sub";
  align-items: baseline;
  gap: 1px var(--s-2);
  flex: 1 1 210px;
  min-width: 0;
  max-width: 100%;
}
.vchart-swatch {
  grid-area: swatch;
  width: 8px; height: 8px;
  border-radius: 50%;
  align-self: center;
  background: var(--fg-dim);
  /* A soft ring in the series' own colour. currentColor is set inline by
     chart.js along with the background, so the halo always matches. */
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent);
}
.vchart-legend-label {
  grid-area: label;
  font-size: var(--t-2);
  color: var(--fg-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vchart-legend-value {
  grid-area: value;
  font-family: var(--mono);
  font-size: var(--t-3);
  font-weight: 600;
  color: var(--fg-strong);
  white-space: nowrap;
}
/* Ellipsis rather than wrapping: a second line here would be a third line in
   the item, and the height has to be constant. */
.vchart-legend-sub {
  grid-area: sub;
  font-size: var(--t-1);
  color: var(--fg-dim);
  font-family: var(--mono);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vchart-table { width: 100%; }

/* Hover tooltip. */
/* Glass: translucent, blurred, and floating clear of the plot. It sits over
   live data, so it has to be readable without hiding what is under it. */
.vchart-tip {
  position: absolute;
  z-index: 20;
  pointer-events: none;
  padding: var(--s-2) var(--s-3);
  background: var(--glass);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--glass-edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-2);
  font-size: var(--t-2);
  white-space: nowrap;
  min-width: 148px;
}
.vchart-tip-time { font-family: var(--mono); font-size: var(--t-1); color: var(--fg-dim); margin-bottom: 4px; }
.vchart-tip-row { display: flex; align-items: center; gap: var(--s-2); padding: 1px 0; }
.vchart-tip-row .vchart-swatch { box-shadow: none; }
.vchart-tip-label { color: var(--fg-muted); }
.vchart-tip-value { margin-left: auto; font-family: var(--mono); font-weight: 600; color: var(--fg-strong); }

/* The time-range toolbar is a toolbar, not a segmented control. It shared the
   inline-flex pill styling with .view-toggle, so its date inputs, selects and
   zoom controls ran off the right edge of the page instead of wrapping. */
.range-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  border-radius: var(--r-2);
  margin-bottom: var(--s-4);
}
.range-bar input[type="datetime-local"] { width: auto; max-width: 180px; }
.range-sep { width: 1px; height: 18px; background: var(--hairline-strong); margin: 0 var(--s-1); padding: 0; }

/* The legend must not change height when the cursor enters the plot. Each item
   is one unbreakable unit, and the value has a floor width so a shorter reading
   does not drag the statistics after it. With a synced crosshair this is the
   difference between pointing at one chart and every panel on the page
   twitching. */
.vchart-legend { align-content: flex-start; }

/* ============================================ dashboard panel internals === */
/* dashboard.js builds these seventeen classes, and not one of them was styled.
   The shipped "Fleet health" dashboard rendered its top-N and alert-list panels
   as a single run-on line — "▲/db-0189.9%▲/web-0387.4%…" — because every row,
   label and value was an unstyled inline span. Same failure as the chart
   legend, in the other script; there is now a test that reads class names out
   of both. */
.vpanel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.vpanel-head { flex: none; }
/* A panel's height comes from the stored grid, so its contents scroll inside it
   rather than spilling over the panel below. */
.vpanel-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
}

.vpanel-rows { display: flex; flex-direction: column; }
.vpanel-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-2);
  padding: 5px 0;
  border-bottom: 1px solid var(--hairline);
  color: var(--fg);
  font-size: var(--t-2);
}
.vpanel-row:last-child { border-bottom: 0; }
a.vpanel-row:hover { background: var(--surface-2); text-decoration: none; }
.vpanel-row-label { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
.vpanel-row-name {
  color: var(--fg-strong);
  font-weight: 550;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vpanel-row-sub { font-size: var(--t-1); color: var(--fg-dim); white-space: nowrap; }
.vpanel-row-value { text-align: right; font-weight: 600; color: var(--fg-strong); white-space: nowrap; }
/* The bar spans the full row underneath, so a list reads as a chart as well as
   a table without stealing width from the label. */
.vpanel-row-bar {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  overflow: hidden;
}
.vpanel-row-fill { display: block; height: 100%; border-radius: var(--r-pill); background: var(--accent); }
.vpanel-row-fill.status-ok       { background: var(--status-ok-fill); }
.vpanel-row-fill.status-warning  { background: var(--status-warn-fill); }
.vpanel-row-fill.status-critical { background: var(--status-crit-fill); }
.vpanel-row-fill.status-offline  { background: var(--status-unknown-fill); }

.vpanel-stat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 0;
}
.vpanel-stat-value {
  font-size: clamp(var(--t-6), 7cqw, 44px);
  font-weight: 650;
  letter-spacing: -.03em;
  line-height: 1.05;
  color: var(--fg-strong);
}
.vpanel-stat-label { font-size: var(--t-1); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-dim); }

.vpanel-text { font-size: var(--t-2); color: var(--fg-muted); line-height: 1.55; }
.vpanel-hint { font-size: var(--t-1); color: var(--fg-dim); margin-top: var(--s-2); }
.vpanel-empty, .vpanel-error {
  flex: 1;
  display: grid;
  place-items: center;
  padding: var(--s-4);
  text-align: center;
  font-size: var(--t-2);
  color: var(--fg-dim);
}
.vpanel-error { color: var(--status-crit-fg); }

/* Inside a fixed-height panel the legend takes what is left and scrolls; it
   must never be what pushes a chart out of its own card. */
.vpanel .vchart { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.vpanel .vchart-legend { flex: none; max-height: 7.5em; overflow-y: auto; }

/* uPlot's own cursor is a dashed border on a positioned div. One crisp
   vertical hairline in the accent colour; the horizontal one is switched off
   in the cursor options rather than hidden here, so uPlot stops tracking it. */
.u-cursor-x { border-right: 1px solid var(--chart-crosshair) !important; border-right-style: solid !important; }
.u-cursor-y { display: none !important; }
.u-cursor-pt { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 25%, transparent); }
.u-select { background: var(--chart-band) !important; border-radius: var(--r-1); }

/* The chart card lifts slightly under the pointer — enough to say "this is the
   one you are reading", not enough to move anything. */
.chart-card {
  transition: box-shadow var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}
.chart-card:hover {
  box-shadow: var(--shadow-2);
  border-color: var(--hairline-strong);
}

/* Service check errors are driver text: verbose, and the only clue when a check
   fails. Wrapped and bounded rather than truncated — a message cut off before
   "password authentication failed" would be worse than no message. */
.svc-error {
  max-width: 42ch;
  white-space: normal;
  word-break: break-word;
  font-size: var(--t-1);
  line-height: 1.45;
}

/* ─── The uptime matrix (SPEC §12.3) ────────────────────────────────────────
   This replaced a honeycomb — one hexagon per entity, coloured by error budget
   spent. The colour scale was right (99.5% is excellent against a 99% target
   and a broken promise against 99.9), but a hexagon has room for a colour and
   no room for a number, so every real question ended in a hover and three of
   the four could not be answered at all: which host, how much budget, how fast
   it is burning, and whether the month went in one afternoon or a fortnight of
   flapping.

   A row answers the first three by being a row. The fourth is the one thing the
   honeycomb genuinely had — shape over time — and it survives as the timeline
   below, given a scale and a tooltip per day. */
.uptime-matrix { table-layout: fixed; }
.uptime-matrix .ub-head { text-align: left; }
.uptime-matrix td { overflow-wrap: anywhere; }
.uptime-matrix tfoot td {
  border-top: 1px solid var(--border);
  font-weight: 600;
  color: var(--fg);
}

/* Thirty whole UTC days, oldest at the left. Fixed slots rather than a flex
   fill: every row's bar has to start and end at the same x or the columns
   cannot be compared down the page, which is the only reason to stack them.

   Named .uptime-bar, not .timeline. .timeline was already taken by the vertical
   event list on the alert detail page, whose `.timeline > *` rule gives every
   child `grid-template-columns: 84px 120px 1fr` — so each of these thirty day
   slots inherited a 228px minimum and the table measured 6869px. Two components
   sharing a name with nothing checking they agree, again. */
.uptime-bar {
  display: grid;
  grid-template-columns: repeat(30, 1fr);
  gap: 1px;
  height: 22px;
}
.ub-day {
  border-radius: 1px;
  background: var(--surface-3);
  transition: transform var(--dur-1) var(--ease);
}
.ub-day:hover { transform: scaleY(1.15); }
.ub-day.is-ok   { background: var(--status-ok-fill); }
.ub-day.is-warn { background: var(--status-warn-fill); }
.ub-day.is-crit { background: var(--status-crit-fill); }
/* A day before the entity existed is not a day it was up. Drawn as a slot
   rather than skipped, so "we only started watching last Tuesday" is visible
   instead of implied by a shorter bar. */
.ub-day.is-empty {
  background: none;
  box-shadow: inset 0 0 0 1px var(--hairline-strong);
}

.sla-card { padding: 0; overflow: hidden; }
.sla-card > .sla-head, .sla-card > .sla-bar { margin-left: var(--pad-card); margin-right: var(--pad-card); }
.sla-card > .tbl-wrap { border-radius: 0; border-left: 0; border-right: 0; }
.sla-foot { padding: var(--s-3) var(--pad-card) var(--pad-card); margin: 0; max-width: 78ch; }

.sla-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-5);
  margin-bottom: var(--s-3);
}
.sla-figure {
  font-size: var(--t-6);
  font-weight: 650;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.sla-figure.miss { color: var(--status-crit-fg); }
.sla-figure.hit  { color: var(--status-ok-fg); }

.sla-stat { display: flex; flex-direction: column; gap: 2px; }
.sla-stat-label {
  font-size: var(--t-1);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--fg-muted);
}
.sla-stat-value { font-variant-numeric: tabular-nums; font-weight: 600; }
.sla-stat-value.over { color: var(--status-crit-fg); }

/* The budget bar: how much of the allowance is gone, at a glance. */
/* The budget bar is a rule between two things, so it needs room on both sides
   of it. With only a top margin it sat flush against the table head below and
   read as part of the table's chrome rather than as the summary's last line. */
.sla-bar {
  height: 8px;
  border-radius: 4px;
  background: var(--surface-3);
  border: 1px solid var(--edge);
  overflow: hidden;
  margin-top: var(--s-4);
  margin-bottom: var(--s-5);
}
.sla-bar-fill { height: 100%; border-radius: 3px; }
.sla-bar-fill.ok    { background: var(--status-ok-fill); }
.sla-bar-fill.warn  { background: var(--status-warn-fill); }
.sla-bar-fill.crit  { background: var(--status-crit-fill); }

.sla-partial {
  font-size: var(--t-1);
  color: var(--status-warn-fg);
}

/* SLA card header: the name on the left, its controls on the right. Editing
   lives on the card rather than behind a menu — there are rarely more than a
   handful of targets, and a hidden control on a page someone visits monthly is
   a control nobody finds. */
.sla-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: var(--pad-card);
  border-bottom: 1px solid var(--border);
}
.sla-card-actions { display: flex; align-items: center; gap: var(--s-2); }
.sla-card-actions form { margin: 0; }

.form-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--hairline);
}

/* A copyable configuration snippet in an empty state. The empty services page
   is the one place someone needs to be told what to type, so it shows the
   three lines rather than linking to documentation nobody opens. */
.code-block {
  margin: var(--s-3) 0 0;
  padding: var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--edge);
  border-radius: var(--r-2);
  font-family: var(--mono);
  font-size: var(--t-2);
  line-height: 1.6;
  color: var(--fg);
  overflow-x: auto;
  white-space: pre;
  text-align: left;
}

/* ─── Clusters (SPEC §13.6) ────────────────────────────────────────────────
   Namespace is the filter, kind is the grouping. Both at once nested two levels
   of heading around tables that were often one row each, and the page read as
   an outline of the cluster rather than a view of it. Kind is the better of the
   two to group by because it is bounded: four or five kinds against a hundred
   namespaces. */
.ns-filter {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-4);
}
.ns-filter select { min-width: 260px; }
.ns-filter-label {
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.ns-filter-clear { font-size: var(--t-1); color: var(--fg-muted); }
/* The no-JavaScript fallback. The select submits itself, so for anyone with
   scripts this button never has anything to do — but it is the only way the
   filter works without them, so it is hidden rather than absent, and comes
   back the moment focus lands anywhere inside the form. */
.ns-filter-go { opacity: 0; pointer-events: none; }
.ns-filter:focus-within .ns-filter-go { opacity: 1; pointer-events: auto; }

/* A collapsible section per kind. <details> rather than a scripted accordion:
   it collapses with the keyboard, works with scripts off, and stays findable
   by the browser's own find-in-page. */
.k8s-kind { margin-bottom: var(--s-4); }
.k8s-kind-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-2);
  padding: var(--s-2) 0;
  font-size: var(--t-2);
  color: var(--fg-muted);
  cursor: pointer;
  list-style: none;
}
.k8s-kind-head::-webkit-details-marker { display: none; }
/* The disclosure triangle, drawn rather than inherited, so it matches the rest
   of the chrome and rotates with the same 200ms everything else uses. */
.k8s-kind-head::before {
  content: "";
  width: 0; height: 0;
  border-left: 4px solid var(--fg-dim);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform var(--dur-2) var(--ease);
}
.k8s-kind[open] > .k8s-kind-head::before { transform: rotate(90deg); }
.k8s-kind-head:hover { color: var(--fg); }
.k8s-kind-name {
  font-size: var(--t-2);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--fg);
}

/* An unhealthy pod nested under its workload. These rows only exist when
   something is wrong, so one is always an explanation rather than noise. */
.k8s-pod-row > td {
  background: var(--surface-2);
  border-top: none;
  padding-left: var(--s-5);
  font-size: var(--t-2);
}
.k8s-pod-row .dot { margin-right: var(--s-1); }
.k8s-pod-msg {
  margin-top: 4px;
  padding-left: var(--s-3);
  border-left: 2px solid var(--hairline-strong);
  color: var(--fg-dim);
  font-family: var(--mono);
  font-size: var(--t-1);
  line-height: 1.5;
  white-space: normal;
  word-break: break-word;
  max-width: 90ch;
}

/* A partial-permission notice. Not an error — somebody made a decision, and
   the page has to say so rather than looking broken. */
.banner-warn {
  background: color-mix(in srgb, var(--status-warn-fill) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--status-warn-fill) 40%, transparent);
  color: var(--fg);
}
.banner-warn ul { margin: var(--s-2) 0 0; padding-left: 1.2rem; }
.banner-warn li { font-size: var(--t-1); color: var(--fg-muted); }

/* A rejected action, redirected back with the reason. Distinct from
   .banner-warn because "somebody chose this" and "this did not work" should not
   look the same. */
.banner-crit {
  background: color-mix(in srgb, var(--status-crit-fill) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--status-crit-fill) 40%, transparent);
  color: var(--fg);
}

/* ── "Why did/didn't this alert?" (SPEC §9.2) ───────────────────────────────
   The headline carries the answer; the chain is folded away because most
   people want to know whether their phone will ring, not which of twelve
   checks decided it. */
.explain { padding: var(--s-4); border-left: 3px solid var(--fg-muted); }
.explain-sends { border-left-color: var(--status-ok-fg); }
.explain-blocked { border-left-color: var(--status-warn-fg); }
.explain-headline { margin: 0; font-size: var(--t-3); line-height: 1.5; }
.explain-more { margin-top: var(--s-3); }
.explain-more > summary {
  cursor: pointer; font-size: var(--t-1); color: var(--fg-muted);
  user-select: none;
}
.explain-more > summary:hover { color: var(--fg); }
.explain-sub { font-size: var(--t-1); margin: var(--s-4) 0 var(--s-2); }

.explain-chain { list-style: none; margin: var(--s-3) 0 0; padding: 0; }
.explain-step {
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3);
  align-items: baseline;
  padding: var(--s-2) 0 var(--s-2) var(--s-4);
  border-left: 2px solid var(--border);
  font-size: var(--t-1);
}
/* The number is the step's number in §4.5, so an operator reading the spec and
   an operator reading this page are looking at the same list. */
.explain-n {
  display: inline-block; min-width: 1.8em; color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}
.explain-title { color: var(--fg); }
.explain-detail { color: var(--fg-muted); }

.explain-pass { border-left-color: var(--status-ok-fg); }
.explain-stop { border-left-color: var(--status-crit-fg); }
.explain-stop .explain-title, .explain-stop .explain-detail { color: var(--status-crit-fg); }
.explain-bypass { border-left-color: var(--status-ok-fg); }
/* Unreached steps are dimmed rather than hidden: "these were never asked"
   is the fact that stops somebody fixing step 8 when step 3 stopped it. */
.explain-unreached { opacity: 0.45; }

/* ── Alert quality (SPEC §9.3) ──────────────────────────────────────────────
   The verdict rides under its rule rather than in a column, because the
   verdicts are sentences and a column of sentences is unreadable. */
.row-flagged > td { border-bottom: none; }
.row-verdicts > td {
  padding-top: 0; padding-bottom: var(--s-3);
  border-left: 2px solid var(--status-warn-fg);
}
.verdicts { list-style: none; margin: 0 0 var(--s-2); padding: 0; }
.verdicts li {
  font-size: var(--t-1); color: var(--fg-muted); line-height: 1.6;
  padding-left: var(--s-3); position: relative;
}
.verdicts li::before { content: "→"; position: absolute; left: 0; }
.empty { padding: var(--s-5); }
.empty p { margin: 0 0 var(--s-2); }
.empty p:last-child { margin-bottom: 0; }

/* ── One-click silence (SPEC §9.4) ──────────────────────────────────────────
   The popover hangs off a header control rather than a table row, so it opens
   to the left of its trigger and does not push the page wider. */
/* A real popover, unlike the row-action panels next door: those sit inside a
   table cell where growing the row is fine, and this one hangs off a page
   header where growing it would shove the whole page down every time somebody
   glanced at the durations. */
/* .pops marks a block that contains a popover, and it exists because of a
   non-obvious interaction: `.page > * { animation: rise ... both }` gives every
   direct child of .page its own stacking context for good (fill-mode both), so
   a z-index inside the page header is scoped to the header and a later sibling
   — the meta row, the first chart — paints straight over it no matter how high
   that z-index is. Raising the container is the only thing that helps. */
.pops { position: relative; z-index: 20; }

.silence-control { display: inline-block; position: relative; }
.silence-control > .action-pop {
  position: absolute;
  top: calc(100% + var(--s-2));
  right: 0;
  z-index: 40;
  width: 380px;
  max-width: min(380px, 90vw);
  margin-top: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  padding: var(--s-2);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.28);
}
/* Four buttons of equal weight: choosing a length should take no thought, and
   a default among them would make three of them look like the wrong answer. */
.silence-durations { flex-wrap: wrap; justify-content: flex-start; }
/* A section heading that carries a control. The label itself is uppercased, so
   the control has to sit beside it rather than inside it — a popover nested in
   a .section-label renders every sentence in it in capitals. */
.section-head { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-2); }
.section-head .section-label { margin-bottom: 0; }
.section-head > .silence-control { margin-left: auto; }
.silence-what { margin: 0; font-size: var(--t-2); line-height: 1.45; }
.silence-note { color: var(--status-warn-fg); }

/* ── Time to full (SPEC §9.6) ───────────────────────────────────────────────
   The urgency classes use the same boundaries §4.6.3's predictive rule fires
   on, so the colour on the page and the alert in the inbox cannot disagree
   about what counts as soon. */
.filling-widget { border-left: 3px solid var(--status-warn-fg); }
.fill-critical { color: var(--status-crit-fg); font-weight: 600; }
.fill-warning { color: var(--status-warn-fg); }
.fill-info { color: var(--fg); }

/* ── Config import (SPEC §9.5) ──────────────────────────────────────────────
   form-group--wide only widens inside a rule card, and this form is not one —
   without this the paste box is a 180px column and the file it takes is 77KB. */
.cfg-form .form-group { flex: 1 1 100%; min-width: 0; }
.cfg-form textarea, .cfg-form input[type="file"] { width: 100%; }

/* The action is the column somebody scans, so it is a chip and not a word. */
.cfg-create { background: var(--status-ok-bg); color: var(--status-ok-fg); }
.cfg-update { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.cfg-unchanged { color: var(--fg-dim); }
.cfg-skip { background: var(--status-crit-bg); color: var(--status-crit-fg); }

/* ── TLS findings (SPEC §16) ────────────────────────────────────────────────
   Three severities, and the difference matters: an error means nothing was
   stored, a warning means it was and you should know something, and info is
   a statement of fact. Rendering them alike would make the first two
   indistinguishable at the moment somebody most needs to tell them apart. */
.tls-findings { list-style: none; margin: var(--s-3) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.tls-findings li {
  font-size: var(--t-1); line-height: 1.6; padding-left: var(--s-3);
  border-left: 2px solid var(--border); white-space: pre-line;
}
.tls-error { border-left-color: var(--status-crit-fg); color: var(--status-crit-fg); }
.tls-warning { border-left-color: var(--status-warn-fg); color: var(--fg); }
.tls-info { border-left-color: var(--status-ok-fg); color: var(--fg-muted); }

/* ═══════════════════════════════════════════════ motion and surface (§19) ══
 *
 * Everything below is CSS. The brief asked for Framer Motion; this is a Go
 * application that renders HTML on the server and has no React to attach it
 * to, and shipping a component framework to animate a hover state would cost
 * more than the whole interface weighs. What Framer Motion is actually being
 * asked for here — spring-ish easing, transforms on press, a shared indicator
 * that moves between routes — is expressible in CSS, and the last of those is
 * expressible *only* in CSS on a multi-page application, through view
 * transitions.
 */

/* Cross-document view transitions. Chrome and Safari animate; Firefox ignores
   this entirely and navigates as it always did, which is the right fallback —
   nothing is broken, it simply does not slide. */
@view-transition { navigation: auto; }

/* The whole page cross-fades, the active nav marker slides. Without naming the
   marker it would cross-fade with everything else and read as a flicker. */
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: var(--dur-2);
  animation-timing-function: var(--ease);
}
::view-transition-group(nav-active) {
  animation-duration: var(--dur-3);
  animation-timing-function: var(--ease-out);
}

/* ── press physics ─────────────────────────────────────────────────────────
   A control that moves under the finger is the difference between an interface
   that feels built and one that feels drawn. 0.98 is small enough to read as
   pressure rather than as an animation. */
.btn, .cmdk-trigger, .range-btn, .view-toggle button, .tag-chip, .tag-add-btn,
details.row-actions > summary, .silence-control > summary {
  transition: background var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease);
}
.btn:active, .cmdk-trigger:active, .range-btn:active,
.view-toggle button:active, details.row-actions > summary:active {
  transform: scale(.98);
}

/* A focus ring that is a ring, not an outline butted against the control. The
   two-layer shadow puts a dark gap between the control and the halo so it
   reads on any surface. */
:where(a, button, input, select, textarea, [tabindex], summary):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--canvas), 0 0 0 4px var(--accent-line);
  border-radius: var(--r-1);
}

/* ── rows ──────────────────────────────────────────────────────────────────
   White at 3%, not a grey. On six different surfaces a fixed grey is right on
   one of them. */
.tbl tbody tr { transition: background var(--dur-1) var(--ease); }
.tbl tbody tr:hover { background: rgba(255, 255, 255, .03); }
/* Not on a row that is already carrying a state colour: two backgrounds fight
   and the state is the one that matters. */
.tbl tbody tr.vol-hl:hover, .tbl tbody tr.row-flagged:hover { background: inherit; }

/* ── status, breathing ─────────────────────────────────────────────────────
   A firing alert should draw the eye without being a solid red block. The
   pulse is on the halo, not on the dot: animating the dot's own colour makes
   text beside it appear to flicker, and animating opacity on the element makes
   it disappear twice a second. Only what is firing pulses — a critical alert
   that has been acknowledged is being handled, and does not need to keep
   shouting. */
@keyframes torwarden-breathe {
  0%, 100% { box-shadow: 0 0 0 0 var(--status-crit-glow); }
  50%      { box-shadow: 0 0 0 5px transparent; }
}
.status-dot.status-crit:not(.is-ack),
.chip-critical.is-firing .dot,
.dot.status-crit.is-firing {
  animation: torwarden-breathe 2s cubic-bezier(.4, 0, .6, 1) infinite;
}

/* ── skeletons ─────────────────────────────────────────────────────────────
   A chart that has not loaded should look like a chart that has not loaded,
   not like a chart with no data — those are different facts and an empty axis
   claims the second one. */
.skeleton {
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, .04);
  border-radius: var(--r-2);
}
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg,
    transparent, rgba(255, 255, 255, .06), transparent);
  animation: torwarden-shimmer 1.6s var(--ease) infinite;
}
@keyframes torwarden-shimmer { to { transform: translateX(100%); } }
.skeleton-chart { height: 180px; }

/* A chart mount is an empty div until the script fills it, so :empty is the
   whole loading state — no flag to set, nothing to clear, and nothing that can
   be left behind if the fetch throws.
   
   It matters because a chart that has not loaded and a chart with no data look
   identical otherwise: both are a blank rectangle. Those are opposite findings
   on a monitoring dashboard — one is "wait" and the other is "this host has
   stopped reporting" — and an operator should never have to guess which. */
.chart-card > div[id^="chart-"]:empty,
.chart-card > div[id^="ec-"]:empty {
  display: block;
  min-height: 180px;
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, .04);
  border-radius: var(--r-2);
}
.chart-card > div[id^="chart-"]:empty::after,
.chart-card > div[id^="ec-"]:empty::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg,
    transparent, rgba(255, 255, 255, .06), transparent);
  animation: torwarden-shimmer 1.6s var(--ease) infinite;
}
.skeleton-row { height: 14px; margin-bottom: var(--s-2); }
.skeleton-row:nth-child(2) { width: 78%; }
.skeleton-row:nth-child(3) { width: 62%; }

/* ── vibrancy on the things that float ─────────────────────────────────────
   The sticky topbar, popovers and chart tooltips. Not on cards: a card that is
   translucent is a card with a chart showing through it. */
.topbar, .action-pop, .silence-control > .action-pop, .u-tooltip, .chart-tip {
  background: var(--glass);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border-color: var(--glass-edge);
}

/* ═══════════════════════════════════════════════════ vitals as gauges (§20) ══
   A percentage is a proportion of something, and a ring is the shape of a
   proportion. The number alone makes the reader supply the scale from memory
   every time they glance at it, which is the difference between reading a
   dashboard and doing arithmetic on one. */
.vitals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--s-3);
  margin: var(--s-4) 0;
}
.vital {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--s-4) var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-3);
  text-align: center;
}
.vital-plain { justify-content: center; }

.ring-wrap { position: relative; display: grid; place-items: center; margin-bottom: var(--s-2); }
/* The hole has to be wider than the number in it.
   
   The arc sits at r=15.915 because the circumference must be 100 units for the
   dasharray to be a percentage, so the hole is r minus half the stroke — and
   nothing about the figure can widen it. Measured at 92px with an 8-unit
   stroke, the hole was 55px and "62.6%" was 54: one pixel of clearance, which
   is why the text looked pressed into the arc. The ring grows and the stroke
   comes in slightly; both are needed, because either alone still leaves it
   tight. */
.ring { width: 72px; height: 72px; display: block; }
/* The circumference is 100 units by construction — r = 15.915 gives 2πr = 100 —
   so a dasharray of "37.4 100" is 37.4%, with no arithmetic anywhere in the
   template. */
/* Thick strokes. A 3.5px arc was right for a hairline interface; on a page
   built from 2px edges and 5px offsets it reads as something that failed to
   load. */
.ring circle { fill: none; stroke-width: 6; }
.ring-track { stroke: var(--surface-3); }
.ring-value {
  stroke-linecap: round;
  stroke: var(--accent-text);
  transition: stroke-dasharray var(--dur-3) var(--ease-out);
}

/* Metric identity. A gauge is violet because it is the CPU gauge, not because
   CPU is in any particular state — the tones alias the series palette, which
   is the one solved to sit clear of every status hue. */
.tone-cpu     .ring-value { stroke: var(--metric-cpu); }
.tone-memory  .ring-value { stroke: var(--metric-memory); }
.tone-storage .ring-value { stroke: var(--metric-storage); }

/* …and severity takes it back the moment there is severity to report. These
   come after the tones deliberately: equal specificity, so source order is the
   rule, and the rule is that a warning outranks an identity. */
.ring-value.prog-warn, .vital .ring-value.prog-warn { stroke: var(--status-warn-fill); }
.ring-value.prog-crit, .vital .ring-value.prog-crit { stroke: var(--status-crit-fill); }

/* A round line cap on a zero-length dash still paints a dot, so a metric at 0%
   drew a stray mark at twelve o'clock that looked like a tiny reading rather
   than like nothing. */
.ring-value[stroke-dasharray^="0.0 "] { stroke-linecap: butt; }

/* The figure sits in the hole. Absolutely positioned rather than an SVG
   <text>, so it takes the interface's mono face and tabular figures instead of
   whatever the SVG renderer picks. */
.ring-figure {
  position: absolute;
  font-family: var(--mono);
  font-size: var(--t-3);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.03em;
  color: var(--fg-strong);
  line-height: 1;
}

/* The fleet gauges on the overview: the same component, given the room to be
   the thing you look at first rather than a detail inside a host page. */
.vitals-lg { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); max-width: 620px; }
.vitals-lg .ring { width: 108px; height: 108px; }
.vitals-lg .ring circle { stroke-width: 7; }
.vitals-lg .ring-figure { font-size: 18px; letter-spacing: -.04em; }
/* The gauge tiles stretch to match the panel beside them, so the ring has to
   centre in whatever height it is given rather than sit at the top of it. */
.vitals-lg .vital { padding: var(--pad-card) var(--s-3); justify-content: center; }

/* A vital that is a plain figure rather than a proportion — load average,
   uptime. No ring, because neither has a ceiling to be a fraction of. */
.vital-figure {
  font-size: var(--t-5);
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--fg-strong);
  line-height: 1.1;
}
.vital-label {
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.vital-sub { font-size: var(--t-1); color: var(--fg-muted); }
.vital { border-color: var(--edge); border-width: var(--edge-w); box-shadow: var(--shadow-hard-sm); }

/* The .num class is the typographic rule the brief asks for, stated once:
   a raw number, an address, a threshold or a log line is monospace; a label,
   a title, a badge or a sentence is not. */
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ═══════════════════════════════════════════════ dense enterprise (§21) ══ */

/* The content column. Without a cap, a five-row table on a 2560px monitor is
   five rows and two thousand pixels of nothing, and the eye has to travel the
   whole width to get from a hostname to its status. 1440 is about twelve words
   of a wide table — past that a row stops being one glance. */
.page { max-width: 1440px; }
/* Except the things that genuinely want the room: a chart grid reads better
   wide, and a dashboard is defined by its own layout. */
.page > .chart-grid, .page > .vdash-grid, .page > .widget-grid { max-width: none; }

/* ── code pills ────────────────────────────────────────────────────────────
   A status code and a latency are machine output quoted inside prose, and a
   pill is what quoting looks like. It also fixes a real scanning problem: a
   bare 200 and a bare 3ms sit at the same weight as the words around them, so
   a column of them reads as text rather than as values. */
.code-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border: 1px solid var(--edge);
  border-radius: var(--r-1);
  background: var(--surface-3);
  color: var(--fg);
  font-family: var(--mono);
  font-size: var(--t-1);
  font-variant-numeric: tabular-nums;
  line-height: 1.6;
  white-space: nowrap;
}
.code-pill.is-ok   { color: var(--status-ok-fg);   border-color: var(--status-ok-line);   background: var(--status-ok-bg); }
.code-pill.is-warn { color: var(--status-warn-fg); border-color: var(--status-warn-line); background: var(--status-warn-bg); }
.code-pill.is-crit { color: var(--status-crit-fg); border-color: var(--status-crit-line); background: var(--status-crit-bg); }
.code-pill.is-quiet { color: var(--fg-muted); }

/* ── the inline micro bar ──────────────────────────────────────────────────
   A 4px rail with the number beside it. The bar answers "how full" before the
   number is read, and the number answers "how full exactly" — which is the
   division of labour a vertical histogram cannot do, because a histogram
   answers "how has it been moving" and that is a different question from the
   one a fleet list is asked. */
.meter {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--s-2);
  min-width: 88px;
}
.meter-rail {
  height: 10px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  border: 1px solid var(--edge);
  overflow: hidden;
}
.meter-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur-3) var(--ease-out);
}
.meter-fill.prog-warn { background: var(--status-warn-fill); }
.meter-fill.prog-crit { background: var(--status-crit-fill); }
/* The figure gets a box of its own (§24). A bare percentage beside a bar is two
   renderings of one number where only one of them is legible at a glance; in a
   pill it becomes the thing you read and the bar becomes the thing you scan. */
.meter-val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-1);
  font-weight: 700;
  color: var(--fg);
  /* Fixed, not min-width. These sit in a column and the eye compares them down
     the page; a pill that grows for "100.0%" and shrinks for "7.7%" makes the
     column ragged and the reading harder, which is the opposite of what putting
     a border round the number was for. 52px is exactly "100.0%" at this face
     and weight plus the padding and border — measured, not guessed, so the
     widest value the cell can hold still fits without the pill growing. */
  width: 52px;
  box-sizing: border-box;
  text-align: center;
  padding: 1px 5px;
  border: 1px solid var(--edge);
  border-radius: var(--r-pill);
  background: var(--surface-3);
}
/* Filled at warning and critical; outlined with green ink at ok.
   
   The first attempt filled all three, which is what the mockup did — but the
   mockup was six rows of a capacity-at-risk table, where everything listed is
   already notable. On the real pages these appear in rankings: "busiest CPU" is
   twenty rows of which sixteen are fine, and twenty filled green pills is a
   green wall. A wall carries no signal.
   
   So the fill means "look at this" everywhere, and ok still gets its colour —
   in the ink, where it answers "what state is this" without competing for the
   attention that belongs to the two rows above it. */
.meter-val.prog-warn { background: var(--status-warn-fill); color: var(--status-warn-ink); }
.meter-val.prog-crit { background: var(--status-crit-fill); color: var(--status-crit-ink); }
.meter-val.prog-ok   { background: var(--surface-3); color: var(--status-ok-fg); }
.meter-val.is-absent { color: var(--fg-dim); background: none; }

/* A certificate's remaining life, as a bar and a number. The bar is what makes
   a wall of them scannable — sorting by days tells you the order, but only the
   lengths tell you whether the next three are a fortnight apart or a year. */
.cert-life { display: grid; grid-template-columns: minmax(60px, 1fr) auto; align-items: center; gap: var(--s-2); margin-bottom: 3px; }
.meter-fill.prog-ok { background: var(--status-ok-fill); }

/* Numbers right, words left. A column of right-aligned figures shares a
   decimal point and can be compared without reading any of them; the same
   column left-aligned has to be read one row at a time. */
.tbl td.n, .tbl th.n { text-align: right; }
.tbl td.n { font-variant-numeric: tabular-nums; }
/* Heavier chips make taller rows. The table gives back what it can: the chip is
   the thing carrying the language, the row padding is not. */
.tbl td, .tbl th { padding-top: 7px; padding-bottom: 7px; }

/* ── status filter badges ──────────────────────────────────────────────────
   The estate's states, as the way into the list of them.

   These replaced two things at once: a strip of one coloured tick per host,
   and a row of KPI cards below it carrying the same counts. The strip was
   inert — nineteen marks and nothing to do with them — and the cards restated
   its totals, so the page opened with the same fact twice and a way through to
   neither. A badge is the count, the delta and the link in one control, which
   is the only arrangement where the summary and the navigation cannot drift
   apart.

   The glow is an inset ring in the state's own colour rather than an outer
   shadow: on a near-black canvas an outer glow is a smudge, and an inset ring
   reads as the badge being lit from within by what it is reporting. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.status-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-4);
}
.sfilter {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  padding: 8px 14px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-hard-sm);
  color: var(--fg);
  white-space: nowrap;
  transition: background var(--dur-2) var(--ease), transform var(--dur-press) var(--ease),
              box-shadow var(--dur-press) var(--ease);
}
.sfilter:hover { background: var(--surface-3); text-decoration: none; }
.sfilter:active { transform: var(--press-sm); box-shadow: none; }
.sfilter-n {
  font-family: var(--mono);
  font-size: var(--t-4);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--fg-strong);
  line-height: 1;
}
.sfilter-label {
  font-size: var(--t-1);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.sfilter-delta {
  font-size: var(--t-1);
  font-variant-numeric: tabular-nums;
  color: var(--fg-dim);
  padding-left: var(--s-1);
  border-left: 1px solid var(--hairline);
}
.sfilter-arrow { font-size: 8px; margin-right: 2px; }
.sfilter-delta.worse  { color: var(--status-crit-fg); }
.sfilter-delta.better { color: var(--status-ok-fg); }

/* A state with hosts in it is a filled block; the ink comes from the same pair
   the chips use, so a critical badge and a critical chip are the same two
   colours. An inset glow was the old language's way of saying "this one" and
   has no meaning here — the fill says it. */
.sfilter.chip-critical { background: var(--status-crit-fill); color: var(--status-crit-ink); }
.sfilter.chip-warning  { background: var(--status-warn-fill); color: var(--status-warn-ink); }
.sfilter.chip-ok       { background: var(--status-ok-fill);   color: var(--status-ok-ink); }
.sfilter.chip-maint    { background: var(--status-maint-fill); color: var(--status-maint-ink); }
.sfilter.chip-offline  { background: var(--surface-3); color: var(--fg); }
.sfilter.chip-critical .sfilter-n,
.sfilter.chip-warning  .sfilter-n,
.sfilter.chip-ok       .sfilter-n,
.sfilter.chip-maint    .sfilter-n { color: inherit; }
.sfilter.chip-critical .sfilter-label,
.sfilter.chip-warning  .sfilter-label,
.sfilter.chip-ok       .sfilter-label,
.sfilter.chip-maint    .sfilter-label { color: inherit; opacity: .78; }
.sfilter.chip-critical .sfilter-delta,
.sfilter.chip-warning  .sfilter-delta { color: inherit; opacity: .78; border-left-color: currentColor; }

/* An empty state still has to be reachable — "show me the four that are in
   maintenance" is a question you ask when the answer is zero too — but it has
   not earned the same weight as a state with hosts in it. */
/* A state with nothing in it stays hollow and loses the offset. Every filled
   badge is a thing that exists; the outlines are the ones that do not. */
.sfilter.is-zero {
  background: none;
  border-color: var(--border);
  box-shadow: none;
  padding: 6px 12px;
}
.sfilter.is-zero .sfilter-n    { font-size: var(--t-2); color: var(--fg-dim); }
.sfilter.is-zero .sfilter-label { color: var(--fg-dim); }

/* ── the overview's two grids ──────────────────────────────────────────────
   Utilisation and silence on one line; the two load lists on the next.

   The gauges alone left half a screen of empty dark to their right, which on a
   dashboard reads as "something failed to render" rather than as breathing
   room. Pairing them with the down list also states the caveat the gauges
   cannot: a mean over eight machines looks exactly like a mean over twelve when
   four have gone quiet. */
.ov-top, .ov-load {
  display: grid;
  gap: var(--s-4);
  margin-bottom: var(--s-4);
  align-items: start;
}
.ov-top  { grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr); align-items: stretch; }
.ov-top > .vitals { height: 100%; }
.ov-load { grid-template-columns: minmax(0, 1fr); }
/* .widget-side carries `grid-column: span 4` from the twelve-column widget
   grid, which is not the grid it is in here — so it spanned four of two and
   wrapped to its own row. */
.ov-top > .widget, .ov-top > .widget-side, .ov-top > .vitals,
.ov-load > .widget, .ov-load > .widget-side {
  grid-column: auto;
}
/* The gauge row is a grid inside a grid cell; it has to fill the cell rather
   than sit at its intrinsic width, or the pairing above buys nothing. */
.ov-top > .vitals { margin: 0; max-width: none; }
@media (max-width: 1000px) { .ov-top { grid-template-columns: minmax(0, 1fr); } }

.load-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--s-4); }
.load-row {
  display: grid;
  /* A fixed track for the meter, not `auto`. With `auto` the name took the
     whole row and the rail collapsed to its minimum — a 30-pixel stub sitting
     a long way from the name it belongs to, which reads as a rendering fault
     rather than as a bar. */
  grid-template-columns: minmax(0, 1fr) 168px;
  align-items: center;
  gap: var(--s-3);
  padding: 9px var(--s-2);
  margin: 0 calc(var(--s-2) * -1);
  border-radius: var(--r-1);
  color: var(--fg);
  transition: background var(--dur-1) var(--ease);
}
.load-row:hover { background: rgba(255, 255, 255, .04); text-decoration: none; }
/* The same treatment a hostname gets in the host list: mono, heavy, and
   coloured by the row's own state. A name that is red on one page and plain on
   another is the reader having to learn the product twice. */
.load-name, .down-name {
  font-family: var(--mono);
  font-size: var(--t-2);
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--fg-strong);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.load-row.status-critical .load-name, .down-row.status-critical { color: var(--status-crit-fg); }
.load-row.status-warning  .load-name { color: var(--status-warn-fg); }
.load-row.status-offline  .load-name, .down-row.status-offline .down-name { color: var(--status-unknown-fg); }
.down-row.status-critical .down-name, .down-row.status-unreachable .down-name { color: var(--status-crit-fg); }
/* The hostname in a down row is a bare text node, not an element — so a
   :first-child selector picks the address underneath it instead, which is how
   the IP ended up set in the hostname's weight and the hostname in the
   address's. The treatment goes on the container and the address opts out. */
.down-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.down-ip {
  font-family: var(--sans);
  font-size: var(--t-1);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--fg-dim);
}
.down-ip.mono { font-family: var(--mono); }
/* A leading pill, drawn — not an inset shadow.
   
   The shadow followed the row's 6px border-radius, so at this row height it
   curled at both ends and read as a curly brace beside the hostname. A drawn
   element is a rectangle with its own radius and cannot inherit the row's. */
.load-row, .down-row { position: relative; padding-left: var(--s-4); }
.load-row.status-critical::before, .load-row.status-warning::before,
.load-row.status-offline::before, .load-row.status-ok::before,
.down-row.status-critical::before, .down-row.status-offline::before,
.down-row.status-unreachable::before {
  content: "";
  position: absolute; left: 2px; top: 50%;
  width: 3px; height: 18px;
  transform: translateY(-50%);
  border-radius: var(--r-pill);
}
.load-row.status-critical::before, .down-row.status-critical::before,
.down-row.status-unreachable::before { background: var(--status-crit-fill); }
.load-row.status-warning::before  { background: var(--status-warn-fill); }
.load-row.status-ok::before       { background: var(--status-ok-fill); }
.load-row.status-offline::before, .down-row.status-offline::before { background: var(--status-unknown-fill); }

.down-list { display: flex; flex-direction: column; }
.down-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: 7px var(--s-2);
  margin: 0 calc(var(--s-2) * -1);
  border-radius: var(--r-1);
  color: var(--fg);
  transition: background var(--dur-1) var(--ease);
}
.down-row:hover { background: rgba(255, 255, 255, .04); text-decoration: none; }
.down-for { flex: none; }

/* ── reduced motion ────────────────────────────────────────────────────────
   Everything above is decoration. For somebody who gets motion sickness from
   a sliding indicator it is not decoration, it is the reason they close the
   tab — and a monitoring dashboard is a thing people stare at all day. */
/* ── banded lists ──────────────────────────────────────────────────────────
   A certificate list sorted by days-left tells you the order; it does not tell
   you where the line is between "renew this week" and "ignore until October".
   The band header states that line once, so a row's position carries the
   verdict and the days column is only the detail. The tinted top edge is the
   only colour a band gets — a fully tinted card would make the healthy section
   compete with the critical one for attention. */
.page-body { display: flex; flex-direction: column; gap: var(--s-5); }

/* Sections of one page, as opposed to rows inside one section.
   
   The alerts page ran a table, a bare heading and a form together on the same
   vertical rhythm as the rows inside them, so where one section ended and the
   next began was something the reader had to work out. 32px is twice the
   largest gap anything *inside* a section uses, which is what makes the break
   read as a break; the rule above each one says the same thing again for
   anyone scanning rather than reading. */
.page-sections { display: flex; flex-direction: column; gap: var(--s-6); }
/* The rule is its own element rather than a border on the section below it.
   Every section on this page is a card, and a border-top on a card lands on
   the card's own edge and reads as a doubled line; a rule in the gap is in the
   gap, which is where the break actually is. */
.section-rule {
  height: 1px; border: 0; margin: 0;
  background: var(--border);
}

.cert-band { position: relative; overflow: hidden; }
.cert-band::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
}
.cert-band-crit::before { background: var(--status-crit-fill); }
.cert-band-warn::before { background: var(--status-warn-fill); }
.cert-band-ok::before { background: var(--status-ok-fill); }
.cert-band-unknown::before { background: var(--border); }

.tbl-head {
  display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--pad-card) var(--pad-card) var(--s-3);
  border-bottom: 1px solid var(--border);
}
.tbl-title {
  display: flex; align-items: center; gap: var(--s-2);
  margin: 0; font-size: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--fg);
}
.tbl-title .count {
  font-family: var(--mono); font-size: 11px; font-variant-numeric: tabular-nums;
  color: var(--fg-dim); background: var(--surface-3);
  border: 1px solid var(--edge);
}
.tbl-note { margin: 0; font-size: 11px; color: var(--fg-dim); }
.tbl-head + .tbl thead th { border-top: 0; }

/* The second line inside a cell — a URL under a hostname, a reason under a
   bar. One size for all of them, rather than a style attribute per <td>. */
.cell-sub { font-size: 11px; }
.cell-life { min-width: 210px; }
.cert-tbl, .svc-tbl { table-layout: fixed; }
.cert-tbl td, .svc-tbl td { overflow-wrap: anywhere; }

/* ── card sections ─────────────────────────────────────────────────────────
   A section that owns a form and a table — maintenance windows is the one that
   drove this. It used to be a heading, a bare form and a floating table all at
   page level, which read as three unrelated things stacked by accident. One
   card with a ruled header, a padded body and a flush table makes the form and
   the list it produces visibly the same feature. */
/* The card supplies its own padding; this one delegates it to the header and
   body so the table can run flush to the edges. */
.mx-card { padding: 0; overflow: hidden; }

.card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s-4); flex-wrap: wrap;
  padding: var(--pad-card);
  border-bottom: 1px solid var(--border);
}
.card-title { margin: 0 0 4px; font-size: 13px; font-weight: 600; color: var(--fg); }
.card-note { margin: 0; font-size: 11px; line-height: 1.5; color: var(--fg-dim); max-width: 62ch; }
.card-body { padding: var(--pad-card); }

/* The form's own footer, ruled off so the button belongs to the fields above
   it and not to the table below. */
.form-foot {
  display: flex; gap: var(--s-2); align-items: center;
  padding-top: var(--s-3); border-top: 1px solid var(--border);
}
.mx-form .form-row { margin-bottom: var(--s-3); }
.mx-card > .tbl { border-top: 1px solid var(--border); }
.row-end { text-align: right; }
.row-end form { display: inline; }

/* The search sits at the far end of the filter bar, away from the chips: it
   narrows the same list they do, but by typing rather than by clicking. */
.filter-search { display: flex; gap: var(--s-2); margin-left: auto; }
.status-cell { display: flex; align-items: center; gap: 6px; }

/* ── the detail drawer ─────────────────────────────────────────────────────
   A panel over the list, with its own URL.

   Server-rendered rather than scripted: /services/{id} renders the same list
   with this open, so the panel is linkable, survives a reload, and closes with
   the back button. The scrim is an <a> to the list for the same reason —
   closing is navigation, and navigation should be a link, which also means it
   works with the keyboard without a single line of JavaScript. */
.drawer-scrim {
  position: fixed; inset: 0; z-index: 60;
  will-change: opacity;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(2px);
  animation: drawer-fade var(--dur-2) var(--ease-out);
}
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 61;
  /* Belt and braces on the height. `top: 0; bottom: 0` already resolves to the
     viewport, but only while nothing above has quietly become a containing
     block — and something did, once. A viewport unit cannot be fooled that way,
     so the drawer can never again be taller than the window it is in. */
  max-height: 100dvh;
  /* The drawer slides and the scrim fades, and both sit over a page that has
     twenty panels on it. Promoting them means the compositor moves a layer
     instead of the browser repainting everything underneath — which is the
     other half of the flicker, the half that survives after the reload is
     gone. */
  will-change: transform, opacity;
  width: min(560px, 100vw);
  display: flex; flex-direction: column;
  background: var(--surface-2);
  border-left: var(--edge-w) solid var(--edge);
  box-shadow: var(--shadow-2);
  animation: drawer-in var(--dur-3) var(--ease-out);
}
.drawer-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s-3);
  padding: var(--pad-card);
  border-bottom: 1px solid var(--border);
}
.drawer-title { margin: 0 0 2px; font-size: var(--t-4); font-weight: 600; color: var(--fg-strong); }
.drawer-sub { margin: 0; font-size: var(--t-1); color: var(--fg-muted); }
.drawer-close {
  font-size: 20px; line-height: 1; color: var(--fg-dim);
  padding: 2px 6px; border-radius: var(--r-1);
  transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.drawer-close:hover { color: var(--fg-strong); background: var(--surface-3); text-decoration: none; }
/* min-height: 0 is what actually lets this scroll. A flex item's default
   min-height is `auto`, which refuses to shrink below its content — so the body
   grew to fit and pushed the drawer past the bottom of the screen instead of
   scrolling inside it. `overflow-y: auto` alone does nothing about that. */
.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--pad-card); }
.drawer-state { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-4); }
.drawer-section { margin-bottom: var(--s-5); }
.drawer-section-title {
  margin: 0 0 var(--s-2);
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-1); font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-dim);
}
.drawer-foot { padding-top: var(--s-3); border-top: 1px solid var(--hairline); }
/* A table already inside a padded panel does not need the panel chrome again. */
.tbl-flush { width: 100%; }

/* One reading: the series name, its newest value, and the sentence that says
   why anyone should care. The sentence is the point — a list of
   `pg.longest_txn_seconds 412 seconds` is a list of trivia unless you already
   know what an idle-in-transaction session does to vacuum. */
.reading-list { margin: 0; display: flex; flex-direction: column; gap: var(--s-3); }
.reading {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--hairline);
}
.reading:last-child { border-bottom: none; padding-bottom: 0; }
.reading-name { font-size: var(--t-2); color: var(--fg); min-width: 0; overflow-wrap: anywhere; }
.reading-value {
  margin: 0; grid-column: 2; grid-row: 1;
  font-size: var(--t-3); font-weight: 600; color: var(--fg-strong); text-align: right;
}
.reading-unit { font-size: var(--t-1); font-weight: 400; color: var(--fg-dim); }
.reading-help {
  margin: 0; grid-column: 1 / -1;
  font-size: var(--t-1); line-height: 1.5; color: var(--fg-muted);
}

@keyframes panel-land { from { transform: scale(.97); opacity: .4; } }
@keyframes drawer-in  { from { transform: translateX(24px); opacity: 0; } }
@keyframes drawer-fade { from { opacity: 0; } }

/* ── the certificate drawer ────────────────────────────────────────────────
   Wider than the service drawer, because a distinguished name is long and
   wrapping one across three lines makes a chain unreadable. */
.drawer-wide { width: min(680px, 100vw); }

/* The countdown, which is the whole question this page exists to answer. The
   bar under it is the same 90-day horizon the list uses, so a certificate's
   length here and in the table cannot mean two different things. */
.cert-countdown {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 2px var(--s-3);
  padding: var(--s-4);
  margin-bottom: var(--s-4);
  background: var(--surface-3);
  border: 1px solid var(--edge);
  border-radius: var(--r-2);
}
.cert-countdown-n {
  font-size: var(--t-6);
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--fg-strong);
}
.cert-countdown.is-crit .cert-countdown-n { color: var(--status-crit-fg); }
.cert-countdown.is-warn .cert-countdown-n { color: var(--status-warn-fg); }
.cert-countdown.is-ok   .cert-countdown-n { color: var(--status-ok-fg); }
.cert-countdown-label { font-size: var(--t-1); color: var(--fg-muted); }
.cert-countdown .meter-rail { grid-column: 1 / -1; margin-top: var(--s-2); }
.cert-countdown .cell-sub { grid-column: 1 / -1; }

/* A list of things that either hold or do not. Two questions, not one badge:
   a perfectly trusted certificate for the wrong hostname is still an outage. */
.check-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.check { display: flex; align-items: baseline; gap: var(--s-2); font-size: var(--t-2); color: var(--fg); }
.check-mark {
  flex: none;
  width: 16px; height: 16px;
  display: grid; place-items: center;
  border-radius: 50%;
  font-size: 10px; line-height: 1;
}
.check.is-ok   .check-mark { background: var(--status-ok-bg);   color: var(--status-ok-fg); }
.check.is-warn .check-mark { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.check.is-crit .check-mark { background: var(--status-crit-bg); color: var(--status-crit-fg); }
/* Neither pass nor fail: a deliberate configuration, stated rather than judged. */
.check.is-info .check-mark { background: var(--surface-3); color: var(--fg-dim); }

/* Label/value pairs. A two-column grid rather than a definition list's default
   flow, so the values line up and the column can be scanned on its own. */
.fact-grid {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr);
  gap: var(--s-2) var(--s-3);
  margin: 0;
  font-size: var(--t-2);
}
.fact-grid dt { color: var(--fg-dim); }
.fact-grid dd { margin: 0; color: var(--fg); overflow-wrap: anywhere; }

.san-list { display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* The chain as the server presented it, in order. A missing intermediate is
   the most common TLS misconfiguration there is and is invisible from a
   browser that happens to have cached one, so the count matters as much as
   the contents. */
.chain { list-style: none; margin: 0; padding: 0; }
.chain-link {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px var(--s-2);
  padding: var(--s-2) 0 var(--s-2) var(--s-3);
  border-left: 2px solid var(--border);
  position: relative;
}
.chain-link + .chain-link { margin-top: -1px; }
.chain-role {
  font-size: 9px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-dim);
  align-self: center;
  padding: 1px 5px;
  border: 1px solid var(--edge);
  border-radius: var(--r-pill);
}
.chain-subject { font-size: var(--t-2); color: var(--fg); overflow-wrap: anywhere; }
.chain-meta { grid-column: 2; }

.drawer-actions {
  display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap;
  padding-top: var(--s-4);
  border-top: 1px solid var(--hairline);
}
.drawer-actions form { display: inline; }

/* ── the drag-and-drop layout editor (SPEC §5.3) ───────────────────────────
   What this replaced: a preview grid where each panel carried four move
   buttons, five width buttons, three height buttons and a form — thirteen
   controls per panel, each a round trip, to express what a pointer does in one
   gesture. At six panels the controls were taller than the layout. */
.vedit-bar {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  margin-bottom: var(--s-3);
}
.vedit-bar .section-title { margin: 0; }
.vedit-hint { font-size: var(--t-1); }
.vedit-status { margin-left: auto; font-size: var(--t-1); color: var(--fg-dim); }
.vedit-status.is-warn  { color: var(--status-warn-fg); }
.vedit-status.is-error { color: var(--status-crit-fg); }
.vedit-status.is-ok    { color: var(--status-ok-fg); }

/* The canvas shows its own grid. A drag onto an invisible grid is a guess:
   the columns have to be visible while you are aiming at them, and they are
   drawn as a repeating gradient rather than as elements so they cost nothing
   and cannot be dragged by accident. */
.grid-canvas {
  position: relative;
  padding: var(--s-2);
  border: 1px dashed var(--border);
  border-radius: var(--r-3);
  background-color: var(--surface);
  background-image: linear-gradient(90deg,
    var(--hairline) 0 1px, transparent 1px 100%);
  background-size: calc((100% - 8px * 11) / 12 + 8px) 100%;
  background-origin: content-box;
  background-clip: content-box;
}

.grid-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-hard-sm);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.grid-panel:hover { border-color: var(--border-strong); }
/* A panel that just arrived from the library. A short ring rather than a slide:
   the grid has already placed it, and animating it in from somewhere it never
   was would be a lie about where it went. */
.grid-panel.is-new {
  animation: panel-land var(--dur-3) var(--ease-out);
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  transition: outline-color var(--dur-3) var(--ease);
}
.grid-panel:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* The panel under the pointer lifts and stops animating its position: a
   transition on a dragged element lags behind the cursor, which reads as the
   interface being slow rather than as easing. */
.grid-panel.is-dragging {
  z-index: 5;
  will-change: transform;
  border-color: var(--accent);
  box-shadow: var(--shadow-2), 0 0 0 1px var(--accent-line);
  transition: none;
  cursor: grabbing;
}
.is-editing-live .grid-panel:not(.is-dragging) {
  transition: none;
}

.gp-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: 6px var(--s-2);
  border-bottom: 1px solid var(--hairline);
  cursor: grab;
  /* A drag that selects the title text instead of moving the panel is the
     first thing anyone hits. */
  user-select: none;
  touch-action: none;
}
.gp-head:active { cursor: grabbing; }
.gp-grip { color: var(--fg-dim); font-size: 12px; line-height: 1; }
.gp-title {
  font-size: var(--t-2); font-weight: 600; color: var(--fg);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.gp-size {
  margin-left: auto;
  font-family: var(--mono); font-size: 10px; font-variant-numeric: tabular-nums;
  color: var(--fg-dim);
}

/* The per-panel controls appear on hover and on keyboard focus. Hover alone
   would make them unreachable without a pointer, which is the usual way this
   pattern is got wrong. */
.gp-actions { display: flex; gap: 2px; opacity: 0; transition: opacity var(--dur-2) var(--ease); }
.grid-panel:hover .gp-actions,
.grid-panel:focus-within .gp-actions { opacity: 1; }
.gp-btn {
  appearance: none; border: 0; background: none;
  color: var(--fg-dim); cursor: pointer;
  font-size: 11px; line-height: 1; padding: 3px 5px; border-radius: var(--r-1);
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.gp-btn:hover { color: var(--fg-strong); background: var(--surface-3); }
.gp-btn[data-action="delete"]:hover { color: var(--status-crit-fg); background: var(--status-crit-bg); }

.gp-body {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s-1); padding: var(--s-2);
}
/* The sketch caps rather than fills: on a tall panel a stretched sparkline
   would read as data, and it is not data. */
.gp-sketch { width: 100%; max-width: 120px; opacity: .7; }
.gp-sketch svg { display: block; width: 100%; height: auto; }
.gp-sketch polyline { fill: none; stroke: var(--metric-cpu); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.gp-sketch .s-2 { stroke: var(--metric-memory); }
.gp-sketch .s-bar { fill: var(--metric-memory); }
.gp-sketch .s-strong { fill: var(--fg-muted); }
.gp-sketch .s-dot { fill: var(--status-warn-fill); }
.gp-sketch .s-track { fill: none; stroke: var(--surface-3); stroke-width: 4.5; }
.gp-sketch .s-arc { fill: none; stroke: var(--metric-cpu); stroke-width: 4.5; stroke-linecap: round; }
.gp-sketch .s-text { fill: var(--fg-strong); font-family: var(--mono); font-size: 14px; font-weight: 600; }
.gp-type {
  font-family: var(--mono); font-size: var(--t-1); letter-spacing: .04em;
  color: var(--fg-dim);
}

/* The resize grip. Bottom-right because that is where every window corner has
   been for forty years. */
.gp-resize {
  position: absolute; right: 0; bottom: 0;
  width: 16px; height: 16px;
  cursor: nwse-resize;
  touch-action: none;
  background:
    linear-gradient(135deg, transparent 0 50%, var(--border-strong) 50% 60%, transparent 60% 72%,
                    var(--border-strong) 72% 82%, transparent 82%);
  opacity: 0; transition: opacity var(--dur-2) var(--ease);
}
.grid-panel { position: relative; }
.grid-panel:hover .gp-resize, .grid-panel:focus-within .gp-resize { opacity: 1; }

/* ── the panel library ───────────────────────────────────────────────────
   Named .lib-*, not .widget-*.
   
   A library entry was called .widget, and .widget is what a dashboard panel has
   been called since §5.3 — so this block's `display: grid` and `background:
   var(--surface-3)` landed on every panel on the Overview, laying their headers
   out in a narrow left-hand column. It shipped, because the class WAS styled and
   the page threw no error; only looking at it found it. Third instance of the
   same failure in three phases, after .timeline and .ID — hence
   TestNoClassIsLaidOutTwice below it. */
.lib-list { display: flex; flex-direction: column; gap: var(--s-2); }
.lib-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  padding: var(--s-3);
  text-align: left;
  background: var(--surface-3);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-hard-sm);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease),
              transform var(--dur-press) var(--ease), box-shadow var(--dur-press) var(--ease);
}
.lib-item:hover { background: var(--surface-2); }
.lib-item:active { transform: var(--press-sm); box-shadow: none; }
.lib-item:disabled { opacity: .5; cursor: default; }
.lib-thumb {
  display: block; width: 52px; height: 36px;
  background: var(--canvas);
  border: 1px solid var(--edge);
  border-radius: var(--r-1);
  padding: 2px;
}
.thumb { display: block; width: 100%; height: 100%; }
.thumb-line { fill: none; stroke: var(--metric-cpu); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.thumb-line-2 { stroke: var(--metric-memory); }
.thumb-bar { fill: var(--metric-memory); }
.thumb-bar-strong { fill: var(--fg-muted); }
.thumb-dot { fill: var(--status-warn-fill); }
.thumb-track { fill: none; stroke: var(--surface-3); stroke-width: 4; }
.thumb-arc { fill: none; stroke: var(--metric-cpu); stroke-width: 4; stroke-linecap: round; }

.lib-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.lib-label {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-2); font-weight: 600; color: var(--fg-strong);
}
.lib-blurb { font-size: var(--t-1); line-height: 1.45; color: var(--fg-muted); }
.lib-add {
  font-size: 15px; line-height: 1; color: var(--fg-dim);
  width: 22px; height: 22px; display: grid; place-items: center;
  border: 1px solid var(--edge);
}
.lib-item:hover .lib-add { color: var(--accent-text); border-color: var(--accent-line); }

.inspector-form { display: flex; flex-direction: column; gap: var(--s-3); }

/* The alert action drawer's forms. Three of them stacked in one panel, so each
   needs to say what it is for before its controls — a bare "Snooze" button
   under a duration select assumes the reader already knows the difference
   between snoozing and closing. */
.act-form { display: flex; flex-direction: column; gap: var(--s-3); }
.act-hint { margin: 0; font-size: var(--t-1); line-height: 1.45; color: var(--fg-muted); max-width: 52ch; }
.act-open { white-space: nowrap; }

/* A gauge panel is the ring component in a dashboard cell: no card of its own,
   because the panel already is one. */
.vital-gauge { background: none; border: 0; padding: 0; height: 100%; justify-content: center; }

/* The host list pager.
   
   Laid out with flex and a gap rather than margins on the children, so the
   two ends stay put whichever of them is a link and which is a disabled
   span — the previous and next controls swap between the two on the first
   and last page, and margins that only exist on one of them shift the row. */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
  font-size: 13px;
  color: var(--fg-muted);
}
.pager-count {
  font-variant-numeric: tabular-nums;
}
.pager-links {
  display: flex;
  align-items: center;
  gap: 8px;
}
.pager-page {
  font-variant-numeric: tabular-nums;
}
/* Rows-per-page. Small targets, so they get real padding rather than relying
   on the text box: 10 and 100 are different widths and a row of numbers that
   jitters as you move between them reads as broken. */
.pager-sizes {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-right: 6px;
}
.pager-sizes-label {
  margin-right: 4px;
  color: var(--fg-dim);
}
.pager-size {
  display: inline-block;
  min-width: 28px;
  padding: 3px 6px;
  text-align: center;
  border-radius: var(--r-1);
  color: var(--fg-muted);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
a.pager-size:hover {
  background: var(--surface-3);
  color: var(--fg-strong);
}
.pager-size.is-current {
  background: var(--surface-3);
  color: var(--fg-strong);
}
/* A dead end reads as a dead end. Rendered as a span rather than a disabled
   anchor because an <a> with no href is still in the tab order in some
   browsers, and tabbing onto a control that does nothing is worse than not
   reaching it. */
.pager .is-disabled {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

/* The audit export menu. A popover rather than a bare link, because the
   question it answers — which period, and outages or every threshold crossed —
   has no sensible default that is right for everybody, and a link that guesses
   produces a file somebody sends to an auditor without reading. */
.export-menu {
  position: relative;
  display: inline-block;
}
.export-menu > summary {
  list-style: none;
  cursor: pointer;
}
.export-menu > summary::-webkit-details-marker {
  display: none;
}
.export-form {
  position: absolute;
  right: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 320px;
  margin-top: 6px;
  padding: 14px;
  background: var(--surface-2);
  border: var(--edge-w) solid var(--edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-hard);
  text-align: left;
  font-size: 13px;
}
.export-form label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--fg-muted);
}
.export-dates {
  display: flex;
  gap: 10px;
}
.export-dates label {
  flex: 1;
}
.export-hint {
  margin: 0;
  color: var(--fg-dim);
  font-size: 12px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* The sign-in notice: how to get an account, or which credentials a shared
 * instance expects. Set through the login_notice setting. */
.auth-notice {
  /* Line breaks in login_notice are kept: "Username: demo" above
     "Password: torwarden-demo" reads as two facts, where one run-together line
     reads as a sentence nobody parses. */
  white-space: pre-line;
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--hairline);
  text-align: center;
}
.auth-notice code {
  padding: 1px 6px;
  border-radius: 5px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
