/*
 * ============================================================================
 * THESIS: an on-call console that answers "what is broken, and what do I do
 * about it" before it answers anything else. It refuses the flat header of
 * eleven equal links and the spreadsheet front door, because neither of them
 * ever says anything.
 *
 * OWN-WORLD: a dark instrument panel in three planes. The chrome (rail,
 * topbar) is the fixed apparatus, the surface is the live readout, and the
 * raised plane is what the operator pulled open. Saturated colour belongs to
 * verdicts and to nothing else; one indigo carries selection and the primary
 * action. Hairlines, not shadows, do the separating. Every number is tabular.
 *
 * FIRST VIEWPORT: the rail on the inline-start edge with four named groups,
 * the topbar carrying search and live state, and — where a table used to be —
 * a health bar that shows the whole fleet's verdict as one proportional
 * measure, with the failing monitors listed underneath it, already actionable.
 * ============================================================================
 *
 * Rules this file is held to:
 *
 * 1. Every colour a component uses is a semantic token (--down, --accent),
 *    never a raw hex and never a palette name (--red-500). A component that
 *    reads --down keeps working when the palette moves; one that reads #ff6b7a
 *    has to be found and edited.
 * 2. Light mode overrides *every* colour token, not just the surfaces. An
 *    earlier version overrode four surface tokens and left the status colours
 *    at their dark-mode values, so a light-mode operator read "up" at about
 *    2.3:1 — below the 4.5:1 floor, on the one word the page exists to convey.
 *    Every value in both schemes is measured, not judged by eye.
 * 3. Colour is never the only channel. Every verdict is a word first; the hue,
 *    the dot and the pulse are reinforcement. That is what keeps the look from
 *    costing anything at 3am on a bad monitor.
 *
 * Dark is the default because this is an on-call console read at the hours
 * on-call happens, and the light values are the override rather than the other
 * way round.
 */

:root {
  color-scheme: dark light;

  /* --------------------------------------------------------------------
   * Planes.
   *
   * Four, and they mean different things rather than being four brightnesses.
   * `--chrome` is the apparatus that never scrolls; `--surface` is content;
   * `--raised` is something the operator opened over the top; `--sunken` is a
   * hole in the surface — an input, a table head, a block of YAML.
   * -------------------------------------------------------------------- */
  /*
   * Spaced by ΔL*, not by contrast ratio. Between code values 9 and 31 the
   * +0.05 flare term dominates the WCAG formula, so the ratio says nothing
   * about whether anyone sees the step: the previous ramp measured a
   * defensible-looking 1.09:1 between --surface and --bg and was invisible.
   * Every adjacent pair here clears ΔL* 3.8, which is where a large-field edge
   * is seen without a border helping it.
   *
   * --chrome is also cooler and bluer than --surface rather than only darker.
   * Near black there is not enough luminance range left to separate two planes
   * by lightness alone, and the apparatus/readout distinction is the one the
   * whole shell rests on.
   */
  --bg: #05070c;
  --chrome: #0e131b;
  --chrome-hover: #1a2130;
  --surface: #171c27;
  --surface-hover: #1f2634;
  --raised: #242c3c;
  --raised-hover: #2d3648;
  --sunken: #0e131c;

  /*
   * Three border tiers, because one token cannot say both "a rule inside this
   * box" and "the edge of the box". A container's outline is always heavier
   * than the rules drawn inside it.
   *
   * --line-strong is the only tier WCAG 1.4.11 actually governs — it is what
   * bounds a control somebody operates — so it is the only one held to 3:1.
   */
  --line: #272f3d;        /* rules inside a container that already exists */
  --edge: #3a4457;        /* the boundary of a container: 1.74:1 on --surface */
  --line-strong: #616b82; /* operated controls: 3.19:1 on --surface, 3.49:1 on --sunken */

  /* Three text weights. Measured at 14.2:1, 7.5:1 and 5.6:1 on --surface. */
  --text: #e7eaf1;
  --text-2: #a3acc0;
  /*
   * Lifted from #78829a. The old value cleared 4.5:1 on the old --surface and
   * failed at 4.28:1 on --raised, which is where .menu-field's label lives.
   * On the retuned planes this clears on every one of them.
   */
  --text-3: #8a94ab;

  /* --------------------------------------------------------------------
   * Status. These four are the vocabulary of the whole product, and they are
   * the only saturated colours allowed to touch content.
   * -------------------------------------------------------------------- */
  --up: #34d399;
  --down: #ff6b7a;
  --degraded: #f5b544;
  --unknown: #94a3b8;

  /*
   * Two tints per status: a `-wash` for large fills (pill backgrounds, chart
   * bands, health-bar segments) and a `-line` for the border that has to read
   * against the wash without shouting. Kept as tokens because a raw rgba() in
   * a component cannot follow the palette into light mode.
   */
  --up-wash: rgb(52 211 153 / 0.13);
  --down-wash: rgb(255 107 122 / 0.15);
  --degraded-wash: rgb(245 181 68 / 0.14);
  --unknown-wash: rgb(148 163 184 / 0.12);
  --up-line: rgb(52 211 153 / 0.34);
  --down-line: rgb(255 107 122 / 0.38);
  --degraded-line: rgb(245 181 68 / 0.34);
  --unknown-line: rgb(148 163 184 / 0.30);
  --down-glow: rgb(255 107 122 / 0.42);

  /*
   * The accent. One colour, and it is spoken for: the current section, the
   * primary action, a focus ring, a selected row. It never decorates. An ops
   * console that paints indigo on inactive things has spent the one signal it
   * had for "this is the one you are on".
   */
  --accent: #7c8fff;
  --accent-strong: #6478ff;
  --accent-wash: rgb(124 143 255 / 0.13);
  /*
   * A second, heavier wash, and its own token rather than a bump to
   * --accent-wash — which also draws the focus ring, ::selection, the avatar
   * and .banner.info, none of which want to get louder. This one has exactly
   * one job: the nav item you are currently on, which is the single most
   * important piece of state in a console with eleven destinations and was
   * previously carried by a fill 1.19:1 from the rail behind it.
   */
  --accent-wash-strong: rgb(124 143 255 / 0.22);
  --accent-line: rgb(124 143 255 / 0.35);
  --on-accent: #070a14;
  --focus: #9fb0ff;

  /* --------------------------------------------------------------------
   * Space. A 4px scale, dense end: this is a console, the operator is
   * comparing rows rather than reading prose, and vertical air costs rows.
   * -------------------------------------------------------------------- */
  --space-1: 0.25rem;   /*  4px */
  --space-2: 0.5rem;    /*  8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-5: 1.25rem;   /* 20px */
  --space-6: 1.5rem;    /* 24px */
  --space-7: 2rem;      /* 32px */
  --space-8: 3rem;      /* 48px */

  /*
   * Rhythm, on a 1:2:4 ladder, and the reason it is three named tokens rather
   * than three raw --space picks: 21 of the 27 block margins in this file used
   * to resolve to 16px, which made the gap *between* two panels identical to
   * the gap between a panel's own title and its body. Proximity is the
   * strongest grouping cue there is, and at a ratio of 1.0 it groups nothing.
   *
   * The binding constraint is the middle rung: --stack must exceed a
   * component's own padding (20px), or the last line of each panel is closer to
   * the next panel than to its own and proximity groups backwards.
   */
  --stack-tight: var(--space-3); /* 12px — inside a component */
  --stack: var(--space-6);       /* 24px — between components */
  --stack-loose: var(--space-8); /* 48px — between sections */

  --radius-sm: 4px;
  --radius: 7px;
  --radius-lg: 10px;
  --radius-xl: 14px;
  --radius-pill: 999px;

  /*
   * Type scale, ratio ~1.15. Product UI carries more type elements than a
   * brand page does, and an exaggerated ratio between them reads as noise
   * rather than as hierarchy.
   */
  --text-xs: 0.6875rem; /* 11px, the floor: column headers */
  --text-sm: 0.75rem;   /* 12px */
  --text-base: 0.8125rem; /* 13px, body */
  --text-md: 0.875rem;  /* 14px, card and panel titles */
  --text-lg: 1rem;      /* 16px, section headings */
  --text-xl: 1.1875rem; /* 19px, the verdict and the metric figure */
  --text-2xl: 1.5rem;   /* 24px, the page title */
  /* --text-3xl removed: eight rungs were authored and this one had no consumer. */

  /*
   * Three weights, not eight. The Latin system stack resolves to Segoe UI here,
   * which ships 400/600/700 only, so 550, 580, 620, 640 and 650 collapsed onto
   * two rendered faces under CSS Fonts 4 weight matching — td.strong at 550 and
   * .row-link at 580 were the same face in the same table row. Vazirmatn is a
   * true variable font and rendered all eight faithfully, in 20-30 unit steps
   * that are below the perceptual threshold at 13px. Either way the weight axis
   * carried nothing. Three real steps widen the Persian gap to 100 units.
   */
  --fw-body: 400;
  --fw-label: 600;
  --fw-title: 700;

  --hit: 2rem;          /* dense control height, fine pointers */
  --hit-lg: 2.25rem;

  /*
   * Elevation. Every one carries an offset and a real blur; a zero-offset
   * coloured halo is decoration, not depth.
   */
  --shadow-1: 0 1px 2px rgb(0 0 0 / 0.32);
  --shadow-2: 0 4px 12px -2px rgb(0 0 0 / 0.42), 0 2px 4px -2px rgb(0 0 0 / 0.3);
  --shadow-3: 0 16px 40px -12px rgb(0 0 0 / 0.62), 0 4px 10px -4px rgb(0 0 0 / 0.4);

  /*
   * Durations, not one duration. A hover has to keep up with the pointer; a
   * panel arriving is allowed to take its time. One number for both makes the
   * fast things sluggish and the slow things abrupt.
   */
  --t-instant: 80ms;
  --t-fast: 140ms;
  --t-slow: 260ms;
  --ease: cubic-bezier(0.2, 0.8, 0.3, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Shell geometry. The rail width is a variable because it collapses. */
  --rail: 15.5rem;
  --rail-collapsed: 3.5rem;
  --topbar-h: 3.25rem;
  --content-max: 100%;
}

/*
 * Collapsed rail. Set on <html> by app.js from a stored preference, so the
 * shell is already the right width on the first paint rather than snapping
 * narrower once the script runs.
 */
:root[data-rail='collapsed'] { --rail: var(--rail-collapsed); }

/*
 * Light, selected rather than merely preferred.
 *
 * This was `@media (prefers-color-scheme: light)`, which is the operating
 * system's preference and is not something a page can talk back to. The
 * Appearance control set `color-scheme` on <html> believing that would
 * override it — but `color-scheme` only tells the browser how to paint the
 * widgets it draws itself, the scrollbars and the form controls, and has no
 * say over which media query matches. So on a machine set to dark the query
 * never matched, this whole block never applied, and all three buttons
 * produced the same dark page.
 *
 * The preference is now read in script — once before first paint, and again
 * whenever the system changes its mind while nobody has chosen — and lands
 * on <html> as `data-theme`. One input to the stylesheet instead of two that
 * disagreed, and the choice is the one that wins.
 */
:root[data-theme='light'] {
  /*
   * The light scheme used to set --chrome, --surface and --raised all to
   * #ffffff: the rail, every panel and every dialog were one pixel value,
   * separated by a 1.23:1 hairline. The plane channel was not weak for a
   * daytime operator, it was absent.
   *
   * The page ground drops instead of the surfaces rising, because --surface
   * has to stay white for the readout to be the brightest thing on screen.
   */
  --bg: #e9edf5;
  --chrome: #f5f8fd;
  --chrome-hover: #e7ecf6;
  --surface: #ffffff;
  --surface-hover: #f0f3fa;
  --raised: #ffffff;
  --raised-hover: #eef2f9;
  --sunken: #eaeef6;

  --line: #d7dde9;
  --edge: #aab4c8;
  --line-strong: #78849c;

  --text: #131722;
  --text-2: #4d566e;
  /* Darkened to hold 4.5:1 on the new, darker --bg as well as on --surface. */
  --text-3: #5d6780;

  /*
   * Darkened until each clears 4.5:1 against --surface (#ffffff). The
   * dark-mode green and amber are tuned for a near-black ground and are
   * close to invisible on white — which on the one word this page exists to
   * convey is not a cosmetic problem.
   */
  --up: #067647;
  /*
   * Darkened from #d0273c. The page ground is no longer white, and on the new
   * --bg the old value measured 4.43:1 — under the floor, on the one word
   * this page exists to convey.
   */
  --down: #c41c36;
  --degraded: #8a5a00;
  --unknown: #55607a;

  --up-wash: rgb(6 118 71 / 0.10);
  --down-wash: rgb(196 28 54 / 0.10);
  --degraded-wash: rgb(138 90 0 / 0.12);
  --unknown-wash: rgb(85 96 122 / 0.10);
  --up-line: rgb(6 118 71 / 0.30);
  --down-line: rgb(196 28 54 / 0.32);
  --degraded-line: rgb(138 90 0 / 0.30);
  --unknown-line: rgb(85 96 122 / 0.28);
  --down-glow: rgb(196 28 54 / 0.28);

  --accent: #3550e8;
  --accent-strong: #2a41cc;
  --accent-wash: rgb(53 80 232 / 0.09);
  --accent-wash-strong: rgb(53 80 232 / 0.14);
  --accent-line: rgb(53 80 232 / 0.28);
  --on-accent: #ffffff;
  --focus: #2a41cc;

  --shadow-1: 0 1px 2px rgb(19 23 34 / 0.07);
  --shadow-2: 0 4px 12px -2px rgb(19 23 34 / 0.10), 0 2px 4px -2px rgb(19 23 34 / 0.06);
  --shadow-3: 0 16px 40px -12px rgb(19 23 34 / 0.20), 0 4px 10px -4px rgb(19 23 34 / 0.08);
}

* { box-sizing: border-box; }

/* ------------------------------------------------------------------ *
 * Type
 * ------------------------------------------------------------------ *
 *
 * Latin runs on whatever the operating system already has: it is the fastest
 * possible font, it is the one the rest of the machine is drawing, and no
 * download can fail.
 *
 * Persian does not get that. The system Arabic-script fallback on a Linux
 * server, a Windows workstation and a Mac are three different faces with three
 * different metrics, and two of them were never drawn for Persian — the ye and
 * the kaf come out Arabic, the digits are the wrong shape, and a table of them
 * does not align. So Persian ships its own face and stops depending on what
 * happens to be installed.
 *
 * Self-hosted, not from a font CDN, for the same reason the icons are inline in
 * index.html: this console is installed on networks with no route out, and it
 * is read during an outage. A stylesheet that reaches for fonts.googleapis.com
 * is a stylesheet that renders in a fallback exactly when it matters.
 */
@font-face {
  font-family: 'Vazirmatn';
  /*
   * One variable file for every weight this page uses — 400 through 700 — at
   * 111KB. The four static faces that would cover the same range are 200KB and
   * four requests.
   */
  /*
   * `woff2`, not `woff2-variations`.
   *
   * CSS Fonts 4 defines exactly seven format keywords and that is not one of
   * them — it was a draft spelling that browsers were once told to expect. An
   * implementation that follows the spec skips a `src` whose format it does not
   * recognise, so the entry would have been silently dropped and Persian would
   * have rendered in the system fallback with nothing anywhere saying why. A
   * variable woff2 loads correctly under the plain keyword.
   *
   * And no `local()`. The saving is one 111KB request on the same origin; the
   * cost is that a machine with any older Vazir installed under that name
   * renders the console in it instead — different metrics, possibly missing
   * glyphs, and different on one operator's screen than on another's. What this
   * page draws should not depend on what somebody once installed.
   */
  src: url('/fonts/vazirmatn-variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  /*
   * `swap`, not `block`. The alternative is invisible text while the font
   * loads, on the screen somebody opened to find out whether the service is
   * down. A reflow is cheaper than a blank page.
   */
  font-display: swap;
}

:root {
  --font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace;
}

/*
 * Keyed on the language, not on the direction: a right-to-left language that
 * is not Persian would want its own face, and Persian set left-to-right for
 * some reason would still want this one.
 */
:root[lang='fa'] {
  --font-sans: 'Vazirmatn', ui-sans-serif, system-ui, sans-serif;
}

/*
 * Persian sits lower and reads smaller at the same nominal size than the Latin
 * system stack, and the descenders of a Persian face need the extra leading.
 * One rule rather than a size bump per component.
 */
:root[lang='fa'] body { line-height: 1.75; }

/*
 * The whole ladder, not two rungs of it.
 *
 * Bumping only --text-base and --text-md compressed every ratio the Latin
 * scale had been tuned for: h1 against body fell from 1.846 to 1.714, and the
 * section label went from one pixel smaller than its body text to two. Persian
 * is the locale this console is read in, so it got the *worse* hierarchy of the
 * two. Every rung moves, at the same ~1.15 ratio.
 */
:root[lang='fa'] {
  --text-xs: 0.75rem;    /* 12 */
  --text-sm: 0.8125rem;  /* 13 */
  --text-base: 0.875rem; /* 14 */
  --text-md: 0.9375rem;  /* 15 */
  --text-lg: 1.0625rem;  /* 17 */
  --text-xl: 1.25rem;    /* 20 */
  --text-2xl: 1.625rem;  /* 26 */
}

/*
 * The two channels the eyebrow idiom runs on, both of which are void here.
 *
 * `text-transform: uppercase` is an identity mapping on a caseless script, so
 * in Persian it does nothing at all — and it was the primary thing separating a
 * section label from body text. `letter-spacing` is worse than nothing: it
 * forcibly disconnects Arabic-script cursive joining, so the label is not
 * merely undifferentiated but harder to read than the text it heads.
 *
 * Rank comes back through size and ink instead, which is what the scale above
 * and the --text promotion on .section are for.
 *
 * Keyed on lang, not dir, so Persian set left-to-right gets the same treatment.
 */
:root[lang='fa'] .section,
:root[lang='fa'] thead th,
:root[lang='fa'] .nav-group-label,
:root[lang='fa'] .menu-field label,
:root[lang='fa'] .cond-preview-label,
:root[lang='fa'] .palette-group,
:root[lang='fa'] .avatar {
  text-transform: none;
  letter-spacing: normal;
}

/* Negative tracking joins Arabic-script faces badly. */
:root[lang='fa'] .page-head h1,
:root[lang='fa'] .verdict-headline,
:root[lang='fa'] .metric .metric-value,
:root[lang='fa'] h2,
:root[lang='fa'] h3 { letter-spacing: normal; }

html {
  /*
   * The header is sticky, so keyboard focus landing just under it would be
   * covered. `scroll-padding` keeps the focused control clear of it — WCAG 2.2
   * Focus Not Obscured.
   */
  scroll-padding-top: calc(var(--topbar-h) + var(--space-4));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /*
   * 13px, under the 16px body default. Justified only because there is no
   * running prose on these screens: every string is a label, a value, or a
   * table cell, and the density buys roughly four more monitor rows per
   * screen. Anything that *is* a sentence — help text, empty states — is not
   * shrunk further, and --text-xs is the floor nothing goes below.
   */
  font: var(--text-base)/1.6 var(--font-sans);
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

/*
 * Numbers line up column-wise. Latency and counts are read by comparison — a
 * proportional 1 next to a proportional 8 makes "is this row worse" a reading
 * task instead of a glance.
 */
table,
.metric,
.stat-value,
.num,
time,
code,
pre { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ *
 * Browser surfaces
 *
 * The parts nobody draws still carry the design. Selection, the caret and the
 * scrollbar ship with defaults that belong to no design system, and on a dark
 * console the stock scrollbar is a bright white gutter down the side of the
 * page. Themed from the palette, like everything else.
 * ------------------------------------------------------------------ */
::selection { background: var(--accent-wash); color: var(--text); }
:root { accent-color: var(--accent); caret-color: var(--accent); }

* { scrollbar-color: var(--line-strong) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border: 3px solid transparent;
  border-radius: var(--radius-pill);
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ------------------------------------------------------------------ *
 * Icons
 *
 * One rule for all of them. Sized in `em` so an icon tracks the text it sits
 * beside instead of needing a size class per context, and stroked with
 * `currentColor` so it inherits the colour of a danger button or a status pill
 * without a variant.
 * ------------------------------------------------------------------ */
/*
 * The sprite is never rendered, only referenced. Collapsed to nothing rather
 * than `display: none`, which in some browsers also stops <use> resolving the
 * symbols inside it.
 */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.ico {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.19em;
}
.ico-lg { width: 1.5em; height: 1.5em; stroke-width: 1.6; }
.ico-xl { width: 2.25rem; height: 2.25rem; stroke-width: 1.25; }

/* ------------------------------------------------------------------ *
 * Focus
 *
 * One rule, applied to everything focusable, before any component styling gets
 * a chance to compete with it. `:focus-visible` rather than `:focus` so a mouse
 * click on a button leaves no ring behind, but every keyboard arrival does.
 * ------------------------------------------------------------------ */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: fixed;
  inset-inline-start: var(--space-3);
  top: -4rem;
  z-index: 200;
  background: var(--accent);
  color: var(--on-accent);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius);
  text-decoration: none;
  font-weight: var(--fw-label);
  box-shadow: var(--shadow-3);
  transition: top var(--t-fast) var(--ease);
}
.skip-link:focus { top: var(--space-3); }

/* Present to a screen reader, absent to the eye. Used by the route announcer. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * `hidden` as a class rather than the attribute, because several of these
 * elements are toggled after a `display` has already been set by a component
 * rule, and the attribute loses to any `display`. This one is explicit.
 */
.hidden { display: none !important; }

/* ==================================================================== *
 * The shell
 *
 * A grid of two columns: the rail, and everything else. Grid places along the
 * inline axis, so `dir="rtl"` moves the rail to the right edge with no rule
 * here mentioning direction.
 *
 * The rail and the topbar are one plane (--chrome) and the content is another
 * (--bg), which is the whole reason the console reads as an instrument rather
 * than as one flat slab: the apparatus does not move, and the readout does.
 * ==================================================================== */

#shell {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100vh;
  min-height: 100dvh;
  transition: grid-template-columns var(--t-slow) var(--ease);
}

/*
 * Signed out there is no chrome at all: the sign-in screen owns the viewport.
 * A rail full of links to places that will refuse you is not navigation, and a
 * search field that can find nothing is worse than none.
 */
#shell[data-chrome='bare'] { grid-template-columns: minmax(0, 1fr); }
#shell[data-chrome='bare'] #rail,
#shell[data-chrome='bare'] #topbar { display: none; }

/* ------------------------------------------------------------------ *
 * The rail
 * ------------------------------------------------------------------ */

#rail {
  grid-row: 1;
  display: flex;
  flex-direction: column;
  background: var(--chrome);
  border-inline-end: 1px solid var(--edge);
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  z-index: 30;
  overflow: hidden;
}

.rail-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: var(--topbar-h);
  padding-inline: var(--space-4);
  flex: none;
  border-bottom: 1px solid var(--line);
}
.rail-brand a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text);
  text-decoration: none;
  font-weight: var(--fw-title);
  font-size: var(--text-md);
  letter-spacing: -0.012em;
  min-width: 0;
}
.rail-brand .brand-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/*
 * The mark. Drawn rather than tinted: a rounded plate carrying the pulse
 * glyph, which is the one place on the page the accent is allowed to be a
 * field rather than a line — it is the product's identity, not a status.
 */
