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

@media (prefers-color-scheme: light) {
  :root {
    /*
     * 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; }

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

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

/*
 * `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) {
  :root { --hit: 2.5rem; --hit-lg: 2.75rem; }
  .nav-link { min-height: 2.75rem; }
}

/* ==================================================================== *
 * 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; }
.field.inline input { 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); }
label.inline input { display: inline-block; width: auto; 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; }

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

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

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

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

/* ==================================================================== *
 * 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.
 * ==================================================================== */
.chart { display: block; width: 100%; overflow: visible; }
/*
 * The baseline is a full rule; the two above it are dashed, so they read as a
 * scale rather than as three 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; }
.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;
}
@keyframes draw {
  from { stroke-dashoffset: var(--path-length, 2000); }
  to { stroke-dashoffset: 0; }
}
/*
 * The chart's only categorical encoding — "this window failed" against "this
 * window had no usable result" — rendered as two grey smudges 1.02:1 apart.
 * Separated by hue AND amplitude now, off the status tokens so both still
 * follow the palette into light mode.
 */
.chart .band-down { fill: var(--down); fill-opacity: 0.18; }
.chart .band-unknown { fill: var(--unknown); fill-opacity: 0.10; }

/*
 * 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); }
.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; }
.uptime-scale {
  display: flex;
  justify-content: space-between;
  color: var(--text-3);
  font-size: var(--text-sm);
  margin-top: var(--space-2);
}

/* ==================================================================== *
 * 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); }
  .search-trigger { min-width: 0; }
  .search-trigger .search-label, .search-trigger kbd { display: none; }
  .avatar-button .who { display: none; }
  .crumbs .crumb-parent, .crumbs .crumb-group, .crumbs .sep { display: none; }
}

@media (max-width: 40rem) {
  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; }
}

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

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