.brand-mark {
  width: 1.625rem;
  height: 1.625rem;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-1);
}
.brand-mark .ico { width: 1rem; height: 1rem; stroke-width: 2.25; }

/* The scrolling middle. Only this scrolls; the brand and the footer are fixed. */
.rail-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-3) var(--space-2) var(--space-4);
}

.nav-group { margin-bottom: var(--space-4); }
/*
 * A real separator. This was `margin-top: var(--space-1)` — 4px against an
 * adjoining 16px, which collapses to 16 and therefore did nothing at all. Four
 * group labels were the only thing grouping eleven links.
 */
.nav-group + .nav-group {
  border-top: 1px solid var(--line);
  padding-top: var(--space-3);
}
/* The collapsed rail draws its own rule in place of the label; don't double. */
:root[data-rail='collapsed'] .nav-group + .nav-group { border-top: 0; }

/*
 * The group label. Not a heading anyone navigates to — the <nav> already has
 * an accessible name — so it is presentational text with the list beneath it
 * carrying the semantics.
 */
.nav-group-label {
  font-size: var(--text-xs);
  font-weight: var(--fw-label);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  /* Lifted from --text-3, which is what the inactive nav icons below it use:
     the legend has to outrank the things it organises. */
  color: var(--text-2);
  padding: var(--space-2) var(--space-3) var(--space-1);
  white-space: nowrap;
  overflow: hidden;
}

.nav-group ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }

.nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  min-height: var(--hit-lg);
  border-radius: var(--radius);
  color: var(--text-2);
  text-decoration: none;
  font-weight: var(--fw-label);
  position: relative;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.nav-link .ico { color: var(--text-3); transition: color var(--t-fast) var(--ease); }
.nav-link .nav-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Each hover uses the hover of the plane it actually sits on. One shared
   --surface-hover was serving three planes, which is why a menu hover went the
   wrong way and a rail hover in light mode moved by ΔL* 1.7. */
.nav-link:hover { color: var(--text); background: var(--chrome-hover); }
.nav-link:hover .ico { color: var(--text-2); }

/*
 * The current section. Marked with aria-current by the router, so the fill,
 * the marker and the announcement all come from the same fact rather than
 * from three places that can disagree.
 */
.nav-link[aria-current='page'] {
  color: var(--text);
  /* Its own token. --accent-wash measured 1.19:1 against the rail — the same
     as a hover — on the single most important piece of state in the shell. */
  background: var(--accent-wash-strong);
  font-weight: var(--fw-label);
}
.nav-link[aria-current='page'] .ico { color: var(--accent); }
.nav-link[aria-current='page']::before {
  content: '';
  position: absolute;
  /* Inside the link. It was parked at negative space-2, i.e. flush against
     x=0 of the viewport, where the eye never rests. */
  inset-inline-start: var(--space-1);
  top: 50%;
  translate: 0 -50%;
  width: 3px;
  height: 1.125rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
}

/*
 * A count beside a link — open incidents, probes needing approval. Only ever
 * rendered when it is non-zero, because a badge reading 0 is a thing to read
 * and dismiss on every single navigation.
 */
.nav-count {
  margin-inline-start: auto;
  font-size: var(--text-xs);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
  padding: 0.0625rem var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--chrome-hover);
  color: var(--text-2);
  border: 1px solid var(--line);
}
.nav-count.is-down { background: var(--down-wash); color: var(--down); border-color: var(--down-line); }
.nav-count.is-degraded { background: var(--degraded-wash); color: var(--degraded); border-color: var(--degraded-line); }

.rail-foot {
  flex: none;
  border-top: 1px solid var(--line);
  padding: var(--space-2);
}

/* ------------------------------------------------------------------ *
 * The collapsed rail
 *
 * Labels go, icons stay centred, and the accessible name moves onto the link
 * itself — set by app.js, so a screen reader and a tooltip say the same word.
 * The group labels are replaced by a rule, because four unlabelled icon
 * clusters with no separator is a worse map than none.
 * ------------------------------------------------------------------ */
:root[data-rail='collapsed'] .rail-brand { padding-inline: 0; justify-content: center; }
:root[data-rail='collapsed'] .brand-name,
:root[data-rail='collapsed'] .nav-text,
:root[data-rail='collapsed'] .rail-foot .btn-text { display: none; }
:root[data-rail='collapsed'] .nav-group-label {
  height: 1px;
  padding: 0;
  margin: var(--space-3) var(--space-3);
  background: var(--line);
  color: transparent;
  overflow: hidden;
}
:root[data-rail='collapsed'] .nav-link { justify-content: center; padding-inline: 0; }
/* Redundant now that the expanded marker also sits inside the link. */
/* The count becomes a dot: there is no room for a number, and the fact that
   there IS something is what the collapsed rail has to carry. */
:root[data-rail='collapsed'] .nav-count {
  position: absolute;
  top: 0.3125rem;
  inset-inline-end: 0.3125rem;
  min-width: 0.5rem;
  height: 0.5rem;
  padding: 0;
  font-size: 0;
  border-radius: 50%;
}
:root[data-rail='collapsed'] .rail-foot .btn { justify-content: center; padding-inline: 0; }

/* ------------------------------------------------------------------ *
 * The topbar
 * ------------------------------------------------------------------ */

#content { grid-row: 1; display: flex; flex-direction: column; min-width: 0; }

#topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--topbar-h);
  padding-inline: var(--space-4);
  border-bottom: 1px solid var(--edge);
  /*
   * Opaque. This carried `color-mix(in srgb, var(--chrome) 82%, transparent)`
   * over a blur, and the composite measured 1.026:1 against the page — flatter
   * than the raw --chrome it started from, so the decoration was cancelling the
   * plane it decorated on the largest persistent element of every route. The
   * blur was the only thing masking content scrolling under the bar; the solid
   * fill and the --edge rule below do that better.
   */
  background: var(--chrome);
}

/*
 * Where the operator is. A breadcrumb rather than a repeat of the page
 * heading: the heading says what this page is, and this says how they got
 * here — which on a detail route is the only way back up.
 */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  font-size: var(--text-base);
  color: var(--text-3);
}
.crumbs a { color: var(--text-2); text-decoration: none; white-space: nowrap; }
.crumbs a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 0.2em; }
.crumbs .sep { color: var(--line-strong); flex: none; }
/* The group this screen belongs to. Not a link: the four groups are a map, not
   destinations — there is no page for "Monitoring". */
.crumbs .crumb-group { color: var(--text-3); white-space: nowrap; }
.crumbs .current {
  color: var(--text);
  font-weight: var(--fw-label);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The separator is a glyph that must not mirror with the page. */
[dir='rtl'] .crumbs .sep { scale: -1 1; }

/*
 * The bar's fixed controls, which must not be what gives when it is tight.
 * They are all flex items and so all shrink by default, which is how a
 * 44px-square toggle became a 17px one on a phone. Only the breadcrumb and
 * the live button are allowed to absorb it; both truncate rather than crush.
 */
#rail-toggle, #search-trigger, #identity { flex: none; }

.topbar-spacer { flex: 1; }

/*
 * The search trigger. A button that opens the palette, not an input: the
 * palette is a dialog with its own field, and two fields that look the same
 * and behave differently is the worst of both.
 */
.search-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--hit);
  padding: 0 var(--space-2) 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-3);
  font: inherit;
  cursor: pointer;
  min-width: 13rem;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.search-trigger:hover { border-color: var(--text-3); color: var(--text-2); }
.search-trigger .search-label { flex: 1; text-align: start; }

kbd {
  font: var(--fw-label) var(--text-xs)/1 var(--font-sans);
  padding: 0.1875rem var(--space-1);
  min-width: 1.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  background: var(--sunken);
  color: var(--text-3);
  /* A shortcut is Latin whatever the interface language is. */
  direction: ltr;
}

/* ------------------------------------------------------------------ *
 * The live indicator
 *
 * The socket says something changed; this says so and offers the one thing it
 * can do about it. Re-rendering under the operator instead would reorder a
 * list they are reading — worse than a list that is thirty seconds old and
 * says so.
 * ------------------------------------------------------------------ */
#live {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--hit);
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--degraded-line);
  background: var(--degraded-wash);
  color: var(--degraded);
  font: var(--fw-label) var(--text-sm)/1 var(--font-sans);
  cursor: pointer;
  max-width: 22rem;
  /*
   * Without this the ellipsis below is unreachable. A flex item will not
   * shrink past its content's intrinsic width unless it is told it may, so
   * this button appeared at its full 205px on a 360px topbar and took the
   * space out of everything else: the rail toggle went to 17px, the
   * breadcrumb to 2px, and the avatar off the right-hand edge -- a page that
   * scrolled sideways only while a live event happened to be showing.
   */
  min-width: 0;
  animation: live-in var(--t-slow) var(--ease-out);
}
#live:hover { background: color-mix(in srgb, var(--degraded) 20%, transparent); }
#live .live-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@keyframes live-in {
  from { opacity: 0; translate: 0 -0.5rem; scale: 0.96; }
  to { opacity: 1; translate: 0; scale: 1; }
}

/* ------------------------------------------------------------------ *
 * The identity menu
 * ------------------------------------------------------------------ */

.menu-anchor { position: relative; }

.avatar-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--hit);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-2);
  font: inherit;
  cursor: pointer;
  max-width: 14rem;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.avatar-button:hover, .avatar-button[aria-expanded='true'] {
  background: var(--chrome-hover);
  border-color: var(--edge);
  color: var(--text);
}
.avatar {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font: var(--fw-title) var(--text-xs)/1 var(--font-sans);
  text-transform: uppercase;
  /* Initials are Latin from an email address; never mirror them. */
  direction: ltr;
}
.avatar-button .who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-base);
}

/*
 * The dropdown. `position: absolute` inside a relatively-positioned anchor in
 * the topbar, which has no clipping ancestor — the rail scrolls, the topbar
 * does not.
 */
.menu {
  position: absolute;
  top: calc(100% + var(--space-2));
  inset-inline-end: 0;
  min-width: 15rem;
  z-index: 40;
  background: var(--raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-3);
  padding: var(--space-2);
  animation: menu-in var(--t-fast) var(--ease-out);
}
@keyframes menu-in {
  from { opacity: 0; translate: 0 -0.25rem; scale: 0.98; }
  to { opacity: 1; translate: 0; scale: 1; }
}
.menu-head { padding: var(--space-2) var(--space-3) var(--space-3); border-bottom: 1px solid var(--line); margin-bottom: var(--space-2); }
.menu-head .email { font-weight: var(--fw-label); word-break: break-all; }
.menu-head .role { color: var(--text-3); font-size: var(--text-sm); }
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: var(--hit-lg);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-2);
  font: inherit;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}
.menu-item:hover { background: var(--raised-hover); color: var(--text); }
.menu-item .ico { color: var(--text-3); }
.menu-item:hover .ico { color: var(--text-2); }
.menu-sep { height: 1px; background: var(--line); margin: var(--space-2) 0; }
.menu-field { padding: var(--space-2) var(--space-3) var(--space-1); }
.menu-field label { display: block; font-size: var(--text-xs); color: var(--text-3); margin-bottom: var(--space-1); text-transform: uppercase; letter-spacing: 0.07em; }

/* ------------------------------------------------------------------ *
 * The drawer trigger, and the rail as a drawer
 *
 * Below the breakpoint the rail leaves the grid and becomes an overlay. It
 * keeps its markup and every one of its rules; only its positioning changes,
 * so there is no second navigation to keep in step with the first.
 * ------------------------------------------------------------------ */
#rail-toggle { display: none; }
#scrim { display: none; }

@media (max-width: 60rem) {
  #shell { grid-template-columns: minmax(0, 1fr); }
  #rail-toggle { display: inline-flex; }

  #rail {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: min(19rem, 84vw);
    /* Off-canvas on the inline-start edge, which flips with the document. */
    translate: -100% 0;
    box-shadow: none;
    transition: translate var(--t-slow) var(--ease-out), box-shadow var(--t-slow) var(--ease);
    z-index: 60;
  }
  [dir='rtl'] #rail { translate: 100% 0; }
  :root[data-drawer='open'] #rail { translate: 0 0; box-shadow: var(--shadow-3); }
  [dir='rtl']:root[data-drawer='open'] #rail { translate: 0 0; }

  /* The drawer never collapses to a rail: on a phone it is open or gone. */
  :root[data-rail='collapsed'] { --rail: 15.5rem; }
  :root[data-rail='collapsed'] .brand-name,
  :root[data-rail='collapsed'] .nav-text,
  :root[data-rail='collapsed'] .rail-foot .btn-text { display: revert; }
  :root[data-rail='collapsed'] .nav-link { justify-content: flex-start; padding-inline: var(--space-3); }
  :root[data-rail='collapsed'] .nav-group-label {
    height: auto;
    padding: var(--space-2) var(--space-3) var(--space-1);
    margin: 0;
    background: none;
    color: var(--text-3);
  }
  :root[data-rail='collapsed'] .nav-count {
    position: static;
    min-width: auto;
    height: auto;
    padding: 0.0625rem var(--space-2);
    font-size: var(--text-xs);
    border-radius: var(--radius-pill);
  }

  #scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgb(0 0 0 / 0.5);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-slow) var(--ease);
  }
  :root[data-drawer='open'] #scrim { opacity: 1; pointer-events: auto; }
  /* The page behind a drawer must not scroll under the operator's thumb. */
  :root[data-drawer='open'] body { overflow: hidden; }
}

/* ------------------------------------------------------------------ *
 * The view
 * ------------------------------------------------------------------ */

main {
  flex: 1;
  padding: var(--space-6);
  max-width: var(--content-max);
  width: 100%;
  /* Forms get a reading measure; lists get the whole display. */
  margin-inline: 0;
}
main[data-width='reading'] { --content-max: 56rem; }
main[data-width='narrow'] { --content-max: 26rem; margin-inline: auto; padding-block: var(--space-8); }
/* Focused programmatically after a route change; the ring would be noise. */
main:focus { outline: none; }

/*
 * The footer, on every screen.
 *
 * Quiet on purpose: it is a fact somebody goes looking for once a month, when
 * they are writing a bug report or checking whether a rollout actually landed
 * here. It must be legible and it must never compete with the page — so it
 * takes the smallest step of the type scale and the dimmest text token, sits
 * above the safe-area inset for a phone, and is separated by a rule rather
 * than by a block of empty space.
 *
 * `margin-top: auto` inside the flex column pins it to the bottom of short
 * pages without pinning it over long ones, which is what `position: fixed`
 * would have done — and a bar that floats over the last row of a table is a
 * bar somebody has to scroll past.
 */
.app-foot {
  margin-top: auto;
  padding: var(--space-3) var(--space-6);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--edge);
  color: var(--text-3);
  font-size: var(--text-xs);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
}
/* Empty until the version arrives, and invisible while it is. */
.app-foot:empty { display: none; }
.app-foot .foot-version {
  font-family: var(--font-mono, ui-monospace, monospace);
  color: var(--text-2);
}
/* The sign-in screen centres a 26rem card; its footer follows the card. */
#shell[data-chrome='bare'] .app-foot { justify-content: center; border-top: none; }

/* ------------------------------------------------------------------ *
 * A limit, and how close to it we are
 *
 * A bar is drawn only where there is a ceiling. An unlimited resource has no
 * denominator, and a full-width bar with nothing behind it reads as "you are at
 * your limit" — the exact opposite of what it means.
 * ------------------------------------------------------------------ */

.usage-row + .usage-row { margin-top: var(--space-3); }
.usage-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-sm);
  margin-bottom: var(--space-1);
}
.usage-bar {
  height: 6px;
  border-radius: 3px;
  background: var(--edge);
  overflow: hidden;
}
.usage-bar > span {
  display: block;
  height: 100%;
  background: var(--accent, currentColor);
  /* Full is the one state worth a colour: it is the one that refuses the next
     write, and it should not have to be read off a number to be noticed. */
}
.usage-bar > span.is-full { background: var(--down, #d9534f); }

/* The price list. Cards rather than a table: they are being compared, not read
   down a column. */
.plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.plan-card {
  border: 1px solid var(--edge);
  border-radius: var(--radius-2, 8px);
  padding: var(--space-3);
}
.plan-card.is-current { border-color: var(--accent, currentColor); }
.plan-card h3 { margin: 0 0 var(--space-1); font-size: var(--text-sm); }
.plan-buy { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }

/* A two-way choice inside a form — email or mobile, on the sign-up screen. */
.seg { display: flex; gap: var(--space-1); margin-bottom: var(--space-3); }
.seg > button {
  flex: 1;
  padding: var(--space-2);
  border: 1px solid var(--edge);
  border-radius: var(--radius-2, 8px);
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
}
.seg > button.is-on { border-color: var(--accent, currentColor); color: var(--text-1); }

.new-plan-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.plain-list { margin: 0; padding-inline-start: var(--space-4); }
.plain-list > li + li { margin-top: var(--space-1); }

/* ==================================================================== *
 * Page furniture
 * ==================================================================== */

/*
 * The page header. Every screen gets one, and it carries the same four things
 * in the same places: what this is, one sentence of what it means, the live
 * count, and the actions. Twenty screens that each invented their own heading
 * arrangement is the reason the console read as twenty unrelated pages.
 */
.page-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
}
.page-head-text { min-width: 0; flex: 1 1 22rem; }
.page-head h1 {
  font-size: var(--text-2xl);
  font-weight: var(--fw-title);
  letter-spacing: -0.022em;
  margin: 0;
  line-height: 1.25;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
/* More space above a heading's description than below it does not apply here:
   the description belongs to the heading and sits tight under it. */
.page-head .lede {
  margin: var(--space-2) 0 0;
  color: var(--text-2);
  font-size: var(--text-md);
  /* Prose measure. The only running sentences on these screens. */
  max-width: 68ch;
}
.page-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/*
 * The page's mark.
 *
 * Shape, not hue — which is what lets it identify a screen without spending any
 * of the saturation budget the verdicts own. The glyph is the same one the rail
 * gives this destination, so an operator who learned the rail already knows it.
 *
 * `flex: none` or the plate squashes at the narrow breakpoint.
 */
.page-mark {
  flex: none;
  width: 3rem;
  height: 3rem;
  display: grid;
  place-items: center;
  border-radius: var(--radius-lg);
  background: var(--sunken);
  border: 1px solid var(--edge);
  color: var(--text-2);
}

/*
 * A count beside a page title. Rendered as its own element rather than
 * concatenated into the heading, so the heading text stays the thing the route
 * announcer reads and the number can update without rewriting it.
 */
.count-chip {
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.125rem var(--space-3);
}

/*
 * A section inside a page. The rule runs out from the label to the edge, which
 * is what makes a long screen scannable without a second heading level
 * competing with the page title.
 *
 * This used to be 12px, uppercase, 0.07em tracked, in --text-3 — character for
 * character the treatment `thead th` gets, at one pixel of size difference. The
 * one element whose whole job is to say "a new part starts here" rendered as
 * one more column header, and smaller than the body text it headed. It takes
 * the 16px rung instead, which the scale declared and no content page ever
 * used, at full --text ink and in sentence case.
 *
 * Sentence case is also what makes it work in Persian at all, where uppercase
 * is an identity mapping; see the [lang='fa'] block near the type tokens.
 */
.section {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-lg);
  font-weight: var(--fw-title);
  text-transform: none;
  letter-spacing: -0.01em;
  color: var(--text);
  margin: var(--stack-loose) 0 var(--stack-tight);
}
.section::after { content: ''; flex: 1; height: 1px; background: var(--edge); }
/* The icon stays quiet: it is the secondary channel, not the heading. */
.section .ico { color: var(--text-3); }
/*
 * Scoped, and load-bearing.
 *
 * A bare `.section:first-child` also matched in the ten places where app.js
 * wraps a section in an unstyled <div>, silently deleting the lead-in that is
 * the separator between two sections. It may only fire where there genuinely is
 * nothing above.
 */
main > .section:first-child,
.panel > .section:first-child,
.block > .section:first-child { margin-top: 0; }

/*
 * A section as an object. Padding and a rule can only exist once there is an
 * element to hang them on, which is what block() in app.js provides.
 *
 * The rule is on the boundary between two blocks rather than under each one, so
 * a screen does not end with a hanging line.
 */
.block { padding-block: var(--space-6); }
.block:first-child { padding-block-start: 0; }
.block:last-child { padding-block-end: 0; }
.block + .block { border-top: 1px solid var(--edge); }
.block > :last-child { margin-bottom: 0; }
.section-note {
  margin: calc(-1 * var(--space-2)) 0 var(--space-4);
  color: var(--text-2);
  max-width: 68ch;
}

/*
 * No `h2` rule any more. app.js emits exactly two <h2>s — section() at 194,
 * which classes to .section and overrode it anyway, and the sign-in heading,
 * which is styled inline. The old 19px rule was dead code, and reviving it
 * would put a section heading five pixels under the page title.
 *
 * h3 folds onto the 14px rung beside .panel-title, so a card title is one size
 * whether it was written as a heading or as a paragraph.
 */
h3 { font-size: var(--text-md); font-weight: var(--fw-title); letter-spacing: -0.01em; margin: 0 0 var(--space-2); }

.muted { color: var(--text-2); }
.dim { color: var(--text-3); }

/*
 * `code` names its own family and keeps it. A token, a digest, a header name
 * and a cron expression are all things the operator will paste somewhere else,
 * and they must be the glyphs they will paste.
 */
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 0.0625rem 0.3125rem;
}

pre {
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  line-height: 1.6;
  margin: 0 0 var(--space-4);
}
pre code { background: none; border: 0; padding: 0; font-size: inherit; }

/* ==================================================================== *
 * Toolbar and filters
 * ==================================================================== */

.toolbar {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
.toolbar-grow { flex: 1; }

/*
 * The filter bar above a list. A real control strip, not a heading with a
 * button glued to it: search, then the status segments, then the count of what
 * survived the filter — which is the number that tells the operator their
 * filter did something.
 */
/*
 * Apparatus, so it recedes: --sunken and the smaller radius. It used to be the
 * identical recipe to a .panel and a .table-scroll, which is how a screen ended
 * up reading as five interchangeable rectangles.
 */
.filters {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-2);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: var(--stack);
}
/* The segment group is already on --sunken here, so it needs its own edge back
   and the pressed segment needs a ring rather than only a fill. */
.filters .segmented { background: transparent; border-color: var(--edge); }
.filters .segmented button[aria-pressed='true'] { box-shadow: inset 0 0 0 1px var(--edge); }

.search-field { position: relative; flex: 1 1 15rem; min-width: 11rem; display: flex; align-items: center; }
.search-field .ico {
  position: absolute;
  inset-inline-start: var(--space-3);
  color: var(--text-3);
  pointer-events: none;
}
.search-field input { padding-inline-start: calc(var(--space-3) * 2 + 1.15em); }

/*
 * Segmented control. One choice from a small closed set, all options visible:
 * the alternative is a <select> that hides three of the four states an
 * operator is scanning for.
 */
/*
 * Wraps rather than overflowing. Six verdict segments do not fit on a 390px
 * screen, and the alternative — a horizontal scroller — hides the very
 * filters an operator is looking for behind a gesture nothing signposts.
 */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  padding: 2px;
  gap: 2px;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.segmented button {
  min-height: calc(var(--hit) - 4px);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: var(--text-2);
  font: var(--fw-label) var(--text-base)/1 var(--font-sans);
  cursor: pointer;
  box-shadow: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.segmented button:hover { background: var(--surface-hover); color: var(--text); transform: none; box-shadow: none; }
.segmented button[aria-pressed='true'] {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-1);
  font-weight: var(--fw-title);
}
.segmented button .seg-count { font-variant-numeric: tabular-nums; color: var(--text-3); font-size: var(--text-sm); }
.segmented button[aria-pressed='true'] .seg-count { color: var(--text-2); }

/* ==================================================================== *
 * Status
 * ==================================================================== */

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.125rem var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  line-height: 1.5;
  border: 1px solid var(--pill-line, var(--line));
  background: var(--pill-wash, var(--surface-hover));
  color: var(--pill-ink, var(--text-2));
  /* A verdict that wraps to two lines is a verdict misread at a glance. */
  white-space: nowrap;
}
.status-up { --pill-ink: var(--up); --pill-wash: var(--up-wash); --pill-line: var(--up-line); }
.status-down { --pill-ink: var(--down); --pill-wash: var(--down-wash); --pill-line: var(--down-line); }
.status-degraded { --pill-ink: var(--degraded); --pill-wash: var(--degraded-wash); --pill-line: var(--degraded-line); }
.status-unknown { --pill-ink: var(--unknown); --pill-wash: var(--unknown-wash); --pill-line: var(--unknown-line); }
.status-accent { --pill-ink: var(--accent); --pill-wash: var(--accent-wash); --pill-line: var(--accent-line); }

/*
 * The dot in front of the word. Decoration in the strict sense: the word is
 * always there beside it, so nothing is lost if the dot is not perceived.
 */
.dot {
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/*
 * Only `down` pulses, and that is the whole point. If every status animated,
 * the animation would say nothing; one pulsing row in a list of two hundred is
 * findable from across a room. It is also the only motion on these screens
 * that loops, which is why reduced-motion stops it rather than shortening it.
 */
.status-down .dot {
  animation: pulse-dot 1.8s var(--ease) infinite;
  box-shadow: 0 0 0 0 var(--down-glow);
}
@keyframes pulse-dot {
  0% { box-shadow: 0 0 0 0 var(--down-glow); }
  70% { box-shadow: 0 0 0 0.4375rem transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0.0625rem var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--sunken);
  border: 1px solid var(--line);
  color: var(--text-3);
  font-size: var(--text-sm);
  line-height: 1.5;
  white-space: nowrap;
}
.tag + .tag, .pill + .tag, .tag + .pill { margin-inline-start: var(--space-1); }

/* ==================================================================== *
 * Buttons
 *
 * Every interactive control here has default, hover, focus, active, disabled
 * and — where it does work — a busy state. Shipping half of those is what
 * makes an interface feel unfinished in a way nobody can point at.
 * ==================================================================== */

.btn, button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--hit-lg);
  padding: 0 var(--space-3);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  cursor: pointer;
  font: var(--fw-label) var(--text-base)/1 var(--font-sans);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: var(--shadow-1);
  transition:
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.btn:hover:not(:disabled), button:hover:not(:disabled) {
  background: var(--surface-hover);
  border-color: var(--text-3);
  box-shadow: var(--shadow-2);
}
/* Presses go down. A control that only ever lifts never feels pressed. */
.btn:active:not(:disabled), button:active:not(:disabled) { box-shadow: none; translate: 0 1px; }

.btn .ico, button .ico { color: var(--text-3); transition: color var(--t-fast) var(--ease); }
.btn:hover .ico, button:hover:not(:disabled) .ico { color: var(--text-2); }

button.primary, .btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: var(--fw-title);
}
button.primary:hover:not(:disabled), .btn.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}
button.primary .ico, .btn.primary .ico { color: currentColor; }

button.danger, .btn.danger {
  color: var(--down);
  border-color: var(--down-line);
  background: var(--surface);
}
button.danger:hover:not(:disabled) { background: var(--down-wash); border-color: var(--down); color: var(--down); }
button.danger .ico { color: currentColor; }

/* Quiet: the row-level actions, where a full button per row would make the
   table read as a wall of chrome instead of as data. */
button.quiet, .btn.quiet {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--text-2);
  min-height: var(--hit);
}
button.quiet:hover:not(:disabled), .btn.quiet:hover {
  background: var(--surface-hover);
  border-color: var(--line);
  color: var(--text);
  box-shadow: none;
}

button.icon-only, .btn.icon-only { padding: 0; width: var(--hit-lg); }
button.small, .btn.small { min-height: var(--hit); font-size: var(--text-sm); padding: 0 var(--space-2); }

button:disabled, .btn[aria-disabled='true'] {
  opacity: 0.42;
  cursor: not-allowed;
  box-shadow: none;
  translate: none;
}

/*
 * Busy. The label stays — a button that swaps its text for a spinner has taken
 * away the one word describing what is running — and the spinner joins it.
 */
button[data-busy='true'] { cursor: progress; pointer-events: none; opacity: 0.75; }
button[data-busy='true']::before {
  content: '';
  width: 0.875em;
  height: 0.875em;
  flex: none;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: spin 640ms linear infinite;
}
@keyframes spin { to { rotate: 360deg; } }

.btn-group { display: inline-flex; gap: var(--space-1); align-items: center; }

/* ==================================================================== *
 * Controls
 * ==================================================================== */

input, select, textarea {
  min-height: var(--hit-lg);
  background: var(--sunken);
  color: var(--text);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  width: 100%;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 1; }
input:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled) { border-color: var(--text-3); }
input:focus, select:focus, textarea:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
}
input:disabled, select:disabled, textarea:disabled { opacity: 0.5; cursor: not-allowed; }
input[aria-invalid='true'],
select[aria-invalid='true'],
textarea[aria-invalid='true'] { border-color: var(--down); }
input[aria-invalid='true']:focus { box-shadow: 0 0 0 3px var(--down-wash); }

input[type='checkbox'], input[type='radio'] {
  min-height: 0;
  width: 1rem;
  height: 1rem;
  flex: none;
  padding: 0;
}
/* Global `input { width:100%; padding }` collapses the native colour
 * swatch to a hairline. The visible face is `.radar-swatch-face`; the
 * native control sits on top as an invisible hit target. */
input[type='color'] {
  -webkit-appearance: none;
  appearance: none;
  width: 2.75rem;
  min-width: 2.75rem;
  max-width: 2.75rem;
  height: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  flex: none;
  cursor: pointer;
  background: transparent;
  border: none;
}
input[type='color']::-webkit-color-swatch-wrapper { padding: 0; }
input[type='color']::-webkit-color-swatch {
  border: none;
  border-radius: calc(var(--radius) - 1px);
}
input[type='color']::-moz-color-swatch {
  border: none;
  border-radius: calc(var(--radius) - 1px);
}
.radar-swatch-wrap {
  position: relative;
  display: inline-block;
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  overflow: hidden;
}
.radar-swatch-wrap input[type='color'] {
  position: absolute;
  inset: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  min-width: 0;
  min-height: 0;
  opacity: 0;
  cursor: pointer;
}
.radar-swatch-face {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
select {
  /* The native arrow is drawn by the platform in the platform's colour, which
     on a dark console is a black triangle in a dark well. */
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378829a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-2) center;
  background-size: 1rem;
  padding-inline-end: var(--space-7);
}
[dir='rtl'] select { background-position: left var(--space-2) center; }
select[multiple] { background-image: none; padding-inline-end: var(--space-3); min-height: 0; }

/*
 * A text box that has suggestions behind it, and says so.
 *
 * A bare `<datalist>` is invisible: Chrome and Firefox draw no affordance at
 * all on a text input, so the list opens only for an operator who already
 * guessed it was there — which is nobody, and the field may as well not have
 * had one. It gets the same chevron the select gets, because it behaves like
 * one; the difference is that this one can also be typed into, which is the
 * whole reason these fields are not enums.
 *
 * The rule is attribute-driven rather than a class, so it covers every
 * `suggest` field without `renderField` having to remember to mark them.
 */
input[list] {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378829a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-2) center;
  background-size: 1rem;
  padding-inline-end: var(--space-7);
}
[dir='rtl'] input[list] { background-position: left var(--space-2) center; }

/*
 * The spinner and the chevron cannot both have that corner, and between the
 * two the chevron is worth more: these are ports and OIDs, and nobody arrives
 * at 993 by stepping up from 992.
 */
input[list][type='number'] { appearance: textfield; }
input[list][type='number']::-webkit-outer-spin-button,
input[list][type='number']::-webkit-inner-spin-button { appearance: none; margin: 0; }

/*
 * `textarea` names its own family and keeps it. Everything typed into one on
 * these screens is machine-readable — a YAML document, a JSON body, a list of
 * conditions — and none of it should be set in the interface face.
 */
textarea {
  min-height: 6rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  line-height: 1.65;
  resize: vertical;
}

/*
 * Touch. The dense sizes above are for a mouse on a wall-mounted ops screen.
 * On a coarse pointer — the phone somebody is actually holding when this pages
 * them at 3am — every control goes to the 44px floor and the rows get air.
 */
@media (pointer: coarse) {
  /*
   * 2.75rem, not 2.5rem. This block has always said "the 44px floor" and then
   * set --hit to 40px, so every control sized by it -- .btn, button.small, the
   * segmented filters, the row links, the checkbox rows below -- landed four
   * pixels short of the floor this section exists to enforce. --hit-lg was
   * already 2.75rem, so the two now meet rather than sitting a step apart.
   */
  :root { --hit: 2.75rem; --hit-lg: 2.75rem; }
  .nav-link { min-height: 2.75rem; }

  /*
   * iOS Safari zooms the viewport when a control whose text is under 16px
   * takes focus, and does not zoom back out. The body here is 13px on purpose
   * (see body, above) and controls inherit it, so every text field, select and
   * textarea in the product -- the sign-in form included -- pushed the layout
   * to roughly 1.2x on the first tap and left the operator panning sideways to
   * reach the button they were aiming for.
   *
   * 16px on touch only. The dense desktop sizing above is untouched, and this
   * is the one place where the platform, not the design, sets the number.
   */
  input, select, textarea { font-size: 1rem; }

  /*
   * The segmented filters -- status on the monitor and incident lists, the
   * range picker on a monitor, Form/YAML on config -- subtract 4px to sit
   * inside their container's 2px padding, which put them under the floor
   * again. The container grows instead.
   */
  .segmented button { min-height: var(--hit); }

  /*
   * A 13x16 tickbox is not a touch target, and the pointer that lands on it is
   * a thumb. 24px is the box itself; the label beside it is 44px and carries
   * the same click through its `for`, so the pair clears the floor twice over.
   * Growing the box rather than only the row, because the row is the label's
   * job and several of these -- the status page component list, the template
   * include list -- have no label row to grow.
   */
  input[type='checkbox'], input[type='radio'] { width: 1.5rem; height: 1.5rem; }

  /*
   * The search trigger drops its 13rem min-width below the breakpoint so it can
   * shrink to its icon; with nothing holding it open it shrank to 37px, which
   * is narrower than it is tall.
   */
  .search-trigger { min-width: var(--hit); justify-content: center; }

  /*
   * The floor is square, not just tall. These all reached 44px in height from
   * --hit above and stayed 37-43px wide: a range button reading "1h", the
   * avatar, "Pin" on a rollout, the skip link.
   */
  button, .btn { min-width: var(--hit); }
  .btn.icon-only, button.icon-only { min-width: var(--hit-lg); }
  .avatar-button { min-width: var(--hit); }
  .skip-link { min-height: var(--hit); display: flex; align-items: center; }

  /*
   * A toolbar's links are controls, not prose: the status page list puts the
   * page's own name in one and its public URL in the next, and they were 17px
   * and 21px tall.
   */
  .toolbar a { display: inline-flex; align-items: center; min-height: var(--hit); }

  /*
   * A <summary> is a control -- "Raw configuration" on a monitor, "Schedule,
   * agreement and placement" on the new-monitor form -- and it is the only way
   * to reach what it hides.
   */
  summary { min-height: var(--hit); display: flex; align-items: center; }

  /*
   * The two links that sit on their own outside the shell -- "Already have an
   * account? Sign in" under the sign-up form, "I have the code" after an SMS --
   * are the whole next step of the screen they are on, and they were 17px.
   */
  #shell[data-chrome='bare'] .card > a,
  #shell[data-chrome='bare'] .card > p > a { display: inline-flex; align-items: center; min-height: var(--hit); }

  /* The mark in the drawer's header is the link home, and it was 26px. */
  .rail-brand a { min-height: var(--hit); }

  /*
   * Two controls the floor above does not reach, because neither is sized by
   * --hit.
   *
   * A checkbox is 16px square and its label is a sibling rather than a
   * wrapper, so the pair was a 21px-tall target on the device this section
   * exists for. The box keeps its size -- a tickbox that grows on touch reads
   * as a different control -- and the row it sits in takes the height
   * instead, which the label's `for` attribute already makes clickable.
   */
  .field.inline { min-height: var(--hit); }
  .field.inline label { display: flex; align-items: center; min-height: var(--hit); }

  /*
   * A table row is 49px tall and the link inside it was 21px, so two thirds
   * of the row a thumb naturally lands on did nothing. The cell is already
   * the right size; the link simply was not filling it.
   */
  td .row-link,
  /* Not every link in a table has the class: the person list renders a bare
     anchor for an email address, and it was 17px tall. */
  td a { display: flex; align-items: center; min-height: var(--hit); }

  /*
   * `.field.inline label` above covers the form layout; this covers the other
   * one, a `label.inline` that wraps its own checkbox. Same 16px box, same
   * 21px target, different markup -- so it needs saying twice rather than
   * being missed once.
   */
  label.inline { display: flex; align-items: center; min-height: var(--hit); }
}

/* ==================================================================== *
 * Forms
 * ==================================================================== */

.field { margin-bottom: var(--space-4); max-width: 34rem; }
.field > label,
.form-label {
  display: block;
  margin-bottom: var(--space-2);
  font-weight: var(--fw-label);
  font-size: var(--text-base);
  color: var(--text);
}
.field .help {
  font-size: var(--text-sm);
  color: var(--text-2);
  margin-top: var(--space-2);
  max-width: 60ch;
}
/*
 * Not scoped to `.field`. The same class is used for a message that has no
 * field to sit under — a validation problem about a whole entity, a report line
 * on the configuration screen — and scoped, those rendered as ordinary body
 * text: the one paragraph on the page that is a failure, styled as if it were
 * not.
 */
.error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--down);
  margin-top: var(--space-2);
  animation: nudge var(--t-slow) var(--ease);
}
/*
 * A short nudge on the message, once. Motion here says "this is new" on a page
 * that otherwise redraws silently — it is not decoration, and it does not loop.
 */
@keyframes nudge {
  0%, 100% { translate: 0 0; }
  30% { translate: -3px 0; }
  70% { translate: 3px 0; }
}
.field.inline { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/*
 * Not the tickboxes. This rule exists so a text input in an inline field stops
 * filling the row, but it has equal specificity to the input[type='checkbox']
 * rule above and comes later, so it also overrode that rule's width and left
 * every checkbox at the UA's intrinsic 13px -- a box narrower than it is tall.
 */
.field.inline input:not([type='checkbox']):not([type='radio']) { width: auto; }
.field.inline label { margin-bottom: 0; font-weight: var(--fw-label); }
/*
 * The help sentence belongs under the checkbox, not beside it. As a third flex
 * item on the same row it ran alongside the label and turned a one-word control
 * into a paragraph with a tickbox in front of it.
 */
/* `flex-shrink: 0`, or it simply shrinks to fit on the same row and never wraps. */
.field.inline .help,
.field.inline .error { flex: 1 0 100%; margin-top: 0; }

/* Stacked label + control, which is what every generated form here is. */
.card label,
form > label {
  display: block;
  margin-bottom: var(--space-4);
  color: var(--text);
  font-size: var(--text-base);
  font-weight: var(--fw-label);
  max-width: 34rem;
}
.card label input,
.card label select,
.card label textarea,
form > label input,
form > label select,
form > label textarea {
  display: block;
  margin-top: var(--space-2);
  font-weight: var(--fw-body);
}
label.inline { display: flex; align-items: center; gap: var(--space-3); font-weight: var(--fw-label); }
/* Not the tickboxes -- see the note on .field.inline above; this is the same
   override, on the other of the two checkbox layouts. */
label.inline input:not([type='checkbox']):not([type='radio']) { display: inline-block; width: auto; margin: 0; }
label.inline input[type='checkbox'], label.inline input[type='radio'] { margin: 0; }

fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin: 0 0 var(--space-4);
  background: var(--surface);
}
legend {
  color: var(--text);
  font-weight: var(--fw-title);
  font-size: var(--text-base);
  padding: 0 var(--space-2);
}
/* A long checkbox list inside a fieldset reads as a column, not a stack. */
fieldset .check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--space-1) var(--space-4);
  margin-bottom: var(--space-3);
}
fieldset .check-grid .field.inline { margin-bottom: 0; padding: var(--space-1) 0; }
fieldset.radar-bands .radar-swatch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  font-weight: var(--fw-label);
  min-height: var(--hit);
  cursor: pointer;
}

/* ==================================================================== *
 * The condition builder
 *
 * A row is a sentence being assembled — subject, comparison, value — so the
 * controls sit on one line and wrap as a line of text does, rather than
 * stacking into a column of labelled boxes that reads as a form about nothing.
 * Each part keeps its own small label above it: the row is too wide for a
 * placeholder to survive being typed over.
 * ==================================================================== */

/* --sunken, not --raised: --raised is the same hex as --surface in light mode,
   so the deepest-nested thing in the product had no containment signal at all
   for a daytime operator. */
fieldset.cond { padding: var(--space-4); margin-bottom: var(--space-3); background: var(--sunken); border-radius: var(--radius); }
fieldset.cond > legend { font-size: var(--text-sm); color: var(--text-2); font-weight: var(--fw-label); }

.cond-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
}
.cond-part { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; flex: 0 1 auto; }
.cond-part > .cond-label {
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  color: var(--text-3);
  margin: 0;
}
/*
 * The floor is for the boxes somebody types into. A checkbox given an 8rem
 * minimum is a tickbox with four inches of nothing after it, and it pushed the
 * rest of the row onto a second line.
 */
.cond-part > input:not([type='checkbox']),
.cond-part > select,
.cond-part > textarea { margin: 0; min-width: 8rem; }
.cond-part > textarea { min-width: min(100%, 34rem); width: 100%; }
.cond-part.cond-check { flex-direction: row; align-items: center; gap: var(--space-2); padding-bottom: var(--space-2); }
.cond-part.cond-check input { width: auto; min-width: 0; flex: none; margin: 0; }
.cond-part.cond-check label { font-size: var(--text-sm); font-weight: var(--fw-label); color: var(--text-2); margin: 0; }
.cond .num-input { max-width: 10rem; }

.cond-guide {
  font-size: var(--text-sm);
  color: var(--text-2);
  max-width: 68ch;
  margin: var(--space-3) 0 0;
}

.cond-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
}
.cond-preview-label {
  font-size: var(--text-sm);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  flex: none;
}
/*
 * The expression, exactly as it will be saved. It is the answer to the only
 * question the builder cannot answer by itself — "what did I just write" — so
 * it is never truncated and never hidden behind a disclosure.
 */
.cond-preview {
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  color: var(--text);
  overflow-wrap: anywhere;
  min-width: 0;
  flex: 1 1 12rem;
}
.cond-preview.is-empty { color: var(--text-3); font-style: italic; }
.cond-actions { display: flex; gap: var(--space-2); flex: none; margin-inline-start: auto; }

.cond-none {
  color: var(--text-2);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-3);
  max-width: 68ch;
}

/* ==================================================================== *
 * The configuration screen
 *
 * Forty monitors on one page, so each is a closed row until it is opened. The
 * summary carries what tells them apart — name, key, checker — and nothing
 * else, because a summary that carries everything is a card that is never
 * closed.
 * ==================================================================== */

.cfg-list { margin-bottom: var(--space-4); }

/* A row, not a panel: the smaller radius is what says so. */
details.cfg-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: var(--space-2);
}
details.cfg-card[open] { border-color: var(--edge); }

summary.cfg-summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-lg);
  min-height: var(--hit-lg);
}
summary.cfg-summary::-webkit-details-marker { display: none; }
summary.cfg-summary:hover { background: var(--surface-hover); }
summary.cfg-summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.cfg-chevron { color: var(--text-3); flex: none; transition: rotate var(--t-fast) var(--ease); }
details.cfg-card[open] .cfg-chevron { rotate: 90deg; }
[dir='rtl'] .cfg-chevron { scale: -1 1; }
[dir='rtl'] details.cfg-card[open] .cfg-chevron { rotate: -90deg; }

.cfg-name { font-weight: var(--fw-label); color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.cfg-key {
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  color: var(--text-3);
  min-width: 0;
  overflow-wrap: anywhere;
}
summary.cfg-summary .pill { margin-inline-start: auto; flex: none; }
/* Shown only when the thing is actually off, so its absence is the good case. */
.cfg-paused { display: none; }
details.cfg-card.is-paused .cfg-paused {
  display: inline-block;
  flex: none;
  font-size: var(--text-sm);
  color: var(--degraded);
  border: 1px solid var(--degraded-line);
  background: var(--degraded-wash);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
}

.cfg-body { padding: 0 var(--space-4) var(--space-4); border-top: 1px solid var(--line); padding-top: var(--space-4); }
.cfg-body > fieldset { background: transparent; }
.cfg-note { font-size: var(--text-sm); color: var(--text-2); max-width: 68ch; margin: 0 0 var(--space-4); }
.cfg-problems:not(:empty) { margin-bottom: var(--space-4); }

.cfg-attachment { background: var(--sunken); border-radius: var(--radius); padding: var(--space-4); }
.cfg-attachment-flags {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
  gap: 0 var(--space-4);
}
.cfg-attachment-flags .field { margin-bottom: var(--space-3); }

.cfg-group { display: flex; align-items: flex-start; gap: var(--space-3); }
.cfg-group .field { flex: 1 1 20rem; margin-bottom: var(--space-3); }

/* ==================================================================== *
 * Surfaces
 * ==================================================================== */

/*
 * The readout plane: what the operator reads, as opposed to the apparatus that
 * frames it or the strip that filters it. --edge rather than --line, because
 * this is a container boundary and not a rule inside one.
 *
 * No box-shadow. It was --shadow-1, which composites to 1.023:1 on a near-black
 * ground — so the box had two nominal separation channels and zero working
 * ones. The shadow is kept where it is a real affordance (buttons, the pressed
 * segment, the brand mark) and dropped where it renders nothing.
 */
.panel, .card {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin-bottom: var(--stack);
}
.card.narrow { max-width: 26rem; }
.panel > :last-child, .card > :last-child { margin-bottom: 0; }

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--stack-tight);
}
.panel-head h3 { margin: 0; }
/* A panel head that carries a control rather than only words. Baseline
   alignment hangs a 36px segmented control off the title's baseline and drops
   the title to the bottom of the row. */
.panel-head:has(.segmented) { align-items: center; }
/* The 14px rung, shared with h3 — a card title is one size either way. It was
   body size, which left a titled panel with no title. */
.panel-title {
  font-size: var(--text-md);
  font-weight: var(--fw-title);
  color: var(--text);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/*
 * The banner. A one-pixel inline-start rule, not a four-pixel bar: the wash
 * and the icon already carry the severity, and a thick coloured edge is the
 * decoration every alert component reaches for by default.
 */
.banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  border: 1px solid var(--down-line);
  background: var(--down-wash);
  color: var(--text);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--stack);
  border-radius: var(--radius);
  animation: banner-in var(--t-slow) var(--ease-out);
}
.banner .ico { color: var(--down); margin-top: 0.15em; flex: none; }
.banner .banner-body { min-width: 0; flex: 1; }
.banner .banner-body > :last-child { margin-bottom: 0; }
.banner.ok { border-color: var(--up-line); background: var(--up-wash); }
.banner.ok .ico { color: var(--up); }
.banner.warn { border-color: var(--degraded-line); background: var(--degraded-wash); }
.banner.warn .ico { color: var(--degraded); }
.banner.info { border-color: var(--accent-line); background: var(--accent-wash); }
.banner.info .ico { color: var(--accent); }
@keyframes banner-in {
  from { opacity: 0; translate: 0 -0.375rem; }
  to { opacity: 1; translate: 0; }
}

dialog {
  background: var(--raised);
  color: var(--text);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
  max-width: 30rem;
  width: calc(100vw - var(--space-6) * 2);
  box-shadow: var(--shadow-3);
}
dialog::backdrop {
  background: rgb(0 0 0 / 0.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}
dialog menu { display: flex; gap: var(--space-2); justify-content: flex-end; padding: 0; margin: var(--space-5) 0 0; }
dialog h2 { font-size: var(--text-lg); }

/* ==================================================================== *
 * Tables
 * ==================================================================== */

/*
 * Applied by show() rather than by every call site. A table wider than the
 * viewport has to scroll inside its own box; letting it scroll the body
 * sideways takes the rail and the topbar with it.
 *
 * The box is a tab stop with a name, because a region that scrolls and holds
 * no focusable child — the probe evidence table, for one — is otherwise
 * reachable by pointer only.
 */
.table-scroll {
  overflow-x: auto;
  width: 100%;
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface);
  margin-bottom: var(--stack);
}
.table-scroll:focus-visible { outline-offset: -2px; }

/*
 * Local paging under a table. The scroll box already claimed the stack
 * margin; the pager sits against it rather than adding a second gap, and
 * the wrapper carries the space the table used to.
 */
.paged { margin-bottom: var(--stack); }
.paged > .table-scroll { margin-bottom: 0; }
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-2);
}
.pager-status {
  margin: 0;
  flex: 1;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

table { width: 100%; border-collapse: collapse; }
th, td {
  text-align: start;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
td { white-space: nowrap; color: var(--text-2); }
td:first-child, th:first-child { padding-inline-start: var(--space-5); }
td:last-child, th:last-child { padding-inline-end: var(--space-5); }
/* The one column allowed to wrap: free-text detail, which is often a sentence. */
td.wrap { white-space: normal; min-width: 18rem; max-width: 34rem; color: var(--text-2); }
td.num { font-variant-numeric: tabular-nums; }
td.strong { color: var(--text); font-weight: var(--fw-label); }
/* An actions cell hugs the end of the row rather than claiming a share of it. */
td.actions { width: 1%; }
td.actions > * { vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }

/* --fw-label, not 650. The section heading above it is now 16px/700 and the
   column header has to sit clearly under it rather than beside it. */
thead th {
  color: var(--text-3);
  font-weight: var(--fw-label);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  background: var(--sunken);
  white-space: nowrap;
  border-bottom-color: var(--line-strong);
}

/*
 * Sticky headers, but only where they can actually work.
 *
 * `.table-scroll` sets overflow-x, which makes the browser compute overflow-y
 * as auto too — so it becomes a scroll container whose height is its content,
 * and a sticky <th> inside it has nothing to stick against. The evidence table
 * has a real max-height and scrolls vertically, so there the header stays put
 * while an operator reads down forty probe results.
 */
.evidence thead th, .scroll-y thead th { position: sticky; top: 0; z-index: 1; }

tbody tr { transition: background var(--t-instant) var(--ease); }
tbody tr:hover { background: var(--surface-hover); }

/*
 * A row whose verdict is bad. Tinted rather than badged, because the point of
 * the tint is to be findable in peripheral vision on a list of two hundred —
 * and the pill in the first cell is still the thing that says which verdict.
 */
/*
 * The amplitudes were 5% and 4%, which measured 1.06:1 — weaker than the plain
 * hover. In light mode a pointer resting on a healthy row was more visible than
 * a monitor that was down: 1.100:1 against 1.081:1, i.e. 1.003:1 apart. The one
 * peripheral-vision signal in a two-hundred-row table was null.
 *
 * Mixed at the call site rather than by raising --down-wash, because that token
 * also fills the status pill, where --down on its own wash is already at 4.99:1
 * and has no headroom.
 */
tr.row-down { background: color-mix(in srgb, var(--down) 16%, transparent); }
tr.row-down:hover { background: color-mix(in srgb, var(--down) 22%, transparent); }
tr.row-degraded { background: color-mix(in srgb, var(--degraded) 8%, transparent); }
tr.row-degraded:hover { background: color-mix(in srgb, var(--degraded) 13%, transparent); }

/*
 * Which of the two bad verdicts, as a categorical channel.
 *
 * Two washes this close cannot be told apart by amplitude alone, however far
 * they are from the plain row. The marker goes on the cell and not the <tr>,
 * because box-shadow on a row is unreliable under `border-collapse: collapse`.
 * `inset` offsets are physical, so RTL needs its own sign.
 */
tr.row-down > td:first-child { box-shadow: inset 3px 0 0 var(--down); }
tr.row-degraded > td:first-child { box-shadow: inset 3px 0 0 var(--degraded); }
[dir='rtl'] tr.row-down > td:first-child { box-shadow: inset -3px 0 0 var(--down); }
[dir='rtl'] tr.row-degraded > td:first-child { box-shadow: inset -3px 0 0 var(--degraded); }

/*
 * Row navigation is an anchor in the first cell, not a click handler on the
 * <tr>. A handler on the row is invisible to the keyboard, cannot be opened in
 * a new tab, and gives a screen reader nothing to announce. The row click stays
 * as a convenience for the mouse; the anchor is what makes it real.
 */
tr.clickable { cursor: pointer; }
.row-link {
  color: var(--text);
  text-decoration: none;
  font-weight: var(--fw-label);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.row-link:hover { color: var(--accent); }
.row-link::after {
  content: '';
  width: 0.85em;
  height: 0.85em;
  background: currentColor;
  opacity: 0;
  translate: -0.25rem 0;
  transition: opacity var(--t-fast) var(--ease), translate var(--t-fast) var(--ease);
  /* A chevron drawn as a mask, so it needs no second <svg> per row. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E") center / contain no-repeat;
}
[dir='rtl'] .row-link::after { scale: -1 1; translate: 0.25rem 0; }
tr:hover .row-link::after,
.row-link:focus-visible::after { opacity: 0.55; translate: 0 0; }
[dir='rtl'] tr:hover .row-link::after { translate: 0 0; }

/* The cell that holds a name plus its tags, which must not be one nowrap run. */
.cell-stack { display: flex; flex-direction: column; gap: var(--space-1); }
.cell-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
/*
 * Inside a table it does not wrap. The table already scrolls sideways in its
 * own box, and a cell that wraps instead makes one row twice the height of its
 * neighbours — which in a list read by scanning is a row that looks special
 * for no reason.
 */
td .cell-row { flex-wrap: nowrap; }

/* Probe evidence: dense, monospaced, scrolls on its own. */
.evidence, .scroll-y {
  max-height: 26rem;
  overflow: auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background: var(--surface);
  margin-bottom: var(--stack);
}
.evidence table { font-size: var(--text-sm); }
.evidence td {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
}

/* ==================================================================== *
 * The overview
 *
 * The one screen whose job is a verdict rather than a list. Its centre is a
 * proportional bar, not four big numbers in four boxes: "eleven up, two down"
 * as two figures is arithmetic, and the same fact as one measured band is a
 * glance. The failing monitors are listed directly under it because the next
 * thing the operator does is open one of them.
 * ==================================================================== */

/*
 * Two columns, not auto-fit cells.
 *
 * `repeat(auto-fit, minmax(19rem, 1fr))` put the wide panel across two tracks
 * and then left a third of the second row empty — a ragged hole that reads as
 * a layout bug rather than as a decision. The main column is what needs a
 * person; the side column is context, stacked in the order it is read.
 *
 * `minmax(0, …)` on both tracks rather than `1.6fr 1fr`: a grid item's default
 * `min-width: auto` refuses to shrink below its content, so one long monitor
 * name in the attention list pushed the whole page sideways.
 */
.overview-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5);
  margin-bottom: var(--stack-loose);
  /*
   * Stretch, not start. The two panels in a row are read as a pair, and a pair
   * whose boxes end at different heights reads as one of them having failed to
   * load rather than as one of them having less to say.
   */
  align-items: stretch;
}
/*
 * `minmax(0, …)` rather than `1fr`: a grid item's default `min-width: auto`
 * refuses to shrink below its content, so one long monitor name in the
 * attention list pushed the whole page sideways at every width.
 */
.overview-grid > * { min-width: 0; }
/* A grid item's margin is neither collapsed nor absorbed by `gap`, so the
   panels' own margin-bottom made the row gutter 32px against a 20px column
   gutter on the one screen in the product with two-dimensional structure. */
.overview-grid > .panel, .overview-grid > .card { margin-bottom: 0; }
@media (max-width: 64rem) {
  .overview-grid { grid-template-columns: minmax(0, 1fr); }
}

/*
 * Compact strip of live monitors under the health bar. Keeps an all-green
 * overview from reading as four empty cards with nothing to do.
 */
.monitor-strip-panel { margin-bottom: var(--stack-loose); }
.monitor-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--space-3);
}
.monitor-chip {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2, var(--card, transparent));
  text-decoration: none;
  color: inherit;
  min-width: 0;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.monitor-chip:hover {
  border-color: var(--accent, #6366f1);
}
.monitor-chip-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.monitor-chip-meta {
  font-size: 0.8125rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cap-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  max-width: 18rem;
}
.monitor-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.monitor-picker > input[type='search'] {
  max-width: 24rem;
}
.monitor-picker-list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-height: 16rem;
  overflow: auto;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
#monitor-type-filter {
  margin-bottom: var(--space-2);
}

/*
 * The headline verdict. One sentence saying what is true right now, sized to
 * be read from a metre away, tinted by the worst thing in it.
 */
.verdict {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  border-radius: var(--radius-xl);
  border: 1px solid var(--verdict-line, var(--line));
  background: var(--verdict-wash, var(--surface));
  margin-bottom: var(--space-4);
}
.verdict.is-up { --verdict-line: var(--up-line); --verdict-wash: var(--up-wash); --verdict-ink: var(--up); }
.verdict.is-down { --verdict-line: var(--down-line); --verdict-wash: var(--down-wash); --verdict-ink: var(--down); }
.verdict.is-degraded { --verdict-line: var(--degraded-line); --verdict-wash: var(--degraded-wash); --verdict-ink: var(--degraded); }
.verdict.is-unknown { --verdict-line: var(--unknown-line); --verdict-wash: var(--unknown-wash); --verdict-ink: var(--unknown); }
.verdict-mark {
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: var(--verdict-ink, var(--text-2));
  background: var(--surface);
  border: 1px solid var(--verdict-line, var(--line));
}
.verdict-mark .ico { width: 1.375rem; height: 1.375rem; }
.verdict-text { min-width: 0; }
.verdict-headline {
  font-size: var(--text-xl);
  font-weight: var(--fw-title);
  letter-spacing: -0.015em;
  color: var(--text);
  line-height: 1.3;
}
.verdict-sub { color: var(--text-2); margin-top: var(--space-1); }

/*
 * A version report. One group per distinct build, members underneath — the
 * reading is "what is running", not a second copy of the fleet table.
 */
.version-report {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.vrep-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.vrep-head code {
  font-size: var(--text-md);
  color: var(--text);
}
.vrep-head .n {
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.vrep-members {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--sunken);
}
.vrep-members > li {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-sm);
}
.vrep-members > li:last-child { border-bottom: none; }
.vrep-name { color: var(--text); font-weight: var(--fw-label); }

/*
 * The health bar. Segments are proportional to the counts, and each one
 * carries its own label — the bar is the picture and the legend under it is
 * the reading, so nothing here depends on telling four hues apart.
 */
.health-bar {
  display: flex;
  height: 0.625rem;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--sunken);
  border: 1px solid var(--line);
  margin-bottom: var(--space-4);
}
.health-seg { transition: flex-grow var(--t-slow) var(--ease); min-width: 2px; }
.health-seg.is-up { background: var(--up); }
.health-seg.is-down { background: var(--down); }
.health-seg.is-degraded { background: var(--degraded); }
.health-seg.is-unknown { background: var(--unknown); }
/*
 * Hatched, not tinted. Paused is not a fifth verdict — it is a monitor that
 * was taken out of the count — and a flat grey segment beside four coloured
 * ones reads as "some other status" rather than as "excluded".
 */
.health-seg.is-paused {
  background:
    repeating-linear-gradient(45deg, var(--line-strong) 0 3px, transparent 3px 6px),
    var(--surface-hover);
}

.health-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  list-style: none;
  margin: 0;
  padding: 0;
}
.health-legend li { display: flex; align-items: baseline; gap: var(--space-2); }
.health-legend .swatch {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  flex: none;
  translate: 0 -0.1em;
}
.health-legend .n {
  font-size: var(--text-lg);
  font-weight: var(--fw-title);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  letter-spacing: -0.02em;
}
.health-legend .k { color: var(--text-2); font-size: var(--text-base); }
.health-legend .is-up .swatch { background: var(--up); }
.health-legend .is-down .swatch { background: var(--down); }
.health-legend .is-degraded .swatch { background: var(--degraded); }
.health-legend .is-unknown .swatch { background: var(--unknown); }
.health-legend .is-paused .swatch {
  background:
    repeating-linear-gradient(45deg, var(--edge) 0 2px, transparent 2px 4px),
    var(--surface-hover);
}

/*
 * A single figure with its label. Used for the fleet counts, never as the page
 * structure: four of these in a row IS the hero-metric template, and it says
 * less than one sentence would.
 */
/*
 * Given a box of its own. It had display/flex-direction/gap and nothing else,
 * so four value/label pairs 16px apart read as one run of text rather than as
 * four measurements. The inline-start rule is what makes them count.
 */
.metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding-inline: var(--space-3);
  border-inline-start: 1px solid var(--line);
}
.metric:first-child { border-inline-start: none; padding-inline-start: 0; }
/* One rung down from the page title: at --text-2xl it was the same size and
   weight as the h1, so a detail page had two competing top-level elements. */
.metric .metric-value {
  font-size: var(--text-xl);
  font-weight: var(--fw-title);
  line-height: 1.1;
  letter-spacing: -0.025em;
  color: var(--text);
}
.metric .metric-label {
  font-size: var(--text-sm);
  color: var(--text-2);
}
.metric.is-down .metric-value { color: var(--down); }
.metric.is-degraded .metric-value { color: var(--degraded); }
.metric.is-up .metric-value { color: var(--up); }
.metric-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
  gap: var(--space-4);
}

/*
 * The attention list. Not a table: each entry is one thing that is wrong, with
 * the name, why, and how long — and the whole row is the link to it.
 */
.attention {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Explicit, so the track cannot size itself to the longest name. */
  grid-template-columns: minmax(0, 1fr);
  /*
   * Was `gap: 1px`, which exposes one pixel of the panel's own fill — i.e. no
   * separator at all. This is the panel the whole overview exists to deliver
   * and it was the least differentiated list in the product.
   */
  gap: 0;
}
.attention li { position: relative; min-width: 0; }
.attention li + li { border-top: 1px solid var(--line); }
.attention a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  /* Out to the panel's padding edge, so the hover reads as a row rather than as
     a floating chip. margin-inline flips in RTL for free. */
  margin-inline: calc(-1 * var(--space-2));
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
  min-width: 0;
  transition: background var(--t-fast) var(--ease);
}
.attention a:hover { background: var(--surface-hover); }
.attention .att-name { font-weight: var(--fw-label); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attention .att-why { color: var(--text-2); font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attention .att-main { min-width: 0; flex: 1; }
.attention .att-when { color: var(--text-3); font-size: var(--text-sm); flex: none; white-space: nowrap; }

/* ==================================================================== *
 * Key / value
 *
 * What a monitor's configuration is rendered as, instead of a block of JSON.
 * The JSON is still one click away — it is what the operator pastes into a
 * ticket — but it is not what they read to answer "which URL is this hitting".
 * ==================================================================== */
.kv {
  display: grid;
  grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr);
  gap: 0;
  /* It was `margin: 0` — the only bordered surface in the file with no bottom
     gap, which is why the raw-config disclosure abutted it at 0px. */
  margin: 0 0 var(--stack);
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
}
.kv > dt {
  padding: var(--space-3) var(--space-4);
  color: var(--text-3);
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  border-bottom: 1px solid var(--line);
  background: var(--sunken);
  white-space: nowrap;
}
.kv > dd {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}
.kv > dt:last-of-type, .kv > dd:last-of-type { border-bottom: none; }
.kv .kv-mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
}

/*
 * A disclosure for the raw form of something rendered above it. `<details>`
 * rather than a tab, because the raw JSON is the exception and a tab strip
 * would give it equal billing with the reading everyone actually wants.
 */
details.raw { margin-bottom: var(--stack); }
/* Given a control's box. It is 12px like a .kv > dt and was abutting one, so
   the disclosure read as one more key rather than as something to press. */
details.raw > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  color: var(--text-2);
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  padding: var(--space-2) var(--space-3);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  list-style: none;
  user-select: none;
}
details.raw > summary::-webkit-details-marker { display: none; }
details.raw > summary:hover { background: var(--surface-hover); color: var(--text); }
details.raw > summary .ico { transition: rotate var(--t-fast) var(--ease); }
details.raw[open] > summary .ico { rotate: 90deg; }
[dir='rtl'] details.raw > summary .ico { scale: -1 1; }
[dir='rtl'] details.raw[open] > summary .ico { rotate: -90deg; }
details.raw > pre { margin-top: var(--space-2); }

/*
 * The advanced half of the monitor form, behind the same disclosure.
 *
 * `raw` for the summary, because it is the same gesture — the thing under it is
 * the exception, and a tab strip would give eleven fields nobody usually
 * touches equal billing with the three that name the monitor. Its body is a
 * stack of fieldsets rather than a <pre>, so it needs the gap `raw` gives its
 * code block and none of the rest.
 */
details.advanced > .adv-body { margin-top: var(--space-4); }
details.advanced > .adv-body > .section-note { margin-top: 0; }
/* The last fieldset in a settings stack sits on the toolbar otherwise. */
.monitor-settings > fieldset:last-child { margin-bottom: 0; }

/*
 * What the probe selector above it actually reaches.
 *
 * Below the fields rather than beside them, and given a rule, because it is a
 * consequence of all three and not a note about any one of them. `is-empty` is
 * the case worth colouring: a selector matching nothing saves without complaint
 * and then never runs, and the hub's only report of that is a log line.
 */
.placement-reach {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
}
.placement-reach.is-empty { color: var(--degraded); }

.probe-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.probe-pick {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--sunken);
  cursor: pointer;
}
.probe-pick:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.probe-pick.is-off { cursor: not-allowed; }
.probe-pick input { margin-top: 0.2em; flex: none; }
.probe-pick-copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.probe-pick-copy .help { margin: 0; }

/* ==================================================================== *
 * Timeline
 *
 * The rail and the node are drawn in CSS rather than added to the markup, so a
 * timeline entry stays one <li> with a date and a sentence in it.
 * ==================================================================== */
.timeline {
  list-style: none;
  margin: 0 0 var(--stack);
  padding: 0;
  padding-inline-start: var(--space-6);
  position: relative;
}
.timeline::before {
  content: '';
  position: absolute;
  inset-inline-start: 4px;
  top: 0.75rem;
  bottom: 0.75rem;
  width: 1px;
  background: var(--line-strong);
}
.timeline li { padding: var(--space-3) 0; position: relative; }
.timeline li + li { border-top: 1px solid var(--line); }
.timeline li::before {
  content: '';
  position: absolute;
  inset-inline-start: calc(-1 * var(--space-6));
  top: 1.15rem;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--line-strong);
}
.timeline li.is-down::before { border-color: var(--down); }
.timeline li.is-up::before { border-color: var(--up); }
.timeline li.is-note::before { border-color: var(--accent); }
.timeline .when { color: var(--text-3); font-size: var(--text-sm); }
.timeline .what { color: var(--text); font-weight: var(--fw-label); }
.timeline .detail { color: var(--text-2); margin-top: var(--space-1); overflow-wrap: anywhere; }

/* ==================================================================== *
 * Empty states
 *
 * "No incidents. That is the good outcome." deserves better than grey text on
 * the margin — and an empty state that only says "nothing here" has wasted the
 * one moment the operator was going to read an explanation.
 * ==================================================================== */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-8) var(--space-5);
  border: 1px dashed var(--edge);
  border-radius: var(--radius-lg);
  text-align: center;
  margin-bottom: var(--stack);
  background: var(--surface);
}
.empty .ico-xl { color: var(--text-3); opacity: 0.7; }
.empty .empty-title { font-size: var(--text-md); font-weight: var(--fw-title); color: var(--text); }
.empty .empty-body { color: var(--text-2); max-width: 48ch; margin: 0; }
.empty .empty-actions { margin-top: var(--space-2); display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; }
.empty.is-good .ico-xl { color: var(--up); opacity: 1; }

/* ==================================================================== *
 * Loading
 *
 * A skeleton in the shape of what is coming, not a spinner in the middle of
 * the page. The previous screen's numbers were the last thing that was true,
 * so a view that is refreshing dims instead of blanking; a view that has
 * nothing yet draws the shape it is about to fill.
 * ==================================================================== */
main[aria-busy='true'] .refreshable { opacity: 0.55; transition: opacity var(--t-fast) var(--ease); }

.skeleton {
  border-radius: var(--radius-sm);
  background:
    linear-gradient(90deg, transparent 25%, var(--surface-hover) 37%, transparent 63%)
    var(--sunken);
  background-size: 300% 100%;
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer { to { background-position: -300% 0; } }
.sk-line { height: 0.75rem; margin-bottom: var(--space-2); }
.sk-row { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--line); }
.sk-row:last-child { border-bottom: none; }
.sk-block { border-radius: var(--radius-lg); }

/* ==================================================================== *
 * The command palette
 *
 * Twenty routes and two hundred monitors is past the point where a list of
 * links is the fastest way anywhere. `<dialog>` rather than a positioned div:
 * it gets the top layer, the backdrop, the focus trap and Escape from the
 * platform rather than from four hundred lines of this file.
 * ==================================================================== */
#palette {
  padding: 0;
  width: min(38rem, calc(100vw - var(--space-6) * 2));
  max-width: none;
  border-radius: var(--radius-xl);
  overflow: hidden;
  margin-top: 12vh;
  margin-bottom: auto;
}
#palette::backdrop { background: rgb(0 0 0 / 0.5); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
#palette[open] { animation: palette-in var(--t-slow) var(--ease-out); }
@keyframes palette-in {
  from { opacity: 0; translate: 0 -0.75rem; scale: 0.98; }
  to { opacity: 1; translate: 0; scale: 1; }
}

.palette-field { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line); }
.palette-field .ico { color: var(--text-3); flex: none; }
.palette-field input {
  border: 0;
  background: none;
  padding: 0;
  min-height: 1.75rem;
  font-size: var(--text-lg);
}
.palette-field input:focus { box-shadow: none; background: none; }

.palette-list { list-style: none; margin: 0; padding: var(--space-2); max-height: min(24rem, 50vh); overflow-y: auto; }
.palette-group {
  font-size: var(--text-xs);
  font-weight: var(--fw-title);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-3);
  padding: var(--space-3) var(--space-3) var(--space-1);
}
.palette-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  min-height: var(--hit-lg);
  border-radius: var(--radius);
  color: var(--text-2);
  text-decoration: none;
  cursor: pointer;
}
.palette-item .ico { color: var(--text-3); flex: none; }
.palette-item .pi-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.palette-item .pi-where { margin-inline-start: auto; color: var(--text-3); font-size: var(--text-sm); flex: none; }
/* Selection follows the keyboard, not the pointer: the highlighted row is the
   one Enter will open, and a stray mouse position must not change that. */
.palette-item[data-active='true'] { background: var(--accent-wash); }
.palette-item[data-active='true'] .pi-name { color: var(--text); }
.palette-item[data-active='true'] .ico { color: var(--accent); }
.palette-foot {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--line);
  background: var(--sunken);
  color: var(--text-3);
  font-size: var(--text-sm);
  flex-wrap: wrap;
}
.palette-foot span { display: inline-flex; align-items: center; gap: var(--space-2); }
.palette-empty { padding: var(--space-6); text-align: center; color: var(--text-2); }

/* ==================================================================== *
 * Latency chart
 *
 * Coloured by class, not by an attribute written from JavaScript. An earlier
 * version wrote #58a6ff and #2a2f3d straight onto the SVG nodes, so the chart
 * kept its dark-mode palette on a light background and the axis disappeared.
 * ==================================================================== */

/* The positioning context for the readout, which is HTML rather than SVG
   because a <text> that has to wrap, follow the type scale and sit on the page
   background is three things SVG makes hard and one <div> makes free. */
.chart-wrap { position: relative; }
/*
 * `direction: ltr`, in every language.
 *
 * The chart is not mirrored in RTL and must not be: time running right to left
 * is not a convention anywhere, and neither is a latency axis. Without this the
 * page's own `dir="rtl"` reaches the <text> nodes, `text-anchor: end` resolves
 * to the *left* edge, and the whole scale lands inside the plot it labels.
 * Persian digits still draw as Persian digits — that is the numeral system, not
 * the direction.
 */
.chart { display: block; width: 100%; overflow: visible; touch-action: pan-y; direction: ltr; }
.chart-empty { margin: 0; padding: var(--space-6) 0; text-align: center; }
/*
 * Five rules: the baseline solid, the four above it dashed, so they read as a
 * scale rather than as five equal lines competing with the series.
 */
/* --edge. At --line the scale measured 1.16:1, i.e. was not drawn, so a
   latency spike had nothing to be tall relative to. */
.chart .grid-line { stroke: var(--edge); stroke-width: 1; }
.chart .grid-line:not(:first-of-type) { stroke-dasharray: 3 4; }
/* 11px, the same floor every other label on these screens sits on. It was
   10px, which is small enough that the one number naming the chart's scale
   was the least legible text in the product. */
.chart .axis-label { fill: var(--text-3); font-size: 11px; stroke: none; font-variant-numeric: tabular-nums; }
/* The scale sits outside the plot and reads inward; the clock sits under it and
   is centred on its own tick. Both are physical: the chart is not mirrored in
   RTL, because time running right to left is not a convention anywhere. */
.chart .axis-label.y { text-anchor: end; }
.chart .axis-label.x { text-anchor: middle; }
.chart .tick { stroke: var(--edge); stroke-width: 1; }
.chart .area { fill: url(#chart-fade); stroke: none; }
.chart .series {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  /*
   * The line draws itself once on arrival. The dash length is set from the
   * measured path in app.js, because a fixed value either falls short on a
   * wide chart or overshoots on a narrow one.
   */
  animation: draw 800ms var(--ease-out) backwards;
}
/*
 * Per-probe strokes. Eight hues that stay distinct on both palettes and avoid
 * the purple/glow defaults; beyond eight the index wraps so a twenty-probe
 * fleet still reads as separate lines rather than one accent soup.
 */
.chart .series-probe,
.chart .point.series-probe,
.chart .marker.series-probe,
.chart-legend .key-line.series-probe,
.chart-tip .key-line.series-probe {
  stroke: var(--chart-series);
  background: var(--chart-series);
  fill: var(--chart-series);
}
.chart .series-probe[data-i='0'], .chart .point.series-probe[data-i='0'], .chart .marker.series-probe[data-i='0'],
.chart-legend .key-line.series-probe[data-i='0'], .chart-tip .key-line.series-probe[data-i='0'] {
  --chart-series: var(--accent);
}
.chart .series-probe[data-i='1'], .chart .point.series-probe[data-i='1'], .chart .marker.series-probe[data-i='1'],
.chart-legend .key-line.series-probe[data-i='1'], .chart-tip .key-line.series-probe[data-i='1'] {
  --chart-series: #0d9488;
}
.chart .series-probe[data-i='2'], .chart .point.series-probe[data-i='2'], .chart .marker.series-probe[data-i='2'],
.chart-legend .key-line.series-probe[data-i='2'], .chart-tip .key-line.series-probe[data-i='2'] {
  --chart-series: #ca8a04;
}
.chart .series-probe[data-i='3'], .chart .point.series-probe[data-i='3'], .chart .marker.series-probe[data-i='3'],
.chart-legend .key-line.series-probe[data-i='3'], .chart-tip .key-line.series-probe[data-i='3'] {
  --chart-series: #dc2626;
}
.chart .series-probe[data-i='4'], .chart .point.series-probe[data-i='4'], .chart .marker.series-probe[data-i='4'],
.chart-legend .key-line.series-probe[data-i='4'], .chart-tip .key-line.series-probe[data-i='4'] {
  --chart-series: #2563eb;
}
.chart .series-probe[data-i='5'], .chart .point.series-probe[data-i='5'], .chart .marker.series-probe[data-i='5'],
.chart-legend .key-line.series-probe[data-i='5'], .chart-tip .key-line.series-probe[data-i='5'] {
  --chart-series: #16a34a;
}
.chart .series-probe[data-i='6'], .chart .point.series-probe[data-i='6'], .chart .marker.series-probe[data-i='6'],
.chart-legend .key-line.series-probe[data-i='6'], .chart-tip .key-line.series-probe[data-i='6'] {
  --chart-series: #ea580c;
}
.chart .series-probe[data-i='7'], .chart .point.series-probe[data-i='7'], .chart .marker.series-probe[data-i='7'],
.chart-legend .key-line.series-probe[data-i='7'], .chart-tip .key-line.series-probe[data-i='7'] {
  --chart-series: #0891b2;
}
.chart .series-probe { stroke: var(--chart-series); fill: none; }
.chart .point.series-probe { fill: var(--chart-series); stroke: none; }
.chart .marker.series-probe { fill: var(--chart-series); }
/* A single measured bucket between two gaps. It is not a line and cannot be
   drawn as one, and dropping it would lose the only reading there is. */
.chart .point { fill: var(--accent); stroke: none; }
@keyframes draw {
  from { stroke-dashoffset: var(--path-length, 2000); }
  to { stroke-dashoffset: 0; }
}
/*
 * The chart's categorical encoding. Four states, and the fourth is the one the
 * chart could not previously say at all:
 *
 *   down     — checks ran and failed
 *   degraded — checks ran and passed, badly
 *   unknown  — checks ran and could not tell
 *   gap      — nothing reported, which is a different claim from all three
 *
 * `gap` is hatched rather than tinted, because it is the absence of data and
 * every tint on this page means data of some colour. It also has to survive
 * beside `unknown`, which is the neighbouring grey.
 */
.chart .band-down { fill: var(--down); fill-opacity: 0.18; }
.chart .band-degraded { fill: var(--degraded); fill-opacity: 0.15; }
.chart .band-unknown { fill: var(--unknown); fill-opacity: 0.10; }
.chart .band-gap { fill: url(#chart-gap); }
.chart .gap-hatch { stroke: var(--text-3); stroke-width: 1.5; opacity: 0.3; }

/* The readout's furniture. Transparent until a pointer is over the plot. */
.chart .hit { fill: transparent; cursor: crosshair; }
.chart .crosshair { stroke: var(--text-3); stroke-width: 1; stroke-dasharray: 2 3; pointer-events: none; }
.chart .marker { fill: var(--accent); stroke: var(--surface); stroke-width: 2; pointer-events: none; }

/*
 * The readout itself. Above the pointer and off to one side of it, flipping
 * sides past the middle so it never runs off the edge the operator is reading.
 */
.chart-tip {
  position: absolute;
  top: var(--space-2);
  z-index: 2;
  pointer-events: none;
  min-width: 9rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.chart-tip[data-side='end'] { transform: translateX(var(--space-3)); }
.chart-tip[data-side='start'] { transform: translateX(-100%) translateX(calc(var(--space-3) * -1)); }
.chart-tip .tip-when { color: var(--text-3); font-variant-numeric: tabular-nums; }
.chart-tip .tip-value { color: var(--text); font-weight: var(--fw-title); font-variant-numeric: tabular-nums; display: flex; gap: var(--space-2); flex-wrap: wrap; }
.chart-tip .tip-peak { color: var(--text-3); font-weight: var(--fw-body); }
.chart-tip .tip-probe {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-variant-numeric: tabular-nums;
}
.chart-tip .tip-probe-name { color: var(--text-2); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chart-tip .tip-probe-ms { color: var(--text); font-weight: var(--fw-title); }
.chart-tip .tip-counts { display: flex; gap: var(--space-2); flex-wrap: wrap; color: var(--text-2); }
.chart-tip .tip-counts .is-up { color: var(--up); }
.chart-tip .tip-counts .is-down { color: var(--down); }
.chart-tip .tip-counts .is-degraded { color: var(--degraded); }
.chart-tip .tip-counts .is-unknown { color: var(--unknown); }

/*
 * The key. Only the bands actually on screen are listed — a legend naming
 * conditions that did not occur is four words of noise on a healthy monitor.
 */
.chart-legend {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: var(--space-2);
  color: var(--text-3);
  font-size: var(--text-sm);
}
.chart-legend span,
.chart-legend .legend-probe {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.chart-legend .legend-probe {
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 0;
  cursor: pointer;
  border-radius: var(--radius);
}
.chart-legend .legend-probe:hover { color: var(--text); }
.chart-legend .legend-probe.is-off {
  opacity: 0.45;
  text-decoration: line-through;
}
.chart-legend .legend-probe.is-off .key-line { opacity: 0.5; }
.chart-legend .key-line { width: 1rem; height: 2px; border-radius: 1px; background: var(--accent); }
.chart-legend .key-line.series-probe { background: var(--chart-series); }
.chart-legend .key-swatch { width: 0.75rem; height: 0.75rem; border-radius: 2px; }
.chart-legend .key-swatch.is-down { background: color-mix(in srgb, var(--down) 30%, transparent); }
.chart-legend .key-swatch.is-degraded { background: color-mix(in srgb, var(--degraded) 26%, transparent); }
.chart-legend .key-swatch.is-unknown { background: color-mix(in srgb, var(--unknown) 22%, transparent); }
.chart-legend .key-swatch.is-gap {
  background: repeating-linear-gradient(
    45deg,
    color-mix(in srgb, var(--text-3) 45%, transparent) 0 1.5px,
    transparent 1.5px 5px
  );
}

/* The sentence under a chart that says how it was aggregated. */
.panel-note { margin: var(--space-2) 0 0; color: var(--text-3); font-size: var(--text-sm); }

/*
 * The uptime strip. One cell per bucket, worst-verdict-wins, which is the
 * shape every status page uses because it is the one picture that answers
 * "has this been reliable" without a number.
 */
.uptime-strip { display: flex; gap: 2px; align-items: stretch; height: 2rem; }
/* --edge, not --line-strong: this sits directly beside the verdict hues, and at
   3.19:1 the "no data" cell would start competing with them. */
.uptime-cell {
  flex: 1 1 0;
  min-width: 2px;
  border-radius: 1.5px;
  background: var(--edge);
  border: 0;
  padding: 0;
  cursor: pointer;
}
.uptime-cell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  z-index: 1;
}
.uptime-cell[data-active] { box-shadow: inset 0 0 0 1px var(--text); }
.uptime-cell.is-up { background: var(--up); }
.uptime-cell.is-down { background: var(--down); }
.uptime-cell.is-degraded { background: var(--degraded); }
.uptime-cell.is-unknown { background: var(--unknown); opacity: 0.45; }
/* Nothing reported, which the strip previously could not say either: it was
   built from a fixed count of results, so an interval nobody measured simply
   was not one of the cells. */
.uptime-cell.is-none { background: var(--edge); opacity: 0.6; }
/* Unmeasured, not missing: the cell the clock is still inside, one cut by the
   edge of the window, or one newer than the rollup's refresh lag. Hatched
   rather than filled, and the same hatch the chart draws over the same spans,
   so the two pictures on this page agree about their right-hand edge. */
.uptime-cell.is-pending {
  background: repeating-linear-gradient(
    45deg,
    color-mix(in srgb, var(--text-3) 30%, transparent) 0 1.5px,
    transparent 1.5px 5px
  );
}
.uptime-strip-wrap { position: relative; }
.strip-tip {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  transform: translateX(-50%);
  z-index: 3;
  min-width: 12rem;
  max-width: 18rem;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  color: var(--text);
  font-size: var(--text-sm);
  pointer-events: none;
}
.strip-tip[data-side='start'] { transform: translateX(-90%); }
.strip-tip[data-side='end'] { transform: translateX(-10%); }
.strip-tip .tip-when { color: var(--text-3); margin-bottom: var(--space-1); }
.strip-tip .tip-counts { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); }
.strip-tip .tip-peak { color: var(--text-3); margin-inline-start: var(--space-2); }

/* The count beside a filter, and the paging control under a table. */
.result-count { color: var(--text-3); font-size: var(--text-sm); margin-inline-start: auto; }
.result-page { color: var(--text-3); font-size: var(--text-sm); }
.more-row { display: flex; justify-content: center; align-items: center; gap: var(--space-3); margin-top: var(--space-3); flex-wrap: wrap; }
.more-row:has(> :not([hidden])) { display: flex; }
.more-row:not(:has(> :not([hidden]))) { display: none; }
.results-pager .btn[hidden] { display: none; }
.monitor-range-bar {
  align-items: center;
}
.monitor-range-label {
  color: var(--text-2);
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
}
.results-dates {
  align-items: flex-end;
  gap: var(--space-3);
}
.date-range {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-2);
}
.date-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.date-field-label {
  font-size: var(--text-xs);
  font-weight: var(--fw-label);
  color: var(--text-3);
  letter-spacing: 0.02em;
}
.date-field-control {
  position: relative;
  display: flex;
  align-items: stretch;
  min-height: var(--hit);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: visible;
}
.date-field-control:hover { border-color: var(--text-3); }
.date-field-control:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
}
.date-field-input {
  flex: 1;
  min-width: 7.5rem;
  min-height: calc(var(--hit) - 2px);
  margin: 0;
  padding: 0 var(--space-3);
  border: 0 !important;
  border-radius: 0;
  background: transparent !important;
  box-shadow: none !important;
  color: var(--text);
  font: inherit;
  font-variant-numeric: tabular-nums;
}
.date-field-input::placeholder { color: var(--text-3); }
.date-field-toggle,
.date-field-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--hit);
  min-height: calc(var(--hit) - 2px);
  margin: 0;
  padding: 0;
  border: 0;
  border-inline-start: 1px solid var(--edge);
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
}
.date-field-toggle:hover,
.date-field-clear:hover { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }
.date-field-toggle:focus-visible,
.date-field-clear:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.date-field-clear[hidden] { display: none; }
.date-field-toggle .ico,
.date-field-clear .ico { width: 1rem; height: 1rem; }

/* Floating month — `--raised`, hairline, no OS chrome. */
.date-pop {
  position: absolute;
  inset-inline-start: 0;
  top: calc(100% + 0.4rem);
  z-index: 8;
  width: 17.5rem;
  padding: var(--space-3);
  background: var(--raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}
.date-pop-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.date-pop-title {
  flex: 1;
  text-align: center;
  font-size: var(--text-sm);
  font-weight: var(--fw-title);
  color: var(--text);
}
.date-pop-nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--sunken);
  color: var(--text-2);
  cursor: pointer;
}
.date-pop-nav-btn:hover { color: var(--text); border-color: var(--text-3); }
.date-pop-nav-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.date-pop-flip { transform: scaleX(-1); }
.date-pop-dows,
.date-pop-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.date-pop-dows { margin-bottom: var(--space-1); }
.date-pop-dow {
  text-align: center;
  font-size: var(--text-xs);
  color: var(--text-3);
  padding: var(--space-1) 0;
}
.date-pop-blank { min-height: 2rem; }
.date-pop-day {
  min-height: 2rem;
  border: 0;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.date-pop-day:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
.date-pop-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.date-pop-day.is-selected {
  background: var(--accent);
  color: var(--accent-contrast, #fff);
  font-weight: var(--fw-title);
}
.date-pop-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--edge);
}
.date-pop-today {
  border: 0;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
}
.date-pop-today:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }

.uptime-scale {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--sunken) 70%, transparent);
  color: var(--text-2);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}
.uptime-bucket-hint {
  flex: 1;
  text-align: center;
  color: var(--text-3);
}
.results-advanced { margin-bottom: var(--stack); }
.results-advanced-body {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
  padding-top: var(--space-2);
}
.results-advanced-body label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-2);
}
.results-advanced-body input {
  min-height: var(--hit);
  min-width: 8rem;
  padding: 0 var(--space-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}
/* A row whose first cell carries an absolute time and a relative one. The
   absolute is what a log is searched by; the relative is what a human reads. */
td.nowrap { white-space: nowrap; }
td .row-ago { display: block; color: var(--text-3); font-size: var(--text-sm); }

/* ==================================================================== *
 * Responsive
 *
 * Structural, not fluid. Product UI is read at a consistent DPI, so nothing
 * here resizes type; what changes is what is on screen and how it is stacked.
 * ==================================================================== */

@media (max-width: 60rem) {
  main { padding: var(--space-4); }
  main[data-width='reading'] { --content-max: 100%; }
  .page-head { gap: var(--space-3); }
  .page-head h1 { font-size: var(--text-xl); }
  /* Was min-width: 0, which let it shrink to 37px -- narrower than it is tall.
     --hit is 2rem on a mouse and 2.75rem on a thumb, so one value serves both. */
  .search-trigger { min-width: var(--hit); }
  .search-trigger .search-label, .search-trigger kbd { display: none; }
  .avatar-button .who { display: none; }
  /* 22rem is most of a phone. The text still truncates rather than wrapping. */
  #live { max-width: 10rem; }
  .crumbs .crumb-parent, .crumbs .crumb-group, .crumbs .sep { display: none; }
}

@media (max-width: 40rem) {
  /*
   * The strip drew 96 cells with a 2px floor and a 2px gap, which needs
   * 382px before anything else on the row. A phone is 360-390px wide and the
   * panel it sits in is narrower still, so the cells hit their floor, stopped
   * shrinking, and pushed the **document** sideways -- the one thing the
   * responsive rules here are supposed to make impossible.
   *
   * Narrowing the gap rather than dropping cells, because every cell is a
   * real interval: showing fewer would be a different window than the one the
   * label above it names, and clipping or scrolling would cut the picture at
   * an edge nobody chose. At a 1px gap and a 1px floor the same 96 cells fit
   * inside 200px, which is narrower than any phone this will be read on.
   */
  .uptime-strip { gap: 1px; }
  .uptime-cell { min-width: 1px; border-radius: 1px; }

  main { padding: var(--space-3); }
  .field { max-width: none; }
  .card, .panel { padding: var(--space-4); border-radius: var(--radius); }
  th, td { padding: var(--space-3); }
  td:first-child, th:first-child { padding-inline-start: var(--space-3); }
  td:last-child, th:last-child { padding-inline-end: var(--space-3); }
  .verdict { flex-direction: column; align-items: flex-start; gap: var(--space-3); padding: var(--space-4); }
  .health-legend { gap: var(--space-2) var(--space-4); }
  .kv { grid-template-columns: minmax(0, 1fr); }
  .kv > dt { border-bottom: none; padding-bottom: var(--space-1); }
  .kv > dd { padding-top: 0; }
  #palette { margin-top: 0; width: 100vw; max-height: 100dvh; border-radius: 0; }

  /* ---------------------------------------------------------------- *
   * Tables stop being tables
   *
   * .table-scroll keeps a wide table from dragging the document sideways,
   * and on a desktop that is the whole fix. On a phone it is not: the probe
   * list is 1177px of table inside a 390px viewport, so the operator saw two
   * columns of eight and read a row by dragging it past a window narrower
   * than the row. The horizontal scroll was never the answer here, it was
   * the containment.
   *
   * So below this breakpoint each row becomes a card and each cell a
   * label/value line. The column name comes from data-label, which
   * labelCells() in app.js copies onto every cell from its <th>; the header
   * row is then saying it twice, so it goes -- visually. It stays in the
   * accessibility tree, and the roles labelCells() states keep this a table
   * for a screen reader even though none of it is laid out as one any more.
   *
   * Scoped to .table-scroll, so the probe evidence table -- which is a fixed
   * two-column dump that reads fine narrow, and is deliberately not wrapped
   * -- keeps the behaviour it has.
   * ---------------------------------------------------------------- */
  .table-scroll { overflow-x: visible; border: 0; background: none; }
  .table-scroll table { display: block; width: 100%; border: 0; background: none; }
  .table-scroll thead {
    /* Not display:none: that takes the column names out of the accessibility
       tree as well, and the cells below reference them by name. */
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .table-scroll tbody { display: flex; flex-direction: column; gap: var(--space-3); }
  .table-scroll tbody > tr {
    display: block;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    /*
     * Vertical only. As a flex item with horizontal padding this row measured
     * two pixels wider than the viewport and put the sideways scroll back on
     * the document -- the exact fault the wrapper exists to prevent. The
     * cells carry the inset instead, which looks the same and measures right.
     */
    padding: var(--space-1) 0;
  }
  .table-scroll tbody > tr:hover { background: var(--surface); }
  .table-scroll tbody > tr > td,
  .table-scroll tbody > tr > th {
    display: grid;
    grid-template-columns: minmax(5.5rem, 38%) minmax(0, 1fr);
    align-items: baseline;
    gap: var(--space-3);
    width: auto;
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-bottom: 1px solid var(--line);
    text-align: start;
    white-space: normal;
  }
  .table-scroll tbody > tr > :last-child { border-bottom: 0; }
  .table-scroll tbody > tr > [data-label]::before {
    content: attr(data-label);
    color: var(--text-3);
    font-size: var(--text-xs);
    font-weight: var(--fw-label);
    text-transform: none;
    letter-spacing: 0;
  }
  /* A cell that spans the row -- an empty state, a note -- answers no column,
     so it gets the full width rather than an empty label gutter beside it. */
  .table-scroll tbody > tr > :not([data-label]) { display: block; }

  /*
   * The row link was the cell's whole tap target when the row was a row. Now
   * the row is a card, the name line is its title.
   */
  .table-scroll tbody > tr > [data-label] .row-link { font-weight: var(--fw-title); }

  /*
   * Nothing inside a stacked cell may reintroduce the sideways drag.
   *
   * .tag is nowrap because a tag is normally one word. The releases table
   * puts a whole sentence in one -- "no ADVERTISE_URL - readiness inferred" --
   * and 225px of unbreakable text in a 199px column was enough to push the
   * document two pixels sideways again. Inside a stacked cell a tag wraps.
   */
  .table-scroll tbody > tr .tag { white-space: normal; }
  .table-scroll tbody > tr code,
  .table-scroll tbody > tr .tag,
  .table-scroll tbody > tr .mono { overflow-wrap: anywhere; }
}

/* ==================================================================== *
 * Reduced motion
 *
 * Every animation above is either decoration (the shimmer, the chart draw) or
 * a one-shot arrival cue that has a text equivalent — the banner says what
 * happened, the live button says what changed. So all of it is removed
 * outright rather than shortened, and nothing is lost by removing it. The
 * shimmer becomes a flat fill rather than a still gradient, which would read
 * as a rendering fault.
 * ==================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .btn:active:not(:disabled), button:active:not(:disabled) { translate: none; }
  .skeleton { background: var(--surface-hover); animation: none; }
  button[data-busy='true']::before { animation: none; border-top-color: currentColor; opacity: 0.5; }
}

/* ==================================================================== *
 * Right to left
 *
 * The layout itself needs almost nothing here: every rule above uses logical
 * properties, so `dir="rtl"` on <html> mirrors the page — the rail moves to
 * the right edge, the drawer slides in from the right, the chevrons already
 * flipped above. What is left is the content that must NOT mirror.
 *
 * A YAML document, a JSON payload, a URL, a token, a stack trace and a cron
 * expression are left-to-right regardless of the language around them.
 * Rendered in an RTL context the browser reorders their punctuation — a
 * leading `-` of a YAML list jumps to the end of the line, `https://host/path`
 * renders with the scheme on the right — and the result is not the text the
 * operator has to copy, paste and compare. So the machine-readable panes are
 * pinned.
 * ==================================================================== */
[dir='rtl'] pre,
[dir='rtl'] code,
[dir='rtl'] textarea,
[dir='rtl'] .kv-mono,
[dir='rtl'] .evidence td,
[dir='rtl'] .ltr {
  direction: ltr;
  text-align: left;
  /*
   * `isolate`, or `direction` does nothing at all on the inline ones.
   *
   * With the initial `unicode-bidi: normal` an inline box opens no embedding
   * level, and the spec is explicit that `direction` then has no effect — so
   * this rule worked on `pre` and `textarea`, which are blocks, and was inert
   * on every inline `<code>` in the product. A `${param:name}` in the middle of
   * a Persian sentence was still laid out by the sentence's direction and came
   * out as `}param:name{$`, which is the one thing a reader is copying
   * character for character.
   *
   * `isolate` also stops the run from disturbing the text on either side of it,
   * which `embed` does not: without it a URL between two Persian phrases drags
   * the punctuation of its neighbours around with it.
   */
  unicode-bidi: isolate;
}

/*
 * A number beside a unit is one run of text: `40ms`, `99.95%`, `p95`. Left to
 * the bidirectional algorithm inside an RTL line these split around their
 * punctuation and read back wrong. `dir="auto"` on the cell is not enough —
 * the digits are neutral-leading — so the numeric columns are declared.
 */
[dir='rtl'] .num,
[dir='rtl'] td.num {
  direction: ltr;
  text-align: right;
}

/*
 * An email address, a version string and a git sha are Latin identifiers in
 * every language, and each of them ends up beside Persian text somewhere on
 * these screens.
 */
[dir='rtl'] .id { direction: ltr; text-align: right; unicode-bidi: isolate; }

/* Two glyphs that carry direction and must point the way the reader reads. */
[dir='rtl'] .flip { scale: -1 1; }

/*
 * The template gallery.
 *
 * A grid of cards rather than the tables the rest of the console uses, because
 * the unit here is a thing somebody is choosing between rather than a row they
 * are scanning for one value. Auto-fill with a sensible minimum, so the same
 * markup is one column on a phone and four on a desk without a breakpoint
 * anywhere.
 */
.template-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: var(--space-3);
}

.template-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  text-decoration: none;
  color: inherit;
}

.template-card:hover,
.template-card:focus-visible {
  border-color: var(--accent);
}

.template-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  justify-content: space-between;
}

.template-card p { margin: 0; }

/* The checker types, as pills. Wraps rather than scrolls: a card is a summary,
 * and a summary that needs a scrollbar has stopped being one. */
.template-types {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* ==================================================================== *
 * What the checker measured
 *
 * Every check writes a small map of numbers — the status code it got, the days
 * left on the certificate, the bytes it read — and they were stored and never
 * shown. They live in their own table column rather than folded into "Detail",
 * because Detail is the error text and is empty on a healthy check, which is
 * exactly the check whose measurements somebody came to read.
 *
 * The label is beside the value rather than in the header: a result row has a
 * different set of measurements per checker, and per row when a redirect adds
 * a hop, so there is no fixed column set to head.
 * ==================================================================== */
.metrics-cell {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  max-width: 28rem;
}

.metric-pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  padding: 0.0625rem var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-hover);
  font-size: var(--text-xs);
  line-height: 1.6;
  white-space: nowrap;
}

/*
 * A measurement that is itself the news.
 *
 * Borrowed from the status pills rather than given its own palette, so a
 * certificate about to expire is the same amber as a degraded check and reads
 * as the same severity. The words are still in the pill, so nothing here is
 * carried by colour alone.
 */
.metric-pill.tone-degraded {
  border-color: var(--degraded-line);
  background: var(--degraded-wash);
}
.metric-pill.tone-degraded .metric-pill-value { color: var(--degraded); }

.metric-pill.tone-down {
  border-color: var(--down-line);
  background: var(--down-wash);
}
.metric-pill.tone-down .metric-pill-value { color: var(--down); }

.metric-pill-label {
  color: var(--text-2);
}

.metric-pill-value {
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-label);
  color: var(--text-1);
}

/*
 * The required marker.
 *
 * Its own element, not " *" on the end of the label, so that the bidirectional
 * algorithm never gets to place it: a trailing asterisk is a neutral character,
 * and at the end of a Latin label inside a Persian page it lands on the far
 * left and the field reads "* Url". As a flex item it sits after the label on
 * both sides, which is what "after" means in either direction.
 */
label > .req {
  color: var(--down);
  margin-inline-start: 0.125rem;
  font-weight: var(--fw-label);
}

/*
 * Prose that arrives from the hub rather than from the catalogue.
 *
 * Checker labels, checker help, template names and template descriptions are
 * written in English in the source and are shown untranslated until somebody
 * writes the Persian. Each of them therefore has to carry its own direction:
 * an English sentence laid out as part of a right-to-left paragraph keeps its
 * words in order but moves its full stop to the left-hand end, so "for the
 * common shape." renders as ".for the common shape". `dir="auto"` in the
 * markup resolves the direction from the first strong character in the string,
 * which is the one thing that is right for both languages; `isolate` stops the
 * result from leaking into the text around it.
 */
[dir='rtl'] [dir='auto'] {
  /*
   * `plaintext` rather than `isolate`, and the direction put back.
   *
   * `dir="auto"` on its own does two things, and only one of them is wanted
   * here. It resolves the *ordering* from the content, which is the fix — the
   * full stop stays at the end of the English sentence. But it also sets the
   * element's `direction` to ltr, and `text-align: start` is resolved against
   * that, so every English label jumped to the left edge while the Persian
   * labels beside it stayed on the right and the form came out ragged.
   *
   * `unicode-bidi: plaintext` keeps the content-derived ordering while
   * `direction: rtl` puts the alignment back on the page's own axis. The label
   * then sits where every other label sits, and reads correctly inside itself.
   */
  unicode-bidi: plaintext;
  direction: rtl;
  text-align: right;
}

/*
 * `text-align: right` above is deliberate, and `start` is wrong here.
 *
 * Under `unicode-bidi: plaintext` the `start` and `end` keywords resolve
 * against the *content-derived* paragraph direction rather than the `direction`
 * property — that is what the property is for. So `start` on an English label
 * means its left, which is the ragged form this rule exists to prevent, while
 * the physical keyword pins every label to the same edge whichever language it
 * happens to be in. It is scoped to `[dir='rtl']`, so the left-to-right page is
 * untouched.
 */

/* ==================================================================== *
 * Signing in
 *
 * The one screen where the reader may not yet know what they have opened, and
 * the one screen with nothing else on it — so it is worth more than a card with
 * two boxes in it. What it has to carry: the mark, the two proofs this hub
 * accepts, and the way back in for somebody who has lost one of them.
 *
 * The two proofs are tabs rather than two routes. A person whose password has
 * just been refused is exactly the person who needs the other option in front
 * of them, not one navigation away.
 * ==================================================================== */
.auth-screen {
  display: grid;
  place-items: center;
  min-height: min(100dvh, 60rem);
  padding: var(--space-6) 0;
}

.auth-card {
  width: 100%;
  padding: var(--space-6);
  position: relative;
  overflow: hidden;
}

/*
 * A hairline of accent along the top edge, fading out.
 *
 * Decoration, and therefore `aria-hidden` by construction — it is a pseudo
 * element, so it is not in the tree at all — and it is the only ornament on the
 * screen. It reads as "this is a product, not a login prompt" without asking
 * the reader to look at anything.
 */
.auth-card::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 2px;
  background: linear-gradient(to right, transparent, var(--accent), transparent);
}

.auth-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

/* Larger here than in the rail: this is the only place it introduces itself. */
.auth-mark {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-lg, var(--radius));
}
.auth-mark .ico { width: 1.375rem; height: 1.375rem; }

/*
 * The mark breathes, once every few seconds.
 *
 * The icon is a heartbeat trace and this is a monitoring product, so the motion
 * says what the screen is for. Slow and small on purpose — it is scenery, not a
 * notification — and it is opacity and transform only, so it stays off the main
 * thread. Switched off entirely under `prefers-reduced-motion` below.
 */
@keyframes auth-pulse {
  0%, 88%, 100% { transform: scale(1); }
  92% { transform: scale(1.06); }
}
.auth-mark { animation: auth-pulse 4.5s ease-in-out infinite; }

.auth-title {
  font-size: var(--text-lg, 1.1875rem);
  font-weight: 640;
  letter-spacing: -0.015em;
  margin: 0;
}
.auth-lede { margin: 0.125rem 0 0; font-size: var(--text-sm); }

/* The two proofs. A tablist, so the arrow keys and the reader both work. */
.auth-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-1);
  padding: var(--space-1);
  margin-bottom: var(--space-4);
  background: var(--surface-hover);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.auth-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 44px including the padding: the minimum a thumb can hit reliably.
     2.75rem, because box-sizing is border-box here and 2.5rem measured 40. */
  min-height: 2.75rem;
  padding: var(--space-2) var(--space-3);
  border: 1px solid transparent;
  border-radius: calc(var(--radius) - 2px);
  background: transparent;
  color: var(--text-2);
  font-size: var(--text-sm);
  font-weight: var(--fw-label);
  cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.auth-tab .ico { width: 1rem; height: 1rem; }
.auth-tab:hover { color: var(--text-1); }

.auth-tab[aria-selected='true'] {
  background: var(--surface);
  border-color: var(--line);
  color: var(--text-1);
  box-shadow: var(--shadow-1);
}

/* The panel that is showing. The fade is what makes the swap read as one
 * control changing rather than as the page reloading under the cursor. */
.auth-panel { animation: auth-fade 160ms ease both; }
@keyframes auth-fade {
  from { opacity: 0; transform: translateY(-2px); }
  to { opacity: 1; transform: none; }
}

.auth-submit {
  width: 100%;
  min-height: 2.75rem;
  margin-top: var(--space-4);
  justify-content: center;
}

/*
 * The rejection, in the colour of a rejection.
 *
 * It used to be `.muted`, which is the colour this screen uses for asides —
 * so "email or password is incorrect" was typeset as a footnote. `role=alert`
 * on the element means a reader hears it when it changes rather than only if
 * they happen to arrow past.
 */
.auth-problem {
  margin: var(--space-3) 0 0;
  min-height: 1.25rem;
  font-size: var(--text-sm);
  color: var(--down);
}
.auth-problem:empty { display: none; }

.auth-note {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
}

.auth-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-4);
  font-size: var(--text-sm);
}

/*
 * A button that reads as a link.
 *
 * "I've forgotten my password" changes what this screen is showing rather than
 * going anywhere, so it is a <button> — an <a href="#"> that does not navigate
 * is a link that lies to everything that reads links. This makes it look like
 * its neighbour, which does navigate.
 */
.linkish {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.linkish:hover { color: var(--accent-strong, var(--accent)); }

.auth-foot {
  margin: var(--space-4) 0 0;
  font-size: var(--text-xs);
  line-height: 1.6;
}

@media (prefers-reduced-motion: reduce) {
  .auth-mark { animation: none; }
  .auth-panel { animation: none; }
}

/*
 * Where one rollout target's update is (docs/releases.md, "Watching an update
 * happen"): its stage, one bar for the whole update, who said so, and — when it
 * failed — the reason and the last lines it logged behind a disclosure.
 *
 * A native <progress>, not a styled div with an inline width: it is announced
 * as a progress bar without any ARIA, and it needs no style attribute, which a
 * strict CSP would refuse. Accent while moving, the down colour once it failed,
 * so a stopped bar cannot be read as a slow one.
 */
.update-progress { display: grid; gap: var(--space-1); min-inline-size: 14rem; }
.update-stage { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.update-bar { display: flex; align-items: center; gap: var(--space-2); }
.update-bar progress {
  flex: 1;
  block-size: 0.5rem;
  appearance: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--sunken);
  overflow: hidden;
}
.update-bar progress::-webkit-progress-bar { background: var(--sunken); }
.update-bar progress::-webkit-progress-value { background: var(--accent); }
.update-bar progress::-moz-progress-bar { background: var(--accent); }
.update-bar.is-failed progress::-webkit-progress-value { background: var(--down); }
.update-bar.is-failed progress::-moz-progress-bar { background: var(--down); }
.update-meta { font-size: var(--text-sm); }
details.update-failure { margin-bottom: 0; }
details.update-failure > p { margin: var(--space-2) 0; }
.update-log {
  max-block-size: 18rem;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: var(--text-sm);
}
